Rails - Add Record to join table from controller - ruby-on-rails

I'm trying to create a record within a join table from the action of a button. I would have an events model and would like to track selected events from each user.
I used the HABTM relationship since I don't really need any extra fields.
User.rb:
has_to_and_belongs_to_many :events
Event.rb:
has_to_and_belongs_to_many :users
Events_Users Migration:
[user_id, event_id, id=>false]
I'm getting stuck on the actual creation of the record. Someone helped me earlier with adding the record in within the console:
u = User.find(1)
u.events << Event.find(1)
Now I would like to perform the action as a result of clicking a link... Is this in the right direction?
def add
#user = User.find(session[:user_id])
#event = Event.find(params[:id])
if #user.events.save(params[:user][:event])
flash[:notice] = 'Event was saved.'
end
end
Should I add a #user.events.new somewhere and if so where do I put the params of which user and which event?

The following code should work (assuming that you pass in an parameter with the name id that corresponds to the id of an event object):
def add
#user = User.find(session[:user_id])
#event = Event.find(params[:id])
#user.events << #event
flash[:notice] = 'Event was saved.'
end
The problems I see in your code are:
You are passing a hash to .save. Save should only take a boolean value corresponding whether validations should be run and is true by default. However .create and .new can accept a hash of values. (.save would be used after .new).
You load an event through params[:id] but then you attempt to create an event through params[:user][:event]. Which do you want to do? Create or load? (my example assumes load)
Actions that have an effect such as this one should happen when a user clicks a button and submits a form rather than 'clicking a link'. This code may be vulnerable to cross site request forgery (Someone could trick someone into clicking a link on another site that ran this action). Rails forms, if correctly implemented, are protected against this because they use a request forgery protection token.
Most likely you want to redirect the user after this action. Rendering pages after executing actions like this (rather than redirecting) is considered bad practice.

What you did in the console you need to do in the controller.
def add
#user = User.find(session[:user_id])
#event = Event.find(params[:id])
#user.events << #event
flash[:notice] = 'Event was saved.'
end
The thing to note here is that the << operator for existing records will cause the association to be persisted immediately.
Take a look at the ActiveRecord documentation for more info.

If the event_id is passed as params[:id] and you are adding only one event in this call then, you can do the following in your controller code:
User.find(session[:user_id]).events << Event.find(params[:id])
flash[:notice] = 'Event was saved.'
You don't need explicit save to save the has_many association of an existing model instance.
Scenario 1
u = User.new(..)
u.events << Event.first
# Now you need to call `save` in order to save the user object
# and the events association
u.save
Scenario 2
u = User.first
u.events << Event.first
# Don't need to call `save` on `u` OR `u.events`

Related

Rails: first_or_create not saving

My goal for my application is to only show a form page with existing data or a blank form if new. I've accomplished this by using a callback that created a blank record when the user is created.
User model:
before_create :build_health_profile
However, if for whatever reason a users "health_profile" were to be destroyed or non-existant, it breaks my entire app with:
"undefined method `health_profile' for nil:NilClass"
It was mentioned to me that the "first_or_create" method could solve this by show a new form or finding the existing one, but I can't get it to save the fields. It directs to my root with my save alert like it saved, but nothing gets actually saved.
Controller:
class HealthProfilesController < ApplicationController
def new
#health_profile = current_user.build_health_profile
end
def create
#health_profile = HealthProfile.where(user_id: current_user).first_or_create(health_profile_params)
if #health_profile.save
flash[:success] = "Health profile saved."
redirect_to root_path
else
render 'new'
end
end
private
def health_profile_params
params.require(:health_profile).permit(
:age,
:weight,
:height,
:gender
)
end
end
I've seen where I could use a block for "first_or_create", but no luck getting that to work.
View:
<%= link_to "Health Profile", new_health_profile_path %>
Models:
class User < ActiveRecord::Base
has_one :health_profile, dependent: :destroy
end
class HealthProfile < ActiveRecord::Base
belongs_to :user
end
If you use first_or_create then that calls the save method as part of it on the record and tries to save that in the database. If it can't save the record, then the transaction is rolled back. So, you want to use: first_or_initialize here which is like new and does not save the record in the database immediately. It just loads the data. So, you can call save on it in the next line of your code.
So, in your code, where you have:
#health_profile = HealthProfile.where(user_id: current_user).first_or_create(health_profile_params)
Here you are not controlling the save part, that's already being done by the first_or_create method.
So, you actually want to just load the object (NOT save yet) by using first_or_initialize:
#health_profile = HealthProfile.where(user_id: current_user).first_or_initialize(health_profile_params)
and then, in the next line, you can call the save and based on it's return value you can take the decision:
if #health_profile.save
# do stuff if successfully saved health_profile
else
# otherwise
render 'new'
end
Because you have #health_profile.save,
You should change first_or_create into first_or_initialize
first_or_create immediately trigger save, whereas first_or_initialize would just assign the values to a New record or to an already existing record if record exists already
I was able to fix the problem of the record resetting itself when going back to the form by adjusting the new action. Thats everyone for the help.
def new
#health_profile = current_user.health_profile || HealthProfile.new
end
def create
#health_profile = HealthProfile.where(user_id: current_user).first_or_initialize(health_profile_params)
if #health_profile.save
flash[:success] = "Health profile saved."
redirect_to root_path
else
render 'new'
end
end

Disable autoincrementing id inside of ActiveRecord::Base.transaction

I have a FormObject for registration which creates a user and a lot of models for him inside create method.
def create
ActiveRecord::Base.transaction do
#user = User.create(#params[:user])
process_bonuses_for_user
process_actions_for_user
# et.c., giant amount of methods
end
#user.persisted? # to return the true of false to controller
end
I met the strange behaviour of my FormObject. Even it ran successfull (create a lot of models) or unsuccessful (not saving them), the id of User model is autoincrementing. So, every trying to save something using my FormObject increment the value of next id for User. This is normal situation when User created successfully, but not normal when user makes a mistake on registration form.
How can I disable this unpredictable behaviour?
P.S. I know that everything is work when I write #user = User.new(#params[:user]) at the start of create method and #user.save at the end, but there are a lot of associations, and I don't want to write a lot of autosave or inverse_of in my models.
P.P.S. I'm postgresql-9.4 user
Your transaction is not working because you're using create. You need to use the bang version (create!) to raise an exception on failure which triggers the rollback. Do note that you'll need to rescue the InvalidRecord exception yourself.
In my opinion, it could be something like this:
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
process_bonuses_for_user
process_actions_for_user
# et.c., giant amount of methods
...
format.html { redirect_to ... }
end
end

Get any id to any user exists in the database

I am new to rails and have a task that asks me to send an invitation for any user to be admin in my magazine here is my piece of code
def invite
inviteUser = { 'user_id' => current_user.id, 'Magazine_id' => params[:id] }
CollaborationInvitation.create(inviteUser)
#magazine = Magazine.find(params[:id])
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to root_path
end
I need to replace current_user.id with something that refers to any user's id which exists in my database to send him an invitation to be admin with me I tried to add #User=Users.All and then pass it as a variable but it got me an error I tried a lot of things but every time I get an error except for adding current_user.id
ps: I am using devise for authentication
You asked a couple things, and it is kind of confusing what you want to do.
Here is how you get all ids of records in a model.
Rails4: User.ids
Rails3: User.all.map(&:id)
Or (not sure if #pluck is in Rails 3 or not)
User.pluck(:id)
If you want to get a random user (you mentioned "any user") you could do.
User.find(User.pluck(:id).sample)
Though I think what you really want to do is to pass the id or some other attribute of a user as a param to the action and send that user an invitation.
Presumably you either have a post or get route for "users#invite" (the action you wrote in your question). You can add a named parameter there or you can pass a url param or if you are using a post route, you could add the param to the post body.
Then in your contoller you can do something like this (I'll use email as an attribute):
def invite
#user = User.find_by(email: params[:user_email])
#Rails 3 like this
# #user = User.find_by_email(params[:user_email])
# now do stuff with user
end
User.all will return you the collection of users. So,
Find the user object to get an id...
Try this code....
def invite
inviteUser = { 'user_id' => User.find_by_email('user#example.com').id, 'Magazine_id' => params[:id] }
CollaborationInvitation.create(inviteUser)
#magazine = Magazine.find(params[:id])
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to root_path
end
You can try
User.last.id
or
User.find_by_email("xyz#test.com").id
or
User.where(email: "xyz#test.com").first.id
Replace xyz#test.com with desired user email address. To get more details on rails active record query interface, please read rails guides http://guides.rubyonrails.org/active_record_querying.html

updating user review in Ruby on Rails

Hi i was wondering if there was a way a user can update a review they have already written, i tried using cancan but ran into a few problems so i rather find out if there is an easier way. This is code from the 'new' method in the reviews controller
def new
if logged_in?
#review = Review.new(:film_id => params[:id], :name =>
User.find(session[:user_id]).name)
session[:return_to] = nil
else
session[:return_to] = request.url
redirect_to login_path, alert: "You must be logged in to write a review"
end
end
and the 'create' method
def create
# use the class method 'new' with the parameter 'review', populated
# with values from a form
#review = Review.new(params[:review])
# attempt to save to the database, the new review instance variable
if #review.save
# use the class method 'find' with the id of the product of the
# saved review and assign this product object to the variable 'product'
film = Film.find(#review.film.id)
# redirect the reviewer to the show page of the product they reviewed,
# using the product variable, and send a notice indicating the review
# was successfully added
redirect_to film, notice: "Your review was successfully added"
else
# if the review could not be saved, return / render the new form
render action: "new"
end
end
i want the user to edit their review if they have already written a review for a product. Instead of having two reviews from the same user for the same product.
You could potentially sub something like this into the create method:
# Assumes that your user names are unique
#review = Review.find_or_create_by_film_id_and_name(params[:review][:film_id], User.find(session[:user_id]).name)
#review.update_attributes(params[:review])
This does the following
Checks whether the user has created a review for the film
If yes, assigns the existing review to the #review instance variable
If not, creates a new Review object and assigns it to #review
Updates #review with params[:review]
Alternatively, the following statements will accomplish the same without using the Rails find_or_create convenience method:
user_name = User.find(session[:user_id]).name # To avoid two DB lookups below
#review = Review.find_by_film_id_and_name(params[:review][:film_id], user_name) || Review.new(:film_id => params[:review][:film_id], :name => user_name)
#review.update_attributes(params[:review])
To update a record, you should use the update action, which is requested after the user submitted the edit form.
Make your User model have has_many/has_one :reviews. And Review model belongs_to :user. And then if you have any kind of authorization(and you should have, for ex: devise) you'll know if user of review is currently logged user. If so then render edit button, otherwise not render.
Also according to CRUD conventions, there are 2 actions you need. First its edit and other one update. You can read about it on railsguides.com

Passing parameters for nested relationship

I am having trouble passing parameters
My application that is setup like this:
Fact belongs_to Source
Source has_many Facts
Source is nested under User in routes
I am using the Facts form to create the Source data. So I have getter and setter methods in the Facts model like this:
def source_name
source.try(:name)
end
def source_name=(name)
self.source = source.find_or_create_by_name(name) if name.present?
end
This is working great, but it is not setting the user_id for the parent User attribute. As a result, sources are created, but they are not associated with the User.
I have a hidden field with user_id in the form, but the user_id is still being set. What is the easiest way to pass and save the user_id so the nested relationship is set?
Here is the create method for the Source controller:
def create
#user = User.find(params[:user_id])
#source = #user.source.build(params[:source])
...
end
I think the problem is that you are creating source directly from the setter method in the Fact model. Unless you establish the chain by using something like build in the FactController, the user_id will not be set. What you are doing in SourceController needs to be done in the FactsController too. Also, it seems that the ids are set only for the immediate parent when you use the build command. You can try something as below:
def create
#source = current_user.sources.find_or_create_by_name(params["source_name"])
#fact = #source.facts.build(:user_id => #source.user_id)
....
end
Hope that helps.
If your user has a single Source, try the following as your create() method:
def create
#user = User.find params[:user_id]
#user.source = Source.new params[:source]
if #user.save
redirect_to #user, :flash => { :success => "Source updated!" }
else
flash[:error] = "Failed to update the source!"
render :action => "new"
end
end
Creating the Source as an attribute on the User object and then saving the User object should automatically associate the Source with the User.

Resources