From 0d05a50e19b71cade636d9ca4882e453f614a78c Mon Sep 17 00:00:00 2001 From: Petr Vobornik Date: Wed, 13 Nov 2013 15:49:25 +0100 Subject: webui: field and widget binding refactoring This is a Web UI wide change. Fields and Widgets binding was refactored to enable proper two-way binding between them. This should allow to have one source of truth (field) for multiple consumers - widgets or something else. One of the goal is to have fields and widget implementations independent on each other. So that one could use a widget without field or use one field for multiple widgets, etc.. Basically a fields logic was split into separate components: - adapters - parsers & formatters - binder Adapters - extract data from data source (FreeIPA RPC command result) - prepares them for commands. Parsers - parse extracted data to format expected by field - parse widget value to format expected by field Formatters - format field value to format suitable for widgets - format field value to format suitable for adapter Binder - is a communication bridge between field and widget - listens to field's and widget's events and call appropriate methods Some side benefits: - better validation reporting in multivalued widget Reviewed-By: Adam Misnyovszki --- install/ui/doc/categories.json | 26 +- install/ui/less/forms-override.less | 22 + install/ui/src/freeipa/FieldBinder.js | 336 +++++++++ install/ui/src/freeipa/_base/Builder.js | 2 +- install/ui/src/freeipa/_base/Provider.js | 8 +- install/ui/src/freeipa/_base/debug.js | 41 ++ install/ui/src/freeipa/aci.js | 23 +- install/ui/src/freeipa/automember.js | 31 +- install/ui/src/freeipa/certificate.js | 7 +- install/ui/src/freeipa/details.js | 13 +- install/ui/src/freeipa/dialog.js | 9 + install/ui/src/freeipa/dns.js | 115 ++- install/ui/src/freeipa/field.js | 1142 +++++++++++++----------------- install/ui/src/freeipa/host.js | 11 +- install/ui/src/freeipa/rule.js | 2 +- install/ui/src/freeipa/service.js | 60 +- install/ui/src/freeipa/user.js | 6 +- install/ui/src/freeipa/util.js | 338 +++++++++ install/ui/src/freeipa/widget.js | 386 +++++++--- install/ui/test/aci_tests.js | 15 +- install/ui/test/data/ipa_init.json | 2 + install/ui/test/details_tests.js | 10 +- install/ui/test/utils_tests.js | 20 +- install/ui/test/widget_tests.js | 35 +- ipalib/plugins/internal.py | 2 + 25 files changed, 1741 insertions(+), 921 deletions(-) create mode 100644 install/ui/src/freeipa/FieldBinder.js create mode 100644 install/ui/src/freeipa/_base/debug.js create mode 100644 install/ui/src/freeipa/util.js diff --git a/install/ui/doc/categories.json b/install/ui/doc/categories.json index 2634986f2..ec0776687 100644 --- a/install/ui/doc/categories.json +++ b/install/ui/doc/categories.json @@ -74,8 +74,8 @@ "reg", "details.details_builder", "details.section_builder", - "IPA.field_builder", - "IPA.widget_builder" + "field.field_builder", + "widget.widget_builder" ] }, { @@ -96,8 +96,10 @@ "IPA.bulk_associator", "IPA.association_config", "spec_util", + "_base.debug", "_base.Spec_mod", - "datetime" + "datetime", + "util" ] } ] @@ -115,7 +117,7 @@ "facet.FacetState", "facet.action_holder", "details.facet_policies", - "IPA.field_container", + "field.field_container", "IPA.widget_container", "details.update_info", "details.command_info", @@ -179,10 +181,22 @@ { "name": "Fields", "classes": [ - "IPA.field", + "field.field", "*_field" ] }, + { + "name": "Binders", + "classes": [ + "*Binder" + ] + }, + { + "name": "Adapters", + "classes": [ + "*Adapter" + ] + }, { "name": "Formatters", "classes": [ @@ -193,7 +207,7 @@ { "name": "Validators", "classes": [ - "IPA.validator", + "field.validator", "*_validator" ] } diff --git a/install/ui/less/forms-override.less b/install/ui/less/forms-override.less index b8c2e5d35..b987ed59f 100644 --- a/install/ui/less/forms-override.less +++ b/install/ui/less/forms-override.less @@ -184,3 +184,25 @@ input[type="radio"]:checked + label:before { right: -2px; } } + +// do not show error hint for valid value in multivalued widget if some +// other value is invalid +.control-group.error .valid .help-inline { + display: none; +} + +// only type text for now - it should be replaced when we adopt Bootstrap 3 +// based RCUE (patternfly) +.control-group.error .valid input[type=text] { + + border-color: #62afdb !important; + + &:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6); + -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6); + } +} diff --git a/install/ui/src/freeipa/FieldBinder.js b/install/ui/src/freeipa/FieldBinder.js new file mode 100644 index 000000000..ed05d2531 --- /dev/null +++ b/install/ui/src/freeipa/FieldBinder.js @@ -0,0 +1,336 @@ +/* Authors: + * Petr Vobornik + * + * Copyright (C) 2013 Red Hat + * see file 'COPYING' for use and warranty information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +define(['dojo/_base/declare', + 'dojo/_base/lang', + 'dojo/on', + './util' + ], + function(declare, lang, on, util) { + + /** + * Field binder + * + * Binds input widget with field - defines standard communication logic + * between widget and a field. + * + * Usage: + * + * var binder = new FieldBinder(widget, field).bind(); + * + * // or + * var binder = new FieldBinder({ + * field: field, + * widget: widget + * }); + * binder.bind() + * + * @class FieldBinder + */ + var FieldBinder = declare([], { + + /** + * Field + * @property {IPA.field} + */ + field: null, + + /** + * Widget + * @property {IPA.input_widget} + */ + widget: null, + + /** + * Binder is enabled + * + * Handlers are not be called when set to false. + * + * @property {boolean} + */ + enabled: true, + + /** + * Handlers + * @protected + * @property {Function[]} + */ + handlers: null, + + /** + * Value update is in progress + * + * When set, binder should not react to field's nor widget's value-change + * event. + * + * @property {boolean} + */ + updating: false, + + /** + * Bind widget with field + * + * Listens for field's: + * + * - enable-change + * - valid-change + * - value-change + * - dirty-change + * - require-change + * - writable-change + * - readonly-change + * - reset + * + * Listens for widget's: + * + * - value-change + * - undo-click + * + * @param {boolean} hard + * Hard binding. Sets `field.widget` to `this.widget`. + * This option is for backward compatibility. + */ + bind: function(hard) { + + var field = this.field; + var widget = this.widget; + + if (hard) field.widget = widget; + + this.handle(field, 'enable-change', this.on_field_enable_change); + this.handle(field, 'valid-change', this.on_field_valid_change); + this.handle(field, 'value-change', this.on_field_value_change); + this.handle(field, 'dirty-change', this.on_field_dirty_change); + this.handle(field, 'require-change', this.on_field_require_change); + this.handle(field, 'writable-change', this.on_field_writable_change); + this.handle(field, 'readonly-change', this.on_field_readonly_change); + this.handle(field, 'reset', this.on_field_reset); + + this.handle(widget, 'value-change', this.on_widget_value_change); + this.handle(widget, 'undo-click', this.on_widget_undo_click); + + return this; + }, + + /** + * Unbind all handlers + */ + unbind: function() { + + var handler; + while ((handler = this.handlers.pop())) { + handler.remove(); + } + }, + + /** + * Creates and registers the handler. + * Handler will be called in binder context and only if + * `this.enabled === true`. + * + * Do not use `on(target, type, handler)` directly. + * + * @param {Function} handler + * @return {Function} context bound handler + * @protected + */ + handle: function(target, type, handler) { + + var _this = this; + + var hndlr = function() { + if (_this.enabled !== true) return; + else { + handler.apply(_this, Array.prototype.slice.call(arguments, 0)); + } + }; + + var reg_hndl = on(target, type, hndlr); + this.handlers.push(reg_hndl); + + return hndlr; + }, + + /** + * Field enable change handler + * + * Reflect enabled state to widget + * + * @protected + */ + on_field_enable_change: function(event) { + this.widget.set_enabled(event.enabled); + }, + + /** + * Field valid change handler + * @protected + */ + on_field_valid_change: function(event) { + this.widget.set_valid(event.result); + }, + + /** + * Field dirty change handler + * + * Controls showing of widget's undo button + * + * @protected + */ + on_field_dirty_change: function(event) { + + if (!this.field.undo) return; + if (event.dirty) { + this.widget.show_undo(); + } else { + this.widget.hide_undo(); + } + }, + + /** + * Field require change handler + * + * Updates widget's require state + * + * @protected + */ + on_field_require_change: function(event) { + + this.widget.set_required(event.required); + }, + + /** + * Field require change handler + * + * Updates widget's require state + * + * @protected + */ + on_field_writable_change: function(event) { + + this.widget.set_writable(event.writable); + }, + + /** + * Field require change handler + * + * Updates widget's require state + * + * @protected + */ + on_field_readonly_change: function(event) { + + this.widget.set_read_only(event.read_only); + }, + + /** + * Field reset handler + * + * @param {Object} event + * @protected + */ + on_field_reset: function(event) { + this.copy_properties(); + }, + + /** + * Field value change handler + * @protected + */ + on_field_value_change: function(event) { + + if (this.updating) return; + + var format_result = util.format(this.field.ui_formatter, event.value); + if (format_result.ok) { + this.updating = true; + this.widget.update(format_result.value); + this.updating = false; + } else { + // this should not happen in ideal world + window.console.warn('field format error: '+this.field.name); + } + }, + + /** + * Widget value change handler + * @protected + */ + on_widget_value_change: function(event) { + + if (this.updating) return; + + var val = this.widget.save(); + var format_result = util.parse(this.field.ui_parser, val); + if (format_result.ok) { + this.updating = true; + this.field.set_value(format_result.value); + this.updating = false; + } else { + this.field.set_valid(format_result); + } + }, + + /** + * Widget undo click handler + * @protected + */ + on_widget_undo_click: function(event) { + + this.field.reset(); + }, + + /** + * Copies `label`, `tooltip`, `measurement_unit`, `undo`, `writable`, + * `read_only` from field to widget + */ + copy_properties: function() { + + var field = this.field; + var widget = this.widget; + + if (field.label) widget.label = field.label; + if (field.tooltip) widget.tooltip = field.tooltip; + if (field.measurement_unit) widget.measurement_unit = field.measurement_unit; + widget.undo = field.undo; + widget.set_writable(field.writable); + widget.set_read_only(field.read_only); + widget.set_required(field.is_required()); + + return this; + }, + + constructor: function(arg1, arg2) { + + this.handlers = []; + + if (arg2) { + this.field = arg1; + this.widget = arg2; + } else { + arg1 = arg1 || {}; + this.field = arg1.field; + this.widget = arg1.widget; + } + } + }); + + return FieldBinder; +}); diff --git a/install/ui/src/freeipa/_base/Builder.js b/install/ui/src/freeipa/_base/Builder.js index e487aa542..890a98a49 100644 --- a/install/ui/src/freeipa/_base/Builder.js +++ b/install/ui/src/freeipa/_base/Builder.js @@ -366,7 +366,7 @@ define(['dojo/_base/declare', var temp = lang.clone(preop); this.spec_mod.mod(spec, temp); this.spec_mod.del_rules(temp); - lang.mixin(spec, preop); + lang.mixin(spec, temp); } } return spec; diff --git a/install/ui/src/freeipa/_base/Provider.js b/install/ui/src/freeipa/_base/Provider.js index b2e55aed3..fddb45aba 100644 --- a/install/ui/src/freeipa/_base/Provider.js +++ b/install/ui/src/freeipa/_base/Provider.js @@ -44,7 +44,11 @@ * @class _base.Provider * */ -define(['dojo/_base/declare','dojo/_base/lang'], function(declare, lang) { +define([ + 'dojo/_base/declare', + 'dojo/_base/lang', + './debug'], + function(declare, lang, debug) { var undefined; var Provider = declare(null, { @@ -177,7 +181,7 @@ define(['dojo/_base/declare','dojo/_base/lang'], function(declare, lang) { } var ret = value || alternate; - if (!ret && key) { + if (!ret && key && debug.provider_missing_value) { window.console.log('No value for:'+key); } diff --git a/install/ui/src/freeipa/_base/debug.js b/install/ui/src/freeipa/_base/debug.js new file mode 100644 index 000000000..1332aa7dd --- /dev/null +++ b/install/ui/src/freeipa/_base/debug.js @@ -0,0 +1,41 @@ +/* Authors: + * Petr Vobornik + * + * Copyright (C) 2014 Red Hat + * see file 'COPYING' for use and warranty information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +define([], function() { + + /** + * Debug module + * + * One can set flags to enable console output of various messages. + * + * """ + * var debug = require('freeipa._base.debug'); + * debug.provider_missing_value = true; + * """ + * + * Currently used flags + * + * - provider_missing_value + * + * @class _base.debug + */ + return { + provider_missing_value: false + }; +}); \ No newline at end of file diff --git a/install/ui/src/freeipa/aci.js b/install/ui/src/freeipa/aci.js index 9aab2d7ec..0615184c0 100644 --- a/install/ui/src/freeipa/aci.js +++ b/install/ui/src/freeipa/aci.js @@ -20,6 +20,7 @@ */ define([ + 'dojo/on', './metadata', './ipa', './jquery', @@ -30,7 +31,7 @@ define([ './search', './association', './entity'], - function(metadata_provider, IPA, $, phases, reg, text) { + function(on, metadata_provider, IPA, $, phases, reg, text) { /** * Widgets, entities and fields related to Access Control that means @@ -577,6 +578,7 @@ aci.attributes_widget = function(spec) { $('.aci-attribute', that.table). prop('checked', $(this).prop('checked')); that.value_changed.notify([], that); + that.emit('value-change', { source: that }); } }, th); @@ -615,6 +617,7 @@ aci.attributes_widget = function(spec) { 'class': 'aci-attribute', change: function() { that.value_changed.notify([], that); + that.emit('value-change', { source: that }); } }, td); td = $('').appendTo(tr); @@ -827,15 +830,9 @@ aci.permission_target_policy = function (spec) { that.init = function() { that.permission_target = that.container.widgets.get_widget(that.widget_name); - var type_select = that.permission_target.widgets.get_widget('type'); - - type_select.value_changed.attach(function() { - that.apply_type(); - }); + var type_f = that.container.fields.get_field('type'); - type_select.undo_clicked.attach(function() { - that.apply_type(); - }); + on(type_f, 'value-change', that.apply_type); }; that.apply_type = function () { @@ -861,9 +858,7 @@ aci.permission_target_policy = function (spec) { // permission plugin resets ipapermlocation to basedn when // type is unset. -> use it as pristine value so undo will // work correctly. - var loc = [IPA.env.basedn]; - loc_w.update(loc); - loc_f.values = loc; + loc_f.set_value([IPA.env.basedn], true); } else { attrs = attr_multi.save(); attr_table.update(attrs); @@ -1077,9 +1072,9 @@ aci.register = function() { e.register({ type: 'delegation', spec: aci.delegation_entity_spec }); w.register('attributes', aci.attributes_widget); - f.register('attributes', IPA.checkboxes_field); + f.register('attributes', IPA.field); w.register('rights', aci.rights_widget); - f.register('rights', IPA.checkboxes_field); + f.register('rights', IPA.field); w.register('permission_target', aci.permission_target_widget); }; diff --git a/install/ui/src/freeipa/automember.js b/install/ui/src/freeipa/automember.js index e9619b731..ae7304d95 100644 --- a/install/ui/src/freeipa/automember.js +++ b/install/ui/src/freeipa/automember.js @@ -19,6 +19,8 @@ */ define([ + 'dojo/_base/declare', + './field', './metadata', './ipa', './jquery', @@ -31,7 +33,8 @@ define([ './search', './association', './entity'], - function(metadata_provider, IPA, $, navigation, phases, reg, rpc, text) { + function(declare, field_mod, metadata_provider, IPA, $, navigation, + phases, reg, rpc, text) { var exp = IPA.automember = {}; @@ -445,28 +448,26 @@ IPA.automember.parse_condition_regex = function(regex) { IPA.automember.condition_field = function(spec) { spec = spec || {}; + spec.adapter = spec.adapter || IPA.automember.condition_adapter; var that = IPA.field(spec); + return that; +}; - that.attr_name = spec.attribute || that.name; - - that.load = function(record) { - - var regexes = record[that.attr_name]; - that.values = []; +IPA.automember.condition_adapter = declare([field_mod.Adapter], { + load: function(record) { + var regexes = this.inherited(arguments); + var values = []; if (regexes) { for (var i=0, j=0; i} */ - that.values = []; + that.value = []; + + /** + * Default value + * @property {Mixed} + */ + that.default_value = null; /** * Field is dirty (value is modified) @@ -230,6 +274,20 @@ IPA.field = function(spec) { */ that.dirty_changed = IPA.observer(); + /** + * Last validation result + * @property {Object} + */ + that.validation_result = null; + + /** + * Controls if field should perform validation when it's not supposed to + * be edited by user (`is_editable()`). + * @property {boolean} + */ + that.validate_noneditable = spec.validate_noneditable !== undefined ? + spec.validate_noneditable : false; + var init = function() { if (typeof that.metadata === 'string') { that.metadata = metadata_provider.get(that.metadata); @@ -246,6 +304,7 @@ IPA.field = function(spec) { } } + that.set_value([], true); // default value that.validators.push(IPA.metadata_validator()); }; @@ -263,107 +322,109 @@ IPA.field = function(spec) { /** * Required setter + * + * Note that final required state also depends on `read_only` and + * `writable` states. + * * @param {boolean} required */ that.set_required = function(required) { + var old = that.is_required(); that.required = required; + var current = that.is_required(); - that.update_required(); - }; - - /** - * Update required state in widget to match field's - * @protected - */ - that.update_required = function() { - if(that.widget && that.widget.set_required) { - that.widget.set_required(that.is_required()); + if (current !== old) { + that.emit('require-change', { source: that, required: current }); } }; /** - * Check if value is set when it has to be. Show error if not. - * @return {boolean} + * Check if value is set when it has to be. Report if not. + * @return {boolean} value passes the require check */ that.validate_required = function() { - var values = that.save(); - if (IPA.is_empty(values) && that.is_required() && that.enabled) { - that.valid = false; - var message = text.get('@i18n:widget.validation.required', + var values = that.get_value(); + var result = { valid: true, message: null }; + if ((that.validate_noneditable || that.is_editable()) && + util.is_empty(values) && that.is_required()) { + result.valid = false; + result.message = text.get('@i18n:widget.validation.required', "Required field"); - that.show_error(message); - return false; + that.set_valid(result); } - return true; + return result.valid; }; /** - * Returns true and clears the error message if the field value passes - * the validation pattern. If the field value does not pass validation, - * displays the error message and returns false. - * @return {boolean} + * Validates the field. + * Sets the result by `set_valid` call. + * @return {boolean} field is valid */ that.validate = function() { - that.hide_error(); - that.valid = true; - - if (!that.enabled) return that.valid; - - var values = that.get_widget_values(); - - if (IPA.is_empty(values)) { - return that.valid; - } - - var value = values[0]; - for (var i=0; i -1) { - that.writable = false; + writable = false; } } @@ -411,44 +472,62 @@ IPA.field = function(spec) { // For all others, lack of rights means no write. if ((!rights && !(that.flags.indexOf('w_if_no_aci') > -1 && write_oc)) || (rights && rights.indexOf('w') < 0)) { - that.writable = false; + writable = false; } } + + that.set_writable(writable); }; /** - * Reset field and widget to loaded values + * Set writable + * @fires writable-change + * @param {boolean} writable */ - that.reset = function() { - that.set_widget_flags(); - that.update_required(); - that.update(); - that.validate(); - that.set_dirty(false); + that.set_writable = function(writable) { + + var old = !!that.writable; + that.writable = writable; + if (old !== writable) { + that.emit('writable-change', { source: that, writable: writable }); + } + + that.set_required(that.required); // force update of required }; /** - * Update widget with loaded values. + * Set read only + * @fires readonly-change + * @param {boolean} writable */ - that.update = function() { + that.set_read_only = function(read_only) { - if (!that.widget || !that.widget.update) return; + var old = !!that.read_only; + that.read_only = read_only; + if (old !== read_only) { + that.emit('readonly-change', { source: that, readonly: read_only }); + } + that.set_required(that.required); // force update of required + }; - var formatted_values; + /** + * Get if field is intended to be edited + * + * It's a combination of `enabled`, 'writable` and `read_only` state. + * + * @returns {Boolean} + */ + that.is_editable = function() { - // Change loaded value to human readable value - if (that.formatter) { - formatted_values = []; - for (var i=0; that.values && i -1) return that.false_result(); @@ -864,10 +973,11 @@ IPA.unsupported_validator = function(spec) { * * - designed for password confirmation * - * @class IPA.same_password_validator + * @class + * @alternateClassName IPA.same_password_validator * @extends IPA.validator */ -IPA.same_password_validator = function(spec) { +field.same_password_validator = IPA.same_password_validator = function(spec) { spec = spec || {}; @@ -887,7 +997,7 @@ IPA.same_password_validator = function(spec) { */ that.validate = function(value, context) { - var other_field = context.container.fields.get_field(that.other_field); + var other_field = context.container.get_field(that.other_field); var other_value = other_field.save(); var this_value = context.save(); @@ -899,49 +1009,26 @@ IPA.same_password_validator = function(spec) { return that; }; -/** - * Check if input value is a valid datetime - * - * @class IPA.datetime_validator - * @extends IPA.validator - */ -IPA.datetime_validator = function(spec) { - - spec = spec || {}; - - var that = IPA.validator(spec); - - that.message = text.get(spec.message || '@i18n:widget.validation.datetime'); - - /** - * @inheritDoc - */ - that.validate = function(value, context) { - - var valid = datetime.parse(value) !== null; - if (!valid) return that.false_result(); - - return that.true_result(); - }; - - return that; -}; - /** * Used along with checkbox widget * - * @class IPA.checkbox_field + * @class + * @alternateClassName IPA.datetime_field * @extends IPA.field */ -IPA.datetime_field = function(spec) { +field.datetime_field = IPA.datetime_field = function(spec) { spec = spec || {}; - spec.validators = spec.validators || ['datetime']; - spec.output_formatter = spec.output_formatter || { + spec.data_formatter = spec.data_formatter || { $type: 'datetime', template: datetime.templates.generalized }; - spec.formatter = spec.formatter || 'datetime'; + spec.data_parser = spec.formatter || 'datetime'; + spec.ui_formatter = spec.ui_formatter || spec.formatter || { + $type: 'datetime', + template: datetime.templates.human + }; + spec.ui_parser = spec.ui_parser || 'datetime'; var that = IPA.field(spec); return that; @@ -950,55 +1037,17 @@ IPA.datetime_field = function(spec) { /** * Used along with checkbox widget * - * @class IPA.checkbox_field + * @class + * @alternateClassName IPA.checkbox_field * @extends IPA.field */ -IPA.checkbox_field = function(spec) { +field.checkbox_field = IPA.checkbox_field = function(spec) { spec = spec || {}; + spec.data_parser = 'boolean'; var that = IPA.field(spec); - /** - * Check value by default - * @property {boolean} - */ - that.checked = spec.checked || false; - - /** - * Boolean formatter for parsing loaded values. - * @property {IPA.boolean_formatter} - */ - that.boolean_formatter = IPA.boolean_formatter(); - - /** - * @inheritDoc - */ - that.load = function(record) { - - that.record = record; - - that.values = that.get_value(record, that.param); - - var value = that.boolean_formatter.parse(that.values); - if (value === '') value = that.widget.checked; //default value - - that.values = [value]; - - that.load_writable(record); - - that.reset(); - }; - - /** - * @inheritDoc - */ - that.widgets_created = function() { - - that.field_widgets_created(); - that.widget.checked = that.checked; - }; - /** * A checkbox will always have a value, so it's never required. * @@ -1008,33 +1057,17 @@ IPA.checkbox_field = function(spec) { return false; }; - that.checkbox_load = that.load; - - return that; -}; - -/** - * Used along with checkboxes widget - * - * @class IPA.checkboxes_field - * @extends IPA.field - */ -IPA.checkboxes_field = function(spec) { - - spec = spec || {}; - - var that = IPA.field(spec); - return that; }; /** * Used along with radio widget * - * @class IPA.radio_field + * @class + * @alternateClassName IPA.radio_field * @extends IPA.field */ -IPA.radio_field = function(spec) { +field.radio_field = IPA.radio_field = function(spec) { spec = spec || {}; @@ -1048,125 +1081,55 @@ IPA.radio_field = function(spec) { return false; }; - /** - * @inheritDoc - */ - that.widgets_created = function() { - - that.field_widgets_created(); - }; - return that; }; /** - * Used along with multivalued widget + * Used along with ssh key widget + * + * - by default has `w_if_no_aci` to workaround missing object class * - * @class IPA.multivalued_field + * @class + * @alternateClassName IPA.sshkeys_field * @extends IPA.field */ -IPA.multivalued_field = function(spec) { +field.sshkeys_field = IPA.sshkeys_field = function(spec) { spec = spec || {}; + spec.adapter = spec.adapter || field.SshKeysAdapter; + spec.flags = spec.flags || ['w_if_no_aci']; var that = IPA.field(spec); - - /** - * @inheritDoc - */ - that.load = function(record) { - - that.field_load(record); - }; - - /** - * @inheritDoc - */ - that.test_dirty = function() { - var dirty = that.field_test_dirty(); - dirty = dirty || that.widget.test_dirty(); //also checks order - return dirty; - }; - - /** - * @inheritDoc - */ - that.validate = function() { - - var values = that.save(); - - return that.validate_core(values); - }; - - /** - * Validate each value separately. - * @protected - * @param {Array} values - * @return {boolean} valid - */ - that.validate_core = function(values) { - - that.hide_error(); - that.valid = true; - - if (IPA.is_empty(values)) { - return that.valid; - } - - for (var i=0; i 0) { //no need to delete if has no values diff --git a/install/ui/src/freeipa/service.js b/install/ui/src/freeipa/service.js index 09880a937..bd1d3842b 100644 --- a/install/ui/src/freeipa/service.js +++ b/install/ui/src/freeipa/service.js @@ -19,6 +19,8 @@ */ define([ + 'dojo/_base/declare', + './field', './ipa', './jquery', './phases', @@ -29,7 +31,7 @@ define([ './search', './association', './entity'], - function(IPA, $, phases, reg, rpc, text) { + function(declare, field_mod, IPA, $, phases, reg, rpc, text) { var exp =IPA.service = {}; @@ -66,16 +68,16 @@ return { fields: [ 'krbprincipalname', { - $type: 'service_name', name: 'service', label: '@i18n:objects.service.service', - read_only: true + read_only: true, + adapter: IPA.service_name_adapter }, { - $type: 'service_host', name: 'host', label: '@i18n:objects.service.host', - read_only: true + read_only: true, + adapter: IPA.service_host_adapter }, { name: 'ipakrbauthzdata', @@ -271,45 +273,21 @@ IPA.service_adder_dialog = function(spec) { return that; }; -IPA.service_name_field = function(spec) { - - spec = spec || {}; - - var that = IPA.field(spec); - - that.load = function(record) { - - that.field_load(record); - +IPA.service_name_adapter = declare([field_mod.Adapter], { + load: function(record) { var krbprincipalname = record.krbprincipalname[0]; var value = krbprincipalname.replace(/\/.*$/, ''); - that.values = [value]; - - that.reset(); - }; - - return that; -}; - -IPA.service_host_field = function(spec) { - - spec = spec || {}; - - var that = IPA.field(spec); - - that.load = function(record) { - - that.field_load(record); + return [value]; + } +}); +IPA.service_host_adapter = declare([field_mod.Adapter], { + load: function(record) { var krbprincipalname = record.krbprincipalname[0]; var value = krbprincipalname.replace(/^.*\//, '').replace(/@.*$/, ''); - that.values = [value]; - - that.reset(); - }; - - return that; -}; + return [value]; + } +}); IPA.service_provisioning_status_widget = function (spec) { @@ -512,10 +490,6 @@ phases.on('registration', function() { e.register({type: 'service', spec: exp.entity_spec}); - f.register('service_name', IPA.service_name_field); - w.register('service_name', IPA.text_widget); - f.register('service_host', IPA.service_host_field); - w.register('service_host', IPA.text_widget); f.register('service_provisioning_status', IPA.field); w.register('service_provisioning_status', IPA.service_provisioning_status_widget); a.register('service_unprovision', IPA.service.unprovision_action); diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js index aee1b694e..e3ada9844 100644 --- a/install/ui/src/freeipa/user.js +++ b/install/ui/src/freeipa/user.js @@ -133,10 +133,10 @@ return { metadata: '@mo-param:user:userpassword' }, { + $type: 'datetime', name: 'krbpasswordexpiration', label: '@i18n:objects.user.krbpasswordexpiration', - read_only: true, - formatter: 'datetime' + read_only: true }, 'uidnumber', 'gidnumber', @@ -473,7 +473,7 @@ IPA.user_adder_dialog = function(spec) { var password2 = field2.save()[0]; if (password1 !== password2) { - field2.show_error(text.get('@i18n:password.password_must_match')); + field2.set_valid({ valid: false, message: text.get('@i18n:password.password_must_match') }); valid = false; } diff --git a/install/ui/src/freeipa/util.js b/install/ui/src/freeipa/util.js new file mode 100644 index 000000000..d6fbbf4f1 --- /dev/null +++ b/install/ui/src/freeipa/util.js @@ -0,0 +1,338 @@ +/* Authors: + * Petr Vobornik + * + * Copyright (C) 2014 Red Hat + * see file 'COPYING' for use and warranty information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +define([ + 'dojo/_base/lang', + './text' + ], + function(lang, text) { + + function equals_obj_def(a, b, options) { + var same = true; + var checked = {}; + + var check_same = function(a, b, skip) { + + var same = true; + skip = skip || {}; + + for (var key in a) { + if (a.hasOwnProperty(key) && !(key in skip)) { + + var va = a[key]; + var vb = b[key]; + + if (!equals(va, vb, options)) { + same = false; + skip[a] = true; + break; + } + } + } + return same; + }; + + same = check_same(a,b, checked); + same = same && check_same(b,a, checked); + return same; + } + + function equals_obj(a, b, options) { + + if (options.comparator) { + return options.comparator(a, b, options); + } else { + return equals_obj_def(a, b, options); + } + } + + function equals_array(a1, b1, options) { + + var a = a1, + b = b1; + + if (!a || !b) return false; + + if (a1.length !== b1.length) return false; + + if (options.unordered) { + a = a1.slice(0); + b = b1.slice(0); + a.sort(); + b.sort(); + } + + for (var i=0; i -1) { + return false; + } else if (a === null || b === null) { + return false; + } else if (lang.isArray(a)) { + return equals_array(a, b, options); + } else if (a instanceof Date) { + return a.getTime() === b.getTime(); + } else { // objects + return equals_obj(a, b, options); + } + } + + function is_empty(value) { + var empty = false; + + if (!value) empty = true; + + if (lang.isArray(value)) { + empty = empty || value.length === 0 || + (value.length === 1) && (value[0] === ''); + } else if (typeof value === 'object') { + var has_p = false; + for (var p in value) { + if (value.hasOwnProperty(p)) { + has_p = true; + break; + } + } + empty = !has_p; + } else if (value === '') empty = true; + + return empty; + } + + function dirty(value, pristine, options) { + + // check for empty value: null, [''], '', [] + var orig_empty = is_empty(pristine); + var new_empty= is_empty(value); + if (orig_empty && new_empty) return false; + if (orig_empty != new_empty) return true; + + // strict equality - checks object's ref equality, numbers, strings + if (value === pristine) return false; + + return !equals(value, pristine, options); + } + + function format_single(formatter, value, error_text, method) { + var val = value, + ok = true, + msg = null; + try { + if (method === 'format') { + val = formatter.format(val); + } else { + val = formatter.parse(val); + } + } catch (e) { + if (e.reason !== method) throw e; + ok = false; + value = e.value; + msg = e.message || error_text; + } + return { + ok: ok, + value: val, + message: msg + }; + } + + function format_core(formatter, value, error_text, method) { + + if (!formatter) return { ok: true, value: value }; + if (lang.isArray(value)) { + var res = { + ok: true, + value: [], + messages: [] + }; + for (var i=0, l=value.length; i 1) res.message = error_text; + else res.message = res.messages[0]; + } + } + return res; + } else { + return format_single(formatter, value, error_text, method); + } + } + + function format(formatter, value, error_text) { + + var err = error_text || text.get('@i18n:widget.validation.format'); + return format_core(formatter, value, err, 'format'); + } + + function parse(formatter, value, error_text) { + + var err = error_text || text.get('@i18n:widget.validation.parse'); + return format_core(formatter, value, err, 'parse'); + } + + function normalize_value(value) { + + if (!(value instanceof Array)) { + value = value !== undefined ? [value] : []; + } + if (!value.length) { + value = ['']; + } + return value; + } + + function emit_delayed(target, type, event, delay) { + + delay = delay || 0; + window.setTimeout(function() { + target.emit(type, event); + }, 0); + } + + /** + * Module with utility functions + * @class + * @singleton + */ + var util = { + + /** + * Checks if two variables have equal value + * + * - `string`, `number`, `function`, `boolean`, `null`, + * `undefined` are compared with strict equality + * - 'object' and arrays are compared by values + * + * Available options: + * + * - `unordered` - boolean, sort arrays before value comparison. Does + * not modify original values. + * - `comparator`- function(a,b), returns bool - custom object comparator + * + * @param {Mixed} a + * @param {Mixed} b + * @param {String[]} [options] + * @return {boolean} `a` and `b` are value-equal + */ + equals: equals, + + /** + * Check if value is empty. + * + * True when: + * + * - value is undefined or `null` or `''` + * - value is empty Array + * - value is Array with an empty string (`''`) + * - value is empty Object- `{}` + * @param value - value to check + * @return {boolean} + */ + is_empty: is_empty, + + /** + * Special kind of negative `equals` where variants of `empty_value` are + * considered same. + * + * @param {Mixed} value New value + * @param {Mixed} pristine Pristine value + * @param {String[]} [options] control options, same as in `equals` + * @return {boolean} `value` and `pristine` differs + */ + dirty: dirty, + + /** + * Format value or values using a formatter + * + * Output format for single values: + * + * { + * ok: true|false, + * value: null | formatted value, + * message: null | string + * } + * + * Output form for array: + * + * { + * ok: true|false, + * value: array of formatted values, + * messages: array of error messages + * message: null | string + * } + * + * @param {IPA.formatter} formatter + * @param {Mixed} value + * @param {string} error Default error message + * @return {Object} + */ + format: format, + + /** + * Basically the same as format method, just uses formatter's `parse` + * method instead of `format` method. + * + * @param {IPA.formatter} formatter + * @param {Mixed} value + * @param {string} error Default error message + * @return {Object} + */ + parse: parse, + + /** + * Encapsulates value into array if it's not already an array. + * + * @param {Mixed} value + * @returns {Array} normalized value + */ + normalize_value: normalize_value, + + /** + * Emit delayed event + * + * Uses timer in order to wait for current processing to finish. + * + * @param {Evented} object Source object which emits the event + * @param {String} type Name of the event to emit + * @param {Object} event Event object + * @param {Number} [delay=0] + */ + emit_delayed: emit_delayed + }; + + return util; +}); diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js index a9b77694e..9b04acc91 100644 --- a/install/ui/src/freeipa/widget.js +++ b/install/ui/src/freeipa/widget.js @@ -32,13 +32,15 @@ define(['dojo/_base/array', './datetime', './ipa', './jquery', + './navigation', './phases', './reg', './rpc', - './text' + './text', + './util' ], - function(array, lang, Evented, has, keys, on, builder, datetime, IPA, $, - phases, reg, rpc, text) { + function(array, lang, Evented, has, keys, on, builder, datetime, + IPA, $, navigation, phases, reg, rpc, text, util) { /** * Widget module @@ -127,6 +129,12 @@ IPA.widget = function(spec) { */ that.enabled = spec.enabled === undefined ? true : spec.enabled; + /** + * Enables showing of validation errors + * @property {boolean} + */ + that.show_errors = spec.show_errors === undefined ? true : spec.show_errors; + /** * Create HTML representation of a widget. * @method @@ -190,6 +198,24 @@ IPA.widget = function(spec) { return child; }; + that.add_class = function(cls) { + if (that.container) { + that.container.addClass(cls); + } + }; + + that.remove_class = function(cls) { + if (that.container) { + that.container.removeClass(cls); + } + }; + + that.toggle_class = function(cls, flag) { + if (that.container) { + that.container.toggleClass(cls, flag); + } + }; + that.widget_create = that.create; that.widget_set_enabled = that.set_enabled; @@ -259,6 +285,7 @@ IPA.input_widget = function(spec) { * Value changed event. * * Raised when user modifies data by hand. + * @deprecated * * @event */ @@ -266,6 +293,7 @@ IPA.input_widget = function(spec) { /** * Undo clicked event. + * @deprecated * * @event */ @@ -273,6 +301,7 @@ IPA.input_widget = function(spec) { /** * Updated event. + * @deprecated * * Raised when widget content gets updated - raised by * {@link IPA.input_widget#update} method. @@ -326,6 +355,13 @@ IPA.input_widget = function(spec) { return []; }; + /** + * Alias of save + */ + that.get_value = function() { + return that.save(); + }; + /** * This function creates an undo link in the container. * On_undo is a link click callback. It can be specified to custom @@ -383,27 +419,60 @@ IPA.input_widget = function(spec) { * @return {jQuery} error link jQuery reference */ that.get_error_link = function() { - return $('span[name="error_link"]', that.container); + return $('span[name="error_link"]', that.container).eq(0); + }; + + /** + * Set's validity of widget's value. Usually checked by outside logic. + * @param {Object} result Validation result as defined in IPA.validator + */ + that.set_valid = function(result) { + + var old = that.valid; + that.valid = result.valid; + + that.toggle_class('valid', that.valid); + if (!that.valid) { + that.show_error(result.message); + } else { + that.hide_error(); + } + if (old !== that.valid) { + that.emit("valid-change", { + source: that, + valid: that.valid, + result: result + }); + } }; /** * Show error message * @protected - * @param {string} message + * @fires error-show + * @param {Object} error */ that.show_error = function(message) { - var error_link = that.get_error_link(); - error_link.html(message); - error_link.css('display', ''); - that.emit('error-show', { source: that, error: message }); + if (that.show_errors) { + var error_link = that.get_error_link(); + error_link.html(message); + error_link.css('display', ''); + } + that.emit('error-show', { + source: that, + error: message, + displayed: that.show_errors + }); }; /** * Hide error message * @protected + * @fires error-hide */ that.hide_error = function() { var error_link = that.get_error_link(); + error_link.html(''); error_link.css('display', 'none'); that.emit('error-hide', { source: that }); }; @@ -457,6 +526,38 @@ IPA.input_widget = function(spec) { return !that.read_only && !!that.writable; }; + /** + * Set writable + * @fires writable-change + * @param {boolean} writable + */ + that.set_writable = function(writable) { + + var changed = writable !== that.writable; + + that.writable = writable; + + if (changed) { + that.emit('writable-change', { source: that, writable: writable }); + } + }; + + /** + * Set read only + * @fires readonly-change + * @param {boolean} writable + */ + that.set_read_only = function(read_only) { + + var changed = read_only !== that.read_only; + + that.read_only = read_only; + + if (changed) { + that.emit('readonly-change', { source: that, read_only: read_only }); + } + }; + /** * Focus input element * @abstract @@ -499,6 +600,7 @@ IPA.input_widget = function(spec) { // methods that should be invoked by subclasses that.widget_hide_error = that.hide_error; that.widget_show_error = that.show_error; + that.widget_set_valid = that.set_valid; return that; }; @@ -680,7 +782,7 @@ IPA.multivalued_widget = function(spec) { that.child_spec = spec.child_spec; that.size = spec.size || 30; that.undo_control; - that.initialized = false; + that.initialized = true; that.rows = []; @@ -693,24 +795,16 @@ IPA.multivalued_widget = function(spec) { row.remove_link.show(); } - that.value_changed.notify([], that); - that.emit('child-value-change', { source: that, row: row }); that.emit('value-change', { source: that }); + that.emit('child-value-change', { source: that, row: row }); }; that.on_child_undo_clicked = function(row) { if (row.is_new) { that.remove_row(row); } else { - //reset - row.widget.update(row.original_values); - row.widget.set_deleted(false); - row.deleted = false; - row.remove_link.show(); + that.reset_row(row); } - - row.widget.hide_undo(); - that.value_changed.notify([], that); that.emit('child-undo-click', { source: that, row: row }); }; @@ -745,20 +839,45 @@ IPA.multivalued_widget = function(spec) { } }; - that.show_child_error = function(index, error) { + that.set_valid = function (result) { - that.rows[index].widget.show_error(error); - }; + var old = that.valid; + that.valid = result.valid; - that.get_saved_value_row_index = function(index) { + if (!result.valid && result.errors) { + var offset = 0; + for (var i=0; i 0) { + var error_link = that.get_error_link(); + error_link.css('display', 'none'); + error_link.html(''); + } else { + that.show_error(result.message); + } - if(that.rows[i].deleted) index++; - if(i === index) return i; + } else { + that.hide_error(); } - return -1; //error state + if (old !== that.valid) { + that.emit("valid-change", { + source: that, + valid: that.valid, + result: result + }); + } }; that.save = function() { @@ -829,10 +948,10 @@ IPA.multivalued_widget = function(spec) { row.original_values = values; row.widget.update(values); - row.widget.value_changed.attach(function() { + on(row.widget, 'value-change', function() { that.on_child_value_changed(row); }); - row.widget.undo_clicked.attach(function() { + on(row.widget, 'undo-click', function() { that.on_child_undo_clicked(row); }); on(row.widget, 'error-show', function() { @@ -847,8 +966,6 @@ IPA.multivalued_widget = function(spec) { html: text.get('@i18n:buttons.remove'), click: function () { that.remove_row(row); - that.value_changed.notify([], that); - that.emit('value-change', { source: that }); return false; } }).appendTo(row.container); @@ -902,6 +1019,17 @@ IPA.multivalued_widget = function(spec) { }).appendTo(container); }; + that.reset_row = function(row) { + row.widget.update(row.original_values); + row.widget.set_deleted(false); + row.deleted = false; + row.remove_link.show(); + row.widget.hide_undo(); + + that.value_changed.notify([], that); + that.emit('value-change', { source: that }); + }; + that.remove_row = function(row) { if (row.is_new) { row.container.remove(); @@ -912,6 +1040,8 @@ IPA.multivalued_widget = function(spec) { row.remove_link.hide(); row.widget.show_undo(); } + that.value_changed.notify([], that); + that.emit('value-change', { source: that }); }; that.remove_rows = function() { @@ -929,30 +1059,14 @@ IPA.multivalued_widget = function(spec) { if (row.deleted || row.is_new) return true; - var values = row.widget.save(); - if (!values) return false; - - if (row.original_values.length !== values.length) return true; + var value = row.widget.save(); - for (var i=0; i 0) { - that.nonlink.text(values[0]); - that.link.text(values[0]); + that.check_entity_link(); + that.updated.notify([], that); + that.emit('update', { source: that }); + }; + + that.update_link = function() { + if (that.value) { if(that.is_link) { that.link.css('display',''); that.nonlink.css('display','none'); @@ -3641,13 +3799,54 @@ IPA.link_widget = function(spec) { that.nonlink.css('display',''); } } else { - that.link.html(''); - that.nonlink.html(''); that.link.css('display','none'); that.nonlink.css('display','none'); } - that.updated.notify([], that); - that.emit('update', { source: that }); + }; + + /** + * Handler for widget `link_click` event + */ + that.on_link_clicked = function() { + + navigation.show_entity( + that.other_entity.name, + 'default', + that.other_pkeys()); + }; + + /** + * Check if entity exists + * + * - only if link points to an entity + * - updates link visibility accordingly + */ + that.check_entity_link = function() { + + //In some cases other entity may not be present. + //For example when DNS is not configured. + if (!that.other_entity) { + that.is_link = false; + return; + } + + rpc.command({ + entity: that.other_entity.name, + method: 'show', + args: that.other_pkeys(), + options: {}, + retry: false, + on_success: function(data) { + that.is_link = data.result && data.result.result; + that.update_link(); + }, + on_error: function() { + that.is_link = false; + that.update_link(); + } + }).execute(); + + that.update_link(); }; /** @inheritDoc */ @@ -4119,7 +4318,7 @@ exp.fluid_layout = IPA.fluid_layout = function(spec) { text: label_text }).appendTo(label_cont); - var input = widget.get_input(); + var input = widget.get_input && widget.get_input(); if (input && input.length) input = input[0]; @@ -4150,6 +4349,8 @@ exp.fluid_layout = IPA.fluid_layout = function(spec) { that.register_state_handlers = function(widget) { on(widget, 'require-change', that.on_require_change); on(widget, 'enabled-change', that.on_enabled_change); + on(widget, 'readonly-change', that.on_require_change); + on(widget, 'writable-change', that.on_require_change); on(widget, 'error-show', that.on_error_show); on(widget, 'error-hide', that.on_error_hide); }; @@ -4165,7 +4366,7 @@ exp.fluid_layout = IPA.fluid_layout = function(spec) { var row = that._get_row(event); if (!row) return; - row.toggleClass('required', !!event.required); + row.toggleClass('required', !!event.required && event.source.is_writable()); }; that.on_error_show = function(event) { @@ -4184,7 +4385,7 @@ exp.fluid_layout = IPA.fluid_layout = function(spec) { that.update_state = function(row, widget) { row.toggleClass('disabled', !widget.enabled); - row.toggleClass('required', !!widget.required); + row.toggleClass('required', !!widget.required && widget.is_writable()); }; that._get_row = function(event) { @@ -4685,9 +4886,10 @@ IPA.widget_container = function(spec) { /** * Widget builder - * @class + * @class widget.widget_builder + * @alternateClassName IPA.widget_builder */ -IPA.widget_builder = function(spec) { +exp.widget_builder = IPA.widget_builder = function(spec) { spec = spec || {}; @@ -4792,9 +4994,9 @@ IPA.sshkey_widget = function(spec) { that.create_error_link(container); }; - that.update = function(values) { + that.update = function(value) { - var key = values && values.length ? values[0] : null; + var key = value[0]; if (!key || key === '') { key = {}; @@ -4821,9 +5023,7 @@ IPA.sshkey_widget = function(spec) { }; that.save = function() { - var value = that.key.key; - value = value ? [value] : ['']; - return value; + return that.key; }; that.update_link = function() { diff --git a/install/ui/test/aci_tests.js b/install/ui/test/aci_tests.js index 17db5b5d6..e82dd86df 100644 --- a/install/ui/test/aci_tests.js +++ b/install/ui/test/aci_tests.js @@ -239,7 +239,17 @@ test("Testing type target.", function() { same(target_widget.target, 'type', 'type selected'); - $("input[type=checkbox]").attr("checked",true); + var attrs_w = target_widget.widgets.get_widget('attrs'); + var options = attrs_w.options; + ok(options.length > 0, "Attrs has some options"); + // check them all + var values = []; + for (var i=0,l=options.length; i 10), - "response length shows some attrs set"); + same(record.attrs.length, options.length, "response contains all checked attrs"); }); diff --git a/install/ui/test/data/ipa_init.json b/install/ui/test/data/ipa_init.json index e7c58e66c..059726fea 100644 --- a/install/ui/test/data/ipa_init.json +++ b/install/ui/test/data/ipa_init.json @@ -560,6 +560,7 @@ "error": "Text does not match field pattern", "datetime": "Must be an UTC date/time value (e.g., \"2014-01-20 17:58:01Z\")", "decimal": "Must be a decimal number", + "format": "Format error", "integer": "Must be an integer", "ip_address": "Not a valid IP address", "ip_v4_address": "Not a valid IPv4 address", @@ -567,6 +568,7 @@ "max_value": "Maximum value is ${value}", "min_value": "Minimum value is ${value}", "net_address": "Not a valid network address", + "parse": "Parse error", "port": "'${port}' is not a valid port", "required": "Required field", "unsupported": "Unsupported value" diff --git a/install/ui/test/details_tests.js b/install/ui/test/details_tests.js index c6c33bd17..33d49aa47 100644 --- a/install/ui/test/details_tests.js +++ b/install/ui/test/details_tests.js @@ -24,12 +24,12 @@ define([ 'freeipa/jquery', 'freeipa/details', 'freeipa/facet', + 'freeipa/field', 'freeipa/reg', 'freeipa/rpc', 'freeipa/entity', - 'freeipa/field', 'freeipa/widget'], - function(md, IPA, $, mod_details, mod_facet, reg, rpc) { + function(md, IPA, $, mod_details, mod_facet, mod_field, reg, rpc) { return function() { var details_container; @@ -41,6 +41,7 @@ module('details', { mod_facet.register(); mod_details.register(); + mod_field.register(); IPA.init({ url: 'data', @@ -255,7 +256,10 @@ test("Testing details lifecycle: create, load.", function(){ ok (load_called, 'load manager called'); var field = facet.fields.get_field('test'); - field.set_dirty(true); + field.set_value("foo"); + var widget = facet.widgets.get_widget('contact.test'); + // simulate user change + widget.emit('value-change', { source: widget, value: "foo" }); facet.update( function(){update_success_called = true;}, diff --git a/install/ui/test/utils_tests.js b/install/ui/test/utils_tests.js index b725f55eb..84424a2e1 100644 --- a/install/ui/test/utils_tests.js +++ b/install/ui/test/utils_tests.js @@ -22,9 +22,10 @@ define([ 'freeipa/ipa', 'freeipa/jquery', 'freeipa/datetime', + 'freeipa/util', 'freeipa/field', 'freeipa/widget'], - function(IPA, $, datetime) { return function() { + function(IPA, $, datetime, util) { return function() { var old; @@ -143,6 +144,23 @@ test('Testing IPA.defined', function() { same(IPA.defined(null), false, 'null'); }); +test('Testing util.equals', function() { + + ok(util.equals([], []), 'Empty Arrays'); + ok(util.equals([1, "a", false, true], [1, "a", false, true]), 'Arrays'); + ok(util.equals(true, true), 'Boolean: true'); + ok(util.equals(false, false), 'Boolean: false'); + ok(!util.equals(true, false), 'Negative: boolean'); + ok(!util.equals(false, true), 'Negative: boolean'); + ok(util.equals("abc", "abc"), 'Positive: strings'); + ok(!util.equals("abc", "aBC"), 'Negative: string casing'); + ok(util.equals(1, 1), 'Positive: number'); + ok(util.equals(1.0, 1), 'Positive: number'); + ok(util.equals(2.2, 2.2), 'Positive: number'); + + ok(!util.equals([], [""]), 'Negative: empty array'); +}); + test('Testing datetime', function() { var valid = [ diff --git a/install/ui/test/widget_tests.js b/install/ui/test/widget_tests.js index 5499f5529..d24ab4cbe 100644 --- a/install/ui/test/widget_tests.js +++ b/install/ui/test/widget_tests.js @@ -18,9 +18,15 @@ * along with this program. If not, see . */ -define(['freeipa/ipa', 'freeipa/jquery', 'freeipa/field', 'freeipa/widget', - 'freeipa/entity'], - function(IPA, $) { return function() { +define([ + 'dojo/on', + 'freeipa/ipa', + 'freeipa/jquery', + 'freeipa/group', + 'freeipa/field', + 'freeipa/widget', + 'freeipa/entity' +], function(on, IPA, $, group) { return function() { var widget_container; var widget; @@ -43,6 +49,7 @@ module('widget',{ factory = null; spec = null; + group.register(); }, teardown: function() { @@ -119,23 +126,28 @@ function text_tests(widget,input){ function multivalued_text_tests(widget) { var values = ['val1', 'val2', 'val3']; + var changed = false; + function on_change (event) { + changed = true; + } + on(widget, 'value-change', on_change); widget.update(values); same(widget.save(), values, "All values loaded"); - same(widget.test_dirty(), false, "Field initially clean"); values = ['val1', 'val2', 'val3', 'val4']; widget.add_row(['val4']); - same(widget.save(), values, "Value added"); - same(widget.test_dirty(), true, "Field is dirty"); + ok(changed, "Value changed"); + changed = false; values = ['val1', 'val3', 'val4']; widget.remove_row(widget.rows[1]); same(widget.save(), values, "Value removed"); - same(widget.test_dirty(), true, "Field is dirty"); + ok(changed, "Value changed"); + changed = false; } test("IPA.table_widget" ,function(){ @@ -294,7 +306,10 @@ test("IPA.entity_link_widget" ,function(){ factory = IPA.link_widget; spec = { name: 'gidnumber', - other_entity:'group' + other_entity:'group', + other_pkeys: function() { + return ['kfrog']; + } }; base_widget_test(widget,'user','test_value'); @@ -304,8 +319,6 @@ test("IPA.entity_link_widget" ,function(){ } }; - var mock_record = { uid: ['kfrog'], gidnumber: ['123456']}; - widget.entity = mock_entity; widget.create(widget_container); @@ -315,7 +328,7 @@ test("IPA.entity_link_widget" ,function(){ ok(nonlink.length > 1); ok(link.length > 1); - widget.is_link = true; //setting is_link is responsibility of field + var mock_record = { gidnumber: ['123456']}; widget.update(mock_record.gidnumber); link = widget_container.find('a:contains("123456")'); diff --git a/ipalib/plugins/internal.py b/ipalib/plugins/internal.py index b20597ef2..a34d2cb16 100644 --- a/ipalib/plugins/internal.py +++ b/ipalib/plugins/internal.py @@ -696,6 +696,7 @@ class i18n_messages(Command): "error": _("Text does not match field pattern"), "datetime": _("Must be an UTC date/time value (e.g., \"2014-01-20 17:58:01Z\")"), "decimal": _("Must be a decimal number"), + "format": _("Format error"), "integer": _("Must be an integer"), "ip_address": _('Not a valid IP address'), "ip_v4_address": _('Not a valid IPv4 address'), @@ -703,6 +704,7 @@ class i18n_messages(Command): "max_value": _("Maximum value is ${value}"), "min_value": _("Minimum value is ${value}"), "net_address": _("Not a valid network address"), + "parse": _("Parse error"), "port": _("'${port}' is not a valid port"), "required": _("Required field"), "unsupported": _("Unsupported value"), -- cgit