pallets-eco / flask-admin

Simple and extensible administrative interface framework for Flask
https://flask-admin.readthedocs.io
BSD 3-Clause "New" or "Revised" License
5.79k stars 1.57k forks source link

password field handling, form POST hook for data transformation, and list_formatters using jinja2.Markup #173

Closed gordol closed 11 years ago

gordol commented 11 years ago

I have a user database that stores the password as a hash. My first issue is two-fold...

First, flask-admin treats the password field as a text field. It's encrypted, and admin-only so it's not a huge concern, but is there a way to over-ride this to make it a password field? Additionally, what is the best way to hook into the form POST processing code so I can add code to hash the password before storing it? Registration is typically handled elsewhere on the site, I would just like to add new users directly as well.

I also had one more question...

I would like to markup certain fields in the model list view. However, the only way that I can figure out to do this is by using list_formatters, like so:

class SomeView(ModelView):
    list_formatters = dict(some_field = lambda c, m, p: m.Markup('<a href="https://blah.foor/'+m.some_field+'">'+m.some_field+'</a>'))

This is great, and the only problem is that I have to instantiate Markup in my Model. While it does work, I feel like there has to be a better way.

Ideas?

mrjoes commented 11 years ago

Yes, you can hook into model handling logic and hash password if necessary.

Here's how I would do it:

  1. Hide password field: http://flask-admin.readthedocs.org/en/latest/api/mod_model/#flask.ext.admin.model.BaseModelView.form_excluded_columns
  2. Contribute new password field to the form with different name:
    class MyAdmin(ModelAdmin):
        def scaffold_form(self):
            form_class = super(UserAdmin, self).scaffold_form()
            form_class.password = wtf.TextField('Password')
            return form_class
  1. Override on_model_change method, check if password field is not empty, hash the value and assign to original password field: http://flask-admin.readthedocs.org/en/latest/api/mod_model/#flask.ext.admin.model.BaseModelView.on_model_change

As far as column formatting involved - feel free to use Jinja2 API to load template and render it. Alternatively, you can import template and call a jinja2 macro, etc.

Edit: Ah, I already made a helper which calls jinja2 macro: https://github.com/mrjoes/flask-admin/blob/master/flask_admin/model/template.py

Macro will accept two parameters: model and column.

gordol commented 11 years ago

Thanks, the scaffold_form stuff helped a lot, this library is great! Thanks for the tips.

Any thoughts about a better way to tackle the issue with formatters and having to load the jinja2 Mapper in my mongoengine models? It's not really obvious at first where Mapper gets loaded from, I only figured it out though trial and error.

nMustaki commented 11 years ago

@gordol : that's fun, I am working on an artist related website too

More to the point, there is a PasswordField in the wtf library so 2. becomes

class MyAdmin(ModelAdmin):
    def scaffold_form(self):
        form_class = super(UserAdmin, self).scaffold_form()
        form_class.password = wtf.PasswordField('Password')
        return form_class
gordol commented 11 years ago

Yup, here's what I ended up with, hopefully it's a help to someone:

class UserView(ModelView):
    can_create = True 
    form_excluded_columns = ('password', 'created_on', 'updated_on', 'token', 'following')
    column_list = ('name', 'username', 'email', 'created_on', 'updated_on', 'is_admin', 'is_moderator')
    column_exclude_list = ('password', 'token')
    column_searchable_list = ('username', 'name')
    def is_accessible(self):
        try:
            profile = User.objects.get(token=session['token'])
        except:
            return False
        if profile.is_admin: return True

    def scaffold_form(self):
        form_class = super(UserView, self).scaffold_form()
        form_class.password2 = PasswordField('New Password')
        return form_class

    def on_model_change(self, form, model):
        if len(model.password2):
            model.password = generate_password_hash(form.password2.data)

Here is the column listing:

And here is the resulting form: