Fix missing release field on admin interface

- Force SQLAlcheny relationships init before rendering the form
- Add tests covering this problem

Fixes #279

Assisted-by: Claude Code
This commit is contained in:
Jaroslav Groman 2026-06-08 14:46:06 +02:00
commit 30089575e2
2 changed files with 70 additions and 0 deletions

View file

@ -25,6 +25,7 @@ import logging
from flask import flash
from flask_admin.babel import gettext
import flask_admin
from sqlalchemy.orm import configure_mappers
from blockerbugs import app, db
from blockerbugs.models.release import Release
from blockerbugs.models.milestone import Milestone
@ -97,5 +98,10 @@ class MilestoneView(FasAuthModelView):
return True
# Force SQLAlchemy to resolve all Mapped[] forward references and back_populates linkages
# before Flask-Admin introspects models to scaffold forms. Without this, relationship
# fields (e.g. Milestone.release) are silently omitted, causing AttributeErrors on submit.
configure_mappers()
admin.add_view(MilestoneView(Milestone, db.session))
admin.add_view(ReleaseView(Release, db.session))

View file

@ -0,0 +1,64 @@
"""Tests for the admin interface"""
import pytest
from wtforms.fields.core import UnboundField
from blockerbugs.controllers.admin import admin
def _get_admin_view(model_name):
"""Look up a Flask-Admin ModelView by its model class name."""
for view in admin._views:
if hasattr(view, "model") and view.model.__name__ == model_name:
return view
raise AssertionError(f"{model_name} view not found in admin views")
def _get_form_field_names(model_name):
"""Return the set of field names that Flask-Admin scaffolds for a model."""
view = _get_admin_view(model_name)
form_class = view.scaffold_form()
return {name for name in dir(form_class) if isinstance(getattr(form_class, name), UnboundField)}
class TestAdminForms:
"""Test that Flask-Admin generates the correct form fields for each model.
Flask-Admin auto-scaffolds form fields by introspecting the SQLAlchemy model.
With SQLAlchemy 2.0's deferred relationship initialization, relationship fields
(like 'release') can be silently dropped if configure_mappers() hasn't been
called before Flask-Admin scaffolds the form. These tests guard against that
regression.
"""
@pytest.mark.parametrize(
"model_name, required_fields",
[
(
"Milestone",
{
"release",
"version",
"blocker_tracker",
"fe_tracker",
"name",
"active",
"current",
"succeeded_by",
},
),
("Release", {"number", "active"}),
],
)
def test_form_has_all_required_fields(self, model_name, required_fields):
"""The admin form must include all fields used by create_model().
Each ModelView.create_model() accesses specific form fields when creating
a new instance through the admin interface. If any are missing, form
submission will fail with an AttributeError.
"""
fields = _get_form_field_names(model_name)
missing = required_fields - fields
assert not missing, (
f"{model_name} admin form is missing fields required by create_model(): {missing}"
)