bullet_train 1.0.36 → 1.0.39
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/views/layouts/docs.html.erb +1 -1
- data/config/routes.rb +1 -1
- data/docs/api.md +3 -0
- data/docs/authentication.md +13 -0
- data/docs/billing/stripe.md +90 -0
- data/docs/desktop.md +13 -0
- data/docs/field-partials/buttons.md +42 -0
- data/docs/field-partials/super-select.md +58 -0
- data/docs/field-partials.md +132 -0
- data/docs/font-awesome-pro.md +50 -0
- data/docs/getting-started.md +55 -0
- data/docs/heroku.md +91 -0
- data/docs/i18n.md +3 -0
- data/docs/index.md +52 -0
- data/docs/indirection.md +3 -0
- data/docs/modeling.md +93 -0
- data/docs/namespacing.md +11 -0
- data/docs/oauth.md +27 -0
- data/docs/onboarding.md +41 -0
- data/docs/overriding.md +53 -0
- data/docs/permissions.md +18 -0
- data/docs/seeds.md +48 -0
- data/docs/super-scaffolding/delegated-types.md +328 -0
- data/docs/super-scaffolding.md +246 -0
- data/docs/teams.md +8 -0
- data/docs/testing.md +34 -0
- data/docs/themes.md +101 -0
- data/docs/tunneling.md +29 -0
- data/docs/upgrades.md +69 -0
- data/docs/webhooks/incoming.md +3 -0
- data/docs/webhooks/outgoing.md +3 -0
- data/lib/bullet_train/version.rb +1 -1
- metadata +30 -1
data/docs/indirection.md
ADDED
data/docs/modeling.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# Domain Modeling in Bullet Train
|
2
|
+
|
3
|
+
Domain modeling is one of the most important activities in software development. With [Super Scaffolding](/docs/super-scaffolding.md), it's also one of the highest leverage activities in Bullet Train development.
|
4
|
+
|
5
|
+
## What is a "Domain Model"?
|
6
|
+
|
7
|
+
In software application development, your domain model is the object-oriented representation (or "abstraction") of the problem space you're solving problems for. In Rails, the classes that represent your domain model live in `app/models`.
|
8
|
+
|
9
|
+
## What is "Domain Modeling"?
|
10
|
+
|
11
|
+
"Domain modeling" refers to the process by which you decide which entities to introduce into your application's domain model, how those models relate to each other, and which attributes belong on which models.
|
12
|
+
|
13
|
+
Because in Rails most of your models will typically be backed by tables in a database, the process of domain modeling substantially overlaps with "database design". This is especially true in systems like Rails that implement their domain model with the Active Record pattern, because your table structure and object-oriented models are pretty much mapped one-to-one[<sup>*</sup>](https://en.wikipedia.org/wiki/Object–relational_impedance_mismatch).
|
14
|
+
|
15
|
+
## The Importance of Domain Modeling
|
16
|
+
|
17
|
+
### Usability
|
18
|
+
|
19
|
+
Generally speaking, the better your domain model is mapped to the real-world problem space, the more likely it is to map to your own users conceptual model of the problem space in their own head. Since the structure of your domain model ends up having such a large influence on the default structure and navigation of your application UI, getting the domain model "right" is the first step in creating an application that is easy for users to understand and use.
|
20
|
+
|
21
|
+
### Extendability
|
22
|
+
|
23
|
+
Feature requests come from the real world, and specifically from the parts of reality your application doesn't already solve for. The better your existing domain model captures the existing real-world problem space you've tried to solve problems for, the easier it will be for your application to add new models for features that solve the new problems your users are actually experiencing in real life. Inversely, if you take the wrong shortcuts when representing the problem space, it will be difficult to find the right place to add new features in a way that makes sense to your users.
|
24
|
+
|
25
|
+
## Important Bullet Train Concepts
|
26
|
+
|
27
|
+
### The "Parent Model"
|
28
|
+
|
29
|
+
The idea of a "parent model" is different than [a "parent class" in an object-oriented sense](https://en.wikipedia.org/wiki/Inheritance_\(object-oriented_programming\)). When we say "parent model", we're referring to the model that another model primarily belongs to. For example, a `Task` might primarily belong to a `Project`. Although this type of hierarchy isn't an entirely natural concept in object-oriented programming itself, (where our UML diagrams have many types of relationships flying in different directions,) it's actually a very natural concept in the navigation structure of software, which is why breadcrumbs are such a popular tool for navigation. It's also a concept that is very natural in traditional Rails development, expressed in the definition of nested RESTful routes for resources.
|
30
|
+
|
31
|
+
## Philosophies
|
32
|
+
|
33
|
+
### Take your time and get it right.
|
34
|
+
|
35
|
+
Because Super Scaffolding makes it so easy to bring your domain model to life, you don't have to rush into that implementation phase of writing code. Instead, you can take your sweet time thinking through your proposed domain model and mentally running it through the different scenarios and use cases it needs to solve for. If you get aspects of your domain model wrong, it can be really hard to fix later.
|
36
|
+
|
37
|
+
### More minds are better.
|
38
|
+
|
39
|
+
Subject your proposed domain model to review from other developers or potential users and invite their thoughts.
|
40
|
+
|
41
|
+
### Tear it down to get it right.
|
42
|
+
|
43
|
+
In traditional Rails development, it can be so much work to bring your domain model to life in views and controllers that if you afterward realize you missed something or got something wrong structurally, it can be tempting not to fix it or refactor it. Because Super Scaffolding eliminates so much of the busy work of bringing your domain model to life in the initial implementation phase, you don't have to worry so much about tearing down your scaffolds, reworking the domain model, and running through the scaffolding process again.
|
44
|
+
|
45
|
+
### Focus on the structure and namespacing. Don't worry about every attribute.
|
46
|
+
|
47
|
+
One of the unique features of Super Scaffolding is that it allows you to scaffold additional attributes with `bin/super-scaffold crud-field` after the initial scaffolding of a model with `bin/super-scaffold crud`. That means that you don't have to worry about figuring out every single attribute that might exist on a model before running Super Scaffolding. Instead, the really important piece is:
|
48
|
+
|
49
|
+
1. Naming the model.
|
50
|
+
2. Determining which parent model it primarily belongs to.
|
51
|
+
3. Determining whether the model should be [in a topic namespace](https://blog.bullettrain.co/rails-model-namespacing/).
|
52
|
+
|
53
|
+
### Start with CRUD, then polish.
|
54
|
+
|
55
|
+
Even if you know there's an attribute or model that you're going to want to polish up the user experience for, still start with the scaffolding. This ensures that any model or attribute is also represented in your REST API and you have feature parity between your web-based experience and what developers can integrate with and automate.
|
56
|
+
|
57
|
+
### Pluralize preemptively.
|
58
|
+
|
59
|
+
> Before you write any code — ask if you could ever possibly want multiple kinds of the thing you are coding. If yes, just do it. Now, not later.
|
60
|
+
|
61
|
+
— [Shawn Wang](https://twitter.com/swyx)
|
62
|
+
|
63
|
+
> I've done this refactoring a million times. I'll be like, I thought there would only ever be one subscription team, user plan, name, address, and it always ends up being like, "Oh, actually there's more." I almost never go the other way. What if you just paid the upfront cost of thinking "This is just always a collection"?
|
64
|
+
|
65
|
+
— [Ben Orenstein](https://twitter.com/r00k)
|
66
|
+
|
67
|
+
[I believe this is one of the most important articles in software development in the last ten years.](https://www.swyx.io/preemptive-pluralization/) However, with great domain modeling power comes great UX responsibility, which we'll touch on later.
|
68
|
+
|
69
|
+
## A Systematic Approach
|
70
|
+
|
71
|
+
### 1. Write `rails g` and `bin/super-scaffold` commands in a scratch file.
|
72
|
+
|
73
|
+
See the [Super Scaffolding documentation](/docs/super-scaffolding.md) for more specific guidance. Leave plenty of comments in your scratch file describing anything that isn't obvious and providing examples of values that might populate attributes.
|
74
|
+
|
75
|
+
### 2. Review with other developers.
|
76
|
+
|
77
|
+
Push up a pull request with your scratch file and invite review from other developers. They might bring up scenarios and use cases you didn't think of, better ways of representing something, or generate questions that don't have an obvious answer and require feedback from a subject matter expert.
|
78
|
+
|
79
|
+
### 3. Review with non-developer stakeholders.
|
80
|
+
|
81
|
+
Engage with product owners or potential users and talk through each part of the domain model in your scratch file without showing it to them or getting too technical. Just talk through which entities you chose to represent, which attributes and options are available. Talk through the features those models and attributes allow you to provide and which use cases you think you've covered.
|
82
|
+
|
83
|
+
In these discussions, you're looking for inspiration of additional nouns, verbs, and use cases you may not have considered in your initial modeling. Even if you choose not to incorporate certain ideas or feature requests immediately, you're at least taking into consideration whether they would fit nicely into the mental model you have represented in your domain model.
|
84
|
+
|
85
|
+
### 4. Run the commands.
|
86
|
+
|
87
|
+
Be sure to commit your results at this point. This helps isolate the computer generated code from the work you'll do in the next step (which you may want to have someone review.)
|
88
|
+
|
89
|
+
### 5. Polish the UI.
|
90
|
+
|
91
|
+
By default, Bullet Train produces a very CRUD-y user experience. The intention has always been that the productivity gains provided by Super Scaffolding should be reinvested into the steps that come before and after. Spend more time domain modeling, and then spend more time polishing up the resulting UX.
|
92
|
+
|
93
|
+
This is especially true in situations where you've chosen to pluralize preemptively. Ask yourself: Does every user need this option in the plural? If not, should we try to simplify the conceptual model by representing it as a "has one" until they opt-in to complexity? Doing this allows you to have the best of both worlds: Simplicity for those who can fit within it, and advanced functionality that users can opt into.
|
data/docs/namespacing.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Namespacing in Bullet Train
|
2
|
+
|
3
|
+
## The `Account` Namespace for Controllers and Views
|
4
|
+
Bullet Train comes preconfigured with an `Account` and controller and view namespace. This is the place where Super Scaffolding will, by default, put new resource views and controllers. The intention here is to ensure that in systems that have both authenticated resource workflows and public-facing resources, those two different facets of the application are served by separate resource views and controllers. (By default, public-facing resources would be in the `Public` namespace.)
|
5
|
+
|
6
|
+
## Alternative Authenticated Namespaces
|
7
|
+
In Bullet Train applications with [multiple team types](/docs/teams.md), you may find it helpful to introduce additional controller and view namespaces to represent and organize user interfaces and experiences for certain team types that vary substantially from the `Account` namespace default. In Super Scaffolding, you can specify a namespace other than `Account` with the `--namespace` option, for example:
|
8
|
+
|
9
|
+
```
|
10
|
+
$ bin/super-scaffold crud Event Team name:text_field --namespace=customers
|
11
|
+
```
|
data/docs/oauth.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# OAuth Providers
|
2
|
+
Bullet Train includes [Omniauth](https://github.com/omniauth/omniauth) by default which enables [Super Scaffolding](/docs/super-scaffolding) to easily add [any of the third-party OAuth providers in its community-maintained list of strategies](https://github.com/omniauth/omniauth/wiki/List-of-Strategies) for user-level authentication and team-level integrations via API and incoming webhooks.
|
3
|
+
|
4
|
+
For specific instructions on adding new OAuth providers, run the following on your shell:
|
5
|
+
|
6
|
+
> TODO This scaffolder still needs to be updated to support the new way we distribute the Stripe Connect example via Ruby gem.
|
7
|
+
|
8
|
+
```
|
9
|
+
bin/super-scaffold oauth-provider
|
10
|
+
```
|
11
|
+
|
12
|
+
## Stripe Connect Example
|
13
|
+
Similar to the "Tangible Things" template for [Super Scaffolding CRUD workflows](/docs/super-scaffolding.md), Bullet Train includes a Stripe Connect integration by default and this example also serves as a template for Super Scaffolding to implement other providers you might want to add.
|
14
|
+
|
15
|
+
## Dealing with Last Mile Issues
|
16
|
+
|
17
|
+
You should be able to add many third-party OAuth providers with Super Scaffolding without any manual effort. However, there are sometimes quirks from provider to provider, so if you need to dig in to get things working on a specific provider, here are the files you'll probably be looking for:
|
18
|
+
|
19
|
+
### Core Functionality
|
20
|
+
- `config.omniauth` in `config/initializers/devise.rb`
|
21
|
+
- Third-party OAuth providers are registered at the top of this file.
|
22
|
+
- `app/controllers/account/oauth/omniauth_callbacks_controller.rb`
|
23
|
+
- This controller contains all the logic that executes when a user returns back to your application after working their way through the third-party OAuth provider's workflow.
|
24
|
+
- `omniauth_callbacks` in `config/routes.rb`
|
25
|
+
- This file just registers the above controller with Devise.
|
26
|
+
- `app/views/devise/shared/_oauth.html.erb`
|
27
|
+
- This partial includes all the buttons for presentation on the sign in and sign up pages.
|
data/docs/onboarding.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# Onboarding
|
2
|
+
Bullet Train provides an easy to understand and modifiable structure for defining required onboarding steps and forms. This system makes it easy for users to complete required steps before seeing your application's full account interface. The code for this feature is well documented in the code with comments, so rather than duplicating that document here, we'll simply direct you to the relevant files.
|
3
|
+
|
4
|
+
## Included Onboarding Steps
|
5
|
+
|
6
|
+
### Collect User Details
|
7
|
+
|
8
|
+
The included "user details" onboarding step is intended to collect any fields that are required for a user account to be "complete" while not requiring those fields to be collected on the initial sign up form. This is a deliberate UX decision to try and increase conversions on the initial form.
|
9
|
+
|
10
|
+
### Collect User Email
|
11
|
+
|
12
|
+
The "user email" onboarding step is specifically used in situations where a user signs up with an OAuth provider that either doesn't supply their email address to the application or doesn't have a verified email address for the user. In this situation, we want to have an email address on their account, so we prompt them for it.
|
13
|
+
|
14
|
+
## Relevant Files
|
15
|
+
|
16
|
+
> TODO This section needs to be updated now that a bunch of these files are coming in from gems.
|
17
|
+
|
18
|
+
### Controllers
|
19
|
+
- `ensure_onboarding_is_complete` in `app/controllers/account/application_controller.rb`
|
20
|
+
- `app/controllers/account/onboarding/user_details_controller.rb`
|
21
|
+
- `app/controllers/account/onboarding/user_email_controller.rb`
|
22
|
+
|
23
|
+
### Views
|
24
|
+
- `app/views/account/onboarding/user_details/edit.html.erb`
|
25
|
+
- `app/views/account/onboarding/user_email/edit.html.erb`
|
26
|
+
|
27
|
+
### Models
|
28
|
+
- `user#details_provided?` in `app/models/user.rb`
|
29
|
+
|
30
|
+
### Routes
|
31
|
+
- `namespace :onboarding` in `config/routes.rb`
|
32
|
+
|
33
|
+
## Adding Additional Steps
|
34
|
+
Although you can implement onboarding steps from scratch, we always just copy and paste one of the existing steps as a starting point, like so:
|
35
|
+
|
36
|
+
1. Copy, rename, and modify of the existing onboarding controllers.
|
37
|
+
2. Copy, rename, and modify the corresponding `edit.html.erb` view.
|
38
|
+
3. Copy and rename the route entry in `config/routes.rb`.
|
39
|
+
4. Add the appropriate gating logic in `ensure_onboarding_is_complete` in `app/controllers/account/application_controller.rb`
|
40
|
+
|
41
|
+
Onboarding steps aren't limited to targeting `User` models. It's possible to add onboarding steps to help flesh out team `Membership` records or `Team` records as well. You can use this pattern for setting up any sort of required data for either the user or the team.
|
data/docs/overriding.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Dealing with Indirection
|
2
|
+
|
3
|
+
## The Problem with Indirection
|
4
|
+
|
5
|
+
In software development, indirection is everywhere and takes many forms.
|
6
|
+
|
7
|
+
For example, in vanilla Rails development, you introduce a type of indirection when you extract a button label out of a view file and use the `t` helper to render the string from a translation YAML file. In the future, when another developer goes to update the button label, they will first open the view, they'll see `t(".submit")` and then have to reason a little bit about which translation file they need to open up in order to update that label.
|
8
|
+
|
9
|
+
Our goal in Bullet Train is to improve developer experience, not reduce it, so it was important that along with any instances of indirection we were introducing, we also included new tooling to ensure it was never a burden to developers. Thankfully, in practice we found that some of this new tooling improves even layers of indirection that have always been with us in Rails development.
|
10
|
+
|
11
|
+
## Solving Indirection in Views
|
12
|
+
|
13
|
+
### Resolving Partial Paths with `bin/resolve`
|
14
|
+
|
15
|
+
Even in vanilla Rails development, when you're looking at a view file, the path you see passed to a `render` call isn't the actual file name of the partial that will be rendered. This is even more true in Bullet Train where certain partial paths are [magically served from theme gems](/docs/themes.md).
|
16
|
+
|
17
|
+
`bin/resolve` makes it easy to figure out where where a partial is being served from:
|
18
|
+
|
19
|
+
```
|
20
|
+
$ bin/resolve shared/box
|
21
|
+
```
|
22
|
+
|
23
|
+
### Exposing Rendered Views with Xray
|
24
|
+
|
25
|
+
> TODO Is this still true in Rails 7? Does it not do something like this by default now?
|
26
|
+
|
27
|
+
If you're looking at a rendered view in the browser, it can be hard to know which file to open in order to make a change. To help, Bullet Train includes [Xray](https://github.com/brentd/xray-rails) by default, so you can right click on any element you see, select "Inspect Element", and you'll see comments in the HTML source telling you which file is powering a particular portion of the view, like this:
|
28
|
+
|
29
|
+
```
|
30
|
+
<!--XRAY START 90 /Users/andrewculver/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/bullet_train-themes-light-1.0.10/app/views/themes/light/workflow/_box.html.erb-->
|
31
|
+
```
|
32
|
+
|
33
|
+
Note that in the example above, the view in question isn't actually coming from the application repository. Instead, it's being included from the `bullet_train-themes-light` package. For instructions on how to customize it, see [Overriding the Framework](/docs/override).
|
34
|
+
|
35
|
+
### Drilling Down on Translation Keys
|
36
|
+
|
37
|
+
Even in vanilla Rails applications, extracting strings from view files into I18N translation YAML files introduces a layer of indirection. Bullet Train tries to improve the resulting DX with a couple tools that make it easier to figure out where a translation you see in your browser is coming from.
|
38
|
+
|
39
|
+
#### Show Translation Keys in the Browser with `?show_locales=true`
|
40
|
+
|
41
|
+
You can see the full translation key of any string on the page by adding `?show_locales=true` to the URL.
|
42
|
+
|
43
|
+
#### Log Translation Keys to the Console with `?log_locales=true`
|
44
|
+
|
45
|
+
You can also log all the translation key for anything being rendered to the console by adding `?log_locales=true` to the request URL. This can make it easier to copy and paste translation keys for strings that are rendered in non-selectable UI elements.
|
46
|
+
|
47
|
+
#### Resolving Translation Keys with `bin/resolve`
|
48
|
+
|
49
|
+
Once you have the full I18N translation key, you can use `bin/resolve` to figure out which package and file it's coming from. At that point, if you need to customize it, you can also use the `--eject` option to copy the the framework for customization in your local application:
|
50
|
+
|
51
|
+
```
|
52
|
+
$ bin/resolve en.account.onboarding.user_details.edit.header --eject --open
|
53
|
+
```
|
data/docs/permissions.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Roles, Permissions, Abilities, and Authorization
|
2
|
+
|
3
|
+
## CanCanCan
|
4
|
+
Bullet Train leans heavily on [CanCanCan](https://github.com/CanCanCommunity/cancancan) for implementing authorization and permissions. (We’re also proud sponsors of its ongoing maintenance.) The original CanCan library by Ryan Bates was, in our opinion, a masterpiece and a software engineering marvel that has stood the test of time. It's truly a diamond among Ruby Gems. If you're not already familiar with CanCanCan, you should [read its documentation](https://github.com/CanCanCommunity/cancancan) to get familiar with its features and DSL.
|
5
|
+
|
6
|
+
## Bullet Train Roles
|
7
|
+
Over many years of successfully implementing applications with CanCanCan, it became apparent to us that a supplemental level of abstraction could help streamline and simplify the definition of many common permissions, especially in large applications. We've since extracted this functionality into [a standalone Ruby Gem](https://github.com/bullet-train-co/bullet_train-roles) and moved the documentation that used to be here into [the README for that project](https://github.com/bullet-train-co/bullet_train-roles/blob/main/README.md). Should you encounter situations where this abstraction doesn't meet your specific needs, you can always implement the permissions you need using standard CanCanCan directives in `app/models/ability.rb`.
|
8
|
+
|
9
|
+
## Additional Notes
|
10
|
+
|
11
|
+
### Caching
|
12
|
+
Because abilities are being evaluated on basically every request, it made sense to introduce a thin layer of caching to help speed things up. When evaluating permissions, we store a cache of the result in the `ability_cache` attribute of the `User`. By default, making changes to a model that includes the `Roles::Support` concern will invalidate that user's cache.
|
13
|
+
|
14
|
+
### Naming and Labeling
|
15
|
+
What we call a `Role` in the domain model is referred to as “Special Privileges” in the user-facing application. You can rename this to whatever you like in `config/locales/en/roles.en.yml`.
|
16
|
+
|
17
|
+
## Note About Pundit
|
18
|
+
There’s nothing stopping you from utilizing Pundit in a Bullet Train project for specific hard-to-implement cases in your permissions model, but you wouldn’t want to try and replace CanCanCan with it. We do too much automatically with CanCanCan for that to be recommended. That said, in those situations where there is a permission that needs to be implemented that isn’t easily implemented with CanCanCan, consider just writing vanilla Ruby code for that purpose.
|
data/docs/seeds.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Database Seeds
|
2
|
+
|
3
|
+
Bullet Train introduces a new, slightly different expectation for Rails seed data: **It should be possible to run `rake db:seed` multiple times without creating duplicate data.**
|
4
|
+
|
5
|
+
## The Rails Default
|
6
|
+
|
7
|
+
This is different than the Rails default, [as evidenced by the Rails example](https://guides.rubyonrails.org/v6.1.1/active_record_migrations.html#migrations-and-seed-data) which uses `Product.create`:
|
8
|
+
|
9
|
+
```
|
10
|
+
5.times do |i|
|
11
|
+
Product.create(name: "Product ##{i}", description: "A product.")
|
12
|
+
end
|
13
|
+
```
|
14
|
+
|
15
|
+
## Bullet Train Example
|
16
|
+
|
17
|
+
In Bullet Train applications, you would implement that same `db/seeds.rb` logic like so:
|
18
|
+
|
19
|
+
```
|
20
|
+
5.times do |i|
|
21
|
+
Product.find_or_create_by(name: "Product ##{i}") do |product|
|
22
|
+
# this only happens if on a `create`.
|
23
|
+
production.description = "A product."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
## Why?
|
29
|
+
We do this so Bullet Train applications can re-use the logic in `db/seeds.rb` for three purposes:
|
30
|
+
|
31
|
+
1. Set up new local development environments.
|
32
|
+
2. Ensure the test suite has the same configuration for features whose configuration is backed by Active Record (e.g. [subscriptions](/docs/subscriptions.md) and [outgoing webhooks](/docs/webhooks/outgoing.md)).
|
33
|
+
3. Ensure any updates to the baseline configuration that have been tested both locally and in CI are the exact same updates being executed in production upon deploy.
|
34
|
+
|
35
|
+
This makes `db/seeds.rb` a single source of truth for this sort of baseline data, instead of having this concern spread and sometimes duplicated across `db/seeds.rb`, `db/migrations/*`, and `test/fixtures`.
|
36
|
+
|
37
|
+
## Seeds for Different Environments
|
38
|
+
In some cases, you may have core seed data like roles that needs to exist in every environment, but you also have development data to populate in your non-production environments. Bullet Train makes this easy by supporting per-environment seed files in the `db/seeds` folder like `db/seeds/test.rb` and `db/seeds/development.rb`.
|
39
|
+
|
40
|
+
Then in `db/seeds.rb`, you can load all of the shared core seed data at the beginning of `db/seeds.rb` and then load the environment-specific seeds only when you've specified one of those environments.
|
41
|
+
|
42
|
+
```
|
43
|
+
load "#{Rails.root}/db/seeds/development.rb" if Rails.env.development?
|
44
|
+
load "#{Rails.root}/db/seeds/test.rb" if Rails.env.test?
|
45
|
+
```
|
46
|
+
|
47
|
+
## Feedback
|
48
|
+
We're always very hesitant to stray from Rails defaults, so it must be said that our commitment to this approach isn't set in stone. It's worked very well for us in a number of applications, so we've standardized on it, but the approach is certainly open to discussion.
|
@@ -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's Delegated Types. From the application developer level, very little of it feels "new". 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>— 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.
|