How does rails generate the object from a form submission? - ruby-on-rails

My Controller
class MessagesController < ApplicationController
def index
#messages = Message.all
end
def new
#message = Message.new
end
def create
#message = Message.new(message_params)
if #message.save
redirect_to '/messages'
else
render 'new'
end
end
private
def message_params
params.require(:message).permit(:content)
end
end
My corresponding view
<div class="create">
<div class="container">
<%= form_for(#message) do |f| %>
<div class="field">
<%= f.label :message %><br>
<%= f.text_area :content %>
</div>
<div class="actions">
<%= f.submit "Create" %>
</div>
<% end %>
</div>
</div>
I'm a bit confused about how things work under the hood for Rails object creations. Currently doing the Codecademy tutorial, but they've skipped a couple of explanation steps.
When the form submits button is pressed does f.submit generate
a JSON object in a POST request?
After getting routed to the message controllers' create action. How does #message.save know if it's been saved successfully? Isn't it just an object populated by the parameters passed in at this point? Does it route to the DB first before the controller?

You can see what gets submitted by a form submission in your rails server logs. Just run rails server in a terminal, open up your localhost, submit a form and immediately check what gets spit out in the terminal.
You might get something like this:
Started POST "/messages" for ::1 at 2021-10-25 11:41:33 +0200
Processing by MessagesController#create as HTML Parameters: {"message"=>"text", "content"=>"hey"}
[here you will see the SQL run to INSERT new data into the database]
Completed 201 Created in 1ms (ActiveRecord: 2.0ms | Allocations: 2073)
Breaking this down you get 5 pieces of information.
Type of request and endpoint with a timestamp
Controller and format used
Params in JSON
SQL query run if any
Response status with benchmarks for parts used in response, how much time queries took, how much rendering took etc.
save method from rails will try to save an instance of an initialized model into the database and will return true or false depending on the result of the action. There is also save! method that will raise an error if the operation fails, instead of simply returning false boolean value. So to answer your question specifically:
JSON object is sent in params of your POST request, generated based on the HTML form.
#message is an object populated by params (in your case it's only content param that is actually used). Using save on it will prompt ActiveRecord to connect to the database and perform an INSERT action, saving it to the database.
It does not route to the DB first, the controller controls the actions performed based on the request. If something hits the database, you will have to prompt it, as you do by using the save method.
save method in rails documentation

Related

Active Storage on a remote true form. Invalid Authenticity error and wrong controller format

After upgrading my app to Rails 5.2, I've found some time to look at Active Storage. Following the guide, I've installed it and ran the migrations necessary.
On my User model I want to attach an avatar as per the example here: Edge Guide for Active Storage
The error I am receiving upon submitting my form is ActionController::InvalidAuthenticityToken
<%= form_for #user, remote: true do |f| %>
<%# Other user fields redacted %>
<div class="form-group">
<%= f.label :avatar %>
<%= f.file_field :avatar %>
</div>
<%= f.submit "Save", remote: true %>
<% end %>
I changed the form_for to include authenticity_token: true like this:
<%= form_for #user, remote: true, authenticity_token: true do |f| %>
This removed my authenticity error and inserted the file into my DB, however this has caused an Unknown format error, in that it is routing to my controller with html instead of js.
Logs:
Started PATCH "/users/22" for 127.0.0.1 at 2018-11-07 13:36:22 +0000
Processing by UsersController#update as HTML
Disk Storage (5.7ms) Uploaded file to key: aJQ3m2skk8zkHguqvhjV6tNk (checksum: 7w6T1YJX2LNIU9oPxG038w==)
ActiveStorage::Blob Create (23.6ms) INSERT INTO `active_storage_blobs` (`key`, `filename`, `content_type`, `metadata`, `byte_size`, `checksum`, `created_at`) VALUES ('aJQ3m2skk8zkHguqvhjV6tNk', 'Dq3gtJjU0AAbdIj.jpg-large.jpeg', 'image/jpeg', '{\"identified\":true}', 50642, '7w6T1YJX2LNIU9oPxG038w==', '2018-11-07 13:36:22')
ActiveStorage::Attachment Create (3.4ms) INSERT INTO `active_storage_attachments` (`name`, `record_type`, `record_id`, `blob_id`, `created_at`) VALUES ('avatar', 'User', 22, 1, '2018-11-07 13:36:22')
(9.4ms) ROLLBACK
Completed 406 Not Acceptable in 630ms (ActiveRecord: 93.1ms)
ActionController::UnknownFormat (ActionController::UnknownFormat):
Users#Update
def update
respond_to do |format|
if #user.update(user_params)
flash.now[:notice] = 'User saved successfully!'
format.js do
#users = User.all
end
else
flash.now[:alert] = #user.errors.full_messages.to_sentence
format.js do
#users = User.all
render layout: false, content_type: 'text/javascript'
end
end
end
end
Any ideas as to why it is being submitted as HTML instead of JS?
Edit: Form Markup
<form class="edit_user" id="edit_user_22" enctype="multipart/form-data" action="/users/22" accept-charset="UTF-8" data-remote="true" method="post">
After much trial & error, it took using the Direct Upload feature of Active Storage to allow this to work. Allow me to explain my findings:
remote: true, multipart: true don't play well together. See this stack overflow post for more details. Essentially you need to use jQuery or a gem to submit files remotely.
Following this edgeguides post (direct uploads). It seems as though when you click on submit; direct upload will catch the submit event and submit the file directly to a cloud server (or local in my dev case). It will then use the reference of that image in the form submit, instead of submitting the actual image.
This hit my Users#update using JS and successfully attached the Avatar.
Also transiting to Active Storage and stuck here.
But (I recalled that) in my previous implementation, where I used github/jQuery-File-Upload I solved (as it turns out) the same problem:
Rails.fire($("#form_id")[0], 'submit');
This is a rails-ujs method I got rom this Q/A: /with-rails-ujs-how-to-submit-a-remote-form-from-a-function
As I get now all it goes from the fact that:
The event — submit — is not raised when calling the form.submit() method directly, — MDN:
— https://developer.mozilla.org/en-US/docs/Web/Events/submit
— https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit
— https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Sending_forms_through_JavaScript

Each Loop in Rails 4 Controller to create new instance variable

I have a parent form (lead) and child form (QuoteMetal) (which is rendered multiple times on the same submit). All the information from the forms gets written to their respective data tables, but I need the information from the child forms to perform a query and return those values. I have the forms created and a controller which writes the information to the data tables.
I need help with making the query results for each of the child forms then accessing them in the views. Here is what I currently have.
class LeadsController < ApplicationController
def index
#lead = Lead.new
#quote_metal = #lead.quote_metals.build
end
def create
#raise params.inspect
#lead = Lead.create!(lead_params) #write to data tables (which works)
#lead.quote_metals.each do |f|
#metal = Metal.calculate_metal(f).first #here is where my problem is! the #calculate_metal is the query located in my model
end
end
def show
end
private
def lead_params
params.require(:lead).permit([:name, .....
quote_metals_attributes: [:id...],
quote_melees_attributes: [:shape...],
quote_diamonds_attributes: [:shape...]
])
end
end
and the view:
<div class='container'>
<div class="row">
<div class='col-sm-3 col-sm-offset-2'>
<h3 class="text-center">Your Search: </h3>
</div>
<div class='col-sm-5'>
<h4 class="text-center">Returning estimates for a <%= #metal.metal %> setting
weighing <%= #metal.weight %> <%= #metal.unit %>. I am paying
<%= number_to_currency(#metal.price, precision: 2) %> per <%= #metal.unit %>.</h4>
</div>
</div>
Actions on QuoteMetal instances should be handled in the QuoteMetal class. So, I would replace:
#lead.quote_metals.each do |f|
#metal = Metal.calculate_metal(f).first #here is where my problem is! the #calculate_metal is the query located in my model
end
with:
#lead.quote_metals.each do |f|
f.create
end
And then in your QuoteMetal class, you can use a before_save callback and perform the calculation there. Keep in mind this will invoke calculate_metal every time a QuoteMetal is saved or updated.
Even better would be to use accepts_nested_attributes_for in the Lead model so that quote_metals can be automatically created when leads are created. See Rails documentation here. With this approach, you could eliminate the above three lines in the controller, but would still need the callback in the QuoteMetal class to perform the custom calculation.
Separately, be aware that your call to create! will raise an exception if validation fails. Not sure you intend that.

matching POST route rails 4

I am trying to upload a photo but after I press the upload button, I get this error. I am new to rails 4 so I'm not sure what I am missing.
My logic is when I click the submit button. This will cause the create action to fire and create a IncomePicture object and store it in my database.
No route matches [POST] "/income_pictures/new"
Routes:
root_path GET / static_pages#home
income_pictures_path GET /income_pictures(.:format) income_pictures#index
POST /income_pictures(.:format) income_pictures#create
new_income_picture_path GET /income_pictures/new(.:format) income_pictures#new
edit_income_picture_path GET /income_pictures/:id/edit(.:format) income_pictures#edit
income_picture_path GET /income_pictures/:id(.:format) income_pictures#show
PATCH /income_pictures/:id(.:format) income_pictures#update
PUT /income_pictures/:id(.:format) income_pictures#update
DELETE /income_pictures/:id(.:format) income_pictures#destroy
Controller:
class IncomePicturesController < ApplicationController
def new
#income_picture = IncomePicture.new
end
def create
#income_picture = IncomePicture.new(IncomePicture_params)
if #income_picture.save
flash[:notice] = "Income picture successfully uploaded"
redirect_to #income_picture
end
end
def show
#income_picture = IncomePicture.find(params[:id])
end
def index
#income_picture = IncomePicture.all
end
private
def IncomePicture_params
params.require(:income_picture).permit(:image, :name)
end
end
View:
<%= form_for :income_picture, :html => { :multipart => true } do |f| %>
<p>
<%= f.label :name %>
<%= f.text_field :name %>
</p>
<p>
<%= f.label :image %>
<%= f.file_field :image %>
</p>
<p><%= f.submit %></p>
<% end %>
I think you want form_for #income_picture rather than form_for :income_picture.
From the form guide: Using a symbol creates a form to new_income_picture_path, i.e. /income_picture/new whereas using a populated instance variable creates a form to income_pictures_path, i.e. income/pictures. Both set the form's method to POST. However, there's no such route as POSTing to /income_picture/new/, which is what caused the error.
form_for
To elaborate on the accepted answer, you have to remember that when calling form_for, Rails does some pretty amazing things:
It takes an ActiveRecord object and builds a "route" out of it (from the model)
It populates the form with the ActiveRecord object's data
It allows you to retain a perceived persistent state on the form (by perpetuating the data)
The problem you have is you're passing a simple symbol to the form - which prevents Rails from being able to accurately access the data required to make the 3 "magic" steps above possible.
This means you'll get random errors like the one you're seeing (IE in the absence of an ActiveRecord object, Rails will just use the same URL that you have on your page - /new)
--
ActiveRecord
The way to fix the issue you have is to replace the symbol with an ActiveRecord object, which was suggested in the accepted answer.
The reason why using an ActiveRecord object (#instance_variable) works is because of Ruby's core functionality -- it's a object orientated language. Being object orientated, it means that each time you populate an ActiveRecord object, you'll basically give Rails a series of other information, such as model_name etc.
This means when you pass the #instance_variable to the form_for method, Rails will be able to take the data from ActiveRecord & process it on screen for you

Rails - How do I refresh a page without reloading it? (updated)

This question is about problems I ran into after asking my previous question Rails - How do I refresh a page without reloading it?
Have been googling but cannot find a solution.
I'm building a function that gives out a random record every time people go to that page but the random record will change again if the user refresh the page.
Therefore I have tried using cookies
#posts_controller.rb
def new
#random = cookies[:stuff] ||= Stuff.random
end
and call #random.content in my posts/new view since I want only the content of the Stuff model, when I go to the page for the first time is fine but when I refresh the page, it gives me this error
undefined method 'content' for "#<Stuff:0x0000010331ac00>":String
Is there a way to resolve this?
Thanks!
----Update----
I have fixed the content missing problem using <%= show_random(#random)['content'] %> and the Internal Server Error expected Hash (got Array) for param 'post'using
<%= f.hidden_field :stuff_id , :value => show_random(#random)[:id] %>
stuff/new.html.erb
# app/views/stuff/new.html.erb
<%= show_random(#random)[:content] %>
<div>
<%= f.hidden_field :stuff_id , :value => show_random(#random)[:id] %><br>
</div>
But when creating the Post without meeting the validation, it gives me
no implicit conversion of Stuff into String
Extracted source (around line #2):
1
2 <%= show_random(#random)['title'] %>
I think it has something to do with my create action in my Posts_controller.erb
Please have a look below of my Posts_controller.erb
def create
#post = current_user.posts.build(post_params)
if #post.save
flash[:success] = "Post created successfully!"
else
#random = Stuff.where(id: params[:post][:stuff_id]).first
render 'new'
end
end
The first time , as cookies[:stuff] is null, so a new Stuff instance is assigned to both cookies[:stuff] and #random, so the content method call on #random will be fine. But as you store an object into the cookies[:stuff], the value will be converted into a string by rails automatically.
The second time, you visit the page, the cookies[:stuff] is not empty, and is assigned to the #random variable. But as previous saying, the content inside the cookies is a string, so calling content method on a string can not work.
Make your .random method defined on Stuff return a serialized record, which you can then deserialize in your views (possibly with a helper method) to make it a hash:
# app/models/stuff.rb
def self.random
random_record_magic.to_json
end
end
# app/views/stuff/index.html.erb
<%= show_random(#random)['content'] %>
# app/helpers/stuff_helper.rb
def show_random(random)
JSON.parse(random)
end

"Couldn't find <object> without an ID"

I'm having problems implementing a kind of comments form, where comments (called "microposts") belong_to both users and posts, users have_many comments, and posts (called "propositions") have_many comments.
My code for the comments form is:
<%= form_for #micropost do |f| %>
<div class="field">
<%= f.text_area :content %>
</div>
<div class="actions">
<%= f.submit "Submit" %>
</div>
<% end %>
The MicropostsController has this in the create action:
def create
#proposition = Proposition.find(params[:proposition_id])
#micropost = current_user.microposts.build(params[:micropost])
#micropost.proposition = #proposition
if #micropost.save
flash[:success] = "Contribution submitted"
redirect_to root_path
else
#feed_items = []
render 'pages/home'
end
end
The form for creating a new micropost is on the same page as a proposition, yet the proposition id doesn't seem to get passed at any point.
This is the error I get on submitting the micropost form:
ActiveRecord::RecordNotFound in
MicropostsController#create
Couldn't find Proposition without an
ID
Parameters are:
{"commit"=>"Submit",
"micropost"=>{"proposition_id"=>"",
"content"=>"First comment"},
"authenticity_token"=>"TD6kZaHv3CPWM7xLzibEbaLJHI0Uw43H+pq88HLZFjc=",
"utf8"=>"✓"}
I'm completely new to rails and very new to coding anything at all, so I'd be grateful for any help you can give me!
Thanks in advance.
Your params are:
"micropost"=>{"proposition_id"=>"", "content"=>"First comment"}
So to get proposition_id, you have to do :
params[:micropost][:proposition_id]
But this is empty. And there is nowhere else to get this id, that's why this line retrieves nil:
#proposition = Proposition.find(params[:proposition_id])
Making this fail:
#micropost.proposition = #proposition
You must either:
add the proposition_id as an hidden_field
store it in session
But I don't know your context enough to give you the proper solution here.
EDIT:
In your link, replace:
<%= f.hidden_field :proposition_id %>
with:
<%= f.hidden_field :proposition_id, :value => #proposition.id %>
If it doesn't work, show your params.
Note: it's bad practice to rely on instance variables, you should send local variable to each partial
As you can see, the proposition_id parameter is empty, so your controller can't find the proposition unless you give it a valid id.
You need to make sure your new form sends the proposition_id attribute. One way to do this is:
Set the proposition in the new action in the controller: #micropost.proposition = ...
In the form, add a hidden field for the id: f.hidden_field :proposition_id
In the create action, find the appropriate Proposition with params[:micropost][:proposition_id]
(You'll also want to make sure to use attr_accessible in your Micropost model, and make sure proposition_id is NOT in that list. Otherwise, you'll be open to nasty security holes. See http://www.kalzumeus.com/2010/09/22/security-lessons-learned-from-the-diaspora-launch/ and Which fields should be protected from mass assignment?)
EDIT (due to comment):
Your new action should be like this:
def new
#micropost = Micropost.new
#micropost.proposition_id = params[:proposition_id]
This is slightly different from what is said above, and is due to the fact you're sending the proposition id in the request to new. There's no need to look up the actual proposition record, since we're only interested in the id field (which we already have).

Resources