compony 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -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 +6 -6
  8. data/doc/ComponentGenerator.html +3 -3
  9. data/doc/Components.html +3 -3
  10. data/doc/ComponentsGenerator.html +3 -3
  11. data/doc/Compony/Component.html +11 -11
  12. data/doc/Compony/ComponentMixins/Default/Labelling.html +3 -3
  13. data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +3 -3
  14. data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +8 -8
  15. data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +3 -3
  16. data/doc/Compony/ComponentMixins/Default/Standalone.html +3 -3
  17. data/doc/Compony/ComponentMixins/Default.html +3 -3
  18. data/doc/Compony/ComponentMixins/Resourceful.html +3 -3
  19. data/doc/Compony/ComponentMixins.html +3 -3
  20. data/doc/Compony/Components/Button.html +3 -3
  21. data/doc/Compony/Components/Destroy.html +3 -3
  22. data/doc/Compony/Components/Edit.html +20 -20
  23. data/doc/Compony/Components/Form.html +318 -92
  24. data/doc/Compony/Components/New.html +20 -20
  25. data/doc/Compony/Components/WithForm.html +96 -21
  26. data/doc/Compony/Components.html +3 -3
  27. data/doc/Compony/ControllerMixin.html +3 -3
  28. data/doc/Compony/Engine.html +3 -3
  29. data/doc/Compony/MethodAccessibleHash.html +3 -3
  30. data/doc/Compony/ModelFields/Anchormodel.html +3 -3
  31. data/doc/Compony/ModelFields/Association.html +3 -3
  32. data/doc/Compony/ModelFields/Attachment.html +3 -3
  33. data/doc/Compony/ModelFields/Base.html +3 -3
  34. data/doc/Compony/ModelFields/Boolean.html +3 -3
  35. data/doc/Compony/ModelFields/Color.html +3 -3
  36. data/doc/Compony/ModelFields/Currency.html +3 -3
  37. data/doc/Compony/ModelFields/Date.html +3 -3
  38. data/doc/Compony/ModelFields/Datetime.html +3 -3
  39. data/doc/Compony/ModelFields/Decimal.html +3 -3
  40. data/doc/Compony/ModelFields/Email.html +3 -3
  41. data/doc/Compony/ModelFields/Float.html +3 -3
  42. data/doc/Compony/ModelFields/Integer.html +3 -3
  43. data/doc/Compony/ModelFields/Percentage.html +3 -3
  44. data/doc/Compony/ModelFields/Phone.html +3 -3
  45. data/doc/Compony/ModelFields/RichText.html +3 -3
  46. data/doc/Compony/ModelFields/String.html +3 -3
  47. data/doc/Compony/ModelFields/Text.html +3 -3
  48. data/doc/Compony/ModelFields/Time.html +3 -3
  49. data/doc/Compony/ModelFields/Url.html +3 -3
  50. data/doc/Compony/ModelFields.html +3 -3
  51. data/doc/Compony/ModelMixin.html +28 -28
  52. data/doc/Compony/NaturalOrdering.html +3 -3
  53. data/doc/Compony/RequestContext.html +72 -5
  54. data/doc/Compony/Version.html +3 -3
  55. data/doc/Compony/ViewHelpers.html +3 -3
  56. data/doc/Compony.html +4 -4
  57. data/doc/ComponyController.html +3 -3
  58. data/doc/_index.html +4 -4
  59. data/doc/file.README.html +44 -3
  60. data/doc/frames.html +10 -5
  61. data/doc/index.html +44 -3
  62. data/doc/method_list.html +74 -42
  63. data/doc/top-level-namespace.html +3 -3
  64. data/lib/compony/component.rb +4 -4
  65. data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +3 -3
  66. data/lib/compony/components/edit.rb +4 -1
  67. data/lib/compony/components/form.rb +74 -9
  68. data/lib/compony/components/new.rb +4 -1
  69. data/lib/compony/components/with_form.rb +12 -1
  70. data/lib/compony/model_mixin.rb +8 -1
  71. data/lib/compony/request_context.rb +8 -1
  72. data/lib/compony.rb +1 -1
  73. metadata +6 -6
@@ -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.
@@ -45,7 +45,9 @@ module Compony
45
45
  # Renders a content block from the current component.
46
46
  def content(name)
47
47
  name = name.to_sym
48
- content_block = component.content_blocks.find { |el| el.name == name } || fail("Content block #{name.inspect} not found in #{component.inspect}.")
48
+ content_block = component.content_blocks.find { |el| el.name == name }
49
+ return false if content_block.nil?
50
+
49
51
  # We have to clear Rails' output_buffer to prevent double rendering of blocks. To achieve this, a fresh render context is instanciated.
50
52
  concat controller.render_to_string(
51
53
  type: :dyny,
@@ -54,6 +56,11 @@ module Compony
54
56
  Compony::RequestContext.new(render_component, render_controller, helpers: self, locals: local_assigns).evaluate_with_backfire(&render_block.payload)
55
57
  RUBY
56
58
  )
59
+ return true
60
+ end
61
+
62
+ def content!(name)
63
+ content(name) || fail("Content block #{name.inspect} not found in #{component.inspect}.")
57
64
  end
58
65
  end
59
66
  end
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.2
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-01 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,14 +323,14 @@ 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
  - - ">="
330
330
  - !ruby/object:Gem::Version
331
331
  version: '0'
332
332
  requirements: []
333
- rubygems_version: 3.5.9
333
+ rubygems_version: 3.5.11
334
334
  signing_key:
335
335
  specification_version: 4
336
336
  summary: Compony is a Gem that allows you to write your Rails application in component-style