Skip to content

[feature] Add extraction metadata fields, state machine, and async extraction task#437

Open
atif09 wants to merge 5 commits into
gsoc26-metadata-extractionfrom
issues/412-model-fields-state-machine
Open

[feature] Add extraction metadata fields, state machine, and async extraction task#437
atif09 wants to merge 5 commits into
gsoc26-metadata-extractionfrom
issues/412-model-fields-state-machine

Conversation

@atif09

@atif09 atif09 commented May 22, 2026

Copy link
Copy Markdown
Member
  • Add extraction metadata fields to FirmwareImage (board, compatible, target, fw_version, compat_version, source, extraction_status, extraction_log, failure_reason)
  • Add state machine with statuses: unconfirmed, in_progress, success, failed, invalid, manually_confirmed
  • Add unique_together on (build, type) to prevent duplicate image uploads
  • Add _validate_locked() to make metadata fields read-only after confirmation, allowing empty fields to be filled
  • Add trigger_metadata_extraction post_save signal to schedule Celery task and set build status to analyzing
  • Add auto_create_device_firmware signal to trigger device pairing on confirmed status
  • Ad extract_firmware_metadata Celery task with handling for different exception cases
  • Add _compat_blocks_pairing() to skip device pairing when compat_version > 1.0

Related to #412 & #413

Checklist

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

Reference to Existing Issue

Closes #412 & #413

Description of Changes

Adds extraction metadata fields and a state machine to FirmwareImage to track async metadata extraction. A Celery task extracts firmware metadata (board, target, compatible, version, source) after upload, with a locked state preventing edits after confirmation. Prevents duplicate image uploads via a unique_together constraint and skips device firmware pairing when compat_version exceeds 1.0

NOTE: These changes focus on the async Celery task, state machine wiring, fwtool/DTB metadata extraction, and admin notifications. UI improvements (inline display of extraction status and metadata fields in the build admin etc) will be covered in #414 after discussing more with the mentors.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces end-to-end firmware metadata extraction tracking. AbstractFirmwareImage gains extraction status, failure reason, extraction log, and extracted metadata fields (board, compatible, target, fw_version, compat_version, source). AbstractBuild gains a status field aggregating image extraction states via _update_extraction_status(), which triggers a notification on transition. A new extract_firmware_metadata Celery task handles async extraction with per-exception failure mapping. FirmwareImageAdmin and BuildAdmin are extended with badge displays, dynamic readonly fields, and save behavior. Three chained migrations, signal wiring, and comprehensive test coverage complete the implementation.

Sequence Diagram

sequenceDiagram
    participant Admin
    participant FirmwareImageAdmin
    participant extract_firmware_metadata
    participant OpenWrtMetadataExtractor
    participant Build
    participant notify

    Admin->>FirmwareImageAdmin: Upload/save FirmwareImage
    FirmwareImageAdmin->>FirmwareImageAdmin: save_model() — reset extraction fields
    FirmwareImageAdmin->>extract_firmware_metadata: transaction.on_commit → .delay(image_pk)
    extract_firmware_metadata->>FirmwareImage: set STATUS_IN_PROGRESS
    extract_firmware_metadata->>OpenWrtMetadataExtractor: .extract(image_file)
    alt Extraction succeeds
        OpenWrtMetadataExtractor-->>extract_firmware_metadata: metadata (board, target, fw_version…)
        extract_firmware_metadata->>FirmwareImage: update fields, STATUS_SUCCESS
        extract_firmware_metadata->>Build: _update_extraction_status()
        Build-->>notify: _notify_extraction_complete() if status changes
    else Extraction fails
        OpenWrtMetadataExtractor-->>extract_firmware_metadata: UnsupportedImageError / DecompressionLimitExceeded / Timeout
        extract_firmware_metadata->>FirmwareImage: STATUS_FAILED + failure_reason + extraction_log
        extract_firmware_metadata->>notify: send error notification with admin URL
        extract_firmware_metadata->>Build: _update_extraction_status()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #412: The PR directly implements the model layer, status tracking, and build-level completion notification requested in issue #412, including extraction_status/failure_reason/extraction_log fields, Build.status aggregation via _update_extraction_status(), and _notify_extraction_complete() with admin URL linking.
  • #413: The PR also implements the asynchronous extraction pipeline from issue #413 via the extract_firmware_metadata Celery task, _compat_blocks_pairing(), signal wiring in apps.py, and exception-to-failure-reason mapping.

Possibly related PRs

  • openwisp/openwisp-firmware-upgrader#421: Both PRs modify AbstractFirmwareImage.clean() and model-level validation — this PR adds _validate_locked() into the clean flow while PR #421 adds _validate_file_header() and _validate_rootfs(), and both changes are invoked in the same clean() method sequence.

Suggested reviewers

  • asmodehn
  • nemesifier
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: addition of extraction metadata fields, a state machine, and async extraction task implementation.
Description check ✅ Passed The PR description covers key implementation details, includes a checklist (mostly completed), references related issues (#412 & #413), and explains the main changes and rationale.
Linked Issues check ✅ Passed The PR implements all coding requirements from #412: extraction state tracking on FirmwareImage with proper statuses, Build.status field with aggregate state logic, _update_extraction_status() method, and _notify_extraction_complete() for notifications.
Out of Scope Changes check ✅ Passed All changes are within scope: model fields, state machine, migrations, Celery task, signal handlers, admin UI integration, and comprehensive test coverage all directly support the extraction metadata workflow defined in #412 and #413.
Bug Fixes ✅ Passed This PR is a feature addition, not a bug fix. The custom check applies only to bug fixes. Issues #412 and #413 are feature requests, not bug reports.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issues/412-model-fields-state-machine

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.

@atif09 atif09 added the enhancement New feature or request label May 22, 2026
@atif09 atif09 self-assigned this May 22, 2026
@kilo-code-bot

kilo-code-bot Bot commented May 22, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental re-review of the changes since ef601884 (adds the re_extract_metadata admin action and two regression tests in test_admin.py).

The new action is sound:

  • Correctly follows the existing transaction.on_commit(partial(extract_firmware_metadata.delay, pk)) scheduling pattern (mirrors save_model at admin.py:277).
  • queryset.update() bypass of model save()/_validate_locked() is intentional for a reset action and satisfies the task's conditional UNCONFIRMED → IN_PROGRESS transition in tasks.py:128-131.
  • Tenant isolation preserved: FirmwareImageAdmin extends MultitenantAdminMixin, so the action queryset is tenant-filtered, and build_ids are derived from that filtered set.
  • Reset fields (compatible=[] on a JSONField, blank strings) serialize correctly; tests verify post-state.
  • permissions=["change"] gating is appropriate.
  • Regression tests cover single-image and multi-image paths and assert both the DB reset and the scheduled task calls via captureOnCommitCallbacks.

Previously reviewed findings on unchanged files are carried forward unchanged.

Files Reviewed (2 files, incremental)
  • openwisp_firmware_upgrader/admin.pyre_extract_metadata action
  • openwisp_firmware_upgrader/tests/test_admin.pytest_re_extract_metadata_action, test_re_extract_metadata_action_multiple
Previous Review Summaries (4 snapshots, latest commit ef60188)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit ef60188)

Status: No Issues Found | Recommendation: Merge

Full re-review performed after the branch history was force-pushed (the previous review commit d9174a71 no longer exists), so this pass re-evaluated the complete gh pr diff rather than just the incremental delta.

The previously flagged critical issues remain resolved in the current code:

  • openwisp_firmware_upgrader/base/models.py:508save() refreshes _original_extraction_status (typo fixed), preventing duplicate create_all_device_firmwares pairing tasks on re-save of confirmed images.
  • openwisp_firmware_upgrader/tasks.py:987-995 — the extraction-failure notification uses format_html(..., url=admin_url, ...) with matching placeholders and safely escaped interpolation.

Additional checks performed on the new/changed production code:

  • _update_extraction_status() atomic conditional-UPDATE transition (analyzing → terminal, with non-regressing fallback update) is correct and avoids lost updates.
  • _validate_locked() correctly keys off both the persisted DB status and the in-memory status, blocking lock-bypass via a transient status change while still allowing empty fields to be filled.
  • auto_create_device_firmwares() dedup guard (_original_extraction_status in confirmed) prevents re-pairing on re-save of already-confirmed images.
  • Single-trigger pairing design is preserved: the Celery task mutates rows via queryset.update() (which does not emit post_save), then explicitly schedules create_all_device_firmwares.delay(...), avoiding double scheduling with the post_save signal path.
  • Admin display helpers (_extraction_status_badge, build_status_display, failure_reason_display, extraction_log_display) use format_html / rely on admin auto-escaping, so interpolated metadata is not rendered as raw HTML.
  • Migration 0019 backfill marks pre-existing unconfirmed images/builds as manually_confirmed (with reverse_code=RunPython.noop), which is appropriate for an upgrade path with no in-flight extractions.
  • batch_upgrade() now rejects builds with any non-confirmed image, matching the new state machine semantics.

Regression/behavior tests are present and aligned with the changes (test_models.py, test_admin.py, test_tasks.py, test_api.py query-count adjustments).

Files Reviewed (15 files)
  • openwisp_firmware_upgrader/admin.py — FirmwareImageAdmin, save_model state transitions, status badges
  • openwisp_firmware_upgrader/apps.py — metadata signal wiring
  • openwisp_firmware_upgrader/base/models.py — Build/FirmwareImage status fields, state machine, locking, pairing dedup
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py — schema additions
  • openwisp_firmware_upgrader/migrations/0019_backfill_extraction_status.py — data backfill
  • openwisp_firmware_upgrader/migrations/0020_alter_firmwareimage_board_and_more.py — verbose-name AlterField
  • openwisp_firmware_upgrader/tasks.pyextract_firmware_metadata task, _compat_blocks_pairing, notifications
  • openwisp_firmware_upgrader/tests/base.py — default extraction_status fixture
  • openwisp_firmware_upgrader/tests/test_admin.py — save_model / readonly-field tests
  • openwisp_firmware_upgrader/tests/test_api.py — query-count adjustments
  • openwisp_firmware_upgrader/tests/test_extractors.py — extractor class wiring test
  • openwisp_firmware_upgrader/tests/test_models.py — state machine, locking, pairing tests
  • openwisp_firmware_upgrader/tests/test_tasks.py — extraction task coverage
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_*.py — sample app schema additions
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_*.py — sample app verbose-name AlterField

Previous review (commit d9174a7)

Status: No Issues Found | Recommendation: Merge

The previously flagged CRITICAL issues have been resolved in the latest commits:

  • openwisp_firmware_upgrader/base/models.py:452 — the _original_extractions_status typo is fixed; save() now correctly refreshes _original_extraction_status, preventing duplicate create_all_device_firmwares pairing tasks on re-save of confirmed images.
  • openwisp_firmware_upgrader/tasks.py:234 — the extraction-failure notification now uses format_html(..., url=admin_url, ...), fixing the format-string placeholder mismatch and safely escaping interpolated values.

The new _update_extraction_status() atomic conditional-UPDATE pattern and the hardened _validate_locked() (which now keys off both the persisted and in-memory status to prevent lock-bypass via a transient status change) look correct. Regression tests for both behaviors were added (test_validate_locked_blocks_bypass_via_status_change, test_auto_create_device_firmwares_skips_already_confirmed_resave, admin save_model tests).

Files Reviewed (incremental: 6 files)
  • openwisp_firmware_upgrader/base/models.py_update_extraction_status atomic transition, __init__/save status tracking, hardened _validate_locked
  • openwisp_firmware_upgrader/tasks.pyformat_html notification fix
  • openwisp_firmware_upgrader/tests/test_admin.py — save_model reset/flip tests
  • openwisp_firmware_upgrader/tests/test_models.py — lock-bypass and re-save dedup tests
  • tests/openwisp2/sample_connection/api/views.py — import-name typo fix
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py — verbose-name AlterField migration

Previous review (commit 7e44f65)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
openwisp_firmware_upgrader/base/models.py 452 Typo in save() (_original_extractions_status vs _original_extraction_status) defeats the status-tracking refresh, causing duplicate create_all_device_firmwares pairing tasks on every subsequent save of a confirmed image.
Files Reviewed (15 files)
  • openwisp_firmware_upgrader/admin.py - FirmwareImageAdmin, status badges, save_model reset logic
  • openwisp_firmware_upgrader/apps.py - metadata signal wiring
  • openwisp_firmware_upgrader/base/models.py - extraction fields, state machine, validation, signals (1 issue)
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py - new fields
  • openwisp_firmware_upgrader/migrations/0019_backfill_extraction_status.py - backfill existing rows
  • openwisp_firmware_upgrader/migrations/0020_alter_firmwareimage_board_and_more.py - verbose names
  • openwisp_firmware_upgrader/tasks.py - extract_firmware_metadata task
  • openwisp_firmware_upgrader/tests/base.py - default extraction_status
  • openwisp_firmware_upgrader/tests/test_admin.py - admin readonly/save_model tests
  • openwisp_firmware_upgrader/tests/test_api.py - query count updates
  • openwisp_firmware_upgrader/tests/test_extractors.py - extractor class test
  • openwisp_firmware_upgrader/tests/test_models.py - state machine + validation tests
  • openwisp_firmware_upgrader/tests/test_tasks.py - extraction task tests
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_* - sample migration
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_* - sample migration

Fix these issues in Kilo Cloud

Previous review (commit 048b616)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (9 files)
  • openwisp_firmware_upgrader/admin.py - Added FirmwareImageAdmin with extraction status display, readonly field logic based on extraction state
  • openwisp_firmware_upgrader/apps.py - Added connect_metadata_signals for post_save trigger
  • openwisp_firmware_upgrader/base/models.py - Added extraction status fields, validation logic, and state machine
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py - Database migration for new fields
  • openwisp_firmware_upgrader/tasks.py - Added extract_firmware_metadata Celery task with exception handling
  • openwisp_firmware_upgrader/tests/base.py - Updated test base to set default extraction_status
  • openwisp_firmware_upgrader/tests/test_admin.py - Added tests for admin readonly field logic
  • openwisp_firmware_upgrader/tests/test_models.py - Added tests for extraction status and validation
  • openwisp_firmware_upgrader/tests/test_tasks.py - Added tests for metadata extraction task

Overview

This PR implements a comprehensive metadata extraction system for firmware images:

  1. State Machine: Firmware images track extraction status (unconfirmed → in_progress → success/failed/invalid/manually_confirmed)
  2. Async Processing: Celery task extracts metadata using fwtool/DTB with proper error handling
  3. Validation: Locked metadata fields prevent changes after confirmation; batch upgrades blocked until all images confirmed
  4. Admin Interface: Status badges, readonly field management, and extraction logs displayed
  5. Device Pairing: Auto-pairing skipped when compat_version > 1.0

Code Quality Observations

  • Proper exception handling with specific error types (DecompressionLimitExceeded, UnsupportedImageError, SoftTimeLimitExceeded)
  • Good test coverage for new functionality
  • Clean separation of concerns between models, tasks, and admin
  • Proper use of transaction.on_commit for async task scheduling
  • Validation prevents using unconfirmed images for upgrades

The implementation follows Django and OpenWISP patterns and appears ready for merge.


Reviewed by glm-5.2-20260616 · Input: 121K · Output: 3.3K · Cached: 173K

@atif09

atif09 commented May 22, 2026

Copy link
Copy Markdown
Member Author

Based on #421's branch to keep the diff clean, this PR only contains the changes for #412 and #413

@atif09

atif09 commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
✅ 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.

@coderabbitai coderabbitai Bot added the python Pull requests that update Python code label May 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

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

Inline comments:
In `@openwisp_firmware_upgrader/admin.py`:
- Around line 273-277: The code currently flips obj.extraction_status from
FirmwareImage.STATUS_SUCCESS to FirmwareImage.STATUS_MANUALLY_CONFIRMED whenever
obj.source == "dtb" regardless of what was edited; change this to only flip when
one of the DTB-editable metadata fields was actually changed by checking
form.changed_data. In the admin save/update path where obj and form are
available (the branch that checks obj.extraction_status ==
FirmwareImage.STATUS_SUCCESS and obj.source == "dtb"), compute the intersection
between form.changed_data and the set of DTB-editable field names (e.g., the
metadata fields you consider editable for DTB) and only set
obj.extraction_status = FirmwareImage.STATUS_MANUALLY_CONFIRMED if that
intersection is non-empty.
- Around line 255-263: The branch that handles file replacement (the block
starting with if change and "file" in form.changed_data) resets extracted
metadata but omits compat_version; update that block to also clear
obj.compat_version (set to the empty string or None consistent with the model)
so stale compatibility data isn't retained when a new file is uploaded; locate
the block that sets obj.extraction_status, obj.extraction_log,
obj.failure_reason, obj.board, obj.compatible, obj.target, obj.fw_version and
add clearing of obj.compat_version there.
- Around line 264-265: The task is enqueued immediately after super().save_model
which can run before the DB transaction commits; wrap the Celery enqueue in
django.db.transaction.on_commit so the task is scheduled only after commit. In
the save_model override (the method containing super().save_model(request, obj,
form, change)), replace the direct extract_firmware_metadata.delay(obj.pk) call
with transaction.on_commit(lambda: extract_firmware_metadata.delay(obj.pk)) and
ensure django.db.transaction is imported at the top of the file.

In `@openwisp_firmware_upgrader/base/models.py`:
- Around line 755-765: auto_create_device_firmwares currently enqueues
create_all_device_firmwares on every save when instance.extraction_status is
success/manually_confirmed; change it to only enqueue when the status
transitions into those states by reading the previous persisted status and
comparing it to the current one. In auto_create_device_firmwares, when created
is False, fetch the previous extraction_status (e.g. prev =
cls.objects.filter(pk=instance.pk).values_list("extraction_status",
flat=True).first()), then only call
transaction.on_commit(partial(create_all_device_firmwares.delay,
str(instance.pk))) if prev is not in (STATUS_SUCCESS, STATUS_MANUALLY_CONFIRMED)
and instance.extraction_status is in (STATUS_SUCCESS,
STATUS_MANUALLY_CONFIRMED); keep the early returns for created and for
non-confirmed current status.
- Around line 504-533: The current _validate_locked only checks the incoming
self.extraction_status which allows changing metadata when extraction_status is
moved into a locked state during the same save; modify _validate_locked to
always fetch the original row (keep the existing early return only for missing
pk), include "extraction_status" in the .values(...) call, and then proceed only
if either the original['extraction_status'] or self.extraction_status is in
self.LOCKED_STATUSES; if locked, compare the original metadata fields
("board","compatible","target","fw_version","compat_version","source") against
self and raise ValidationError on changes. Ensure you reference
_validate_locked, LOCKED_STATUSES and the original variable when implementing
this.
- Around line 418-430: The model fields extraction_log, failure_reason, board,
compatible, target, fw_version, compat_version, and source need translatable
verbose_name wrappers; update each field declaration (e.g., extraction_log =
models.TextField(...), failure_reason = models.CharField(...), board =
models.CharField(...), compatible = models.JSONField(...), target =
models.CharField(...), fw_version = models.CharField(...), compat_version =
models.CharField(...), source = models.CharField(...)) to pass a human-readable
first argument wrapped with gettext_lazy ( _("...") ) and ensure gettext_lazy is
imported as _ at the top of the file so Django i18n picks up labels in
admin/forms.
- Around line 337-350: The notification code hard-codes the admin reverse and
builds untranslated/raw-HTML messages; update _notify_extraction_complete in
AbstractFirmwareImage (and the similar notification in tasks.py) to reverse the
correct admin change URL using self._meta.app_label and self._meta.model_name
(build admin name dynamically), wrap HTML with format_html and
gettext/translation (e.g., use _()) to produce a translatable, escaped message,
and ensure any exception during reverse is logged/propagated instead of being
silently swallowed; modify AbstractDeviceFirmware.auto_create_device_firmwares
to only call create_all_device_firmwares on a transition into the confirmed
extraction_status (i.e., detect the status change on save to avoid recreating
OneToOne DeviceFirmware duplicates) and short-circuit if DeviceFirmware already
exists for the device; finally add verbose_name translations for metadata fields
on AbstractFirmwareImage (board, compatible, target, compat_version, source)
using gettext_lazy for user-facing labels.

In
`@openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py`:
- Around line 13-99: The migration adds defaults that leave existing
FirmwareImage rows with extraction_status="unconfirmed" but
trigger_metadata_extraction() is only invoked on post_save when created, so
existing rows will never be queued and will block Build.batch_upgrade() and
AbstractDeviceFirmware.clean(); fix by adding a RunPython data migration in
0018_build_status_firmwareimage_board_and_more.py that finds FirmwareImage
objects with extraction_status "unconfirmed" and either (a) calls
FirmwareImage.trigger_metadata_extraction() (or enqueues the
extract_firmware_metadata Celery task with the image PK) for each row, or (b) if
you can safely infer confirmation, set extraction_status to "success" or
"manually_confirmed" accordingly; ensure the migration imports FirmwareImage and
the task name extract_firmware_metadata and performs the enqueueing/updates
atomically and idempotently.

In `@openwisp_firmware_upgrader/tasks.py`:
- Around line 225-229: Mark the user-facing notification assigned to the message
variable as translatable and ensure HTML is safely rendered: import Django i18n
and HTML helpers (e.g. gettext/gettext_lazy as _ and format_html), replace the
plain f-string with a translatable string that uses named placeholders (e.g.
_('Metadata extraction failed for <a href="{admin_url}">{image}</a>: {reason}.
You can manually enter metadata or re-upload the image.')) and then call
format_html(...) or format_html_lazy(...) with admin_url=image and
reason=update.get("failure_reason", "unknown error") to safely interpolate
values; update the message assignment accordingly in
openwisp_firmware_upgrader/tasks.py.
- Around line 106-111: The _compat_blocks_pairing function incorrectly rejects
semantic versions with more than two components (e.g., "1.0.1"); update its
parsing so it only considers the major and minor components: split
compat_version on ".", take the first two pieces (treat missing minor as 0),
convert them to ints, then compare (major, minor) > (1, 0); keep the existing
exception handling for ValueError/AttributeError/TypeError to return False.
Ensure you update the logic inside _compat_blocks_pairing to handle strings,
ints, and malformed inputs robustly while preserving the intended comparison.
- Around line 216-218: Don't hard-code the admin route name; instead construct
the URL name from the actual Build model metadata so swapped/custom models work.
Retrieve the Build model (e.g. via image._meta.get_field('build').related_model
or apps.get_model if a string was used), then build the reverse name with
"%s_%s_change" % (build_model._meta.app_label, build_model._meta.model_name) and
call reverse("admin:%s" % that_name, args=[str(image.build_id)]). Replace the
literal "admin:firmware_upgrader_build_change" assignment to admin_url in
tasks.py (the line that sets admin_url using image.build_id) with this dynamic
construction. Ensure any exception handling still surfaces errors instead of
silently swallowing them.

In `@openwisp_firmware_upgrader/tests/test_models.py`:
- Around line 35-38: The class TestModels currently hardcodes app_label and
image_type which makes tests brittle; update TestModels to compute these from
the actual model metadata/constants instead of literals: remove the class
attributes app_label and image_type and set them by reading the target model's
meta and constants (e.g. use the model class referenced by TestUpgraderMixin or
FirmwareImage._meta.app_label and a canonical image type constant on the model
such as FirmwareImage.DEFAULT_IMAGE_TYPE or similar) so tests use
model._meta.app_label and the model's image-type constant at runtime.
🪄 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: 9d26c19e-bee9-472e-87ce-8ce246abb75c

📥 Commits

Reviewing files that changed from the base of the PR and between 026e112 and 048b616.

📒 Files selected for processing (12)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

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

Files:

  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/apps.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
🧠 Learnings (4)
📚 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/base.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
📚 Learning: 2026-02-21T20:21:02.014Z
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:02.014Z
Learning: In Django migrations within this repo, avoid adding explicit related_name to ForeignKey fields when the default reverse accessor (<model>_set) is acceptable. Rely on Django’s default naming convention to keep migrations concise and maintainable. Only specify related_name if you have a concrete, needed override for reverse relations.

Applied to files:

  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
📚 Learning: 2026-02-23T21:36:22.028Z
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:22.028Z
Learning: In the sample_firmware_upgrader app within tests, avoid creating new migration files for small changes since the sample is for demonstration and testing. Review migrations with this context: the sample's initial migration may differ from the main app and need not be strictly aligned. If you change models for the demo, keep migrations minimal and document why divergence exists; don’t require parity with the main app migrations in PRs focused on the sample.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.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
🔇 Additional comments (6)
openwisp_firmware_upgrader/apps.py (1)

32-33: LGTM!

Also applies to: 58-63, 126-133

openwisp_firmware_upgrader/tests/test_models.py (1)

614-730: LGTM!

openwisp_firmware_upgrader/tests/base.py (1)

80-81: LGTM!

openwisp_firmware_upgrader/tests/test_api.py (1)

329-329: LGTM!

Also applies to: 419-419, 1162-1162

openwisp_firmware_upgrader/tests/test_extractors.py (1)

7-8: LGTM!

Also applies to: 77-78

openwisp_firmware_upgrader/tests/test_tasks.py (1)

1-260: LGTM!

Comment thread openwisp_firmware_upgrader/admin.py
Comment thread openwisp_firmware_upgrader/admin.py Outdated
Comment thread openwisp_firmware_upgrader/admin.py
Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/tasks.py
Comment thread openwisp_firmware_upgrader/tasks.py
Comment thread openwisp_firmware_upgrader/tasks.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_models.py
@nemesifier

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_firmware_upgrader/tasks.py`:
- Line 142: Replace direct use of image.file.path (which fails for
non-filesystem storages) by streaming the file to a local temporary file and
passing that temp path to the extractor: in the extract_firmware_metadata flow
replace the call site where meta = extractor_class(image.file.path).extract()
with logic that opens image.file (or reads it) and writes its contents to a
tempfile.NamedTemporaryFile (or TemporaryDirectory) then call
extractor_class(temp_path).extract(); ensure the temp file is cleaned up after
extraction and preserve existing exception handling and status updates (use the
same extractor_class symbol and the same extraction logic, just feed it a local
filesystem path instead of image.file.path).

In `@openwisp_firmware_upgrader/tests/test_admin.py`:
- Around line 785-860: Extract a helper on the test class (e.g.,
_assert_readonly_fields_for_status) that uses self._create_firmware_image(),
sets fw.extraction_status and optionally fw.source, creates MockRequest with
User.objects.first(), instantiates FirmwareImageAdmin and calls
get_readonly_fields(request, obj=fw), then performs the membership assertions
for lists of fields (expected_in and expected_not_in) instead of repeating the
same setup in each test; replace bodies of test_firmware_image_readonly_fields_*
to call this helper with the appropriate status, source, and expected field
lists so all duplicated setup and loops are centralized.

In `@openwisp_firmware_upgrader/tests/test_tasks.py`:
- Around line 188-194: The test test_extract_firmware_metadata_image_not_found
currently checks mock_warning.call_args.args for the UUID which is brittle;
update the assertion to inspect the actual logged message(s) robustly by
converting the call arguments to strings and asserting that at least one
argument contains fake_pk (e.g., after calling
tasks.extract_firmware_metadata.run(fake_pk) use
mock_warning.assert_called_once() and then assert any(fake_pk in str(arg) for
arg in mock_warning.call_args_list[0].args) so the check works regardless of
logger formatting).
🪄 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: 4ec40440-09d1-4d51-9ed7-2e5f1889abd1

📥 Commits

Reviewing files that changed from the base of the PR and between 099f237 and 048b616.

📒 Files selected for processing (12)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

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

Files:

  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
🧠 Learnings (4)
📚 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_api.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
📚 Learning: 2026-02-21T20:21:02.014Z
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:02.014Z
Learning: In Django migrations within this repo, avoid adding explicit related_name to ForeignKey fields when the default reverse accessor (<model>_set) is acceptable. Rely on Django’s default naming convention to keep migrations concise and maintainable. Only specify related_name if you have a concrete, needed override for reverse relations.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
📚 Learning: 2026-02-23T21:36:22.028Z
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:22.028Z
Learning: In the sample_firmware_upgrader app within tests, avoid creating new migration files for small changes since the sample is for demonstration and testing. Review migrations with this context: the sample's initial migration may differ from the main app and need not be strictly aligned. If you change models for the demo, keep migrations minimal and document why divergence exists; don’t require parity with the main app migrations in PRs focused on the sample.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.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
🔇 Additional comments (33)
openwisp_firmware_upgrader/apps.py (3)

32-32: LGTM!


58-63: LGTM!


126-133: LGTM!

openwisp_firmware_upgrader/tests/test_models.py (2)

38-38: Avoid hard-coded image_type test fixture values.

Please derive this from shared model/test constants to keep tests stable if model/image mappings are swapped.

Based on learnings: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding, and apply this pattern across openwisp_firmware_upgrader/tests.


614-729: LGTM!

openwisp_firmware_upgrader/tests/test_tasks.py (1)

1-1: LGTM!

Also applies to: 10-10, 15-15, 18-19, 75-187, 195-260

openwisp_firmware_upgrader/tests/base.py (1)

80-81: LGTM!

openwisp_firmware_upgrader/tests/test_api.py (1)

329-329: LGTM!

Also applies to: 419-419, 1162-1162

openwisp_firmware_upgrader/tests/test_extractors.py (1)

7-8: LGTM!

Also applies to: 77-78

openwisp_firmware_upgrader/base/models.py (5)

337-354: Use dynamic admin URL + translated safe notification message.

This is still unresolved: Line 338 hard-codes the admin URL name, and Line 347-Line 350 builds a non-translated HTML message string.


418-430: Wrap new user-facing metadata labels in Django i18n.

These new field labels remain non-translatable (for example, Line 418-Line 430), so admin/form labels won’t be localized.

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


504-533: Lock validation can be bypassed when status changes in the same save.

This remains unresolved: _validate_locked gates only on current self.extraction_status (Line 505), so metadata edits can slip through when transitioning out of a locked persisted status.


755-765: Only enqueue pairing on transition into confirmed states.

Still unresolved: this hook schedules on every save while confirmed, instead of only when status transitions into success/manually_confirmed.


150-323: LGTM!

Also applies to: 495-503, 629-638

openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py (1)

13-100: LGTM!

tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py (1)

13-100: LGTM!

openwisp_firmware_upgrader/tests/test_admin.py (2)

23-23: LGTM!

Also applies to: 551-552


862-921: LGTM!

openwisp_firmware_upgrader/tasks.py (7)

106-111: Semantic version parsing still fails for versions with more than two components.

The existing concern from previous review is still present: versions like "1.0.1" will throw ValueError during tuple unpacking, causing the function to return False and incorrectly allowing auto-pairing for versions that should be blocked.


216-229: Previously raised issues remain: hard-coded admin route and non-translatable message.

Two concerns from prior review still apply:

  1. Line 216-218: The admin URL uses a hard-coded route name "admin:firmware_upgrader_build_change" which breaks for swapped/customized models.

  2. Line 225-229: The notification message is user-facing but not marked as translatable using Django i18n. As per coding guidelines for **/*.{py,html}: user-facing strings must be marked translatable.


7-16: LGTM!


114-132: LGTM!


155-204: LGTM!


234-240: LGTM!


242-251: LGTM!

openwisp_firmware_upgrader/admin.py (8)

255-265: Previous review issues remain unaddressed.

Two issues from prior review are still present:

  1. Missing compat_version clear (lines 259-263): When the file is replaced, compat_version is not reset, leaving stale compatibility data that affects the pairing gate.

  2. Missing transaction.on_commit (line 265): Calling extract_firmware_metadata.delay(obj.pk) immediately after save risks the Celery task running before the transaction commits, causing a race condition where the worker can't find the row.

Suggested fix
+from django.db import transaction
 ...
         if change and "file" in form.changed_data:
             obj.extraction_status = FirmwareImage.STATUS_UNCONFIRMED
             obj.extraction_log = ""
             obj.failure_reason = ""
             obj.board = ""
             obj.compatible = []
             obj.target = ""
             obj.fw_version = ""
+            obj.compat_version = ""
             obj.source = ""
             super().save_model(request, obj, form, change)
-            extract_firmware_metadata.delay(obj.pk)
+            transaction.on_commit(
+                lambda pk=obj.pk: extract_firmware_metadata.delay(pk)
+            )
             return

273-277: DTB branch sets manually_confirmed without checking which fields changed.

This block runs for any edit on a successful DTB image, including changes to build or type, which shouldn't trigger the status flip. Gate on the editable metadata fields.

Suggested fix
             elif (
                 obj.extraction_status == FirmwareImage.STATUS_SUCCESS
                 and obj.source == "dtb"
+                and any(
+                    field in form.changed_data for field in ["target", "fw_version"]
+                )
             ):
                 obj.extraction_status = FirmwareImage.STATUS_MANUALLY_CONFIRMED

56-93: LGTM!


117-121: LGTM!


148-204: LGTM!


206-233: LGTM!


235-252: LGTM!


345-377: LGTM!

Comment thread openwisp_firmware_upgrader/tasks.py
Comment thread openwisp_firmware_upgrader/tests/test_admin.py
Comment thread openwisp_firmware_upgrader/tests/test_tasks.py
@atif09 atif09 moved this from In progress to In review in [GSoC26] Firmware Image Metadata May 27, 2026
@atif09 atif09 moved this from In review to In progress in [GSoC26] Firmware Image Metadata May 28, 2026
@atif09 atif09 marked this pull request as draft May 28, 2026 19:06

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @atif09, this is a significant piece of work and the foundation looks solid. I see it is still marked as draft and depends on #421, so I will hold off on a detailed line-by-line review until it is ready.

A few high-level points to keep in mind while you finish it up:

  1. Sequencing with #421. This depends on the extractor foundation in #421. Let's agree on the overall scope first (ideally on #418), then decide whether to land the two together or behind a feature flag. I would rather review them as a coherent pair than piecemeal.
  2. Migrations. New model fields plus a state machine means migrations (and sample-app migrations) need to be correct and reversible. Make sure they are included and that the state-machine field has sensible defaults for existing rows.
  3. Async task safety. The async extraction task should be idempotent, bounded in resource use (it parses firmware binaries), and must fail into a clear error state rather than leaving objects stuck mid-state. Tie the states explicitly to the exception hierarchy from #421.

Ping me when it is out of draft and #421's scope is settled, and I will go through it properly.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds an asynchronous firmware-metadata extraction pipeline to OpenWISP Firmware Upgrader by introducing extraction metadata fields + state tracking on FirmwareImage, build-level aggregate extraction status on Build, and wiring Celery tasks + admin behaviors to automate extraction, notifications, and device pairing safeguards.

Changes:

  • Added extraction-related model fields/statuses for FirmwareImage and aggregate Build.status, plus validation to block mass upgrades until metadata is confirmed.
  • Introduced extract_firmware_metadata Celery task, post-save scheduling, build status recomputation, and notifications on completion/failure.
  • Updated admin to display extraction statuses/logs and enforce read-only metadata editing rules; expanded/adjusted test suite accordingly.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py Test app migration adding build/image extraction fields for sample project.
openwisp_firmware_upgrader/tests/test_tasks.py New tests for extraction task behavior, failure modes, and pairing gating.
openwisp_firmware_upgrader/tests/test_models.py New tests for build status aggregation, locking rules, and upgrade blocking.
openwisp_firmware_upgrader/tests/test_extractors.py Asserts OpenWrt upgrader is wired to OpenWrt metadata extractor.
openwisp_firmware_upgrader/tests/test_api.py Updates query-count expectations impacted by new validation/status logic.
openwisp_firmware_upgrader/tests/test_admin.py Adds admin tests for read-only behavior and save_model extraction/manual-confirm flows.
openwisp_firmware_upgrader/tests/base.py Changes test helper default extraction status to simplify setup.
openwisp_firmware_upgrader/tasks.py Adds _compat_blocks_pairing and extract_firmware_metadata task + failure notification logic.
openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py Main app migration adding build/image extraction fields.
openwisp_firmware_upgrader/base/models.py Adds build status tracking + notifications, image extraction fields + lock validation, and blocks upgrades for unconfirmed metadata.
openwisp_firmware_upgrader/apps.py Connects metadata-extraction post_save signal and adds Firmware Images to the admin menu group.
openwisp_firmware_upgrader/admin.py Introduces FirmwareImageAdmin, status badges/log display, and save_model behaviors for re-extraction/manual confirmation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread openwisp_firmware_upgrader/tasks.py
Comment thread openwisp_firmware_upgrader/admin.py
Comment thread openwisp_firmware_upgrader/admin.py
Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/base/models.py Outdated
@atif09 atif09 force-pushed the issues/412-model-fields-state-machine branch 2 times, most recently from 7743c6b to 048b616 Compare June 10, 2026 14:22
Base automatically changed from issues/418-core-architecture-with-validation to gsoc26-metadata-extraction June 11, 2026 19:23
@atif09 atif09 force-pushed the issues/412-model-fields-state-machine branch from 1ea0b6d to a0ed12b Compare June 15, 2026 18:30
@openwisp-companion

Copy link
Copy Markdown

CI Failures: Code Style and Test Failures

Hello @atif09,
(Analysis for commit a0ed12b)

  • Code Style/QA: The CI failed due to incorrectly sorted or formatted imports in openwisp_firmware_upgrader/tests/test_extractors.py.

  • Fix: Run openwisp-qa-format to automatically fix the import order and formatting.

  • Test Failure: Several tests failed, indicated by [31;1m (red) in the test summary. The specific tests that failed are listed with [31;1m (X.Xs) [0m next to their names.

  • Fix: Review the test logs for the specific AssertionError or other exceptions raised by these tests to identify the root cause and fix the failing tests.

@atif09 atif09 force-pushed the issues/412-model-fields-state-machine branch 2 times, most recently from 624a269 to 2845f49 Compare June 15, 2026 18:51
@openwisp-companion

Copy link
Copy Markdown
{
  "failures": [
    {
      "category": "Test Failure",
      "description": "The CI environment encountered an `AttributeError` during the Django system checks, indicating a missing attribute in the URL configuration. This is likely due to a typo or an incorrect import in the URL routing setup.",
      "details": "An `AttributeError: module 'openwisp2.sample_connection.api.views' has no attribute 'deviceconnection_detail_view'. Did you mean: 'deviceconnection_details_view'?` was raised. This points to an issue in the URL configuration, specifically in `tests/openwisp2/urls.py`, where `deviceconnection_detail_view` is referenced but likely misspelled or not correctly imported. The error message suggests `deviceconnection_details_view` as a possible correct attribute name.",
      "fix": "Review the URL configuration in `tests/openwisp2/urls.py` and correct the reference to `deviceconnection_detail_view` to match the actual attribute name in `openwisp2.sample_connection.api.views` (likely `deviceconnection_details_view`)."
    }
  ]
}

Django URL Configuration Error

Hello @atif09,
(Analysis for commit 624a269)

  • Test Failure: An AttributeError occurred during Django's system checks, specifically when resolving URL handlers. The error message module 'openwisp2.sample_connection.api.views' has no attribute 'deviceconnection_detail_view'. Did you mean: 'deviceconnection_details_view'? indicates a likely typo or incorrect reference in the URL configuration file (tests/openwisp2/urls.py).

Fix: Please correct the reference to deviceconnection_detail_view in tests/openwisp2/urls.py to deviceconnection_details_view to resolve the AttributeError.

…traction task #412

- Add extraction metadata fields to FirmwareImage (board, compatible, target, fw_version, compat_version, source, extraction_status, extraction_log, failure_reason)
- Add state machine with statuses: unconfirmed, in_progress, success, failed, invalid, manually_confirmed
- Add unique_together on (build, type) to prevent duplicate image uploads
- Add _validate_locked() to make metadata fields read-only after confirmation, allowing empty fields to be filled
- Add trigger_metadata_extraction post_save signal to schedule Celery task and set build status to analyzing
- Add auto_create_device_firmware signal to trigger device pairing on confirmed status
- Ad extract_firmware_metadata Celery task with handling for different exception cases
- Add _compat_blocks_pairing() to skip device pairing when compat_version > 1.0

Related to #412
@atif09 atif09 force-pushed the issues/412-model-fields-state-machine branch from 2845f49 to 9d343f4 Compare June 15, 2026 18:59
@openwisp-companion

Copy link
Copy Markdown

Hi @atif09,

Multiple Code Style Failures

(Analysis for commit 2845f49)

There are several code style issues detected in your commit:

  1. Missing newline at end of file: The file tests/openwisp2/sample_connection/api/views.py is missing a newline at the end.
  • Fix: Add a newline to the end of the file.
  1. Black check failed: The code formatting is not compliant with Black's standards.
  • Fix: Run openwisp-qa-format to automatically fix formatting issues.
  1. Flake8 check failed: There are linting errors reported by Flake8, specifically W292 no newline at end of file.
  • Fix: This is related to the first point. Ensure the file has a newline at the end.

Please address these issues and push a new commit.

- Clear compat_version when the firmware file is replaced
- Define Celery enqueue until after the admin save commits
- Only mark DTB-backed images as manually confirmed when editable metadata changed
- Add translatable verbose names for new metadata fields
- Lock metadata based on persisted status not only the incoming one
- Only enqueue device pairing on the transition into a confirmed state
- Backfill legacy FirmwareImage extraction state before rollout
- Removed hard-coded admin route namespace for swappable models
- Mark notification text as translatable
- Make the warning assertion robust to logger formatting style
- Fix hardcoded admin reverse + missing i18n for extraction notifications and metadata field labels

Related to #412
@atif09 atif09 marked this pull request as ready for review June 16, 2026 21:19
… locking #412

- track original extraction status on load so transition checks work correctly
- fix new-object guard in _validate_locked to use _state.adding instead of pk check
- lock metadata against bypass where status and fields change in the same save
- skip device pairing re-trigger when image was already confirmed before the save
- drop defensive getattr now that _original_extraction_status is always set in __init__
- add test to confirm metadata lock can't be bypassed by changing status in the same save
- add test to confirm device pairing doesn't re-trigger on re-saves of confirmed images
- add test to confirm compat_version is cleared when a firmware file is replaced
- add test to confirm dtb status flip only triggers on target/fw_version changes, not other fields

Related to #412
@atif09 atif09 force-pushed the issues/412-model-fields-state-machine branch from a2c9227 to ce27733 Compare June 17, 2026 10:03
@atif09

atif09 commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

Inline comments:
In `@openwisp_firmware_upgrader/base/models.py`:
- Around line 440-443: The `_original_extraction_status` attribute is only
initialized once in the `__init__` method, causing the
`auto_create_device_firmwares()` method to incorrectly treat repeated saves of
the same instance as new transitions if the extraction_status changes. After
each save cycle (in the save method or post_save signal handler referenced in
lines 777-790), refresh `_original_extraction_status` by setting it equal to the
current `extraction_status` value to ensure that subsequent saves don't trigger
duplicate pairing enqueues.
- Around line 319-326: The _update_extraction_status() method has a race
condition where the initial check against self.status can become stale across
concurrent extraction tasks, allowing multiple workers to send duplicate
completion notifications. Refactor the status update in the
Build.objects.filter() call to include the current old_status as part of the
filter condition, making the update atomic and ensuring only the worker that
successfully transitions from the old status to new status proceeds with the
notification logic. Use the return value from the update call to determine if
the transition succeeded before calling _notify_extraction_complete().

In `@openwisp_firmware_upgrader/tasks.py`:
- Around line 227-234: In the message template format string on lines 227-234,
there is a placeholder mismatch causing a silent failure. The template uses the
placeholder {admin_url} but the .format() method is given the argument
url=admin_url instead of admin_url=admin_url. Fix this by changing the keyword
argument in the .format() call from url=admin_url to admin_url=admin_url so that
the placeholder name matches the provided argument name.

In `@tests/openwisp2/sample_connection/api/views.py`:
- Line 17: The import statement for the base view class contains a typo in the
name DeviceConnenctionListCreateView where there is a double "nn" in
"Connenction". This will cause an ImportError since the actual class exported
from the upstream openwisp_controller.connection.api.views module is
DeviceConnectionListCreateView with a single "nn". Correct the typo by changing
DeviceConnenctionListCreateView to DeviceConnectionListCreateView in the import
statement.
🪄 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: 6604dcbb-6616-49a1-a336-81c127427c2a

📥 Commits

Reviewing files that changed from the base of the PR and between 048b616 and ce27733.

📒 Files selected for processing (16)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/migrations/0019_backfill_extraction_status.py
  • openwisp_firmware_upgrader/migrations/0020_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • tests/openwisp2/sample_connection/api/views.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

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

Files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/migrations/0019_backfill_extraction_status.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • tests/openwisp2/sample_connection/api/views.py
  • openwisp_firmware_upgrader/migrations/0020_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/base/models.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Place imports at the top of the file. Only defer imports when necessary (e.g., Django model imports inside functions or methods where the app registry is not yet ready)
Avoid unnecessary blank lines inside function and method bodies
Mark user-facing strings for translation with Django i18n helpers in Django code
Write comments and docstrings only when they explain why code is shaped a certain way. Put comments before the relevant code block instead of scattering them inside it

Files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/migrations/0019_backfill_extraction_status.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • tests/openwisp2/sample_connection/api/views.py
  • openwisp_firmware_upgrader/migrations/0020_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/base/models.py
🧠 Learnings (6)
📚 Learning: 2026-02-21T20:21:02.014Z
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:02.014Z
Learning: In Django migrations within this repo, avoid adding explicit related_name to ForeignKey fields when the default reverse accessor (<model>_set) is acceptable. Rely on Django’s default naming convention to keep migrations concise and maintainable. Only specify related_name if you have a concrete, needed override for reverse relations.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/migrations/0019_backfill_extraction_status.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/migrations/0020_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
📚 Learning: 2026-02-23T21:36:22.028Z
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:22.028Z
Learning: In the sample_firmware_upgrader app within tests, avoid creating new migration files for small changes since the sample is for demonstration and testing. Review migrations with this context: the sample's initial migration may differ from the main app and need not be strictly aligned. If you change models for the demo, keep migrations minimal and document why divergence exists; don’t require parity with the main app migrations in PRs focused on the sample.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
📚 Learning: 2026-06-02T08:16:00.439Z
Learnt from: asmodehn
Repo: openwisp/openwisp-firmware-upgrader PR: 362
File: tests/openwisp2/urls.py:10-38
Timestamp: 2026-06-02T08:16:00.439Z
Learning: In this repository, treat `SAMPLE_APP` as a conventionally truthy environment variable: code should use the established pattern `os.environ.get("SAMPLE_APP", False)` and rely on the resulting value’s truthiness (truthy string → enabled). During code reviews, avoid recommending changes to explicit boolean parsing (e.g., checking for `

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/migrations/0019_backfill_extraction_status.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • tests/openwisp2/sample_connection/api/views.py
  • openwisp_firmware_upgrader/migrations/0020_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-06-15T20:19:28.326Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 437
File: openwisp_firmware_upgrader/tests/test_admin.py:1072-1147
Timestamp: 2026-06-15T20:19:28.326Z
Learning: In this codebase (OpenWISP projects, including openwisp-firmware-upgrader), prefer test methods that are fully self-contained: each test should do its own setup and make its own explicit assertions within the test body. Avoid introducing shared assertion helpers or parameterized helper methods that abstract away assertions across tests, even if it leads to some duplication.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • tests/openwisp2/sample_connection/api/views.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_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/base.py
  • openwisp_firmware_upgrader/tests/test_extractors.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_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
🪛 ast-grep (0.43.0)
tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py

[info] 15-15: use help_text to document model columns
Context: models.CharField(blank=True, max_length=200, verbose_name="board")
Note: Security best practice.

(model-help-text)


[info] 20-22: use help_text to document model columns
Context: models.CharField(
blank=True, max_length=10, verbose_name="compat version"
)
Note: Security best practice.

(model-help-text)


[info] 37-48: use help_text to document model columns
Context: models.CharField(
blank=True,
choices=[
("unsupported_format", "Unsupported format"),
("out_of_memory", "Out of memory"),
("invalid_file", "Invalid file"),
("timeout", "Extraction timed out"),
],
default="",
max_length=20,
verbose_name="failure reason",
)
Note: Security best practice.

(model-help-text)


[info] 53-53: use help_text to document model columns
Context: models.CharField(blank=True, max_length=20, verbose_name="source")
Note: Security best practice.

(model-help-text)


[info] 58-58: use help_text to document model columns
Context: models.CharField(blank=True, max_length=100, verbose_name="target")
Note: Security best practice.

(model-help-text)


[warning] 15-15: always specify max_length for a Charfield
Context: models.CharField(blank=True, max_length=200, verbose_name="board")
Note: Security best practice.

(model-charfield-max-length)


[warning] 20-22: always specify max_length for a Charfield
Context: models.CharField(
blank=True, max_length=10, verbose_name="compat version"
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 37-48: always specify max_length for a Charfield
Context: models.CharField(
blank=True,
choices=[
("unsupported_format", "Unsupported format"),
("out_of_memory", "Out of memory"),
("invalid_file", "Invalid file"),
("timeout", "Extraction timed out"),
],
default="",
max_length=20,
verbose_name="failure reason",
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 53-53: always specify max_length for a Charfield
Context: models.CharField(blank=True, max_length=20, verbose_name="source")
Note: Security best practice.

(model-charfield-max-length)


[warning] 58-58: always specify max_length for a Charfield
Context: models.CharField(blank=True, max_length=100, verbose_name="target")
Note: Security best practice.

(model-charfield-max-length)

tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py

[info] 15-27: use help_text to document model columns
Context: models.CharField(
choices=[
("analyzing", "Analyzing"),
("success", "Success"),
("failed", "Failed"),
("invalid", "Invalid"),
("manually_confirmed", "Manually Confirmed"),
],
db_index=True,
default="analyzing",
max_length=20,
verbose_name="extraction status",
)
Note: Security best practice.

(model-help-text)


[info] 32-32: use help_text to document model columns
Context: models.CharField(blank=True, max_length=200)
Note: Security best practice.

(model-help-text)


[info] 37-37: use help_text to document model columns
Context: models.CharField(blank=True, max_length=10)
Note: Security best practice.

(model-help-text)


[info] 52-65: use help_text to document model columns
Context: models.CharField(
choices=[
("unconfirmed", "Unconfirmed"),
("in_progress", "In Progress"),
("success", "Success"),
("failed", "Failed"),
("manually_confirmed", "Manually Confirmed"),
("invalid", "Invalid"),
],
db_index=True,
default="unconfirmed",
max_length=20,
verbose_name="extraction status",
)
Note: Security best practice.

(model-help-text)


[info] 70-80: use help_text to document model columns
Context: models.CharField(
blank=True,
choices=[
("unsupported_format", "Unsupported format"),
("out_of_memory", "Out of memory"),
("invalid_file", "Invalid file"),
("timeout", "Extraction timed out"),
],
default="",
max_length=20,
)
Note: Security best practice.

(model-help-text)


[info] 85-87: use help_text to document model columns
Context: models.CharField(
blank=True, max_length=50, verbose_name="firmware version"
)
Note: Security best practice.

(model-help-text)


[info] 92-92: use help_text to document model columns
Context: models.CharField(blank=True, max_length=20)
Note: Security best practice.

(model-help-text)


[info] 97-97: use help_text to document model columns
Context: models.CharField(blank=True, max_length=100)
Note: Security best practice.

(model-help-text)


[warning] 15-27: always specify max_length for a Charfield
Context: models.CharField(
choices=[
("analyzing", "Analyzing"),
("success", "Success"),
("failed", "Failed"),
("invalid", "Invalid"),
("manually_confirmed", "Manually Confirmed"),
],
db_index=True,
default="analyzing",
max_length=20,
verbose_name="extraction status",
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 52-65: always specify max_length for a Charfield
Context: models.CharField(
choices=[
("unconfirmed", "Unconfirmed"),
("in_progress", "In Progress"),
("success", "Success"),
("failed", "Failed"),
("manually_confirmed", "Manually Confirmed"),
("invalid", "Invalid"),
],
db_index=True,
default="unconfirmed",
max_length=20,
verbose_name="extraction status",
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 85-87: always specify max_length for a Charfield
Context: models.CharField(
blank=True, max_length=50, verbose_name="firmware version"
)
Note: Security best practice.

(model-charfield-max-length)

openwisp_firmware_upgrader/migrations/0020_alter_firmwareimage_board_and_more.py

[info] 15-15: use help_text to document model columns
Context: models.CharField(blank=True, max_length=200, verbose_name="board")
Note: Security best practice.

(model-help-text)


[info] 20-22: use help_text to document model columns
Context: models.CharField(
blank=True, max_length=10, verbose_name="compat version"
)
Note: Security best practice.

(model-help-text)


[info] 37-48: use help_text to document model columns
Context: models.CharField(
blank=True,
choices=[
("unsupported_format", "Unsupported format"),
("out_of_memory", "Out of memory"),
("invalid_file", "Invalid file"),
("timeout", "Extraction timed out"),
],
default="",
max_length=20,
verbose_name="failure reason",
)
Note: Security best practice.

(model-help-text)


[info] 53-53: use help_text to document model columns
Context: models.CharField(blank=True, max_length=20, verbose_name="source")
Note: Security best practice.

(model-help-text)


[info] 58-58: use help_text to document model columns
Context: models.CharField(blank=True, max_length=100, verbose_name="target")
Note: Security best practice.

(model-help-text)


[warning] 15-15: always specify max_length for a Charfield
Context: models.CharField(blank=True, max_length=200, verbose_name="board")
Note: Security best practice.

(model-charfield-max-length)


[warning] 20-22: always specify max_length for a Charfield
Context: models.CharField(
blank=True, max_length=10, verbose_name="compat version"
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 37-48: always specify max_length for a Charfield
Context: models.CharField(
blank=True,
choices=[
("unsupported_format", "Unsupported format"),
("out_of_memory", "Out of memory"),
("invalid_file", "Invalid file"),
("timeout", "Extraction timed out"),
],
default="",
max_length=20,
verbose_name="failure reason",
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 53-53: always specify max_length for a Charfield
Context: models.CharField(blank=True, max_length=20, verbose_name="source")
Note: Security best practice.

(model-charfield-max-length)


[warning] 58-58: always specify max_length for a Charfield
Context: models.CharField(blank=True, max_length=100, verbose_name="target")
Note: Security best practice.

(model-charfield-max-length)

openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py

[info] 15-27: use help_text to document model columns
Context: models.CharField(
choices=[
("analyzing", "Analyzing"),
("success", "Success"),
("failed", "Failed"),
("invalid", "Invalid"),
("manually_confirmed", "Manually Confirmed"),
],
db_index=True,
default="analyzing",
max_length=20,
verbose_name="extraction status",
)
Note: Security best practice.

(model-help-text)


[info] 32-32: use help_text to document model columns
Context: models.CharField(blank=True, max_length=200)
Note: Security best practice.

(model-help-text)


[info] 37-37: use help_text to document model columns
Context: models.CharField(blank=True, max_length=10)
Note: Security best practice.

(model-help-text)


[info] 52-65: use help_text to document model columns
Context: models.CharField(
choices=[
("unconfirmed", "Unconfirmed"),
("in_progress", "In Progress"),
("success", "Success"),
("failed", "Failed"),
("manually_confirmed", "Manually Confirmed"),
("invalid", "Invalid"),
],
db_index=True,
default="unconfirmed",
max_length=20,
verbose_name="extraction status",
)
Note: Security best practice.

(model-help-text)


[info] 70-80: use help_text to document model columns
Context: models.CharField(
blank=True,
choices=[
("unsupported_format", "Unsupported format"),
("out_of_memory", "Out of memory"),
("invalid_file", "Invalid file"),
("timeout", "Extraction timed out"),
],
default="",
max_length=20,
)
Note: Security best practice.

(model-help-text)


[info] 85-87: use help_text to document model columns
Context: models.CharField(
blank=True, max_length=50, verbose_name="firmware version"
)
Note: Security best practice.

(model-help-text)


[info] 92-92: use help_text to document model columns
Context: models.CharField(blank=True, max_length=20)
Note: Security best practice.

(model-help-text)


[info] 97-97: use help_text to document model columns
Context: models.CharField(blank=True, max_length=100)
Note: Security best practice.

(model-help-text)


[warning] 15-27: always specify max_length for a Charfield
Context: models.CharField(
choices=[
("analyzing", "Analyzing"),
("success", "Success"),
("failed", "Failed"),
("invalid", "Invalid"),
("manually_confirmed", "Manually Confirmed"),
],
db_index=True,
default="analyzing",
max_length=20,
verbose_name="extraction status",
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 52-65: always specify max_length for a Charfield
Context: models.CharField(
choices=[
("unconfirmed", "Unconfirmed"),
("in_progress", "In Progress"),
("success", "Success"),
("failed", "Failed"),
("manually_confirmed", "Manually Confirmed"),
("invalid", "Invalid"),
],
db_index=True,
default="unconfirmed",
max_length=20,
verbose_name="extraction status",
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 85-87: always specify max_length for a Charfield
Context: models.CharField(
blank=True, max_length=50, verbose_name="firmware version"
)
Note: Security best practice.

(model-charfield-max-length)

openwisp_firmware_upgrader/base/models.py

[info] 165-171: use help_text to document model columns
Context: models.CharField(
_("extraction status"),
max_length=20,
choices=BUILD_STATUS_CHOICES,
default=BUILD_STATUS_ANALYZING,
db_index=True,
)
Note: Security best practice.

(model-help-text)


[warning] 165-171: always specify max_length for a Charfield
Context: models.CharField(
_("extraction status"),
max_length=20,
choices=BUILD_STATUS_CHOICES,
default=BUILD_STATUS_ANALYZING,
db_index=True,
)
Note: Security best practice.

(model-charfield-max-length)


[info] 417-423: use help_text to document model columns
Context: models.CharField(
_("extraction status"),
max_length=20,
choices=EXTRACTION_STATUS_CHOICES,
default=STATUS_UNCONFIRMED,
db_index=True,
)
Note: Security best practice.

(model-help-text)


[info] 425-431: use help_text to document model columns
Context: models.CharField(
_("failure reason"),
max_length=20,
choices=FAILURE_REASON_CHOICES,
blank=True,
default="",
)
Note: Security best practice.

(model-help-text)


[info] 432-432: use help_text to document model columns
Context: models.CharField(_("board"), max_length=200, blank=True)
Note: Security best practice.

(model-help-text)


[info] 434-434: use help_text to document model columns
Context: models.CharField(_("target"), max_length=100, blank=True)
Note: Security best practice.

(model-help-text)


[info] 435-435: use help_text to document model columns
Context: models.CharField(_("firmware version"), max_length=50, blank=True)
Note: Security best practice.

(model-help-text)


[info] 436-436: use help_text to document model columns
Context: models.CharField(_("compat version"), max_length=10, blank=True)
Note: Security best practice.

(model-help-text)


[info] 437-437: use help_text to document model columns
Context: models.CharField(_("source"), max_length=20, blank=True)
Note: Security best practice.

(model-help-text)


[warning] 417-423: always specify max_length for a Charfield
Context: models.CharField(
_("extraction status"),
max_length=20,
choices=EXTRACTION_STATUS_CHOICES,
default=STATUS_UNCONFIRMED,
db_index=True,
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 425-431: always specify max_length for a Charfield
Context: models.CharField(
_("failure reason"),
max_length=20,
choices=FAILURE_REASON_CHOICES,
blank=True,
default="",
)
Note: Security best practice.

(model-charfield-max-length)


[warning] 432-432: always specify max_length for a Charfield
Context: models.CharField(_("board"), max_length=200, blank=True)
Note: Security best practice.

(model-charfield-max-length)


[warning] 434-434: always specify max_length for a Charfield
Context: models.CharField(_("target"), max_length=100, blank=True)
Note: Security best practice.

(model-charfield-max-length)


[warning] 435-435: always specify max_length for a Charfield
Context: models.CharField(_("firmware version"), max_length=50, blank=True)
Note: Security best practice.

(model-charfield-max-length)


[warning] 436-436: always specify max_length for a Charfield
Context: models.CharField(_("compat version"), max_length=10, blank=True)
Note: Security best practice.

(model-charfield-max-length)


[warning] 437-437: always specify max_length for a Charfield
Context: models.CharField(_("source"), max_length=20, blank=True)
Note: Security best practice.

(model-charfield-max-length)

🔇 Additional comments (37)
openwisp_firmware_upgrader/migrations/0018_build_status_firmwareimage_board_and_more.py (1)

3-100: LGTM!

openwisp_firmware_upgrader/migrations/0019_backfill_extraction_status.py (1)

4-30: LGTM!

openwisp_firmware_upgrader/migrations/0020_alter_firmwareimage_board_and_more.py (1)

3-61: LGTM!

tests/openwisp2/sample_firmware_upgrader/migrations/0005_build_status_firmwareimage_board_and_more.py (1)

3-100: LGTM!

tests/openwisp2/sample_firmware_upgrader/migrations/0006_alter_firmwareimage_board_and_more.py (1)

3-61: LGTM!

openwisp_firmware_upgrader/tasks.py (4)

7-16: LGTM!


106-111: LGTM!


114-206: LGTM!


239-256: LGTM!

openwisp_firmware_upgrader/tests/test_tasks.py (6)

1-18: LGTM!


32-78: LGTM!


80-130: LGTM!


132-191: LGTM!


193-222: LGTM!


224-264: LGTM!

openwisp_firmware_upgrader/apps.py (1)

32-33: LGTM!

Also applies to: 58-63, 126-133

openwisp_firmware_upgrader/tests/base.py (1)

80-81: LGTM!

openwisp_firmware_upgrader/tests/test_models.py (1)

570-756: LGTM!

Also applies to: 766-861, 979-1230, 1233-1393

openwisp_firmware_upgrader/tests/test_extractors.py (1)

1-93: LGTM!

openwisp_firmware_upgrader/tests/test_api.py (1)

329-330: LGTM!

Also applies to: 419-420, 1162-1163, 1312-1313, 1493-1494, 1532-1533

openwisp_firmware_upgrader/admin.py (9)

263-291: Past review comments have been addressed.

The previous review raised three issues on this method:

  1. Missing compat_version reset on file change → now cleared at line 272
  2. Task should use transaction.on_commit → now wrapped at line 275
  3. DTB manual-confirm should gate on actual field changes → now checks ["target", "fw_version"] at lines 286-288

All three are resolved.


65-103: LGTM!


126-130: LGTM!


157-214: LGTM!


215-242: LGTM!


244-262: LGTM!


358-390: LGTM!


569-596: LGTM!


961-972: LGTM!

openwisp_firmware_upgrader/tests/test_admin.py (8)

125-128: LGTM!


217-253: LGTM!


596-606: LGTM!


835-857: LGTM!


859-1041: LGTM!


1042-1107: LGTM!


1109-1184: LGTM!


1186-1245: LGTM!

Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/base/models.py
Comment thread openwisp_firmware_upgrader/tasks.py Outdated
Comment thread tests/openwisp2/sample_connection/api/views.py Outdated
Comment thread openwisp_firmware_upgrader/base/models.py Outdated
@atif09 atif09 force-pushed the issues/412-model-fields-state-machine branch from 7e44f65 to d9174a7 Compare June 17, 2026 13:52
@openwisp-companion

Copy link
Copy Markdown

Test Failure in test_auto_create_device_firmwares_triggers_on_confirmed

Hello @atif09,
(Analysis for commit d9174a7)

The test test_auto_create_device_firmwares_triggers_on_confirmed failed because the mock_on_commit assertion expected on_commit to be called once, but it was called zero times. This indicates that the signal handler for post_save on FirmwareImage was not triggered as expected.

Fix:
Ensure that the post_save signal for FirmwareImage is correctly connected in openwisp_firmware_upgrader/apps.py. The signal handler DeviceFirmware.auto_create_device_firmwares should be connected to the post_save signal of FirmwareImage.

Additionally, the test logic might need adjustment if the signal is only intended to trigger when extraction_status changes to STATUS_SUCCESS or STATUS_MANUALLY_CONFIRMED. The current test setup might be missing this condition.

- Make build status transition atomic to prevent duplicate notifications
- Refresh _original_extraction_status in save() to prevent duplicate device pairing enqueue
- Fix format string placeholder mismatch in failure notification using format_html
- Fix the typo in the imported base view name

Related to #412
@atif09 atif09 force-pushed the issues/412-model-fields-state-machine branch from d9174a7 to ef60188 Compare June 18, 2026 19:11
- Added re_extract_metadata bulk action to FirmwareImageAdmin that resets image metadata fields and reschedules extraction tasks
- Added tests to cover single and multi-image bulk re-extraction

Related to #413
@atif09 atif09 force-pushed the issues/412-model-fields-state-machine branch from 8b6b7b6 to 57d4cfe Compare June 19, 2026 22:46
@openwisp-companion

Copy link
Copy Markdown

Black Formatting Errors

Hello @atif09,

(Analysis for commit 8b6b7b6)

It looks like there are some formatting issues in your code. The black tool detected these problems.

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

openwisp-qa-format

This command will automatically format your code according to the project's style guidelines.

@atif09

atif09 commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/tasks.py (1)

228-236: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Notification shows raw failure_reason value instead of human-readable label.

The notification message interpolates update.get("failure_reason", "unknown error") which yields raw values like "timeout" or "out_of_memory". For consistency with _notify_extraction_complete in models.py (which maps BUILD_STATUS_CHOICES to display names), consider mapping failure_reason to its display name from FAILURE_REASON_CHOICES.

🔧 Suggested improvement
+            failure_reason_choices = dict(FirmwareImage.FAILURE_REASON_CHOICES)
+            reason_display = failure_reason_choices.get(
+                update.get("failure_reason", ""),
+                _("unknown error"),
+            )
             notify.send(
                 sender=image,
                 type="generic_message",
                 level="error",
                 url=admin_url,
                 target=image.build,
                 message=format_html(
                     _(
                         'Metadata extraction failed for <a href="{url}">{image}</a>: '
                         "{reason}. You can manually enter metadata or re-upload the image."
                     ),
                     url=admin_url,
                     image=image,
-                    reason=update.get("failure_reason", "unknown error"),
+                    reason=reason_display,
                 ),
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_firmware_upgrader/tasks.py` around lines 228 - 236, The
failure_reason value being interpolated in the format_html call is using raw
string values instead of human-readable display labels. Replace the
`update.get("failure_reason", "unknown error")` with a mapped value that looks
up the display name from FAILURE_REASON_CHOICES, similar to how the
_notify_extraction_complete method in models.py maps BUILD_STATUS_CHOICES to
display names. This ensures the notification message shows user-friendly text
like "Timeout" instead of the raw key "timeout".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@openwisp_firmware_upgrader/tasks.py`:
- Around line 228-236: The failure_reason value being interpolated in the
format_html call is using raw string values instead of human-readable display
labels. Replace the `update.get("failure_reason", "unknown error")` with a
mapped value that looks up the display name from FAILURE_REASON_CHOICES, similar
to how the _notify_extraction_complete method in models.py maps
BUILD_STATUS_CHOICES to display names. This ensures the notification message
shows user-friendly text like "Timeout" instead of the raw key "timeout".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 18107094-8125-48c7-bb88-89f9d96035a3

📥 Commits

Reviewing files that changed from the base of the PR and between ce27733 and 57d4cfe.

📒 Files selected for processing (5)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_models.py
💤 Files with no reviewable changes (1)
  • openwisp_firmware_upgrader/tests/test_models.py
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=4.2.0
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

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

Files:

  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Place imports at the top of the file. Only defer imports when necessary (e.g., Django model imports inside functions or methods where the app registry is not yet ready)
Avoid unnecessary blank lines inside function and method bodies
Mark user-facing strings for translation with Django i18n helpers in Django code
Write comments and docstrings only when they explain why code is shaped a certain way. Put comments before the relevant code block instead of scattering them inside it

Files:

  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
🧠 Learnings (4)
📚 Learning: 2026-06-02T08:16:00.439Z
Learnt from: asmodehn
Repo: openwisp/openwisp-firmware-upgrader PR: 362
File: tests/openwisp2/urls.py:10-38
Timestamp: 2026-06-02T08:16:00.439Z
Learning: In this repository, treat `SAMPLE_APP` as a conventionally truthy environment variable: code should use the established pattern `os.environ.get("SAMPLE_APP", False)` and rely on the resulting value’s truthiness (truthy string → enabled). During code reviews, avoid recommending changes to explicit boolean parsing (e.g., checking for `

Applied to files:

  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/admin.py
  • 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-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-06-15T20:19:28.326Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 437
File: openwisp_firmware_upgrader/tests/test_admin.py:1072-1147
Timestamp: 2026-06-15T20:19:28.326Z
Learning: In this codebase (OpenWISP projects, including openwisp-firmware-upgrader), prefer test methods that are fully self-contained: each test should do its own setup and make its own explicit assertions within the test body. Avoid introducing shared assertion helpers or parameterized helper methods that abstract away assertions across tests, even if it leads to some duplication.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
🔇 Additional comments (24)
openwisp_firmware_upgrader/base/models.py (8)

152-173: LGTM!


214-227: LGTM!


299-331: LGTM!


333-367: LGTM!


395-453: LGTM!


471-473: LGTM!


516-524: LGTM!

Also applies to: 526-561


787-800: LGTM!

openwisp_firmware_upgrader/tasks.py (3)

8-8: LGTM!


116-206: LGTM!


241-258: LGTM!

openwisp_firmware_upgrader/admin.py (8)

130-131: LGTM!


159-178: LGTM!


180-215: LGTM!


217-244: LGTM!


246-263: LGTM!


265-293: LGTM!


295-323: LGTM!


416-422: LGTM!

openwisp_firmware_upgrader/tests/test_admin.py (5)

217-253: LGTM!


254-329: LGTM!


666-666: LGTM!


1186-1262: LGTM!


1263-1322: LGTM!

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

Labels

enhancement New feature or request python Pull requests that update Python code

Projects

Status: In progress

3 participants