hot-glue 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 822437dfda890941c29f2ff18b927c60a84279306c0937d354f69e18c67f51e5
4
- data.tar.gz: 193b9bb92313ae7440c72d8d7598f918400136f7edabc70bf7ec7def1b4f96ed
3
+ metadata.gz: 98f341b6907ade21c64956cab48b59503280aefaf9836b718d33e26ed854d32a
4
+ data.tar.gz: ada2f154b32372c90c107c7507cb57d65850e761017bd17c04e6d5afc7302097
5
5
  SHA512:
6
- metadata.gz: '0210383122eded31e46b9cc02bc6a7c230d95936f13635149c884774e762b178eb326e3722466a6ff8e365fd3d45782a05c0a60733d678499762ff49d6f7b58d'
7
- data.tar.gz: 8cc9ed4dc85875d66dbd66ecac22908bfd6a8759588e37a2b26bb03522a16b8559ca0bc0ee0311a28a627f2f25af703ed744ea1aba6f8f1d48c5fbc35d28748a
6
+ metadata.gz: 7f38775a9cd7743c6dfb39fe4c4bed6e8b27e6987dd455b460205db6b0e13f8ea5c25e7bd24d59992ead7ca8d689f31fc71c73c5f40ea7578aaace49c41dfaca
7
+ data.tar.gz: ce1fa03301507fdc462d8a43d6d0253a4bbf8d1c2184d1a582683d56ff67d5d5b9eb21b60902a8d4dfe390d41e4b3ade485e255db659ab7683e34558d838388a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hot-glue (0.5.26)
4
+ hot-glue (0.6.0.1)
5
5
  ffaker (~> 2.16)
6
6
  kaminari (~> 1.2)
7
7
  rails (> 5.1)
@@ -90,7 +90,7 @@ GEM
90
90
  xpath (~> 3.2)
91
91
  concurrent-ruby (1.1.10)
92
92
  crass (1.0.6)
93
- date (3.3.3)
93
+ date (3.3.4)
94
94
  devise (4.8.1)
95
95
  bcrypt (~> 3.0)
96
96
  orm_adapter (~> 0.1)
@@ -139,12 +139,12 @@ GEM
139
139
  mini_mime (1.1.2)
140
140
  mini_portile2 (2.8.4)
141
141
  minitest (5.16.3)
142
- net-imap (0.4.2)
142
+ net-imap (0.4.4)
143
143
  date
144
144
  net-protocol
145
145
  net-pop (0.1.2)
146
146
  net-protocol
147
- net-protocol (0.2.1)
147
+ net-protocol (0.2.2)
148
148
  timeout
149
149
  net-smtp (0.4.0)
150
150
  net-protocol
@@ -235,7 +235,7 @@ GEM
235
235
  stimulus-rails (1.1.1)
236
236
  railties (>= 6.0.0)
237
237
  thor (1.2.1)
238
- timeout (0.4.0)
238
+ timeout (0.4.1)
239
239
  turbo-rails (1.3.2)
240
240
  actionpack (>= 6.0.0)
241
241
  activejob (>= 6.0.0)
data/README.md CHANGED
@@ -64,25 +64,23 @@ _If you are on Rails 6, see [LEGACY SETUP FOR RAILS 6](https://github.com/jasonf
64
64
 
65
65
  ## The Super-Quick Setup
66
66
 
67
- https://jasonfleetwoodboldt.com/courses/stepping-up-rails/rails-quick-scripts/
67
+ https://jasonfleetwoodboldt.com/courses/stepping-up-rails/jason-fleetwood-boldts-rails-cookbook/
68
68
 
69
- Copy & paste the whole code block from each section into your terminal. (Pick only ONE option for each section.)
69
+ Copy & paste the whole code block from each section into your terminal. Remember, there is a small "Copy" button at the top-right of each code block to help you copy & paste the script into your terminal.
70
70
 
71
+ These are the sections you need, you can ignore any others:
71
72
 
72
- From section #1 (`rails new`), choose either (1) ImportMap Rails, (2) JSBundling, or (3) Shakapacker.
73
-
74
- For Hot Glue, you will need:
75
-
76
- Section #1 is to create a new Rails app. (Or you can do that yourself.)
77
-
78
- Section #2 is to setup Rspec, FactoryBot, and Faker choose 2B for Rspec0
79
-
80
- Skip #3 and #4 is optional. #5 is optional but recommended.
81
-
82
- Sectoin #6 is for Hot Glue itself, and Section #7 is for Kaminari
83
-
84
- You will also need section #8 to setup Devise if you want authentication.
73
+ * Section 1A for a new JS Bundling app, then skip down to
74
+ * Section 2B: Rspec + Friends
75
+ * Section 2B-Capy: Capybara for Rspec, then skip down to
76
+ * Section 3 for a welcome controller
77
+ ( you can skip everything in Section 4 )
78
+ * Section 5 for debugging tools
79
+ * _Section 6 is the Hot Glue installer itself_ (this gem) - for Bootstrap, choose section 6A
80
+ * Section 7A to install Bootstrap along with CSSBundling
81
+ * Section 8 to set up Devise if you want authentication. (See how Hot Glue interacts with Devise below.)
85
82
 
83
+ If you do this through the quick setup above, you can then skip down past the next section to the "HOT GLUE DOCS" below.
86
84
 
87
85
  ## Step-By-Step Setup
88
86
 
@@ -294,10 +292,21 @@ Alternatively, you can define your own driver like so:
294
292
 
295
293
  # HOT GLUE DOCS
296
294
 
295
+ Remember: Use `bin/rails generate model Thing` to generate models. Then add `has_many`, `belongs_to`, and _migrate your database_ before building the scaffold with Hot Glue.
296
+
297
+ You will also need every Rails model to contain _either_ a database column _or_ an object-level method named one of these five things:
298
+ `name`
299
+ `to_label`
300
+ `full_name`
301
+ `display_name`
302
+ `email`
303
+
304
+ If your database doesn't contain one of these five, add a method to your model using `def to_label`. This will be used as the default label for the object throughout the Hot Glue build system.
305
+
297
306
  ## First Argument
298
307
  (no double slash)
299
308
 
300
- TitleCase class name of the thing you want to build a scaffoling for.
309
+ TitleCase class name of the thing you want to build a scaffolding for.
301
310
 
302
311
  ```
303
312
  ./bin/rails generate hot_glue:scaffold Thing
@@ -672,8 +681,9 @@ Notice that each modifiers can be used with specific field types.
672
681
  | (truthy label)\|(falsy label) | specify a binary switch with a pipe (\|) character if the value is truthy, it will display as "truthy label" if the value is falsy, it will display as "falsy label" | booleans, datetimes, dates, times | | |
673
682
  | partials | applies to enums only, you must have a partial whose name matches each enum type | enums only | | |
674
683
  | tinymce | applies to text fields only, be sure to setup TineMCE globally | text fields only | | |
684
+ | typeahead | turns a foreign key (only) into a searchable typeahead field | foreign keys only | | |
675
685
 
676
- Except for "(truthy label)" and "(falsy label)" which represent the labels you should specify separated by the pipe character (|), use the modifier exactly as shown.
686
+ Except for "(truthy label)" and "(falsy label)" which use the special syntax, use the modifier _exactly_ as it is named.
677
687
 
678
688
  ### `--pundit`
679
689
  If you enable Pundit, your controllers will look for a Policy that matches the name of the thing being built.
@@ -954,6 +964,25 @@ This happens using two interconnected mechanisms:
954
964
  please note that *creating* and *deleting* do not yet have a full & complete implementation: Your pages won't re-render the pages being viewed cross-peer (that is, between two users using the app at the same time) if the insertion or deletion causes the pagination to be off for another user.
955
965
 
956
966
 
967
+ ### `--related-sets`
968
+
969
+ Used to show a checkbox set of related records. The relationship should be a `has_and_belongs_to_many` or a `has_many through:` from the object being built.
970
+
971
+ Consider the classic example of three tables: users, user_roles, and roles
972
+
973
+ User `has_many :user_roles`; UserRole `belongs_to :user` and `belongs_to :role`; and Role `has_many :user_roles` and `has_many :user, through: :user_roles`
974
+
975
+ We'll generate a scaffold to edit the users table. A checkbox set of related roles will also appear to allow editing of roles. (In this example, the only field to be edited is the email field.)
976
+
977
+ ```
978
+ rails generate hot_glue:scaffold User --related-sets=roles --include=email,roles --gd
979
+ ```
980
+
981
+ Note this leaves open a privileged escalation attack (a security vulnerability).
982
+
983
+ To fix this, you'll need to use Pundit with special syntax designed for this purpose. Please see Example #16 in the [https://school.jfbcodes.com/8188](Hot Glue Tutorial).
984
+
985
+
957
986
  ## "Thing" Label
958
987
 
959
988
  Note that on a per model basis, you can also globally omit the label or set a unique label value using
@@ -1393,11 +1422,11 @@ You can now use a typeahead when editing the book. Instead of displaying the aut
1393
1422
  You will do these three things:
1394
1423
 
1395
1424
  1. As a one-time setup step for your app, run
1396
- `bin/rails generate hot_glue:install_typeahead`
1425
+ `bin/rails generate hot_glue:typeahead_install`
1397
1426
  2. When generating a scaffold you want to make a typeahead association, use `--modify='parent_id{typeahead}'` where `parent_id` is the foreign key
1398
1427
  `bin/rails generate hot_glue:scaffold Book --include=title,author_id --modify='author_id{typeahead}'`
1399
1428
  3. Within each namespace, you will generate a special typeahead controller (it exists for the associated object to be searched on
1400
- `bin/rails generate hot_glue:typehead Author`
1429
+ `bin/rails generate hot_glue:typeahead Author`
1401
1430
  This will create a controller for `AuthorsTypeaheadController` that will allow text search against any *string* field on the `Author` model.
1402
1431
  This special generator takes flags `--namespace` as the normal generator and also `--search-by` to let you specify the list of fields you want to search by.
1403
1432
 
@@ -1464,6 +1493,34 @@ bin/rails generate Thing --include=my_story --modify='my_story{tinymce}'
1464
1493
 
1465
1494
  # VERSION HISTORY
1466
1495
 
1496
+ #### v0.6.1 - `--related-sets`
1497
+
1498
+ Used to show a checkbox set of related records. The relationship should be a `has_and_belongs_to_many` or a `has_many through:` from the object being built.
1499
+
1500
+ Consider the classic example of three tables: users, user_roles, and roles
1501
+
1502
+ User `has_many :user_roles`; UserRole `belongs_to :user` and `belongs_to :role`; and Role `has_many :user_roles` and `has_many :user, through: :user_roles`
1503
+
1504
+ We'll generate a scaffold to edit the users table. A checkbox set of related roles will also appear to allow editing of roles. (In this example, the only field to be edited is the email field.)
1505
+
1506
+ ```
1507
+ rails generate hot_glue:scaffold User --related-sets=roles --include=email,roles --gd
1508
+ ```
1509
+
1510
+ Note that when making a scaffold like this, you may leave open a privileged escalation attack (a security vulnerability).
1511
+
1512
+ To fix this, you'll need to use Pundit with special syntax designed for this purpose.
1513
+
1514
+ For a complete solution, please see Example #16 in the [https://school.jfbcodes.com/8188](Hot Glue Tutorial).
1515
+
1516
+ Without Pundit, due to a quirk in how this code works with ActiveRecord, all update operates to the related sets table are permitted (and go through), even if the update operation otherwise fails validation for the fields on the object. (ActiveRecord doesn't seem to have a way to validate the related sets directly.)
1517
+
1518
+ In this case, your update actions may update the relate sets table but fail to update the current object.
1519
+
1520
+ Using this feature with Pundit will fix this problem, and it is achieved with a (hacky) implementation that performs a pre-check for each related set against the Pundit policy.
1521
+
1522
+ #### v0.6.0.1 - small tweaks to typeahead
1523
+
1467
1524
  #### 2023-11-03 - v0.6.0
1468
1525
 
1469
1526
  Typeahead Associations
@@ -1476,7 +1533,7 @@ from a searchable typehead input.
1476
1533
  The typeahead is implemented with a native Stimulus JS pair of controllers and is a modern & clean replacement to the old typeahead options.
1477
1534
 
1478
1535
  1. As a one-time setup step for your app, run
1479
- `bin/rails generate hot_glue:install_typeahead`
1536
+ `bin/rails generate hot_glue:typeahead_install`
1480
1537
  2. When generating a scaffold you want to make a typeahead association, use `--modify='parent_id{typeahead}'` where `parent_id` is the foreign key
1481
1538
  `bin/rails generate hot_glue:scaffold Book --include=title,author_id --modify='author_id{typeahead}'`
1482
1539
  3. Within each namespace, you will generate a special typeahead controller (it exists for the associated object to be searched on
@@ -10,6 +10,8 @@ require_relative "fields/text_field"
10
10
  require_relative "fields/time_field"
11
11
  require_relative "fields/uuid_field"
12
12
  require_relative "fields/attachment_field"
13
+ require_relative "fields/related_set_field"
14
+
13
15
 
14
16
 
15
17
  class FieldFactory
@@ -42,6 +44,8 @@ class FieldFactory
42
44
  EnumField
43
45
  when :attachment
44
46
  AttachmentField
47
+ when :related_set
48
+ RelatedSetField
45
49
  end
46
50
  @class_name = class_name
47
51
 
@@ -61,6 +65,7 @@ class FieldFactory
61
65
  modify_as: generator.modify_as[name.to_sym] || nil,
62
66
  display_as: generator.display_as[name.to_sym] || nil,
63
67
  default_boolean_display: generator.default_boolean_display,
64
- namespace: generator.namespace_value)
68
+ namespace: generator.namespace_value,
69
+ pundit: generator.pundit )
65
70
  end
66
71
  end
@@ -10,7 +10,7 @@ class AssociationField < Field
10
10
  update_show_only: ,
11
11
  hawk_keys: , auth: , sample_file_path:, ownership_field: ,
12
12
  attachment_data: nil , layout_strategy: , form_placeholder_labels: nil,
13
- form_labels_position:, modify_as: , self_auth: , namespace: )
13
+ form_labels_position:, modify_as: , self_auth: , namespace:, pundit: )
14
14
  super
15
15
 
16
16
  @assoc_model = eval("#{class_name}.reflect_on_association(:#{assoc})")
@@ -80,20 +80,20 @@ class AssociationField < Field
80
80
  assoc = eval("#{class_name}.reflect_on_association(:#{assoc_name})")
81
81
 
82
82
  if modify_as && modify_as[:typeahead]
83
- search_url = "#{namespace ? namespace + "_" : ""}#{assoc.plural_name}_typeahead_index_url"
84
-
85
- "<div class='typeahead typeahead--#{singular}--#{assoc.name}_id'
86
- data-controller='typeahead'
87
- data-typeahead-url-value='<%= #{search_url} %>'
88
- data-typeahead-typeahead-results-outlet='#search-results'>
89
- <%= text_field_tag :#{assoc.plural_name}_query, '', placeholder: 'Search #{assoc.plural_name}', class: 'search__input',
90
- data: { action: 'keyup->typeahead#fetchResults keydown->typeahead#navigateResults', typeahead_target: 'query' },
91
- autofocus: true,
92
- autocomplete: 'off',
93
- value: #{singular}.try(:#{assoc.name}).try(:name) %>
94
- <%= f.hidden_field :#{assoc.name}_id, value: #{singular}.try(:#{assoc.name}).try(:id), 'data-typeahead-target': 'hiddenFormValue' %>
95
- <div data-typeahead-target='results'></div>
96
- </div>"
83
+ search_url = "#{namespace ? namespace + "_" : ""}#{assoc.plural_name}_typeahead_index_url"
84
+ "<div class='typeahead typeahead--#{assoc.name}_id'
85
+ data-controller='typeahead'
86
+ data-typeahead-url-value='<%= #{search_url} %>'
87
+ data-typeahead-typeahead-results-outlet='#search-results'>
88
+ <%= text_field_tag :#{assoc.plural_name}_query, '', placeholder: 'Search #{assoc.plural_name}', class: 'search__input',
89
+ data: { action: 'keyup->typeahead#fetchResults keydown->typeahead#navigateResults', typeahead_target: 'query' },
90
+ autofocus: true,
91
+ autocomplete: 'off',
92
+ value: #{singular}.try(:#{assoc.name}).try(:name) %>
93
+ <%= f.hidden_field :#{assoc.name}_id, value: #{singular}.try(:#{assoc.name}).try(:id), 'data-typeahead-target': 'hiddenFormValue' %>
94
+ <div data-typeahead-target='results'></div>
95
+ <div data-typeahead-target='classIdentifier' data-id=\"typeahead--#{assoc_name}_id\"></div>
96
+ </div>"
97
97
  else
98
98
  if assoc.nil?
99
99
  exit_message = "*** Oops. on the #{class_name} object, there doesn't seem to be an association called '#{assoc_name}'"
@@ -1,10 +1,9 @@
1
1
  class AttachmentField < Field
2
2
  attr_accessor :attachment_data
3
3
  def initialize(name:, class_name:, default_boolean_display: ,
4
- display_as:,
5
- singular:, update_show_only:, hawk_keys:, auth:,
4
+ display_as:, singular:, update_show_only:, hawk_keys:, auth:,
6
5
  sample_file_path: nil, attachment_data:, ownership_field:, layout_strategy: ,
7
- form_placeholder_labels: , form_labels_position:, modify_as:, self_auth: , namespace: )
6
+ form_placeholder_labels: , form_labels_position:, modify_as:, self_auth: , namespace:, pundit: )
8
7
  super
9
8
 
10
9
  @attachment_data = attachment_data
@@ -5,7 +5,7 @@ class Field
5
5
  :hawk_keys, :layout_strategy, :limit, :modify_as, :name, :object, :sample_file_path,
6
6
  :self_auth,
7
7
  :singular_class, :singular, :sql_type, :ownership_field,
8
- :update_show_only, :namespace
8
+ :update_show_only, :namespace, :pundit
9
9
 
10
10
  def initialize(
11
11
  auth: ,
@@ -24,7 +24,8 @@ class Field
24
24
  singular: ,
25
25
  update_show_only:,
26
26
  self_auth:,
27
- namespace:
27
+ namespace:,
28
+ pundit:
28
29
  )
29
30
  @name = name
30
31
  @layout_strategy = layout_strategy
@@ -40,13 +41,14 @@ class Field
40
41
  @form_labels_position = form_labels_position
41
42
  @modify_as = modify_as
42
43
  @display_as = display_as
44
+ @pundit = pundit
43
45
 
44
46
  @self_auth = self_auth
45
47
  @default_boolean_display = default_boolean_display
46
- @namesapce = namespace
48
+ @namespace = namespace
47
49
 
48
50
  # TODO: remove knowledge of subclasses from Field
49
- unless self.class == AttachmentField
51
+ unless self.class == AttachmentField || self.class == RelatedSetField
50
52
  @sql_type = eval("#{class_name}.columns_hash['#{name}']").sql_type
51
53
  @limit = eval("#{class_name}.columns_hash['#{name}']").limit
52
54
  end
@@ -56,6 +58,10 @@ class Field
56
58
  @name
57
59
  end
58
60
 
61
+ def form_field_output
62
+ raise "superclass must implement"
63
+ end
64
+
59
65
  def field_error_name
60
66
  name
61
67
  end
@@ -0,0 +1,60 @@
1
+ class RelatedSetField < Field
2
+
3
+ attr_accessor :assoc_name, :assoc_class, :assoc
4
+
5
+ def initialize( class_name: , default_boolean_display:, display_as: ,
6
+ name: , singular: ,
7
+ update_show_only: ,
8
+ hawk_keys: , auth: , sample_file_path:, ownership_field: ,
9
+ attachment_data: nil , layout_strategy: , form_placeholder_labels: nil,
10
+ form_labels_position:, modify_as: , self_auth: , namespace:, pundit:)
11
+ super
12
+
13
+ @related_set_model = eval("#{class_name}.reflect_on_association(:#{name})")
14
+
15
+ if @related_set_model.nil?
16
+ raise "You specified a related set #{name} but there is no association on #{singular_class} for #{name}; please add a `has_and_belongs_to_many :#{name}` OR a `has_many :#{name}, through: ...` to the #{singular_class} model"
17
+
18
+ # exit_message = "*** Oops: The model #{class_name} is missing an association for :#{assoc_name} or the model #{assoc_name.titlecase} doesn't exist. TODO: Please implement a model for #{assoc_name.titlecase}; or add to #{class_name} `belongs_to :#{assoc_name}`. To make a controller that can read all records, specify with --god."
19
+ puts exit_message
20
+ raise(HotGlue::Error, exit_message)
21
+ end
22
+
23
+ @assoc_class = eval(@related_set_model.try(:class_name))
24
+
25
+ name_list = [:name, :to_label, :full_name, :display_name, :email]
26
+
27
+ if assoc_class && name_list.collect{ |field|
28
+ assoc_class.respond_to?(field.to_s) || assoc_class.instance_methods.include?(field)
29
+ }.none?
30
+ exit_message = "Oops: Missing a label for `#{assoc_class}`. Can't find any column to use as the display label for the #{@assoc_name} association on the #{class_name} model. TODO: Please implement just one of: 1) name, 2) to_label, 3) full_name, 4) display_name 5) email. You can implement any of these directly on your`#{assoc_class}` model (can be database fields or model methods) or alias them to field you want to use as your display label. Then RERUN THIS GENERATOR. (Field used will be chosen based on rank here.)"
31
+ raise(HotGlue::Error, exit_message)
32
+ end
33
+
34
+ end
35
+
36
+
37
+ def form_field_output
38
+ disabled_syntax = +""
39
+ if pundit
40
+ disabled_syntax << ", {disabled: ! #{class_name}Policy.new(#{auth}, @#{singular}).role_ids_able?}"
41
+ end
42
+ " <%= f.collection_check_boxes :#{association_ids_method}, #{association_class_name}.all, :id, :label, {}#{disabled_syntax} do |m| %>
43
+ <%= m.check_box %> <%= m.label %><br />
44
+ <% end %>"
45
+ end
46
+
47
+ def association_ids_method
48
+ eval("#{class_name}.reflect_on_association(:#{name})").class_name.underscore + "_ids"
49
+ end
50
+
51
+ def association_class_name
52
+ eval("#{class_name}.reflect_on_association(:#{name})").class_name
53
+ end
54
+
55
+ def viewable_output
56
+ "<%= #{singular}.#{name}.collect(&:label).join(\", \") %>"
57
+ end
58
+ end
59
+
60
+
@@ -8,7 +8,7 @@ module HotGlue
8
8
  :inline_list_labels, :layout_object,
9
9
  :columns, :col_identifier, :singular,
10
10
  :form_placeholder_labels, :hawk_keys, :update_show_only,
11
- :attachments, :show_only, :columns_map, :pundit
11
+ :attachments, :show_only, :columns_map, :pundit, :related_sets
12
12
 
13
13
 
14
14
  def initialize(singular:, singular_class: ,
@@ -17,7 +17,7 @@ module HotGlue
17
17
  ownership_field: , form_labels_position: ,
18
18
  inline_list_labels: ,
19
19
  form_placeholder_labels:, hawk_keys: ,
20
- update_show_only:, attachments: , columns_map:, pundit: )
20
+ update_show_only:, attachments: , columns_map:, pundit:, related_sets: )
21
21
 
22
22
  @singular = singular
23
23
  @singular_class = singular_class
@@ -39,6 +39,7 @@ module HotGlue
39
39
  @hawk_keys = hawk_keys
40
40
  @update_show_only = update_show_only
41
41
  @attachments = attachments
42
+ @related_sets = related_sets
42
43
  end
43
44
 
44
45
  def add_spaces_each_line(text, num_spaces)
@@ -150,7 +151,7 @@ module HotGlue
150
151
 
151
152
  "<div class='#{col_identifier} #{singular}--#{column.join("-")}'#{style_with_flex_basis}> " +
152
153
  column.map { |col|
153
- if eval("#{singular_class}.columns_hash['#{col}']").nil? && !attachments.keys.include?(col)
154
+ if eval("#{singular_class}.columns_hash['#{col}']").nil? && !attachments.keys.include?(col) && !related_sets.include?(col)
154
155
  raise "Can't find column '#{col}' on #{singular_class}, are you sure that is the column name?"
155
156
  end
156
157
  field_output = columns_map[col].line_field_output
@@ -23,7 +23,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
23
23
  :nest_with, :path, :plural, :sample_file_path, :show_only_data, :singular,
24
24
  :singular_class, :smart_layout, :stacked_downnesting, :update_show_only, :ownership_field,
25
25
  :layout_strategy, :form_placeholder_labels, :form_labels_position, :pundit,
26
- :self_auth, :namespace_value
26
+ :self_auth, :namespace_value, :related_sets
27
27
  # important: using an attr_accessor called :namespace indirectly causes a conflict with Rails class_name method
28
28
  # so we use namespace_value instead
29
29
 
@@ -89,6 +89,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
89
89
  class_option :modify, default: {}
90
90
  class_option :display_as, default: {}
91
91
  class_option :pundit, default: nil
92
+ class_option :related_sets, default: ''
92
93
 
93
94
  def initialize(*meta_args)
94
95
  super
@@ -320,7 +321,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
320
321
  end
321
322
 
322
323
  if @god
323
- @auth = nil
324
+ # @auth = nil
324
325
  end
325
326
  # when in self auth, the object is the same as the authenticated object
326
327
 
@@ -370,6 +371,24 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
370
371
  puts "NESTING: #{@nested_set}"
371
372
  end
372
373
 
374
+ # related_sets
375
+ related_set_input = options['related_sets'].split(",")
376
+ @related_sets = {}
377
+ related_set_input.each do |setting|
378
+ name = setting.to_sym
379
+ association_ids_method = eval("#{singular_class}.reflect_on_association(:#{setting.to_sym})").class_name.underscore + "_ids"
380
+ class_name = eval("#{singular_class}.reflect_on_association(:#{setting.to_sym})").class_name
381
+
382
+ @related_sets[setting.to_sym] = { name: setting.to_sym,
383
+ association_ids_method: association_ids_method,
384
+ class_name: class_name }
385
+ end
386
+
387
+ if @related_sets.any?
388
+ puts "RELATED SETS: #{@related_sets}"
389
+
390
+ end
391
+
373
392
  # OBJECT OWNERSHIP & NESTING
374
393
  @reference_name = HotGlue.derrive_reference_name(singular_class)
375
394
  if @auth && @self_auth
@@ -409,13 +428,16 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
409
428
 
410
429
  buttons_width = ((!@no_edit && 1) || 0) + ((!@no_delete && 1) || 0) + @magic_buttons.count
411
430
 
431
+
432
+
433
+
412
434
  # build a new polymorphic object
413
435
  @associations = []
414
436
  @columns_map = {}
415
437
  @columns.each do |col|
416
- if !(@the_object.columns_hash.keys.include?(col.to_s) || @attachments.keys.include?(col))
417
- raise "couldn't find #{col} in either field list or attachments list"
418
- end
438
+ # if !(@the_object.columns_hash.keys.include?(col.to_s) || @attachments.keys.include?(col))
439
+ # raise "couldn't find #{col} in either field list or attachments list"
440
+ # end
419
441
 
420
442
  if col.to_s.starts_with?("_")
421
443
  @show_only << col
@@ -425,6 +447,10 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
425
447
  type = @the_object.columns_hash[col.to_s].type
426
448
  elsif @attachments.keys.include?(col)
427
449
  type = :attachment
450
+ elsif @related_sets.keys.include?(col)
451
+ type = :related_set
452
+ else
453
+ raise "couldn't find #{col} in either field list, attachments, or related sets"
428
454
  end
429
455
  this_column_object = FieldFactory.new(name: col.to_s,
430
456
  generator: self,
@@ -440,7 +466,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
440
466
  if field.is_a?(AssociationField)
441
467
  if @modify_as && @modify_as[key] && @modify_as[key][:typeahead]
442
468
  assoc_name = field.assoc_name
443
- file_path = "app/controllers/#{namespace ? namspace + "/" : ""}#{assoc_name.pluralize}_typeahead_controller.rb"
469
+ file_path = "app/controllers/#{@namespace ? @namespace + "/" : ""}#{assoc_name.pluralize}_typeahead_controller.rb"
444
470
 
445
471
  if ! File.exist?(file_path)
446
472
 
@@ -455,6 +481,8 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
455
481
  end
456
482
  end
457
483
 
484
+
485
+
458
486
  # create the template object
459
487
  if @markup == "erb"
460
488
  @template_builder = HotGlue::ErbTemplate.new(
@@ -473,6 +501,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
473
501
  attachments: @attachments,
474
502
  columns_map: @columns_map,
475
503
  pundit: @pundit,
504
+ related_sets: @related_sets
476
505
  )
477
506
  elsif @markup == "slim"
478
507
  raise(HotGlue::Error, "SLIM IS NOT IMPLEMENTED")
@@ -607,6 +636,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
607
636
  end
608
637
 
609
638
  def identify_object_owner
639
+ return if @god
610
640
  auth_assoc = @auth && @auth.gsub("current_", "")
611
641
 
612
642
  if @object_owner_sym && !@self_auth
@@ -657,6 +687,16 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
657
687
 
658
688
  check_if_sample_file_is_present
659
689
  end
690
+
691
+ if @related_sets.any?
692
+ if !@pundit
693
+ puts "********************\nWARNING: You are using --related-sets without using Pundit. This makes the set fully accessible. Use Pundit to prevent a privileged escalation vulnerability\n********************\n"
694
+ end
695
+ @related_sets.each do |key, related_set|
696
+ @columns << related_set[:name] if !@columns.include?(related_set[:name])
697
+ puts "Adding related set :#{related_set[:name]} as-a-column"
698
+ end
699
+ end
660
700
  end
661
701
 
662
702
  def check_if_sample_file_is_present
@@ -676,13 +716,13 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
676
716
  puts ""
677
717
  end
678
718
 
679
- def fields_filtered_for_email_lookups
680
- @columns
719
+ def fields_filtered_for_strong_params
720
+ @columns - @related_sets.collect{|key, set| set[:name]}
681
721
  end
682
722
 
683
723
  def creation_syntax
684
724
  if @factory_creation == ''
685
- "@#{singular } = #{ class_name }.create(modified_params)"
725
+ "@#{singular } = #{ class_name }.new(modified_params)"
686
726
  else
687
727
  "#{@factory_creation}\n" +
688
728
  " @#{singular } = factory.#{singular}"
@@ -969,7 +1009,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
969
1009
  end
970
1010
 
971
1011
  def object_scope
972
- if @auth
1012
+ if @auth && !@god
973
1013
  if @nested_set.none?
974
1014
  @auth + ".#{plural}"
975
1015
  else
@@ -1296,7 +1336,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
1296
1336
 
1297
1337
  def n_plus_one_includes
1298
1338
  if @associations.any? || @attachments.any?
1299
- ".includes(" + (@associations.map { |x| x } + @attachments.collect { |k, v| "#{k}_attachment" }).map { |x| ":#{x.to_s}" }.join(", ") + ")"
1339
+ ".includes(" + (@associations.map { |x| x } + @attachments.collect { |k, v| "#{k}_attachment" } ).map { |x| ":#{x.to_s}" }.join(", ") + ")"
1300
1340
  else
1301
1341
  ""
1302
1342
  end
@@ -1342,7 +1382,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
1342
1382
  end
1343
1383
 
1344
1384
  def any_datetime_fields?
1345
- (@columns - @attachments.keys.collect(&:to_sym)).collect { |col| eval("#{singular_class}.columns_hash['#{col}']").type }.include?(:datetime)
1385
+ (@columns - @attachments.keys.collect(&:to_sym) - @related_sets.keys ).collect { |col| eval("#{singular_class}.columns_hash['#{col}']").type }.include?(:datetime)
1346
1386
  end
1347
1387
 
1348
1388
  def post_action_parental_updates
@@ -72,7 +72,7 @@ class <%= controller_class_name %> < <%= controller_descends_from %>
72
72
 
73
73
  <% end %>def load_all_<%= plural %><% if @pundit %>
74
74
  @<%= plural_name %> = policy_scope(<%= object_scope %>).page(params[:page])<%= n_plus_one_includes %><%= ".per(per)" if @paginate_per_page_selector %>
75
- authorize @<%= plural_name %>.all<% else %> <% if !@self_auth %>
75
+ <% else %> <% if !@self_auth %>
76
76
  @<%= plural_name %> = <%= object_scope.gsub("@",'') %><%= n_plus_one_includes %>.page(params[:page])<%= ".per(per)" if @paginate_per_page_selector %><%= " if params.include?(:#{ @nested_set.last[:singular]}_id)" if @nested_set.any? && @nested_set[0] && @nested_set[0][:optional] %><% if @nested_set[0] && @nested_set[0][:optional] %>
77
77
  @<%= plural_name %> = <%= class_name %>.all<% end %><% else %>
78
78
  @<%= plural_name %> = <%= class_name %>.where(id: <%= auth_object.gsub("@",'') %>.id)<%= n_plus_one_includes %>.page(params[:page])<%= ".per(per)" if @paginate_per_page_selector %><% end %>
@@ -80,7 +80,8 @@ class <%= controller_class_name %> < <%= controller_descends_from %>
80
80
  end
81
81
 
82
82
  def index
83
- load_all_<%= plural %><% if @pundit %>
83
+ load_all_<%= plural %><% if @pundit %><% if @pundit %>
84
+ authorize @<%= plural_name %><% end %>
84
85
  rescue Pundit::NotAuthorizedError
85
86
  flash[:alert] = "You are not authorized to perform this action."
86
87
  render "layouts/error"<% end %>
@@ -94,19 +95,23 @@ class <%= controller_class_name %> < <%= controller_descends_from %>
94
95
  @action = "new"
95
96
  rescue Pundit::NotAuthorizedError
96
97
  flash[:alert] = "You are not authorized to perform this action."
98
+ load_all_users
97
99
  render :index<% end %>
98
100
  end
99
101
 
100
102
  def create
101
103
  modified_params = modify_date_inputs_on_params(<%= singular_name %>_params.dup, <%= current_user_object %>, <%= datetime_fields_list %>) <% if @object_owner_sym && eval("#{class_name}.reflect_on_association(:#{@object_owner_sym})").class == ActiveRecord::Reflection::BelongsToReflection %>
102
104
  modified_params = modified_params.merge(<%= @object_owner_sym %>: <%= @object_owner_eval %>) <% elsif @object_owner_optional && any_nested? %>
103
- modified_params = modified_params.merge(<%= @object_owner_name %> ? {<%= @object_owner_sym %>: <%= @object_owner_eval %>} : {}) <% elsif !@object_owner_eval.empty? %>
105
+ modified_params = modified_params.merge(<%= @object_owner_name %> ? {<%= @object_owner_sym %>: <%= @object_owner_eval %>} : {}) <% elsif !@object_owner_eval.empty? && !@god %>
104
106
  modified_params = modified_params.merge(<%= @object_owner_eval %>) <% end %>
105
107
 
106
108
  <% if @hawk_keys.any? %>
107
109
  modified_params = hawk_params({<%= hawk_to_ruby %>}, modified_params)<% end %>
108
- <%= controller_attachment_orig_filename_pickup_syntax %>
110
+ <%= controller_attachment_orig_filename_pickup_syntax %>
109
111
  <%= creation_syntax %>
112
+ <% if @pundit %><% @related_sets.each do |key, related_set| %>
113
+ check_<%= related_set[:association_ids_method].to_s %>_permissions(modified_params, :create)<% end %><% end %>
114
+ <% if @pundit %>authorize @<%= singular %><% end %>
110
115
 
111
116
  if @<%= singular_name %>.save
112
117
  flash[:notice] = "Successfully created #{@<%= singular %>.<%= display_class %>}"
@@ -152,17 +157,20 @@ class <%= controller_class_name %> < <%= controller_descends_from %>
152
157
  flash[:notice] << "<% singular %> <%= button.titlecase %>."
153
158
  end
154
159
  <% end %>
160
+
155
161
  modified_params = modify_date_inputs_on_params(<% if @update_show_only %>update_<% end %><%= singular_name %>_params.dup<%= controller_update_params_tap_away_magic_buttons %>, <%= current_user_object %>, <%= datetime_fields_list %>) <% if @object_owner_sym && eval("#{class_name}.reflect_on_association(:#{@object_owner_sym})").class == ActiveRecord::Reflection::BelongsToReflection %>
156
162
  modified_params = modified_params.merge(<%= @object_owner_sym %>: <%= @object_owner_eval %>) <% elsif @object_owner_optional && any_nested? %>
157
- modified_params = modified_params.merge(<%= @object_owner_name %> ? {<%= @object_owner_sym %>: <%= @object_owner_eval %>} : {}) <% elsif ! @object_owner_eval.empty? && !@self_auth%>
163
+ modified_params = modified_params.merge(<%= @object_owner_name %> ? {<%= @object_owner_sym %>: <%= @object_owner_eval %>} : {}) <% elsif ! @object_owner_eval.empty? && !@self_auth && ! @god%>
158
164
  modified_params = modified_params.merge(<%= @object_owner_eval %>) <% end %>
165
+ <% if @pundit %><% @related_sets.each do |key, related_set| %>
166
+ check_<%= related_set[:association_ids_method].to_s %>_permissions(modified_params, :update)<% end %><% end %>
159
167
 
160
168
  <% if @hawk_keys.any? %> modified_params = hawk_params({<%= hawk_to_ruby %>}, modified_params)<% end %>
161
169
  <%= controller_attachment_orig_filename_pickup_syntax %>
162
170
  <% if @pundit %>
163
171
  if @<%= singular_name %>.attributes = modified_params
164
- authorize @<%= singular_name %>
165
- @<%= singular_name %>.save
172
+ authorize @<%= singular_name %>
173
+ @<%= singular_name %>.save
166
174
  <% else %>
167
175
  if @<%= singular_name %>.update(modified_params)
168
176
  <% end %>
@@ -194,12 +202,23 @@ class <%= controller_class_name %> < <%= controller_descends_from %>
194
202
  render :update<% end %>
195
203
  end<% end %>
196
204
 
205
+ <% if @pundit %><% @related_sets.each do |key, rs| %>
206
+ def check_<%= rs[:association_ids_method] %>_permissions(modified_params, action)
207
+ if modified_params[:<%= rs[:association_ids_method] %>].present? && modified_params[:<%= rs[:association_ids_method] %>] != @<%= singular %>.<%= rs[:association_ids_method] %>
208
+ # authorize the <%= rs[:association_ids_method] %> change using special modified_relations: {<%= rs[:association_ids_method] %>: modified_params[:<%= rs[:association_ids_method] %>>]} syntax for Pundit
209
+ if ! <%= singular_class %>Policy.new(current_user, @<%= singular %>, modified_relations: {role_ids: modified_params[:<%= rs[:association_ids_method] %>]}).method("#{action}?".to_sym).call
210
+ authorize @<%= singular %>, "#{action}?".to_sym
211
+ raise Pundit::NotAuthorizedError, message: @<%= singular %>.errors.collect{|k| "#{k.attribute} #{k.message}"}.join(" ")
212
+ end
213
+ end
214
+ end<% end %><% end %>
215
+
197
216
  def <%=singular_name%>_params
198
- params.require(:<%= testing_name %>).permit(<%= (fields_filtered_for_email_lookups - @show_only ) + @magic_buttons.collect{|x| "__#{x}".to_sym }%>)
217
+ params.require(:<%= testing_name %>).permit(<%= ((fields_filtered_for_strong_params - @show_only ) + @magic_buttons.collect{|x| "__#{x}"}).collect{|sym| ":#{sym}"}.join(", ") %><%= ", " + @related_sets.collect{|key, rs| "#{rs[:association_ids_method]}: []"}.join(", ") if @related_sets.any? %>)
199
218
  end<% if @update_show_only %>
200
219
 
201
220
  def update_<%=singular_name%>_params
202
- params.require(:<%= testing_name %>).permit(<%= (fields_filtered_for_email_lookups - @update_show_only) + @magic_buttons.collect{|x| "__#{x}".to_sym }%>)
221
+ params.require(:<%= testing_name %>).permit(<%= ((fields_filtered_for_strong_params - @update_show_only) + @magic_buttons.collect{|x| "__#{x}"}).collect{|sym| ":#{sym}"}.join(", ") %><%= ", " + @related_sets.collect{|key, rs| "#{rs[:association_ids_method]}: []"}.join(", ") if @related_sets.any? %>)
203
222
  end<% end %>
204
223
 
205
224
  def namespace
@@ -6,7 +6,7 @@ class <%= ((@namespace.titleize.gsub(" ", "") + "::" if @namespace) || "") + @pl
6
6
 
7
7
  def index
8
8
  query = params[:query]
9
-
9
+ @typeahead_identifier = params[:typeahead_identifier]
10
10
  @<%= @plural %> = <%= @singular.titleize.gsub(" ", "") %>.where("<%= @search_by.collect{|search| "LOWER(#{search}) LIKE ?" }.join(" OR ") %>", <%= @search_by.collect{|search| "\"%\#{query.downcase}%\"" }.join(", ") %>).limit(10)
11
11
 
12
12
  render layout: false
@@ -1,7 +1,7 @@
1
1
 
2
2
  <div class="typeahead-results__<%= @plural %>"
3
3
  data-controller="typeahead-results"
4
- data-typeahead-results-typeahead-outlet=".typeahead--book--<%= @singular %>_id"
4
+ data-typeahead-results-typeahead-outlet=".<\%= @typeahead_identifier %>"
5
5
  data-typeahead-results-current-class="search__result--current" >
6
6
  <ul class="search__results" data-typeahead-results-target="result">
7
7
  <\% if @<%= @plural %>.any? %>
@@ -1,7 +1,8 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
3
  export default class extends Controller {
4
- static targets = [ "query", "results", "hiddenFormValue" ]
4
+ static targets = [ "query", "results", "hiddenFormValue",
5
+ "classIdentifier"]
5
6
  static values = { url: String }
6
7
  static outlets = [ "typeahead-results" ]
7
8
 
@@ -10,6 +11,10 @@ export default class extends Controller {
10
11
  }
11
12
 
12
13
  fetchResults() {
14
+
15
+
16
+ var typeaheadIdentifier = this.classIdentifierTarget.dataset.id
17
+
13
18
  if(this.query == "") {
14
19
  this.reset()
15
20
  return
@@ -22,6 +27,7 @@ export default class extends Controller {
22
27
 
23
28
  const url = new URL(this.urlValue)
24
29
  url.searchParams.append("query", this.query)
30
+ url.searchParams.append("typeahead_identifier", typeaheadIdentifier)
25
31
 
26
32
  this.abortPreviousFetchRequest()
27
33
 
@@ -12,7 +12,6 @@ export default class extends Controller {
12
12
 
13
13
  connect() {
14
14
  this.currentResultIndex = 0
15
-
16
15
  const allElements = this.resultTarget.querySelectorAll(".search-result-item");
17
16
 
18
17
  allElements.forEach((element, index) => {
@@ -29,7 +28,6 @@ export default class extends Controller {
29
28
  const result_id = element.dataset.id;
30
29
 
31
30
  // how to pass this to the search controller, set the field value and clear out the search
32
- console.log("search item clicked...", result_value, result_id)
33
31
 
34
32
  this.typeaheadOutlets.forEach(outlet => {
35
33
  outlet.hiddenFormValueTarget.value = result_id;
@@ -1,5 +1,5 @@
1
1
  module HotGlue
2
2
  class Version
3
- CURRENT = '0.6.0'
3
+ CURRENT = '0.6.1'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hot-glue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Fleetwood-Boldt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-03 00:00:00.000000000 Z
11
+ date: 2023-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -90,6 +90,7 @@ files:
90
90
  - lib/generators/hot_glue/fields/field.rb
91
91
  - lib/generators/hot_glue/fields/float_field.rb
92
92
  - lib/generators/hot_glue/fields/integer_field.rb
93
+ - lib/generators/hot_glue/fields/related_set_field.rb
93
94
  - lib/generators/hot_glue/fields/string_field.rb
94
95
  - lib/generators/hot_glue/fields/text_field.rb
95
96
  - lib/generators/hot_glue/fields/time_field.rb
@@ -174,5 +175,5 @@ requirements: []
174
175
  rubygems_version: 3.4.10
175
176
  signing_key:
176
177
  specification_version: 4
177
- summary: A gem to build Tubro Rails scaffolding.
178
+ summary: A gem to build Turbo Rails scaffolding.
178
179
  test_files: []