Closes #496: Add optional link title to URL fields - #641
Conversation
A url-type CustomObjectTypeField now expands into two real DB columns: the URL itself, and an optional <name>_title used as the visible link text on an object's detail page instead of the raw URL (falling back to the URL when no title is set). Mirrors CoordinatesFieldType's existing two-column pattern, but unlike coordinates the primary URL column keeps behaving like any other single-value column -- unique, default, and regex validation all still apply to it. This required making three previously coordinates-only DDL/validation code paths in models.py dict-aware (the generic single-column schema helpers, the unique-conversion probe in clean(), and the backing- column-collision guard), since URL is the first multi-column type to also flow through the generic single-column path via its unique/ default support. No new migration or upgrade script is needed for existing installations: the plugin's existing post_migrate schema-heal pass already covers any nullable non-mixin column whose attribute name doesn't match a user field's own name, which the new title column satisfies the same way coordinates' latitude/longitude columns already do.
url_scheme_is_allowed() only exists on NetBox's feature branch (added 2026-07-23), not in main or any released version, so importing it broke CI's NetBox-main test matrix leg with an ImportError at plugin load time. This plugin supports NetBox 4.5.2+, well before that function existed. Reimplement the same scheme-allowlist check locally in custom_object_utils.py instead. ALLOWED_URL_SCHEMES itself (the config parameter the function reads) has existed since 2020 and is safe to rely on across the whole supported version range.
|
@claude /review and limit response only to areas of potential concern. |
|
Claude finished @bctiemann's task in 6m 45s —— View job Review: Areas of Potential Concern
1. No tests for
|
- Add tests for get_url_field_html and its security-relevant scheme guard (_url_scheme_is_allowed): allowed scheme with title, allowed scheme falling back to the URL text, a disallowed scheme (e.g. javascript:) rendering as plain text instead of a link, and an unset URL returning ''. - Document that CSV bulk import only populates the URL value, not the title (matches the existing, pre-established limitation for coordinates fields' backing columns). - Make the title column's schema_editor.add_field() call idempotent, checking existing_cols first, matching the established pattern in _schema_add_field() (the URL column already had this guard; the title column's separate add_field() call did not). - Change the title column from CharField(null=True, blank=True) to CharField(blank=True, default=""), so "no title" has one canonical representation instead of two (NULL vs ''). default="" keeps the column eligible for mixin_migration.py's auto-heal pass on existing installations, which requires a column to be nullable or have a Django-level default before auto-adding it.
pheus
left a comment
There was a problem hiding this comment.
Thanks for working on this. The normal URL and title flow looks good.
I found three cases that still need another pass: upgrading existing branches, deferred replay during squash operations, and rename/history handling for the second backing column. I also left two smaller comments about form help text and the field-deletion warning.
I’m requesting changes for now.
- heal_branch(): wire netbox-branching's own post_migrate signal to the existing heal_all_cots() pass so a branch provisioned before this plugin version gains the url field's title column in its own schema, not just main's. Regression test included. - _apply_deferred_co_field(): also replay a URL field's title value from buffered squash-merge data, matching the existing base-column replay. - Extract _alter_column_with_rename_conflict_resolution() from _schema_alter_field() and reuse it for the title column's rename, so an independent-rename conflict resolves the same way for both backing columns. Also rewrite the title column's ObjectChange audit key on rename, alongside the existing base-column rewrite. - URLFieldType.get_form_fields(): surface the field's configured description as help_text on the URL input, and add help_text to the title input. - Field-deletion impact preview: count/list objects with either the URL or the title set, since they can be set independently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks for the thorough review, @pheus! Pushed 14d6bbb addressing all five points:
|
- URLFieldType.render_table_column() now renders the same title-as-link-text HTML as the detail page instead of the raw URL, via a new shared render_url_html() helper. The two backing columns are never shown as separate table columns. - Refactor get_url_field_html() to delegate to the same helper so both views render identically. - Document the change and add a release note explaining that existing url fields (including on existing branches) get the new title column healed automatically on upgrade. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The upgrade-healing note for #496 was added under the 0.6.0 section, but that version is already released. No 0.7.0/1.0.0 section exists yet to hold it, so it needs to wait until that section is drafted. The mechanism itself is still documented in field-attributes.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pheus
left a comment
There was a problem hiding this comment.
Thanks for the follow-up.
I found three remaining upgrade and replay issues: existing branches still need a reliable trigger for the schema heal, deferred title values are applied before the backing column exists, and pre-existing <url>_title field collisions need to be handled safely.
I’ve left the details inline. I’m requesting changes for now, but this looks close.
- heal_all_branches(): the reliable trigger for healing existing branches is the main upgrade path (post_migrate signal handler and upgrade_custom_objects), not netbox-branching's own migration signal -- this feature ships no Django migration, so Branch.migrate() never has anything "pending" to detect and never fires it. Wired into both entry points; heal_branch()/_heal_branch_on_migrate remain as a secondary path for a future release that does ship a migration alongside a schema change. Guards against netbox-branching being pip-installed but not enabled in PLUGINS via apps.is_installed() rather than a bare import, matching the existing pattern in checks.py. - Reorder CustomObjectTypeField.save() so a url field's title column is added before _apply_deferred_co_field() replays buffered values -- replaying the title value used to run before that column existed. - Add detect_backing_column_collisions(), shared with clean()'s existing guard, and surface it as a heal_cot() warning: a plain field literally named "<url_field>_title" could have been created before that guard existed, and would otherwise silently and non-deterministically lose data in _fetch_and_generate_field_attrs() with no warning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pheus
left a comment
There was a problem hiding this comment.
Thanks for the follow-up. The automatic branch trigger and deferred replay ordering look good now, and surfacing existing column collisions is a useful improvement.
I found two remaining upgrade-safety issues: the branch sweep can operate against a missing or outdated schema, and the collision warning currently suggests a recovery path that can move another field's data. I also left one small documentation correction.
I’m requesting changes for those upgrade cases, but the main URL/title implementation looks close.
| ) | ||
|
|
||
| total = healed = warnings = 0 | ||
| for branch in Branch.objects.exclude(status__in=no_schema_statuses): |
There was a problem hiding this comment.
Could we validate and isolate each branch before healing it?
FAILED can mean provisioning failed, in which case the schema has already been dropped. Since the branch connection uses <branch>,<main> as its search path, those queries can then fall through to main. This also includes branches with pending migrations, where the current ORM may not match the branch schema yet.
Please verify the schema exists, skip branches with a pending migration plan, and handle failures per branch. The branch post_migrate hook can heal skipped branches after migration.
| f"({field.type}) and field {sibling.name!r} ({sibling.type}) " | ||
| f"both map to backing column {column!r}. This predates " | ||
| f"validation that now blocks creating this combination; " | ||
| f"rename one of the two fields to resolve the collision, " |
There was a problem hiding this comment.
Could we make the recovery guidance more specific? Renaming either field is not safe.
For an existing website / website_title pair, renaming website also renames the shared website_title column. That moves the sibling field's data into the URL title and leaves the sibling without its own column.
The safe path is to rename the field whose own name matches the colliding column, then rerun the heal. A data-preservation test for that recovery flow would be helpful.
| - **Upgrading from an older release.** Existing `url` fields gain the `<name>_title` | ||
| column automatically on the next `manage.py migrate` (or immediately via | ||
| `manage.py upgrade_custom_objects`). On a NetBox Branching branch, it's healed the | ||
| next time that branch itself is migrated. |
There was a problem hiding this comment.
Could we update this sentence to match the new upgrade path? Existing branch schemas are now healed by the main migrate or upgrade_custom_objects run; the branch migration hook is only the secondary path.
| # "<url_field>_title" predating the validation that now blocks this). | ||
| # Independent of DB introspection -- purely a field-definition check -- | ||
| # so it runs even if the table itself can't be introspected below. | ||
| from netbox_custom_objects.models import detect_backing_column_collisions # noqa: PLC0415 |
There was a problem hiding this comment.
Could we keep only the optional netbox_branching imports local?
I don't see an import-cycle or app-loading reason for delaying detect_backing_column_collisions, and django.apps.apps is also safe to import at module scope. Moving those to the regular imports would make it clearer that the remaining lazy imports are specifically required for the optional Branching integration.
Closes: #496
Summary
Upgrading
Existing
urlfields gain the new<name>_titlecolumn automatically — no manual migration is needed. On the main schema this happens the next timemanage.py migrateruns (or immediately viamanage.py upgrade_custom_objects, which also supports--dry-run). On a NetBox Branching branch it happens the next time that branch itself is migrated (its "Migrate branch" action, available whenever the branch's migration state lags behind main).This will need to be captured in a release note for the next minor release in which this feature ships.
Test plan