Flask-admin: New feature in details view

Created on 5 Aug 2015  路  37Comments  路  Source: flask-admin/flask-admin

I was playing around with details view in my admin panel and thought that it would be nice to have ability to group data for viewing convenience.
Is this kind of feature wanted in flask-admin? If so I could make it.

Code example:

def get_columns_grouped():
    d = OrderedDict() #using ordered dict for strict ordering
    d['Main'] = ('user', 'status', 'created_at')
    d['Private'] = ('ssn', 'dob', 'dl_state', 'dl_number', 'sex')
    d['Financial Info'] = ('account', 'routing')
    d['Billing Address'] = ('address', 'city', 'state', 'zip')
    return d

class AccountModelView(SecureModelView):
    details_grouped = True
    details_columns_grouped = get_columns_grouped()

I'm attaching a screenshot so you can better understand what I'm talking about.
screen shot 2015-08-04 at 5 37 47 pm

Versus

screen shot 2015-08-04 at 5 38 25 pm

Most helpful comment

class AccountModelView(SecureModelView):

   def get_details_grouped():
        d = {
            'main': {
                'user',
                'status',
                'created_at'
            },
            'billing_address': {
                'address',
                'city',
                'state',
                'zip'
            }
        }

        return d

details template:

    {% block details_table %}

      <table class="table">

        {% for group in admin_view.get_details_grouped() %}

          <tbody>
          <tr>
            <th colspan="2">{{ group  | capitalize }}</th>
          </tr>
          {% for i in admin_view.get_details_grouped()[group] %}
            <tr>
              <td class="field-name">
                <b>{{ i | replace("_", " ") | capitalize }}</b>
              </td>
              <td>
                {{ get_value(model, i) }}
              </td>
            </tr>
          {% endfor %}

          </tbody>
        {% endfor %}

      </table>

     {% endblock %}

Update:
Of course, if needed, use OrderedDict as OP recommends.

 d = OrderedDict() #using ordered dict for strict ordering
    d['main'] = ('user', 'status', 'created_at')
    d['billing_address'] = ('address', 'city', 'state', 'zip')
    return d

All 37 comments

If I understood what you mean,

But for edit view I think it would be better if the syntax is more like Django.

from flask_admin import Fieldset

class MyView(ModelView):
    form_columns = [
        "title",  # normal outside a fieldset
        Fieldset("Publish Info", fields=["published", "date"]),  # grouped in to a <fieldset>
        Fieldset("Author info", fields=[author", "license"], css_class="custom_class")  # with custom css class
    ]

Another thing I was thinking about was re-using render_form_fields for the details view and adding a "readonly" parameter: https://github.com/flask-admin/flask-admin/blob/master/flask_admin/templates/bootstrap3/admin/lib.html#L152

I really like the idea of using an OrderedDict to group fields. That seems really elegant.

Would be great to have something like form rendering rules - it already gives full control over form rendering and probably can be reused for read-only views.

@pawl, @mrjoes Thank you for reply. I took a quick look on form_rules (never used them before). The code is too complicated for me so it will take me some time to figure things out. I'll make a pull request as soon as I have basic stuff done so you can review it.

Field rendering rules are classes that return HTML snippet. It is similar to example @rochacbruno provided.

@mrjoes, Yeah I understand how to define rules in model view but I need to understand how it works under the hood. It's complicated but after spending some time reading source I almost got it.

Is it possible to group fields in to fieldsets using form_rules?

:+1: nice! So I think it can be easily used to customize read-only views also

@mrjoes, @pawl I managed to reuse some code from form rules but in process of coding I created additional classes like (ReadonlyFieldSet, ReadonlyField ReadonlyRuleSet), code became ugly and I will start all over. Now I have better understanding of how rules work and now I'm concerned about what's the best way to make this feature.
I see three ways:

  1. Display all form widgets with readonly attribute, which I think will involve less coding, but then the details view with rules would look completely different comparing to details view without rules (which displays table without widgets). Off course then I could make the same changes to details view without rules but is this ok? Also for this to work, form object needs to be created in details view and I don't think this makes sense.
  2. Not to display form widgets but rather display separate table for each FieldSet with text-values only. This will involve more complex coding because right now Macro class (https://github.com/flask-admin/flask-admin/blob/master/flask_admin/form/rules.py#L189) is assuming that we work with form and probably it will require some complex refactoring (so the code doesn't become ugly). I don't think I can handle this but I really want to make this feature happen so I'm relying on your feedback and advice.
  3. Not to reuse form rules. Add two variables in BaseModelView as I mentioned in my very first post.
    Any thoughts?

Are there any news about this feature?

does this implement EmbeddedDocuments ?

This can be currently implemented by using declaring a method in at the view class which returns an dict, then accessing dict via admin_view variable in the template :

class AccountModelView(SecureModelView):

   def get_details_grouped():
    d = OrderedDict() #using ordered dict for strict ordering
    d['Main'] = ('user', 'status', 'created_at')
    d['Private'] = ('ssn', 'dob', 'dl_state', 'dl_number', 'sex')
    d['Financial Info'] = ('account', 'routing')
    d['Billing Address'] = ('address', 'city', 'state', 'zip')
    return d
{{ admin_view.get_details_grouped()['Main'].user }}

@rochacbruno , why the confusion?
I wasn't trying to show a complete solution; I was just pointing out that a method in a View is accessible from the admin_view variable in Jinja2.

@macfire Can you add full example ?

@macfire

The admin_view.get_details_grouped() returns an OrderedDict its Main key returns a tuple ('user', 'status', 'created_at')

so

{{ admin_view.get_details_grouped()['Main'].user }}

Will raise AttributeError: tuple object has no attribute user

class AccountModelView(SecureModelView):

   def get_details_grouped():
        d = {
            'main': {
                'user',
                'status',
                'created_at'
            },
            'billing_address': {
                'address',
                'city',
                'state',
                'zip'
            }
        }

        return d

details template:

    {% block details_table %}

      <table class="table">

        {% for group in admin_view.get_details_grouped() %}

          <tbody>
          <tr>
            <th colspan="2">{{ group  | capitalize }}</th>
          </tr>
          {% for i in admin_view.get_details_grouped()[group] %}
            <tr>
              <td class="field-name">
                <b>{{ i | replace("_", " ") | capitalize }}</b>
              </td>
              <td>
                {{ get_value(model, i) }}
              </td>
            </tr>
          {% endfor %}

          </tbody>
        {% endfor %}

      </table>

     {% endblock %}

Update:
Of course, if needed, use OrderedDict as OP recommends.

 d = OrderedDict() #using ordered dict for strict ordering
    d['main'] = ('user', 'status', 'created_at')
    d['billing_address'] = ('address', 'city', 'state', 'zip')
    return d

For conditional grouping:

    {% block details_table %}

      {% if admin_view.get_details_grouped %}
        {% set groups = admin_view.get_details_grouped() %}

        {% for group in groups %}
          <table class="table">
            <thead>
            <tr>
              <th colspan="2">{{ group  | capitalize }}</th>
            </tr>
            </thead>
            <tbody>
            {% for i in groups[group] %}
              <tr>
                <td class="field-name">
                  <b>{{ i | replace("_", " ") | capitalize }}</b>
                </td>
                <td>
                  {{ get_value(model, i) }}
                </td>
              </tr>
            {% endfor %}
            </tbody>
          </table>
        {% endfor %}

      {% else %}

        <table class="table table-hover">
          {% for c, name in details_columns %}
            <tr>
              <td class="field-name">
                <b>{{ name | replace("_", " ") }}</b>
              </td>
              <td>
                {{ get_value(model, c) }}
              </td>
            </tr>
          {% endfor %}
        </table>

      {% endif %}
    {% endblock %}

@macfire can you also visualise EmbeddedDocuments ? I need full demo app.. please

@ufo911 , you may want to look at how the form edit pages handle EmbeddedDocuments.
Also take a look at using column formatters.

If you use a column formatter for an EmbeddedDocument, then the command in the template, {{ get_value(model, c) }}, should return the pre-formatted value.


Update:
https://blog.sneawo.com/blog/2017/02/10/flask-admin-formatters-examples/

without mods list template ignores EmbeddedDocumets. I think in your example there have to be handler for EmbeddedDocumets

Correct, the List view ignores EmbeddedDocuments by default. This is why I recommend using a custom column formatter.

Edit:
An EmbeddedDocument or ReferenceField can be rendered in a List and Detail view by including column name in the column_list variable. However, it will need a custom column formatter assigned to it in the column_formatters dict in order to render like you want.

Can you add minimal example ? Flask admin coding style is higher and difficult to read for starter or medium dev

The examples of column formatters shown on Andrey Zhukov's blog are very helpful: https://blog.sneawo.com/blog/2017/02/10/flask-admin-formatters-examples/

You will need create a formatter to handle your specific EmbeddedDocument.

Thanks I will Check it further But My goal was getting EmbeddedDocument in details view, as I guess It will not be possible by column formatters.

But My goal was getting EmbeddedDocument in details view, as I guess It will not be possible by column formatters.

The column formatters work in Detail view

@macfire the main problem I will face is that generally I always have list of EmbeddeDocuments not only one EmbeddedDocument in the Model

That's why I suggested to look at the Flask-Admin source code to see how the create/edit views handle the form rendering.

Sounds like you will need to create a formatter that will parse the EmbeddedDocument field, and add it to your dict of default formatters. I'm not sure what data-type the EmbeddedDocument is ... maybe a dict?

@macfire

class EmbeddedSubCategory(EmbeddedDocument):
    uid = StringField(max_length=120)
    title_geo = StringField(max_length=120)
    title_eng = StringField(max_length=120)
    link = StringField(max_length=120)
    position = IntField()
    is_enabled = BooleanField(default=True)

    @queryset_manager
    def live_cats(doc_cls, queryset):
        return queryset.filter(is_enabled=True)

class MainMenu(Document):
    uid = StringField(max_length=120)
    title_geo = StringField(max_length=120)
    title_eng = StringField(max_length=120)
    link = StringField(max_length=120)
    position = IntField()
    is_enabled = BooleanField()
    categories = ListField(EmbeddedDocumentField(EmbeddedSubCategory))

    @queryset_manager
    def live_menu(doc_cls, queryset):
        return queryset.filter(is_enabled=True)

So, MainMenu.categories is a ListField ... which means you will first need to test for a list object; then iterate through the columns of each EmbeddedDocument object in the list.

It would take me some time to figure out how to implement what you want to accomplish.
Hopefully, I've pointed you in the right direction with the several previous suggestions.

You may still need to add each ListField to the columns_list in order for Flask-Admin to render them in DetailsView. In which case, you could still assign a custom formatter to handle your requirements.

@macfire I have seen source code before, it was diffuicult how EmbeddedDocuments are skipped in column_list or how can I show them up. after that formatters are also a bit not too easy to do but the main problem is column_list without EmbeddedDocuments.. I am not lazy but I could not figured it out how can I show them up.. I have tried several things but did not work

    def get_list(self, *args, **kwargs):
        count, data = super().get_list(*args, **kwargs)
        for d in data:
            for cat in d.categories:
                print(cat.title_geo)
        return count, data

this prints categories objects, now it needs implementation for formatters

I got closer
https://stackoverflow.com/questions/31801770/how-to-set-the-value-of-an-extra-field-in-the-list-view

class MainMenuView(ModelView):
    def _list_name(self, context, model, name):
        names = []
        for cat in model.categories:
            names.append(cat.title_geo)
        return names

    column_formatters = {
        'example': _list_name
    }

    def get_column_names(self, only_columns, excluded_columns):
        only_columns.append('example')
        data = super().get_column_names(only_columns, excluded_columns)
        return data

    can_view_details = True
    pass

This is getting title_geo to the example column but my EmbeddeDocument Class has many filds and if I will have many documents in the categories list do i have to add all records in column_list ?

So the problem is formatting list of columns _list_names giving me, also get_column_names needs column names from _list_names
my question is, can I have global lists in MODELVIEW ? accessible from ModelView functions ?
some_list = [] to be in my ModelView
and I will extend it from _list_names and then used it in get_list_names

passing dict to column_formatters is not working, I am returning full dict from _list_names

column_formatters = {
    dict : _name_list
}

scaffold_list_columns does not implement ListField or EmbeddedDocuments, that is why in details view we can not see ListField data, may it can be implemented for details view to get ListField data ?

    def scaffold_list_columns(self):
        """
            Scaffold list columns
        """
        columns = []

        for n, f in self._get_model_fields():
            # Verify type
            field_class = type(f)

            if (field_class == mongoengine.ListField and
                    isinstance(f.field, mongoengine.EmbeddedDocumentField)):
                continue

            if field_class == mongoengine.EmbeddedDocumentField:
                continue

            if self.column_display_pk or field_class != mongoengine.ObjectIdField:
                columns.append(n)

        return columns

Correct, the List view ignores EmbeddedDocuments by default. This is why I recommend using a custom column formatter.

Edit:
An EmbeddedDocument or ReferenceField can be rendered in a List and Detail view by including column name in the column_list variable. However, it will need a custom column formatter assigned to it in the column_formatters dict in order to render like you want.

I ended up using column_formatters

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DandyDev picture DandyDev  路  7Comments

bool-dev picture bool-dev  路  13Comments

timgogochen picture timgogochen  路  7Comments

MaasErwin picture MaasErwin  路  7Comments

fwiersENO picture fwiersENO  路  6Comments