Last active
April 9, 2021 07:16
-
-
Save sykire/398a05e4945805bc09d1 to your computer and use it in GitHub Desktop.
Usually in flask-admin inline-models use the same converter. You can set a different converter setting 'inline_converter' in the ModelView but this will apply the same converter for every Model in the inline_models list.So I've overridden the 'scaffold_inline_form_models' method from ModelView just to check for every inline model in the list if …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from flask_admin.form import FormOpts | |
from flask_admin.contrib.sqla.form import InlineModelFormList, \ | |
InlineModelConverter, \ | |
get_form | |
from flask_admin.contrib.sqla.tools import get_primary_key | |
from flask_admin.contrib.sqla import ModelView | |
from flask_admin.model.fields import InlineModelFormField | |
from flask_admin.model.form import InlineFormAdmin | |
from flask_admin._compat import iteritems | |
class ForwardInlineModelForm(InlineModelFormField): | |
def __init__(self, form, session, model, prop, inline_view, **kwargs): | |
""" | |
Default constructor. | |
:param form: | |
Form for the related model | |
:param session: | |
SQLAlchemy session | |
:param model: | |
Related model | |
:param prop: | |
Related property name | |
:param inline_view: | |
Inline view | |
""" | |
self.form = form | |
self.session = session | |
self.model = model | |
self.prop = prop | |
self.inline_view = inline_view | |
self._pk = get_primary_key(model) | |
# Generate inline form field | |
form_opts = FormOpts(widget_args=getattr(inline_view, 'form_widget_args', None), | |
form_rules=inline_view._form_rules) | |
super().__init__(form, self._pk, form_opts=form_opts, **kwargs) | |
def populate_obj(self, obj, name): | |
value = getattr(obj, name, None) | |
inline_form = self.form | |
if value: | |
model = value | |
else: | |
model = self.model() | |
setattr(obj, name, model) | |
for name, field in iteritems(self.form._fields): | |
if name != self._pk: | |
field.populate_obj(model, name) | |
self.inline_view.on_model_change(inline_form, model) | |
class ForwardInlineModelConverter(InlineModelConverter): | |
inline_field_list_type = ForwardInlineModelForm | |
def contribute(self, model, form_class, inline_model): | |
mapper = model._sa_class_manager.mapper | |
info = self.get_info(inline_model) | |
# Find property from target model to current model | |
target_mapper = info.model._sa_class_manager.mapper | |
forward_prop = None | |
for prop in mapper.iterate_properties: | |
if hasattr(prop, 'direction') and prop.direction.name == 'MANYTOONE': | |
if prop.mapper.class_ == target_mapper.class_: | |
forward_prop = prop | |
break | |
else: | |
raise Exception('Cannot find forward relation for model %s' % info.model) | |
reverse_prop = None | |
for prop in target_mapper.iterate_properties: | |
if hasattr(prop, 'direction') and prop.direction.name == 'ONETOMANY': | |
if prop.mapper.class_ == mapper.class_: | |
reverse_prop = prop | |
break | |
else: | |
raise Exception('Cannot find reverse relation for model %s' % info.model) | |
# Remove reverse property from the list | |
ignore = [reverse_prop.key] | |
if info.form_excluded_columns: | |
exclude = ignore + list(info.form_excluded_columns) | |
else: | |
exclude = ignore | |
# Create converter | |
converter = self.model_converter(self.session, info) | |
# Create form | |
child_form = info.get_form() | |
if child_form is None: | |
child_form = get_form(info.model, | |
converter, | |
only=info.form_columns, | |
exclude=exclude, | |
field_args=info.form_args, | |
hidden_pk=True) | |
# Post-process form | |
child_form = info.postprocess_form(child_form) | |
kwargs = dict() | |
label = self.get_label(info, forward_prop.key) | |
if label: | |
kwargs['label'] = label | |
if self.view.form_args: | |
field_args = self.view.form_args.get(forward_prop.key, {}) | |
kwargs.update(**field_args) | |
setattr(form_class, | |
forward_prop.key, | |
self.inline_field_list_type(child_form, | |
self.session, | |
info.model, | |
reverse_prop.key, | |
info, | |
**kwargs)) | |
return form_class | |
#This is to use a different inline converter, in this case a ForwardInlineModelConverter | |
class ManyToOneInlineForm(InlineFormAdmin): | |
inline_converter = ForwardInlineModelConverter | |
#This is to use to default inline converter | |
class OneToManyInlineForm(InlineFormAdmin): | |
pass | |
class PerInlineModelConverterView(ModelView): | |
def scaffold_inline_form_models(self, form_class): | |
""" | |
Contribute inline models to the form | |
:param form_class: | |
Form class | |
""" | |
inline_converter = self.inline_model_form_converter(self.session, | |
self, | |
self.model_form_converter) | |
for m in self.inline_models: | |
if hasattr(m, 'inline_converter'): | |
custom_converter = m.inline_converter(self.session, | |
self, | |
self.model_form_converter) | |
form_class = custom_converter.contribute(self.model, form_class, m) | |
else: | |
form_class = inline_converter.contribute(self.model, form_class, m) | |
return form_class | |
#EXAMPLE | |
class PackagesView(PerInlineModelConverterView): | |
inline_models = ( OneToManyInlineForm(Place), SomeModelUsingDefaultConverter, ManyToOneInlineForm(Content)) |
Hello, guys! Here is an updated variant suited with flask-admin === 1.5.7 (is latest). Moreover, i fixed a bug when you have more that one relationship to same table (in this case inline model is applied only to first found field)
For better efficient, provide for your relationships backref or back_populates attributes
from flask_admin.contrib.sqla.form import InlineModelConverter, get_form
from flask_admin import form
from flask_admin.model.fields import InlineModelFormField
from flask_admin.contrib.sqla import ModelView
from flask_admin._compat import iteritems
from flask_admin.form import FormOpts
from flask_admin.contrib.sqla.tools import get_primary_key
class InlineOneToOneField(InlineModelFormField):
def __init__(self, form, session, model, prop, inline_view, **kwargs):
self.form = form
self.session = session
self.model = model
self.prop = prop
self.inline_view = inline_view
self._pk = get_primary_key(model)
# Generate inline form field
form_opts = FormOpts(
widget_args=getattr(inline_view, 'form_widget_args', None),
form_rules=inline_view._form_rules
)
super().__init__(form, self._pk, form_opts=form_opts, **kwargs)
@staticmethod
def _looks_empty(field):
"""
Check while installed fields is not null
"""
if field is None:
return True
if isinstance(field, str) and not field:
return True
return False
def populate_obj(self, model, field_name):
inline_model = getattr(model, field_name, None)
is_created = False
form_is_empty = True
if not inline_model:
is_created = True
inline_model = self.model()
# iterate all inline form fields and fill model
for name, field in iteritems(self.form._fields):
if name != self._pk:
field.populate_obj(inline_model, name)
if form_is_empty and not self._looks_empty(field.data):
form_is_empty = False
# don't create inline model if perhaps one field was not filled
if form_is_empty:
return
# set for our model updated inline model
setattr(model, field_name, inline_model)
# save results
self.inline_view.on_model_change(self.form, model, is_created)
class OneToOneConverter(InlineModelConverter):
inline_field_list_type = InlineOneToOneField
def _calculate_mapping_key_pair(self, model, info):
mapper = model._sa_class_manager.mapper
target_mapper = info.model._sa_class_manager.mapper.base_mapper
inline_relationship = dict()
for forward_prop in mapper.iterate_properties:
if not hasattr(forward_prop, 'direction'):
continue
if forward_prop.direction.name != 'MANYTOONE':
continue
if forward_prop.mapper.class_ != target_mapper.class_:
continue
# in case when model has few relationships to target model or
# has just installed references manually. This is more quick
# solution rather than rotate yet another one loop
ref = getattr(forward_prop, 'backref')
if not ref:
ref = getattr(forward_prop, 'back_populates')
if ref:
inline_relationship[forward_prop.key] = ref
continue
# here we suppose that model has only one relationship
# to target model and prop has not any reference
for backward_prop in target_mapper.iterate_properties:
if not hasattr(backward_prop, 'direction'):
continue
if backward_prop.direction.name != 'ONETOMANY':
continue
if issubclass(model, backward_prop.mapper.class_):
inline_relationship[forward_prop.key] = backward_prop.key
break
else:
raise Exception(
'Cannot find reverse relation for model %s' % info.model)
break
if not inline_relationship:
raise Exception(
'Cannot find forward relation for model %s' % info.model)
return inline_relationship
def contribute(self, model, form_class, inline_model):
info = self.get_info(inline_model)
inline_relationships = self._calculate_mapping_key_pair(model, info)
# Remove reverse property from the list
ignore = [value for value in inline_relationships.values()]
if info.form_excluded_columns:
exclude = ignore + list(info.form_excluded_columns)
else:
exclude = ignore
# Create converter
converter = self.model_converter(self.session, info)
# Create form
child_form = info.get_form()
if child_form is None:
child_form = get_form(info.model,
converter,
base_class=info.form_base_class or form.BaseForm,
only=info.form_columns,
exclude=exclude,
field_args=info.form_args,
hidden_pk=True,
extra_fields=info.form_extra_fields)
# Post-process form
child_form = info.postprocess_form(child_form)
kwargs = dict()
# fix it if you need
# label = self.get_label(info, forward_prop_key)
# if label:
# kwargs['label'] = label
#
# if self.view.form_args:
# field_args = self.view.form_args.get(forward_prop_key, {})
# kwargs.update(**field_args)
# Contribute field
for key in inline_relationships.keys():
setattr(form_class, key, self.inline_field_list_type(
child_form,
self.session,
info.model,
inline_relationships[key],
info,
**kwargs
))
return form_class
class ExtendedModelView(ModelView):
def scaffold_inline_form_models(self, form_class):
default_converter = self.inline_model_form_converter(
self.session, self, self.model_form_converter)
for m in self.inline_models:
if not hasattr(m, 'inline_converter'):
form_class = default_converter.contribute(
self.model, form_class, m)
continue
custom_converter = m.inline_converter(
self.session, self, self.model_form_converter)
form_class = custom_converter.contribute(
self.model, form_class, m)
return form_class
pallets-eco/flask-admin#2091
also, here is a pull request
Thank you very much for doing that :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sorry, It's been so much time I don't remember what I've done here.