From 14e58276cf4cba3abfc5066b536b869b1c5914a7 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 10 Nov 2017 18:41:41 -0600 Subject: [PATCH] form handling of f.options, f.regex, f.listable, f.rearchable --- applications/admin/static/js/web2py.js | 58 ++++++++++++++++------- applications/examples/static/js/web2py.js | 58 ++++++++++++++++------- gluon/dal.py | 23 +++++---- gluon/packages/dal | 2 +- gluon/sqlhtml.py | 8 ++-- 5 files changed, 103 insertions(+), 46 deletions(-) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 313d00bc..7131727d 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -12,6 +12,10 @@ $.error('web2py.js has already been loaded!'); } + var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined'; + var animateIn = 'fadeIn'; + // var animateIn = 'slideDown'; + String.prototype.reverse = function () { return this.split('').reverse().join(''); }; @@ -178,7 +182,7 @@ }, /* manage errors in forms */ manage_errors: function (target) { - $('div.error', target).hide().slideDown('slow'); + $('div.error', target).hide()[animateIn]('slow'); }, after_ajax: function (xhr) { /* called whenever an ajax request completes */ @@ -263,13 +267,17 @@ } }); /* help preventing double form submission for normal form (not LOADed) */ - $(doc).on('submit', 'form', function () { - var submit_button = $(this).find(web2py.formInputClickSelector); - web2py.disableElement(submit_button); + $(doc).on('submit', 'form', function (e) { + var submit_buttons = $(this).find(web2py.formInputClickSelector); + submit_buttons.each(function() { + web2py.disableElement($(this)); + }) /* safeguard in case the form doesn't trigger a refresh, see https://github.com/web2py/web2py/issues/1100 */ setTimeout(function () { - web2py.enableElement(submit_button); + submit_buttons.each(function() { + web2py.enableElement($(this)); + }); }, 5000); }); doc.ajaxSuccess(function (e, xhr) { @@ -320,7 +328,15 @@ form.submit(function (e) { web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.hide_flash(); - web2py.ajax_page('post', url, form.serialize(), target, form); + + var formData; + if (FORMDATA_IS_SUPPORTED) { + formData = new FormData(form[0]); // Allows file uploads. + } else { + formData = form.serialize(); // Fallback for older browsers. + } + web2py.ajax_page('post', url, formData, target, form); + e.preventDefault(); }); form.on('click', web2py.formInputClickSelector, function (e) { @@ -339,11 +355,18 @@ if (web2py.isUndefined(element)) element = $(document); /* if target is not there, fill it with something that there isn't in the page*/ if (web2py.isUndefined(target) || target === '') target = 'w2p_none'; + + /* processData and contentType must be set to false when passing a FormData + object to jQuery.ajax. */ + var isFormData = Object.prototype.toString.call(data) === '[object FormData]'; + var contentType = isFormData ? false : 'application/x-www-form-urlencoded; charset=UTF-8'; if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */ $.ajax({ 'type': method, 'url': action, 'data': data, + 'processData': !isFormData, + 'contentType': contentType, 'beforeSend': function (xhr, settings) { xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-element', target); @@ -595,7 +618,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ')[animateIn](); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -609,7 +632,7 @@ for (var k = 0; k < triggers[id].length; k++) { var dep = $('#' + triggers[id][k], target); var tr = $('#' + triggers[id][k] + '__row', target); - if (t.is(dep.attr('data-show-if'))) tr.slideDown(); + if (t.is(dep.attr('data-show-if'))) tr[animateIn](); else tr.hide(); } }; @@ -699,8 +722,9 @@ }); }, /* Disables form elements: + - Does not disable elements with 'data-w2p_disable' attribute - Caches element value in 'w2p_enable_with' data store - - Replaces element text with value of 'data-disable-with' attribute + - Replaces element text with value of 'data-w2p_disable_with' attribute - Sets disabled property to true */ disableFormElements: function (form) { @@ -712,13 +736,15 @@ if (!web2py.isUndefined(disable)) { return false; } - if (web2py.isUndefined(disable_with)) { - element.data('w2p_disable_with', element[method]()); + if (!element.is(':file')) { // Altering file input values is not allowed. + if (web2py.isUndefined(disable_with)) { + element.data('w2p_disable_with', element[method]()); + } + if (web2py.isUndefined(element.data('w2p_enable_with'))) { + element.data('w2p_enable_with', element[method]()); + } + element[method](element.data('w2p_disable_with')); } - if (web2py.isUndefined(element.data('w2p_enable_with'))) { - element.data('w2p_enable_with', element[method]()); - } - element[method](element.data('w2p_disable_with')); element.prop('disabled', true); }); }, @@ -799,4 +825,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ \ No newline at end of file +/* compatibility code - end*/ diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index 313d00bc..7131727d 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -12,6 +12,10 @@ $.error('web2py.js has already been loaded!'); } + var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined'; + var animateIn = 'fadeIn'; + // var animateIn = 'slideDown'; + String.prototype.reverse = function () { return this.split('').reverse().join(''); }; @@ -178,7 +182,7 @@ }, /* manage errors in forms */ manage_errors: function (target) { - $('div.error', target).hide().slideDown('slow'); + $('div.error', target).hide()[animateIn]('slow'); }, after_ajax: function (xhr) { /* called whenever an ajax request completes */ @@ -263,13 +267,17 @@ } }); /* help preventing double form submission for normal form (not LOADed) */ - $(doc).on('submit', 'form', function () { - var submit_button = $(this).find(web2py.formInputClickSelector); - web2py.disableElement(submit_button); + $(doc).on('submit', 'form', function (e) { + var submit_buttons = $(this).find(web2py.formInputClickSelector); + submit_buttons.each(function() { + web2py.disableElement($(this)); + }) /* safeguard in case the form doesn't trigger a refresh, see https://github.com/web2py/web2py/issues/1100 */ setTimeout(function () { - web2py.enableElement(submit_button); + submit_buttons.each(function() { + web2py.enableElement($(this)); + }); }, 5000); }); doc.ajaxSuccess(function (e, xhr) { @@ -320,7 +328,15 @@ form.submit(function (e) { web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.hide_flash(); - web2py.ajax_page('post', url, form.serialize(), target, form); + + var formData; + if (FORMDATA_IS_SUPPORTED) { + formData = new FormData(form[0]); // Allows file uploads. + } else { + formData = form.serialize(); // Fallback for older browsers. + } + web2py.ajax_page('post', url, formData, target, form); + e.preventDefault(); }); form.on('click', web2py.formInputClickSelector, function (e) { @@ -339,11 +355,18 @@ if (web2py.isUndefined(element)) element = $(document); /* if target is not there, fill it with something that there isn't in the page*/ if (web2py.isUndefined(target) || target === '') target = 'w2p_none'; + + /* processData and contentType must be set to false when passing a FormData + object to jQuery.ajax. */ + var isFormData = Object.prototype.toString.call(data) === '[object FormData]'; + var contentType = isFormData ? false : 'application/x-www-form-urlencoded; charset=UTF-8'; if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */ $.ajax({ 'type': method, 'url': action, 'data': data, + 'processData': !isFormData, + 'contentType': contentType, 'beforeSend': function (xhr, settings) { xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-element', target); @@ -595,7 +618,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ')[animateIn](); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -609,7 +632,7 @@ for (var k = 0; k < triggers[id].length; k++) { var dep = $('#' + triggers[id][k], target); var tr = $('#' + triggers[id][k] + '__row', target); - if (t.is(dep.attr('data-show-if'))) tr.slideDown(); + if (t.is(dep.attr('data-show-if'))) tr[animateIn](); else tr.hide(); } }; @@ -699,8 +722,9 @@ }); }, /* Disables form elements: + - Does not disable elements with 'data-w2p_disable' attribute - Caches element value in 'w2p_enable_with' data store - - Replaces element text with value of 'data-disable-with' attribute + - Replaces element text with value of 'data-w2p_disable_with' attribute - Sets disabled property to true */ disableFormElements: function (form) { @@ -712,13 +736,15 @@ if (!web2py.isUndefined(disable)) { return false; } - if (web2py.isUndefined(disable_with)) { - element.data('w2p_disable_with', element[method]()); + if (!element.is(':file')) { // Altering file input values is not allowed. + if (web2py.isUndefined(disable_with)) { + element.data('w2p_disable_with', element[method]()); + } + if (web2py.isUndefined(element.data('w2p_enable_with'))) { + element.data('w2p_enable_with', element[method]()); + } + element[method](element.data('w2p_disable_with')); } - if (web2py.isUndefined(element.data('w2p_enable_with'))) { - element.data('w2p_enable_with', element[method]()); - } - element[method](element.data('w2p_disable_with')); element.prop('disabled', true); }); }, @@ -799,4 +825,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ \ No newline at end of file +/* compatibility code - end*/ diff --git a/gluon/dal.py b/gluon/dal.py index 215fd4e1..0ebfd6a5 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -50,6 +50,10 @@ def _default_validators(db, field): requires.append(validators.IS_TIME()) elif field_type == 'datetime': requires.append(validators.IS_DATETIME()) + elif field.options is not None: + requires = IS_IN_SET(field.options, multiple=field_type.startswith('list:')) + elif field.regex and not requires: + requires = IS_REGEX(regex) elif db and field_type.startswith('reference') and \ field_type.find('.') < 0 and \ field_type[10:] in db.tables: @@ -76,16 +80,17 @@ def _default_validators(db, field): requires._and = validators.IS_NOT_IN_DB(db, field) if not field.notnull: requires = validators.IS_EMPTY_OR(requires) - return requires + return requires # does not get here for reference and list:reference - if field.unique: - requires.insert(0, validators.IS_NOT_IN_DB(db, field)) - excluded_fields = ['string', 'upload', 'text', 'password', 'boolean'] - if (field.notnull or field.unique) and field_type not in excluded_fields: - requires.insert(0, validators.IS_NOT_EMPTY()) - elif not field.notnull and not field.unique and requires: - requires[0] = \ - validators.IS_EMPTY_OR(requires[0], null='' if field.type in ('string', 'text', 'password') else None) + if isinstance(requires, list): + if field.unique: + requires.insert(0, validators.IS_NOT_IN_DB(db, field)) + excluded_fields = ['string', 'upload', 'text', 'password', 'boolean'] + if (field.notnull or field.unique) and field_type not in excluded_fields: + requires.insert(0, validators.IS_NOT_EMPTY()) + elif not field.notnull and not field.unique and requires: + null = null='' if field.type in ('string', 'text', 'password') else None + requires[0] = validators.IS_EMPTY_OR(requires[0], null=null) return requires DAL.serializers = {'json': custom_json, 'xml': xml} diff --git a/gluon/packages/dal b/gluon/packages/dal index 35dd4fc6..d8631f68 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 35dd4fc6f8fb8187e7c08217eebf3074e0d27fbc +Subproject commit d8631f683cd374b274f7fe5caa0abf79b7fdc54c diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 594339dd..93e431fb 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2348,12 +2348,12 @@ class SQLFORM(FORM): for k, f in iteritems(table): if isinstance(f, Field.Virtual): f.tablename = table._tablename - columns = [f for f in fields if f.tablename in tablenames] + columns = [f for f in fields if f.tablename in tablenames and f.listable] else: fields = [] columns = [] filter1 = lambda f: isinstance(f, Field) and (f.type!='blob' or showblobs) - filter2 = lambda f: isinstance(f, Field) and f.readable + filter2 = lambda f: isinstance(f, Field) and f.readable and f.listable for table in tables: fields += filter(filter1, table) columns += filter(filter2, table) @@ -2571,8 +2571,8 @@ class SQLFORM(FORM): try: # the query should be constructed using searchable # fields but not virtual fields - sfields = reduce(lambda a, b: a + b, - [[f for f in t if f.readable and not isinstance(f, Field.Virtual)] for t in tables]) + is_searchable = lambda f: f.readable and not isinstance(f, Field.Virtual) and f.searchable + sfields = reduce(lambda a, b: a + b, [filter(is_searchable, t) for t in tables]) # use custom_query using searchable if callable(searchable): dbset = dbset(searchable(sfields, keywords))