turboboost 0.0.11 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Appraisals +17 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +6 -44
- data/README.md +36 -22
- data/Rakefile +2 -0
- data/app/assets/javascripts/turboboost.js.coffee +19 -7
- data/gemfiles/rails_3_2.gemfile +13 -0
- data/gemfiles/rails_3_2.gemfile.lock +135 -0
- data/gemfiles/rails_4_0.gemfile +12 -0
- data/gemfiles/rails_4_0.gemfile.lock +116 -0
- data/gemfiles/rails_4_1.gemfile +12 -0
- data/gemfiles/rails_4_1.gemfile.lock +121 -0
- data/gemfiles/rails_4_2.gemfile +13 -0
- data/gemfiles/rails_4_2.gemfile.lock +150 -0
- data/lib/turboboost.rb +2 -159
- data/lib/turboboost/controller.rb +111 -0
- data/lib/turboboost/form_helper.rb +46 -0
- data/lib/turboboost/version.rb +1 -1
- data/test/controller_test.rb +51 -88
- data/test/controllers/items_controller.rb +23 -0
- data/test/controllers/posts_controller.rb +27 -0
- data/test/controllers/users_controller.rb +32 -0
- data/test/locales/en.yml +4 -0
- data/test/test_helper.rb +27 -15
- data/test/view_test.rb +9 -11
- data/test/views/items/show.html.erb +1 -0
- metadata +24 -3
@@ -0,0 +1,111 @@
|
|
1
|
+
module Turboboost
|
2
|
+
module Controller
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
CATCHABLE_ERRORS = {
|
6
|
+
'EOFError' => 500,
|
7
|
+
'Errno::ECONNRESET' => 500,
|
8
|
+
'Errno::EINVAL' => 500,
|
9
|
+
'Timeout::Error' => :request_timeout,
|
10
|
+
'Net::HTTPBadResponse' => 500,
|
11
|
+
'Net::HTTPHeaderSyntaxError' => 500,
|
12
|
+
'Net::ProtocolError' => 500,
|
13
|
+
'ActiveRecord::RecordNotFound' => :not_found,
|
14
|
+
'ActiveRecord::StaleObjectError' => :conflict,
|
15
|
+
'ActiveRecord::RecordInvalid' => :unprocessable_entity,
|
16
|
+
'ActiveRecord::RecordNotSaved' => :unprocessable_entity,
|
17
|
+
'ActiveModel::StrictValidationFailed' => :unprocessable_entity,
|
18
|
+
'ActiveModel::MissingAttributeError' => :unprocessable_entity
|
19
|
+
}
|
20
|
+
|
21
|
+
included do
|
22
|
+
send :rescue_from, *(CATCHABLE_ERRORS.keys), with: :turboboost_error_handler
|
23
|
+
end
|
24
|
+
|
25
|
+
def turboboost_error_handler(error)
|
26
|
+
if request.xhr? && request.headers['HTTP_X_TURBOBOOST']
|
27
|
+
error_status = CATCHABLE_ERRORS[error.class.name]
|
28
|
+
response.headers['X-Turboboosted'] = '1'
|
29
|
+
if defined?(error.record)
|
30
|
+
render_turboboost_errors_for(error.record)
|
31
|
+
else
|
32
|
+
translation = I18n.t("turboboost.errors.#{error.class.name}")
|
33
|
+
message = translation.match('translation missing: (.+)') ? error.class.name : translation
|
34
|
+
render json: [message], status: error_status || 500, root: false
|
35
|
+
end
|
36
|
+
else
|
37
|
+
raise error
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def render_turboboost_errors_for(record)
|
42
|
+
render json: record.errors.full_messages.to_a, status: :unprocessable_entity, root: false
|
43
|
+
end
|
44
|
+
|
45
|
+
def head_turboboost_success(turboboost_flash = {})
|
46
|
+
turboboost_flash = _turboboost_get_flash_messages(turboboost_flash)
|
47
|
+
head :ok, 'X-Flash' => turboboost_flash.to_json, 'X-Turboboosted' => '1'
|
48
|
+
end
|
49
|
+
|
50
|
+
def render(*args, &block)
|
51
|
+
if request.xhr? && request.headers['HTTP_X_TURBOBOOST']
|
52
|
+
turboboost_render(*args, &block)
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def turboboost_render(*args, &block)
|
59
|
+
options = _normalize_render(*args, &block)
|
60
|
+
[:replace, :within, :append, :prepend, :before, :after].each do |h|
|
61
|
+
response.headers["X-#{h.capitalize}"] = options[h] if options[h]
|
62
|
+
end
|
63
|
+
response.headers['X-Flash'] = _turboboost_get_flash_messages(options).to_json
|
64
|
+
response.headers['X-Turboboosted'] = '1'
|
65
|
+
self.response_body = render_to_body(options)
|
66
|
+
end
|
67
|
+
|
68
|
+
def redirect_to(options = {}, response_status_and_flash = {})
|
69
|
+
if request.xhr? && request.headers['HTTP_X_TURBOBOOST']
|
70
|
+
turboboost_redirect_to(options, response_status_and_flash)
|
71
|
+
else
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def turboboost_redirect_to(options = {}, response_status_and_flash = {})
|
77
|
+
raise ActionControllerError.new('Cannot redirect to nil!') unless options
|
78
|
+
raise AbstractController::DoubleRenderError if response_body
|
79
|
+
|
80
|
+
# set flash for turbo redirect headers
|
81
|
+
turboboost_flash = _turboboost_get_flash_messages(response_status_and_flash)
|
82
|
+
|
83
|
+
if Rails.version < '4.2'
|
84
|
+
self.location = _compute_redirect_to_location(options)
|
85
|
+
else
|
86
|
+
self.location = _compute_redirect_to_location(request, options)
|
87
|
+
end
|
88
|
+
|
89
|
+
head :ok, 'X-Flash' => turboboost_flash.to_json
|
90
|
+
|
91
|
+
flash.update(turboboost_flash) # set flash for rendered view
|
92
|
+
end
|
93
|
+
|
94
|
+
def _turboboost_get_flash_messages(response_status_and_flash = {})
|
95
|
+
turboboost_flash = {}
|
96
|
+
flash_types = defined?(self.class._flash_types) ? self.class._flash_types : [:alert, :notice]
|
97
|
+
flash_types.each do |flash_type|
|
98
|
+
if type = flash.delete(type)
|
99
|
+
turboboost_flash.update(type)
|
100
|
+
end
|
101
|
+
if type = response_status_and_flash.delete(flash_type)
|
102
|
+
turboboost_flash[flash_type] = type
|
103
|
+
end
|
104
|
+
end
|
105
|
+
if other_flashes = response_status_and_flash.delete(:flash)
|
106
|
+
turboboost_flash.update(other_flashes)
|
107
|
+
end
|
108
|
+
turboboost_flash
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Turboboost
|
2
|
+
# borrowed from - https://github.com/fs/turboforms/blob/master/lib/turboforms.rb
|
3
|
+
module FormHelper
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
alias_method_chain :form_for, :data_turboboost
|
8
|
+
alias_method_chain :form_tag, :data_turboboost
|
9
|
+
end
|
10
|
+
|
11
|
+
def form_for_with_data_turboboost(record_or_name_or_array, *args, &proc)
|
12
|
+
options = args.extract_options!
|
13
|
+
|
14
|
+
if options.key?(:turboboost) && options.delete(:turboboost)
|
15
|
+
options[:html] ||= {}
|
16
|
+
options[:html]['data-turboboost'] = true
|
17
|
+
options[:remote] = true
|
18
|
+
end
|
19
|
+
|
20
|
+
form_for_without_data_turboboost(record_or_name_or_array, *(args << options), &proc)
|
21
|
+
end
|
22
|
+
|
23
|
+
def form_tag_with_data_turboboost(record_or_name_or_array, *args, &proc)
|
24
|
+
options = args.extract_options!
|
25
|
+
|
26
|
+
if options.key?(:turboboost) && options.delete(:turboboost)
|
27
|
+
options[:data] ||= {}
|
28
|
+
options[:data]['turboboost'] = true
|
29
|
+
options[:remote] = true
|
30
|
+
end
|
31
|
+
|
32
|
+
form_tag_without_data_turboboost(record_or_name_or_array, *(args << options), &proc)
|
33
|
+
end
|
34
|
+
|
35
|
+
def convert_options_to_data_attributes(options, html_options)
|
36
|
+
if html_options
|
37
|
+
html_options = html_options.stringify_keys
|
38
|
+
if html_options.delete('turboboost')
|
39
|
+
html_options['data-remote'] = 'true'
|
40
|
+
html_options['data-turboboost'] = 'true'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
super options, html_options
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/turboboost/version.rb
CHANGED
data/test/controller_test.rb
CHANGED
@@ -1,124 +1,87 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
|
3
|
-
class PostsController < ApplicationController
|
4
|
-
|
5
|
-
respond_to :html
|
6
|
-
respond_to :js, only: [:create, :update, :destroy]
|
7
|
-
|
8
|
-
def create
|
9
|
-
post = Post.create!(post_params)
|
10
|
-
redirect_to posts_url, notice: "Post was successfully created."
|
11
|
-
end
|
12
|
-
|
13
|
-
def update
|
14
|
-
post = Post.find(params[:id])
|
15
|
-
post.update_attributes!(post_params)
|
16
|
-
redirect_to post_url(post), notice: "Post was successfully updated."
|
17
|
-
end
|
18
|
-
|
19
|
-
def destroy
|
20
|
-
post = Post.find(params[:id])
|
21
|
-
post.destroy!
|
22
|
-
redirect_to posts_url
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def post_params
|
28
|
-
params.require(:post).permit(:title, :user_id)
|
29
|
-
end
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
class UsersController < ApplicationController
|
34
|
-
|
35
|
-
def create
|
36
|
-
user = User.new(user_params)
|
37
|
-
if user.save
|
38
|
-
flash[:notice] = "User was successfully created."
|
39
|
-
redirect_to user_url(user)
|
40
|
-
else
|
41
|
-
respond_to do |format|
|
42
|
-
format.html { render :new }
|
43
|
-
format.js { render_turboboost_errors_for(user) }
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def update
|
49
|
-
user = User.find(params[:id])
|
50
|
-
if user.update_attributes(user_params)
|
51
|
-
redirect_to user_url(user)
|
52
|
-
else
|
53
|
-
respond_to do |format|
|
54
|
-
format.html { redirect_to users_url }
|
55
|
-
format.js { render_turboboost_errors_for(user) }
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
private
|
61
|
-
|
62
|
-
def user_params
|
63
|
-
params.require(:user).permit(:name, :email)
|
64
|
-
end
|
65
|
-
|
66
|
-
end
|
2
|
+
Dir[File.dirname(__FILE__) + '/controllers/*.rb'].each { |file| require file }
|
67
3
|
|
68
4
|
class PostsControllerTest < ActionController::TestCase
|
69
|
-
|
70
5
|
tests PostsController
|
71
6
|
|
72
7
|
setup do
|
73
|
-
@request.headers[
|
8
|
+
@request.headers['X-Turboboost'] = '1'
|
74
9
|
end
|
75
10
|
|
76
|
-
test
|
77
|
-
xhr :post, :create, post: { title:
|
11
|
+
test 'On a successful turboboost request, return an empty response with headers containing the redirect location and flash message' do
|
12
|
+
xhr :post, :create, post: { title: 'Foobar', user_id: '1' }
|
78
13
|
|
79
14
|
assert @response.body.strip.blank?
|
80
15
|
assert_equal flash[:notice], 'Post was successfully created.'
|
81
|
-
assert_equal @response.headers[
|
82
|
-
assert_equal JSON.parse(@response.headers[
|
16
|
+
assert_equal @response.headers['Location'], posts_url
|
17
|
+
assert_equal JSON.parse(@response.headers['X-Flash'])['notice'], 'Post was successfully created.'
|
83
18
|
end
|
84
19
|
|
85
|
-
test
|
86
|
-
xhr :post, :create, post: { title:
|
20
|
+
test 'On an unsuccessful turboboost request, catch and return the error message(s) as an array' do
|
21
|
+
xhr :post, :create, post: { title: 'Title', user_id: nil }
|
87
22
|
|
88
23
|
assert_equal @response.status, 422
|
89
|
-
assert_equal @response.body.strip, [
|
24
|
+
assert_equal @response.body.strip, ['User can\'t be blank'].to_json
|
90
25
|
|
91
|
-
xhr :post, :create, post: { title:
|
26
|
+
xhr :post, :create, post: { title: 'Tit', user_id: nil }
|
92
27
|
|
93
28
|
assert_equal @response.status, 422
|
94
|
-
assert_equal @response.body.strip, [
|
29
|
+
assert_equal @response.body.strip, ['Title is too short.', "User can't be blank"].to_json
|
95
30
|
end
|
96
|
-
|
97
31
|
end
|
98
32
|
|
99
|
-
|
100
33
|
class UsersControllerTest < ActionController::TestCase
|
101
|
-
|
102
34
|
tests UsersController
|
103
35
|
|
104
36
|
setup do
|
105
|
-
@request.headers[
|
37
|
+
@request.headers['X-Turboboost'] = '1'
|
106
38
|
end
|
107
39
|
|
108
|
-
test
|
109
|
-
xhr :post, :create, user: { name:
|
40
|
+
test 'On a successful turboboost request, return an empty response with headers containing the redirect location and flash message' do
|
41
|
+
xhr :post, :create, user: { name: 'Mike', email: 'mike@mike.com' }
|
110
42
|
|
111
|
-
|
43
|
+
assert_equal @response.body.strip.blank?, true
|
112
44
|
assert_equal flash[:notice], 'User was successfully created.'
|
113
|
-
assert_equal @response.headers[
|
114
|
-
assert_equal JSON.parse(@response.headers[
|
45
|
+
assert_equal @response.headers['Location'], user_url(1)
|
46
|
+
assert_equal JSON.parse(@response.headers['X-Flash'])['notice'], 'User was successfully created.'
|
115
47
|
end
|
116
48
|
|
117
|
-
test
|
118
|
-
xhr :post, :create, user: { name:
|
49
|
+
test 'On an unsuccessful turboboost request, explicitly render the error message(s)' do
|
50
|
+
xhr :post, :create, user: { name: 'Mike', email: 'mike at mike.com' }
|
119
51
|
|
120
52
|
assert_equal @response.status, 422
|
121
|
-
assert_equal @response.body.strip, [
|
53
|
+
assert_equal @response.body.strip, ['Email is invalid'].to_json
|
122
54
|
end
|
55
|
+
end
|
123
56
|
|
57
|
+
class ItemsControllerTest < ActionController::TestCase
|
58
|
+
tests ItemsController
|
59
|
+
|
60
|
+
setup do
|
61
|
+
@request.headers['X-Turboboost'] = '1'
|
62
|
+
end
|
63
|
+
|
64
|
+
test 'On a failed turboboost get request, return custom internationalization messaging' do
|
65
|
+
xhr :get, :show, id: 123
|
66
|
+
i18n_message = I18n.t("turboboost.errors.ActiveRecord::RecordNotFound")
|
67
|
+
assert_equal @response.body.strip, [i18n_message].to_json
|
68
|
+
end
|
69
|
+
|
70
|
+
test 'On a successful turboboost post request, return rendering options in the headers' do
|
71
|
+
xhr :post, :create, item: { name: 'Bottle' }
|
72
|
+
|
73
|
+
assert_equal @response.headers['Location'], nil
|
74
|
+
assert_equal JSON.parse(@response.headers['X-Flash'])['notice'], 'Item was successfully created.'
|
75
|
+
assert_equal @response.headers['X-Within'], '#sidebar'
|
76
|
+
assert_equal @response.body.strip, "<div id=\"item\">Bottle</div>"
|
77
|
+
end
|
78
|
+
|
79
|
+
test 'On a successful turboboost update using render nothing: true, still return flash headers' do
|
80
|
+
@item = Item.create(name: 'Opener')
|
81
|
+
xhr :put, :update, id: @item.id, item: { name: 'Bottle Opener' }
|
82
|
+
|
83
|
+
assert_equal @response.headers['Location'], nil
|
84
|
+
assert_equal @response.body.strip, ''
|
85
|
+
assert_equal JSON.parse(@response.headers['X-Flash'])['notice'], 'Item updated.'
|
86
|
+
end
|
124
87
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class ItemsController < ApplicationController
|
2
|
+
def show
|
3
|
+
item = Item.find(params[:id])
|
4
|
+
render json: item
|
5
|
+
end
|
6
|
+
|
7
|
+
def create
|
8
|
+
@item = Item.create!(item_params)
|
9
|
+
render :show, within: '#sidebar', flash: { notice: 'Item was successfully created.' }
|
10
|
+
end
|
11
|
+
|
12
|
+
def update
|
13
|
+
item = Item.find(params[:id])
|
14
|
+
item.update_attributes!(item_params)
|
15
|
+
render nothing: true, notice: 'Item updated.'
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def item_params
|
21
|
+
params.require(:item).permit(:name)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class PostsController < ApplicationController
|
2
|
+
respond_to :html
|
3
|
+
respond_to :js, only: [:create, :update, :destroy]
|
4
|
+
|
5
|
+
def create
|
6
|
+
Post.create!(post_params)
|
7
|
+
redirect_to posts_url, notice: 'Post was successfully created.'
|
8
|
+
end
|
9
|
+
|
10
|
+
def update
|
11
|
+
post = Post.find(params[:id])
|
12
|
+
post.update_attributes!(post_params)
|
13
|
+
redirect_to post_url(post), notice: 'Post was successfully updated.'
|
14
|
+
end
|
15
|
+
|
16
|
+
def destroy
|
17
|
+
post = Post.find(params[:id])
|
18
|
+
post.destroy!
|
19
|
+
redirect_to posts_url
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def post_params
|
25
|
+
params.require(:post).permit(:title, :user_id)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class UsersController < ApplicationController
|
2
|
+
def create
|
3
|
+
user = User.new(user_params)
|
4
|
+
if user.save
|
5
|
+
flash[:notice] = 'User was successfully created.'
|
6
|
+
redirect_to user_url(user)
|
7
|
+
else
|
8
|
+
respond_to do |format|
|
9
|
+
format.html { render :new }
|
10
|
+
format.js { render_turboboost_errors_for(user) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def update
|
16
|
+
user = User.find(params[:id])
|
17
|
+
if user.update_attributes(user_params)
|
18
|
+
redirect_to user_url(user)
|
19
|
+
else
|
20
|
+
respond_to do |format|
|
21
|
+
format.html { redirect_to users_url }
|
22
|
+
format.js { render_turboboost_errors_for(user) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def user_params
|
30
|
+
params.require(:user).permit(:name, :email)
|
31
|
+
end
|
32
|
+
end
|
data/test/locales/en.yml
ADDED
data/test/test_helper.rb
CHANGED
@@ -2,31 +2,39 @@ require 'rubygems'
|
|
2
2
|
require 'bundler'
|
3
3
|
Bundler.setup
|
4
4
|
|
5
|
-
ENV[
|
5
|
+
ENV['RAILS_ENV'] = 'test'
|
6
6
|
|
7
7
|
require 'rails'
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
8
|
+
require 'active_record'
|
9
|
+
require 'action_controller'
|
10
|
+
require 'action_controller'
|
11
|
+
require 'action_controller/railtie'
|
12
12
|
require 'rails/test_help'
|
13
13
|
require 'awesome_print'
|
14
|
+
require 'responders' if Rails.version >= '4.2'
|
15
|
+
require 'strong_parameters' if Rails.version < '4.0'
|
14
16
|
require 'turboboost'
|
15
17
|
|
16
|
-
|
18
|
+
I18n.enforce_available_locales = true
|
19
|
+
I18n.load_path << File.expand_path('../locales/en.yml', __FILE__)
|
20
|
+
I18n.reload!
|
21
|
+
|
22
|
+
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
|
17
23
|
|
18
24
|
class TestApp < Rails::Application; end
|
19
25
|
Rails.application = TestApp
|
20
|
-
Rails.configuration.secret_key_base =
|
26
|
+
Rails.configuration.secret_key_base = 'abc123'
|
21
27
|
|
22
28
|
Turboboost::Routes = ActionDispatch::Routing::RouteSet.new
|
23
29
|
Turboboost::Routes.draw do
|
24
30
|
resources 'posts'
|
25
31
|
resources 'users'
|
32
|
+
resources 'items'
|
26
33
|
end
|
27
34
|
|
28
35
|
class ApplicationController < ActionController::Base
|
29
36
|
include Turboboost::Routes.url_helpers
|
37
|
+
self.view_paths = File.join(File.dirname(__FILE__), 'views')
|
30
38
|
end
|
31
39
|
|
32
40
|
class ActiveSupport::TestCase
|
@@ -36,38 +44,42 @@ class ActiveSupport::TestCase
|
|
36
44
|
end
|
37
45
|
|
38
46
|
class ActionView::TestCase
|
39
|
-
|
40
|
-
def default_url_options; {}; end
|
41
|
-
|
42
47
|
include Turboboost::Routes.url_helpers
|
43
48
|
include Turboboost::FormHelper
|
49
|
+
def default_url_options
|
50
|
+
{}
|
51
|
+
end
|
44
52
|
|
45
53
|
setup do
|
46
54
|
@controller = ApplicationController
|
47
55
|
end
|
48
|
-
|
49
56
|
end
|
50
57
|
|
51
58
|
posts_table = %{CREATE TABLE posts (id INTEGER PRIMARY KEY, title VARCHAR(5), user_id INTEGER);}
|
52
59
|
ActiveRecord::Base.connection.execute(posts_table)
|
53
60
|
|
54
61
|
class Post < ActiveRecord::Base
|
55
|
-
|
56
62
|
attr_accessor :title, :user_id
|
57
63
|
|
58
|
-
validates :title, length: { minimum: 5, message:
|
64
|
+
validates :title, length: { minimum: 5, message: 'is too short.' }
|
59
65
|
validates :user_id, presence: true
|
60
|
-
|
61
66
|
end
|
62
67
|
|
63
68
|
users_table = %{CREATE TABLE users (id INTEGER PRIMARY KEY, name VARCHAR(5), email VARCHAR(255));}
|
64
69
|
ActiveRecord::Base.connection.execute(users_table)
|
65
70
|
|
66
71
|
class User < ActiveRecord::Base
|
67
|
-
|
68
72
|
attr_accessor :name, :email
|
69
73
|
|
70
74
|
validates :name, presence: true
|
71
75
|
validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, allow_blank: false }
|
76
|
+
end
|
72
77
|
|
78
|
+
items_table = %{CREATE TABLE items (id INTEGER PRIMARY KEY, name VARCHAR(5));}
|
79
|
+
ActiveRecord::Base.connection.execute(items_table)
|
80
|
+
|
81
|
+
class Item < ActiveRecord::Base
|
82
|
+
attr_accessor :name
|
83
|
+
|
84
|
+
validates :name, presence: true
|
73
85
|
end
|