saasy 0.0.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 (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