saucy 0.1.18 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +4 -1
- data/Gemfile.lock +12 -2
- data/README.md +75 -0
- data/app/controllers/accounts_controller.rb +2 -0
- data/app/controllers/billings_controller.rb +22 -0
- data/app/controllers/plans_controller.rb +12 -0
- data/app/models/limit.rb +5 -0
- data/app/models/signup.rb +20 -6
- data/app/views/accounts/_tab_bar.html.erb +3 -1
- data/app/views/accounts/edit.html.erb +6 -0
- data/app/views/accounts/new.html.erb +14 -4
- data/app/views/billings/_form.html.erb +8 -0
- data/app/views/billings/edit.html.erb +11 -0
- data/app/views/billings/show.html.erb +3 -0
- data/app/views/plans/_plan.html.erb +2 -0
- data/app/views/plans/edit.html.erb +23 -0
- data/config/routes.rb +2 -0
- data/features/run_features.feature +4 -0
- data/lib/generators/saucy/features/features_generator.rb +4 -0
- data/lib/generators/saucy/features/templates/factories.rb +14 -2
- data/lib/generators/saucy/features/templates/features/manage_account.feature +0 -2
- data/lib/generators/saucy/features/templates/features/manage_billing.feature +119 -0
- data/lib/generators/saucy/features/templates/features/sign_up.feature +2 -0
- data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +76 -0
- data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +7 -0
- data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +22 -0
- data/lib/generators/saucy/features/templates/step_definitions/plan_steps.rb +9 -0
- data/lib/generators/saucy/features/templates/support/braintree.rb +5 -0
- data/lib/generators/saucy/install/templates/create_saucy_tables.rb +14 -1
- data/lib/generators/saucy/specs/specs_generator.rb +20 -0
- data/lib/generators/saucy/specs/templates/support/braintree.rb +5 -0
- data/lib/saucy/account.rb +134 -1
- data/lib/saucy/braintree.rb +100 -0
- data/lib/saucy/plan.rb +23 -0
- data/spec/controllers/accounts_controller_spec.rb +2 -2
- data/spec/environment.rb +4 -1
- data/spec/models/account_spec.rb +182 -0
- data/spec/models/limit_spec.rb +9 -0
- data/spec/models/plan_spec.rb +54 -0
- data/spec/models/signup_spec.rb +9 -0
- data/spec/support/braintree.rb +5 -0
- metadata +74 -8
- data/README +0 -38
data/Gemfile
CHANGED
@@ -5,11 +5,14 @@ gem "rake"
|
|
5
5
|
gem "rspec-rails", :require => false
|
6
6
|
gem "rails", ">= 3.0.3"
|
7
7
|
gem "thin"
|
8
|
-
gem "clearance", :
|
8
|
+
gem "clearance", :git => "git://github.com/thoughtbot/clearance.git", :require => false
|
9
9
|
gem "shoulda", :require => false
|
10
10
|
gem "bourne", :require => false
|
11
11
|
gem "sqlite3-ruby", :require => false
|
12
12
|
gem "factory_girl", :require => false
|
13
|
+
gem "sinatra"
|
14
|
+
gem "sham_rack"
|
15
|
+
gem "braintree"
|
13
16
|
|
14
17
|
# used by the rails app in cucumber
|
15
18
|
gem "cucumber-rails", :require => false
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
GIT
|
2
2
|
remote: git://github.com/thoughtbot/clearance.git
|
3
|
-
revision:
|
4
|
-
ref: 71d7c7f7a29ea757cf84fcc497cbc8ecf6cac211
|
3
|
+
revision: 7fb410051e0e7cce758883c98ec7b13824265cd0
|
5
4
|
specs:
|
6
5
|
clearance (0.9.1)
|
7
6
|
rails (~> 3.0.0)
|
@@ -43,6 +42,8 @@ GEM
|
|
43
42
|
background_process (1.2)
|
44
43
|
bourne (1.0)
|
45
44
|
mocha (= 0.9.8)
|
45
|
+
braintree (2.6.2)
|
46
|
+
builder
|
46
47
|
builder (2.1.2)
|
47
48
|
capybara (0.4.0)
|
48
49
|
celerity (>= 0.7.9)
|
@@ -140,7 +141,12 @@ GEM
|
|
140
141
|
ffi (~> 0.6.3)
|
141
142
|
json_pure
|
142
143
|
rubyzip
|
144
|
+
sham_rack (1.3.3)
|
145
|
+
rack
|
143
146
|
shoulda (2.11.3)
|
147
|
+
sinatra (1.1.2)
|
148
|
+
rack (~> 1.1)
|
149
|
+
tilt (~> 1.2)
|
144
150
|
sqlite3-ruby (1.3.2)
|
145
151
|
term-ansicolor (1.0.5)
|
146
152
|
thin (1.2.7)
|
@@ -148,6 +154,7 @@ GEM
|
|
148
154
|
eventmachine (>= 0.12.6)
|
149
155
|
rack (>= 1.0.0)
|
150
156
|
thor (0.14.6)
|
157
|
+
tilt (1.2.1)
|
151
158
|
treetop (1.4.9)
|
152
159
|
polyglot (>= 0.3.1)
|
153
160
|
tzinfo (0.3.23)
|
@@ -160,6 +167,7 @@ PLATFORMS
|
|
160
167
|
DEPENDENCIES
|
161
168
|
aruba
|
162
169
|
bourne
|
170
|
+
braintree
|
163
171
|
capybara
|
164
172
|
clearance!
|
165
173
|
cucumber
|
@@ -173,6 +181,8 @@ DEPENDENCIES
|
|
173
181
|
rails (>= 3.0.3)
|
174
182
|
rake
|
175
183
|
rspec-rails
|
184
|
+
sham_rack
|
176
185
|
shoulda
|
186
|
+
sinatra
|
177
187
|
sqlite3-ruby
|
178
188
|
thin
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
Saucy
|
2
|
+
=====
|
3
|
+
|
4
|
+
Saucy is a Rails engine for monthly subscription-style SaaS apps.
|
5
|
+
|
6
|
+
Example scenarios covered by Saucy:
|
7
|
+
|
8
|
+
* I sign up for "Free" plan under new account "thoughtbot"
|
9
|
+
* I am an admin and can be reached at "dan@example.com"
|
10
|
+
* I create a project "Hoptoad"
|
11
|
+
* I upgrade to the "Basic" plan and my credit card is charged
|
12
|
+
* I now have permissions to add users and projects to the "thoughtbot" account
|
13
|
+
* I invite "joe@example.com" to "Hoptoad"
|
14
|
+
* I create a project "Trajectory"
|
15
|
+
* I invite "mike@example.com" to "Trajectory"
|
16
|
+
|
17
|
+
Installation
|
18
|
+
------------
|
19
|
+
|
20
|
+
In your Gemfile:
|
21
|
+
|
22
|
+
gem "saucy", :git => 'git@github.com:thoughtbot/saucy.git'
|
23
|
+
|
24
|
+
After you bundle, run the generators:
|
25
|
+
|
26
|
+
rails generate saucy:install
|
27
|
+
rails generate saucy:views
|
28
|
+
|
29
|
+
Development environment
|
30
|
+
-----------------------
|
31
|
+
|
32
|
+
Plans need to exist for users to sign up for. In db/seeds.rb:
|
33
|
+
|
34
|
+
%w(free expensive mega-expensive).each do |plan_name|
|
35
|
+
Plan.find_or_create_by_name(plan_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
Then run: rake db:seed
|
39
|
+
|
40
|
+
Test environment
|
41
|
+
----------------
|
42
|
+
|
43
|
+
Generate the Braintree Fake for your specs:
|
44
|
+
|
45
|
+
rails generate saucy:specs
|
46
|
+
|
47
|
+
Generate feature coverage:
|
48
|
+
|
49
|
+
rails generate saucy:features
|
50
|
+
|
51
|
+
To use seed data in your Cucumber, add this to features/support/seed.rb:
|
52
|
+
|
53
|
+
require Rails.root.join('db','seeds')
|
54
|
+
|
55
|
+
Customization
|
56
|
+
-------------
|
57
|
+
|
58
|
+
To change the layout for a controller inside of saucy, add a line like this to
|
59
|
+
your config/application.rb:
|
60
|
+
|
61
|
+
config.saucy.layouts.accounts.index = "custom"
|
62
|
+
|
63
|
+
Your layout should yield(:header) in order to get the headers from saucy views.
|
64
|
+
|
65
|
+
To extend the ProjectsController:
|
66
|
+
|
67
|
+
class ProjectsController < ApplicationController
|
68
|
+
include Saucy::ProjectsController
|
69
|
+
|
70
|
+
def edit
|
71
|
+
super
|
72
|
+
@deleters = @project.deleters
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -9,8 +9,10 @@ class AccountsController < ApplicationController
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def create
|
12
|
+
@plan = Plan.find(params[:plan_id])
|
12
13
|
@signup = Signup.new(params[:signup])
|
13
14
|
@signup.user = current_user
|
15
|
+
@signup.plan = @plan
|
14
16
|
if @signup.save
|
15
17
|
flash[:success] = "Account was created."
|
16
18
|
sign_in @signup.user
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class BillingsController < ApplicationController
|
2
|
+
def show
|
3
|
+
end
|
4
|
+
|
5
|
+
def edit
|
6
|
+
@account = current_account
|
7
|
+
|
8
|
+
@account.cardholder_name = @account.credit_card.cardholder_name
|
9
|
+
@account.billing_email = @account.customer.email
|
10
|
+
@account.expiration_month = @account.credit_card.expiration_month
|
11
|
+
@account.expiration_year = @account.credit_card.expiration_year
|
12
|
+
end
|
13
|
+
|
14
|
+
def update
|
15
|
+
@account = current_account
|
16
|
+
if @account.save_braintree!(params[:account])
|
17
|
+
redirect_to account_billing_path(@account), :notice => t('.update.notice', :default => "Billing information updated successfully")
|
18
|
+
else
|
19
|
+
render :edit
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -4,4 +4,16 @@ class PlansController < ApplicationController
|
|
4
4
|
def index
|
5
5
|
@plans = Plan.all
|
6
6
|
end
|
7
|
+
|
8
|
+
def edit
|
9
|
+
@plans = Plan.all
|
10
|
+
@account = current_account
|
11
|
+
end
|
12
|
+
|
13
|
+
def update
|
14
|
+
@plans = Plan.all
|
15
|
+
@account = current_account
|
16
|
+
@account.save_braintree!(params[:account])
|
17
|
+
redirect_to edit_account_path(@account), :notice => t('.update.notice', :default => "Plan changed successfully")
|
18
|
+
end
|
7
19
|
end
|
data/app/models/limit.rb
ADDED
data/app/models/signup.rb
CHANGED
@@ -9,6 +9,13 @@ class Signup
|
|
9
9
|
:account => {
|
10
10
|
:name => :account_name,
|
11
11
|
:keyword => :keyword,
|
12
|
+
:cardholder_name => :cardholder_name,
|
13
|
+
:billing_email => :billing_email,
|
14
|
+
:card_number => :card_number,
|
15
|
+
:expiration_month => :expiration_month,
|
16
|
+
:expiration_year => :expiration_year,
|
17
|
+
:verification_code => :verification_code,
|
18
|
+
:plan => :plan
|
12
19
|
},
|
13
20
|
:user => {
|
14
21
|
:name => :user_name,
|
@@ -18,8 +25,9 @@ class Signup
|
|
18
25
|
}
|
19
26
|
}.freeze
|
20
27
|
|
21
|
-
attr_accessor :account_name, :keyword, :user_name, :
|
22
|
-
:password_confirmation
|
28
|
+
attr_accessor :account_name, :keyword, :user_name, :billing_email, :password,
|
29
|
+
:password_confirmation, :cardholder_name, :email, :card_number,
|
30
|
+
:expiration_month, :expiration_year, :plan, :verification_code
|
23
31
|
|
24
32
|
def initialize(attributes = {})
|
25
33
|
attributes.each do |attribute, value|
|
@@ -38,8 +46,14 @@ class Signup
|
|
38
46
|
delegate_attributes_for(:user) unless existing_user
|
39
47
|
|
40
48
|
if valid?
|
41
|
-
|
42
|
-
|
49
|
+
begin
|
50
|
+
save!
|
51
|
+
true
|
52
|
+
rescue ActiveRecord::RecordNotSaved
|
53
|
+
delegate_errors_for(:account)
|
54
|
+
delegate_errors_for(:user)
|
55
|
+
false
|
56
|
+
end
|
43
57
|
else
|
44
58
|
false
|
45
59
|
end
|
@@ -68,8 +82,8 @@ class Signup
|
|
68
82
|
|
69
83
|
def membership
|
70
84
|
@membership ||= Membership.new(:user => user,
|
71
|
-
|
72
|
-
|
85
|
+
:account => account,
|
86
|
+
:admin => true)
|
73
87
|
end
|
74
88
|
|
75
89
|
def valid?
|
@@ -3,6 +3,8 @@
|
|
3
3
|
<li class="accounts"><%= link_to "Account Info", edit_account_url(current_account) %></li>
|
4
4
|
<li class="projects"><%= link_to "Projects", account_projects_url(current_account) %></li>
|
5
5
|
<li class="users"><%= link_to "Users", account_memberships_url(current_account) %></li>
|
6
|
-
|
6
|
+
<% if current_account.plan.billed? %>
|
7
|
+
<li class="billing"><%= link_to "Billing", account_billing_path(current_account) %></li>
|
8
|
+
<% end -%>
|
7
9
|
</ul>
|
8
10
|
</div>
|
@@ -4,6 +4,12 @@
|
|
4
4
|
|
5
5
|
<%= render :partial => 'tab_bar' %>
|
6
6
|
|
7
|
+
<div class="plan current">
|
8
|
+
<h3>Your Plan</h3>
|
9
|
+
<%= render @account.plan %>
|
10
|
+
<%= link_to "Upgrade", edit_account_plan_path(@account) %>
|
11
|
+
</div>
|
12
|
+
|
7
13
|
<%= content_tag_for :div, @account do -%>
|
8
14
|
<%= semantic_form_for @account do |form| %>
|
9
15
|
<%= form.inputs do %>
|
@@ -7,20 +7,30 @@
|
|
7
7
|
|
8
8
|
<h5 class="legend">Basic Information</h5>
|
9
9
|
<%= form.inputs do %>
|
10
|
-
<%= form.input :account_name, :label => 'Company Name', :hint => account_url(Account.new(:keyword => 'keyword')).html_safe %>
|
10
|
+
<%= form.input :account_name, :label => 'Company Name', :hint => account_url(Account.new(:keyword => 'keyword')).html_safe, :required => true %>
|
11
11
|
<%= form.input :keyword, :wrapper_html => { :style => 'display: none' } %>
|
12
12
|
<% end %>
|
13
13
|
|
14
14
|
<% unless signed_in? -%>
|
15
15
|
<h5 class="legend">User Information</h5>
|
16
16
|
<%= form.inputs do %>
|
17
|
-
<%= form.input :user_name, :label => "Your name" %>
|
18
|
-
<%= form.input :email %>
|
19
|
-
<%= form.input :password %>
|
17
|
+
<%= form.input :user_name, :label => "Your name", :required => true %>
|
18
|
+
<%= form.input :email, :required => true %>
|
19
|
+
<%= form.input :password, :required => true %>
|
20
20
|
<%= form.input :password_confirmation, :label => "Confirm password", :required => true %>
|
21
21
|
<% end -%>
|
22
22
|
<% end -%>
|
23
23
|
|
24
|
+
<div class="plan chosen">
|
25
|
+
<h3>You're signing up for:</h3>
|
26
|
+
<%= render @plan %>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<% if @plan.billed? -%>
|
30
|
+
<h5 class="legend">Billing Information</h5>
|
31
|
+
<%= render :partial => 'billings/form', :locals => { :form => form } %>
|
32
|
+
<% end -%>
|
33
|
+
|
24
34
|
<%= form.buttons do %>
|
25
35
|
<%= form.commit_button "Sign up" %>
|
26
36
|
<% end %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<%= form.inputs do %>
|
2
|
+
<%= form.input :cardholder_name, :required => true %>
|
3
|
+
<%= form.input :billing_email, :required => true, :hint => "We'll send receipts and billing issues to this address." %>
|
4
|
+
<%= form.input :card_number, :required => true, :input_html => { :autocomplete => "off" } %>
|
5
|
+
<%= form.input :verification_code, :required => true, :input_html => { :autocomplete => "off" } %>
|
6
|
+
<%= form.input :expiration_month, :collection => [['January', '01'], ['February', '02'], ['March', '03'], ['April', '04'], ['May', '05'], ['June', '06'], ['July', '07'], ['August', '08'], ['September', '09'], ['October', '10'], ['November', '11'], ['December', '12']], :required => true %>
|
7
|
+
<%= form.input :expiration_year, :collection => (Date.today.year..(Date.today.year+10)).to_a, :required => true %>
|
8
|
+
<% end -%>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<%= content_for :header do -%>
|
2
|
+
<h2>Billing Information</h2>
|
3
|
+
<% end -%>
|
4
|
+
|
5
|
+
<%= semantic_form_for @account, :url => account_billing_path(@account) do |form| %>
|
6
|
+
<%= render :partial => 'billings/form', :locals => { :form => form } %>
|
7
|
+
|
8
|
+
<%= form.buttons do %>
|
9
|
+
<%= form.commit_button "Update" %>
|
10
|
+
<% end %>
|
11
|
+
<% end %>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<%= content_for :header do -%>
|
2
|
+
<h2>Upgrade Your Plan</h2>
|
3
|
+
<% end -%>
|
4
|
+
|
5
|
+
<%= semantic_form_for @account, :url => account_plan_path(@account) do |form| %>
|
6
|
+
<%= form.inputs :class => "radio optional" do %>
|
7
|
+
<% @plans.each do |plan| -%>
|
8
|
+
<%= content_tag :li, :class => "#{'disabled' if !@account.can_change_plan_to?(plan)}" do %>
|
9
|
+
<%= form.label :plan_id, render(plan), :value => plan.id %>
|
10
|
+
<%= form.radio_button :plan_id, plan.id, :disabled => !@account.can_change_plan_to?(plan) %>
|
11
|
+
<% end %>
|
12
|
+
<% end -%>
|
13
|
+
<% end %>
|
14
|
+
|
15
|
+
<% if !@account.credit_card -%>
|
16
|
+
<h5 class="legend">Billing Information</h5>
|
17
|
+
<%= render :partial => 'billings/form', :locals => { :form => form } %>
|
18
|
+
<% end -%>
|
19
|
+
|
20
|
+
<%= form.buttons do %>
|
21
|
+
<%= form.commit_button "Upgrade" %>
|
22
|
+
<% end %>
|
23
|
+
<% end %>
|
data/config/routes.rb
CHANGED
@@ -4,6 +4,8 @@ Rails.application.routes.draw do
|
|
4
4
|
resources :accounts, :only => [:index, :edit, :update]
|
5
5
|
|
6
6
|
through :accounts do
|
7
|
+
resource :billing
|
8
|
+
resource :plan
|
7
9
|
resources :projects
|
8
10
|
resources :memberships, :only => [:index, :edit, :update, :destroy]
|
9
11
|
resources :invitations, :only => [:show, :update, :new, :create]
|
@@ -25,6 +25,7 @@ Feature: generate a saucy application and run rake
|
|
25
25
|
|
26
26
|
Scenario: generate a saucy application and run rake
|
27
27
|
When I successfully run "rails generate saucy:install"
|
28
|
+
And I successfully run "rails generate saucy:specs"
|
28
29
|
And I successfully run "rails generate saucy:features"
|
29
30
|
And I successfully run "rake db:migrate"
|
30
31
|
And I run "rake"
|
@@ -37,6 +38,7 @@ Feature: generate a saucy application and run rake
|
|
37
38
|
|
38
39
|
Scenario: A new saucy app with custom views
|
39
40
|
When I successfully run "rails generate saucy:install"
|
41
|
+
And I successfully run "rails generate saucy:specs"
|
40
42
|
And I successfully run "rails generate saucy:features"
|
41
43
|
And I successfully run "rails generate saucy:views"
|
42
44
|
And I successfully run "rake db:migrate"
|
@@ -51,6 +53,7 @@ Feature: generate a saucy application and run rake
|
|
51
53
|
|
52
54
|
Scenario: A new saucy app with custom layouts
|
53
55
|
When I successfully run "rails generate saucy:install"
|
56
|
+
And I successfully run "rails generate saucy:specs"
|
54
57
|
And I successfully run "rails generate saucy:features"
|
55
58
|
And I successfully run "rake db:migrate"
|
56
59
|
And I add a custom layout to the accounts index
|
@@ -64,6 +67,7 @@ Feature: generate a saucy application and run rake
|
|
64
67
|
|
65
68
|
Scenario: run specs
|
66
69
|
When I successfully run "rails generate saucy:install"
|
70
|
+
And I successfully run "rails generate saucy:specs"
|
67
71
|
And I successfully run "rails generate saucy:features"
|
68
72
|
And I successfully run "rake db:migrate"
|
69
73
|
And I copy the specs for this project
|
@@ -12,6 +12,7 @@ DESC
|
|
12
12
|
def copy_feature_files
|
13
13
|
directory "features", "features/saucy"
|
14
14
|
directory "step_definitions", "features/step_definitions/saucy"
|
15
|
+
directory "support", "features/support/saucy"
|
15
16
|
template "README", "features/saucy/README"
|
16
17
|
template "README", "features/step_definitions/saucy/README"
|
17
18
|
empty_directory "spec"
|
@@ -47,6 +48,9 @@ DESC
|
|
47
48
|
when /sign up page for the "([^"]+)" plan$/i
|
48
49
|
plan = Plan.find_by_name!($1)
|
49
50
|
new_plan_account_path(plan)
|
51
|
+
when /^the billing page for the "([^"]+)" account$/
|
52
|
+
account = Account.find_by_name!($1)
|
53
|
+
account_billing_path(account)
|
50
54
|
PATHS
|
51
55
|
|
52
56
|
replace_in_file "features/support/paths.rb",
|
@@ -14,8 +14,9 @@ Factory.define :user do |user|
|
|
14
14
|
end
|
15
15
|
|
16
16
|
Factory.define :account do |f|
|
17
|
-
f.name
|
18
|
-
f.keyword
|
17
|
+
f.name { Factory.next(:name) }
|
18
|
+
f.keyword { Factory.next(:name) }
|
19
|
+
f.association :plan
|
19
20
|
end
|
20
21
|
|
21
22
|
Factory.define :membership do |f|
|
@@ -30,6 +31,7 @@ Factory.define :signup do |f|
|
|
30
31
|
f.email { Factory.next :email }
|
31
32
|
f.password { "password" }
|
32
33
|
f.password_confirmation { "password" }
|
34
|
+
f.association :plan
|
33
35
|
end
|
34
36
|
|
35
37
|
Factory.define :project do |f|
|
@@ -51,3 +53,13 @@ end
|
|
51
53
|
Factory.define :plan do |f|
|
52
54
|
f.name 'Free'
|
53
55
|
end
|
56
|
+
|
57
|
+
Factory.define :paid_plan, :parent => :plan do |f|
|
58
|
+
f.name 'Paid'
|
59
|
+
f.price 1
|
60
|
+
end
|
61
|
+
|
62
|
+
Factory.define :limit do |f|
|
63
|
+
f.name { Factory.next(:name) }
|
64
|
+
f.association :plan
|
65
|
+
end
|