saucy 0.1.1

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.
Files changed (86) hide show
  1. data/Gemfile +8 -0
  2. data/Gemfile.lock +106 -0
  3. data/README +9 -0
  4. data/app/controllers/accounts_controller.rb +57 -0
  5. data/app/controllers/invitations_controller.rb +36 -0
  6. data/app/controllers/memberships_controller.rb +9 -0
  7. data/app/controllers/permissions_controller.rb +17 -0
  8. data/app/controllers/plans_controller.rb +7 -0
  9. data/app/controllers/profiles_controller.rb +17 -0
  10. data/app/controllers/projects_controller.rb +56 -0
  11. data/app/models/account_membership.rb +8 -0
  12. data/app/models/invitation.rb +86 -0
  13. data/app/models/project_membership.rb +16 -0
  14. data/app/models/signup.rb +134 -0
  15. data/app/views/accounts/_account.html.erb +12 -0
  16. data/app/views/accounts/_blank_slate.html.erb +6 -0
  17. data/app/views/accounts/_projects.html.erb +12 -0
  18. data/app/views/accounts/_tab_bar.html.erb +8 -0
  19. data/app/views/accounts/edit.html.erb +17 -0
  20. data/app/views/accounts/index.html.erb +3 -0
  21. data/app/views/accounts/new.html.erb +26 -0
  22. data/app/views/invitation_mailer/invitation.text.erb +6 -0
  23. data/app/views/invitations/new.html.erb +11 -0
  24. data/app/views/invitations/show.html.erb +20 -0
  25. data/app/views/memberships/index.html.erb +16 -0
  26. data/app/views/permissions/edit.html.erb +15 -0
  27. data/app/views/plans/index.html.erb +3 -0
  28. data/app/views/profiles/_inputs.html.erb +6 -0
  29. data/app/views/profiles/edit.html.erb +35 -0
  30. data/app/views/projects/_form.html.erb +4 -0
  31. data/app/views/projects/edit.html.erb +18 -0
  32. data/app/views/projects/index.html.erb +13 -0
  33. data/app/views/projects/new.html.erb +11 -0
  34. data/config/routes.rb +18 -0
  35. data/features/run_features.feature +73 -0
  36. data/features/step_definitions/clearance_steps.rb +45 -0
  37. data/features/step_definitions/rails_steps.rb +70 -0
  38. data/features/support/env.rb +4 -0
  39. data/features/support/file.rb +11 -0
  40. data/lib/generators/saucy/base.rb +18 -0
  41. data/lib/generators/saucy/features/features_generator.rb +68 -0
  42. data/lib/generators/saucy/features/templates/factories.rb +55 -0
  43. data/lib/generators/saucy/features/templates/step_definitions/email_steps.rb +13 -0
  44. data/lib/generators/saucy/features/templates/step_definitions/factory_girl_steps.rb +1 -0
  45. data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +3 -0
  46. data/lib/generators/saucy/features/templates/step_definitions/project_steps.rb +4 -0
  47. data/lib/generators/saucy/features/templates/step_definitions/session_steps.rb +27 -0
  48. data/lib/generators/saucy/features/templates/step_definitions/user_steps.rb +54 -0
  49. data/lib/generators/saucy/install/install_generator.rb +36 -0
  50. data/lib/generators/saucy/install/templates/create_saucy_tables.rb +72 -0
  51. data/lib/generators/saucy/install/templates/models/account.rb +3 -0
  52. data/lib/generators/saucy/install/templates/models/invitation_mailer.rb +9 -0
  53. data/lib/generators/saucy/install/templates/models/plan.rb +3 -0
  54. data/lib/generators/saucy/install/templates/models/project.rb +3 -0
  55. data/lib/generators/saucy/views/views_generator.rb +23 -0
  56. data/lib/saucy.rb +7 -0
  57. data/lib/saucy/account.rb +50 -0
  58. data/lib/saucy/account_authorization.rb +34 -0
  59. data/lib/saucy/configuration.rb +12 -0
  60. data/lib/saucy/engine.rb +9 -0
  61. data/lib/saucy/layouts.rb +36 -0
  62. data/lib/saucy/plan.rb +11 -0
  63. data/lib/saucy/project.rb +39 -0
  64. data/lib/saucy/user.rb +37 -0
  65. data/spec/controllers/accounts_controller_spec.rb +204 -0
  66. data/spec/controllers/application_controller_spec.rb +27 -0
  67. data/spec/controllers/invitations_controller_spec.rb +155 -0
  68. data/spec/controllers/memberships_controller_spec.rb +33 -0
  69. data/spec/controllers/permissions_controller_spec.rb +69 -0
  70. data/spec/controllers/profiles_controller_spec.rb +43 -0
  71. data/spec/controllers/projects_controller_spec.rb +123 -0
  72. data/spec/layouts_spec.rb +21 -0
  73. data/spec/models/account_membership_spec.rb +13 -0
  74. data/spec/models/account_spec.rb +61 -0
  75. data/spec/models/invitation_spec.rb +160 -0
  76. data/spec/models/project_membership_spec.rb +26 -0
  77. data/spec/models/project_spec.rb +80 -0
  78. data/spec/models/signup_spec.rb +175 -0
  79. data/spec/models/user_spec.rb +96 -0
  80. data/spec/saucy_spec.rb +7 -0
  81. data/spec/spec_helper.rb +31 -0
  82. data/spec/support/authentication_helpers.rb +71 -0
  83. data/spec/support/authorization_helpers.rb +56 -0
  84. data/spec/support/clearance_matchers.rb +55 -0
  85. data/spec/views/accounts/_account.html.erb_spec.rb +66 -0
  86. metadata +203 -0
@@ -0,0 +1,3 @@
1
+ class Account < ActiveRecord::Base
2
+ include Saucy::Account
3
+ end
@@ -0,0 +1,9 @@
1
+ class InvitationMailer < ActionMailer::Base
2
+ default :from => 'donotreply@example.com'
3
+
4
+ def invitation(invitation)
5
+ @invitation = invitation
6
+ mail :to => invitation.email,
7
+ :subject => "Invitation"
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ class Plan < ActiveRecord::Base
2
+ include Saucy::Plan
3
+ end
@@ -0,0 +1,3 @@
1
+ class Project < ActiveRecord::Base
2
+ include Saucy::Project
3
+ end
@@ -0,0 +1,23 @@
1
+ require 'generators/saucy/base'
2
+ require 'rails/generators/active_record/migration'
3
+
4
+ module Saucy
5
+ module Generators
6
+ class ViewsGenerator < Base
7
+ include Rails::Generators::Migration
8
+ extend ActiveRecord::Generators::Migration
9
+
10
+ desc <<-DESC
11
+ Description:
12
+ Copy saucy views to your application.
13
+ DESC
14
+
15
+
16
+ def copy_views
17
+ views_path = File.join(File.dirname(File.expand_path(__FILE__)), '../../../../app/views')
18
+ directory views_path, 'app/views'
19
+ end
20
+ end
21
+ end
22
+ end
23
+
data/lib/saucy.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'saucy/user'
2
+ require 'saucy/account'
3
+ require 'saucy/project'
4
+ require 'saucy/plan'
5
+ require 'saucy/account_authorization'
6
+ require 'saucy/configuration'
7
+ require 'saucy/engine'
@@ -0,0 +1,50 @@
1
+ module Saucy
2
+ module Account
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :account_memberships, :dependent => :destroy
7
+ has_many :users, :through => :account_memberships
8
+ has_many :projects, :dependent => :destroy
9
+ has_many :admins, :through => :account_memberships,
10
+ :source => :user,
11
+ :conditions => { 'account_memberships.admin' => true }
12
+
13
+ belongs_to :plan
14
+
15
+ validates_uniqueness_of :name, :url
16
+ validates_presence_of :name, :url
17
+
18
+ attr_accessible :name, :url
19
+
20
+ validates_format_of :url,
21
+ :with => %r{^[a-z0-9]+$},
22
+ :message => "must be only lower case letters."
23
+ validates_exclusion_of :url,
24
+ :in => %w[app admin blog dev ftp mail pop pop3 imap smtp staging stats status www],
25
+ :message => 'is reserved.'
26
+ end
27
+
28
+ module InstanceMethods
29
+ def to_param
30
+ url
31
+ end
32
+
33
+ def has_member?(user)
34
+ account_memberships.exists?(:user_id => user.id)
35
+ end
36
+
37
+ def users_by_name
38
+ users.by_name
39
+ end
40
+
41
+ def projects_by_name
42
+ projects.by_name
43
+ end
44
+
45
+ def projects_visible_to(user)
46
+ projects.visible_to(user)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ module Saucy
2
+ module AccountAuthorization
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :current_account
7
+ include InstanceMethods
8
+ end
9
+
10
+ module InstanceMethods
11
+ protected
12
+
13
+ def current_account
14
+ ::Account.find_by_url!(params[:account_id])
15
+ end
16
+
17
+ def current_project
18
+ ::Project.find(params[:project_id])
19
+ end
20
+
21
+ def authorize_admin
22
+ unless current_user.admin_of?(current_account)
23
+ deny_access("You must be an admin to access that page.")
24
+ end
25
+ end
26
+
27
+ def authorize_member
28
+ unless current_user.member_of?(current_project)
29
+ deny_access("You do not have permission for this project.")
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ require 'saucy/layouts'
2
+
3
+ module Saucy
4
+ class Configuration
5
+ attr_reader :layouts
6
+
7
+ def initialize
8
+ @layouts = Layouts.new
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,9 @@
1
+ require "saucy"
2
+ require "rails"
3
+
4
+ module Saucy
5
+ class Engine < Rails::Engine
6
+ config.saucy = Configuration.new
7
+ end
8
+ end
9
+
@@ -0,0 +1,36 @@
1
+ module Saucy
2
+ class Layouts
3
+ def initialize
4
+ @controllers = {}
5
+ end
6
+
7
+ def method_missing(controller_name, *args, &block)
8
+ @controllers[controller_name.to_s] ||= Controller.new
9
+ end
10
+
11
+ def self.to_proc
12
+ lambda do |controller|
13
+ controller_name = controller.controller_name
14
+ action_name = controller.action_name
15
+ Rails.application.config.saucy.layouts.send(controller_name).send(action_name)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ class Controller
22
+ def initialize
23
+ @actions = {}
24
+ end
25
+
26
+ def method_missing(method_name, *args, &block)
27
+ action_name = method_name.to_s
28
+ if action_name.sub!(/=$/, '')
29
+ @actions[action_name] = args.first
30
+ else
31
+ @actions[action_name] ||= "application"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
data/lib/saucy/plan.rb ADDED
@@ -0,0 +1,11 @@
1
+ module Saucy
2
+ module Plan
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :accounts
7
+
8
+ validates_presence_of :name
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,39 @@
1
+ module Saucy
2
+ module Project
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ belongs_to :account
7
+ has_many :project_memberships, :dependent => :destroy
8
+ has_many :users, :through => :project_memberships
9
+
10
+ validates_presence_of :account_id
11
+
12
+ after_create :assign_default_memberships
13
+ end
14
+
15
+ module ClassMethods
16
+ def visible_to(user)
17
+ where(['projects.id IN(?)', user.project_ids])
18
+ end
19
+
20
+ def by_name
21
+ order("projects.name")
22
+ end
23
+ end
24
+
25
+ module InstanceMethods
26
+ def has_member?(user)
27
+ project_memberships.exists?(:user_id => user.id)
28
+ end
29
+
30
+ private
31
+
32
+ def assign_default_memberships
33
+ account.admins.each do |admin|
34
+ self.project_memberships.create(:user => admin)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/saucy/user.rb ADDED
@@ -0,0 +1,37 @@
1
+ module Saucy
2
+ module User
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_accessible :name, :project_ids, :email, :password_confirmation, :password
7
+ has_many :project_memberships
8
+ has_many :projects, :through => :project_memberships
9
+ has_many :account_memberships
10
+ has_many :accounts, :through => :account_memberships
11
+ validates_presence_of :name
12
+ end
13
+
14
+ module InstanceMethods
15
+ def admin_of?(account)
16
+ account_memberships.exists?(:account_id => account.id, :admin => true)
17
+ end
18
+
19
+ def member_of?(account_or_project)
20
+ account_or_project.has_member?(self)
21
+ end
22
+
23
+ def update_permissions_for(account, project_ids)
24
+ project_ids_for_other_accounts = projects.
25
+ reject { |project| project.account_id == account.id }.
26
+ map { |project| project.id }
27
+ self.project_ids = project_ids + project_ids_for_other_accounts
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ def by_name
33
+ order('users.name')
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,204 @@
1
+ require 'spec_helper'
2
+
3
+ describe AccountsController, "routes" do
4
+ it { should route(:get, "/plans/abc/accounts/new").
5
+ to(:action => :new, :plan_id => :abc) }
6
+ it { should route(:get, "/accounts").to(:action => :index) }
7
+ it { should route(:post, "/plans/abc/accounts").
8
+ to(:action => :create, :plan_id => :abc) }
9
+ it { should route(:put, "/accounts/1").to(:action => :update, :id => 1) }
10
+ it { should route(:get, "/accounts/1/edit").to(:action => :edit, :id => 1) }
11
+ end
12
+
13
+ describe AccountsController, "new" do
14
+ let(:signup) { stub('signup') }
15
+ let(:plan) { Factory(:plan) }
16
+
17
+ before do
18
+ Signup.stubs(:new => signup)
19
+ get :new, :plan_id => plan.to_param
20
+ end
21
+
22
+ it "renders the new account form" do
23
+ should respond_with(:success)
24
+ should render_template(:new)
25
+ end
26
+
27
+ it "assigns a new signup" do
28
+ Signup.should have_received(:new)
29
+ should assign_to(:signup).with(signup)
30
+ end
31
+ end
32
+
33
+ describe AccountsController, "successful create for a confirmed user" do
34
+ let(:user) { Factory.stub(:email_confirmed_user) }
35
+ let(:signup) { stub('signup', :user => user, :email_confirmed? => true, :user= => nil) }
36
+ let(:signup_attributes) { "attributes" }
37
+ let(:plan) { Factory(:plan) }
38
+
39
+ before do
40
+ Signup.stubs(:new => signup)
41
+ signup.stubs(:save => true)
42
+ sign_in_as user
43
+ post :create, :signup => signup_attributes, :plan_id => plan.to_param
44
+ end
45
+
46
+ it "creates an signup" do
47
+ Signup.should have_received(:new).with(signup_attributes)
48
+ signup.should have_received(:save)
49
+ end
50
+
51
+ it "redirects to the root" do
52
+ should redirect_to(root_url)
53
+ end
54
+
55
+ it "sets the current user" do
56
+ signup.should have_received(:user=).with(user)
57
+ end
58
+
59
+ it { should set_the_flash.to(/created/i) }
60
+ it { should_not set_the_flash.to(/confirm/i) }
61
+ it { should be_signed_in.as(user) }
62
+ end
63
+
64
+ describe AccountsController, "successful create for an unconfirmed user" do
65
+ let(:user) { Factory.stub(:user) }
66
+ let(:signup) { stub('signup', :user => user, :email_confirmed? => false, :user= => nil) }
67
+ let(:signup_attributes) { "attributes" }
68
+ let(:plan) { Factory(:plan) }
69
+
70
+ before do
71
+ Signup.stubs(:new => signup)
72
+ signup.stubs(:save => true)
73
+ post :create, :signup => signup_attributes, :plan_id => plan.to_param
74
+ end
75
+
76
+ it "creates an signup" do
77
+ Signup.should have_received(:new).with(signup_attributes)
78
+ signup.should have_received(:save)
79
+ end
80
+
81
+ it "redirects to the sign in url" do
82
+ should redirect_to(sign_in_url)
83
+ end
84
+
85
+ it { should set_the_flash.to(/created/i) }
86
+ it { should set_the_flash.to(/confirm/i) }
87
+ end
88
+
89
+ describe AccountsController, "failed create" do
90
+ let(:signup) { stub('signup', :email_confirmed? => false, :user= => nil) }
91
+ let(:signup_attributes) { "attributes" }
92
+ let(:plan) { Factory(:plan) }
93
+
94
+ before do
95
+ Signup.stubs(:new => signup)
96
+ signup.stubs(:save => false)
97
+ post :create, :signup => signup_attributes, :plan_id => plan.to_param
98
+ end
99
+
100
+ it "creates an signup" do
101
+ Signup.should have_received(:new).with(signup_attributes)
102
+ signup.should have_received(:save)
103
+ end
104
+
105
+ it "renders the new signup form" do
106
+ should respond_with(:success)
107
+ should render_template(:new)
108
+ end
109
+
110
+ it "assigns a new signup" do
111
+ Signup.should have_received(:new)
112
+ should assign_to(:signup).with(signup)
113
+ end
114
+ end
115
+
116
+ describe AccountsController, "index with multiple projects" do
117
+ let(:user) { Factory.stub(:user) }
118
+ let(:accounts) { %w(one two) }
119
+ let(:projects) { %w(one two) }
120
+
121
+ before do
122
+ user.stubs(:accounts => accounts)
123
+ user.stubs(:projects => projects)
124
+ sign_in_as user
125
+ get :index
126
+ end
127
+
128
+ it "renders the dashboard page" do
129
+ should respond_with(:success)
130
+ should render_template(:index)
131
+ end
132
+
133
+ it "assigns the user's accounts" do
134
+ user.should have_received(:accounts)
135
+ should assign_to(:accounts).with(accounts)
136
+ end
137
+ end
138
+
139
+ describe AccountsController, "index with one project" do
140
+ let(:user) { Factory.stub(:user) }
141
+ let(:accounts) { %w(one two) }
142
+ let(:projects) { %w(one) }
143
+
144
+ before do
145
+ user.stubs(:accounts => accounts)
146
+ user.stubs(:projects => projects)
147
+ sign_in_as user
148
+ get :index
149
+ end
150
+
151
+ it "redirects to the project" do
152
+ should redirect_to(project_url(projects.first))
153
+ end
154
+ end
155
+
156
+ describe AccountsController, "valid update", :as => :account_admin do
157
+ before do
158
+ put :update,
159
+ :account => Factory.attributes_for(:account),
160
+ :id => account.to_param
161
+ end
162
+
163
+ it "redirects to settings" do
164
+ should redirect_to(edit_profile_url)
165
+ end
166
+
167
+ it { should set_the_flash.to(/updated/) }
168
+ end
169
+
170
+ describe AccountsController, "invalid update", :as => :account_admin do
171
+ before do
172
+ put :update,
173
+ :account => {:name => ""},
174
+ :id => account.to_param
175
+ end
176
+
177
+ it { should respond_with(:success) }
178
+ it { should render_template(:edit) }
179
+ end
180
+
181
+ describe AccountsController, "edit", :as => :account_admin do
182
+ before do
183
+ get :edit, :id => account.to_param
184
+ end
185
+
186
+ it "renders the edit template" do
187
+ should respond_with(:success)
188
+ should render_template(:edit)
189
+ end
190
+
191
+ it "assigns the account" do
192
+ should assign_to(:account).with(account)
193
+ end
194
+ end
195
+
196
+ describe AccountsController, "permissions", :as => :account_member do
197
+ it { should deny_access.
198
+ on(:get, :edit, :id => account.to_param).
199
+ flash(/admin/) }
200
+ it { should deny_access.
201
+ on(:put, :update, :id => account.to_param).
202
+ flash(/admin/) }
203
+ end
204
+