Customizing Fields the lazy way

154 views
Skip to first unread message

Joe Barnhart

unread,
Mar 23, 2017, 3:54:58 PM3/23/17
to web2py-users
Here is a bit of syntactic sugar I use for creating fields with less typing.  And better consistency, of course -- there always has to be a good reason for my lazy keyboard-saving shortcuts!

I have a number of fields which are for specific data types, such as phone numbers.  I gather the "common" parts of the Field definition into a dictionary:

phone_fld = {'requires':IS_EMPTY_OR(IS_PHONE()),
             
'widget':lambda fld,val: SQLFORM.widgets.string.widget(fld,val,_type='tel',_class='form-control')}

When defining the field in a table, I invoke the dictionary at the end of the Field definition:

Field("homephone","string",length=20,label=T("Home phone"),**phone_fld),
Field("workphone","string",length=20,label=T("Work phone"),**phone_fld),
Field("cellphone","string",length=20,label=T("Cell phone"),**phone_fld),

Now the field is created exactly as I want.  I keep a list of these "helpers" in a module which I can call out when creating tables.  It really saves the typing and allow me to control how my fields are rendered on one place instead of scattered among all the tables.

-- Joe

Anthony

unread,
Mar 23, 2017, 7:19:05 PM3/23/17
to web2py-users
Note, you might as well also add 'type': 'string' to your dictionary, and maybe 'length': 20. You can also give yourself some flexibility by creating a function:

def phone_field(name, **kwargs):
    defaults
= {'type': 'string',
               
'length': 20,

               
'requires': IS_EMPTY_OR(IS_PHONE()),
               
'widget': lambda fld,val: SQLFORM.widgets.string.widget(
                    fld
, val, _type='tel', _class='form-control')}

    defaults
.update(**kwargs)
   
return Field(name, **defaults)

Anthony

Joe Barnhart

unread,
Mar 26, 2017, 6:34:45 AM3/26/17
to web2py-users
That's kinda clever.  I may think of that!

-- Joe

Richard Vézina

unread,
Mar 15, 2018, 5:48:06 PM3/15/18
to web2py-users
I wonder if we could we also make field lazy that way in context of custom form...

I find some issue with some of my custom form where I use Field() to define my field that will be use... This form is also dynamic, I manipulate field visibility and lazy_option_widget() to make some field depend of other field... The things is with data build up... requires=IS_EMPTY_OR(IS_IN_DB(_set_, 'table.id', ...)), is loading at first (on form load) all the records almost everything as I can't specify the id which will be selected in the parent field the one controlling the lazy_option controlled field (hope I am understandable)... Multiply this by 3-4 time for each lazy_option controlled field and you ends up pulling multiple time the same records...

I would be interred in making the IS_IN_DB() triggered select occuring only when options needed to be displayed... I guess this is a flaw in the way lazy_option_widget() is implemented in the first place though...

Richard

--
Resources:
- http://web2py.com
- http://web2py.com/book (Documentation)
- http://github.com/web2py/web2py (Source code)
- https://code.google.com/p/web2py/issues/list (Report Issues)
---
You received this message because you are subscribed to the Google Groups "web2py-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to web2py+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Richard Vézina

unread,
Mar 15, 2018, 7:20:30 PM3/15/18
to web2py-users
I found a pretty unclean workaround...

You have to set the widget like this :

db.tablename.REFERENCEDTABLEOFCHILDFIELD.widget = \
        lazy_options_widget(on_key='no_table_PARENTFIELDNAME__selected',
                            off_key='PARENTFIELDNAME__unselected',
                            where=lambda PARENTFIELDNAME:
                                    (some where clause),
                            trigger=record_row.get('PARENTFIELDNAME', None),
                            orderby=db.REFERENCEDTABLEOFCHILDFIELD.id,
                            suggest_widget=False,
                            widget_chained=True,
                            row=record_row,
                            field_requires=db.tablename.REFERENCEDTABLEOFCHILDFIELD.requires  # We pass the original field requires that we want to be execute in context to limit as much as possible the number of records that will be pulled out the database and only when the parent field will be updated...
                            #user_signature=True,
                            # If you want to process ajax requests at the time of the object construction
                            # (not at the form rendered), specify your target field in the following:
                            )
    db.tablename.REFERENCEDTABLEOFCHILDFIELD.requires = IS_IN_SET([1, 2, 3])  # Here redefine the child field or lazy option controlled field with dummy not computer intensive requires that will be never displayed


NOTE: I have a customized version of lazy_option_widget (NOT THIS ONE : https://github.com/scubism/sqlabs/blob/master/modules/plugin_lazy_options_widget.py) that support reference field... So it might not make more sens without the code so here it is...

# -*- coding: utf-8 -*-
# This plugins is licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
# Authors: Kenji Hosoda <hos...@s-cubism.jp>
# Support for reference field : Richard Vézina

from gluon import *


class lazy_options_widget(SQLFORM.widgets.options):

    def __init__(self,
                 on_key,
                 off_key,
                 where,
                 trigger=None,  # Rename this attribute
                 suggest_widget=True,  # In case you don't want to use plugin_suggest_widget, set suggest_widget to
                                       # False. In this case, this piece of js with be including :
                                       # $("select[name=<CONDITIONAL_FIELD_NAME>]").change(function() {
                                       #     var val = $(this).children(":selected").attr("value");
                                       #     $(this).trigger($(this).attr("id") + "__selected", [val]);
                                       #     });
                                       # Where <CONDITIONAL_FIELD_NAME> will be replaced with the field name of the
                                       # conditional field name.
                 widget_chained=False,  # In case the widget field is also a conditional field for another widget field
                                        # you need to trigger event like "WIDGET_FIELD_NAME__selected" when the widget
                                        # field option is finally selected
                 widget_trigger_event_js=None,  # If you need to trigger something when the widget field option is
                                                # finally selected you can pass a piece of js here that will be injected
                                                # into the form when the conditional field trigger event "__selected"
                 default='---',
                 keyword='_lazy_options_%(fieldname)s',
                 orderby=None,
                 user_signature=False,
                 hmac_key=None,
                 row=None,  # In order to set values and filtered drop down appropriately based on values of
                            # conditional and widget field when the form is populated. Since you can't get row like this
                            # Field(..., widget=lambda field, value, row: ...
                            # When form is populated (update form) you need to define a row object base on the id of
                            # the record like this :
                            # row = db(db.table.id = request.vars.id).select(db.table.ALL).first()
                            # and pass it to lazy_option_widget...
                 field_requires=None,  # Better to set the field requires here to get it evaluated in a lazy manner
                 ):
        self.on_key, self.off_key, self.where = (
            on_key, off_key, where
        )
        self.field_requires = field_requires
        self.trigger, self.default, self.keyword, self.orderby = (
            trigger, default, keyword, orderby,
        )
        self.user_signature, self.hmac_key = user_signature, hmac_key

        self.row = row

        self.suggest_widget, self.widget_trigger_event_js, self.widget_chained = (suggest_widget,
                                                                                  widget_trigger_event_js,
                                                                                  widget_chained)

        if self.suggest_widget == True:
            self.suggest_widget = 'true'
        else:
            self.suggest_widget = 'false'

        # if field:
        #     self.process_now(field)

    def _get_select_el(self, trigger, value=None):
        trigger_event_selected = SCRIPT('''$(function() {
            $("#%(aux_tag_id)s").change(function() {
                var val = $(this).children(":selected").attr("value");
                $("#%(tag_id)s").trigger("%(tag_id)s__selected", [val]);
                });
            });''' % {'aux_tag_id': self._el_id + '__aux', 'tag_id': self._el_id})
        widget_trigger_event_js = SCRIPT(self.widget_trigger_event_js)
        if trigger:
            self._require.orderby = self.orderby or self._require.orderby
            self._require.dbset = self._require.dbset(self.where(trigger))
            options = self._require.options()
            opts = [OPTION(v, _value=k) for (k, v) in options]
            # multiple = {}
            # if self._require.multiple is True:
            #     multiple['_multiple'] = 'multiple'
            return DIV(SELECT(_id='%s__aux' % self._el_id,
                              value=value,
                              _onchange='jQuery("#%s").val(jQuery(this).val());' % self._hidden_el_id,
                              *opts,
                              **self.multiple),
                       trigger_event_selected if self.widget_chained is True else '',
                       widget_trigger_event_js if self.widget_trigger_event_js is not None else '')
        else:
            return self.default

    def _pre_process(self, field):
        self._keyword = self.keyword % dict(fieldname=field.name)
        self._el_id = '%s_%s' % (field._tablename, field.name)
        self._disp_el_id = '%s__display' % self._el_id
        self._hidden_el_id = '%s__hidden' % self._el_id

        requires = field.requires
        if self.field_requires:
            requires = self.field_requires

        if isinstance(requires, IS_EMPTY_OR):
            requires = requires.other
        if not isinstance(requires, (list, tuple)):
            requires = [requires]
        if requires:
            if hasattr(requires[0], 'options'):
                self._require = requires[0]
            else:
                raise SyntaxError('widget cannot determine options of %s' % field)
        else:
            self._require = []
        self.multiple = {}
        if self._require.multiple is True:
            self.multiple['_multiple'] = 'multiple'

    def process_now(self, field):
        if not hasattr(self, '_keyword'):
            self._pre_process(field)

        if self._keyword in current.request.vars:
            if self.user_signature:
                if not URL.verify(current.request, user_signature=self.user_signature, hmac_key=self.hmac_key):
                    raise HTTP(400)

            trigger = current.request.vars[self._keyword]
            raise HTTP(200, self._get_select_el(trigger))
        return self

    def __call__(self, field, value, **attributes):
        self._pre_process(field)

        request = current.request
        if hasattr(request, 'application'):
            self.url = URL(r=request, args=request.args,
                           user_signature=self.user_signature, hmac_key=self.hmac_key)
            self.process_now(field)
        else:
            self.url = request

        script_el = SCRIPT("""
jQuery(document).ready(function() {
    jQuery("body").on("%(on_key)s", function(e, val) {
        jQuery("#%(disp_el_id)s").html("%(default)s");
        //jQuery("#%(hidden_el_id)s").val("");
        jQuery("#%(hidden_el_id)s option:selected").prop("selected", false);
        var query = {}
        query["%(keyword)s"] = val;
        jQuery.ajax({type: "POST", url: "%(url)s", data: query,
            success: function(html) {
              jQuery("#%(disp_el_id)s").html(html);
        }});

    });
    jQuery("body").on("%(off_key)s", function(e) {
        jQuery("#%(disp_el_id)s").html("%(default)s");
        //jQuery("#%(hidden_el_id)s").val("");
        jQuery("#%(hidden_el_id)s option:selected").prop("selected", false);
    });
    var suggest_widget = '%(suggest_widget)s'
    if (suggest_widget == 'false') {
        $("select#%(conditional_field_name)s").change(function() {
            var val = $(this).children(":selected").attr("value");
            $(this).trigger($(this).attr("id") + "__selected", [val]);
            });
        }

});""" % dict(on_key=self.on_key,
              off_key=self.off_key,
              disp_el_id=self._disp_el_id,
              hidden_el_id=self._hidden_el_id,
              default=self.default,
              keyword=self._keyword,
              url=self.url,
              suggest_widget=self.suggest_widget,
              conditional_field_name=self.on_key[0:-10]
              ))

        if value and self.row and current.request.vars.keyword is None:
            el = DIV(script_el,
                     SPAN(self._get_select_el(trigger=self.row[self.off_key[0:-12]], value=value),
                          _id=self._disp_el_id),
                     SELECT(*[OPTION(v, _value=k) for (k, v) in self.field_requires],
                            value=value,
                            _name=field.name,
                            _id=self._hidden_el_id,
                            _style='display: none;',
                            **self.multiple),
                     _id=self._el_id)
        else:
            # select_el = self._get_select_el(self.trigger, value) if self.trigger else None
            select_el = None  # This is just a test might prevent update form to display selected value properly
            el = DIV(script_el,
                     SPAN(select_el or self.default, _id=self._disp_el_id),
                     SELECT(*[OPTION(v, _value=k) for (k, v) in field.requires.options()],
                            value=value,
                            _name=field.name,
                            _id=self._hidden_el_id,
                            _style='display: none;',
                            **self.multiple),
                     _id=self._el_id)
        return el

Joe Barnhart

unread,
Apr 16, 2018, 12:37:18 AM4/16/18
to web...@googlegroups.com
Actually, I borrowed Anthony's excellent idea and made a "factory" class for my field definitions.

First I defined a factory class:

class Field_Factory(object):
    from gluon import Field
    @staticmethod
    def new(**kwargs):
        default = dict(**kwargs)
        def inner(name, **kwargs):
            args = dict(default)
            args.update(**kwargs)
            if args.pop('hidden',False):
                args.update(dict(readable=False,writable=False))
            req = args.get('requires', None)
            if req and (args.get('represent',None)=='formatter'):
                args['represent'] = req.formatter
            if args.pop('optional',False) and req:
                args['requires'] = IS_EMPTY_OR(req)
            rend = args.pop('render',None)
            rtn = Field(name, **args)
            if rend:
                rtn.render = rend
            return rtn
        return inner

factory = Field_Factory()

Then I used it to create a bunch of field generators:

phone_field = factory.new(
    type='string', length=20,
    requires=IS_PHONE(), optional=True,
    widget=lambda fld,val: SQLFORM.widgets.string.widget(
        fld, val, _type='tel', _class='form-control')
)

email_field = factory.new(
    type='string', length=50,
    requires=IS_EMAIL(), optional=True,
    widget=lambda fld,val: SQLFORM.widgets.string.widget(
        fld, val, _type='email', _class='form-control')
)

date_field = factory.new(
    type='date',
    requires=IS_DATE(format='%m-%d-%Y'), optional=True,
    represent=lambda v,r: v.strftime('%m-%d-%Y'),
    widget=lambda fld,val:SQLFORM.widgets.date.widget(
        fld, val, _class="date form-control")
)

datetime_field = factory.new(
    type='datetime',
    requires=IS_DATETIME(format='%m-%d-%Y %I:%M:%S %p'), optional=True,
    represent=lambda v,r: v.strftime('%m-%d-%Y %I:%M:%S %p'),
    widget=lambda fld,val: SQLFORM.widgets.datetime.widget(
        fld, val, _class="datetime form-control")
)

zipcode_field = factory.new(
    type='string', length=10,
    requires=IS_ZIPCODE(),
    widget=lambda fld,val: SQLFORM.widgets.string.widget(
        fld, val, _type="zip", _class='zipcode form-control')
)

Finally, when I use the field generators in any table definitions, I can further customize them and the changes are passed through.  

define_table('joes_table',
   . . .
   date_field("birth", label=T("Birth date")),
   phone_field('homephone', label=T("Home phone")),
   phone_field('workphone', label=T("Work phone")),
   phone_field('cellphone', label=T("Cell phone")),
   . . .



It all works really well and gives me the single point of control I want.  It requires no changes in web2py and works with current, past, and future versions.  And the lazy programmer in me marvels at all the code I don't have to type.

-- Joe

Richard Vézina

unread,
Apr 16, 2018, 10:57:41 AM4/16/18
to web2py-users
Thanks Joe,

I will have a better read... I had fix my issue, my customization of the plugin introduce a couple of problem... I ends up refactoring the and only load data when all the controlled field has to be updated "once". It fix 80% of the loading time issue the rest of the issue is user related because they don't actualize record status.

The workaround above wasn't really necessary.

Richard

    . . .


It all works really well and gives me the single point of control I want.  It requires no changes in web2py and works with current, past, and future versions.  And the lazy programmer in me marvels at all the code I don't have to type.

-- Joe

On Thursday, March 23, 2017 at 12:54:58 PM UTC-7, Joe Barnhart wrote:
Here is a bit of syntactic sugar I use for creating fields with less typing.  And better consistency, of course -- there always has to be a good reason for my lazy keyboard-saving shortcuts!

I have a number of fields which are for specific data types, such as phone numbers.  I gather the "common" parts of the Field definition into a dictionary:

phone_fld = {'requires':IS_EMPTY_OR(IS_PHONE()),
             
'widget':lambda fld,val: SQLFORM.widgets.string.widget(fld,val,_type='tel',_class='form-control')}

When defining the field in a table, I invoke the dictionary at the end of the Field definition:

Field("homephone","string",length=20,label=T("Home phone"),**phone_fld),
Field("workphone","string",length=20,label=T("Work phone"),**phone_fld),
Field("cellphone","string",length=20,label=T("Cell phone"),**phone_fld),

Now the field is created exactly as I want.  I keep a list of these "helpers" in a module which I can call out when creating tables.  It really saves the typing and allow me to control how my fields are rendered on one place instead of scattered among all the tables.

-- Joe

Anthony

unread,
Apr 16, 2018, 11:03:25 AM4/16/18
to web2py-users
Looks good. A few suggestions:
  • There is no benefit to using a class with a single static method here -- just extract the new() method and make it a standalone function (maybe call it field_factory).
  • If you rename kwargs in the new() function to something else (e.g., fargs), then there is no need to make a copy of it -- just refer to it directly in the inner function. So, you can drop the first line of both new() and inner().
  • The .get() method already returns None if the key does not exist, so no need to add False or None as the second argument.
Anthony
Reply all
Reply to author
Forward
0 new messages