saucy 0.1.18 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|