inline_forms 8.1.0 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3b934b94b7f4b5298dea2e2a51b8c97c37c06969a3484d528e336edf17ddfde
4
- data.tar.gz: fedfafb7fba445712299b4c240fc905418c665d8f29e139c17cb9abc4138fe36
3
+ metadata.gz: 86869c3131530e4764f1a16620e48bd0eaf920461f0471697c07d2e30bda69f3
4
+ data.tar.gz: 5c304a602f87fcc37b403d3b2299b5f467da1eef65f1fde8d624cadc8085b1c9
5
5
  SHA512:
6
- metadata.gz: b9d1abfecdf6fa9c75c4d79967e8604e0bb408a40a08475fb7106e12b0b301a437e8192f836a39b9b5c75261ac97d5dfade6f2ba5ecea9005015814bd257028f
7
- data.tar.gz: 5f62f997de89dd93334a6324d2d02fe3bd283036d3f68ee39947cd400ab57f6d3a2d959745498d2b7f5e5c567889cd02e258ed7c15fef3cf3f051f3a21ee3d60
6
+ metadata.gz: 940a25c0f1ac072dbc7c1bd098e72f37e81b5f24e96230e399bbf0d4e22eba3260177ce2029751ada4c572d9e04585dafd97cfac7513ae9848c8deb5c91ed8a4
7
+ data.tar.gz: 372c3f53240ded87cee02386f6b0b52586c9abb5fec8889b2968483383f4f01c09483f408a06eb6a1ccb0716c5b01ab4db3d6dfb9e76cc72f555a16739ca3f07
data/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [8.1.1] - 2026-05-26
8
+
9
+ ### Added
10
+
11
+ - **`rails g inline_forms_addto <Model> attr:type [...]`:** new generator that adds fields to an *existing* inline_forms model. Emits an additive migration (`add_column` for scalars, `add_reference :table, :name, foreign_key: true` for `:belongs_to`/`:references`/`:dropdown`, single `:string` column for `:image_field`/`:audio_field`; no column for `:rich_text`/`has_many`/`has_one`/`habtm`) and edits the model file: injects `belongs_to` / `has_many` / `has_one` / `has_and_belongs_to_many` / `has_rich_text` / `mount_uploader` lines (idempotent), and inserts new rows into the model's `inline_forms_attribute_list` (handles both the generator-shaped and the installer's hand-written `User`/`Member` block). Does NOT touch routes, controller, or `MODEL_TABS` — those are already wired for the existing model. Use case: extending the narrow `User` (or `--user-model Member`) shipped by the installer with app-specific fields like `occupation`, `birthdate`, `organization:dropdown`, `bio:rich_text` without hand-writing migrations or attribute-list edits. Plan: [`stuff/inline_forms_addto.md`](stuff/inline_forms_addto.md).
12
+ - **`--allow-unknown` parity** with `inline_forms`: unknown field types raise `Thor::Error` by default; pass `--allow-unknown` to keep the legacy commented output (`# add_column …`, `# [ :name , "name", :unknown ]`).
13
+ - **`--replace`** flag: opt-in rewrite of existing `_presentation` / `_list_order` (and the legacy `_order`) / `_list_search` scopes/methods. Without `--replace` these special names are skipped with `say_status :skipped` so re-runs don't silently mutate the model's ordering/search policy.
14
+
15
+ ### Changed
16
+
17
+ - **Refactor — `Rails::Generators::GeneratedAttribute` overrides moved to `lib/generators/inline_forms_attribute_overrides.rb`** and shared by both `InlineFormsGenerator` (`rails g inline_forms`) and the new `InlineFormsAddtoGenerator` (`rails g inline_forms_addto`). Pure refactor: same `column_type` / `attribute_type` / `relation?` / `migration?` / `attribute?` / `SPECIAL_GENERATOR_NAMES` semantics, now qualified with `InlineForms::` constants so the file works outside the `module InlineForms` lexical scope.
18
+
19
+ ### Notes
20
+
21
+ - The new generator class is top-level (`InlineFormsAddtoGenerator`, not `InlineForms::InlineFormsAddtoGenerator`) so `rails g inline_forms_addto …` resolves directly. Rails' generator lookup only collapses `name:name` namespaces (the `inline_forms:inline_forms` → `inline_forms` rule that `InlineFormsGenerator` relies on). A `module InlineForms; InlineFormsAddtoGenerator = ::InlineFormsAddtoGenerator; end` alias is provided for any programmatic invokers that reach for the namespaced constant.
22
+
7
23
  ## [8.1.0] - 2026-05-25
8
24
 
9
25
  ### Added
@@ -0,0 +1,291 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "inline_forms"
3
+ require_relative "inline_forms_attribute_overrides"
4
+
5
+ # == Usage
6
+ #
7
+ # rails g inline_forms_addto Model attribute:type [attribute:type ...] [options]
8
+ #
9
+ # Adds fields to an existing inline_forms model. Emits an additive
10
+ # migration (`add_column` / `add_reference` / single :string column for
11
+ # `:image_field`/`:audio_field`) and edits the model file:
12
+ #
13
+ # * adds `belongs_to` / `has_many` / `has_one` / `has_and_belongs_to_many` /
14
+ # `has_rich_text` / `mount_uploader` lines (idempotent)
15
+ # * inserts new rows into the model's `inline_forms_attribute_list`
16
+ #
17
+ # Does NOT touch routes, the controller, or `MODEL_TABS` (those are
18
+ # already wired for the existing model). Use `rails g inline_forms <Model>`
19
+ # to create new models.
20
+ #
21
+ # == Special generator names
22
+ #
23
+ # `_no_model`, `_no_migration`, `_id`, `_enabled` are install-time only and
24
+ # are rejected. `_presentation`, `_list_order`, `_list_search` are
25
+ # accepted but only acted on when `--replace` is passed; otherwise the
26
+ # generator skips them with a `say_status :skipped` notice.
27
+ #
28
+ # Top-level (no `InlineForms` module wrapper) so Rails resolves
29
+ # `rails g inline_forms_addto Model …` to this class without the
30
+ # `inline_forms:inline_forms_addto` double-segment trick that Rails only
31
+ # applies for matching `name:name` pairs (`inline_forms:inline_forms`).
32
+ class InlineFormsAddtoGenerator < Rails::Generators::NamedBase
33
+ INSTALL_TIME_ONLY_NAMES = %w[_no_model _no_migration _id _enabled].freeze
34
+ REPLACE_ONLY_NAMES = %w[_presentation _list_order _list_search _order].freeze
35
+
36
+ argument :attributes, type: :array, banner: "[name:form_element]..."
37
+ class_option :allow_unknown, type: :boolean, default: false,
38
+ desc: "Allow unknown field types (legacy behavior: comment generated lines instead of failing)."
39
+ class_option :replace, type: :boolean, default: false,
40
+ desc: "Replace existing _presentation/_list_order/_list_search instead of skipping."
41
+
42
+ source_root File.expand_path("templates", __dir__)
43
+
44
+ def validate!
45
+ unless File.exist?(File.join(destination_root, model_file_path))
46
+ raise Thor::Error,
47
+ "Model #{model_file_path} not found. " \
48
+ "Use `rails g inline_forms #{name} ...` for new models."
49
+ end
50
+ end
51
+
52
+ def set_some_flags
53
+ @unknown_attributes = []
54
+ @forbidden_names = []
55
+ @skipped_replace_names = []
56
+ attributes.each do |attribute|
57
+ if INSTALL_TIME_ONLY_NAMES.include?(attribute.name)
58
+ @forbidden_names << attribute.name
59
+ next
60
+ end
61
+ if REPLACE_ONLY_NAMES.include?(attribute.name) && !options[:replace]
62
+ @skipped_replace_names << "#{attribute.name}:#{attribute.type}"
63
+ next
64
+ end
65
+ if !attribute.name.start_with?("_") && (
66
+ (attribute.attribute? && attribute.attribute_type == :unknown) ||
67
+ (attribute.migration? && attribute.column_type == :unknown)
68
+ )
69
+ @unknown_attributes << "#{attribute.name}:#{attribute.type}"
70
+ end
71
+ end
72
+
73
+ unless @forbidden_names.empty?
74
+ raise Thor::Error,
75
+ "Names #{@forbidden_names.uniq.join(', ')} are install-time only " \
76
+ "and not supported by inline_forms_addto."
77
+ end
78
+
79
+ allow_unknown = options[:allow_unknown].to_s == "true"
80
+ if !allow_unknown && !@unknown_attributes.empty?
81
+ raise Thor::Error,
82
+ "Unknown field type(s): #{@unknown_attributes.uniq.join(', ')}. " \
83
+ "Add a valid form element type or pass --allow-unknown to keep legacy commented output."
84
+ end
85
+
86
+ @skipped_replace_names.each do |label|
87
+ say_status :skipped, "#{label} (pass --replace to rewrite the existing scope/method)", :yellow
88
+ end
89
+ end
90
+
91
+ def generate_migration
92
+ @migration_lines = String.new
93
+ attributes.each do |attribute|
94
+ next if INSTALL_TIME_ONLY_NAMES.include?(attribute.name)
95
+ next if REPLACE_ONLY_NAMES.include?(attribute.name)
96
+ next unless attribute.migration?
97
+
98
+ if attribute.column_type == :belongs_to
99
+ @migration_lines << " add_reference :#{table_name}, :#{attribute.name}, foreign_key: true\n"
100
+ else
101
+ commenter = attribute.attribute_type == :unknown ? "#" : " "
102
+ @migration_lines << "#{commenter} add_column :#{table_name}, :#{attribute.name}, :#{attribute.column_type}\n"
103
+ end
104
+ end
105
+
106
+ return if @migration_lines.empty?
107
+
108
+ template "add_columns_migration.erb",
109
+ "db/migrate/#{time_stamp}_#{migration_file_basename}.rb"
110
+ end
111
+
112
+ def update_model
113
+ attributes.each do |attribute|
114
+ next if INSTALL_TIME_ONLY_NAMES.include?(attribute.name)
115
+ next if REPLACE_ONLY_NAMES.include?(attribute.name) && !options[:replace]
116
+
117
+ if REPLACE_ONLY_NAMES.include?(attribute.name) && options[:replace]
118
+ replace_special!(attribute)
119
+ next
120
+ end
121
+
122
+ case attribute.column_type
123
+ when :belongs_to
124
+ inject_class_line!(" belongs_to :#{attribute.name}\n")
125
+ end
126
+
127
+ case attribute.type
128
+ when :image_field, :audio_field
129
+ uploader_class = "#{attribute.name}_uploader".camelcase
130
+ inject_class_line!(" mount_uploader :#{attribute.name}, #{uploader_class}\n")
131
+ when :has_many
132
+ inject_class_line!(" has_many :#{attribute.name}\n")
133
+ when :has_many_destroy
134
+ inject_class_line!(" has_many :#{attribute.name}, :dependent => :destroy\n")
135
+ when :has_one
136
+ inject_class_line!(" has_one :#{attribute.name}\n")
137
+ when :habtm, :has_and_belongs_to_many, :check_list
138
+ inject_class_line!(" has_and_belongs_to_many :#{attribute.name}\n")
139
+ when :rich_text
140
+ inject_class_line!(" has_rich_text :#{attribute.name}\n")
141
+ end
142
+
143
+ next unless attribute.attribute?
144
+
145
+ add_attribute_list_row!(attribute)
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def model_file_name
152
+ name.underscore
153
+ end
154
+
155
+ def model_file_path
156
+ "app/models/#{model_file_name}.rb"
157
+ end
158
+
159
+ def model_class_name
160
+ name.camelize
161
+ end
162
+
163
+ def table_name
164
+ name.pluralize.underscore
165
+ end
166
+
167
+ def migration_class_name
168
+ "InlineFormsAddTo#{table_name.camelize}#{migration_class_suffix}"
169
+ end
170
+
171
+ # Suffix keeps re-runs from colliding (Rails would reject duplicate
172
+ # migration class names). Built from the column names being added.
173
+ def migration_class_suffix
174
+ cols = attributes.reject { |a| INSTALL_TIME_ONLY_NAMES.include?(a.name) || REPLACE_ONLY_NAMES.include?(a.name) }
175
+ .map { |a| a.name.camelize }
176
+ cols.empty? ? "Fields" : cols.first(3).join
177
+ end
178
+
179
+ def migration_file_basename
180
+ cols = attributes.reject { |a| INSTALL_TIME_ONLY_NAMES.include?(a.name) || REPLACE_ONLY_NAMES.include?(a.name) }
181
+ .map(&:name)
182
+ label = cols.first(3).join("_")
183
+ label = "fields" if label.empty?
184
+ "inline_forms_add_to_#{table_name}_#{label}"
185
+ end
186
+
187
+ def time_stamp
188
+ # Mirrors InlineFormsGenerator#time_stamp.
189
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
190
+ end
191
+
192
+ # Idempotent inject: skip if `line` (sans trailing newline) already
193
+ # appears inside the model file. Uses `inject_into_class` so the
194
+ # injection lands at the top of the class body (after `class Foo <
195
+ # ApplicationRecord`).
196
+ def inject_class_line!(line)
197
+ content = File.read(File.join(destination_root, model_file_path))
198
+ probe = line.rstrip
199
+ if content.include?(probe)
200
+ say_status :identical, "#{model_file_path}: #{probe.strip}", :blue
201
+ return
202
+ end
203
+ inject_into_class model_file_path, model_class_name, line
204
+ end
205
+
206
+ # Inserts a new row into the existing `inline_forms_attribute_list`
207
+ # method. Falls back to appending a fresh method when the model has
208
+ # only `attr_writer :inline_forms_attribute_list` (inherited from
209
+ # `ApplicationRecord`) and no body.
210
+ def add_attribute_list_row!(attribute)
211
+ content = File.read(File.join(destination_root, model_file_path))
212
+ row = " [ :#{attribute.name} , \"#{attribute.name}\", :#{attribute.attribute_type} ],\n"
213
+
214
+ if content.match?(/@inline_forms_attribute_list \|\|=\s*\[/)
215
+ if content.match?(/\[\s*:#{Regexp.escape(attribute.name)}\s*,/)
216
+ say_status :identical, "#{model_file_path}: row :#{attribute.name}", :blue
217
+ return
218
+ end
219
+ # Insert before the closing `]` of the array, keeping appended order.
220
+ # Match against the file's full text and write it back so we work
221
+ # around Thor's `gsub_file` block-arg quirk (the block sees only the
222
+ # full match, no $1/$2). MatchData captures are explicit here.
223
+ m = content.match(/(@inline_forms_attribute_list \|\|=\s*\[(?:.|\n)*?)(\n\s*\]\s*\n\s*end)/)
224
+ if m
225
+ replacement = m[1] + "\n" + row.chomp + m[2]
226
+ new_content = content.sub(m[0], replacement)
227
+ File.write(File.join(destination_root, model_file_path), new_content)
228
+ say_status :insert, "#{model_file_path}: row :#{attribute.name}", :green
229
+ else
230
+ say_status :warn, "#{model_file_path}: could not locate end of @inline_forms_attribute_list, skipping :#{attribute.name}", :yellow
231
+ end
232
+ else
233
+ say_status :warn, "#{model_file_path}: no inline_forms_attribute_list found, appending a fresh method", :yellow
234
+ body = <<~RUBY.gsub(/^/, " ")
235
+
236
+ def inline_forms_attribute_list
237
+ @inline_forms_attribute_list ||= [
238
+ [ :#{attribute.name} , "#{attribute.name}", :#{attribute.attribute_type} ],
239
+ ]
240
+ end
241
+ RUBY
242
+ inject_into_class model_file_path, model_class_name, body
243
+ end
244
+ end
245
+
246
+ def replace_special!(attribute)
247
+ case attribute.name
248
+ when "_presentation"
249
+ new_method = " def _presentation\n \"#{attribute.type}\"\n end\n"
250
+ if File.read(File.join(destination_root, model_file_path)).match?(/def _presentation\b/)
251
+ gsub_file model_file_path,
252
+ / def _presentation\b.*?\n end\n/m,
253
+ new_method
254
+ else
255
+ inject_into_class model_file_path, model_class_name, "\n#{new_method}"
256
+ end
257
+ when "_list_order", "_order"
258
+ col = attribute.type
259
+ new_scope = " scope :inline_forms_list, -> { order(:#{col}, :id) }\n"
260
+ new_spaceship = " def <=>(other)\n self.#{col} <=> other.#{col}\n end\n"
261
+ path = File.join(destination_root, model_file_path)
262
+ content = File.read(path)
263
+ if content.match?(/scope :inline_forms_list\b/)
264
+ gsub_file model_file_path, / scope :inline_forms_list, ->.*\n/, new_scope
265
+ else
266
+ inject_into_class model_file_path, model_class_name, new_scope
267
+ end
268
+ if content.match?(/def <=>\(other\)/)
269
+ gsub_file model_file_path, / def <=>\(other\).*?\n end\n/m, new_spaceship
270
+ else
271
+ inject_into_class model_file_path, model_class_name, "\n#{new_spaceship}"
272
+ end
273
+ when "_list_search"
274
+ col = attribute.type
275
+ new_scope = " scope :inline_forms_search, ->(q) { where(\"#{col} LIKE ?\", \"%\#{q}%\") }\n"
276
+ if File.read(File.join(destination_root, model_file_path)).match?(/scope :inline_forms_search\b/)
277
+ gsub_file model_file_path, / scope :inline_forms_search, ->.*\n/, new_scope
278
+ else
279
+ inject_into_class model_file_path, model_class_name, new_scope
280
+ end
281
+ end
282
+ end
283
+ end
284
+
285
+ module InlineForms
286
+ # Backwards-compatible alias (tests and any programmatic invokers that
287
+ # addressed the namespaced version while the generator was scoped to
288
+ # `module InlineForms`). Rails generator discovery uses the top-level
289
+ # constant above.
290
+ InlineFormsAddtoGenerator = ::InlineFormsAddtoGenerator unless const_defined?(:InlineFormsAddtoGenerator, false)
291
+ end
@@ -0,0 +1,85 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "rails/generators"
3
+ require "rails/generators/generated_attribute"
4
+ require "inline_forms"
5
+
6
+ # Shared `Rails::Generators::GeneratedAttribute` extensions used by both
7
+ # `InlineForms::InlineFormsGenerator` (`rails g inline_forms`) and
8
+ # `InlineForms::InlineFormsAddtoGenerator` (`rails g inline_forms_addto`).
9
+ #
10
+ # Loaded once; idempotent (the `class_eval` re-defines methods to the
11
+ # same bodies on a re-require, no behavior drift).
12
+ Rails::Generators::GeneratedAttribute.class_eval do
13
+ # Override Rails::Generators::GeneratedAttribute.valid_type? so that our
14
+ # custom field types (dropdown, check_list, image_field, rich_text, ...)
15
+ # pass through parsing. We do our own unknown-type detection later (with
16
+ # Thor::Error + --allow-unknown), so it is safe to accept everything here.
17
+ #
18
+ # Rails 6.1 used to rescue ActiveRecord::Base.connection failures, which
19
+ # masked the issue; Rails 7+ raises NameError when ActiveRecord is not
20
+ # loaded yet, breaking generator unit tests.
21
+ def self.valid_type?(_type)
22
+ true
23
+ end
24
+
25
+ # Deducts the column_type for migrations from the type.
26
+ #
27
+ # We first merge the Special Column Types with the Default Column Types,
28
+ # which has the effect that the Default Column Types with the same key
29
+ # override the Special Column Types.
30
+ #
31
+ # If the type is not in the merged hash, then column_type defaults to :unknown
32
+ #
33
+ # You are advised to check your migrations for the :unknown, because either you made a
34
+ # typo in the generator command line or you need to add a Form Element!
35
+ def column_type
36
+ InlineForms::SPECIAL_COLUMN_TYPES
37
+ .merge(InlineForms::DEFAULT_COLUMN_TYPES)
38
+ .merge(InlineForms::RELATIONS)
39
+ .merge(InlineForms::SPECIAL_RELATIONS)[type] || :unknown
40
+ end
41
+
42
+ # Override the attribute_type to include our special column types.
43
+ #
44
+ # If a type is not in the Special Column Type hash, then the default
45
+ # column type hash is used, and if that fails, the attribute_type
46
+ # will be :unknown. Make sure to check your models for the :unknown.
47
+ def attribute_type
48
+ if InlineForms::SPECIAL_COLUMN_TYPES.merge(InlineForms::RELATIONS).has_key?(type)
49
+ type
50
+ else
51
+ InlineForms::DEFAULT_FORM_ELEMENTS[type] || :unknown
52
+ end
53
+ end
54
+
55
+ def special_relation?
56
+ InlineForms::SPECIAL_RELATIONS.has_key?(type)
57
+ end
58
+
59
+ def relation?
60
+ InlineForms::RELATIONS.has_key?(type) || special_relation?
61
+ end
62
+
63
+ # Special "attribute" names that drive the generator (presentation,
64
+ # ordering, search, etc.) but never become real columns or fields.
65
+ SPECIAL_GENERATOR_NAMES = %w[
66
+ _presentation
67
+ _order
68
+ _list_order
69
+ _list_search
70
+ _enabled
71
+ _id
72
+ _no_migration
73
+ _no_model
74
+ ].freeze unless const_defined?(:SPECIAL_GENERATOR_NAMES)
75
+
76
+ def migration?
77
+ not ( column_type == :no_migration ||
78
+ SPECIAL_GENERATOR_NAMES.include?(name) )
79
+ end
80
+
81
+ def attribute?
82
+ not ( SPECIAL_GENERATOR_NAMES.include?(name) ||
83
+ relation? )
84
+ end
85
+ end
@@ -1,5 +1,6 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  require "inline_forms"
3
+ require_relative "inline_forms_attribute_overrides"
3
4
  module InlineForms
4
5
  # == Usage
5
6
  # This generator generates a migration, a model and a controller.
@@ -13,82 +14,12 @@ module InlineForms
13
14
  # rails g example_generator Modelname attribute:type attribute:type ...
14
15
  # an array with attributes and types is created for use in the generator.
15
16
  #
16
- # Rails::Generators::GeneratedAttribute creates, among others, a attribute_type.
17
+ # Rails::Generators::GeneratedAttribute creates, among others, an attribute_type.
17
18
  # This attribute_type maps column types to form attribute helpers like text_field.
18
- # We override it here to make our own.
19
+ # We override it in `lib/generators/inline_forms_attribute_overrides.rb`
20
+ # (shared with `InlineFormsAddtoGenerator`).
19
21
  #
20
22
  class InlineFormsGenerator < Rails::Generators::NamedBase
21
- Rails::Generators::GeneratedAttribute.class_eval do #:doc:
22
- # Override Rails::Generators::GeneratedAttribute.valid_type? so that our
23
- # custom field types (dropdown, check_list, image_field, rich_text, ...)
24
- # pass through parsing. We do our own unknown-type detection later (with
25
- # Thor::Error + --allow-unknown), so it is safe to accept everything here.
26
- #
27
- # Rails 6.1 used to rescue ActiveRecord::Base.connection failures, which
28
- # masked the issue; Rails 7+ raises NameError when ActiveRecord is not
29
- # loaded yet, breaking generator unit tests.
30
- def self.valid_type?(_type)
31
- true
32
- end
33
-
34
- # Deducts the column_type for migrations from the type.
35
- #
36
- # We first merge the Special Column Types with the Default Column Types,
37
- # which has the effect that the Default Column Types with the same key override
38
- # the Special Column Types.
39
- #
40
- # If the type is not in the merged hash, then column_type defaults to :unknown
41
- #
42
- # You are advised to check you migrations for the :unknown, because either you made a
43
- # typo in the generator command line or you need to add a Form Element!
44
- #
45
- def column_type
46
- SPECIAL_COLUMN_TYPES.merge(DEFAULT_COLUMN_TYPES).merge(RELATIONS).merge(SPECIAL_RELATIONS)[type] || :unknown
47
- end
48
-
49
- # Override the attribute_type to include our special column types.
50
- #
51
- # If a type is not in the Special Column Type hash, then the default
52
- # column type hash is used, and if that fails, the attribute_type
53
- # will be :unknown. Make sure to check your models for the :unknown.
54
- #
55
- def attribute_type
56
- SPECIAL_COLUMN_TYPES.merge(RELATIONS).has_key?(type) ? type : DEFAULT_FORM_ELEMENTS[type] || :unknown
57
- end
58
-
59
- def special_relation?
60
- SPECIAL_RELATIONS.has_key?(type)
61
- end
62
-
63
- def relation?
64
- RELATIONS.has_key?(type) || special_relation?
65
- end
66
-
67
- # Special "attribute" names that drive the generator (presentation,
68
- # ordering, search, etc.) but never become real columns or fields.
69
- SPECIAL_GENERATOR_NAMES = %w[
70
- _presentation
71
- _order
72
- _list_order
73
- _list_search
74
- _enabled
75
- _id
76
- _no_migration
77
- _no_model
78
- ].freeze
79
-
80
- def migration?
81
- not ( column_type == :no_migration ||
82
- SPECIAL_GENERATOR_NAMES.include?(name) )
83
- end
84
-
85
- def attribute?
86
- not ( SPECIAL_GENERATOR_NAMES.include?(name) ||
87
- relation? )
88
- end
89
-
90
-
91
- end
92
23
  argument :attributes, :type => :array, :banner => "[name:form_element]..."
93
24
  class_option :allow_unknown, :type => :boolean, :default => false, :desc => "Allow unknown field types (legacy behavior: comment generated lines instead of failing)."
94
25
 
@@ -0,0 +1,7 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[8.0]
2
+
3
+ def change
4
+ <%= @migration_lines -%>
5
+ end
6
+
7
+ end
@@ -1,4 +1,4 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module InlineForms
3
- VERSION = "8.1.0"
3
+ VERSION = "8.1.1"
4
4
  end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require "tmpdir"
5
+ require "fileutils"
6
+ require "logger"
7
+ require "rails"
8
+ require "rails/generators"
9
+ require "inline_forms"
10
+ require_relative "../lib/generators/inline_forms_addto_generator"
11
+
12
+ class InlineFormsAddtoGeneratorTest < Minitest::Test
13
+ GENERATOR_SHAPED_MODEL = <<~RUBY
14
+ class Widget < ApplicationRecord
15
+
16
+ belongs_to :category
17
+
18
+ def inline_forms_attribute_list
19
+ @inline_forms_attribute_list ||= [
20
+ [ :name , "name", :text_field ],
21
+ [ :category , "category", :belongs_to ],
22
+ ]
23
+ end
24
+
25
+ end
26
+ RUBY
27
+
28
+ INSTALLER_SHAPED_USER = <<~RUBY
29
+ class User < ApplicationRecord
30
+
31
+ devise :database_authenticatable
32
+ belongs_to :locale
33
+ has_and_belongs_to_many :roles
34
+
35
+ def _presentation
36
+ "\#{name}"
37
+ end
38
+
39
+ def inline_forms_attribute_list
40
+ @inline_forms_attribute_list ||= [
41
+ [ :header_user_login, '', :header ],
42
+ [ :name, '', :text_field ],
43
+ [ :email, '', :text_field ],
44
+ [ :locale , '', :dropdown ],
45
+ [ :password, '', :devise_password_field ],
46
+ [ :header_user_roles, '', :header ],
47
+ [ :roles, '', :check_list ],
48
+ ]
49
+ end
50
+
51
+ end
52
+ RUBY
53
+
54
+ BARE_MODEL = <<~RUBY
55
+ class Bare < ApplicationRecord
56
+ end
57
+ RUBY
58
+
59
+ def setup
60
+ @destination_root = Dir.mktmpdir("inline_forms_addto_generator_test")
61
+ mkdir_p("app/models")
62
+ mkdir_p("db/migrate")
63
+ end
64
+
65
+ def teardown
66
+ FileUtils.remove_entry(@destination_root) if @destination_root && Dir.exist?(@destination_root)
67
+ end
68
+
69
+ def test_raises_when_model_file_missing
70
+ stdout, stderr = capture_io do
71
+ run_generator("DoesNotExist", "occupation:string")
72
+ end
73
+ output = "#{stdout}#{stderr}"
74
+ assert_includes(output, "rails g inline_forms DoesNotExist")
75
+ refute(File.exist?(File.join(@destination_root, "app/models/does_not_exist.rb")))
76
+ end
77
+
78
+ def test_adds_scalar_column_belongs_to_rich_text_and_image_field_to_existing_model
79
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
80
+
81
+ run_generator(
82
+ "Widget",
83
+ "occupation:string",
84
+ "organization:belongs_to",
85
+ "supplier:dropdown",
86
+ "bio:rich_text",
87
+ "avatar:image_field"
88
+ )
89
+
90
+ model = read("app/models/widget.rb")
91
+ migration = read_single_addto_migration_for("widgets")
92
+
93
+ assert_includes(model, "belongs_to :organization")
94
+ assert_includes(model, "belongs_to :supplier")
95
+ assert_includes(model, "has_rich_text :bio")
96
+ assert_includes(model, "mount_uploader :avatar, AvatarUploader")
97
+ assert_includes(model, '[ :occupation , "occupation", :text_field ]')
98
+ # :belongs_to is a relation -> no row in inline_forms_attribute_list
99
+ # (matches InlineFormsGenerator semantics). :dropdown is not a relation
100
+ # at lookup time, so it does get a row.
101
+ refute_includes(model, '[ :organization , "organization", :belongs_to ]')
102
+ assert_includes(model, '[ :supplier , "supplier", :dropdown ]')
103
+ assert_includes(model, '[ :avatar , "avatar", :image_field ]')
104
+
105
+ assert_includes(migration, "add_column :widgets, :occupation, :string")
106
+ assert_includes(migration, "add_reference :widgets, :organization, foreign_key: true")
107
+ assert_includes(migration, "add_reference :widgets, :supplier, foreign_key: true")
108
+ assert_includes(migration, "add_column :widgets, :avatar, :string")
109
+ refute_includes(migration, ":bio")
110
+ refute_includes(migration, "create_table")
111
+ end
112
+
113
+ def test_idempotent_rerun_does_not_duplicate_lines
114
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
115
+
116
+ run_generator("Widget", "occupation:string", "organization:belongs_to")
117
+ sleep 1 # unique migration timestamp on second run
118
+ run_generator("Widget", "occupation:string", "organization:belongs_to")
119
+
120
+ model = read("app/models/widget.rb")
121
+
122
+ assert_equal(1, model.scan("belongs_to :organization").size)
123
+ assert_equal(1, model.scan('[ :occupation , "occupation", :text_field ]').size)
124
+ end
125
+
126
+ def test_appends_row_to_installer_shaped_user_attribute_list
127
+ write_model("user.rb", INSTALLER_SHAPED_USER)
128
+
129
+ run_generator("User", "occupation:string", "birthdate:date")
130
+
131
+ model = read("app/models/user.rb")
132
+
133
+ assert_includes(model, '[ :occupation , "occupation", :text_field ]')
134
+ assert_includes(model, '[ :birthdate , "birthdate", :date_select ]')
135
+ refute_match(/\]\s*\n\s*\[\s*:occupation/, model)
136
+
137
+ user_array_section = model[/@inline_forms_attribute_list \|\|=\s*\[(.|\n)*?\n\s*\]/]
138
+ assert(user_array_section, "could not locate @inline_forms_attribute_list array")
139
+ assert_match(/\[ :roles,.*\].*?\[ :occupation\b/m, user_array_section)
140
+ end
141
+
142
+ def test_bare_model_gets_fresh_attribute_list_method
143
+ write_model("bare.rb", BARE_MODEL)
144
+
145
+ out, _err = capture_io do
146
+ run_generator("Bare", "occupation:string")
147
+ end
148
+
149
+ model = read("app/models/bare.rb")
150
+
151
+ assert_includes(out, "no inline_forms_attribute_list found")
152
+ assert_includes(model, "def inline_forms_attribute_list")
153
+ assert_includes(model, '[ :occupation , "occupation", :text_field ]')
154
+ end
155
+
156
+ def test_unknown_type_raises_thor_error_by_default
157
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
158
+
159
+ stdout, stderr = capture_io do
160
+ run_generator("Widget", "payload:not_a_real_type")
161
+ end
162
+ output = "#{stdout}#{stderr}"
163
+ assert_includes(output, "Unknown field type(s): payload:not_a_real_type")
164
+ assert_includes(output, "--allow-unknown")
165
+
166
+ refute_addto_migration_for("widgets")
167
+ end
168
+
169
+ def test_allow_unknown_keeps_legacy_commented_output
170
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
171
+
172
+ run_generator("Widget", "payload:not_a_real_type", "--allow-unknown")
173
+
174
+ model = read("app/models/widget.rb")
175
+ migration = read_single_addto_migration_for("widgets")
176
+
177
+ assert_includes(model, '[ :payload , "payload", :unknown ]')
178
+ assert_includes(migration, "# add_column :widgets, :payload, :unknown")
179
+ end
180
+
181
+ def test_install_time_only_names_are_rejected
182
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
183
+
184
+ %w[_no_model _no_migration _id _enabled].each do |forbidden|
185
+ stdout, stderr = capture_io do
186
+ run_generator("Widget", "#{forbidden}:yes")
187
+ end
188
+ output = "#{stdout}#{stderr}"
189
+ assert_includes(output, forbidden, "expected error to mention #{forbidden}")
190
+ assert_includes(output, "install-time only")
191
+ end
192
+ end
193
+
194
+ def test_skips_replace_only_names_without_replace_flag
195
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
196
+
197
+ out, _err = capture_io do
198
+ run_generator("Widget", "_list_order:title")
199
+ end
200
+
201
+ assert_includes(out, "_list_order:title")
202
+ assert_includes(out, "skipped")
203
+ model = read("app/models/widget.rb")
204
+ refute_includes(model, "scope :inline_forms_list, -> { order(:title, :id) }")
205
+ end
206
+
207
+ def test_replace_flag_rewrites_list_order_scope
208
+ initial = GENERATOR_SHAPED_MODEL.sub(
209
+ /class Widget < ApplicationRecord\n/,
210
+ "class Widget < ApplicationRecord\n\n scope :inline_forms_list, -> { order(:name, :id) }\n def <=>(other)\n self.name <=> other.name\n end\n"
211
+ )
212
+ write_model("widget.rb", initial)
213
+
214
+ run_generator("Widget", "_list_order:title", "--replace")
215
+
216
+ model = read("app/models/widget.rb")
217
+ assert_includes(model, "scope :inline_forms_list, -> { order(:title, :id) }")
218
+ refute_includes(model, "order(:name, :id)")
219
+ assert_includes(model, "self.title <=> other.title")
220
+ refute_includes(model, "self.name <=> other.name")
221
+ end
222
+
223
+ private
224
+
225
+ def run_generator(*args)
226
+ InlineForms::InlineFormsAddtoGenerator.start(args, destination_root: @destination_root)
227
+ end
228
+
229
+ def write_model(file, content)
230
+ File.write(File.join(@destination_root, "app/models", file), content)
231
+ end
232
+
233
+ def read(relative_path)
234
+ File.read(File.join(@destination_root, relative_path))
235
+ end
236
+
237
+ def mkdir_p(relative_path)
238
+ FileUtils.mkdir_p(File.join(@destination_root, relative_path))
239
+ end
240
+
241
+ def read_single_addto_migration_for(table_name)
242
+ files = Dir.glob(File.join(@destination_root, "db/migrate/*_inline_forms_add_to_#{table_name}_*.rb"))
243
+ assert_equal(1, files.size, "Expected one addto migration for #{table_name}, got #{files.inspect}")
244
+ File.read(files.first)
245
+ end
246
+
247
+ def refute_addto_migration_for(table_name)
248
+ files = Dir.glob(File.join(@destination_root, "db/migrate/*_inline_forms_add_to_#{table_name}_*.rb"))
249
+ assert_equal([], files, "Expected no addto migration for #{table_name}")
250
+ end
251
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inline_forms
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.0
4
+ version: 8.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -495,8 +495,11 @@ files:
495
495
  - inline_forms.gemspec
496
496
  - lib/generators/USAGE
497
497
  - lib/generators/assets/stylesheets/inline_forms_devise.css
498
+ - lib/generators/inline_forms_addto_generator.rb
499
+ - lib/generators/inline_forms_attribute_overrides.rb
498
500
  - lib/generators/inline_forms_generator.rb
499
501
  - lib/generators/templates/_inline_forms_tabs.html.erb
502
+ - lib/generators/templates/add_columns_migration.erb
500
503
  - lib/generators/templates/application_record.rb
501
504
  - lib/generators/templates/controller.erb
502
505
  - lib/generators/templates/migration.erb
@@ -558,6 +561,7 @@ files:
558
561
  - test/archived_form_elements_test.rb
559
562
  - test/form_element_from_callee_test.rb
560
563
  - test/inline_edit_polymorphic_path_test.rb
564
+ - test/inline_forms_addto_generator_test.rb
561
565
  - test/inline_forms_generator_test.rb
562
566
  - test/plain_text_configuration_test.rb
563
567
  - test/test_helper.rb
@@ -590,6 +594,7 @@ test_files:
590
594
  - test/archived_form_elements_test.rb
591
595
  - test/form_element_from_callee_test.rb
592
596
  - test/inline_edit_polymorphic_path_test.rb
597
+ - test/inline_forms_addto_generator_test.rb
593
598
  - test/inline_forms_generator_test.rb
594
599
  - test/plain_text_configuration_test.rb
595
600
  - test/test_helper.rb