Update multiple values with button_to - Rails 4 - ruby-on-rails

I'm having trouble updating columns in my table with a button in Rails.
I've looked around stackoverflow for answers, but can't seem to get anything working...
In my app, I want users to be able to "rent" and "return" pins.
When they click the "Rent" button, I want it to change the pin's columns.
Example: rented: => true, renter_id: 1"
In my Pins controller:
def rent
#pin = Pin.update_attribute(:renter_id => #user.id)
end
In my View:
<%= button_to "Rent Now!", {:controller => "pins", :action => "rent", :renter_id => #user.id }, :class => 'btn btn-success btn-lg btn-block'%>
Schema:
create_table "pins", force: true do |t|
t.string "description"
t.string "link"
t.boolean "rented"
t.integer "renter_id"
end

One problem is that update_attribute is an instance method. But you are trying to call it as a class method on Pin. Perhaps do a controller like so.
def rent
#pin = Pin.find(params[:id])
if #pin.update_attributes(renter_id: #user.id)
#handle success
else
#handle failure
end
end
Also make sure that your routes are properly set up as a post request.

update_attribute takes 2 arguments, the attribute and the value
So it should be like this
update_attribute(:renter_id, #user.id)
If you want to update multiple attributes at once or if you want validations to be triggered, use update_attributes
update_attributes(:attr1 => value1, :attr2 => value2)

Related

Rails form_for results in POST instead of PUT when trying to edit

I am using Rails 4 and have the following error.
Routing Error
No route matches [POST] "/logs/1/meals/13/edit
I’m passing form_for the model object using :meal and the edit page is rendering correctly. However, Rails does not seem to be checking whether or not the meal object has already been saved, so it keeps trying to send the form to the #create action and tries make a POST request instead of sending the form to the update action and making a PUT request when I hit submit.
How do I get the form_for to recognize that I am trying to update an existing object and that PUT is needed instead of POST? Everything else is working and I’ve run all of my migrations. I’m pretty new to Rails, and I’ve spent almost all day trying to figure this out on my own. Please help!
And just to note, when I tried to pass in the model object as #meal.log instead of :meal, Rails was no longer able to recognize :calorie_estimate or :meal_description. Passing the model object as #meal.log left me with a no method error.
meals/edit.html.erb
<h3> EDIT MEAL </h3>
<%= form_for(:meal) do |f| %>
<div id="meal-form">
<%= f.text_field :calorie_estimate, class: 'meal-form-fields', :placeholder => "Calorie Estimate" %>
<%= f.text_field :meal_description, class: 'meal-form-fields', :placeholder => "Food Description" %>
<div class="submit-form" style="width: 75px; height: 15px;">
<%= f.submit 'UPDATE', :class => 'submit-form-text' %>
</div>
</div>
<% end %>
meals_controller.rb
class MealsController < ApplicationController
include MealsHelper
def create
#meal = Meal.new(meal_params)
#meal.log_id = params[:log_id]
#meal.save
redirect_to log_path(#meal.log)
end
def edit
#meal = Meal.find(params[:id])
end
def update
#meal = Meal.find(params[:id])
#meal.update(meal_params)
redirect_to log_path(#log)
end
def meal_params
params.require(:meal).permit(:calorie_estimate, :meal_description)
end
end
possible routes:
Prefix Verb URI Pattern Controller#Action
root GET / logs#index
log_meals GET /logs/:log_id/meals(.:format) meals#index
POST /logs/:log_id/meals(.:format) meals#create
new_log_meal GET /logs/:log_id/meals/new(.:format) meals#new
edit_log_meal GET /logs/:log_id/meals/:id/edit(.:format) meals#edit
log_meal GET /logs/:log_id/meals/:id(.:format) meals#show
PATCH /logs/:log_id/meals/:id(.:format) meals#update
PUT /logs/:log_id/meals/:id(.:format) meals#update
DELETE /logs/:log_id/meals/:id(.:format) meals#destroy
logs GET /logs(.:format) logs#index
POST /logs(.:format) logs#create
new_log GET /logs/new(.:format) logs#new
edit_log GET /logs/:id/edit(.:format) logs#edit
log GET /logs/:id(.:format) logs#show
PATCH /logs/:id(.:format) logs#update
PUT /logs/:id(.:format) logs#update
DELETE /logs/:id(.:format) logs#destroy
routes.rb
Rails.application.routes.draw do
root to: 'logs#index'
resources :logs do
resources :meals
end
end
schema.rb
ActiveRecord::Schema.define(version: 20160128205351) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "logs", force: :cascade do |t|
t.string "entry_date"
t.integer "calorie_goal"
t.string "notes"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "meals", force: :cascade do |t|
t.integer "calorie_estimate"
t.string "meal_description"
t.integer "log_id"
t.datetime "created_at"
t.datetime "updated_at"
end
end
The issue is that you're using nested resources, hence you're confused about which #objects to pass to your form_for.
#app/views/meals/edit.html.erb
<%= form_for [#log, #meal] do |f| %>
As you have it presently, passing :meal is ambiguous - Rails cannot discern the route / method to send its submission to, as it doesn't have that data available.
If you wanted to update an object, you'll have to pass the appropriate data to the form, including the object's id:
<%= form_for :meal, url: { controller: "meals", action: "update", id: "5" }, method: :put do |f| %>
Such as Rails is object orientated, you'll be best passing the actual object to your form_for:
<%= form_for #meal ...
--
The issue you have is that you have a nested resource:
resources :logs do
resources :meals #-> url.com/logs/:log_id/meals/:id
end
This means you need to pass both the Log and Meal values to your form:
#app/controllers/meals_controller.rb
class MealsController < ApplicationController
def edit
#log = Log.find params[:log_id]
#meal = Meal.find params[:id]
end
def update
#log = Log.find params[:log_id]
#meal = Meal.find params[:id]
#meal.update meal_params
end
end
#app/views/meals/edit.html.erb
<%= form_for [#log, #meal] do |f| %>
If your controller is creating an instance variable called #meal you should use that instead of the symbol. So write:
form_for #meal do |f|
and then rails can query the instance variable to see whether it is a new_record? (in which case it will POST the data) or an existing record (in which it will b e a PATCH most likely).
To build a nested route, you will need to set an instance variable #log (I do not see that in your code, but you probably do that already), and then you can write:
form_for [#log, #meal] do |f|
which will calculate the correct path.
The error you get:
Routing Error
No route matches [POST] "/logs/1/meals/13/edit
indicates that your form does a POST instead of a PUT.
To make this work, just add method: :put to the form_for declaration:
<%= form_for(:meal, method: :put) do |f| %>

Rails Ancestry. How can I display a single comment with all its descendants?

helpers/comments_helpers.rb
def nested_comments(comments)
comments.map do |comment, sub_comments|
render(comment) + content_tag(:div, nested_comments(sub_comments), :class => 'nested_comments')
end.join.html_safe
end
def nested_comment(one_comment)
one_comment.instance_eval do |comment, sub_comments|
render(comment) + content_tag(:div, nested_comments(sub_comments), :class => 'nested_comments')
end.join.html_safe
end
controllers/comments_controller.rb
def show
#comment = Comment.find(params[:id])
end
views/comments/show.html.erb
...
<%= nested_comment(#comment) %>
...
I keep getting this error and I don't know why:
undefined method `render' for #
If I remove the render part of the method, I get another error for content_tag
Can someone tell me how to fix these errors?
Comments Schema(From comments section below)
create_table "comments", force: :cascade do |t|
t.text "content"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "ancestry"
end
Does this work? I am taking a stab in the dark because Comment is not very clearly defined.
I have never used ancestry before but here is a wild guess on implementation based on the documentation:
def nest_comments(*comments)
comments.map do |comment|
comment_block = render(comment)
comment_block += content_tag(:div,
nested_comments(*comment.children),
:class => 'nested_comments') if comment.has_children?
comment_block
end.join.html_safe
end
This will recursively run through all the comments and sub comments (beware of the extreme n+1 issues here because if sub_comments executes a new query recursively it could create performance impacts very quickly.
You should be able to call like
<%= nest_comments(#comment) %>
or
#comments = Comment.all
<%= nest_comments(*#comments.to_a) %>
The second one may have increasing performance impacts if the comments are not top level and they are called recursively multiple times but without a better understanding this question is hard to answer.

ActiveRecord::RecordNotFound in Controller#destroy

I'm trying to get the controller's "destroy" to work correctly and I'm wondering what the correct set up should be.
The error that I'm getting is
ActiveRecord::RecordNotFound in AuthenticationsController#destroy
Couldn't find Authentication without an ID
My controller looks like
class AuthenticationsController < InheritedResources::Base
def destroy
#authentication = current_user.authentications.find(params[:id])
#authentication.destroy
redirect_to(:back)
end
database table
create_table "authentications", :force => true do |t|
t.integer "user_id"
t.string "provider"
t.string "uid"
t.string "secret"
t.string "token"
end
I have tried other parameters such as :user_id
How can I get users to destroy their tokens? (with the option to re-authenticate later)
You're not passing id to controller
try
<%= link_to "Disconnect Your Authentication", {:controller=>'authentications', :action=>'destroy', :id=>current_user.authentication_id} %>
or use path helper with #autentication argument as option.
(You will need to edit your routes file)
If you're wanting to destroy all authentications for a user, you could certainly change your controller's destroy method to be:
def destroy
current_user.authentications.destroy_all
end
A more conventional approach would be to destroy a particular authentication. In that case the link_to method needs a path that includes an id parameter (which will end up as your params[:id] value in the controller). You can imagine a view snippet like the following that displays all a user's authentications, each with a destroy link:
<ul>
<% current_user.authentications.each do |a| %>
<li>
<%= a.provider %>
-
<%= link_to 'Disconnect Your Authentication', authentication_path(a), :method => :delete %>
</li>
<% end %>
</ul>
This assumes current_user is a helper and that your routes are set up on your authentication model. The authentication_path helper uses the a authentication instance to generate a path, complete with an id parameter.

undefined method `admin?' for nil:NilClass

I followed railscast #250 Authentication from Scratch & got everthing wworking fine. Now I'm trying to only display edit & destroy links on my index page to admin user's.
I've set up mu User database with a admin boolean field & tried putting a simple if statement in the view of another model (hikingtrails) to only display certain links to admin users but I get this error when I try it out, undefined method 'admin?' for nil:NilClass
Database Schema
create_table "users", :force => true do |t|
t.string "email"
t.string "password_digest"
t.boolean "admin"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
User Model
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation#, :admin
validates :email, :uniqueness => true
has_secure_password
end
Application Controller
class ApplicationController < ActionController::Base
protect_from_forgery
# fetch the currently logged-in user record to see if the user is currently logged in
# putting this method in ApplicationController so that it’s available in all controllers
private
def current_user
# checks for a User based on the session’s user id that was stored when they logged in, and stores result in an instance variable
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
# to give access to this method from all the views, the helper_method makes it a helper method
helper_method :current_user
# basic authorization, user must be logged in!
def authorize
redirect_to login_url, alert: "You must be logged in to perform this action" if current_user.nil?
end
end
views/hikingtrails/index.html.erb
<% if current_user.admin? %>
<%= link_to t('.edit', :default => t("helpers.links.edit")),
edit_hikingtrail_path(hikingtrail), :class => 'btn btn-mini' %>
<%= link_to t('.destroy', :default => t("helpers.links.destroy")),
hikingtrail_path(hikingtrail),
:method => :delete,
:data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
:class => 'btn btn-mini btn-danger' %>
<% end %>
current_user will be nil if a user is not logged in according to your code. So you need to do this:
<% if current_user && current_user.admin? %>
or using the try method Rails adds to all objects.
<% if current_user.try(:admin?) %>
as Dogbert said, current_user will be nil if the user is not logged in.
I would suggest two other alternatives:
1) in the current_user method return a special type "guest" user instead of nil. Il will be useful in case you want to do something else with it later, for example in response to some user action.
As inspiration, look at how Ryan Bates explains the Ability class of his gem cancan: link.
The first thing he does is creating an unitilized (and not persisted in DB) user. An that Ability class will be instantiated each time Rails will parse an ERB template with that kind of user verification.
So, you could do:
def current_user
#current_user ||= ((User.find(session[:user_id]) if session[:user_id]) || User.new)
end
So, if (User.find(session[:user_id]) if session[:user_id]) returns nil, the #current_user will be set to an uninitialized User with no identity in DB.
2) define a new metod just to check if the user is an admin, for example:
# your unmodified current_user implementation
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def is_an_admin?
if current_user && current_user.admin?
end
So that you can use it in this way:
<% if is_an_admin? %>
<div>
<%= do stuff....%>
...It might be an extra method call, but it might also make your code more readable.
I know this is old, but if someone is googling the error as I did, there is actually no error in Rails Tutorial, but they forgot to highlight one thing they added.
Listing 9.54
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
Note that they added :destroy action here, not added before, which makes sure that the user is logged to perform destroy action and just then checks if he's an admin
before_action :admin_user, only: :destroy
Correction:
As of the time of this edit 12/14/2015, the rails tutorial now adds the :destroy action in Listing 9.53. If you miss that one as I did, you will get this error.
it looks like in your User model:
attr_accessible :email, :password, :password_confirmation#, :admin
admin is commented out, you need to delete the #.

Creating scores for an action in a separate table in Ruby

I can create a Game, in that Game I can create Rules, I have a
button that "Completes" the Rule, by changing the Rule boolean
:completed column to false, which pushes it to the "Rules Completed"
view, I have a button that "renews" the Rule, by changing the Rule
boolean :completed column back to true.
So, my next task is to create a create method in the new Score controller that:
Via the same "Completed" button_to action,
Finds the rule of that button_to and inserts that rule ID to a new
table, Score, in column "rule_id".
It also posts the time of completion (the time the user hits the
button) to the Score table, column "complete_time",
What I have tried is to simply copy most of the details of the create
method in the Rule controller, which is:
def create
#rule = #game.rules.new(params[:rule])
if #rule.save
flash[:notice] = "You have added a Rule to your Game!"
redirect_to game_url(#game)
else
flash[:error] = "We couldn't add your Rule."
redirect_to game_url(#game)
end
end
My latest efforts at repeating this is to post the rule_id to the Score table, column "rule_id" as follows:
def create
#rule = Rule.find(params[:id])
#score = #rule.scores.new(params[:rule_id])
if #score.save
flash[:notice] = "You scored!"
redirect_to game_url(#game)
else
flash[:error] = "Wide right, try again."
redirect_to game_url(#game)
end
end
my new score db is:
class CreateScores < ActiveRecord::Migration
def change
create_table :scores do |t|
t.integer :rule_id
t.datetime :completed_time
t.timestamps
end
end
end
my proposed Score button action is:
<%= button_to "Score!", score_path(#game.id,rule.id) %>
and routes are set as:
Tgom::Application.routes.draw do
resources :games do
resources :rules do
resources :scores do
end
end
end
match 'games/:game_id/rules/:id/complete' => 'rules#complete', :as => :complete_rule
match 'games/:game_id/rules/:rule_id/scores' => 'scores#create', :as => :score
match 'games/:game_id/rules/:id/uncomplete' => 'rules#uncomplete', :as => :uncomplete_rule
root :to => 'games#index'
The current error for this setup is reading:
ActiveRecord::RecordNotFound in ScoresController#create
Couldn't find Rule without an ID
Rails.root: c:/Sites/tgom
app/controllers/scores_controller.rb:9:in `create'
your request via score_path url (as per your route) will set :game_id and :rule_id params.
In particular if you press the button for
<%= button_to "Score!", score_path(#game.id,rule.id) %>
params[:game_id] will be #game.id and params[:rule_id] will be rule.id
But your scores controller create method is reading params[:id], which will be nil, hence your error.

Resources