blue_light_special 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +67 -0
  3. data/Rakefile +95 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/blue_light_special/impersonations_controller.rb +44 -0
  6. data/app/controllers/blue_light_special/passwords_controller.rb +84 -0
  7. data/app/controllers/blue_light_special/sessions_controller.rb +70 -0
  8. data/app/controllers/blue_light_special/users_controller.rb +48 -0
  9. data/app/models/blue_light_special_mailer.rb +22 -0
  10. data/app/models/deliver_change_password_job.rb +19 -0
  11. data/app/models/deliver_welcome_job.rb +17 -0
  12. data/app/models/impersonation.rb +26 -0
  13. data/app/views/blue_light_special_mailer/change_password.html.erb +9 -0
  14. data/app/views/impersonations/index.html.erb +5 -0
  15. data/app/views/passwords/edit.html.erb +23 -0
  16. data/app/views/passwords/new.html.erb +15 -0
  17. data/app/views/sessions/new.html.erb +48 -0
  18. data/app/views/users/_form.html.erb +21 -0
  19. data/app/views/users/edit.html.erb +6 -0
  20. data/app/views/users/new.html.erb +6 -0
  21. data/app/views/users/show.html.erb +8 -0
  22. data/generators/blue_light_special/USAGE +1 -0
  23. data/generators/blue_light_special/blue_light_special_generator.rb +78 -0
  24. data/generators/blue_light_special/lib/insert_commands.rb +33 -0
  25. data/generators/blue_light_special/lib/rake_commands.rb +22 -0
  26. data/generators/blue_light_special/templates/README +20 -0
  27. data/generators/blue_light_special/templates/application.html.erb +50 -0
  28. data/generators/blue_light_special/templates/blue_light_special.rb +21 -0
  29. data/generators/blue_light_special/templates/blue_light_special.yml +42 -0
  30. data/generators/blue_light_special/templates/factories.rb +23 -0
  31. data/generators/blue_light_special/templates/migrations/create_users.rb +24 -0
  32. data/generators/blue_light_special/templates/migrations/update_users.rb +44 -0
  33. data/generators/blue_light_special/templates/style.css +31 -0
  34. data/generators/blue_light_special/templates/user.rb +3 -0
  35. data/generators/blue_light_special/templates/xd_receiver.html +10 -0
  36. data/generators/blue_light_special/templates/xd_receiver_ssl.html +10 -0
  37. data/generators/blue_light_special_admin/USAGE +1 -0
  38. data/generators/blue_light_special_admin/blue_light_special_admin_generator.rb +30 -0
  39. data/generators/blue_light_special_admin/lib/insert_commands.rb +33 -0
  40. data/generators/blue_light_special_admin/templates/README +16 -0
  41. data/generators/blue_light_special_admin/templates/app/controllers/admin/admin_controller.rb +14 -0
  42. data/generators/blue_light_special_admin/templates/app/controllers/admin/users_controller.rb +52 -0
  43. data/generators/blue_light_special_admin/templates/app/views/admin/users/_form.html.erb +25 -0
  44. data/generators/blue_light_special_admin/templates/app/views/admin/users/edit.html.erb +6 -0
  45. data/generators/blue_light_special_admin/templates/app/views/admin/users/index.html.erb +7 -0
  46. data/generators/blue_light_special_admin/templates/app/views/admin/users/new.html.erb +6 -0
  47. data/generators/blue_light_special_admin/templates/app/views/admin/users/show.html.erb +10 -0
  48. data/generators/blue_light_special_admin/templates/test/integration/admin/users_test.rb +201 -0
  49. data/generators/blue_light_special_tests/USAGE +1 -0
  50. data/generators/blue_light_special_tests/blue_light_special_tests_generator.rb +21 -0
  51. data/generators/blue_light_special_tests/templates/README +58 -0
  52. data/generators/blue_light_special_tests/templates/test/integration/edit_profile_test.rb +35 -0
  53. data/generators/blue_light_special_tests/templates/test/integration/facebook_test.rb +61 -0
  54. data/generators/blue_light_special_tests/templates/test/integration/impersonation_test.rb +39 -0
  55. data/generators/blue_light_special_tests/templates/test/integration/password_reset_test.rb +128 -0
  56. data/generators/blue_light_special_tests/templates/test/integration/sign_in_test.rb +66 -0
  57. data/generators/blue_light_special_tests/templates/test/integration/sign_out_test.rb +28 -0
  58. data/generators/blue_light_special_tests/templates/test/integration/sign_up_test.rb +47 -0
  59. data/lib/blue_light_special.rb +7 -0
  60. data/lib/blue_light_special/authentication.rb +138 -0
  61. data/lib/blue_light_special/configuration.rb +32 -0
  62. data/lib/blue_light_special/extensions/errors.rb +6 -0
  63. data/lib/blue_light_special/extensions/rescue.rb +5 -0
  64. data/lib/blue_light_special/routes.rb +55 -0
  65. data/lib/blue_light_special/user.rb +241 -0
  66. data/rails/init.rb +4 -0
  67. data/shoulda_macros/blue_light_special.rb +244 -0
  68. data/test/controllers/passwords_controller_test.rb +184 -0
  69. data/test/controllers/sessions_controller_test.rb +129 -0
  70. data/test/controllers/users_controller_test.rb +57 -0
  71. data/test/models/blue_light_special_mailer_test.rb +52 -0
  72. data/test/models/impersonation_test.rb +25 -0
  73. data/test/models/user_test.rb +213 -0
  74. data/test/rails_root/app/controllers/accounts_controller.rb +10 -0
  75. data/test/rails_root/app/controllers/application_controller.rb +6 -0
  76. data/test/rails_root/app/helpers/application_helper.rb +5 -0
  77. data/test/rails_root/app/helpers/confirmations_helper.rb +2 -0
  78. data/test/rails_root/app/helpers/passwords_helper.rb +2 -0
  79. data/test/rails_root/app/models/user.rb +3 -0
  80. data/test/rails_root/config/boot.rb +110 -0
  81. data/test/rails_root/config/environment.rb +22 -0
  82. data/test/rails_root/config/environments/development.rb +19 -0
  83. data/test/rails_root/config/environments/production.rb +1 -0
  84. data/test/rails_root/config/environments/test.rb +37 -0
  85. data/test/rails_root/config/initializers/blue_light_special.rb +4 -0
  86. data/test/rails_root/config/initializers/inflections.rb +10 -0
  87. data/test/rails_root/config/initializers/mime_types.rb +5 -0
  88. data/test/rails_root/config/initializers/requires.rb +13 -0
  89. data/test/rails_root/config/initializers/time_formats.rb +4 -0
  90. data/test/rails_root/config/routes.rb +9 -0
  91. data/test/rails_root/db/migrate/20100305173127_blue_light_special_create_users.rb +21 -0
  92. data/test/rails_root/db/migrate/20100305173129_create_delayed_jobs.rb +20 -0
  93. data/test/rails_root/public/dispatch.rb +10 -0
  94. data/test/rails_root/script/create_project.rb +52 -0
  95. data/test/rails_root/test/factories/user.rb +13 -0
  96. data/test/rails_root/test/functional/accounts_controller_test.rb +23 -0
  97. data/test/rails_root/test/integration/facebook_test.rb +49 -0
  98. data/test/rails_root/test/integration/impersonation_test.rb +38 -0
  99. data/test/rails_root/test/integration/password_reset_test.rb +127 -0
  100. data/test/rails_root/test/integration/sign_in_test.rb +72 -0
  101. data/test/rails_root/test/integration/sign_out_test.rb +28 -0
  102. data/test/rails_root/test/integration/sign_up_test.rb +84 -0
  103. data/test/test_helper.rb +21 -0
  104. metadata +219 -0
@@ -0,0 +1 @@
1
+ script/generate blue_light_special_tests
@@ -0,0 +1,21 @@
1
+ class BlueLightSpecialTestsGenerator < Rails::Generator::Base
2
+
3
+ def manifest
4
+ record do |m|
5
+ m.directory File.join("test", "integration")
6
+
7
+ ["test/integration/facebook_test.rb",
8
+ "test/integration/impersonation_test.rb",
9
+ "test/integration/sign_in_test.rb",
10
+ "test/integration/sign_out_test.rb",
11
+ "test/integration/sign_up_test.rb",
12
+ "test/integration/edit_profile_test.rb",
13
+ "test/integration/password_reset_test.rb"].each do |file|
14
+ m.file file, file
15
+ end
16
+
17
+ m.readme "README"
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,58 @@
1
+
2
+ *******************************************************************************
3
+
4
+ Next:
5
+
6
+ 1. To run the generated tests, you'll need shoulda, factory_girl, webrat, and fakeweb.
7
+ Update your config/environments/test.rb:
8
+
9
+ config.gem "shoulda"
10
+ config.gem "factory_girl"
11
+ config.gem "webrat"
12
+ config.gem "fakeweb"
13
+
14
+ Unless they are already included.
15
+
16
+ 2. Update your test_helper.rb with:
17
+
18
+ FakeWeb.allow_net_connect = false
19
+
20
+ Webrat.configure do |config|
21
+ config.mode = :rails
22
+ config.open_error_files = false
23
+ end
24
+
25
+ class ActionController::IntegrationTest
26
+ include Webrat::Matchers
27
+
28
+ def sign_in_as(email, password, url_to_visit = sign_in_url)
29
+ visit url_to_visit
30
+ fill_in "Email", :with => email
31
+ fill_in "Password", :with => password
32
+ click_button "sign in"
33
+ end
34
+
35
+ def reset_session
36
+ request.reset_session
37
+ controller.instance_variable_set(:@_current_user, nil)
38
+ end
39
+
40
+ def sign_up(options = {})
41
+ visit new_user_url
42
+ fill_in "email", :with => options[:email] || 'bob@bob.bob'
43
+ fill_in "first name", :with => options[:first_name] || 'Bob'
44
+ fill_in "last name", :with => options[:last_name] || 'Bob'
45
+ fill_in "password", :with => options[:password] || 'password'
46
+ fill_in "confirm password", :with => options[:password_confirmation] || options[:password] || 'password'
47
+ click_button 'sign up'
48
+ end
49
+
50
+ def sign_out
51
+ visit session_url, :delete
52
+ end
53
+
54
+ end
55
+
56
+ 3. Be sure to define a root_url in routes.rb.
57
+
58
+ *******************************************************************************
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ class EditProfileTest < ActionController::IntegrationTest
4
+
5
+ context 'Editing a user profile' do
6
+
7
+ setup do
8
+ @user = Factory(:user, :password => 'password')
9
+ sign_in_as(@user.email, 'password')
10
+ visit edit_user_path(@user)
11
+ end
12
+
13
+ should_respond_with :success
14
+
15
+ should "see the form with his info" do
16
+ assert_select "input#user_first_name[value='#{@user.first_name}']"
17
+ assert_select "input#user_last_name[value='#{@user.last_name}']"
18
+ assert_select "input#user_email[value='#{@user.email}']"
19
+ end
20
+
21
+ should "update valid information and see the SHOW page" do
22
+ fill_in "user_first_name", :with => 'OtherName'
23
+ click_button 'Save'
24
+ assert_contain /othername/i
25
+ end
26
+
27
+ should "update invalid information and see errors" do
28
+ fill_in "user_first_name", :with => ''
29
+ click_button 'Save'
30
+ assert_contain /First name .* blank/i
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,61 @@
1
+ require 'test_helper'
2
+
3
+ class FacebookTest < ActionController::IntegrationTest
4
+
5
+ if BlueLightSpecial.configuration.use_facebook_connect
6
+
7
+ context 'Signing in with Facebook' do
8
+
9
+ setup do
10
+ cookies[BlueLightSpecial.configuration.facebook_api_key + "_user"] = "8055"
11
+ cookies[BlueLightSpecial.configuration.facebook_api_key + "_session_key"] = "123456789"
12
+ FakeWeb.register_uri(:post,
13
+ %r|http://api.facebook.com/restserver.php|,
14
+ :body => '[{"about_me":"","activities":"","affiliations":{},"birthday":"July 18","books":"","current_location":{"city":"Orlando","state":"Florida","country":"United States","zip":""},"education_history":[{"name":"Florida Institute of Technology","year":1995,"concentrations":{},"degree":"","school_type":"Unknown"}],"first_name":"Bob","hometown_location":null,"hs_info":{"hs1_name":"Cheyenne Mountain High School","hs2_name":"","grad_year":1992,"hs1_id":3202,"hs2_id":0},"interests":"","is_app_user":true,"last_name":"Jones","meeting_for":{},"meeting_sex":{},"movies":"","music":"","name":"Bob Jones","notes_count":null,"pic":"http:\/\/profile.ak.fbcdn.net\/hprofile-ak-sf2p\/hs272.snc3\/23197_1334019372_5345_s.jpg","pic_big":"http:\/\/profile.ak.fbcdn.net\/v228\/245\/118\/n1334019372_6158.jpg","pic_small":"http:\/\/profile.ak.fbcdn.net\/hprofile-ak-sf2p\/hs272.snc3\/23197_1334019372_5345_t.jpg","political":"","profile_update_time":1267034911,"quotes":"","relationship_status":"","religion":"","sex":"male","significant_other_id":null,"status":{"message":"","time":0,"status_id":0},"timezone":-5,"tv":"","uid":8055,"wall_count":34,"work_history":{},"pic_square":"http:\/\/profile.ak.fbcdn.net\/hprofile-ak-sf2p\/hs272.snc3\/23197_1334019372_5345_q.jpg","has_added_app":true,"email_hashes":{},"locale":"en_US","profile_url":"http:\/\/www.facebook.com\/profile.php?id=1334019372","proxied_email":"apps+339309032618.1334019372.a320f4a38471f7b537079f5c13bb33f1@proxymail.facebook.com","pic_big_with_logo":"http:\/\/external.ak.fbcdn.net\/safe_image.php?logo&d=20fef10357c21b2e1acc8dac7d4bed49&url=http%3A%2F%2Fprofile.ak.fbcdn.net%2Fv228%2F245%2F118%2Fn1334019372_6158.jpg&v=5","pic_small_with_logo":"http:\/\/external.ak.fbcdn.net\/safe_image.php?logo&d=ad4b560e363f5b40ccbe81e1d985c91e&url=http%3A%2F%2Fprofile.ak.fbcdn.net%2Fhprofile-ak-sf2p%2Fhs272.snc3%2F23197_1334019372_5345_t.jpg&v=5","pic_square_with_logo":"http:\/\/external.ak.fbcdn.net\/safe_image.php?logo&d=a0118842ed70fce04e7883f5ab52023f&url=http%3A%2F%2Fprofile.ak.fbcdn.net%2Fhprofile-ak-sf2p%2Fhs272.snc3%2F23197_1334019372_5345_q.jpg&v=5","pic_with_logo":"http:\/\/external.ak.fbcdn.net\/safe_image.php?logo&d=eb90cc8c5f332436f5d56009aab6b467&url=http%3A%2F%2Fprofile.ak.fbcdn.net%2Fhprofile-ak-sf2p%2Fhs272.snc3%2F23197_1334019372_5345_s.jpg&v=5","birthday_date":"07\/18","email":"bob@example.com","allowed_restrictions":"alcohol"}]'
15
+ )
16
+ end
17
+
18
+ teardown do
19
+ cookies[BlueLightSpecial.configuration.facebook_api_key + "_user"] = nil
20
+ cookies[BlueLightSpecial.configuration.facebook_api_key + "_session_key"] = nil
21
+ end
22
+
23
+ should 'find an existing user with the facebook uid' do
24
+ user = Factory( :facebook_user,
25
+ :facebook_uid => 8055,
26
+ :email => 'bob@facebook.com')
27
+
28
+ visit fb_connect_url
29
+ assert controller.signed_in?
30
+ assert_equal controller.current_user, user
31
+ end
32
+
33
+ should 'find an existing user with the facebook email address' do
34
+ user = Factory( :user,
35
+ :facebook_uid => nil,
36
+ :email => 'bob@example.com')
37
+
38
+ visit fb_connect_url
39
+ assert controller.signed_in?
40
+ assert_equal controller.current_user, user
41
+ end
42
+
43
+ should 'create a new user when the facebook uid is not found' do
44
+ assert_nil User.find_by_facebook_uid(8055)
45
+
46
+ visit fb_connect_url
47
+ assert controller.signed_in?
48
+ assert_equal '8055', controller.current_user.facebook_uid
49
+ end
50
+
51
+ should 'copy the facebook user details' do
52
+ visit fb_connect_url
53
+ assert controller.signed_in?
54
+ assert_equal 'bob@example.com', controller.current_user.email
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,39 @@
1
+ require 'test_helper'
2
+
3
+ class ImpersonationTest < ActionController::IntegrationTest
4
+
5
+ context 'When impersonating another user' do
6
+
7
+ setup do
8
+ @bob = Factory(:user, :email => 'bob@bob.bob')
9
+ @admin_user = Factory(:admin_user, :email => 'admin@example.com')
10
+ sign_in_as @admin_user.email, @admin_user.password
11
+ impersonate(@bob)
12
+ end
13
+
14
+ should 'be signed in' do
15
+ assert controller.signed_in?
16
+ end
17
+
18
+ should 'be logged in as bob' do
19
+ assert_equal controller.current_user, @bob
20
+ end
21
+
22
+ should 'be able to go back to the original admin user' do
23
+ click_link "Stop impersonating"
24
+ assert controller.signed_in?
25
+ assert_equal controller.current_user, @admin_user
26
+ end
27
+
28
+ end
29
+
30
+
31
+ private
32
+
33
+
34
+ def impersonate(user)
35
+ visit impersonations_url
36
+ click_link "impersonate_#{user.id}"
37
+ end
38
+
39
+ end
@@ -0,0 +1,128 @@
1
+ require 'test_helper'
2
+
3
+ class PasswordResetTest < ActionController::IntegrationTest
4
+
5
+ context 'When requesting a password reset' do
6
+
7
+ setup do
8
+ ActionMailer::Base.deliveries.clear
9
+ end
10
+
11
+ teardown do
12
+ ActionMailer::Base.deliveries.clear
13
+ end
14
+
15
+ context 'when not signed up' do
16
+
17
+ should 'see "Unknown email"' do
18
+ request_password_reset('unknown@bob.bob')
19
+ assert_match(/Unknown email/, response.body)
20
+ end
21
+
22
+ should 'not send an email' do
23
+ request_password_reset('unknown@bob.bob')
24
+ assert ActionMailer::Base.deliveries.empty?
25
+ end
26
+
27
+ end
28
+
29
+ context 'when signed up' do
30
+
31
+ setup do
32
+ @user = Factory(:user, :email => 'bob@bob.bob')
33
+ end
34
+
35
+ should 'see "instructions for changing your password"' do
36
+ request_password_reset(@user.email)
37
+ assert_match(/instructions for changing your password/, response.body)
38
+ end
39
+
40
+ should 'send a password reset email to the user' do
41
+ request_password_reset(@user.email)
42
+ @user.reload # catch updated confirmation token
43
+ Delayed::Job.work_off
44
+ assert !@user.password_reset_token.blank?
45
+ assert_sent_email do |email|
46
+ email.recipients =~ /#{Regexp.escape @user.email}/i &&
47
+ email.subject =~ /password/i &&
48
+ email.body[:url] =~ /#{Regexp.escape @user.password_reset_token}/
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ context 'After requesting a password reset' do
57
+
58
+ setup do
59
+ ActionMailer::Base.deliveries.clear
60
+ @user = Factory(:user, :email => 'bob@bob.bob')
61
+ end
62
+
63
+ teardown do
64
+ ActionMailer::Base.deliveries.clear
65
+ end
66
+
67
+ context 'with failed password confirmation' do
68
+
69
+ should 'see error messages' do
70
+ request_password_reset('bob@bob.bob')
71
+ @user.reload
72
+ change_password(@user, :password => 'goodpassword', :confirm => 'badpassword')
73
+ assert_match(/Password doesn't match confirmation/, response.body)
74
+ end
75
+
76
+ should 'not be signed in' do
77
+ request_password_reset('bob@bob.bob')
78
+ @user.reload
79
+ change_password(@user, :password => 'goodpassword', :confirm => 'badpassword')
80
+ assert !controller.signed_in?
81
+ end
82
+
83
+ end
84
+
85
+ context 'with valid password and confirmation' do
86
+
87
+ should 'be signed in' do
88
+ request_password_reset('bob@bob.bob')
89
+ @user.reload
90
+ change_password(@user)
91
+ assert controller.signed_in?
92
+ end
93
+
94
+ should 'be able to sign in with new password' do
95
+ request_password_reset('bob@bob.bob')
96
+ @user.reload
97
+ change_password(@user)
98
+ sign_out
99
+ sign_in_as('bob@bob.bob', 'goodpassword')
100
+ assert controller.signed_in?
101
+ end
102
+
103
+ end
104
+
105
+ end
106
+
107
+
108
+ private
109
+
110
+
111
+ def request_password_reset(email)
112
+ visit new_password_url
113
+ fill_in "Email Address", :with => email
114
+ click_button "reset password"
115
+ end
116
+
117
+ def change_password(user, options = {})
118
+ options[:password] ||= 'goodpassword'
119
+ options[:confirm] ||= options[:password]
120
+
121
+ visit edit_user_password_path(:user_id => user,
122
+ :token => user.password_reset_token)
123
+ fill_in "Choose password", :with => options[:password]
124
+ fill_in "Confirm password", :with => options[:confirm]
125
+ click_button "save this password"
126
+ end
127
+
128
+ end
@@ -0,0 +1,66 @@
1
+ require 'test_helper'
2
+
3
+ class SignInTest < ActionController::IntegrationTest
4
+
5
+ context 'Signing in as a User' do
6
+
7
+ context 'who is not in the system' do
8
+
9
+ should 'see a failure message' do
10
+ sign_in_as('someone@somewhere.com', 'password')
11
+ assert_match(/Bad email or password/, response.body)
12
+ end
13
+
14
+ should 'not be signed in' do
15
+ sign_in_as "someone@somewhere.com", 'password'
16
+ assert !controller.signed_in?
17
+ end
18
+
19
+ end
20
+
21
+ context 'when confirmed' do
22
+
23
+ setup do
24
+ @user = Factory(:user, :email => "bob@bob.bob", :password => "password")
25
+ end
26
+
27
+ should 'be signed in' do
28
+ sign_in_as 'bob@bob.bob', 'password'
29
+ assert controller.signed_in?
30
+ end
31
+
32
+ should 'see "Signed In"' do
33
+ sign_in_as 'bob@bob.bob', 'password'
34
+ assert_match %r{Signed in}, @response.body
35
+ end
36
+
37
+ should 'be signed in on subsequent requests' do
38
+ sign_in_as 'bob@bob.bob', 'password'
39
+ reset_session
40
+ visit root_url
41
+ assert controller.signed_in?
42
+ end
43
+
44
+ end
45
+
46
+ context 'when confirmed but with bad credentials' do
47
+
48
+ setup do
49
+ @user = Factory(:user, :email => 'bob@bob.bob', :password => 'password')
50
+ end
51
+
52
+ should 'not be signed in' do
53
+ sign_in_as 'bob@bob.bob', 'badpassword'
54
+ assert !controller.signed_in?
55
+ end
56
+
57
+ should 'see "Bad email or password"' do
58
+ sign_in_as 'bob@bob.bob', 'badpassword'
59
+ assert_match /Bad email or password/, response.body
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,28 @@
1
+ require 'test_helper'
2
+
3
+ class SignOutTest < ActionController::IntegrationTest
4
+
5
+ context 'Signing out as a user' do
6
+
7
+ should 'see "Signed out"' do
8
+ sign_up(:email => 'bob@bob.bob')
9
+ sign_out
10
+ assert_match(/Signed out/, response.body)
11
+ end
12
+
13
+ should 'be signed out' do
14
+ sign_up(:email => 'bob@bob.bob')
15
+ sign_out
16
+ assert !controller.signed_in?
17
+ end
18
+
19
+ should 'be signed out when I return' do
20
+ sign_up(:email => 'bob@bob.bob')
21
+ sign_out
22
+ visit root_url
23
+ assert !controller.signed_in?
24
+ end
25
+
26
+ end
27
+
28
+ end