可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I am using Can.js to add a record to a Ruby on Rails server:
var Todo = can.Model({ findAll : 'GET /todos', findOne : 'GET /todos/{id}', create : 'POST /todos', update : 'PUT /todos/{id}', destroy : 'DELETE /todos/{id}' }, {}); Todo.prototype.description = function() { return this.name + " and " + this.complete; }; Todo.findAll({}, function(todos) { todos.each(function(todo) { console.log(todo.name, todo.complete); console.log(todo.description()); }); }); var todo = new Todo({name: "mow lawn"}); todo.save();
the findAll()
actually can get all the records, but the save()
will insert a blank record. I suspected it might be the CSRF token
, but that shows as a warning, and if it didn't want to insert the record, it probably won't create any record at all, instead of adding a record with name being nothing? (I also tried var todo = new Todo({name: "mow lawn", complete: true});
and it is the same result).
but the Rails server prints out on the terminal as:
Started POST "/todos" for 127.0.0.1 at 2013-04-12 08:16:05 -0700 Processing by TodosController#create as JSON Parameters: {"name"=>"mow lawn"} WARNING: Can't verify CSRF token authenticity (0.2ms) begin transaction SQL (0.8ms) INSERT INTO "todos" ("complete", "created_at", "name", "updated_at") VALUES (?, ?, ?, ?) [["complete", nil], ["created_at", Fri, 12 Apr 2013 15:16:05 UTC +00:00], ["name", nil], ["updated_at", Fri, 12 Apr 2013 15:16:05 UTC +00:00]] (1.5ms) commit transaction Completed 201 Created in 11ms (Views: 1.3ms | ActiveRecord: 2.5ms)
In the Chrome developer tool, I see in Network:
Request URL:http://localhost:3000/todos Request Method:POST part of header: Content-Type:application/x-www-form-urlencoded; charset=UTF-8 form data: name:mow lawn
If my action is:
def create p "params[:todo] is", params[:todo] @todo = Todo.new(params[:todo]) respond_to do |format| if @todo.save format.html { redirect_to @todo, notice: 'Todo was successfully created.' } format.json { render json: @todo, status: :created, location: @todo } else format.html { render action: "new" } format.json { render json: @todo.errors, status: :unprocessable_entity } end end end
then terminal will show:
"params[:todo] is" nil
even though the terminal shows the standard Rails log:
Parameters: {"name"=>"mow lawn"}
回答1:
As shown in the Rails console output, your params
Hash looks like the following:
Parameters: {"name"=>"mow lawn"}
But your controller is creating a new Todo instance using the following:
@todo = Todo.new(params[:todo])
:todo
is not in the params
Hash, so params[:todo]
is nil
and basically your Todo instance has no attributes passed to it, hence it is saving nil
for the name
column in the DB.
Your controller should be using parameter wrapping to solve this, which will wrap all the params
Hash into a params[:todo]
sub-Hash. So if you have this in your controller:
class TodosController < ApplicationController wrap_parameters :todo ... end
Your params
Hash will be transformed to the following before the action is performed:
Parameters: {"name"=>"mow lawn", "todo" => {"name"=>"mow lawn"}}
And then your existing action code should work.
回答2:
Like Stuart M said, you need to set the wrap_parameters :todo
in the controller.
But wrap_parameters :todo
does not work with the default Content-Type: application/x-www-form-urlencoded
canjs sends on it's model updates.
Here are the resources that let me work it out:
https://github.com/bitovi/canjs/issues/111
https://forum.javascriptmvc.com/topic/howto-use-application-json-and-encode-data-automatically-in-can-model
So by reading those I changed my app to look like this:
app/controllers/todo_controller.rb
class TodosController < ApplicationController wrap_parameters :todo def create Todo.new(params[:todo]) end end
app/assets/javascripts/canjs.models.js
$.ajaxPrefilter(function(options, originalOptions, jqXHR){ if( options.processData && /^(post|put|patch|delete)$/i.test( options.type )) { options["contentType"] = "application/json"; options["data"] = JSON.stringify(originalOptions["data"]); } }); Todo = Model.new({ ... });
And everything worked like magic :)
NOTE: You may need to setup CORS for this to work, here is how to set it up on a public api:
config/routes.rb
SomeTodoApp::Application.routes.draw do resources :todos, except: [:new, :edit] match "*all" => "application#cors_preflight_check", via: :options end
app/controllers/application_controller.rb
class ApplicationController < ActionController::API # protect_from_forgery is always a good thing if you know your clients can support get it working protect_from_forgery before_filter :cors_preflight_check after_filter :cors_set_access_control_headers # For all responses in this controller, return the CORS access control headers. def cors_set_access_control_headers headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Methods'] = 'POST, PUT, PATCH, GET, OPTIONS' headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization' headers['Access-Control-Request-Method'] = '*' headers['Access-Control-Max-Age'] = "1728000" end # If this is a preflight OPTIONS request, then short-circuit the # request, return only the necessary headers and return an empty # text/plain. def cors_preflight_check if request.method == :options headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Methods'] = 'POST, PUT, PATCH, GET, OPTIONS' headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization' headers['Access-Control-Request-Method'] = '*' headers['Access-Control-Max-Age'] = '1728000' render :text => '', :content_type => 'text/plain' end end end