Skip to content

Closes #268: Add display_expression for composite custom object names#586

Open
bctiemann wants to merge 8 commits into
featurefrom
268-display-expression
Open

Closes #268: Add display_expression for composite custom object names#586
bctiemann wants to merge 8 commits into
featurefrom
268-display-expression

Conversation

@bctiemann

@bctiemann bctiemann commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Closes: #268

Summary

Adds a display_expression Jinja2 template field to CustomObjectType that lets administrators compose the object display name from multiple field values, replacing the single-field primary marker approach when set.

Example: a COT with make, model, and serial fields can use {{ make }} - {{ model }} as its display expression so objects display as Cisco - ASR1001 instead of just the primary field value.

How it works

  • display_expression is a CharField(max_length=500, blank=True) on CustomObjectType
  • CustomObject.__str__() calls a new _render_display_expression() helper that renders the expression via a Jinja2 SandboxedEnvironment with all field values as context
  • Any rendering error (syntax error, etc.) silently falls through to the existing single-primary-field fallback
  • Fields referenced in the expression that don't exist on the object resolve to an empty string (via jinja2.Undefined)
  • If the rendered result is empty, the fallback is also used

Form layout

The form now has a Display fieldset grouping verbose_name, verbose_name_plural, display_expression, and group_name — all fields that control how the type appears to users — positioned between the Name field and the URL path/slug field.

Screenshot 2026-06-17 at 11 19 00 AM

Surface area

  • Model field + migration 0015
  • _render_display_expression() helper (separately testable/mockable)
  • Form: Display fieldset
  • Serializer: display_expression in CustomObjectTypeSerializer
  • COT detail template: shows expression as <code> when set

Test plan

  • Create a COT with make and model text fields; set display_expression = {{ make }} - {{ model }}; verify objects display as Make - Model
  • Verify that clearing the expression falls back to the primary-field display name
  • Verify a broken expression (e.g. {% bad %}) falls back silently

🤖 Generated with Claude Code

bctiemann and others added 4 commits June 17, 2026 10:59
Adds a Jinja2 display_expression field to CustomObjectType. When set,
CustomObject.__str__() renders it with all field values as context
instead of falling back to the single primary-field display name.

- models.py: display_expression CharField(max_length=500, blank=True)
  on CustomObjectType; CustomObject._render_display_expression() helper
  renders it via a SandboxedEnvironment (security); any rendering error
  silently falls through to the existing primary-field fallback
- migration 0015
- forms.py: Display fieldset with display_expression
- serializers.py: display_expression in CustomObjectTypeSerializer
- customobjecttype.html: shows expression in code block when set
- 5 tests: composite render, missing-field fallback, empty expression,
  rendering error, empty-result fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Group verbose_name, verbose_name_plural, group_name, and display_expression
into a Display fieldset positioned between Name and URL path/slug.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bctiemann

Copy link
Copy Markdown
Contributor Author

@claude /review

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

Claude finished @bctiemann's task in 1m 48s —— View job


PR Review: Add display_expression for composite custom object names

Summary: This PR adds a display_expression Jinja2 template field to CustomObjectType that lets users compose object display names from multiple field values. The implementation is solid overall — using SandboxedEnvironment is the right call, and the fallback logic is clean. There are a few things worth addressing before merge.


Issues

1. Performance: Template recompilation on every __str__() call (models.py:819–820)

env = _JinjaSandbox(undefined=_JinjaUndefined)
rendered = env.from_string(expression).render(**ctx).strip()

env.from_string() compiles the template on every call. In a list view with N objects, this means N compilations of the same template string. The docstring calls this out and suggests caching the compiled template on the model class — that fix should happen here, not deferred. CustomObjectType already has a _model_cache pattern; a _compiled_display_template dict keyed by COT id and cleared in post_save would be straightforward.

Similarly, the full field-object iteration to build ctx runs even when the expression only references two of fifteen fields. Both optimizations are in scope given the docstring already identifies them.

Fix this →

2. custom_object_type access is outside the try block (models.py:816)

expression = getattr(self.custom_object_type, 'display_expression', '')  # line 816 — before the try
if not expression:
    return None
try:
    ...

If self.custom_object_type raises (e.g., RelatedObjectDoesNotExist after a deletion race, or during deserialization), the exception escapes the method and bubbles through __str__(). This line should be inside the try block.

Fix this →

3. No validation of expression syntax on save

An invalid Jinja2 template silently falls back to the primary field at runtime, which will confuse admins who can't tell why their expression isn't working. A clean_display_expression() on the form (or a clean() on the model) should attempt env.parse(expression) and surface a ValidationError with the Jinja2 syntax error message. This is cheap and catches the common typo case immediately.

Fix this →


Minor Issues

4. Multi-paragraph docstring violates project style (models.py:803–814)

AGENTS.md says "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." The performance note belongs in a PR description or a GitHub issue, not the code.

5. display_expression not in CustomObjectTypeImportForm (forms.py:91–101)

CSV import can't set a display expression. Whether intentional or oversight, it's worth a comment in the PR.

6. Trailing separator UX in partial expressions

The test test_expression_with_missing_field_renders_empty_string shows '{{ make }} / {{ nonexistent }}''Juniper /'. Users writing {{ make }} / {{ model }} expecting a clean render when model is blank will get a dangling separator. This is a fundamental Jinja2 behavior, so the docs/help text should warn users about it (e.g., "use {% if model %}{{ make }} / {{ model }}{% else %}{{ make }}{% endif %} for optional fields").


What's Good

  • SandboxedEnvironment is the right choice for user-supplied templates; bare Environment would be a security issue.
  • Double fallback (expression fails → primary field → id) is solid and means no object loses its display name under any error condition.
  • Test coverage hits the key cases: composite rendering, missing fields, syntax errors, empty results, and the primary-field fallback. The tests are well-structured and easy to follow.
  • Migration is clean and minimal; blank=True with no explicit default is correct for a CharField.
  • Form fieldset grouping ("Display") is a good UX improvement.
  • The <code> formatting in the detail template is a nice touch.

Summary

The PR is functionally correct and safe. Before merging I'd address items 1–3 (performance, error-handling gap, validation). Item 4 (docstring style) is a quick fix. Items 5–6 are low priority.

bctiemann and others added 3 commits June 17, 2026 11:48
…m, trailing separator docs

- Move _compile_display_template() after imports (not in the middle of them)
- Cache compiled Jinja2 templates via functools.lru_cache(maxsize=256)
  keyed by expression string; avoids N recompilations on list views
- Move custom_object_type access inside the try block so RelatedObjectDoesNotExist
  and deserialization errors also fall through silently
- Shorten _render_display_expression docstring to one line; move the
  performance/caching note to inline comments
- Add clean_display_expression() to CustomObjectTypeForm: parses the
  expression via SandboxedEnvironment().parse() and surfaces a
  ValidationError with the Jinja2 error message on syntax errors
- Add display_expression (plus verbose_name, verbose_name_plural,
  group_name) to CustomObjectTypeImportForm
- Extend help_text to warn about trailing separators when referenced
  fields are blank, with the recommended {% if %} pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alues to ''

- _render_display_expression: coerce None return from get_display_value()
  to '' so unset nullable fields render as empty string rather than 'None'
- test_trailing_separator_with_blank_optional_field: documents and tests
  the dangling-separator behaviour vs the {% if %} guard pattern
- DisplayExpressionFormValidationTestCase: 4 tests covering valid expression,
  blank expression, invalid Jinja2 syntax, and unclosed block tag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bctiemann bctiemann requested review from a team and jnovinger and removed request for a team June 17, 2026 16:16
Comment thread netbox_custom_objects/migrations/0015_customobjecttype_display_expression.py Outdated
Comment thread netbox_custom_objects/models.py Outdated
@bctiemann bctiemann requested a review from jnovinger June 22, 2026 23:02
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.

2 participants