Search code examples
pythonflaskflask-sqlalchemycolor-pickerflask-admin

Flask Admin need HTML5 color picker field


I'm using Flask-Admin to create views of my database tables. One of our tables has a "color" column and it's "editable" meaning if you click on it, you can edit the text directly and it'll update the database with AJAX. It's a text field and I'm expecting hex colors like "#3CA4FF".

I want this "editable" color column to have an HTML5 color picker widget to aid in color selection (i.e. <input type="color">).

generic color picker

Here's my Python code for:

  1. The SQLAlchemy table model class
  2. Custom ColorField wtforms field
  3. The Flask-Admin model view class

The following works fine to add my custom ColorField to the "edit" form, but what I really want is to edit it using the column_editable_list option.

from flask_admin.contrib.sqla import ModelView
from wtforms.widgets.html5 import ColorInput
from wtforms.widgets import TextInput
from wtforms import StringField
from wtforms.fields import TextAreaField

from app import db


class UnitType(db.Model):
    """Create a SQLAlchemy model for the our manufactured units"""

    __tablename__ = "unit_types"

    id = db.Column(INTEGER, primary_key=True)
    unit = db.Column(TEXT, unique=True, nullable=False)
    color = db.Column(TEXT, nullable=True)


class ColorField(TextAreaField):
    """Create special ColorField for the color picker"""

    # Set the widget for this custom field
    widget = ColorInput()


class UnitModelView(ModelView):
    """The Flask-Admin view for the UnitModel model class"""
    
    # List of columns to display in the view
    column_list = (
        "unit",
        "color",
    )

    # List of columns that are immediately "editable"
    # without clicking "edit" first (super-nice feature)
    column_editable_list = (
        "unit",
        # If I uncomment the following, I get an error...
        # "color",
    )

    # Specify my own custom color picker form field type
    form_overrides = {
        'color': ColorField
    }

    # form_args = {
    #     'color': {
    #         'type': "color",
    #     },
    # }

    # form_widget_args = {
    #     'color': {
    #         'type': "color",
    #     },
    # }

When I add the "color" field to the column_editable_list I get the following error:

Exception: Unsupported field type: <class 'app.admin.admin.ColorField'>

I'm not sure what to try next...


Solution

  • After much trial-and-error, I've at least figured out how to add a custom select2 dropdown menu, which displays the actual color chosen to the left of the hexadecimal color value.

    First create a custom widget so we don't get the following error:

    Exception: Unsupported field type: <class 'flask_admin.model.fields.AjaxSelectField'>
    

    Here's the custom widget:

    from flask_admin.contrib.sqla.ajax import QueryAjaxModelLoader
    from flask_admin.model.widgets import XEditableWidget
    from wtforms.widgets import html_params
    from flask_admin.helpers import get_url
    from flask_admin.babel import gettext
    from flask_admin._backwards import Markup
    from jinja2 import escape
    
    
    class CustomWidget(XEditableWidget):
        """WTForms widget that provides in-line editing for the list view"""
    
        def get_kwargs(self, field, kwargs):
            """Return extra kwargs based on the field type"""
    
            if field.type == "ColorField":
                # A select2 list of pre-defined colors, formatted to display the actual color
                kwargs["data-role"] = "x-editable-color"
                kwargs["data-type"] = "select2"
            else:
                super().get_kwargs(field, kwargs)
    
            return kwargs
    

    Then override the get_list_form() method in your model view, to use your CustomWidget.

    from flask_admin.contrib.sqla import ModelView
    
    
    class MyModelView(ModelView):
        """
        Customized model view for Flask-Admin page (for database tables)
        https://flask-admin.readthedocs.io/en/latest/introduction/#
        """
    
        # Custom templates to include custom JavaScript and override the {% block tail %}
        list_template = 'admin/list_custom.html'
    
        can_create = True
        can_edit = True
    
        def get_list_form(self):
            """Override this function and supply my own CustomWidget with AJAX 
            for lazy-loading dropdown options"""
    
            if self.form_args:
                # get only validators, other form_args can break FieldList wrapper
                validators = dict(
                    (key, {'validators': value["validators"]})
                    for key, value in iteritems(self.form_args)
                    if value.get("validators")
                )
            else:
                validators = None
    
            # Here's where I supply my custom widget!
            return self.scaffold_list_form(validators=validators, widget=CustomWidget())
    

    Here's my list_custom.html template, for overriding the {% block tail %} with my own flask_admin_form.js script for my custom widget.

    {% extends 'admin/model/list.html' %}
    
    {% block tail %}
        {% if filter_groups %}
          <div id="filter-groups-data" style="display:none;">{{ filter_groups|tojson|safe }}</div>
          <div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div>
        {% endif %}
    
        <script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js', v='1.3.22') }}"></script>
        {% if editable_columns %}
          <script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap3-editable.min.js', v='1.5.1.1') }}"></script>
        {% endif %}
    
        <!-- Custom JavaScript -->
        <script src="{{ url_for('static', filename='js/flask_admin_form.js') }}"></script>
    
        <script src="{{ admin_static.url(filename='admin/js/filters.js', v='1.0.0') }}"></script>
    
        {{ actionlib.script(_gettext('Please select at least one record.'),
                            actions,
                            actions_confirmation) }}
    {% endblock %}
    

    Finally, in the flask_admin_form.js (my replacement for the default filename='admin/js/form.js'), I add the following case for x-editable-color (my custom role). I didn't include the whole JavaScript file here for brevity. You can find it here in the source code.

    Notice the select2 I added to the $el.editable( options:

    ...
          switch (name) {
            case "select2-ajax":
              processAjaxWidget($el, name);
              return true;
    
            case "x-editable":
              $el.editable({
                params: overrideXeditableParams,
                combodate: {
                  // prevent minutes from showing in 5 minute increments
                  minuteStep: 1,
                  maxYear: 2030,
                },
              });
              return true;
    
            case "x-editable-color":
              // Nice pastel colors
              var colorsource = [
                {value: "#f7f7f7", text: "#f7f7f7"},
                {value: "#292b2c", text: "#292b2c"},
                {value: "#87CEEB", text: "#87CEEB"},
                {value: "#32CD32", text: "#32CD32"},
                {value: "#BA55D3", text: "#BA55D3"},
                {value: "#F08080", text: "#F08080"},
                {value: "#4682B4", text: "#4682B4"},
                {value: "#9ACD32", text: "#9ACD32"},
                {value: "#40E0D0", text: "#40E0D0"},
                {value: "#FF69B4", text: "#FF69B4"},
                {value: "#F0E68C", text: "#F0E68C"},
                {value: "#D2B48C", text: "#D2B48C"},
                {value: "#8FBC8B", text: "#8FBC8B"},
                {value: "#6495ED", text: "#6495ED"},
                {value: "#DDA0DD", text: "#DDA0DD"},
                {value: "#5F9EA0", text: "#5F9EA0"},
                {value: "#FFDAB9", text: "#FFDAB9"},
                {value: "#FFA07A", text: "#FFA07A"},
                {value: "#fce38a", text: "#fce38a"},
                {value: "#eaffd0", text: "#eaffd0"},
                {value: "#95e1d3", text: "#95e1d3"},
              ]
    
              var optsSelect2 = {
                placeholder: $el.attr("data-value"),
                minimumInputLength: $el.attr("data-minimum-input-length"),
                allowClear: $el.attr("data-allow-blank") == "1",
                multiple: $el.attr("data-multiple") == "1",
                // Display the actual color next to the hex value of the color
                formatResult: function (item) { return "<span style='padding-left: 20px; border-left: 20px solid " + item.text + "'>" + item.text + "</span>"; },
                formatSelection: function (item){ return item.text; },
              };
              
              $el.editable({
                // Source data for list of colors, as array of objects
                source: colorsource,
                params: overrideXeditableParams,
                // select2-specific options
                select2: optsSelect2,
              });
    
              return true;
    ...