bullet_train 1.0.35 → 1.0.38

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,328 @@
1
+ # Super Scaffolding with Delegated Types
2
+
3
+ ## Introduction
4
+ In this guide, we’ll cover how to use Super Scaffolding to build views and controllers around models leveraging delegated types. As a prerequisite, you should read the [native Rails documentation for delegated types](https://edgeapi.rubyonrails.org/classes/ActiveRecord/DelegatedType.html). The examples in that documentation only deal with using delegated types at the Active Record level, but they lay a foundation that we won’t be repeating here.
5
+
6
+ ## Terminology
7
+ For the purposes of our discussion here, and building on the Rails example, we’ll call their `Entry` model the **“Abstract Parent”** and the `Message` and `Comment` models the **“Concrete Children”**.
8
+
9
+ ## One of Multiple Approaches
10
+ It’s worth noting there are at least two different approaches you can take for implementing views and controllers around models using delegated types:
11
+
12
+ 1. Centralize views and controllers around the Abstract Parent (e.g. `Account::EntriesController`).
13
+ 2. Create separate views and controllers for each Concrete Child (e.g. `Account::MessagesController`, `Account::CommentsController`, etc.)
14
+
15
+ **In this guide, we’ll be covering the first approach.** This might not seem like an obvious choice for the `Message` and `Comment` examples we’re drawing on from the Rails documentation (it's not), but it is a very natural fit for other common use cases like:
16
+
17
+ - “I’d like to add a field to this form and there are many kinds of fields.”
18
+ - “I’d like to add a section to this page and there are many kinds of sections.”
19
+
20
+ It’s not to say you can’t do it the other way described above, but this approach has specific benefits:
21
+
22
+ 1. It’s a lot less code. We only have to use Super Scaffolding for the Abstract Parent. It's the only model with views and controllers generated. For the Concrete Children, the only files required are the models, tests, and migrations generated by `rails g model` and some locale Yaml files for each Concrete Child.
23
+ 2. Controller permissions can be enforced the same way they always are, by checking the relationship between the Abstract Parent (e.g. `Entry`) and `Team`. All permissions are defined in `app/models/ability.rb` for `Entry` only, instead of each Concrete Child.
24
+
25
+ ## Steps
26
+
27
+ ### 1. Generate Rails Models
28
+
29
+ Drawing on the [canonical Rails example](https://edgeapi.rubyonrails.org/classes/ActiveRecord/DelegatedType.html), we begin by using Rails' native model generators:
30
+
31
+ ```
32
+ rails g model Entry team:references entryable:references{polymorphic}:index
33
+ rails g model Message subject:string
34
+ rails g model Comment content:text
35
+ ```
36
+
37
+ Note that in this specific approach we don't need a `team:references` on `Message` and `Comment`. That's because in this approach there are no controllers specific to `Message` and `Comment`, so all permissions are being inforced by checking the ownership of `Entry`. (That's not to say it would be wrong to add them for other reasons, we're just keeping it as simple as possible here.)
38
+
39
+ ### 2. Super Scaffolding for `Entry`
40
+
41
+ ```
42
+ bin/super-scaffold crud Entry Team entryable_type:buttons
43
+ ```
44
+
45
+ We use `entryable_type:buttons` because we're going to allow people to choose which type of `Entry` they're creating with a list of buttons. This isn't the only option available to us, but it's the easiest to implement for now.
46
+
47
+ ### 3. Defining Button Options
48
+
49
+ Super Scaffolding will have generated some initial button options for us already in `config/locales/en/entries.en.yml`. We'll want to update the attribute `name`, field `label` (which is shown on the form) and the available options to reflect the available Concrete Children like so:
50
+
51
+ ```
52
+ fields: &fields
53
+ entryable_type:
54
+ name: &entryable_type Entry Type
55
+ label: What type of entry would you like to create?
56
+ heading: *entryable_type
57
+ options:
58
+ "Message": Message
59
+ "Comment": Comment
60
+ ```
61
+
62
+ <small>TODO Insert a live example of what `shared/fields/buttons` looks like with these options passed in.</small>
63
+
64
+ ### 4. Add Our First Step to `new.html.erb`
65
+
66
+ By default, `app/views/account/entries/new.html.erb` has this reference to the shared `_form.html.erb`:
67
+
68
+ ```
69
+ <%= render 'form', entry: @entry %>
70
+ ```
71
+
72
+ However, in this workflow we actually need two steps:
73
+
74
+ 1. Ask the user what type of `Entry` they're creating.
75
+ 2. Show the user the `Entry` form with the appropriate fields for the type of entry they're creating.
76
+
77
+ The first of these two forms is actually not shared between `new.html.erb` and `edit.html.erb`, so we'll copy the contents of `_form.html.erb` into `new.html.erb` as a starting point, like so:
78
+
79
+ ```
80
+ <% if @entry.entryable_type %>
81
+ <%= render 'form', entry: @entry %>
82
+ <% else %>
83
+ <%= form_with model: @entry, url: [:new, :account, @team, :entry], method: :get, local: true, class: 'form' do |form| %>
84
+ <%= render 'account/shared/forms/errors', form: form %>
85
+ <% with_field_settings form: form do %>
86
+ <%= render 'shared/fields/buttons', method: :entryable_type, html_options: {autofocus: true} %>
87
+ <% end %>
88
+ <div class="buttons">
89
+ <%= form.submit t('.buttons.next'), class: "button" %>
90
+ <%= link_to t('global.buttons.cancel'), [:account, @team, :entries], class: "button-secondary" %>
91
+ </div>
92
+ <% end %>
93
+ <% end %>
94
+ ```
95
+
96
+ Here's a summary of the updates required when copying `_form.html.erb` into `new.html.erb`:
97
+
98
+ 1. Add the `if @entry.entryable_type` branch logic, maintaining the existing reference to `_form.html.erb`.
99
+ 2. Add `@` to the `entry` references throughout. `@entry` is an instance variable in this view, not passed in as a local.
100
+ 3. Update the form submission `url` and `method` as seen above.
101
+ 4. Remove the Super Scaffolding hooks. Any additional fields that we add to `Entry` would be on the actual `_form.html.erb`, not this step.
102
+ 5. Simplify button logic because the form is always for a new object.
103
+
104
+ ### 5. Update Locales
105
+
106
+ We need to add a locale entry for the "Next Step" button in `config/locales/en/entries.en.yml`. This goes under the `buttons: &buttons` entry that is already present, like so:
107
+
108
+ ```
109
+ buttons: &buttons
110
+ next: Next Step
111
+ ```
112
+
113
+ Also, sadly, the original locale file wasn't expecting any buttons in `new.html.erb` directly, so we need to include buttons on the `new` page in the same file, below `form: *form`, like so:
114
+
115
+ ```
116
+ new:
117
+ # ...
118
+ form: *form
119
+ buttons: *buttons
120
+ ```
121
+
122
+ ### 6. Add Appropriate Validations in `entry.rb`
123
+
124
+ In `app/models/entry.rb`, we want to replace the default validation of `entryable_type` like so:
125
+
126
+ ```
127
+ ENTRYABLE_TYPES = I18n.t('entries.fields.entryable_type.options').keys.map(&:to_s)
128
+
129
+ validates :entryable_type, inclusion: {
130
+ in: ENTRYABLE_TYPES, allow_blank: false, message: I18n.t('errors.messages.empty')
131
+ }
132
+ ```
133
+
134
+ This makes the locale file, where we define the options to present to the user, the single source of truth for what the valid options are.
135
+
136
+ <small>TODO We should look into whether reflecting on the definition of the delegated types is possible.</small>
137
+
138
+ Also, to make it easy to check the state of this validation, we'll add `entryable_type_valid?` as well:
139
+
140
+ ```
141
+ def entryable_type_valid?
142
+ ENTRYABLE_TYPES.include?(entryable_type)
143
+ end
144
+ ```
145
+
146
+ I don't like this method. If you can think of a way to get rid of it or write it better, please let us know!
147
+
148
+ ### 7. Accept Nested Attributes in `entry.rb` and `entries_controller.rb`
149
+
150
+ In preparation for the second step, we need to configure `Entry` to accept [nested attributes](https://edgeapi.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html). We do this in three parts:
151
+
152
+ In `app/models/entry.rb`, like so:
153
+
154
+ ```
155
+ accepts_nested_attributes_for :entryable
156
+ ```
157
+
158
+ Also in `app/models/entry.rb`, [Rails will be expecting us](https://stackoverflow.com/questions/45295202/cannot-build-nested-polymorphic-associations-are-you-trying-to-build-a-polymor) to define the following method on the model:
159
+
160
+ ```
161
+ def build_entryable(params = {})
162
+ raise 'invalid entryable type' unless entryable_type_valid?
163
+ self.entryable = entryable_type.constantize.new(params)
164
+ end
165
+ ```
166
+
167
+ Finally, in the [strong parameters](https://edgeguides.rubyonrails.org/action_controller_overview.html#strong-parameters) of `app/controllers/account/entries_controller.rb`, _directly below_ this line:
168
+
169
+ ```
170
+ # 🚅 super scaffolding will insert new arrays above this line.
171
+ ```
172
+
173
+ And still within the `permit` parameters, add:
174
+
175
+ ```
176
+ entryable_attributes: [
177
+ :id,
178
+
179
+ # Message attributes:
180
+ :subject,
181
+
182
+ # Comment attributes:
183
+ :content,
184
+ ],
185
+ ```
186
+
187
+ <small>(Eagle-eyed developers will note an edge case here where you would need to take additional steps if you had two Concrete Children classes that shared the same attribute name and you only wanted submitting form data for that attribute to be permissible for one of the classes. That situation should be exceedingly rare, and you can always write a little additional code here to deal with it.)</small>
188
+
189
+ ### 8. Populate `@entry.entryable` in `entries_controller.rb`
190
+
191
+ Before we can present the second step to users, we need to react to the user's input from the first step and initialize either a `Message` or `Comment` object and associate `@entry` with it. We do this in the `new` action of `app/controllers/account/entries_controller.rb` and we can also use the `build_entryable` method we created earlier for this purpose, like so:
192
+
193
+ ```
194
+ def new
195
+ if @entry.entryable_type_valid?
196
+ @entry.build_entryable
197
+ elsif params[:commit]
198
+ @entry.valid?
199
+ end
200
+ end
201
+ ```
202
+
203
+ ### 9. Add the Concrete Children Fields to the Second Step in `_form.html.erb`
204
+
205
+ Since we're now prompting for the entry type on the first step, we can remove the following from the second step in `app/views/account/entries/_form.html.erb`:
206
+
207
+ ```
208
+ <%= render 'shared/fields/buttons', method: :entryable_type, html_options: {autofocus: true} %>
209
+ ```
210
+
211
+ But we need to keep track of which entry type they selected, so we replace it with:
212
+
213
+ ```
214
+ <%= form.hidden_field :entryable_type %>
215
+ ```
216
+
217
+ Also, below that (and below the Super Scaffolding hook), we want to add the `Message` and `Comment` fields as [nested forms](https://guides.rubyonrails.org/form_helpers.html#nested-forms) like so:
218
+
219
+ ```
220
+ <%= form.fields_for :entryable, entry.entryable do |entryable_form| %>
221
+ <%= entryable_form.hidden_field :id %>
222
+ <% with_field_settings form: entryable_form do %>
223
+ <% case entryable_form.object %>
224
+ <% when Message %>
225
+ <%= render 'shared/fields/text_field', method: :subject %>
226
+ <% when Comment %>
227
+ <%= render 'shared/fields/trix_editor', method: :content %>
228
+ <% end %>
229
+ <% end %>
230
+ <% end %>
231
+ ```
232
+
233
+ We add this _below_ the Super Scaffolding hook because we want any additional fields being added to `Entry` directly to appear in the form _above_ the nested form fields.
234
+
235
+ ### 10. Add Attributes of the Concrete Children to `show.html.erb`
236
+
237
+ Under the Super Scaffolding hook in `app/views/account/entries/show.html.erb`, add the following:
238
+
239
+ ```
240
+ <% with_attribute_settings object: @entry.entryable, strategy: :label do %>
241
+ <% case @entry.entryable %>
242
+ <% when Message %>
243
+ <%= render 'shared/attributes/text', attribute: :subject %>
244
+ <% when Comment %>
245
+ <%= render 'shared/attributes/html', attribute: :content %>
246
+ <% end %>
247
+ <% end %>
248
+ ```
249
+
250
+ This will ensure the various different attributes of the Concrete Children are properly presented. However, the `label` strategy for these attribute partials depend on the locales for the individual Concrete Children being defined, so we need to create those files now, as well:
251
+
252
+ `config/locales/en/messages.en.yml`:
253
+ ```
254
+ en:
255
+ messages: &messages
256
+ fields:
257
+ subject:
258
+ _: &subject Subject
259
+ label: *subject
260
+ heading: *subject
261
+ account:
262
+ messages: *messages
263
+ activerecord:
264
+ attributes:
265
+ message:
266
+ subject: *subject
267
+ ```
268
+
269
+ `config/locales/en/comments.en.yml`:
270
+ ```
271
+ en:
272
+ comments: &comments
273
+ fields:
274
+ content:
275
+ _: &content Content
276
+ label: *content
277
+ heading: *content
278
+ account:
279
+ comments: *comments
280
+ activerecord:
281
+ attributes:
282
+ comment:
283
+ content: *content
284
+ ```
285
+
286
+ ### 11. Actually Use Delegated Types?
287
+
288
+ So everything should now be working as expected, and here's the crazy thing: **We haven't even used the delegated types feature yet.** That was part of the beauty of delegated types when it was released in Rails 6.1: It was really just a formalization of an approach that folks had already been doing in Rails for years.
289
+
290
+ <center>
291
+ <br>
292
+ <blockquote class="twitter-tweet"><p lang="en" dir="ltr">Really loving the PR for Rails 6.1&#39;s Delegated Types. From the application developer level, very little of it feels &quot;new&quot;. Instead, the experience reads very similar to what many of us were already doing with the existing tools, but even smoother! <a href="https://t.co/6UkxXNCvaa">https://t.co/6UkxXNCvaa</a></p>&mdash; Andrew Culver (@andrewculver) <a href="https://twitter.com/andrewculver/status/1338189146213543951?ref_src=twsrc%5Etfw">December 13, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
293
+ <br>
294
+ </center>
295
+
296
+ To now incorporate delegated types as put forward in the original documentation, we want to remove this line in `app/models/entry.rb`:
297
+
298
+ ```
299
+ belongs_to :entryable, polymorphic: true
300
+ ```
301
+
302
+ And replace it with:
303
+
304
+ ```
305
+ delegated_type :entryable, types: %w[ Message Comment ]
306
+ ```
307
+
308
+ We also want to follow the other steps seen there, such as defining an `Entryable` concern in `app/models/concerns/entryable.rb`, like so:
309
+
310
+ ```
311
+ module Entryable
312
+ extend ActiveSupport::Concern
313
+
314
+ included do
315
+ has_one :entry, as: :entryable, touch: true
316
+ end
317
+ end
318
+ ```
319
+
320
+ And including the `Entryable` concern in both `app/models/message.rb` and `app/models/comment.rb` like so:
321
+
322
+ ```
323
+ include Entryable
324
+ ```
325
+
326
+ ## Conclusion
327
+
328
+ That's it! You're done! As mentioned at the beginning, this is only one of the ways to approach building views and controllers around your models with delegated types, but it's a common one, and for the situations where it is the right fit, it requires a lot less code and is a lot more [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) than scaffolding views and controllers around each individual delegated type class.
@@ -0,0 +1,246 @@
1
+ # Code Generation with Super Scaffolding
2
+
3
+ Super Scaffolding is Bullet Train’s code generation engine. Its goal is to allow you to produce production-ready CRUD interfaces for your models while barely lifting a finger, and it handles a lot of other grunt-work as well.
4
+
5
+ Here’s a list of what Super Scaffolding takes care of for you each time you add a model to your application:
6
+
7
+ - It generates a basic CRUD controller and accompanying views.
8
+ - It generates a Yaml locale file for the views’ translatable strings.
9
+ - It generates type-specific form fields for each attribute of the model.
10
+ - It generates an API controller and an accompanying entry in the application’s API docs.
11
+ - It generates a serializer that’s used by the API and when dispatching webhooks.
12
+ - It adds the appropriate permissions for multitenancy in CanCanCan’s configuration file.
13
+ - It adds the model’s table view to the show view of its parent.
14
+ - It adds the model to the application’s navigation (if applicable).
15
+ - It generates breadcrumbs for use in the application’s layout.
16
+ - It generates the appropriate routes for the CRUD controllers and API endpoints.
17
+
18
+ When adding just one model, Super Scaffolding generates ~30 different files on your behalf.
19
+
20
+ ## Living Templates
21
+
22
+ Bullet Train's Super Scaffolding engine is a unique approach to code generation, based on template files that are functional code instead of obscure DSLs that are difficult to customize and maintain. Super Scaffolding automates the most repetitive and mundane aspects of building out your application's basic structure. Furthermore, it does this without leaning on the magic of libraries that force too high a level of abstraction. Instead, it generates standard Rails code that is both ready for prime time, but is also easy to customize and modify.
23
+
24
+ ## Prerequisites
25
+
26
+ Before getting started with Super Scaffolding, we recommend reading about [the philosophy of domain modeling in Bullet Train](/docs/topics/domain-modeling.md).
27
+
28
+ ## Usage
29
+
30
+ The Super Scaffolding shell script provides its own documentation. If you're curious about specific scaffolders or parameters, you can run the following in your shell:
31
+
32
+ ```
33
+ bin/super-scaffold
34
+ ```
35
+
36
+ ## Examples
37
+
38
+ ### 1. Basic CRUD Scaffolding with `crud`
39
+
40
+ Let's implement the following feature:
41
+
42
+ > An organization has many projects.
43
+
44
+ First, generate the model with the standard Rails generator:
45
+
46
+ ```
47
+ rails g model Project team:references name:string
48
+ ```
49
+
50
+ In the above example, `team` represents the model that a `Project` primarily belongs to.
51
+
52
+ ⚠️ Don't run migrations right away. It would be fine in this case, but sometimes the subsequent Super Scaffolding step actually updates the migration as part of its magic.
53
+
54
+ Next, run the `crud` scaffolder:
55
+
56
+ ```
57
+ bin/super-scaffold crud Project Team name:text_field
58
+ rake db:migrate
59
+ ```
60
+
61
+ In the above example, `text_field` was selected from [the list of available field partials](/docs/field-partials.md). We'll show examples with `trix_editor` and `super_select` later.
62
+
63
+ ### 2. Nested CRUD Scaffolding with `crud`
64
+
65
+ Building on that example, let's implement the following feature:
66
+
67
+ ```
68
+ A project has many goals.
69
+ ```
70
+
71
+ Again, we'll generate the model using the standard Rails generator:
72
+
73
+ ```
74
+ rails g model Goal project:references description:string
75
+ ```
76
+
77
+ You can see in the example above that there is no direct reference to team, but to `project` instead, since that's where a `Goal` belongs.
78
+
79
+ Now, run the `crud` scaffolder:
80
+
81
+ ```
82
+ bin/super-scaffold crud Goal Project,Team description:text_field
83
+ rake db:migrate
84
+ ```
85
+
86
+ You can see in the example above how we've specified `Project,Team`, because we want to specify the entire chain of ownership back to the `Team`. This allows Super Scaffolding to automatically generate the required permissions.
87
+
88
+ ### 3. Adding New Fields with `crud-field`
89
+
90
+ One of Bullet Train's most valuable features is the ability to add new fields to existing scaffolded models. When you add new fields with the `crud-field` scaffolder, you don't have to remember to add that same attribute to table views, show views, translation files, API endpoints, serializers, tests, documentation, etc.
91
+
92
+ Building on the earlier example, consider the following new requirement:
93
+
94
+ > In addition to a name, a project can have a description.
95
+
96
+ First, use the standard Rails migration generator to add the attribute in the database:
97
+
98
+ ```
99
+ rails g migration add_description_to_projects description:text
100
+ ```
101
+
102
+ Then, use the `crud-field` scaffolder to add it throughout the application:
103
+
104
+ ```
105
+ bin/super-scaffold crud-field Project description:trix_editor
106
+ rake db:migrate
107
+ ```
108
+
109
+ As you can see, when we're using `crud-field`, we don't need to supply the chain of ownership back to `Team`.
110
+
111
+ ### 4. Adding Option Fields with Fixed, Translatable Options
112
+
113
+ Continuing with the earlier example, let's address the following new requirement:
114
+
115
+ > Users can specify the current project status.
116
+
117
+ We have multiple [field partials](/docs/field-partials.md) that we could use for this purpose, including `buttons`, `options`, or `super_select`.
118
+
119
+ In this example, let's add a status attribute and present it as buttons:
120
+
121
+ ```
122
+ rails g migration add_status_to_projects status:string
123
+ bin/super-scaffold crud-field Project status:buttons
124
+ ```
125
+
126
+ By default, Super Scaffolding configures the buttons as "One", "Two", and "Three", but in this example you can edit those options in the `fields` section of `config/locales/en/projects.en.yml`. For example, you could specify the following options:
127
+
128
+ ```
129
+ planned: Planned
130
+ started: Started
131
+ completed: Completed
132
+ ```
133
+
134
+ If you want new `Project` models to be set to `planned` by default, you can add that to the migration file that was generated before running it, like so:
135
+
136
+ ```
137
+ add_column :projects, :status, :string, default: "planned"
138
+ ```
139
+
140
+ ### 5. Scaffolding `belongs_to` Associations, Team Member Assignments
141
+
142
+ Continuing with the example, consider the following requirement:
143
+
144
+ > A project has one specific project lead.
145
+
146
+ Although you might think this calls for a reference to `User`, we've learned the hard way that it's typically much better to assign resources on a `Team` to a `Membership` on the team instead. For one, this allows you to assign resources to new team members that haven't accepted their invitation yet (and don't necessarily have a `User` record yet.)
147
+
148
+ We can accomplish this like so:
149
+
150
+ ```
151
+ rails g migration add_lead_to_projects lead:references
152
+ ```
153
+
154
+ Then, to add the field, we specify the following:
155
+
156
+ ```
157
+ bin/super-scaffold crud-field Project lead_id:super_select[class_name=Membership]
158
+ rake db:migrate
159
+ ```
160
+
161
+ There are two important things to point out here:
162
+
163
+ 1. When we use `rails g model` or `rails g migration`, we specify the `references` column name as `lead`, but when we're specifying the _field_ we want to scaffold, we specify it as `lead_id`, because that's the name of the attribute on the form, in strong parameters, etc.
164
+ 2. We have to specify the model name with the `class_name` option so that Super Scaffolding can fully work it's magic. We can't reflect on the association, because at this point the association isn't properly defined yet. With this information, Super Scaffolding can handle that step for you.
165
+
166
+ Finally, Super Scaffolding will prompt you to edit `app/models/project.rb` and implement the required logic in the `valid_leads` method. This is a template method that will be used to both populate the select field on the `Project` form, but also enforce some important security concerns in this multi-tenant system. In this case, you can define it as:
167
+
168
+ ```
169
+ def valid_leads
170
+ team.memberships.current_and_invited
171
+ end
172
+ ```
173
+
174
+ (The `current_and_invited` scope just filters out people that have already been removed from the team.)
175
+
176
+ ### 6. Scaffolding Has-Many-Through Associations with `join-model`
177
+
178
+ Finally, working from the same example, imagine the following requirement:
179
+
180
+ > A project can be labeled with one or more project-specific tags.
181
+
182
+ We can accomplish this with a new model, a new join model, and a `super_select` field.
183
+
184
+ First, let's create the tag model:
185
+
186
+ ```
187
+ rails g model Projects::Tag team:references name:string
188
+ bin/super-scaffold crud Projects::Tag Team name:text_field
189
+ ```
190
+
191
+ Note that project tags are specifically defined at the `Team` level. The same tag can be applied to multiple `Project` models.
192
+
193
+ Now, let's create a join model for the has-many-through association:
194
+
195
+ ```
196
+ rails g model Projects::AppliedTag project:references tag:references
197
+ ```
198
+
199
+ We're not going to scaffold this model with the typical `crud` scaffolder, but some preparation is needed before we can use it with the `crud-field` scaffolder, so we need to do the following:
200
+
201
+ ```
202
+ bin/super-scaffold join-model Projects::AppliedTag project_id[class_name=Project] tag_id[class_name=Projects::Tag]
203
+ ```
204
+
205
+ All we're doing here is specifying the name of the join model, and the two attributes and class names of the models it joins. Note again that we specify the `_id` suffix on both of the attributes.
206
+
207
+ Now that the join model has been prepared, we can use the `crud-field` scaffolder to create the multi-select field:
208
+
209
+ ```
210
+ bin/super-scaffold crud-field Project tag_ids:super_select[class_name=Projects::Tag]
211
+ rake db:migrate
212
+ ```
213
+
214
+ Just note that the suffix of the field is `_ids` plural, and this is an attribute provided by Rails to interact with the `has_many :tags, through: :applied_tags` association.
215
+
216
+ The `crud-field` step will ask you to define the logic for the `valid_tags` method in `app/models/project.rb`. You can define it like so:
217
+
218
+ ```
219
+ def valid_tags
220
+ team.projects_tags
221
+ end
222
+ ```
223
+
224
+ Honestly, it's crazy that we got to the point where we can handle this particular use case automatically. It seems simple, but there is so much going on to make this feature work.
225
+
226
+ ## Additional Notes
227
+
228
+ ### `TangibleThing` and `CreativeConcept`
229
+
230
+ In order to properly facilitate this type of code generation, Bullet Train includes two models in the `Scaffolding` namespace:
231
+
232
+ 1. `Scaffolding::AbsolutelyAbstract::CreativeConcept`
233
+ 2. `Scaffolding::CompletelyConcrete::TangibleThing`
234
+
235
+ Their peculiar naming is what's required to ensure that their corresponding view and controller templates can serve as the basis for any combination of different model naming or [namespacing](https://blog.bullettrain.co/rails-model-namespacing/) that you may need to employ in your own application.
236
+
237
+ ### Hiding Scaffolding Templates
238
+
239
+ You won't want your end users seeing the Super Scaffolding templates in your environment, so you can disable their presentation by setting `HIDE_THINGS` in your environment. For example, you can add the following to `config/application.yml`:
240
+
241
+ ```
242
+ HIDE_THINGS: true
243
+ ```
244
+
245
+ ## Advanced Examples
246
+ - [Super Scaffolding with Delegated Types](/docs/super-scaffolding/delegated-types.md)
data/docs/teams.md ADDED
@@ -0,0 +1,8 @@
1
+ # Teams
2
+
3
+ Please read the article providing [an overview of organizational teams in Bullet Train](https://blog.bullettrain.co/teams-should-be-an-mvp-feature/). This article doesn't just explain how `Team`, `Membership`, `Invitation` and `Role` relate and function in Bullet Train, but it also explains how to properly think about resource ownership even in complex scenarios. Furthermore, it touches on ways in which Bullet Train's organizational structure has been successfully extended in more complicated systems.
4
+
5
+ Rather than simply duplicating the content of that blog article here, we'll focus on keeping that article up-to-date so it can benefit not only incoming Bullet Train developers, but also the broader developer ecosystem.
6
+
7
+ ## Related Topics
8
+ - [Permissions](/docs/permissions.md)
data/docs/testing.md ADDED
@@ -0,0 +1,34 @@
1
+ # Automated Test Suite
2
+ All of Bullet Train’s core functionality is verifiable using the provided test suite. This foundation of headless browser integration tests took a ton of time to write, but they can give you the confidence and peace of mind that you haven't broken any key functionality in your application before a deploy.
3
+
4
+ You can run the test suite with the following command in your shell:
5
+
6
+ ```
7
+ rails test:system
8
+ ```
9
+
10
+ ## Fixing Broken Tests
11
+
12
+ ### 1. Run Chrome in Non-Headless Mode
13
+
14
+ When debugging tests, it's important to be able to see what Capybara is seeing. You can disable the headless browser mode by prefixing `rails test` like so:
15
+
16
+ ```
17
+ MAGIC_TEST=1 rails test
18
+ ```
19
+
20
+ When you run the test suite with `MAGIC_TEST` set in your environment like this, the browser will appear on your screen after the first Capybara test starts. (This may not be the first test that runs.) Be careful not to interact with the window when it appears, as sometimes your interactions can cause the test to fail needlessly.
21
+
22
+ ### 2. Insert `binding.pry`.
23
+
24
+ Open the failing test file and insert `binding.pry` right before the action or assertion that is causing the test to fail. After doing that, when you run the test, it will actually stop and open a debugging console while the browser is still open to the appropriate page where the test is beginning to fail. You can use this console and the open browser to try and figure out why the test is failing. When you're done, hit <kbd>Control</kbd> + <kbd>D</kbd> to exit the debugger and continue letting the test run.
25
+
26
+ ## Super Scaffolding Test Suite
27
+ In addition to the standard application test suite, there is a separate test available that doesn't run by default that you can run in CI to ensure you haven't accidentally broken the full function of Super Scaffolding in your application. Before the test runs, there is a setup script that runs a number of Super Scaffolding commands, so you should never run the test in anything but a clean working copy that can be reset when you're done.
28
+
29
+ You can run the setup script and test like so:
30
+
31
+ ```
32
+ test/bin/setup-super-scaffolding-system-test
33
+ rails test test/system/super_scaffolding_test.rb
34
+ ```