decidim-admin 0.20.1 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of decidim-admin might be problematic. Click here for more details.

Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -0
  3. data/app/assets/javascripts/decidim/admin/application.js.es6 +1 -0
  4. data/app/assets/javascripts/decidim/admin/bundle.js +5 -5
  5. data/app/assets/javascripts/decidim/admin/bundle.js.map +1 -1
  6. data/app/assets/javascripts/decidim/admin/newsletters.js.es6 +8 -0
  7. data/app/assets/stylesheets/decidim/admin/_variables.scss +1 -1
  8. data/app/assets/stylesheets/decidim/admin/extra/_action-icon.scss +6 -0
  9. data/app/assets/stylesheets/decidim/admin/extra/_cards.scss +11 -0
  10. data/app/assets/stylesheets/decidim/admin/modules/_buttons.scss +5 -0
  11. data/app/assets/stylesheets/decidim/admin/modules/_cards.scss +11 -0
  12. data/app/assets/stylesheets/decidim/admin/modules/_filters.scss +78 -1
  13. data/app/assets/stylesheets/decidim/admin/modules/_layout.scss +1 -1
  14. data/app/assets/stylesheets/decidim/admin/modules/_secondary-nav.scss +5 -1
  15. data/app/assets/stylesheets/decidim/admin/modules/_table-list.scss +24 -3
  16. data/app/cells/decidim/admin/results_per_page/show.erb +16 -0
  17. data/app/cells/decidim/admin/results_per_page_cell.rb +14 -0
  18. data/app/commands/decidim/admin/deliver_newsletter.rb +1 -1
  19. data/app/commands/decidim/admin/update_organization.rb +4 -1
  20. data/app/controllers/concerns/decidim/admin/filterable.rb +152 -0
  21. data/app/controllers/concerns/decidim/admin/officializations/filterable.rb +31 -0
  22. data/app/controllers/concerns/decidim/admin/paginable.rb +20 -0
  23. data/app/controllers/decidim/admin/admin_terms_controller.rb +20 -0
  24. data/app/controllers/decidim/admin/application_controller.rb +2 -0
  25. data/app/controllers/decidim/admin/components/base_controller.rb +5 -1
  26. data/app/controllers/decidim/admin/components_controller.rb +16 -20
  27. data/app/controllers/decidim/admin/concerns/has_private_users.rb +4 -0
  28. data/app/controllers/decidim/admin/newsletters_controller.rb +12 -1
  29. data/app/controllers/decidim/admin/officializations_controller.rb +7 -6
  30. data/app/forms/decidim/admin/organization_form.rb +7 -0
  31. data/app/helpers/decidim/admin/admin_terms_helper.rb +47 -0
  32. data/app/helpers/decidim/admin/application_helper.rb +1 -0
  33. data/app/helpers/decidim/admin/dashboard_helper.rb +25 -0
  34. data/app/helpers/decidim/admin/filterable_helper.rb +121 -0
  35. data/app/helpers/decidim/admin/newsletters_helper.rb +18 -0
  36. data/app/helpers/decidim/admin/paginable/per_page_helper.rb +22 -0
  37. data/app/helpers/decidim/admin/scopes_helper.rb +6 -0
  38. data/app/helpers/decidim/admin/settings_helper.rb +18 -2
  39. data/app/helpers/decidim/admin/user_roles_helper.rb +19 -0
  40. data/app/permissions/decidim/admin/permissions.rb +23 -6
  41. data/app/queries/decidim/admin/newsletter_recipients.rb +11 -4
  42. data/app/views/decidim/admin/admin_terms/show.html.erb +26 -0
  43. data/app/views/decidim/admin/components/_component.html.erb +35 -33
  44. data/app/views/decidim/admin/components/index.html.erb +10 -8
  45. data/app/views/decidim/admin/dashboard/show.html.erb +15 -0
  46. data/app/views/decidim/admin/newsletters/index.html.erb +9 -3
  47. data/app/views/decidim/admin/newsletters/select_recipients_to_deliver.html.erb +4 -4
  48. data/app/views/decidim/admin/officializations/index.html.erb +2 -38
  49. data/app/views/decidim/admin/organization/_form.html.erb +16 -0
  50. data/app/views/decidim/admin/shared/_filters.html.erb +40 -0
  51. data/app/views/layouts/decidim/admin/_application.html.erb +1 -1
  52. data/config/locales/ar.yml +40 -6
  53. data/config/locales/ca.yml +45 -6
  54. data/config/locales/cs.yml +45 -6
  55. data/config/locales/de.yml +9 -6
  56. data/config/locales/el.yml +1 -0
  57. data/config/locales/en.yml +45 -6
  58. data/config/locales/es-MX.yml +45 -6
  59. data/config/locales/es-PY.yml +45 -6
  60. data/config/locales/es.yml +45 -6
  61. data/config/locales/eu.yml +9 -6
  62. data/config/locales/fi-plain.yml +45 -6
  63. data/config/locales/fi.yml +45 -6
  64. data/config/locales/fr.yml +9 -6
  65. data/config/locales/gl.yml +9 -6
  66. data/config/locales/hu.yml +45 -6
  67. data/config/locales/id-ID.yml +9 -6
  68. data/config/locales/is-IS.yml +9 -6
  69. data/config/locales/it.yml +45 -6
  70. data/config/locales/nl.yml +28 -6
  71. data/config/locales/no.yml +45 -6
  72. data/config/locales/pl.yml +9 -6
  73. data/config/locales/pt-BR.yml +9 -6
  74. data/config/locales/pt.yml +9 -6
  75. data/config/locales/ru.yml +9 -6
  76. data/config/locales/sv.yml +9 -6
  77. data/config/locales/tr-TR.yml +9 -6
  78. data/config/locales/uk.yml +9 -6
  79. data/config/routes.rb +6 -0
  80. data/db/migrate/20191118112040_add_accepted_admin_terms_at_field_to_users.rb +7 -0
  81. data/lib/decidim/admin.rb +17 -0
  82. data/lib/decidim/admin/form_builder.rb +5 -0
  83. data/lib/decidim/admin/test.rb +2 -0
  84. data/lib/decidim/admin/test/filterable_examples.rb +129 -0
  85. data/lib/decidim/admin/test/manage_paginated_collection_examples.rb +22 -0
  86. data/lib/decidim/admin/version.rb +1 -1
  87. data/vendor/assets/javascripts/jquery.serializejson.js +344 -0
  88. metadata +26 -8
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddAcceptedAdminTermsAtFieldToUsers < ActiveRecord::Migration[5.2]
4
+ def change
5
+ add_column :decidim_users, :admin_terms_accepted_at, :datetime
6
+ end
7
+ end
@@ -11,5 +11,22 @@ module Decidim
11
11
  module Admin
12
12
  autoload :Components, "decidim/admin/components"
13
13
  autoload :FormBuilder, "decidim/admin/form_builder"
14
+
15
+ include ActiveSupport::Configurable
16
+
17
+ # Public Setting that configures Kaminari configuration options
18
+ # https://github.com/kaminari/kaminari#general-configuration-options
19
+
20
+ # Range of number of results per_page. Defaults to [15, 50, 100].
21
+ # per_page_range.first sets the default number per page
22
+ # per_page_range.last sets the default max_per_page
23
+ config_accessor :per_page_range do
24
+ [15, 50, 100]
25
+ end
26
+
27
+ Kaminari.configure do |config|
28
+ config.default_per_page = Decidim::Admin.per_page_range.first
29
+ config.max_per_page = Decidim::Admin.per_page_range.last
30
+ end
14
31
  end
15
32
  end
@@ -60,6 +60,11 @@ module Decidim
60
60
  template += error_for(attribute, options) if error?(attribute)
61
61
  template.html_safe
62
62
  end
63
+
64
+ # Calls Decidim::FormBuilder#editor with default options for admin.
65
+ def editor(name, options = {})
66
+ super(name, { toolbar: :full, lines: 25 }.merge(options))
67
+ end
63
68
  end
64
69
  end
65
70
  end
@@ -5,3 +5,5 @@ require "decidim/admin/test/manage_attachment_collections_examples"
5
5
  require "decidim/admin/test/manage_categories_examples"
6
6
  require "decidim/admin/test/manage_component_permissions_examples"
7
7
  require "decidim/admin/test/manage_moderations_examples"
8
+ require "decidim/admin/test/manage_paginated_collection_examples"
9
+ require "decidim/admin/test/filterable_examples"
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_context "with filterable context" do
4
+ let(:factory_name) { model_name.singular_route_key }
5
+ let(:module_name) { model_name.route_key.camelize }
6
+ let(:filterable_concern) { "Decidim::#{module_name}::Admin::Filterable".constantize }
7
+
8
+ let(:filterable_fake_controller) do
9
+ FILTERABLE_CONCERN ||= filterable_concern
10
+ class FilterableFakeController < Decidim::ApplicationController; include FILTERABLE_CONCERN; end
11
+ end
12
+
13
+ def filterable_method(method_name)
14
+ (@controller ||= filterable_fake_controller.new).send(method_name)
15
+ end
16
+
17
+ def apply_filter(options, filter)
18
+ within(".filters__section") do
19
+ find_link("Filter").hover
20
+ find_link(options).hover
21
+ click_link(filter, href: /q/)
22
+ end
23
+ end
24
+
25
+ def remove_applied_filter(filter)
26
+ within(".label", text: /#{filter}/i) do
27
+ click_link("Cancel")
28
+ end
29
+ end
30
+
31
+ def search_by_text(text)
32
+ within(".filters__section") do
33
+ fill_in("q[#{filterable_method(:search_field_predicate)}]", with: text)
34
+ find("*[type=submit]").click
35
+ end
36
+ end
37
+
38
+ shared_examples "paginating a collection" do
39
+ unless block_given?
40
+ let!(:collection) do
41
+ create_list(factory_name, 50, organization: organization)
42
+ end
43
+ end
44
+
45
+ it_behaves_like "a paginated collection"
46
+ end
47
+
48
+ shared_examples "searching by text" do
49
+ before { search_by_text(text) }
50
+
51
+ it { expect(page).to have_content(text) }
52
+
53
+ after { search_by_text("") }
54
+ end
55
+
56
+ shared_examples "a filtered collection" do |options:, filter:|
57
+ before { apply_filter(options, filter) }
58
+
59
+ it { expect(page).to have_content(in_filter) }
60
+ it { expect(page).not_to have_content(not_in_filter) }
61
+
62
+ it_behaves_like "searching by text" do
63
+ let(:text) { in_filter }
64
+ end
65
+
66
+ context "when removing applied filter" do
67
+ before { remove_applied_filter(filter) }
68
+
69
+ it { expect(page).to have_content(in_filter) }
70
+ it { expect(page).to have_content(not_in_filter) }
71
+
72
+ it_behaves_like "searching by text" do
73
+ let(:text) { not_in_filter }
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ shared_examples "filtering collection by published/unpublished" do
80
+ include_context "with filterable context"
81
+
82
+ unless block_given?
83
+ let!(:published_space) do
84
+ create(factory_name, published_at: Time.current, organization: organization)
85
+ end
86
+
87
+ let!(:unpublished_space) do
88
+ create(factory_name, published_at: nil, organization: organization)
89
+ end
90
+ end
91
+
92
+ it_behaves_like "a filtered collection", options: "Published", filter: "Published" do
93
+ let(:in_filter) { translated(published_space.title) }
94
+ let(:not_in_filter) { translated(unpublished_space.title) }
95
+ end
96
+
97
+ it_behaves_like "a filtered collection", options: "Published", filter: "Unpublished" do
98
+ let(:in_filter) { translated(unpublished_space.title) }
99
+ let(:not_in_filter) { translated(published_space.title) }
100
+ end
101
+
102
+ it_behaves_like "paginating a collection"
103
+ end
104
+
105
+ shared_examples "filtering collection by private/public" do
106
+ include_context "with filterable context"
107
+
108
+ unless block_given?
109
+ let!(:public_space) do
110
+ create(factory_name, private_space: false, organization: organization)
111
+ end
112
+
113
+ let!(:private_space) do
114
+ create(factory_name, private_space: true, organization: organization)
115
+ end
116
+ end
117
+
118
+ it_behaves_like "a filtered collection", options: "Private", filter: "Public" do
119
+ let(:in_filter) { translated(public_space.title) }
120
+ let(:not_in_filter) { translated(private_space.title) }
121
+ end
122
+
123
+ it_behaves_like "a filtered collection", options: "Private", filter: "Private" do
124
+ let(:in_filter) { translated(private_space.title) }
125
+ let(:not_in_filter) { translated(public_space.title) }
126
+ end
127
+
128
+ it_behaves_like "paginating a collection"
129
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples "a paginated collection" do
4
+ before do
5
+ visit current_path
6
+ end
7
+
8
+ describe "Number of results per page" do
9
+ it "lists 15 resources per page by default" do
10
+ expect(page).to have_css(".table-list tbody tr", count: 15)
11
+ end
12
+
13
+ it "changes the number of results per page" do
14
+ within ".results-per-page__dropdown" do
15
+ page.find("a", text: "15").click
16
+ click_link "50"
17
+ end
18
+
19
+ expect(page).to have_selector(".table-list tbody tr", count: 50)
20
+ end
21
+ end
22
+ end
@@ -4,7 +4,7 @@ module Decidim
4
4
  # This holds the decidim-admin version.
5
5
  module Admin
6
6
  def self.version
7
- "0.20.1"
7
+ "0.21.0"
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,344 @@
1
+ /*!
2
+ SerializeJSON jQuery plugin.
3
+ https://github.com/marioizquierdo/jquery.serializeJSON
4
+ version 2.9.0 (Jan, 2018)
5
+
6
+ Copyright (c) 2012-2018 Mario Izquierdo
7
+ Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
8
+ and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
9
+ */
10
+ (function (factory) {
11
+ if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module.
12
+ define(['jquery'], factory);
13
+ } else if (typeof exports === 'object') { // Node/CommonJS
14
+ var jQuery = require('jquery');
15
+ module.exports = factory(jQuery);
16
+ } else { // Browser globals (zepto supported)
17
+ factory(window.jQuery || window.Zepto || window.$); // Zepto supported on browsers as well
18
+ }
19
+
20
+ }(function ($) {
21
+ "use strict";
22
+
23
+ // jQuery('form').serializeJSON()
24
+ $.fn.serializeJSON = function (options) {
25
+ var f, $form, opts, formAsArray, serializedObject, name, value, parsedValue, _obj, nameWithNoType, type, keys, skipFalsy;
26
+ f = $.serializeJSON;
27
+ $form = this; // NOTE: the set of matched elements is most likely a form, but it could also be a group of inputs
28
+ opts = f.setupOpts(options); // calculate values for options {parseNumbers, parseBoolens, parseNulls, ...} with defaults
29
+
30
+ // Use native `serializeArray` function to get an array of {name, value} objects.
31
+ formAsArray = $form.serializeArray();
32
+ f.readCheckboxUncheckedValues(formAsArray, opts, $form); // add objects to the array from unchecked checkboxes if needed
33
+
34
+ // Convert the formAsArray into a serializedObject with nested keys
35
+ serializedObject = {};
36
+ $.each(formAsArray, function (i, obj) {
37
+ name = obj.name; // original input name
38
+ value = obj.value; // input value
39
+ _obj = f.extractTypeAndNameWithNoType(name);
40
+ nameWithNoType = _obj.nameWithNoType; // input name with no type (i.e. "foo:string" => "foo")
41
+ type = _obj.type; // type defined from the input name in :type colon notation
42
+ if (!type) type = f.attrFromInputWithName($form, name, 'data-value-type');
43
+ f.validateType(name, type, opts); // make sure that the type is one of the valid types if defined
44
+
45
+ if (type !== 'skip') { // ignore inputs with type 'skip'
46
+ keys = f.splitInputNameIntoKeysArray(nameWithNoType);
47
+ parsedValue = f.parseValue(value, name, type, opts); // convert to string, number, boolean, null or customType
48
+
49
+ skipFalsy = !parsedValue && f.shouldSkipFalsy($form, name, nameWithNoType, type, opts); // ignore falsy inputs if specified
50
+ if (!skipFalsy) {
51
+ f.deepSet(serializedObject, keys, parsedValue, opts);
52
+ }
53
+ }
54
+ });
55
+ return serializedObject;
56
+ };
57
+
58
+ // Use $.serializeJSON as namespace for the auxiliar functions
59
+ // and to define defaults
60
+ $.serializeJSON = {
61
+
62
+ defaultOptions: {
63
+ checkboxUncheckedValue: undefined, // to include that value for unchecked checkboxes (instead of ignoring them)
64
+
65
+ parseNumbers: false, // convert values like "1", "-2.33" to 1, -2.33
66
+ parseBooleans: false, // convert "true", "false" to true, false
67
+ parseNulls: false, // convert "null" to null
68
+ parseAll: false, // all of the above
69
+ parseWithFunction: null, // to use custom parser, a function like: function(val){ return parsed_val; }
70
+
71
+ skipFalsyValuesForTypes: [], // skip serialization of falsy values for listed value types
72
+ skipFalsyValuesForFields: [], // skip serialization of falsy values for listed field names
73
+
74
+ customTypes: {}, // override defaultTypes
75
+ defaultTypes: {
76
+ "string": function(str) { return String(str); },
77
+ "number": function(str) { return Number(str); },
78
+ "boolean": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1; },
79
+ "null": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1 ? str : null; },
80
+ "array": function(str) { return JSON.parse(str); },
81
+ "object": function(str) { return JSON.parse(str); },
82
+ "auto": function(str) { return $.serializeJSON.parseValue(str, null, null, {parseNumbers: true, parseBooleans: true, parseNulls: true}); }, // try again with something like "parseAll"
83
+ "skip": null // skip is a special type that makes it easy to ignore elements
84
+ },
85
+
86
+ useIntKeysAsArrayIndex: false // name="foo[2]" value="v" => {foo: [null, null, "v"]}, instead of {foo: ["2": "v"]}
87
+ },
88
+
89
+ // Merge option defaults into the options
90
+ setupOpts: function(options) {
91
+ var opt, validOpts, defaultOptions, optWithDefault, parseAll, f;
92
+ f = $.serializeJSON;
93
+
94
+ if (options == null) { options = {}; } // options ||= {}
95
+ defaultOptions = f.defaultOptions || {}; // defaultOptions
96
+
97
+ // Make sure that the user didn't misspell an option
98
+ validOpts = ['checkboxUncheckedValue', 'parseNumbers', 'parseBooleans', 'parseNulls', 'parseAll', 'parseWithFunction', 'skipFalsyValuesForTypes', 'skipFalsyValuesForFields', 'customTypes', 'defaultTypes', 'useIntKeysAsArrayIndex']; // re-define because the user may override the defaultOptions
99
+ for (opt in options) {
100
+ if (validOpts.indexOf(opt) === -1) {
101
+ throw new Error("serializeJSON ERROR: invalid option '" + opt + "'. Please use one of " + validOpts.join(', '));
102
+ }
103
+ }
104
+
105
+ // Helper to get the default value for this option if none is specified by the user
106
+ optWithDefault = function(key) { return (options[key] !== false) && (options[key] !== '') && (options[key] || defaultOptions[key]); };
107
+
108
+ // Return computed options (opts to be used in the rest of the script)
109
+ parseAll = optWithDefault('parseAll');
110
+ return {
111
+ checkboxUncheckedValue: optWithDefault('checkboxUncheckedValue'),
112
+
113
+ parseNumbers: parseAll || optWithDefault('parseNumbers'),
114
+ parseBooleans: parseAll || optWithDefault('parseBooleans'),
115
+ parseNulls: parseAll || optWithDefault('parseNulls'),
116
+ parseWithFunction: optWithDefault('parseWithFunction'),
117
+
118
+ skipFalsyValuesForTypes: optWithDefault('skipFalsyValuesForTypes'),
119
+ skipFalsyValuesForFields: optWithDefault('skipFalsyValuesForFields'),
120
+ typeFunctions: $.extend({}, optWithDefault('defaultTypes'), optWithDefault('customTypes')),
121
+
122
+ useIntKeysAsArrayIndex: optWithDefault('useIntKeysAsArrayIndex')
123
+ };
124
+ },
125
+
126
+ // Given a string, apply the type or the relevant "parse" options, to return the parsed value
127
+ parseValue: function(valStr, inputName, type, opts) {
128
+ var f, parsedVal;
129
+ f = $.serializeJSON;
130
+ parsedVal = valStr; // if no parsing is needed, the returned value will be the same
131
+
132
+ if (opts.typeFunctions && type && opts.typeFunctions[type]) { // use a type if available
133
+ parsedVal = opts.typeFunctions[type](valStr);
134
+ } else if (opts.parseNumbers && f.isNumeric(valStr)) { // auto: number
135
+ parsedVal = Number(valStr);
136
+ } else if (opts.parseBooleans && (valStr === "true" || valStr === "false")) { // auto: boolean
137
+ parsedVal = (valStr === "true");
138
+ } else if (opts.parseNulls && valStr == "null") { // auto: null
139
+ parsedVal = null;
140
+ } else if (opts.typeFunctions && opts.typeFunctions["string"]) { // make sure to apply :string type if it was re-defined
141
+ parsedVal = opts.typeFunctions["string"](valStr);
142
+ }
143
+
144
+ // Custom parse function: apply after parsing options, unless there's an explicit type.
145
+ if (opts.parseWithFunction && !type) {
146
+ parsedVal = opts.parseWithFunction(parsedVal, inputName);
147
+ }
148
+
149
+ return parsedVal;
150
+ },
151
+
152
+ isObject: function(obj) { return obj === Object(obj); }, // is it an Object?
153
+ isUndefined: function(obj) { return obj === void 0; }, // safe check for undefined values
154
+ isValidArrayIndex: function(val) { return /^[0-9]+$/.test(String(val)); }, // 1,2,3,4 ... are valid array indexes
155
+ isNumeric: function(obj) { return obj - parseFloat(obj) >= 0; }, // taken from jQuery.isNumeric implementation. Not using jQuery.isNumeric to support old jQuery and Zepto versions
156
+
157
+ optionKeys: function(obj) { if (Object.keys) { return Object.keys(obj); } else { var key, keys = []; for(key in obj){ keys.push(key); } return keys;} }, // polyfill Object.keys to get option keys in IE<9
158
+
159
+
160
+ // Fill the formAsArray object with values for the unchecked checkbox inputs,
161
+ // using the same format as the jquery.serializeArray function.
162
+ // The value of the unchecked values is determined from the opts.checkboxUncheckedValue
163
+ // and/or the data-unchecked-value attribute of the inputs.
164
+ readCheckboxUncheckedValues: function (formAsArray, opts, $form) {
165
+ var selector, $uncheckedCheckboxes, $el, uncheckedValue, f, name;
166
+ if (opts == null) { opts = {}; }
167
+ f = $.serializeJSON;
168
+
169
+ selector = 'input[type=checkbox][name]:not(:checked):not([disabled])';
170
+ $uncheckedCheckboxes = $form.find(selector).add($form.filter(selector));
171
+ $uncheckedCheckboxes.each(function (i, el) {
172
+ // Check data attr first, then the option
173
+ $el = $(el);
174
+ uncheckedValue = $el.attr('data-unchecked-value');
175
+ if (uncheckedValue == null) {
176
+ uncheckedValue = opts.checkboxUncheckedValue;
177
+ }
178
+
179
+ // If there's an uncheckedValue, push it into the serialized formAsArray
180
+ if (uncheckedValue != null) {
181
+ if (el.name && el.name.indexOf("[][") !== -1) { // identify a non-supported
182
+ throw new Error("serializeJSON ERROR: checkbox unchecked values are not supported on nested arrays of objects like '"+el.name+"'. See https://github.com/marioizquierdo/jquery.serializeJSON/issues/67");
183
+ }
184
+ formAsArray.push({name: el.name, value: uncheckedValue});
185
+ }
186
+ });
187
+ },
188
+
189
+ // Returns and object with properties {name_without_type, type} from a given name.
190
+ // The type is null if none specified. Example:
191
+ // "foo" => {nameWithNoType: "foo", type: null}
192
+ // "foo:boolean" => {nameWithNoType: "foo", type: "boolean"}
193
+ // "foo[bar]:null" => {nameWithNoType: "foo[bar]", type: "null"}
194
+ extractTypeAndNameWithNoType: function(name) {
195
+ var match;
196
+ if (match = name.match(/(.*):([^:]+)$/)) {
197
+ return {nameWithNoType: match[1], type: match[2]};
198
+ } else {
199
+ return {nameWithNoType: name, type: null};
200
+ }
201
+ },
202
+
203
+
204
+ // Check if this input should be skipped when it has a falsy value,
205
+ // depending on the options to skip values by name or type, and the data-skip-falsy attribute.
206
+ shouldSkipFalsy: function($form, name, nameWithNoType, type, opts) {
207
+ var f = $.serializeJSON;
208
+
209
+ var skipFromDataAttr = f.attrFromInputWithName($form, name, 'data-skip-falsy');
210
+ if (skipFromDataAttr != null) {
211
+ return skipFromDataAttr !== 'false'; // any value is true, except if explicitly using 'false'
212
+ }
213
+
214
+ var optForFields = opts.skipFalsyValuesForFields;
215
+ if (optForFields && (optForFields.indexOf(nameWithNoType) !== -1 || optForFields.indexOf(name) !== -1)) {
216
+ return true;
217
+ }
218
+
219
+ var optForTypes = opts.skipFalsyValuesForTypes;
220
+ if (type == null) type = 'string'; // assume fields with no type are targeted as string
221
+ if (optForTypes && optForTypes.indexOf(type) !== -1) {
222
+ return true
223
+ }
224
+
225
+ return false;
226
+ },
227
+
228
+ // Finds the first input in $form with this name, and get the given attr from it.
229
+ // Returns undefined if no input or no attribute was found.
230
+ attrFromInputWithName: function($form, name, attrName) {
231
+ var escapedName, selector, $input, attrValue;
232
+ escapedName = name.replace(/(:|\.|\[|\]|\s)/g,'\\$1'); // every non-standard character need to be escaped by \\
233
+ selector = '[name="' + escapedName + '"]';
234
+ $input = $form.find(selector).add($form.filter(selector)); // NOTE: this returns only the first $input element if multiple are matched with the same name (i.e. an "array[]"). So, arrays with different element types specified through the data-value-type attr is not supported.
235
+ return $input.attr(attrName);
236
+ },
237
+
238
+ // Raise an error if the type is not recognized.
239
+ validateType: function(name, type, opts) {
240
+ var validTypes, f;
241
+ f = $.serializeJSON;
242
+ validTypes = f.optionKeys(opts ? opts.typeFunctions : f.defaultOptions.defaultTypes);
243
+ if (!type || validTypes.indexOf(type) !== -1) {
244
+ return true;
245
+ } else {
246
+ throw new Error("serializeJSON ERROR: Invalid type " + type + " found in input name '" + name + "', please use one of " + validTypes.join(', '));
247
+ }
248
+ },
249
+
250
+
251
+ // Split the input name in programatically readable keys.
252
+ // Examples:
253
+ // "foo" => ['foo']
254
+ // "[foo]" => ['foo']
255
+ // "foo[inn][bar]" => ['foo', 'inn', 'bar']
256
+ // "foo[inn[bar]]" => ['foo', 'inn', 'bar']
257
+ // "foo[inn][arr][0]" => ['foo', 'inn', 'arr', '0']
258
+ // "arr[][val]" => ['arr', '', 'val']
259
+ splitInputNameIntoKeysArray: function(nameWithNoType) {
260
+ var keys, f;
261
+ f = $.serializeJSON;
262
+ keys = nameWithNoType.split('['); // split string into array
263
+ keys = $.map(keys, function (key) { return key.replace(/\]/g, ''); }); // remove closing brackets
264
+ if (keys[0] === '') { keys.shift(); } // ensure no opening bracket ("[foo][inn]" should be same as "foo[inn]")
265
+ return keys;
266
+ },
267
+
268
+ // Set a value in an object or array, using multiple keys to set in a nested object or array:
269
+ //
270
+ // deepSet(obj, ['foo'], v) // obj['foo'] = v
271
+ // deepSet(obj, ['foo', 'inn'], v) // obj['foo']['inn'] = v // Create the inner obj['foo'] object, if needed
272
+ // deepSet(obj, ['foo', 'inn', '123'], v) // obj['foo']['arr']['123'] = v //
273
+ //
274
+ // deepSet(obj, ['0'], v) // obj['0'] = v
275
+ // deepSet(arr, ['0'], v, {useIntKeysAsArrayIndex: true}) // arr[0] = v
276
+ // deepSet(arr, [''], v) // arr.push(v)
277
+ // deepSet(obj, ['arr', ''], v) // obj['arr'].push(v)
278
+ //
279
+ // arr = [];
280
+ // deepSet(arr, ['', v] // arr => [v]
281
+ // deepSet(arr, ['', 'foo'], v) // arr => [v, {foo: v}]
282
+ // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}]
283
+ // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}, {bar: v}]
284
+ //
285
+ deepSet: function (o, keys, value, opts) {
286
+ var key, nextKey, tail, lastIdx, lastVal, f;
287
+ if (opts == null) { opts = {}; }
288
+ f = $.serializeJSON;
289
+ if (f.isUndefined(o)) { throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined"); }
290
+ if (!keys || keys.length === 0) { throw new Error("ArgumentError: param 'keys' expected to be an array with least one element"); }
291
+
292
+ key = keys[0];
293
+
294
+ // Only one key, then it's not a deepSet, just assign the value.
295
+ if (keys.length === 1) {
296
+ if (key === '') {
297
+ o.push(value); // '' is used to push values into the array (assume o is an array)
298
+ } else {
299
+ o[key] = value; // other keys can be used as object keys or array indexes
300
+ }
301
+
302
+ // With more keys is a deepSet. Apply recursively.
303
+ } else {
304
+ nextKey = keys[1];
305
+
306
+ // '' is used to push values into the array,
307
+ // with nextKey, set the value into the same object, in object[nextKey].
308
+ // Covers the case of ['', 'foo'] and ['', 'var'] to push the object {foo, var}, and the case of nested arrays.
309
+ if (key === '') {
310
+ lastIdx = o.length - 1; // asume o is array
311
+ lastVal = o[lastIdx];
312
+ if (f.isObject(lastVal) && (f.isUndefined(lastVal[nextKey]) || keys.length > 2)) { // if nextKey is not present in the last object element, or there are more keys to deep set
313
+ key = lastIdx; // then set the new value in the same object element
314
+ } else {
315
+ key = lastIdx + 1; // otherwise, point to set the next index in the array
316
+ }
317
+ }
318
+
319
+ // '' is used to push values into the array "array[]"
320
+ if (nextKey === '') {
321
+ if (f.isUndefined(o[key]) || !$.isArray(o[key])) {
322
+ o[key] = []; // define (or override) as array to push values
323
+ }
324
+ } else {
325
+ if (opts.useIntKeysAsArrayIndex && f.isValidArrayIndex(nextKey)) { // if 1, 2, 3 ... then use an array, where nextKey is the index
326
+ if (f.isUndefined(o[key]) || !$.isArray(o[key])) {
327
+ o[key] = []; // define (or override) as array, to insert values using int keys as array indexes
328
+ }
329
+ } else { // for anything else, use an object, where nextKey is going to be the attribute name
330
+ if (f.isUndefined(o[key]) || !f.isObject(o[key])) {
331
+ o[key] = {}; // define (or override) as object, to set nested properties
332
+ }
333
+ }
334
+ }
335
+
336
+ // Recursively set the inner object
337
+ tail = keys.slice(1);
338
+ f.deepSet(o[key], tail, value, opts);
339
+ }
340
+ }
341
+
342
+ };
343
+
344
+ }));