bullet_train 1.1.6 → 1.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/concerns/account/memberships/controller_base.rb +9 -4
- data/app/controllers/concerns/account/teams/controller_base.rb +4 -1
- data/app/controllers/concerns/documentation_support.rb +2 -2
- data/app/helpers/account/teams_helper.rb +10 -0
- data/app/models/billing/mock_limiter.rb +12 -0
- data/app/models/concerns/memberships/base.rb +1 -1
- data/app/models/concerns/records/base.rb +4 -0
- data/app/models/concerns/teams/base.rb +10 -0
- data/app/views/account/invitations/_breadcrumbs.html.erb +1 -1
- data/app/views/account/invitations/_form.html.erb +40 -38
- data/app/views/account/memberships/_form.html.erb +0 -1
- data/app/views/account/memberships/_index.html.erb +56 -53
- data/app/views/public/home/docs.html.erb +2 -2
- data/docs/billing/usage.md +148 -0
- data/lib/bullet_train/version.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b2f6e8a8bd9f4303bd21b03d411b0623a8a334858e767a2495648bc826c5e9f
|
4
|
+
data.tar.gz: abaf4b1241d0d8e501baf65b9fca1aee0d12030fc54c47e83e7d017f20dd0b40
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e65877ac21f2a4109b8a0461af3e68f9773d0c31aa85bb1a2b8d9b95a444965057d36f539bdb2921bd2bba9da77bd028614e468b9188135fd8aa7357c5908a9c
|
7
|
+
data.tar.gz: a766131072e8b221f08d6c21eea4106688cb6b0ea5297730b2ecbb9c0c28f8fcfb5af3424739ffd734f31e1b0c9682b911ba9d0f26dbcf442b0b51857590acbb
|
@@ -84,11 +84,16 @@ module Account::Memberships::ControllerBase
|
|
84
84
|
end
|
85
85
|
|
86
86
|
def reinvite
|
87
|
-
|
88
|
-
|
89
|
-
|
87
|
+
if helpers.current_limits.can?(:create, Membership)
|
88
|
+
@invitation = Invitation.new(membership: @membership, team: @team, email: @membership.user_email, from_membership: current_membership)
|
89
|
+
if @invitation.save
|
90
|
+
redirect_to [:account, @team, :memberships], notice: I18n.t("account.memberships.notifications.reinvited")
|
91
|
+
else
|
92
|
+
redirect_to [:account, @team, :memberships], notice: "There was an error creating the invitation (#{@invitation.errors.full_messages.to_sentence})"
|
93
|
+
end
|
90
94
|
else
|
91
|
-
|
95
|
+
flash[:error] = :create_limit
|
96
|
+
redirect_to [:account, @team, :memberships]
|
92
97
|
end
|
93
98
|
end
|
94
99
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
module Account::Teams::ControllerBase
|
2
2
|
extend ActiveSupport::Concern
|
3
|
+
extend Controllers::Base
|
3
4
|
|
4
5
|
included do
|
5
6
|
load_and_authorize_resource :team, class: "Team", prepend: true,
|
@@ -21,7 +22,9 @@ module Account::Teams::ControllerBase
|
|
21
22
|
|
22
23
|
private
|
23
24
|
|
24
|
-
|
25
|
+
if defined?(Api::V1::ApplicationController)
|
26
|
+
include strong_parameters_from_api
|
27
|
+
end
|
25
28
|
end
|
26
29
|
|
27
30
|
# GET /teams
|
@@ -3,8 +3,8 @@ module DocumentationSupport
|
|
3
3
|
|
4
4
|
def docs
|
5
5
|
target = params[:page].presence || "index"
|
6
|
-
|
7
|
-
@
|
6
|
+
all_paths = ([Rails.root.to_s] + `bundle show --paths`.lines.map(&:chomp))
|
7
|
+
@path = all_paths.map { |path| path + "/docs/#{target}.md" }.detect { |path| File.exists?(path) }
|
8
8
|
render :docs, layout: "docs"
|
9
9
|
end
|
10
10
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Account::TeamsHelper
|
2
2
|
def current_team
|
3
|
+
# TODO We do not want this to be based on the `current_team_id`.
|
4
|
+
# TODO We want this to be based on the current resource being loaded.
|
3
5
|
current_user&.current_team
|
4
6
|
end
|
5
7
|
|
@@ -77,4 +79,12 @@ module Account::TeamsHelper
|
|
77
79
|
def can_invite?
|
78
80
|
can?(:create, Invitation.new(team: current_team))
|
79
81
|
end
|
82
|
+
|
83
|
+
def current_limits
|
84
|
+
@limiter ||= if billing_enabled? && defined?(Billing::Limiter)
|
85
|
+
Billing::Limiter.new(current_team)
|
86
|
+
else
|
87
|
+
Billing::MockLimiter.new(current_team)
|
88
|
+
end
|
89
|
+
end
|
80
90
|
end
|
@@ -4,6 +4,10 @@ module Records::Base
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
included do
|
7
|
+
if billing_enabled? && defined?(Billing::UsageSupport)
|
8
|
+
include Billing::UsageSupport
|
9
|
+
end
|
10
|
+
|
7
11
|
if defined?(Webhooks::Outgoing::IssuingModel)
|
8
12
|
include Webhooks::Outgoing::IssuingModel
|
9
13
|
end
|
@@ -27,6 +27,10 @@ module Teams::Base
|
|
27
27
|
if defined?(Billing::Stripe::Subscription)
|
28
28
|
has_many :billing_stripe_subscriptions, class_name: "Billing::Stripe::Subscription", dependent: :destroy, foreign_key: :team_id
|
29
29
|
end
|
30
|
+
|
31
|
+
if defined?(Billing::Usage::TeamSupport)
|
32
|
+
include Billing::Usage::TeamSupport
|
33
|
+
end
|
30
34
|
end
|
31
35
|
|
32
36
|
# validations
|
@@ -62,6 +66,12 @@ module Teams::Base
|
|
62
66
|
|
63
67
|
# TODO Probably we can provide a way for gem packages to define these kinds of extensions.
|
64
68
|
if billing_enabled?
|
69
|
+
def current_billing_subscription
|
70
|
+
# If by some bug we have two subscriptions, we want to use the one that existed first.
|
71
|
+
# The reasoning here is that it's more likely to be on some legacy plan that benefits the customer.
|
72
|
+
billing_subscriptions.active.order(:created_at).first
|
73
|
+
end
|
74
|
+
|
65
75
|
def needs_billing_subscription?
|
66
76
|
return false if freemium_enabled?
|
67
77
|
billing_subscriptions.active.empty?
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<% invitation ||= @invitation %>
|
2
2
|
<% team ||= @team || invitation&.team %>
|
3
3
|
<%= render 'account/teams/breadcrumbs', team: team %>
|
4
|
-
<%= render 'account/shared/breadcrumb', label: t('.label'), url: [:account, team, :memberships] %>
|
4
|
+
<%= render 'account/shared/breadcrumb', label: t('memberships.label'), url: [:account, team, :memberships] %>
|
5
5
|
<%= render 'account/shared/breadcrumb', label: t('.label'), url: [:account, team, :invitations] %>
|
6
6
|
<% if invitation&.persisted? %>
|
7
7
|
<%= render 'account/shared/breadcrumb', label: invitation.label_string, url: [:account, invitation] %>
|
@@ -1,49 +1,51 @@
|
|
1
1
|
<%= form_with(model: [:account, (@team unless invitation.persisted?), invitation], class: 'form', local: true) do |form| %>
|
2
|
-
<%= render
|
2
|
+
<%= render "shared/limits/form", form: form, model: invitation.membership, cancel_path: @cancel_path || [:account, invitation] do %>
|
3
|
+
<%= render 'account/shared/forms/errors', form: form %>
|
3
4
|
|
4
|
-
|
5
|
+
<%= render 'shared/fields/email_field', form: form, method: :email, options: {autofocus: true} %>
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
<%= form.fields_for :membership do |membership_form| %>
|
8
|
+
<div class="grid grid-cols-1 gap-y gap-x sm:grid-cols-6">
|
9
|
+
<div class="sm:col-span-3">
|
10
|
+
<%= render 'shared/fields/text_field', form: membership_form, method: :user_first_name %>
|
11
|
+
</div>
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
<div class="sm:col-span-3">
|
14
|
+
<%= render 'shared/fields/text_field', form: membership_form, method: :user_last_name %>
|
15
|
+
</div>
|
14
16
|
</div>
|
15
|
-
|
16
|
-
<% end %>
|
17
|
+
<% end %>
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
19
|
+
<% if can? :manage, @team %>
|
20
|
+
<%= form.fields_for :membership do |fields| %>
|
21
|
+
<%= fields.hidden_field :team_id, value: @team.id %>
|
22
|
+
<div class="space-y-3">
|
23
|
+
<% Membership.assignable_roles.each do |role| %>
|
24
|
+
<% if current_membership.can_manage_role?(role) %>
|
25
|
+
<div class="flex items-top">
|
26
|
+
<%= fields.check_box :role_ids, {multiple: true, class: "h-4 w-4 text-blue focus:ring-blue-dark border-gray-300 rounded mt-0.5"}, role.id, nil %>
|
27
|
+
<label for="invitation_membership_attributes_role_ids_<%= role.id %>" class="ml-2 block select-none">
|
28
|
+
<span><%= t('invitations.form.invite_as', role_key: t("memberships.fields.role_ids.options.#{role.key}.label")) %></span>
|
29
|
+
<div class="mt-0.5 text-gray-400 font-light leading-normal">
|
30
|
+
<%= t("memberships.fields.role_ids.options.#{role.key}.description") %>
|
31
|
+
</div>
|
32
|
+
</label>
|
33
|
+
</div>
|
34
|
+
<% end %>
|
33
35
|
<% end %>
|
34
|
-
|
35
|
-
|
36
|
+
</div>
|
37
|
+
<% end %>
|
36
38
|
<% end %>
|
37
|
-
<% end %>
|
38
39
|
|
39
|
-
|
40
|
+
<%# 🚅 super scaffolding will insert new fields above this line. %>
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
42
|
+
<div class="buttons">
|
43
|
+
<%= form.submit (form.object.persisted? ? t('.buttons.update') : t('.buttons.create')), class: "button" %>
|
44
|
+
<% if form.object.persisted? %>
|
45
|
+
<%= link_to t('global.buttons.cancel'), account_invitation_path(invitation), class: "button-secondary" %>
|
46
|
+
<% else %>
|
47
|
+
<%= link_to t('global.buttons.cancel'), @cancel_path || account_team_invitations_path(@team), class: "button-secondary" %>
|
48
|
+
<% end %>
|
49
|
+
</div>
|
50
|
+
<% end %>
|
49
51
|
<% end %>
|
@@ -2,65 +2,68 @@
|
|
2
2
|
<% hide_actions ||= false %>
|
3
3
|
<% hide_back ||= false %>
|
4
4
|
|
5
|
-
<%=
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
<%= updates_for context, :memberships do %>
|
6
|
+
<%= render 'account/shared/box' do |p| %>
|
7
|
+
<% p.content_for :title, t(".contexts.#{context.class.name.underscore}.header") %>
|
8
|
+
<% p.content_for :description do %>
|
9
|
+
<%= raw t(".contexts.#{context.class.name.underscore}.#{memberships.any? ? 'description' : 'description_empty'}") %>
|
10
|
+
<%= render "shared/limits/index", model: memberships.model %>
|
11
|
+
<% end %>
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
13
|
+
<% p.content_for :table do %>
|
14
|
+
<% if memberships.any? %>
|
15
|
+
<table class="table">
|
16
|
+
<thead>
|
17
|
+
<tr>
|
18
|
+
<th><%= t('memberships.singular') %></th>
|
19
|
+
<th><%= t('memberships.fields.role_ids.heading') %></th>
|
20
|
+
<%# 🚅 super scaffolding will insert new field headers above this line. %>
|
21
|
+
<th></th>
|
22
|
+
</tr>
|
23
|
+
</thead>
|
24
|
+
<tbody data-model="Membership" data-scope="current">
|
25
|
+
<% memberships.each do |membership| %>
|
26
|
+
<tr data-id="<%= membership.id %>">
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
29
|
+
<%= link_to [:account, membership], class: 'block flex items-center group hover:no-underline no-underline' do %>
|
30
|
+
<div class="flex-shrink-0 h-10 w-10">
|
31
|
+
<%= image_tag membership_profile_photo_url(membership), title: membership.label_string, class: 'h-10 w-10 rounded-full' %>
|
32
|
+
</div>
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
34
|
+
<div class="ml-3">
|
35
|
+
<span class="group-hover:underline"><%= membership.label_string %></span>
|
36
|
+
<% if membership.unclaimed? %>
|
37
|
+
<span class="ml-1.5 px-2 inline-flex text-xs text-green-dark bg-green-light border border-green-dark rounded-md">
|
38
|
+
Invited
|
39
|
+
</span>
|
40
|
+
<% end %>
|
41
|
+
</div>
|
42
|
+
<% end %>
|
43
|
+
</td>
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
45
|
+
<td>
|
46
|
+
<% if membership.roles_without_defaults.any? %>
|
47
|
+
<%= membership.roles_without_defaults.map { |role| t("memberships.fields.role_ids.options.#{role.key}.label") }.to_sentence %>
|
48
|
+
<% else %>
|
49
|
+
<%= t("memberships.fields.role_ids.options.default.label") %>
|
50
|
+
<% end %>
|
51
|
+
</td>
|
52
|
+
<td class="text-right">
|
53
|
+
<%= link_to t('.buttons.show'), [:account, membership], class: 'button-secondary button-smaller' %>
|
54
|
+
</td>
|
55
|
+
</tr>
|
56
|
+
<% end %>
|
57
|
+
</tbody>
|
58
|
+
</table>
|
59
|
+
<% end %>
|
57
60
|
<% end %>
|
58
|
-
<% end %>
|
59
61
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
62
|
+
<% unless hide_actions %>
|
63
|
+
<% p.content_for :actions do %>
|
64
|
+
<%= link_to t('invitations.buttons.new'), new_account_team_invitation_path(@team, cancel_path: account_team_memberships_path(@team)), class: "#{first_button_primary}" %>
|
65
|
+
<%= link_to t('global.buttons.back'), [:account, context], class: "#{first_button_primary} back" unless hide_back %>
|
66
|
+
<% end %>
|
64
67
|
<% end %>
|
65
68
|
<% end %>
|
66
69
|
<% end %>
|
@@ -1,6 +1,6 @@
|
|
1
|
-
<% @body = markdown(File.read(
|
1
|
+
<% @body = markdown(File.read(@path).gsub('.md)', ')')) %>
|
2
2
|
|
3
|
-
<% if @
|
3
|
+
<% if @path.include?("docs/index.md") %>
|
4
4
|
<% header, groups = @body.split("<h2>", 2) %>
|
5
5
|
<%= header.html_safe %>
|
6
6
|
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# Bullet Train Usage Limits
|
2
|
+
|
3
|
+
Bullet Train provides a holistic method for defining model-based usage limits in your Rails application.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
### 1. Purchase Bullet Train Pro
|
8
|
+
|
9
|
+
First, [purchase Bullet Train Pro](https://buy.stripe.com/aEU7vc4dBfHtfO89AV). Once you've completed this process, you'll be issued a private token for the Bullet Train Pro package server. The process is currently completed manually, so you may have to way a little to receive your keys.
|
10
|
+
|
11
|
+
### 2. Install the Package
|
12
|
+
|
13
|
+
### 2.1. Add the Private Ruby Gems
|
14
|
+
|
15
|
+
You'll need to specify both Ruby gems in your `Gemfile`, since we have to specify a private source for both:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
source "https://YOUR_TOKEN_HERE@gem.fury.io/bullettrain" do
|
19
|
+
gem "bullet_train-billing"
|
20
|
+
gem "bullet_train-billing-stripe" # Or whichever billing provider you're using.
|
21
|
+
gem "bullet_train-billing-usage"
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
### 2.2. Bundle Install
|
26
|
+
|
27
|
+
```
|
28
|
+
bundle install
|
29
|
+
```
|
30
|
+
|
31
|
+
### 2.3. Copy Database Migrations
|
32
|
+
|
33
|
+
Use the following two commands on your shell to copy the required migrations into your local project:
|
34
|
+
|
35
|
+
```
|
36
|
+
cp `bundle show --paths | grep bullet_train-billing | sort | head -n 1`/db/migrate/* db/migrate
|
37
|
+
cp `bundle show --paths | grep bullet_train-billing-stripe | sort | head -n 1`/db/migrate/* db/migrate
|
38
|
+
```
|
39
|
+
|
40
|
+
Note this is different than how many Rails engines ask you to install migrations. This is intentional, as we want to maintain the original timestamps associated with these migrations.
|
41
|
+
|
42
|
+
### 2.4. Run Migrations
|
43
|
+
|
44
|
+
```
|
45
|
+
rake db:migrate
|
46
|
+
```
|
47
|
+
|
48
|
+
## Configuration
|
49
|
+
Usage limit configuration piggybacks on your [product definitions](/docs/billing/stripe.md) in `config/models/billing/products.yml`. It may help to make reference to the [default product definitions in the Bullet Train starter repository](https://github.com/bullet-train-co/bullet_train/blob/main/config/models/billing/products.yml).
|
50
|
+
|
51
|
+
## Basic Usage Limits
|
52
|
+
All limit definitions are organized by product, then by model, and finally by _verb_. For example, you can define the number of projects a team is allowed to have on a basic plan like so:
|
53
|
+
|
54
|
+
```yaml
|
55
|
+
basic:
|
56
|
+
prices:
|
57
|
+
# ...
|
58
|
+
limits:
|
59
|
+
projects:
|
60
|
+
have:
|
61
|
+
count: 3
|
62
|
+
enforcement: hard
|
63
|
+
upgradable: true
|
64
|
+
```
|
65
|
+
|
66
|
+
It's important to note that `have` is a special verb and represents the simple `count` of a given model on a `Team`. All _other_ verbs will be interpreted as time-based usage limits.
|
67
|
+
|
68
|
+
### Options
|
69
|
+
- `enforcement` can be `hard` or `soft`.
|
70
|
+
- When a `hard` limit is hit, the create form will be disabled.
|
71
|
+
- When a `soft` limit is hit, users are simply notified, but can continue to surpass the limit.
|
72
|
+
- `upgradable` indicates whether or not a user should be prompted to upgrade when they hit this limit.
|
73
|
+
|
74
|
+
### Excluding Records from `have` Usage Limits
|
75
|
+
All models have an overridable `billable` scope that includes all records by default. You can override this scope to ensure that certain records are filtered out from consideration when calculating usage limits. For example, we do the following on `Membership` to exclude removed team members from contributing to any limitation put on the number of team members, like so:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
scope :billable, -> { current_and_invited }
|
79
|
+
```
|
80
|
+
|
81
|
+
## Time-Based Usage Limits
|
82
|
+
|
83
|
+
### Configuring Limits
|
84
|
+
In addition to simple `have` usage limits, you can specify other types of usage limits by defining other verbs. For example, you can limit the number of blog posts that can be published in a 3-day period on the free plan like this:
|
85
|
+
|
86
|
+
```yaml
|
87
|
+
free:
|
88
|
+
limits:
|
89
|
+
blogs/posts:
|
90
|
+
publish:
|
91
|
+
count: 1
|
92
|
+
duration: 3
|
93
|
+
interval: days
|
94
|
+
enforcement: hard
|
95
|
+
```
|
96
|
+
|
97
|
+
- `count` is how many times something can happen.
|
98
|
+
- `duration` and `interval` represent the time period we'll track for, e.g. "3 days" in this case.
|
99
|
+
- Valid options for `interval` are anything you can append to an integer, e.g. `minutes`, `hours`, `days`, `weeks`, `months`, etc., both plural and singular.
|
100
|
+
|
101
|
+
### Tracking Usage
|
102
|
+
For these custom verbs, it's important to also instrument the application for tracking when these actions have taken place. For example:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class Blogs::Post < ApplicationRecord
|
106
|
+
# ...
|
107
|
+
|
108
|
+
def publish!
|
109
|
+
update(published_at: Time.zone.now)
|
110
|
+
track_billing_usage(:published)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
If you'd like to increment the usage count by more than one, you can pass a quantity like `count: 5` to this call.
|
116
|
+
|
117
|
+
### Cycling Trackers Regularly
|
118
|
+
We include a Rake task you'll need to run on a regular basis in order to cycle the usage trackers that are created at a `Team` level. By default, you should probably run this every five minutes:
|
119
|
+
|
120
|
+
```
|
121
|
+
rake billing:cycle_usage_trackers
|
122
|
+
```
|
123
|
+
|
124
|
+
## Checking Usage Limits
|
125
|
+
|
126
|
+
### Checking Basic Limits
|
127
|
+
For basic `have` limits, forms generated by Super Scaffolding will be automatically disabled when a `hard` limit has been hit. Index views will also alert users to a limit being hit or broken for both `hard` and `soft` limits.
|
128
|
+
|
129
|
+
> If your Bullet Train application scaffolding predates this feature, you can reference the newest Tangible Things [index template](https://github.com/bullet-train-co/bullet_train-super_scaffolding/blob/main/app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb) and [form template](https://github.com/bullet-train-co/bullet_train-super_scaffolding/blob/main/app/views/account/scaffolding/completely_concrete/tangible_things/_form.html.erb) to see how we're using the `shared/limits/index` and `shared/limits/form` partials to present and enforce usage limits, and copy this usage in your own views.
|
130
|
+
|
131
|
+
### Checking Time-Based Limits
|
132
|
+
To make decisions based on or enforce time-based limits in your views and controllers, you can use the `current_limits` helper like this:
|
133
|
+
|
134
|
+
```
|
135
|
+
current_limits.can?(:publish, Blogs::Post)
|
136
|
+
```
|
137
|
+
|
138
|
+
(You can also pass quantities like `count: 5` as an option.)
|
139
|
+
|
140
|
+
#### Presenting an Error
|
141
|
+
|
142
|
+
If you're checking on this in a controller before taking an action and you want to present an error to the user based on their usage, you can redirect with this special flash error message key:
|
143
|
+
|
144
|
+
```
|
145
|
+
flash[:error] = :create_limit
|
146
|
+
redirect_to [:account, @post]
|
147
|
+
```
|
148
|
+
> TODO This technically works but needs to be redone. Too limited.
|
data/lib/bullet_train/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bullet_train
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Culver
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-11-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: standard
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: bullet_train-routes
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
140
|
name: devise
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -483,6 +497,7 @@ files:
|
|
483
497
|
- app/mailers/concerns/mailers/base.rb
|
484
498
|
- app/mailers/devise_mailer.rb
|
485
499
|
- app/mailers/user_mailer.rb
|
500
|
+
- app/models/billing/mock_limiter.rb
|
486
501
|
- app/models/concerns/current_attributes/base.rb
|
487
502
|
- app/models/concerns/invitations/base.rb
|
488
503
|
- app/models/concerns/memberships/base.rb
|
@@ -612,6 +627,7 @@ files:
|
|
612
627
|
- docs/action-models.md
|
613
628
|
- docs/authentication.md
|
614
629
|
- docs/billing/stripe.md
|
630
|
+
- docs/billing/usage.md
|
615
631
|
- docs/desktop.md
|
616
632
|
- docs/field-partials.md
|
617
633
|
- docs/field-partials/buttons.md
|