aeonscope-rest 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +3 -0
- data/LICENSE.rdoc +20 -0
- data/README.rdoc +172 -0
- data/Rakefile +52 -0
- data/VERSION.yml +4 -0
- data/lib/actions.rb +230 -0
- data/lib/class_methods.rb +35 -0
- data/lib/resource_helper.rb +63 -0
- data/lib/rest.rb +12 -0
- data/rails_generators/ujs_setup/USAGE +11 -0
- data/rails_generators/ujs_setup/templates/README +14 -0
- data/rails_generators/ujs_setup/templates/app/controllers/javascripts_controller.rb +6 -0
- data/rails_generators/ujs_setup/templates/app/views/javascripts/ujs.js.erb +18 -0
- data/rails_generators/ujs_setup/templates/public/javascripts/rest.js +119 -0
- data/rails_generators/ujs_setup/ujs_setup_generator.rb +18 -0
- data/spec/rest_spec.rb +6 -0
- data/spec/spec_helper.rb +9 -0
- metadata +91 -0
data/CHANGELOG.rdoc
ADDED
data/LICENSE.rdoc
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Brooke Kuhlmann of {Berserk Technologies}[http://www.berserktech.com]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
= Overview
|
2
|
+
|
3
|
+
Enables default REST functionality, more than what you get with Rails out-of-the-box, and keeps your code DRY. This means you can write code like this:
|
4
|
+
|
5
|
+
class Posts < ApplicationController
|
6
|
+
include Rest
|
7
|
+
end
|
8
|
+
|
9
|
+
Which would automatically yield the following REST actions:
|
10
|
+
|
11
|
+
* index
|
12
|
+
* show
|
13
|
+
* new
|
14
|
+
* edit
|
15
|
+
* create
|
16
|
+
* update
|
17
|
+
* destroy
|
18
|
+
|
19
|
+
How is that for being {DRY}[http://en.wikipedia.org/wiki/DRY]? Read on to learn more.
|
20
|
+
|
21
|
+
= License
|
22
|
+
|
23
|
+
Copyright (c) 2008-2009 Brooke Kuhlmann of {Berserk Technologies}[http://www.berserktech.com].
|
24
|
+
See the included LICENSE for more info.
|
25
|
+
|
26
|
+
= History
|
27
|
+
|
28
|
+
See the CHANGELOG file for more info.
|
29
|
+
|
30
|
+
= Requirements
|
31
|
+
|
32
|
+
1. {Ruby on Rails}[http://rubyonrails.org] (automatically installed for you if you don't have 2.3.x or higher).
|
33
|
+
2. mislav-will_paginate[http://github.com/mislav/will_paginate/tree/master] gem (automatically installed for you).
|
34
|
+
3. Knowledge of the {Representational State Transfer (REST)}[http://en.wikipedia.com/wiki/REST]. Download and read {RESTful Rails}[http://www.b-simple.de/documents] if you need further info.
|
35
|
+
|
36
|
+
= Installation
|
37
|
+
|
38
|
+
Type the following from the command line to install:
|
39
|
+
|
40
|
+
* *UNIX*: sudo gem install aeonscope-rest
|
41
|
+
* *Windows*: gem install aeonscope-rest
|
42
|
+
|
43
|
+
Update your environment.rb file to include the new gem:
|
44
|
+
|
45
|
+
* config.gem "rest"
|
46
|
+
|
47
|
+
To apply unobtrusive jQuery support, run the following generator (TIP: suffix the command line with -h option for usage):
|
48
|
+
|
49
|
+
* script/generate ujs_setup
|
50
|
+
|
51
|
+
= Usage
|
52
|
+
|
53
|
+
As mentioned in the overview, simply add the following line of code to your controller(s) to make them RESTful:
|
54
|
+
|
55
|
+
include Rest
|
56
|
+
|
57
|
+
Example:
|
58
|
+
|
59
|
+
class Posts < ApplicationController
|
60
|
+
include Rest
|
61
|
+
end
|
62
|
+
|
63
|
+
This will automatically create the seven REST actions (index, show, new, create, edit, update, and destroy) for your controller. The model (i.e. Post) and model instance (i.e. @posts or @post depending on the action) are automatically determined from the controller name and created for you as well which means you can immediately write the following code in your views:
|
64
|
+
|
65
|
+
<b>index.html.erb</b>
|
66
|
+
|
67
|
+
<h2>Posts</h2>
|
68
|
+
<div>
|
69
|
+
<table>
|
70
|
+
<thead>
|
71
|
+
<tr>
|
72
|
+
<th>Label</th>
|
73
|
+
<th>Created At</th>
|
74
|
+
<th>Updated At</th>
|
75
|
+
<th>Actions</th>
|
76
|
+
</tr>
|
77
|
+
</thead>
|
78
|
+
<tbody>
|
79
|
+
<%= render @posts %>
|
80
|
+
</tbody>
|
81
|
+
</table>
|
82
|
+
</div>
|
83
|
+
|
84
|
+
<b>show.html.erb</b>
|
85
|
+
|
86
|
+
<h2>Label</h2>
|
87
|
+
<p><%= @post.label %></p>
|
88
|
+
|
89
|
+
<h2>Content</h2>
|
90
|
+
<p><%= @post.content %></p>
|
91
|
+
|
92
|
+
<p><%= link_to "Back", :back %></p>
|
93
|
+
|
94
|
+
<b>new.html.erb</b>
|
95
|
+
|
96
|
+
<% form_for @post do |form| %>
|
97
|
+
<%= form.error_messages :header_data => :strong, :header_message => "Error" %>
|
98
|
+
|
99
|
+
<div>
|
100
|
+
<%= form.label :label %><br/>
|
101
|
+
<%= form.text_field :label %>
|
102
|
+
</div>
|
103
|
+
|
104
|
+
<div>
|
105
|
+
<%= form.label :content %><br/>
|
106
|
+
<%= form.text_area :content %>
|
107
|
+
</div>
|
108
|
+
|
109
|
+
<div><%= show_submit_and_cancel :cancel_options => posts_path %></div>
|
110
|
+
<% end %>
|
111
|
+
|
112
|
+
<b>edit.html.erb</b>
|
113
|
+
|
114
|
+
Use the same code as shown in the new.html.erb view above. ;-) The Rails form_for helper will automatically determine what action to take as shown in the following code:
|
115
|
+
|
116
|
+
<% form_for @post do |form| %>
|
117
|
+
|
118
|
+
To customize the RESTful behavior of your controller, use any combination of these three macros:
|
119
|
+
|
120
|
+
* *belongs_to* - Enables resource nesting where a controller can belong to a parent controller. This behavior is similar to the ActiveRecord {belongs_to}[http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/belongs_to] macro.
|
121
|
+
* *resource_options* - Allows you to customize the default behavior of your controller(s). There is a lot you can do with this, read the code documentation for more info.
|
122
|
+
* *disabled_actions* - Allows you to disable any of the default REST actions. Follow the link to learn more.
|
123
|
+
|
124
|
+
Example:
|
125
|
+
|
126
|
+
class Comments < ApplicationController
|
127
|
+
include Rest
|
128
|
+
belongs_to :posts
|
129
|
+
resource_options :label => "My Wicked Comments"
|
130
|
+
disabled_actions :show, :destroy
|
131
|
+
end
|
132
|
+
|
133
|
+
Here is the breakdown, line-by-line, of the example shown above:
|
134
|
+
|
135
|
+
1. Enables restful behavior.
|
136
|
+
2. Identifies the controller as a nested resource of posts.
|
137
|
+
3. Instead of using the default label "Comments", a customized label of "My Wicked Comments" is used instead.
|
138
|
+
4. The "show" and "destroy" actions are disabled which means only the following actions will work: index, new, edit, create, and update.
|
139
|
+
|
140
|
+
Using the post and comment controller relationship as defined above, we can break this relationship down even further. The post (parent) resource would have the following values (in this case, all default values):
|
141
|
+
|
142
|
+
* *parent_key* = N/A
|
143
|
+
* *parent_value* = N/A
|
144
|
+
* *parent_resource_method* = N/A
|
145
|
+
* *name* = "posts"
|
146
|
+
* *label* = "Posts"
|
147
|
+
* *controller* = PostsController
|
148
|
+
* *model* = Post
|
149
|
+
* *record* = #<Post id: 1, label: "Test", content: "Test", created_at: "2008-10-31 23:59:28", updated_at: "2008-10-31 23:59:28">
|
150
|
+
* *namespaces* = []
|
151
|
+
* *show_partial* = "/posts/show"
|
152
|
+
* *new_or_edit_partial* = "/posts/new_or_edit"
|
153
|
+
|
154
|
+
The comment (child) resource would have the following values:
|
155
|
+
|
156
|
+
* *parent_key* = post_id
|
157
|
+
* *parent_value* = 1
|
158
|
+
* *parent_resource_method* = N/A
|
159
|
+
* *name* = "comments"
|
160
|
+
* *label* = "My Wicked Comments"
|
161
|
+
* *controller* = CommentsController
|
162
|
+
* *model* = Comment
|
163
|
+
* *record* = #<Post id: 1, post_id: nil, label: "Test", content: "Test", created_at: "2008-10-31 23:59:28", updated_at: "2008-10-31 23:59:28">
|
164
|
+
* *namespaces* = []
|
165
|
+
* *show_partial* = "/comments/show"
|
166
|
+
* *new_or_edit_partial* = "/comments/new_or_edit"
|
167
|
+
|
168
|
+
= Contact/Feedback/Issues
|
169
|
+
|
170
|
+
* {Berserk Technologies}[http://www.berserktech.com] - Company web site.
|
171
|
+
* Aeonscope[http://www.aeonscope.net] - Personal web site.
|
172
|
+
* Twitter[http://www.twitter.com/Aeonscope] - Short bursts of insight and/or noise.
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "rest"
|
8
|
+
gem.summary = "Enables default REST functionality, more than what you get with Rails out-of-the-box, and keeps your code DRY."
|
9
|
+
gem.description = "Ruby on Rails supports RESTful routing out-of-the-box. However, as you move along, you will often find yourself repeating the code for the seven REST actions: index, show, new, create, edit, update, destroy. This gem allows you to get all of the code for free by simply typing 'include Rest' at top of your controller code. That's it! Even better, you can have nested resources as well by adding a 'belongs_to' statement to your controllers much like you would for ActiveRecord models. This is just the tip of the iceburg so make sure to read the RDoc for more info."
|
10
|
+
gem.authors = ["Brooke Kuhlmann"]
|
11
|
+
gem.email = "aeonscope@gmail.com"
|
12
|
+
gem.homepage = "http://github.com/aeonscope/rest"
|
13
|
+
gem.required_ruby_version = ">= 1.8.6"
|
14
|
+
gem.add_dependency "rails", ">= 2.3.2"
|
15
|
+
gem.add_dependency "mislav-will_paginate", ">= 2.3.7"
|
16
|
+
gem.rdoc_options << "CHANGELOG.rdoc"
|
17
|
+
gem.files = FileList["[A-Z]*", "{bin,lib,generators,rails_generators,test,spec}/**/*"]
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'spec/rake/spectask'
|
24
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
27
|
+
end
|
28
|
+
|
29
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
30
|
+
spec.libs << 'lib' << 'spec'
|
31
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
32
|
+
spec.rcov = true
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
task :default => :spec
|
37
|
+
|
38
|
+
require 'rake/rdoctask'
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
if File.exist?('VERSION.yml')
|
41
|
+
config = YAML.load(File.read('VERSION.yml'))
|
42
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
43
|
+
else
|
44
|
+
version = ""
|
45
|
+
end
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "rest #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
52
|
+
|
data/VERSION.yml
ADDED
data/lib/actions.rb
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
module Rest
|
2
|
+
module Actions
|
3
|
+
# Default index action. Feel free to override.
|
4
|
+
def index
|
5
|
+
build_resources
|
6
|
+
if @resources.size > 1
|
7
|
+
# Records for a nested resource (act on the second-to-last parent).
|
8
|
+
parent = @resources[@resources.size - 2][:record]
|
9
|
+
records_name = parent.class.name.underscore.pluralize
|
10
|
+
records = parent.instance_eval("#{@resources.last[:parent_resource_method] || @resources.last[:name]}").paginate :page => params[:page], :per_page => 10
|
11
|
+
else
|
12
|
+
# Records for single resource.
|
13
|
+
records_name = get_model_name
|
14
|
+
if records_name
|
15
|
+
records_name = records_name.pluralize
|
16
|
+
records = @resources.last[:model].all.paginate(:page => params[:page], :per_page => 10)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
instance_variable_set "@#{records_name}", records if records_name
|
20
|
+
end
|
21
|
+
|
22
|
+
# Default show action. Feel free to override.
|
23
|
+
def show
|
24
|
+
build_resources
|
25
|
+
render_action_partial @resources.last[:show_partial]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Default new action. Feel free to override.
|
29
|
+
def new
|
30
|
+
build_resources
|
31
|
+
render_new_or_edit_partial
|
32
|
+
end
|
33
|
+
|
34
|
+
# Default edit action. Feel free to override.
|
35
|
+
def edit
|
36
|
+
build_resources
|
37
|
+
render_new_or_edit_partial
|
38
|
+
end
|
39
|
+
|
40
|
+
# Default create action. Feel free to override.
|
41
|
+
def create
|
42
|
+
build_resources
|
43
|
+
if get_record.update_attributes params[get_model_symbol]
|
44
|
+
redirect_to build_resource_url(@resources)
|
45
|
+
else
|
46
|
+
render_new_or_edit_partial
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Default update action. Feel free to override.
|
51
|
+
def update
|
52
|
+
build_resources
|
53
|
+
if get_record.update_attributes params[get_model_symbol]
|
54
|
+
redirect_to build_resource_url(@resources)
|
55
|
+
else
|
56
|
+
render_new_or_edit_partial
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Default destroy action. Feel free to override.
|
61
|
+
def destroy
|
62
|
+
build_resources
|
63
|
+
get_record.destroy
|
64
|
+
@resources.last.delete :record
|
65
|
+
redirect_to build_resource_url(@resources)
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
# Convenience method for rendering the action partials.
|
71
|
+
def render_action_partial partial
|
72
|
+
symbol = get_model_symbol
|
73
|
+
record = get_record
|
74
|
+
if symbol && record
|
75
|
+
render :partial => partial, :layout => true, :locals => {symbol => record, :resources => @resources}
|
76
|
+
else
|
77
|
+
render :partial => partial, :layout => true, :locals => {:resources => @resources}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Convenience method for rendering the new or edit partial.
|
82
|
+
def render_new_or_edit_partial
|
83
|
+
render_action_partial @resources.last[:new_or_edit_partial]
|
84
|
+
end
|
85
|
+
|
86
|
+
# Builds the RESTful parent URL based on an array of resources.
|
87
|
+
def build_parent_url resources = []
|
88
|
+
namespaces = resources.last[:namespaces]
|
89
|
+
url = namespaces && !namespaces.empty? ? '/' + resources.last[:namespaces].join('/') : nil
|
90
|
+
if resources.size > 1
|
91
|
+
resources.slice(0...resources.size - 1).each do |resource|
|
92
|
+
url = [url, resource[:name], resource[:parent_id]].join('/')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
url
|
96
|
+
end
|
97
|
+
|
98
|
+
# A convenience method for builing RESTful URLs based on an array of resources with the
|
99
|
+
# option to override the default action. This is also defined as a helper method (see base.rb).
|
100
|
+
def build_resource_url resources = [], action = :index
|
101
|
+
name = resources.last[:name]
|
102
|
+
id = resources.last[:record].id if resources.last[:record]
|
103
|
+
parent_url = build_parent_url(resources)
|
104
|
+
url = parent_url ? [parent_url, name].join('/') : '/' + name
|
105
|
+
case action
|
106
|
+
when :show then url = [url, id].compact.join('/')
|
107
|
+
when :new then url = [url, "new"].compact.join('/')
|
108
|
+
when :edit then url = [url, id, "edit"].compact.join('/')
|
109
|
+
when :update then url = [url, id].compact.join('/') if name == name.pluralize
|
110
|
+
when :destroy then url = [url, id].compact.join('/') if name == name.pluralize
|
111
|
+
end
|
112
|
+
logger.debug "Resource URL: #{url}"
|
113
|
+
url
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# Answers the name of the current model.
|
119
|
+
def get_model_name
|
120
|
+
model = @resources.last[:model]
|
121
|
+
model ? model.name.underscore : nil
|
122
|
+
end
|
123
|
+
|
124
|
+
# Answers the symbol of the current model.
|
125
|
+
def get_model_symbol
|
126
|
+
name = get_model_name
|
127
|
+
name ? name.to_sym : nil
|
128
|
+
end
|
129
|
+
|
130
|
+
# Answers the current record (a.k.a. the record of the last resource).
|
131
|
+
def get_record
|
132
|
+
name = get_model_name
|
133
|
+
name ? instance_variable_get("@#{name}") : nil
|
134
|
+
end
|
135
|
+
|
136
|
+
# Builds all resources for the controller(s).
|
137
|
+
def build_resources
|
138
|
+
@resources ||= []
|
139
|
+
if @resources.empty?
|
140
|
+
controller_name = self.class.name
|
141
|
+
add_resource controller_name, @resources
|
142
|
+
@resources.reverse!
|
143
|
+
end
|
144
|
+
# Convenience for accessing the current record.
|
145
|
+
name = get_model_name
|
146
|
+
instance_variable_set "@#{name}", @resources.last[:record] if name
|
147
|
+
end
|
148
|
+
|
149
|
+
# Convenience method for answering back a properly camelized controller name.
|
150
|
+
def camelize_controller_name name
|
151
|
+
name = name.gsub(/^(\w)/) {|c| c.capitalize}
|
152
|
+
name += "Controller" unless name.include? "Controller"
|
153
|
+
name
|
154
|
+
end
|
155
|
+
|
156
|
+
# Answers the child or parent controller namespaces (array) and name (string).
|
157
|
+
# Accepts the following argruments:
|
158
|
+
# * *full_name* - The fully namespaced controller (i.e. PostsController) or parent name (i.e. protected_pages).
|
159
|
+
# Usage:
|
160
|
+
# * Input: Public::PostsController, Output: [Public], "PostsController"
|
161
|
+
# * Input: member_manage_posts, Output: [Member, Manage], "PostsController"
|
162
|
+
# * Input: posts, Output: nil, "PostsController"
|
163
|
+
def get_controller_namespaces_and_name full_name
|
164
|
+
if full_name
|
165
|
+
delimiter = full_name.include?("Controller") ? "::" : '_'
|
166
|
+
if full_name.include? delimiter
|
167
|
+
namespaces = full_name.split delimiter
|
168
|
+
name = camelize_controller_name namespaces.pop
|
169
|
+
return namespaces.collect(&:capitalize), name
|
170
|
+
else
|
171
|
+
return nil, camelize_controller_name(full_name)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Recursively walks the controller belongs_to hierarchy (if any) adding new resources along the way.
|
177
|
+
def add_resource controller_name, resources
|
178
|
+
logger.debug "Adding resource '#{controller_name}' to resources (size = #{resources.size})."
|
179
|
+
resource = configure_resource controller_name
|
180
|
+
if resource
|
181
|
+
resources << resource
|
182
|
+
# Recursively add parents (if any).
|
183
|
+
if resource[:controller].methods.include? "parent_resource"
|
184
|
+
add_resource resource[:controller].parent_resource, resources
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Configures a resource via belongs_to name refrence in controller. By default the controller and model
|
190
|
+
# are assumed to use the same root name regardless of singular or plural context.
|
191
|
+
def configure_resource controller_name
|
192
|
+
namespaces, name = get_controller_namespaces_and_name controller_name
|
193
|
+
controller = [namespaces, name].compact.join("::").constantize
|
194
|
+
name = name.chomp("Controller").underscore
|
195
|
+
parent_key = name.singularize + "_id"
|
196
|
+
resource = controller.resource_options || {}
|
197
|
+
resource.reverse_merge! :parent_key => parent_key,
|
198
|
+
:parent_id => params[parent_key],
|
199
|
+
:name => name,
|
200
|
+
:controller => controller,
|
201
|
+
:label => name.capitalize,
|
202
|
+
:namespaces => (namespaces.collect(&:downcase) if namespaces),
|
203
|
+
:show_partial => '/' + [namespaces, name, "show"].compact.join('/').downcase,
|
204
|
+
:new_or_edit_partial => '/' + [namespaces, name, "new_or_edit"].compact.join('/').downcase
|
205
|
+
begin
|
206
|
+
resource.reverse_merge! :model => name.singularize.camelize.constantize
|
207
|
+
rescue NameError
|
208
|
+
logger.warn "Unable to constantize: " + name
|
209
|
+
end
|
210
|
+
add_record resource
|
211
|
+
end
|
212
|
+
|
213
|
+
# Adds the current record to the resource based on position in chain.
|
214
|
+
def add_record resource
|
215
|
+
if resource[:model]
|
216
|
+
if params.include? resource[:parent_key]
|
217
|
+
# Nested parent.
|
218
|
+
resource[:record] = resource[:model].find resource[:parent_id] if resource[:parent_id]
|
219
|
+
else
|
220
|
+
# Single resource and/or end of nested chain.
|
221
|
+
resource[:record] = params[:id] ? resource[:model].find(params[:id]) : resource[:model].new
|
222
|
+
end
|
223
|
+
else
|
224
|
+
logger.error "Invalid model. Check that your controller and model are of the same name, otherwise specify the model as a resource option."
|
225
|
+
end
|
226
|
+
resource
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rest
|
2
|
+
module ClassMethods
|
3
|
+
# Allows an object to belong to a parent object, thus creating a nested hierarchy. This behaves in a similar
|
4
|
+
# fashion to the belongs_to ActiveRecord macro. Accepts the following parameters:
|
5
|
+
# * *parent* - The parent symbol (including namespaces delimited by underscores). Be sure to reflect singular or plural forms on the name. Example: Public::PostsController, Result: :public_posts
|
6
|
+
def belongs_to parent
|
7
|
+
@parent_resource = parent.to_s
|
8
|
+
class_eval "class << self; attr_accessor :parent_resource end"
|
9
|
+
end
|
10
|
+
|
11
|
+
# Allows one to specify the various options for a resource. The following options are accepted:
|
12
|
+
# * *parent_key* - The ID key of the parent resource (for nested resources only). Defaults to: <belongs_to name>_id
|
13
|
+
# * *parent_value* - The ID value of the parent resource (for nested resources only). Defaults to the record id.
|
14
|
+
# * *parent_resource_method* - The instance method to call for acquiring child resource(s) of the parent (for nested resources only). Example: A post with many comments would be post.comments. Defaults to child resource name.
|
15
|
+
# * *name* - The resource name. Defaults to the controller name.
|
16
|
+
# * *label* - The resource label. Defaults to the controller name with capitalization.
|
17
|
+
# * *controller* - The controller class. Defaults to the current class. You should not need to change this.
|
18
|
+
# * *model* - The model class. Defaults to a model of the same name as the controller (minus namespace, suffix, and pluralization of course).
|
19
|
+
# * *record* - The record (new or found) related to the resource.
|
20
|
+
# * *namespaces* - The namespaces (if any) for routing. Defaults to whatever namespace your controller is using.
|
21
|
+
# * *show_partial* - The show partial. Defaults to "/<namspace(s)>/<controller name>/_show".
|
22
|
+
# * *new_or_edit_partial* - The new or edit partial. Defaults to "/<namspace(s)>/<controller name>/_new_or_edit".
|
23
|
+
def resource_options options = {}
|
24
|
+
@resource_options = options
|
25
|
+
class_eval "class << self; attr_reader :resource_options end"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Allows one to disable any number of default actions. Accepts the following parameters:
|
29
|
+
# * *actions* - A variable list of REST action symbols you wish to disable. You may use any of the following: index, show, new, create, edit, update, and/or destroy.
|
30
|
+
def disabled_actions *actions
|
31
|
+
actions.uniq.each {|action| undef_method action}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ResourceHelper
|
2
|
+
# Builds a DOM ID for a given record. Works the same as the dom_id helper found in Rails except that it returns a record ID with
|
3
|
+
# a "_0" suffix for new records instead of a "new_" prefix. This makes attaching JavaScript events easier since all DOM IDs are numbers.
|
4
|
+
def build_dom_id record
|
5
|
+
name = record.class.name.underscore
|
6
|
+
record.new_record? ? name + "_0" : name + '_' + record.id.to_s
|
7
|
+
end
|
8
|
+
|
9
|
+
# Show a descriptive label based on the current controller action. Useful for new/edit actions. Accepts the following parameters:
|
10
|
+
# * *label* - The action label.
|
11
|
+
def show_action_label label
|
12
|
+
[params[:action].capitalize, label].compact.join ' '
|
13
|
+
end
|
14
|
+
|
15
|
+
# Shows an unobtrusive jQuery link where the UJS event is attached to the link via the "destroy" class.
|
16
|
+
# If JavaScript is disabled then the _show_ action will be called instead of the _destroy_ action. Accepts
|
17
|
+
# the following hash arguments:
|
18
|
+
# * *parent_id* - The ID of the parent element for which the link is a child of. Example: post_1. *NOTE:* The parent ID takes precidence over the link ID if defined.
|
19
|
+
# * *id* - The link ID. Example: post_1_link. *NOTE:* The link ID _must_ include the parent ID.
|
20
|
+
# * *label* - The link label. Defaults to "Delete".
|
21
|
+
# * *url* - The destroy URL. Example: /posts/1. *NOTE:* The proper HTTP POST request will be handled by JavaScript.
|
22
|
+
def show_destroy_link options = {}
|
23
|
+
options.reverse_merge! :id => options[:parent_id].to_s + "_destroy"
|
24
|
+
options.reverse_merge! :label => "Delete", :class => "destroy", :url => '#' + options[:id].to_s
|
25
|
+
link_to options[:label], options[:url], :id => options[:id], :class => options[:class]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Shows a nested, unobtrusive jQuery link where the UJS event is attached to the link via the "destroy-nested" class.
|
29
|
+
# If JavaScript is disabled then the _show_ action will be called instead of the _destroy_ action. Accepts
|
30
|
+
# the following hash arguments:
|
31
|
+
# * *object* - The model object to destroy.
|
32
|
+
# * *parent_id* - The ID of the parent element for which the link is a child of. Example: post_1. *NOTE:* The parent ID takes precidence over the link ID if defined.
|
33
|
+
# * *id* - The link ID. Example: post_1_link. *NOTE:* The link ID _must_ include the parent ID.
|
34
|
+
# * *label* - The link label. Defaults to "Delete".
|
35
|
+
# * *url* - The destroy URL. Example: /posts/1. *NOTE:* The proper HTTP POST request will be handled by JavaScript.
|
36
|
+
def show_destroy_nested_link options = {}
|
37
|
+
unless options[:object].new_record?
|
38
|
+
options.reverse_merge! :id => options[:parent_id].to_s + "_destroy-nested", :class => "destroy-nested"
|
39
|
+
show_destroy_link options
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Shows an unobtrusive jQuery link based on an array of resource hashes. See the show_destroy_link above
|
44
|
+
# for further details. Accepts the following arguments:
|
45
|
+
# * *resources* - The array of resource hashes.
|
46
|
+
def show_destroy_resource_link resources, parent_dom_id
|
47
|
+
show_destroy_link :parent_id => parent_dom_id, :url => build_resource_url(resources, :destroy)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Shows an unobtrusive jQuery link based on an array of resource hashes. See the show_destroy_link above
|
51
|
+
# for further details. Accepts the following arguments:
|
52
|
+
# * *resources* - The array of resource hashes.
|
53
|
+
def show_destroy_nested_resource_link resources, parent_dom_id
|
54
|
+
show_destroy_link(:id => parent_dom_id.to_s + "_destroy-nested", :class => "destroy-nested") unless resources.last[:record].new_record?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Shows edit and delete links for resources. Accepts the following parameters:
|
58
|
+
# * *resources* - The array of resource hashes.
|
59
|
+
# * *parent_dom_id* - The parent ID of the dom object for which the edit and destroy links belong to for UJS manipulation.
|
60
|
+
def show_edit_and_destroy_resource_links resources, parent_dom_id
|
61
|
+
[link_to("Edit", build_resource_url(resources, :edit)), show_destroy_resource_link(resources, parent_dom_id)].join(" | ")
|
62
|
+
end
|
63
|
+
end
|
data/lib/rest.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "class_methods.rb")
|
2
|
+
require File.join(File.dirname(__FILE__), "actions.rb")
|
3
|
+
require File.join(File.dirname(__FILE__), "resource_helper.rb")
|
4
|
+
|
5
|
+
module Rest
|
6
|
+
def self.included base
|
7
|
+
base.extend ClassMethods
|
8
|
+
base.helper "resource"
|
9
|
+
base.helper_method :build_resource_url
|
10
|
+
end
|
11
|
+
include Actions
|
12
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
Tasks You Need to Complete:
|
3
|
+
1. Update your layouts/application.html.erb as follows:
|
4
|
+
|
5
|
+
<%= javascript_include_tag "jquery" %>
|
6
|
+
<%= javascript_include_tag "jquery-ui" %>
|
7
|
+
<%= javascript_include_tag "ujs" %>
|
8
|
+
<%= javascript_include_tag "rest" %>
|
9
|
+
|
10
|
+
2. Add the following to the very bottom of your config/routes.rb file:
|
11
|
+
|
12
|
+
# Defaults
|
13
|
+
map.connect ":controller/:action/.:format"
|
14
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
// Ensures AJAX requests are properly formated so that Rails knows how to respond to them.
|
2
|
+
$.ajaxSetup({
|
3
|
+
beforeSend: function(xhr) {xhr.setRequestHeader("Accept", "text/javascript")}
|
4
|
+
});
|
5
|
+
|
6
|
+
/* Ensures AJAX requests have the proper Rails authenticity token. See the following
|
7
|
+
for more info:
|
8
|
+
|
9
|
+
http://henrik.nyh.se/2008/05/rails-authenticity-token-with-jquery
|
10
|
+
http://dev.jquery.com/ticket/3387
|
11
|
+
*/
|
12
|
+
$(document).ajaxSend(function(event, request, settings){
|
13
|
+
var authToken = "<%= form_authenticity_token %>";
|
14
|
+
if (authToken != null){
|
15
|
+
settings.data = settings.data || "";
|
16
|
+
settings.data += (settings.data ? "&" : "") + "authenticity_token=" + encodeURIComponent(authToken);
|
17
|
+
};
|
18
|
+
});
|
@@ -0,0 +1,119 @@
|
|
1
|
+
// Chops off the last string segment designated by delimiter. If no delimiter is found then the original string is
|
2
|
+
// returned instead. The following attributes are accepted:
|
3
|
+
// string = Required. The string to chop.
|
4
|
+
// delimiter = Optional. The delimiter used to chop up the string. Defaults to '_'.
|
5
|
+
function stringChop(string, delimiter) {
|
6
|
+
var chopped = string;
|
7
|
+
if (delimiter == undefined) {delimiter = '_';}
|
8
|
+
var endIndex = string.lastIndexOf(delimiter);
|
9
|
+
if (endIndex > 1) {chopped = string.slice(0, endIndex);}
|
10
|
+
return chopped;
|
11
|
+
};
|
12
|
+
|
13
|
+
// Increments a number embedded in the text by one. If no number is found then the original text is returned.
|
14
|
+
function incrementText(text) {
|
15
|
+
if (text != undefined) {
|
16
|
+
var match = text.match(/\d+/);
|
17
|
+
if (match != null) {
|
18
|
+
var number = new Number(match);
|
19
|
+
var newNumber = number + 1;
|
20
|
+
text = text.replace(number, newNumber);
|
21
|
+
}
|
22
|
+
}
|
23
|
+
return text;
|
24
|
+
};
|
25
|
+
|
26
|
+
// Increments the input field ID number by one so ActiveRecord can save new record attributes.
|
27
|
+
function incrementInputId(input) {
|
28
|
+
id = $(input).attr("id");
|
29
|
+
id = incrementText(id);
|
30
|
+
$(input).attr("id", id);
|
31
|
+
};
|
32
|
+
|
33
|
+
// Increments the input field name number by one so ActiveRecord can save new record attributes.
|
34
|
+
function incrementInputName(input) {
|
35
|
+
name = $(input).attr("name");
|
36
|
+
name = incrementText(name);
|
37
|
+
$(input).attr("name", name);
|
38
|
+
};
|
39
|
+
|
40
|
+
// Generates a hidden input field based off original input field data that instructs ActiveRecord to delete
|
41
|
+
// a record based on the ID of the input field.
|
42
|
+
function generateDestroyInput(input) {
|
43
|
+
$(input).attr("id", stringChop($(input).attr("id")) + "__delete");
|
44
|
+
$(input).attr("name", stringChop($(input).attr("name"), '[') + "[_delete]");
|
45
|
+
$(input).attr("type", "hidden");
|
46
|
+
$(input).attr("value", 1);
|
47
|
+
return input;
|
48
|
+
};
|
49
|
+
|
50
|
+
// Animates the removal of a DOM element.
|
51
|
+
function animateDestroy(id) {
|
52
|
+
$(id).animate({backgroundColor: "#FF0000", border: "0.2em solid #FF0000"}, 500);
|
53
|
+
$(id).fadeOut(500);
|
54
|
+
};
|
55
|
+
|
56
|
+
// UJS
|
57
|
+
$(document).ready(function(){
|
58
|
+
// Nested New
|
59
|
+
$("a.new-nested").click(function(){
|
60
|
+
var parentId = '#' + stringChop($(this).attr("id"));
|
61
|
+
var child = $(parentId).children(":last").clone(true);
|
62
|
+
var childId = child.attr("id");
|
63
|
+
child.attr("id", incrementText(childId));
|
64
|
+
$(parentId).append(child);
|
65
|
+
// Ensure the cloned input fields are unique.
|
66
|
+
$('#' + child.attr("id") + " :input").each(function(){
|
67
|
+
incrementInputId(this);
|
68
|
+
incrementInputName(this);
|
69
|
+
$(this).attr("value", '');
|
70
|
+
return this;
|
71
|
+
});
|
72
|
+
// Ensure the cloned destroy link is unique.
|
73
|
+
destroyLink = $("a.destroy-nested:last");
|
74
|
+
destroyLink.attr("id", incrementText(destroyLink.attr("id")));
|
75
|
+
// Ensure that the default event does not fire.
|
76
|
+
return false;
|
77
|
+
});
|
78
|
+
|
79
|
+
// Nested Destroy
|
80
|
+
$("a.destroy-nested").click(function(){
|
81
|
+
try {
|
82
|
+
var id = $(this).attr("id");
|
83
|
+
var parentId = stringChop(id);
|
84
|
+
if (parentId != id) {
|
85
|
+
parentId = '#' + parentId;
|
86
|
+
$(parentId).prepend(generateDestroyInput($(parentId + " input:first").clone()));
|
87
|
+
animateDestroy(parentId);
|
88
|
+
} else {
|
89
|
+
throw "Invalid ID";
|
90
|
+
}
|
91
|
+
} catch (e) {
|
92
|
+
alert("Error: " + e + ". Check that parent ID is defined and/or the link ID includes parent ID as part of the link ID.");
|
93
|
+
}
|
94
|
+
// Ensure that the default event does not fire.
|
95
|
+
return false;
|
96
|
+
});
|
97
|
+
|
98
|
+
// Destroy
|
99
|
+
$("a.destroy").click(function(){
|
100
|
+
var result = confirm("Are you sure you want to delete this?");
|
101
|
+
if (result) {
|
102
|
+
try {
|
103
|
+
var id = $(this).attr("id");
|
104
|
+
var parentId = stringChop(id);
|
105
|
+
if (parentId != id) {
|
106
|
+
animateDestroy('#' + parentId);
|
107
|
+
} else {
|
108
|
+
throw "Invalid ID";
|
109
|
+
}
|
110
|
+
// Finally, call the destroy action.
|
111
|
+
$.post($(this).attr("href"), "_method=delete");
|
112
|
+
} catch (e) {
|
113
|
+
alert("Error: " + e + ". Check that parent ID is defined and/or the link ID includes parent ID as part of the link ID.");
|
114
|
+
}
|
115
|
+
}
|
116
|
+
// Ensure that the default event does not fire.
|
117
|
+
return false;
|
118
|
+
});
|
119
|
+
});
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class UjsSetupGenerator < Rails::Generator::Base
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
# Controllers
|
5
|
+
m.file "app/controllers/javascripts_controller.rb", "app/controllers/javascripts_controller.rb"
|
6
|
+
|
7
|
+
# Views
|
8
|
+
m.directory "app/views/javascripts"
|
9
|
+
m.file "app/views/javascripts/ujs.js.erb", "app/views/javascripts/ujs.js.erb"
|
10
|
+
|
11
|
+
# JavaScript
|
12
|
+
m.file "public/javascripts/rest.js", "public/javascripts/rest.js"
|
13
|
+
|
14
|
+
# Instructions
|
15
|
+
m.readme "README"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/spec/rest_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aeonscope-rest
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brooke Kuhlmann
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-03 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rails
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.3.2
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mislav-will_paginate
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.3.7
|
34
|
+
version:
|
35
|
+
description: "Ruby on Rails supports RESTful routing out-of-the-box. However, as you move along, you will often find yourself repeating the code for the seven REST actions: index, show, new, create, edit, update, destroy. This gem allows you to get all of the code for free by simply typing 'include Rest' at top of your controller code. That's it! Even better, you can have nested resources as well by adding a 'belongs_to' statement to your controllers much like you would for ActiveRecord models. This is just the tip of the iceburg so make sure to read the RDoc for more info."
|
36
|
+
email: aeonscope@gmail.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- LICENSE.rdoc
|
43
|
+
- README.rdoc
|
44
|
+
files:
|
45
|
+
- CHANGELOG.rdoc
|
46
|
+
- LICENSE.rdoc
|
47
|
+
- README.rdoc
|
48
|
+
- Rakefile
|
49
|
+
- VERSION.yml
|
50
|
+
- lib/actions.rb
|
51
|
+
- lib/class_methods.rb
|
52
|
+
- lib/resource_helper.rb
|
53
|
+
- lib/rest.rb
|
54
|
+
- rails_generators/ujs_setup/USAGE
|
55
|
+
- rails_generators/ujs_setup/templates/README
|
56
|
+
- rails_generators/ujs_setup/templates/app/controllers/javascripts_controller.rb
|
57
|
+
- rails_generators/ujs_setup/templates/app/views/javascripts/ujs.js.erb
|
58
|
+
- rails_generators/ujs_setup/templates/public/javascripts/rest.js
|
59
|
+
- rails_generators/ujs_setup/ujs_setup_generator.rb
|
60
|
+
- spec/rest_spec.rb
|
61
|
+
- spec/spec_helper.rb
|
62
|
+
has_rdoc: true
|
63
|
+
homepage: http://github.com/aeonscope/rest
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options:
|
66
|
+
- --charset=UTF-8
|
67
|
+
- CHANGELOG.rdoc
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.8.6
|
75
|
+
version:
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "0"
|
81
|
+
version:
|
82
|
+
requirements: []
|
83
|
+
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 1.2.0
|
86
|
+
signing_key:
|
87
|
+
specification_version: 3
|
88
|
+
summary: Enables default REST functionality, more than what you get with Rails out-of-the-box, and keeps your code DRY.
|
89
|
+
test_files:
|
90
|
+
- spec/rest_spec.rb
|
91
|
+
- spec/spec_helper.rb
|