Skip to content

Instantly share code, notes, and snippets.

@sykire
Last active April 9, 2021 07:16
Show Gist options
  • Save sykire/398a05e4945805bc09d1 to your computer and use it in GitHub Desktop.
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 …
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))
@antoine-lizee
Copy link

antoine-lizee commented Dec 14, 2016

This is a nice gists that makes the inline form work for many-to-one relations by simply reverting the forward/reverse props accordingly. Wondering why it's not stock / contributed to flask admin directly?

@bepetersn
Copy link

bepetersn commented Aug 20, 2019

Relevant imports:

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

Also I changed this, I don't if it's right:

    if value:
            model = value
            is_created = False
        else:
            model = self.model()
            setattr(obj, name, model)
            is_created = True

        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, is_created)

And, usage has to include defining the above inline_model_form_convertor on your view (not just the inline_model)

@sykire
Copy link
Author

sykire commented Aug 22, 2019

Sorry, It's been so much time I don't remember what I've done here.

@symstu
Copy link

symstu commented Feb 28, 2021

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

@symstu
Copy link

symstu commented Feb 28, 2021

pallets-eco/flask-admin#2091
also, here is a pull request

@sykire
Copy link
Author

sykire commented Apr 9, 2021

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