compony 0.3.3 → 0.4.0

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Gemfile.lock +3 -3
  4. data/README.md +41 -0
  5. data/Rakefile +2 -2
  6. data/VERSION +1 -1
  7. data/compony.gemspec +5 -5
  8. data/doc/ComponentGenerator.html +1 -1
  9. data/doc/Components.html +1 -1
  10. data/doc/ComponentsGenerator.html +1 -1
  11. data/doc/Compony/Component.html +9 -9
  12. data/doc/Compony/ComponentMixins/Default/Labelling.html +1 -1
  13. data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +1 -1
  14. data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +6 -6
  15. data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +1 -1
  16. data/doc/Compony/ComponentMixins/Default/Standalone.html +1 -1
  17. data/doc/Compony/ComponentMixins/Default.html +1 -1
  18. data/doc/Compony/ComponentMixins/Resourceful.html +1 -1
  19. data/doc/Compony/ComponentMixins.html +1 -1
  20. data/doc/Compony/Components/Button.html +1 -1
  21. data/doc/Compony/Components/Destroy.html +1 -1
  22. data/doc/Compony/Components/Edit.html +18 -18
  23. data/doc/Compony/Components/Form.html +316 -90
  24. data/doc/Compony/Components/New.html +18 -18
  25. data/doc/Compony/Components/WithForm.html +94 -19
  26. data/doc/Compony/Components.html +1 -1
  27. data/doc/Compony/ControllerMixin.html +1 -1
  28. data/doc/Compony/Engine.html +1 -1
  29. data/doc/Compony/MethodAccessibleHash.html +1 -1
  30. data/doc/Compony/ModelFields/Anchormodel.html +1 -1
  31. data/doc/Compony/ModelFields/Association.html +1 -1
  32. data/doc/Compony/ModelFields/Attachment.html +1 -1
  33. data/doc/Compony/ModelFields/Base.html +1 -1
  34. data/doc/Compony/ModelFields/Boolean.html +1 -1
  35. data/doc/Compony/ModelFields/Color.html +1 -1
  36. data/doc/Compony/ModelFields/Currency.html +1 -1
  37. data/doc/Compony/ModelFields/Date.html +1 -1
  38. data/doc/Compony/ModelFields/Datetime.html +1 -1
  39. data/doc/Compony/ModelFields/Decimal.html +1 -1
  40. data/doc/Compony/ModelFields/Email.html +1 -1
  41. data/doc/Compony/ModelFields/Float.html +1 -1
  42. data/doc/Compony/ModelFields/Integer.html +1 -1
  43. data/doc/Compony/ModelFields/Percentage.html +1 -1
  44. data/doc/Compony/ModelFields/Phone.html +1 -1
  45. data/doc/Compony/ModelFields/RichText.html +1 -1
  46. data/doc/Compony/ModelFields/String.html +1 -1
  47. data/doc/Compony/ModelFields/Text.html +1 -1
  48. data/doc/Compony/ModelFields/Time.html +1 -1
  49. data/doc/Compony/ModelFields/Url.html +1 -1
  50. data/doc/Compony/ModelFields.html +1 -1
  51. data/doc/Compony/ModelMixin.html +26 -26
  52. data/doc/Compony/NaturalOrdering.html +1 -1
  53. data/doc/Compony/RequestContext.html +1 -1
  54. data/doc/Compony/Version.html +1 -1
  55. data/doc/Compony/ViewHelpers.html +1 -1
  56. data/doc/Compony.html +2 -2
  57. data/doc/ComponyController.html +1 -1
  58. data/doc/_index.html +1 -1
  59. data/doc/file.README.html +42 -1
  60. data/doc/index.html +42 -1
  61. data/doc/method_list.html +122 -98
  62. data/doc/top-level-namespace.html +1 -1
  63. data/lib/compony/component.rb +4 -4
  64. data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +3 -3
  65. data/lib/compony/components/edit.rb +4 -1
  66. data/lib/compony/components/form.rb +74 -9
  67. data/lib/compony/components/new.rb +4 -1
  68. data/lib/compony/components/with_form.rb +12 -1
  69. data/lib/compony/model_mixin.rb +8 -1
  70. data/lib/compony.rb +1 -1
  71. metadata +5 -5
@@ -102,7 +102,7 @@
102
102
  </div>
103
103
 
104
104
  <div id="footer">
105
- Generated on Mon Jun 3 15:20:40 2024 by
105
+ Generated on Tue Jun 11 11:15:53 2024 by
106
106
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
107
107
  0.9.36 (ruby-3.2.2).
108
108
  </div>
@@ -118,9 +118,9 @@ module Compony
118
118
  # @param [Symbol,String] name The name of the before_render block, defaults to `:main`
119
119
  # @param [nil,Symbol,String] before If nil, the block will be added to the bottom of the before_render chain. Otherwise, pass the name of another block.
120
120
  # @param [Proc] block The block that should be run as part of the before_render pipeline. Will run in the component's context.
121
- def before_render(name = :main, before: nil, **kwargs, &block)
121
+ def before_render(name = :main, before: nil, **, &block)
122
122
  fail("`before_render` expects a block in #{inspect}.") unless block_given?
123
- @before_render_blocks.natural_push(name, block, before:, **kwargs)
123
+ @before_render_blocks.natural_push(name, block, before:, **)
124
124
  end
125
125
 
126
126
  # DSL method
@@ -129,12 +129,12 @@ module Compony
129
129
  # @param [nil,Symbol,String] before If nil, the block will be added to the bottom of the content chain. Otherwise, pass the name of another block.
130
130
  # @param [Hash] kwargs If hidden is true, the content will not be rendered by default, allowing you to nest it in another content block.
131
131
  # @param [Proc] block The block that should be run as part of the content pipeline. Will run in the component's context. You can use Dyny here.
132
- def content(name = :main, before: nil, **kwargs, &block)
132
+ def content(name = :main, before: nil, **, &block)
133
133
  # A block is required here, but if this is an override (e.g. to hide another content block), we can tolerate the missing block.
134
134
  if !block_given? && @content_blocks.find { |b| b.name == name }.nil?
135
135
  fail("`content` expects a block in #{inspect}.")
136
136
  end
137
- @content_blocks.natural_push(name, block || :missing, before:, **kwargs)
137
+ @content_blocks.natural_push(name, block || :missing, before:, **)
138
138
  end
139
139
 
140
140
  # DSL method
@@ -37,18 +37,18 @@ module Compony
37
37
  # DSL call for defining a config for a verb. The block runs within the verb DSL, positional and named arguments are passed to the verb DSL.
38
38
  # @param verb [Symbol] The HTTP verb the config is for (e.g. :get, :post etc.)
39
39
  # @see Compony::ComponentMixins::Default::Standalone::VerbDsl
40
- def verb(verb, *args, **nargs, &)
40
+ def verb(verb, *, **nargs, &)
41
41
  verb = verb.to_sym
42
42
  verb_dsl_class = @component.resourceful? ? ResourcefulVerbDsl : VerbDsl
43
43
  if @verbs[verb]
44
- @verbs[verb].deep_merge! verb_dsl_class.new(@component, verb, *args, **nargs).to_conf(provide_defaults: false, &)
44
+ @verbs[verb].deep_merge! verb_dsl_class.new(@component, verb, *, **nargs).to_conf(provide_defaults: false, &)
45
45
  else
46
46
  # Note about provide_defaults:
47
47
  # - We must pass false if this is the second time `standalone` was called for this component -> see @provide_defaults
48
48
  # - We musst pass false if this is the second time `verb` was called for this component -> handled by the if statement (other branch)
49
49
  # - We must pass true otherwise (handled by this branch)
50
50
  @verbs[verb] = Compony::MethodAccessibleHash.new(
51
- verb_dsl_class.new(@component, verb, *args, **nargs).to_conf(provide_defaults: @provide_defaults, &)
51
+ verb_dsl_class.new(@component, verb, *, **nargs).to_conf(provide_defaults: @provide_defaults, &)
52
52
  )
53
53
  end
54
54
  end
@@ -30,6 +30,8 @@ module Compony
30
30
  label(:short) { |_| I18n.t('compony.components.edit.label.short') }
31
31
  icon { :pencil }
32
32
 
33
+ form_cancancan_action :edit
34
+
33
35
  action :back_to_owner do
34
36
  next if data_class.owner_model_attr.blank?
35
37
  Compony.button(:show, @data.send(data_class.owner_model_attr), icon: :xmark, color: :secondary, label: I18n.t('compony.cancel'))
@@ -47,12 +49,13 @@ module Compony
47
49
  # Validate params against the form's schema
48
50
  local_form_comp = form_comp # Capture form_comp for usage in the Schemacop call
49
51
  local_data = @data # Capture data for usage in the Schemacop call
52
+ local_controller = controller # Capture controller for usage in the Schemacop call
50
53
  schema = Schemacop::Schema3.new :hash, additional_properties: true do
51
54
  any_of! :id do
52
55
  str
53
56
  int cast_str: true
54
57
  end
55
- hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data)
58
+ hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data, local_controller)
56
59
  end
57
60
  validated_params = schema.validate!(controller.request.params)
58
61
  attrs_to_assign = validated_params[form_comp.schema_wrapper_key_for(@data)]
@@ -3,8 +3,9 @@ module Compony
3
3
  # @api description
4
4
  # This component is used for the _form partial in the Rails paradigm.
5
5
  class Form < Component
6
- def initialize(...)
6
+ def initialize(*args, cancancan_action: :missing, **kwargs)
7
7
  @schema_lines_for_data = [] # Array of procs taking data returning a Schemacop proc
8
+ @cancancan_action = cancancan_action
8
9
  super
9
10
  end
10
11
 
@@ -12,6 +13,9 @@ module Compony
12
13
  before_render do
13
14
  # Make sure the error message is going to be nice if form_fields were not implemented
14
15
  fail "#{component.inspect} requires config.form_fields do ..." if @form_fields.nil?
16
+ if @cancancan_action == :missing
17
+ fail("Missing cancancan_action for #{component.inspect}, you must provide one (e.g. :edit) or pass nil explicitely.")
18
+ end
15
19
 
16
20
  # Calculate paths
17
21
  @submit_path = @comp_opts[:submit_path]
@@ -32,7 +36,7 @@ module Compony
32
36
 
33
37
  content do
34
38
  form_html = simple_form_for(data, method: @comp_opts[:submit_verb], url: @submit_path) do |f|
35
- component.with_simpleform(f) do
39
+ component.with_simpleform(f, controller) do
36
40
  instance_exec(&form_fields)
37
41
  div class: 'compony-form-buttons' do
38
42
  content(:buttons)
@@ -60,7 +64,7 @@ module Compony
60
64
  end
61
65
 
62
66
  # Attr reader for @schema_block with auto-calculated default
63
- def schema_block_for(data)
67
+ def schema_block_for(data, controller)
64
68
  if @schema_block
65
69
  return @schema_block
66
70
  else
@@ -68,7 +72,8 @@ module Compony
68
72
  local_schema_lines_for_data = @schema_lines_for_data
69
73
  return proc do
70
74
  local_schema_lines_for_data.each do |schema_line|
71
- instance_exec(&schema_line.call(data))
75
+ schema_line_proc = schema_line.call(data, controller) # This may return nil, e.g. is the user is not authorized to set a field
76
+ instance_exec(&schema_line_proc) unless schema_line_proc.nil?
72
77
  end
73
78
  end
74
79
  end
@@ -78,21 +83,32 @@ module Compony
78
83
  # methods from inside `form_fields`. This is a workaround required because the form does not exist when the
79
84
  # RequestContext is being built, and we want the method `field` to be available inside the `form_fields` block.
80
85
  # @todo Refactor? Could this be greatly simplified by having `form_field to |f|` ?
81
- def with_simpleform(simpleform)
86
+ def with_simpleform(simpleform, controller)
82
87
  @simpleform = simpleform
88
+ @controller = controller
83
89
  @focus_given = false
84
90
  yield
85
91
  @simpleform = nil
92
+ @controller = nil
86
93
  end
87
94
 
88
95
  # Called inside the form_fields block. This makes the method `field` available in the block.
89
96
  # See also notes for `with_simpleform`.
90
97
  def field(name, **input_opts)
91
98
  fail("The `field` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform
99
+ name = name.to_sym
100
+
101
+ # Check per-field authorization
102
+ if @cancancan_action.present? && @controller.current_ability.permitted_attributes(@cancancan_action, @simpleform.object).exclude?(name)
103
+ Rails.logger.debug do
104
+ "Skipping form field #{name.inspect} because the current user is not allowed to perform #{@cancancan_action.inspect} on #{@simpleform.object}."
105
+ end
106
+ return
107
+ end
92
108
 
93
109
  hidden = input_opts.delete(:hidden)
94
- model_field = @simpleform.object.fields[name.to_sym]
95
- fail("Field #{name.to_sym.inspect} is not defined on #{@simpleform.object.inspect} but was requested in #{inspect}.") unless model_field
110
+ model_field = @simpleform.object.fields[name]
111
+ fail("Field #{name.inspect} is not defined on #{@simpleform.object.inspect} but was requested in #{inspect}.") unless model_field
96
112
 
97
113
  if hidden
98
114
  return model_field.simpleform_input_hidden(@simpleform, self, **input_opts)
@@ -105,6 +121,28 @@ module Compony
105
121
  end
106
122
  end
107
123
 
124
+ # Called inside the form_fields block. This makes the method pw_field available in the block.
125
+ # This method should be called for the fields :password and :password_confirmation
126
+ # Note that :hidden is not supported here, as this would make no sense in conjunction with :password or :password_confirmation.
127
+ def pw_field(name, **input_opts)
128
+ fail("The `pw_field` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform
129
+ name = name.to_sym
130
+
131
+ # Check for authorization
132
+ unless @cancancan_action.nil? || @controller.current_ability.can?(:set_password, @simpleform.object)
133
+ Rails.logger.debug do
134
+ "Skipping form pw_field #{name.inspect} because the current user is not allowed to perform :set_password on #{@simpleform.object}."
135
+ end
136
+ return
137
+ end
138
+
139
+ unless @focus_given || @skip_autofocus
140
+ input_opts[:autofocus] = true unless input_opts.key? :autofocus
141
+ @focus_given = true
142
+ end
143
+ return @simpleform.input name, **input_opts
144
+ end
145
+
108
146
  # Called inside the form_fields block. This makes the method `f` available in the block.
109
147
  # See also notes for `with_simpleform`.
110
148
  def f
@@ -120,19 +158,46 @@ module Compony
120
158
  protected
121
159
 
122
160
  # DSL method, adds a new line to the schema whitelisting a single param inside the schema's wrapper
161
+ # The block should be something like `str? :foo` and will run in a Schemacop3 context.
123
162
  def schema_line(&block)
124
- @schema_lines_for_data << proc { block }
163
+ @schema_lines_for_data << proc { |_data, _controller| block }
125
164
  end
126
165
 
127
166
  # DSL method, adds a new field to the schema whitelisting a single field of data_class
128
167
  # This auto-generates the correct schema line for the field.
129
168
  def schema_field(field_name)
130
- @schema_lines_for_data << proc do |data|
169
+ # This runs upon component setup.
170
+ @schema_lines_for_data << proc do |data, controller|
171
+ # This runs within a request context.
131
172
  field = data.class.fields[field_name.to_sym] || fail("No field #{field_name.to_sym.inspect} found for #{data.inspect} in #{inspect}.")
173
+ # Check per-field authorization
174
+ if @cancancan_action.present? && controller.current_ability.permitted_attributes(@cancancan_action.to_sym, data).exclude?(field.name.to_sym)
175
+ Rails.logger.debug do
176
+ "Skipping form schema_field #{field_name.inspect} because the current user is not allowed to perform #{@cancancan_action.inspect} on #{data}."
177
+ end
178
+ next nil
179
+ end
132
180
  next field.schema_line
133
181
  end
134
182
  end
135
183
 
184
+ # DSL method, adds a new password field to the schema whitelisting
185
+ # This checks for the permission :set_password and auto-generates the correct schema line for the field.
186
+ def schema_pw_field(field_name)
187
+ # This runs upon component setup.
188
+ @schema_lines_for_data << proc do |data, controller|
189
+ # This runs within a request context.
190
+ # Check per-field authorization
191
+ unless @cancancan_action.nil? || controller.current_ability.can?(:set_password, data)
192
+ Rails.logger.debug do
193
+ "Skipping form schema_pw_field #{name.inspect} because the current user is not allowed to perform :set_password on #{data}."
194
+ end
195
+ next nil
196
+ end
197
+ next proc { obj? field_name.to_sym }
198
+ end
199
+ end
200
+
136
201
  # DSL method, mass-assigns schema fields
137
202
  def schema_fields(*field_names)
138
203
  field_names.each { |field_name| schema_field(field_name) }
@@ -32,6 +32,8 @@ module Compony
32
32
  label(:short) { I18n.t('compony.components.new.label.short') }
33
33
  icon { :plus }
34
34
 
35
+ form_cancancan_action :new
36
+
35
37
  content :label do
36
38
  h2 component.label
37
39
  end
@@ -43,8 +45,9 @@ module Compony
43
45
  assign_attributes do
44
46
  local_form_comp = form_comp # Capture form_comp for usage in the Schemacop call
45
47
  local_data = @data # Capture data for usage in the Schemacop call
48
+ local_controller = controller # Capture controller for usage in the Schemacop call
46
49
  schema = Schemacop::Schema3.new :hash, additional_properties: true do
47
- hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data)
50
+ hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data, local_controller)
48
51
  end
49
52
  validated_params = schema.validate!(controller.request.params)
50
53
  attrs_to_assign = validated_params[form_comp.schema_wrapper_key_for(@data)]
@@ -7,6 +7,7 @@ module Compony
7
7
  def initialize(...)
8
8
  # TODO: On the next line, use Compony.path instead? Likely, this was implemented before that method existed.
9
9
  @submit_path_block = ->(controller) { controller.helpers.send("#{Compony.path_helper_name(comp_name, family_name)}_path") }
10
+ @form_cancancan_action = :missing
10
11
  super
11
12
  end
12
13
 
@@ -17,7 +18,8 @@ module Compony
17
18
  self,
18
19
  submit_verb:,
19
20
  # If applicable, Rails adds the route keys automatically, thus, e.g. :id does not need to be passed here, as it comes from the request.
20
- submit_path: @submit_path_block
21
+ submit_path: @submit_path_block,
22
+ cancancan_action: form_cancancan_action
21
23
  )
22
24
  end
23
25
 
@@ -39,6 +41,15 @@ module Compony
39
41
  @form_comp_class ||= new_form_comp_class
40
42
  end
41
43
 
44
+ # DSL method
45
+ # Sets and gets the form's cancancan action (for cancancan's accessible_attributes)
46
+ def form_cancancan_action(new_form_cancancan_action = :missing)
47
+ if new_form_cancancan_action != :missing
48
+ @form_cancancan_action = new_form_cancancan_action
49
+ end
50
+ return @form_cancancan_action
51
+ end
52
+
42
53
  # DSL method
43
54
  # Overrides the submit path which would otherwise default to this component
44
55
  # This takes a block that will be called and given a controller
@@ -22,7 +22,14 @@ module Compony
22
22
  def field(name, type, **extra_attrs)
23
23
  name = name.to_sym
24
24
  self.fields = fields.dup
25
- fields[name] = Compony.model_field_class_for(type.to_s.camelize).new(name, self, **extra_attrs)
25
+ field = Compony.model_field_class_for(type.to_s.camelize).new(name, self, **extra_attrs)
26
+ # Handle the case where ActiveType would interfere with attribute registration
27
+ if defined?(ActiveType) && self <= ActiveType::Object && !include?(ActiveModel::Attributes)
28
+ fail "Please add `include ActiveModel::Attributes` at the top of the class #{self}, as attributes cannot be registered otherwise with ActiveType."
29
+ end
30
+ # Register the field as an attribute
31
+ attribute(name)
32
+ fields[name] = field
26
33
  end
27
34
 
28
35
  # DSL method, sets the containing model.
data/lib/compony.rb CHANGED
@@ -237,7 +237,7 @@ module Compony
237
237
  end
238
238
  return_value = block.call
239
239
  # Restore previous value
240
- keys_to_overwrite.each do |key, _new_value|
240
+ keys_to_overwrite.each_key do |key|
241
241
  RequestStore.store[:button_defaults][key] = old_values[key]
242
242
  end
243
243
  # Undefine keys that were not there previously
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compony
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Kalbermatter
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-06-03 00:00:00.000000000 Z
12
+ date: 2024-06-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: yard
@@ -157,14 +157,14 @@ dependencies:
157
157
  requirements:
158
158
  - - "~>"
159
159
  - !ruby/object:Gem::Version
160
- version: 3.4.0
160
+ version: 3.6.1
161
161
  type: :runtime
162
162
  prerelease: false
163
163
  version_requirements: !ruby/object:Gem::Requirement
164
164
  requirements:
165
165
  - - "~>"
166
166
  - !ruby/object:Gem::Version
167
- version: 3.4.0
167
+ version: 3.6.1
168
168
  description:
169
169
  email:
170
170
  executables: []
@@ -323,7 +323,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
323
323
  requirements:
324
324
  - - ">="
325
325
  - !ruby/object:Gem::Version
326
- version: 3.0.0
326
+ version: 3.2.2
327
327
  required_rubygems_version: !ruby/object:Gem::Requirement
328
328
  requirements:
329
329
  - - ">="