saasy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. data/CHANGELOG.md +114 -0
  2. data/Gemfile +26 -0
  3. data/README.md +118 -0
  4. data/Rakefile +38 -0
  5. data/app/controllers/accounts_controller.rb +68 -0
  6. data/app/controllers/billings_controller.rb +25 -0
  7. data/app/controllers/invitations_controller.rb +65 -0
  8. data/app/controllers/memberships_controller.rb +45 -0
  9. data/app/controllers/plans_controller.rb +24 -0
  10. data/app/controllers/profiles_controller.rb +19 -0
  11. data/app/helpers/limits_helper.rb +13 -0
  12. data/app/mailers/billing_mailer.rb +53 -0
  13. data/app/mailers/invitation_mailer.rb +18 -0
  14. data/app/models/invitation.rb +113 -0
  15. data/app/models/limit.rb +49 -0
  16. data/app/models/membership.rb +26 -0
  17. data/app/models/permission.rb +19 -0
  18. data/app/models/signup.rb +163 -0
  19. data/app/views/accounts/_account.html.erb +9 -0
  20. data/app/views/accounts/_blank_slate.html.erb +6 -0
  21. data/app/views/accounts/_projects.html.erb +12 -0
  22. data/app/views/accounts/_subnav.html.erb +10 -0
  23. data/app/views/accounts/edit.html.erb +34 -0
  24. data/app/views/accounts/index.html.erb +9 -0
  25. data/app/views/accounts/new.html.erb +36 -0
  26. data/app/views/billing_mailer/completed_trial.text.erb +13 -0
  27. data/app/views/billing_mailer/expiring_trial.text.erb +15 -0
  28. data/app/views/billing_mailer/new_unactivated.text.erb +1 -0
  29. data/app/views/billing_mailer/problem.html.erb +13 -0
  30. data/app/views/billing_mailer/problem.text.erb +14 -0
  31. data/app/views/billing_mailer/receipt.html.erb +41 -0
  32. data/app/views/billing_mailer/receipt.text.erb +25 -0
  33. data/app/views/billings/_form.html.erb +8 -0
  34. data/app/views/billings/edit.html.erb +13 -0
  35. data/app/views/billings/show.html.erb +29 -0
  36. data/app/views/invitation_mailer/invitation.text.erb +6 -0
  37. data/app/views/invitations/new.html.erb +17 -0
  38. data/app/views/invitations/show.html.erb +22 -0
  39. data/app/views/layouts/saucy.html.erb +36 -0
  40. data/app/views/limits/_meter.html.erb +13 -0
  41. data/app/views/memberships/edit.html.erb +21 -0
  42. data/app/views/memberships/index.html.erb +17 -0
  43. data/app/views/plans/_plan.html.erb +32 -0
  44. data/app/views/plans/_terms.html.erb +15 -0
  45. data/app/views/plans/edit.html.erb +33 -0
  46. data/app/views/plans/index.html.erb +12 -0
  47. data/app/views/profiles/_inputs.html.erb +5 -0
  48. data/app/views/profiles/edit.html.erb +36 -0
  49. data/app/views/projects/_form.html.erb +36 -0
  50. data/app/views/projects/edit.html.erb +22 -0
  51. data/app/views/projects/index.html.erb +28 -0
  52. data/app/views/projects/new.html.erb +13 -0
  53. data/app/views/projects/show.html.erb +0 -0
  54. data/app/views/shared/_project_dropdown.html.erb +55 -0
  55. data/app/views/shared/_saucy_javascript.html.erb +33 -0
  56. data/config/locales/en.yml +37 -0
  57. data/config/routes.rb +19 -0
  58. data/features/run_features.feature +83 -0
  59. data/features/step_definitions/clearance_steps.rb +45 -0
  60. data/features/step_definitions/rails_steps.rb +73 -0
  61. data/features/step_definitions/saucy_steps.rb +8 -0
  62. data/features/support/env.rb +4 -0
  63. data/features/support/file.rb +11 -0
  64. data/lib/generators/saucy/base.rb +18 -0
  65. data/lib/generators/saucy/features/features_generator.rb +91 -0
  66. data/lib/generators/saucy/features/templates/README +3 -0
  67. data/lib/generators/saucy/features/templates/factories.rb +71 -0
  68. data/lib/generators/saucy/features/templates/features/edit_profile.feature +9 -0
  69. data/lib/generators/saucy/features/templates/features/edit_project_permissions.feature +37 -0
  70. data/lib/generators/saucy/features/templates/features/edit_user_permissions.feature +47 -0
  71. data/lib/generators/saucy/features/templates/features/manage_account.feature +35 -0
  72. data/lib/generators/saucy/features/templates/features/manage_billing.feature +93 -0
  73. data/lib/generators/saucy/features/templates/features/manage_plan.feature +143 -0
  74. data/lib/generators/saucy/features/templates/features/manage_projects.feature +139 -0
  75. data/lib/generators/saucy/features/templates/features/manage_users.feature +142 -0
  76. data/lib/generators/saucy/features/templates/features/new_account.feature +33 -0
  77. data/lib/generators/saucy/features/templates/features/project_dropdown.feature +77 -0
  78. data/lib/generators/saucy/features/templates/features/sign_up.feature +32 -0
  79. data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +65 -0
  80. data/lib/generators/saucy/features/templates/features/trial_plans.feature +82 -0
  81. data/lib/generators/saucy/features/templates/step_definitions/account_steps.rb +30 -0
  82. data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +25 -0
  83. data/lib/generators/saucy/features/templates/step_definitions/cron_steps.rb +23 -0
  84. data/lib/generators/saucy/features/templates/step_definitions/email_steps.rb +40 -0
  85. data/lib/generators/saucy/features/templates/step_definitions/factory_girl_steps.rb +1 -0
  86. data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +51 -0
  87. data/lib/generators/saucy/features/templates/step_definitions/plan_steps.rb +16 -0
  88. data/lib/generators/saucy/features/templates/step_definitions/project_steps.rb +4 -0
  89. data/lib/generators/saucy/features/templates/step_definitions/session_steps.rb +37 -0
  90. data/lib/generators/saucy/features/templates/step_definitions/user_steps.rb +100 -0
  91. data/lib/generators/saucy/features/templates/support/braintree.rb +5 -0
  92. data/lib/generators/saucy/install/install_generator.rb +40 -0
  93. data/lib/generators/saucy/install/templates/controllers/projects_controller.rb +3 -0
  94. data/lib/generators/saucy/install/templates/create_saucy_tables.rb +115 -0
  95. data/lib/generators/saucy/install/templates/models/account.rb +3 -0
  96. data/lib/generators/saucy/install/templates/models/plan.rb +3 -0
  97. data/lib/generators/saucy/install/templates/models/project.rb +3 -0
  98. data/lib/generators/saucy/specs/specs_generator.rb +20 -0
  99. data/lib/generators/saucy/specs/templates/support/braintree.rb +5 -0
  100. data/lib/generators/saucy/views/views_generator.rb +23 -0
  101. data/lib/saucy.rb +10 -0
  102. data/lib/saucy/account.rb +132 -0
  103. data/lib/saucy/account_authorization.rb +67 -0
  104. data/lib/saucy/configuration.rb +29 -0
  105. data/lib/saucy/engine.rb +35 -0
  106. data/lib/saucy/fake_braintree.rb +134 -0
  107. data/lib/saucy/layouts.rb +36 -0
  108. data/lib/saucy/plan.rb +54 -0
  109. data/lib/saucy/project.rb +125 -0
  110. data/lib/saucy/projects_controller.rb +94 -0
  111. data/lib/saucy/railties/tasks.rake +28 -0
  112. data/lib/saucy/routing_extensions.rb +121 -0
  113. data/lib/saucy/subscription.rb +237 -0
  114. data/lib/saucy/user.rb +30 -0
  115. data/spec/controllers/accounts_controller_spec.rb +228 -0
  116. data/spec/controllers/application_controller_spec.rb +32 -0
  117. data/spec/controllers/invitations_controller_spec.rb +215 -0
  118. data/spec/controllers/memberships_controller_spec.rb +117 -0
  119. data/spec/controllers/plans_controller_spec.rb +13 -0
  120. data/spec/controllers/profiles_controller_spec.rb +48 -0
  121. data/spec/controllers/projects_controller_spec.rb +216 -0
  122. data/spec/environment.rb +95 -0
  123. data/spec/layouts_spec.rb +21 -0
  124. data/spec/mailers/billing_mailer_spec.rb +68 -0
  125. data/spec/mailers/invitiation_mailer_spec.rb +19 -0
  126. data/spec/models/account_spec.rb +218 -0
  127. data/spec/models/invitation_spec.rb +320 -0
  128. data/spec/models/limit_spec.rb +70 -0
  129. data/spec/models/membership_spec.rb +37 -0
  130. data/spec/models/permission_spec.rb +30 -0
  131. data/spec/models/plan_spec.rb +81 -0
  132. data/spec/models/project_spec.rb +223 -0
  133. data/spec/models/signup_spec.rb +177 -0
  134. data/spec/models/subscription_spec.rb +481 -0
  135. data/spec/models/user_spec.rb +72 -0
  136. data/spec/route_extensions_spec.rb +51 -0
  137. data/spec/saucy_spec.rb +62 -0
  138. data/spec/scaffold/config/routes.rb +5 -0
  139. data/spec/spec_helper.rb +39 -0
  140. data/spec/support/authentication_helpers.rb +81 -0
  141. data/spec/support/authorization_helpers.rb +56 -0
  142. data/spec/support/braintree.rb +7 -0
  143. data/spec/support/clearance_matchers.rb +55 -0
  144. data/spec/support/notifications.rb +57 -0
  145. data/spec/views/accounts/_account.html.erb_spec.rb +37 -0
  146. metadata +325 -0
@@ -0,0 +1,67 @@
1
+ module Saucy
2
+ module AccountAuthorization
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :current_account, :current_project, :current_account?, :current_project?
7
+ include InstanceMethods
8
+ end
9
+
10
+ module InstanceMethods
11
+ protected
12
+
13
+ def current_account
14
+ ::Account.find_by_keyword!(params[:account_id])
15
+ end
16
+
17
+ def current_project
18
+ current_account.projects.find_by_keyword!(params[:project_id])
19
+ end
20
+
21
+ def current_project?
22
+ params[:project_id].present?
23
+ end
24
+
25
+ def current_account?
26
+ params[:account_id].present?
27
+ end
28
+
29
+ def authorize_admin
30
+ unless current_user.admin_of?(current_account)
31
+ deny_access("You must be an admin to access that page.")
32
+ end
33
+ end
34
+
35
+ def authorize_member
36
+ unless current_user.member_of?(current_project)
37
+ deny_access("You do not have permission for this project.")
38
+ end
39
+ end
40
+
41
+ def ensure_active_account
42
+ if current_account?
43
+ if current_account.past_due?
44
+ redirect_unusable_account account_billing_path(current_account),
45
+ "past_due"
46
+ end
47
+ if current_account.expired?
48
+ redirect_unusable_account edit_account_plan_path(current_account),
49
+ "expired"
50
+ end
51
+ end
52
+ end
53
+
54
+ def redirect_unusable_account(path, failure)
55
+ role = current_user.admin_of?(current_account) ? 'admin' : 'user'
56
+ flash[:alert] = t("saucy.errors.#{failure}.#{role}")
57
+ redirect_to path
58
+ end
59
+
60
+ def ensure_account_within_limit(limit_name)
61
+ if !Limit.can_add_one?(limit_name, current_account)
62
+ redirect_to :back, :alert => t("saucy.errors.limited", :default => "You are at your limit of %{name} for your current plan.", :name => limit_name)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,29 @@
1
+ require 'saucy/layouts'
2
+
3
+ module Saucy
4
+ class Configuration
5
+ cattr_reader :layouts
6
+ cattr_accessor :manager_email_address
7
+ cattr_accessor :support_email_address
8
+ cattr_accessor :merchant_account_id
9
+ cattr_accessor :observers
10
+
11
+ def initialize
12
+ @@manager_email_address = 'manager@example.com'
13
+ @@support_email_address = 'support@example.com'
14
+ @@layouts = Layouts.new
15
+ @@observers = []
16
+ end
17
+
18
+ def self.observe(observer)
19
+ @@observers << observer
20
+ end
21
+
22
+ def self.notify(event, data)
23
+ @@observers.each do |observer|
24
+ observer.send(event, data)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,35 @@
1
+ require "saucy"
2
+ require "rails"
3
+ require "braintree"
4
+
5
+ module Saucy
6
+ class Engine < Rails::Engine
7
+ config.saucy = Configuration.new
8
+
9
+ initializer :braintree_logger, :after => :initialize_logger do
10
+ Braintree::Configuration.logger = Rails.logger
11
+ end
12
+
13
+ initializer :filter_credit_card_info do
14
+ Rails.configuration.filter_parameters += [:password,
15
+ :card_number,
16
+ :cardholder_name,
17
+ :verification_code,
18
+ :expiration_month,
19
+ :expiration_year]
20
+ end
21
+
22
+ initializer 'limits.helper' do |app|
23
+ ActionView::Base.send :include, LimitsHelper
24
+ end
25
+
26
+ {:short_date => "%x"}.each do |k, v|
27
+ Time::DATE_FORMATS[k] = v
28
+ end
29
+
30
+ rake_tasks do
31
+ load "saucy/railties/tasks.rake"
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,134 @@
1
+ require 'braintree'
2
+
3
+ Braintree::Configuration.environment = :production
4
+ Braintree::Configuration.merchant_id = "xxx"
5
+ Braintree::Configuration.public_key = "xxx"
6
+ Braintree::Configuration.private_key = "xxx"
7
+
8
+ require 'digest/md5'
9
+ require 'sham_rack'
10
+
11
+ class FakeBraintree
12
+ cattr_accessor :customers, :subscriptions, :failures, :transaction
13
+ @@customers = {}
14
+ @@subscriptions = {}
15
+ @@failures = {}
16
+ @@transaction = {}
17
+
18
+ def self.clear!
19
+ @@customers = {}
20
+ @@subscriptions = {}
21
+ @@failures = {}
22
+ @@transaction = {}
23
+ end
24
+
25
+ def self.failure?(card_number)
26
+ self.failures.include?(card_number)
27
+ end
28
+
29
+ def self.failure_response(card_number)
30
+ failure = self.failures[card_number]
31
+ failure["errors"] ||= { "errors" => [] }
32
+ { "message" => failure["message"], "verification" => { "status" => failure["status"], "processor_response_text" => failure["message"], "processor-response-code" => failure["code"], "gateway_rejection_reason" => "cvv", "cvv_response_code" => failure["code"] }, "errors" => failure["errors"], "params" => {}}
33
+ end
34
+
35
+ def self.generated_transaction
36
+ {"status_history"=>[{"timestamp"=>Time.now, "amount"=>FakeBraintree.transaction[:amount], "transaction_source"=>"CP", "user"=>"copycopter", "status"=>"authorized"}, {"timestamp"=>Time.now, "amount"=>FakeBraintree.transaction[:amount], "transaction_source"=>"CP", "user"=>"copycopter", "status"=>FakeBraintree.transaction[:status]}], "created_at"=>(FakeBraintree.transaction[:created_at] || Time.now), "currency_iso_code"=>"USD", "settlement_batch_id"=>nil, "processor_authorization_code"=>"ZKB4VJ", "avs_postal_code_response_code"=>"I", "order_id"=>nil, "updated_at"=>Time.now, "refunded_transaction_id"=>nil, "amount"=>FakeBraintree.transaction[:amount], "credit_card"=>{"last_4"=>"1111", "card_type"=>"Visa", "token"=>"8yq7", "customer_location"=>"US", "expiration_year"=>"2013", "expiration_month"=>"02", "bin"=>"411111", "cardholder_name"=>"Chad Lee Pytel"}, "refund_id"=>nil, "add_ons"=>[], "shipping"=>{"region"=>nil, "company"=>nil, "country_name"=>nil, "extended_address"=>nil, "postal_code"=>nil, "id"=>nil, "street_address"=>nil, "country_code_numeric"=>nil, "last_name"=>nil, "locality"=>nil, "country_code_alpha2"=>nil, "country_code_alpha3"=>nil, "first_name"=>nil}, "id"=>"49sbx6", "merchant_account_id"=>"Thoughtbot", "type"=>"sale", "cvv_response_code"=>"I", "subscription_id"=>FakeBraintree.transaction[:subscription_id], "custom_fields"=>"\n ", "discounts"=>[], "billing"=>{"region"=>nil, "company"=>nil, "country_name"=>nil, "extended_address"=>nil, "postal_code"=>nil, "id"=>nil, "street_address"=>nil, "country_code_numeric"=>nil, "last_name"=>nil, "locality"=>nil, "country_code_alpha2"=>nil, "country_code_alpha3"=>nil, "first_name"=>nil}, "processor_response_code"=>"1000", "refund_ids"=>[], "customer"=>{"company"=>nil, "id"=>"108427", "last_name"=>nil, "fax"=>nil, "phone"=>nil, "website"=>nil, "first_name"=>nil, "email"=>"cpytel@thoughtbot.com"}, "avs_error_response_code"=>nil, "processor_response_text"=>"Approved", "avs_street_address_response_code"=>"I", "status"=>FakeBraintree.transaction[:status], "gateway_rejection_reason"=>nil}
37
+ end
38
+ end
39
+
40
+ ShamRack.at("www.braintreegateway.com", 443).sinatra do
41
+ set :show_exceptions, false
42
+ set :dump_errors, true
43
+ set :raise_errors, true
44
+ disable :logging
45
+
46
+ post "/merchants/:merchant_id/customers" do
47
+ customer = Hash.from_xml(request.body).delete("customer")
48
+ if !FakeBraintree.failure?(customer["credit_card"]["number"])
49
+ customer["id"] ||= Digest::MD5.hexdigest("#{params[:merchant_id]}#{Time.now.to_f}")
50
+ customer["merchant-id"] = params[:merchant_id]
51
+ if customer["credit_card"] && customer["credit_card"].is_a?(Hash)
52
+ customer["credit_card"].delete("__content__")
53
+ if !customer["credit_card"].empty?
54
+ customer["credit_card"]["last_4"] = customer["credit_card"].delete("number")[-4..-1]
55
+ customer["credit_card"]["token"] = Digest::MD5.hexdigest("#{customer['merchant_id']}#{customer['id']}#{Time.now.to_f}")
56
+ credit_card = customer.delete("credit_card")
57
+ customer["credit_cards"] = [credit_card]
58
+ end
59
+ end
60
+ FakeBraintree.customers[customer["id"]] = customer
61
+ [201, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
62
+ else
63
+ [422, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(FakeBraintree.failure_response(customer["credit_card"]["number"]).to_xml(:root => 'api_error_response'))]
64
+ end
65
+ end
66
+
67
+ get "/merchants/:merchant_id/customers/:id" do
68
+ customer = FakeBraintree.customers[params[:id]]
69
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
70
+ end
71
+
72
+ put "/merchants/:merchant_id/customers/:id" do
73
+ customer = Hash.from_xml(request.body).delete("customer")
74
+ if !FakeBraintree.failure?(customer["credit_card"]["number"])
75
+ customer["id"] = params[:id]
76
+ customer["merchant-id"] = params[:merchant_id]
77
+ if customer["credit_card"] && customer["credit_card"].is_a?(Hash)
78
+ customer["credit_card"].delete("__content__")
79
+ if !customer["credit_card"].empty?
80
+ customer["credit_card"]["last_4"] = customer["credit_card"].delete("number")[-4..-1]
81
+ customer["credit_card"]["token"] = Digest::MD5.hexdigest("#{customer['merchant_id']}#{customer['id']}#{Time.now.to_f}")
82
+ credit_card = customer.delete("credit_card")
83
+ customer["credit_cards"] = [credit_card]
84
+ end
85
+ end
86
+ FakeBraintree.customers[params["id"]] = customer
87
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
88
+ else
89
+ [422, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(FakeBraintree.failure_response(customer["credit_card"]["number"]).to_xml(:root => 'api_error_response'))]
90
+ end
91
+ end
92
+
93
+ delete "/merchants/:merchant_id/customers/:id" do
94
+ FakeBraintree.customers[params["id"]] = nil
95
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress("")]
96
+ end
97
+
98
+ post "/merchants/:merchant_id/subscriptions" do
99
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<subscription>\n <plan-id type=\"integer\">2</plan-id>\n <payment-method-token>b22x</payment-method-token>\n</subscription>\n"
100
+ subscription = Hash.from_xml(request.body).delete("subscription")
101
+ subscription["id"] ||= Digest::MD5.hexdigest("#{subscription["payment_method_token"]}#{Time.now.to_f}")
102
+ subscription["transactions"] = []
103
+ subscription["add_ons"] = []
104
+ subscription["discounts"] = []
105
+ subscription["next_billing_date"] = 1.month.from_now
106
+ subscription["status"] = Braintree::Subscription::Status::Active
107
+ FakeBraintree.subscriptions[subscription["id"]] = subscription
108
+ [201, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
109
+ end
110
+
111
+ get "/merchants/:merchant_id/subscriptions/:id" do
112
+ subscription = FakeBraintree.subscriptions[params[:id]]
113
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
114
+ end
115
+
116
+ put "/merchants/:merchant_id/subscriptions/:id" do
117
+ subscription = Hash.from_xml(request.body).delete("subscription")
118
+ subscription["transactions"] = []
119
+ subscription["add_ons"] = []
120
+ subscription["discounts"] = []
121
+ FakeBraintree.subscriptions[params["id"]] = subscription
122
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
123
+ end
124
+
125
+ post "/merchants/:merchant_id/transactions/advanced_search_ids" do
126
+ # "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<search>\n <created-at>\n <min type=\"datetime\">2011-01-10T14:14:26Z</min>\n </created-at>\n</search>\n"
127
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress("<search-results>\n <page-size type=\"integer\">50</page-size>\n <ids type=\"array\">\n <item>49sbx6</item>\n </ids>\n</search-results>\n")]
128
+ end
129
+
130
+ post "/merchants/:merchant_id/transactions/advanced_search" do
131
+ # "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<search>\n <ids type=\"array\">\n <item>49sbx6</item>\n </ids>\n <created-at>\n <min type=\"datetime\">2011-01-10T14:14:26Z</min>\n </created-at>\n</search>\n"
132
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(FakeBraintree.generated_transaction.to_xml)]
133
+ end
134
+ end
@@ -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
+ Saucy::Configuration.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] ||= "saucy"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,54 @@
1
+ module Saucy
2
+ module Plan
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :accounts
7
+ has_many :limits
8
+
9
+ validates_presence_of :name
10
+
11
+ def self.ordered
12
+ order('price desc')
13
+ end
14
+
15
+ def self.paid_by_price
16
+ paid.ordered
17
+ end
18
+
19
+ def self.trial
20
+ free.first
21
+ end
22
+
23
+ def self.paid
24
+ where('price > 0')
25
+ end
26
+
27
+ def self.free
28
+ where('price = 0')
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+ def free?
34
+ price.zero?
35
+ end
36
+
37
+ def billed?
38
+ !free?
39
+ end
40
+
41
+ def can_add_more?(limit, amount)
42
+ limits.numbered.named(limit).value > amount
43
+ end
44
+
45
+ def allows?(limit)
46
+ limits.boolean.named(limit).allowed?
47
+ end
48
+
49
+ def limit(limit_name)
50
+ limits.named(limit_name)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,125 @@
1
+ module Saucy
2
+ module Project
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ belongs_to :account
7
+ has_many :permissions, :dependent => :destroy
8
+ has_many :users, :through => :permissions
9
+
10
+ validates_presence_of :account_id, :keyword, :name
11
+
12
+ validates_uniqueness_of :keyword, :scope => :account_id
13
+
14
+ validates_format_of :keyword,
15
+ :with => %r{^[a-z0-9_-]+$},
16
+ :message => "must be only lower case letters or underscores."
17
+
18
+
19
+ validate :ensure_account_within_limit, :on => :update
20
+
21
+ after_create :setup_memberships
22
+ after_update :update_memberships
23
+
24
+ attr_protected :account, :account_id
25
+
26
+ # We have to define these here instead of mixing them in,
27
+ # because ActiveRecord does the same.
28
+
29
+ def user_ids=(new_user_ids)
30
+ @new_user_ids = new_user_ids.reject { |user_id| user_id.blank? }
31
+ end
32
+
33
+ def users
34
+ if new_record?
35
+ permissions.map { |permission| permission.membership.user }
36
+ else
37
+ permissions.includes(:user).map { |permission| permission.user }
38
+ end
39
+ end
40
+
41
+ def user_ids
42
+ users.map(&:id)
43
+ end
44
+ end
45
+
46
+ module ClassMethods
47
+ def visible_to(user)
48
+ where(['projects.id IN(?)', user.project_ids])
49
+ end
50
+
51
+ def archived
52
+ where(:archived => true)
53
+ end
54
+
55
+ def active
56
+ where(:archived => false)
57
+ end
58
+
59
+ def by_name
60
+ order("projects.name")
61
+ end
62
+
63
+ def build_with_default_permissions
64
+ new.assign_default_permissions
65
+ end
66
+ end
67
+
68
+ module InstanceMethods
69
+ def to_param
70
+ keyword
71
+ end
72
+
73
+ def has_member?(user)
74
+ permissions.
75
+ joins(:membership).
76
+ exists?(:memberships => { :user_id => user.id })
77
+ end
78
+
79
+ def assign_default_permissions
80
+ account.memberships.where(:admin => true).each do |membership|
81
+ self.permissions.build(:membership => membership)
82
+ end
83
+ self
84
+ end
85
+
86
+ private
87
+
88
+ def setup_memberships
89
+ @new_user_ids ||= []
90
+ @new_user_ids += admin_user_ids
91
+ removed_user_ids = self.user_ids - @new_user_ids
92
+ added_user_ids = @new_user_ids - self.user_ids
93
+
94
+ permissions.where(:user_id => removed_user_ids).destroy_all
95
+ added_user_ids.each do |added_user_id|
96
+ membership =
97
+ account.memberships.where(:user_id => added_user_id).first
98
+ permissions.create!(:membership => membership)
99
+ end
100
+ end
101
+
102
+ def update_memberships
103
+ setup_memberships if @new_user_ids
104
+ end
105
+
106
+ def admin_user_ids
107
+ account.
108
+ memberships.
109
+ where(:admin => true).
110
+ select(:user_id).
111
+ map(&:user_id)
112
+ end
113
+
114
+ def ensure_account_within_limit
115
+ message = "You are at your limit of %{name} for your current plan."
116
+ if archived_changed? && !archived? && !Limit.can_add_one?("projects", account)
117
+ errors.add(:archived, I18n.t("saucy.errors.limited", :default => message, :name => 'projects'))
118
+ end
119
+ if account_id_changed? && !Limit.can_add_one?("projects", account)
120
+ errors.add(:account_id, I18n.t("saucy.errors.limited", :default => message, :name => 'projects'))
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end