Skip to content

Conversation

@Viscous106
Copy link
Contributor

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.

Reference to Existing Issue

Closes #744.

Description of Changes

Problem

When saving a Template with invalid netjsonconfig (e.g., OpenVPN server mode with missing required fields), the Django admin shows a generic "Please correct all validation errors below" banner without highlighting any specific field. This makes it difficult for users to identify what needs to be fixed.

Root Cause

The clean_netjsonconfig_backend() method in BaseConfig was raising ValidationError(message) as a non-field error instead of attaching it to the config field.

Solution

Modified openwisp_controller/config/base/base.py to raise ValidationError({'config': message}), ensuring the error is attached to the config field and properly highlighted in the admin interface.

Changes

  • openwisp_controller/config/base/base.py: Updated exception handling in clean_netjsonconfig_backend() to attach validation errors to the config field
  • openwisp_controller/config/tests/test_template.py: Added regression test test_validation_fix_attached_to_config_field() to verify the fix

Screenshot

_Before:
image

_After:
image

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Walkthrough

The change modifies netjsonconfig validation error shape: clean_netjsonconfig_backend now raises ValidationError({"config": message}) instead of a plain string. Tests were added/updated to assert the validation error is reported under the config key (e.g., e.message_dict["config"][0]) and to verify the error message content for invalid interface/OpenWrt configurations. One of the new tests was duplicated in the test file. No other control flow or public API signatures were changed.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive Test method duplication detected in test_template.py where test_validation_fix_attached_to_config_field appears twice; clarification needed on whether this is intentional. Verify if test duplication is intentional or remove the duplicate test method to maintain code quality.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the fix: attaching netjsonconfig validation errors to the config field, with specific reference to issue #744.
Description check ✅ Passed The description comprehensively covers the problem, root cause, solution, and changes with screenshots, matching the template requirements.
Linked Issues check ✅ Passed All coding requirements from issue #744 are addressed: ValidationError now raises {'config': message} to attach errors to the config field, and regression tests verify the fix.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
openwisp_controller/config/base/base.py (1)

196-206: Chain the exception with raise ... from e to preserve the original traceback.

Ruff B904 flags this: within an except clause, use raise ValidationError(...) from e (or from None) to distinguish the re-raised exception from an error in exception handling. This also preserves the original SchemaError traceback for debugging.

Proposed fix
-            raise ValidationError({"config": message})
+            raise ValidationError({"config": message}) from e
📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87f3569 and e822220.

📒 Files selected for processing (3)
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_template.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openwisp_controller/config/tests/test_config.py
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

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

Applied to files:

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

Applied to files:

  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/base/base.py
🪛 Ruff (0.14.14)
openwisp_controller/config/base/base.py

[warning] 206-206: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (1)
openwisp_controller/config/tests/test_template.py (1)

804-832: Good regression test for the fix.

The test correctly verifies that ValidationError attaches errors to the "config" field and includes the expected trigger path. This aligns well with the PR objective.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

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

196-206: ⚠️ Potential issue | 🟡 Minor

Preserve the original SchemaError via exception chaining.

Line 206 raises a new ValidationError without chaining, which obscures the root cause and triggers Ruff's B904 rule. Use raise ... from e to preserve the exception context.

🔧 Suggested change
-            raise ValidationError({"config": message})
+            raise ValidationError({"config": message}) from e
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 109f28e and 77a4c2f.

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

Applied to files:

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

Applied to files:

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

Applied to files:

  • openwisp_controller/config/tests/test_template.py
🧬 Code graph analysis (1)
openwisp_controller/config/tests/test_template.py (1)
openwisp_controller/config/base/config.py (1)
  • full_clean (558-562)
🪛 Ruff (0.14.14)
openwisp_controller/config/base/base.py

[warning] 206-206: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
🔇 Additional comments (1)
openwisp_controller/config/tests/test_template.py (1)

804-825: Test coverage aligns with the new ValidationError shape.
The assertions verify the error is attached to config and include the expected trigger path.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 5, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 5, 2026
@Viscous106 Viscous106 force-pushed the vpn-template-validation branch from 6ed2edb to d734d2f Compare February 5, 2026 08:25
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

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

191-206: ⚠️ Potential issue | 🟡 Minor

Add exception chaining to preserve the original SchemaError.

This satisfies ruff's B904 rule, which flags raising a new exception inside an except block without explicit chaining. Since you're wrapping SchemaError in ValidationError, use from e to make the relationship explicit and preserve debugging information in tracebacks.

♻️ Proposed fix
-            raise ValidationError({"config": message})
+            raise ValidationError({"config": message}) from e
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ed2edb and d734d2f.

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

Applied to files:

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

Applied to files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/base.py
🪛 Ruff (0.14.14)
openwisp_controller/config/base/base.py

[warning] 206-206: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
🔇 Additional comments (1)
openwisp_controller/config/tests/test_config.py (1)

146-156: LGTM — assertion now targets the config field (Line 155).

This aligns with the updated ValidationError payload.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@Viscous106 Viscous106 force-pushed the vpn-template-validation branch 3 times, most recently from b1cf945 to f36a9c6 Compare February 5, 2026 16:11
@coveralls
Copy link

coveralls commented Feb 6, 2026

Coverage Status

coverage: 98.636% (+0.03%) from 98.607%
when pulling e822220 on Viscous106:vpn-template-validation
into 012a094 on openwisp:master.

@Viscous106 Viscous106 marked this pull request as ready for review February 6, 2026 04:34
Copilot AI review requested due to automatic review settings February 6, 2026 04:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes a user experience issue in the Django admin where netjsonconfig validation errors were not properly highlighted on the specific field that caused the error. The PR changes how ValidationError is raised in the clean_netjsonconfig_backend method to attach errors to the config field instead of raising them as non-field errors.

Changes:

  • Modified clean_netjsonconfig_backend in base.py to raise field-specific ValidationError
  • Added regression test to verify errors are properly attached to the config field
  • Updated existing test to reflect the new error reporting behavior

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
openwisp_controller/config/base/base.py Changed ValidationError from non-field error to field-specific error attached to config field
openwisp_controller/config/tests/test_template.py Added regression test verifying errors are attached to config field
openwisp_controller/config/tests/test_config.py Updated existing test assertion to expect error on config field instead of all

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

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 6, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 6, 2026
@Viscous106 Viscous106 marked this pull request as draft February 10, 2026 06:37
Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

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

This PR is not solving the issue, it's merely moving the validation error message which is not what we want right now.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Defining OpenVPN Server in a template complains about validation and does not allow saving

3 participants