I have a data model which has a column that depends on other column values, following the instructions in this page I've created a context-sensitive function which is used to determine the value of this particular column on creation, something like this:
def get_column_value_from_context(context):
# Instructions to produce value
return value
class MyModel(db.Model):
id = db.Column(db.Integer,
primary_key=True)
my_column = db.Column(db.String(64),
nullable=False,
default=get_column_value_from_context)
name = db.Column(db.String(32),
nullable=False,
unique=True,
index=True)
title = db.Column(db.String(128),
nullable=False)
description = db.Column(db.String(256),
nullable=False)
This approach works pretty decent, I can create rows without problems from the command line or using a script.
I've also added a ModelView
to the app using Flask-Admin:
class MyModelView(ModelView):
can_view_details = True
can_set_page_size = True
can_export = True
admin.add_view(MyModelView(MyModel, db.session))
This also works pretty decent until I click the Create button in the list view. I receive this error:
AttributeError: 'NoneType' object has no attribute 'get_current_parameters'
Because the implementation of the create_model
handler in the ModelView
is this:
def create_model(self, form):
"""
Create model from form.
:param form:
Form instance
"""
try:
model = self.model()
form.populate_obj(model)
self.session.add(model)
self._on_model_change(form, model, True)
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to create record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to create record.')
self.session.rollback()
return False
else:
self.after_model_change(form, model, True)
return model
and here there isn't a context
when the model is instantiated. So, I've created a custom view where the model instantiation in the creation handler could be redefined:
class CustomSQLAView(ModelView):
def __init__(self, *args, **kwargs):
super(CustomSQLAView, self).__init__(*args, **kwargs)
def create_model(self, form):
"""
Create model from form.
:param form:
Form instance
"""
try:
model = self.get_populated_model(form)
self.session.add(model)
self._on_model_change(form, model, True)
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to create record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to create record.')
self.session.rollback()
return False
else:
self.after_model_change(form, model, True)
return model
def get_populated_model(self, form):
model = self.model()
form.populate_obj(model)
return model
Now I can redefine the get_populated_model
method to instantiate the model in the usual way:
class MyModelView(CustomSQLAView):
can_view_details = True
can_set_page_size = True
can_export = True
def get_populated_model(self, form):
model = self.model(
name=form.name.data,
title=form.title.data,
description=form.description.data,
)
return model
Despite that this works, I suspect it breaks something. Flask-Admin has several implementation of the populate_obj
method of forms and fields, so I would like to keep everything safe.
What is the proper way to do this?
So there are several factors which come in to play at this question.
But first, a small fully functional app to illustrate:
from flask_admin.contrib.sqla import ModelView
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask import Flask
app = Flask(__name__)
db = SQLAlchemy(app)
admin = Admin(app)
app.secret_key = 'arstartartsar'
def get_column_value_from_context(context):
print('getting value from context...')
if context.isinsert:
return 'its an insert!'
else:
return 'aww, its not an insert'
class MyModel(db.Model):
id = db.Column(db.Integer, primary_key=True)
my_column = db.Column(db.String(64), nullable=False, default=get_column_value_from_context)
name = db.Column(db.String(32), nullable=False, unique=True, index=True)
class MyModelView(ModelView):
form_excluded_columns = ['my_column']
db.create_all()
admin.add_view(MyModelView(MyModel, db.session))
print('1')
x = MyModel(name='hey')
print('2')
db.session.add(x)
print('3')
db.session.commit()
print('4')
When we fire up this app, what's printed is:
1
2
3
getting value from context...
4
So, only after committing, does the get_column_value_from_context
function fire up. When, in the create view, you 'create' a new model, you don't have a context yet because you're not committing anything to the database yet. You only get the context to set the default when you're committing your instance to the database! That why, in the source code of flask admin, this happens:
if getattr(default, 'is_callable', False):
value = lambda: default.arg(None) # noqa: E731
They check if the default you specified is a function, and if so, they call it without context (because there is no context yet!).
You have several options to overcome this depending on your goals:
1) Only calculate the value of my_column
when adding it to the database:
Simply ignore the field in the form, and it will be determined when you add it to the db.
class MyModelView(ModelView):
form_excluded_columns = ['my_column']
2) Only calculate it when adding it to the db, but making it editable afterwards:
class MyModelView(ModelView):
form_edit_rules = ['name', 'my_column']
form_create_rules = ['name']
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With