I am building a Rails project where I have a user, and that user has many Tests (it's like a Trivia game). I have a UsersController where I query the user (going to implement login later).
In my view I have a button which "starts" the test. I need to associate the user with a test, because my User has_many Tests (user_id is a foreign key on tests). My question is, how do I pass my #user object to my TestsController so I can associate the created test with the logged in user?
Here is my UsersController:
class UsersController < ApplicationController
def show
#user = User.find(1)
end
def start_test
redirect_to tests_path
end
end
In my users show view I have:
<p>Welcome <%= #user.name %>!</p>
<%= button_to "Start Test", users_start_test_path %>
When the button is clicked I redirect to tests_path which is in TestsController:
class TestsController < ApplicationController
def index
# here I need to create the Test belonging to the user
end
end
I am new to Rails and don't know how to pass that #user to TestsController so I can create the test belonging to the user. Theoretically I don't even need the whole #user, just the id. Any help would be greatly appreciated.
I think you should improve this by setting the route properly, Instead linking a controller action directly to another controller. I would create the following route.
resources :users do
resources :tests
end
This will force you to always have a user_id. For your new tests path you get this. new_user_test_path(#user)
This will use basics CRUD actions. So to make a new test for the correct user, use the the def new action in the tests controller.
def new
#user = User.find(params[:user_id]
#new_test = #user.tests.new
end
The controllers are standalone. They cann't share variables. You could try to send user id intro the request to controller TestsController (or better use an authentication system). Or directly in the TestsController calls User.find(1) again.
The only variable you can pass between controllers is a parameter through the routing (or the session).
So you can have a HTML Form that passes a body to an action inside a different controller OR for what I see in your case by visiting a route and pass a friendly query string parameter, like an ID.
So you could:
<%= button_to "Start Test", users_start_test_path(user_id: #user.id) %>
And then in your TestController:
class TestsController < ApplicationController
def index
#user = User.find params[:user_id]
end
end
I don't like to pass the current_user variable because it can then be hijacked.
This is how I would set the TestsController
class TestsController < ApplicationController
def index
#tests = current_user.tests
end
def new
#test = current_user.tests.new
end
def create
current_user.tests.create(test_params)
end
def show
#test = current_user.tests.find(params[:id]) # this will only find tests scoped to that user
end
def edit
#test = current_user.tests.find(params[:id]) # this will only find tests scoped to that user
end
def update
#test = current_user.tests.find(params[:id]) # this will only find tests scoped to that user
#test.update(test_params)
end
def test_params
params.require(:test).permit()
end
end
This way now you have the test scoped to the user.
I am assuming that you have device for user management
Related
I am using rails version 4.2 and ruby version 2.2.0. I am trying to save a record to lollypops table. No exceptions indicating reasons.
TASK: As soon as a member is created and saved, I want to populate the lollypops table by calling the create_lollypop(#member.id) in members controller's create method like this:
# POST /members
# POST /members.json
def create
#member = Member.create(members_params)
return unless request.post?
#member.save!
self.current_user = #member
c = Country.find(#member.country_id)
#member.update_attributes(
:country_code=>c.code)
create_lollypop(#member.id) #From here I want to create lollypop
MemberMailer.signup_notification(#member).deliver_now
redirect_to(:controller => '/admin/members', :action => 'show',
:id=> #member.id)
flash[:notice] = "Thanks for signing up! Check your email now to
confirm that your email is correct!"
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
end
def create_lollypop(member_id)
#member = Member.find(member_id)
Lollypop.create(
:member_id=>#member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name)
end
The 'member' is created but the 'lollypops' table is not populated. The associations are:
MEMBER model:
has_one :lollypop, :dependent=>:destroy
LOLLYPOP model
belongs_to :member
If I use generic SQL command then the lollypops table gets populated but I do not want to do that:
def self.create_lollypop(member_id)
member = Member.find(member_id)
ActiveRecord::Base.connection.execute("insert into lollypops (member_id,product_name,product_price,email,house_flat,street,city_town,country,postcode_index,name)
values(#{member.id},'lollypop',#{100},'#{member.email}','#{member.house_flat}','#{member.street}','#{member.city_town}','#{member.country_code}','#{member.postcode_index}','#{member.name}')")
end
Any advice would be welcomed. Thank you.
In your create_lollypop(), You are not defining #member.
def create_lollypop(member_id)
#member = Member.find member_id
Lollypop.create!(
:member_id=>#member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
end
Also use create! so in case any validation failed then it will raise exception. So it will help you sort out issue.
For the moment try to create lollypop using the association method create_lollypop directly in your controller. use this code in you create controller method, note that create_lollypop method will fill (member_id field automatically):
#member = Member.create(members_params)
return unless request.post?
#member.save!
self.current_user = #member
c = Country.find(#member.country_id)
#member.update_attributes(
:country_code=>c.code)
#From here I want to create lollypop
#member.create_lollypop(
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
MemberMailer.signup_notification(#member).deliver_now
redirect_to(:controller => '/admin/members', :action => 'show',
:id=> #member.id)
flash[:notice] = "Thanks for signing up! Check your email now to
confirm that your email is correct!"
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
This is not exactly an answer, more like tips and notes, it's a little long and I hope you don't mind.
return unless request.post?
This is more of a php thing not a rails thing, in rails already the routing is checking this, so you don't need to do this check inside the controller, if it isn't a post it will be routed elsewhere.
#member = Member.create(members_params)
return unless request.post?
#member.save!
Saving after creating is meaningless, because create already saves the data, if you are doing it for the sake of the bang save!, then you could use the create with bang create!, not to mention that you do the redirection check after the member's create, so if this did work, it would leave you with stray members.
c = Country.find(#member.country_id)
#member.update_attributes(:country_code=>c.code)
If you have your assocciations correctly, you don't need to save the code like this, because the member knows that this country_id belongs to a country.
So add this to the member model
class Member < ActiveRecord::Base
has_one :lollypop, dependent: :destroy
belongs_to :country
end
This way you could always call #member.country to return the country object, then the code could come from there, like #member.country.code, or you could just write a method to shorten that up
def country_code
country.code
end
this way will get the code through an extra query, but it has an advantage, if you for any reason change a country's code, you don't need to loop on all members who have that country and update their codes too, you could also shorten this up even more using #delegate
#member.save!
#member.update_attributes(:country_code=>c.code)
Here you are updating the attributes of member after saving the member, which is kinda a waste, because you are doing 2 queries for what could be done with 1 query, programmatically it is correct and it will work, but it's bad for scaling, when more users start using your app the database will be more busy and the responses will be slower.
Instead i would recommend to postpone the creation of member till you have all the data you want
#member = Member.new(members_params) # this won't save to the database yet
#memeber.code = Country.find(#member.country_id).code
#member.save
This will only do 1 query at the end when all data is ready to be saved.
redirect_to(:controller => '/admin/members', :action => 'show', :id=> #member.id)
This is ok, but you probably have a better shorter path name in your routes, something like members_admin_path, check your routes name by doing a bin/rake routes in your terminal.
redirect_to members_admin_path(id: #member)
redirect_to ...
flash[:notice] = "message"
I'm not sure this will work, because the redirection needs to be returned, but when you added the flash after it, either the redirection will happen without the flash, or the flash will be set and returned as it's the last statement, but the redirection won't happen, not sure which will happen, to fix it you can simply swap the two statements, create the flash first and then redirect, or use the more convenient way of setting the flash while redirecting, cause that's supported
redirect_to ....., notice: 'my message'
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
This will do the job, but it isn't conventional, people tend to use the soft save and then do an if condition on the return value, either true or false, here's a short layout
# prepare #member's data
if #member.save
# set flash and redirect
else
load_data
render :new
end
The lollypop creation
Now there's a few things about this, first you have the method in the controller, which is bad cause it shouldn't be the controller's concern, the second method the self.create_lollypop is better cause it's created on the model level, but it's a class method, then the better way is creating it as a member method, this way the member who creates the lollypop already knows the data because it's his own self, notice i don't need to call #member because i am already inside member, so simple calls like id, email will return the member's data
# inside member.rb
def create_lollypop
Lollypop.create!(
member_id: id,
product_name: 'lollypop',
product_price: 100,
email: email,
house_flat: house_flat,
street: street,
city_town: city_town,
country: country,
postcode_index: postcode_index,
name: name
)
end
if you want you can also add this as an after create callback
after_create :create_lollypop
ps: This method name will probably conflict with the ActiveRecords create_lollypop method, so maybe you should pick a different name for this method.
As Mohammad had suggested to me, I changed Lollypop.create to Lollypop.create! and
while running my code, one validation error popped up. After correcting it and
altering my code to:
Lollypop.create!(
:member_id=> #member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country_code,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
The 'lollypops' table got populated.
I am starting to learn rails and have run into a problem. I am writing a simple application (similar to the twitter tutorials I have seen) where a user logs in and creates a new post.
When a user logs in, I am setting the session information as follows
session[:id] = authorized_user.id
session[:email] = authorized_user.email
So now I have the ID of the user logged in. Upon login, the user is brought to a form where they can submit a new post (3 fields.) When user clicks submit, I want to create a new record with the data they entered, and associate the record to that user (User ID). I am not exactly sure how to do this.
Below is the code on the controller:
def create
#Used for creating new status posts
#Need to get the ID of the user logged in
#user = AdminUser.find(session[:id])
#Instantiate new object using form parameters
#post = Post.new(post_params)
#post.AdminUser = #user # THIS IS THE LINE NOT WORKING
#Save the object
if #post.save
#If save succeeds, redirect to the index action
flash[:notice] = "Status has been saved"
redirect_to(:action => 'index')
else
#If the save fails, redisplay the form so user can fix problems
render('new')
end
end
Here is the Private method for post_params
def post_params
#Defining the params that are allowed to be passed with forms.
params.require(:post).permit(:post_status, :post_title, :post_content)
end
The record is saved but the UserID for the record is NULL.
My first instict was to try to pass UserID as a post parameter, but i think this is a potential security risk, so I am trying to figure out an alternate way. I am sure it is something simple and I am just missing it.
Attributes
Firstly,
#post.AdminUser = #user # THIS IS THE LINE NOT WORKING
You should use snake_case for your attribute names (you're using CamelCase). Calling an attribute AdminUser has all sorts of potential issues which will arise down the line.
Call it admin_user or admin_id or something similar
--
Params
Secondly,
I want to create a new record with the data they entered, and
associate the record to that user (User ID)
If you're trying to save a "dependent" record for an object (for example, saving a post for a user), you'll have to assign the user_id record yourself, and pass it through the params, like so:
#app/controllers/posts_controller.rb
Class PostsController < ApplicationController
def create
#post = Post.new(post_params)
#post.save
end
private
def post_params
params.require(:post).permit(:title, :body).merge({user_id: authorized_user.id})
end
end
When you create an element in your app, you're basically just taking data from the params hash & sending to the model to save. This is done using the strong_params functionality introduced in Rails 4:
def post_params
params.require(:post).permit(:title, :body).merge({user_id: authorized_user.id})
end
As you can see from my example above, you basically need to be able to send through the user_id / admin_id / AdminUser value through to the model (so it can save)
You can also do this by setting the attribute as the example below:
#app/controllers/posts_controller.rb
def create
#post = Post.new(post_params)
#post.user_id = authorized_user.id
#post.save
end
private
def post_params
params.require(:post).permit(:title, :body, :user_id)
end
--
You should also look at the difference between authentication & autorhization for better definition of your logged-in user object :)
Rewrite the line like this, taking UserID as the column name in posts table
#post.UserID = #user.id
I'm using devise to authenticate users. I want them to be able to look at the page without being logged in, but if they wish to download the csv data, they must be logged in. Here's how i've set up the csv download part
respond_to do |format|
format.html{
render :layout => 'indices_show'
}
format.csv{
export_to_csv(idxp)
}
end
and here is the export_to_csv function
def export_to_csv(idxf)
cash = params[:cash]
#title = get_title(#index)
if (cash =='1')
navs = #index.navsc.from(idxf)
r = #index.returnsc.from(idxf)
else
navs = #index.navs.from(idxf)
r = #index.returns.from(idxf)
end
dates = #index.dates.from(idxf)
csv_string = CSV.generate do |csv|
csv << [#title]
csv << ["Date", "Return", "NAV"]
dates.each_with_index do |d, i|
csv << [d,r[i],navs[i]]
end
end
send_data csv_string,
:type => 'text/csv; charset=iso-8859-1; header=present',
:disposition => "attachment; filename ="+ #title +".csv"
end
At the top of this controller, I have,
before_action :authenticate_user!, only:[:export_to_csv]
but it doesn't do anything as the user is still able to download the data without being logged in. I've found a semi work around by doing this,
respond_to do |format|
format.html{
render :layout => 'indices_show'
}
format.csv{
if (user_signed_in?)
export_to_csv(idxp)
else
redirect_to new_user_session_path
end
}
end
The problem here is that once the user logs in, it redirects to the homepage. Is there a way such that when a user clicks the download button, they are forced to sign in, and upon signing in they are redirected back to that page and the data is downloaded? Thanks
The before_action isn't working because export_to_csv is not the method being called as the action - that would normally be the method your routing maps to - i.e. probably the method with your respond_to code in it. Of course, you can split the method into two separate ones and have one for html and one for csv and then have the before_action set up for the csv one. That's perhaps not so nice, especially if it's just two different viewing formats for the same data. If they're not very similar views, perhaps it would be appropriate to separate them. It depends on your app.
Another option is to use your second approach but modify it. Before the redirect_to new_user_session_path, try inserting a call to store_location_for(:user, request.request_uri), or perhaps pass request.original_url. See here for the doc/code:
https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/store_location.rb#L26
You can see that if the stored location is present, it's used in preference to the root path:
https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/helpers.rb#L143
Note that store_location_for seems to be a recent addition to devise. I'm actually using 3.1.x and it doesn't seem to be defined - you'd have to set the user_return_to session variable directly because that's what is checked during signin in 3.1.x.
As Tim said, export_to_csv is not the method being filtered by before_action. It is firing actions based on the controller action methods at the top, and since you are trying to limit access lower down, it has already let the user through.
I'm not sure what the action name is that you are using, so I am going to assume it's show.
You can supply a conditional to your before_action line that may limit what you were hoping for.
before_action :authenticate_user!, only: [:show], if: proc { request.csv? }
I'm rendering a model and it's children Books in JSON like so:
{"id":2,"complete":false,"private":false, "books" [{ "id":2,"name":"Some Book"},.....
I then come to update this model by passing the same JSON back to my controller and I get the following error:
ActiveRecord::AssociationTypeMismatch (Book (#2245089560) expected, got ActionController::Parameters(#2153445460))
In my controller I'm using the following to update:
#project.update_attributes!(project_params)
private
def project_params
params.permit(:id, { books: [:id] } )
end
No matter which attributes I whitelist in permit I can't seem to save the child model.
Am I missing something obvious?
Update - another example:
Controller:
def create
#model = Model.new(model_params)
end
def model_params
params.fetch(:model, {}).permit(:child_model => [:name, :other])
end
Request:
post 'api.address/model', :model => { :child_model => { :name => "some name" } }
Model:
accepts_nested_attributes_for :child_model
Error:
expected ChildModel, got ActionController::Parameters
Tried this method to no avail: http://www.rubyexperiments.com/using-strong-parameters-with-nested-forms/
Are you using accepts_nested_attributes_for :books on your project model? If so, instead of "books", the key should be "books_attributes".
def project_params
params.permit(:id, :complete, :false, :private, books_attributes: [:id, :name])
end
I'm using Angular.js & Rails & Rails serializer, and this worked for me:
Model:
has_many :features
accepts_nested_attributes_for :features
ModelSerializer:
has_many :features, root: :features_attributes
Controller:
params.permit features_attributes: [:id, :enabled]
AngularJS:
ng-repeat="feature in model.features_attributes track by feature.id
My solution to this using ember.js was setting the books_attributes mannualy.
In controller:
def project_params
params[:project][:books_attributes] = params[:project][:books_or_whatever_name_relationships_have] if params[:project][:books_or_whatever_name_relationships_have]
params.require(:project).permit(:attr1, :attr2,...., books_attributes: [:book_attr1, :book_attr2, ....])
end
So rails checks and filters the nested attributes as it expected them to come
This worked for me. My parent model was an Artist and the child model was a Url.
class ArtistsController < ApplicationController
def update
artist = Artist.find(params[:id].to_i)
artist.update_attributes(artist_params)
render json: artist
end
private
def artist_params
remap_urls(params.permit(:name, :description, urls: [:id, :url, :title, :_destroy]))
end
def remap_urls(hash)
urls = hash[:urls]
return hash unless urls
hash.reject{|k,v| k == 'urls' }.merge(:urls_attributes => urls)
end
end
class Artist < ActiveRecord::Base
has_many :urls, dependent: :destroy
accepts_nested_attributes_for :urls, allow_destroy: true
end
class Url < ActiveRecord::Base
belongs_to :artist
end
... and in coffeescript (to handle deletions):
#ArtistCtrl = ($scope, $routeParams, $location, API) ->
$scope.destroyUrls = []
$scope.update = (artist) ->
artist.urls.push({id: id, _destroy: true}) for id in $scope.destroyUrls
artist.$update(redirectToShow, artistError)
$scope.deleteURL = (artist,url) ->
artist.urls.splice(artist.urls.indexOf(url),1)
$scope.destroyUrls.push(url.id)
Something is missing from all of the answers, which is the inputs for fields_for in the form.
The form works if you do this:
f.fields_for #model.submodel do ..
However, the form is sent as model[submodel], but that's what causes the error others have mentioned in their answers. If you try to do model.update(model_params), Rails will raise an error that it's expecting a Submodel type.
To fix this, make sure you follow the :name, value format:
f.fields_for :submodel, #model.submodel do ...
Then in the controller, make sure you put _attributes on your params:
def model_params
params.require(:model).permit(submodel_attributes: [:field])
end
Now the save, update, etc. will work fine.
Wasted several days trying to figure out how to use accepts_nested_attributes with Angular, and the issue is always the same: Rails whitelist will not allow the variables into the params hash. I've tried every single different whitelisting syntax that everyone said on SO and other blogs, tried using :inverse, tried using habtm and mas_many_through, tried manually rolling my own solution but that wont work if the whitelist wont allow params through, tried doing what http://guides.rubyonrails.org says about 'Outside the Scope of Strong Parameters', tried removing whitelisting all together which isnt really an option but it causes other problems anyways. Not sure why rails 4 strong parameter whitelisting wont allow arbitrary data thru, thats a huge problem especially if accepts_nested_attributes doesn't work either.... I guess we are left to just create/delete all associations on a separate page/form/controller and look like an idiot making my end users use several forms/pages to do something that should be easily doable on 1 page with 1 form. Ya know, usually I expect Angular to screw me, but this time Angular worked quite well and it was actually Rails 4 that screwed me twice on 1 issue that should be very straightforward.
When a user logs into my site at example.com, I want him to be logged in when he visits something.example.com. How can I accomplish this? (I'm using subdomain-fu if relevant)
Well, you can, just add following lines into /etc/hosts after "127.0.0.1 localhost"
127.0.0.1 localhost.com
127.0.0.1 sub.localhost.com
Then edit your environments/development.rb and add
config.action_controller.session = { :domain => '.localhost.com' }
From now on use http://localhost.com:3000 or the same but with sub-domain to access your app locally.
[update] oops, it was the answer to Horace Loeb
For Rails3 the code above will raise NoMethodError:
undefined method `session=' for ActionController::Base:Class
So, for Rails3 you should not change you environment config but should set your app/config/initializers/session_store.rb to look like:
YourAppName::Application.config.session_store :active_record_store,
{:key => '_your_namespace_session', :domain => '.yourdomain.com'}
Also after changing the initializer you'll need to restart a webserver in order to apply the initializer.
Notice, that users who were logged in before code update won't be able to logout after that because the default logout action which is looking something like:
destroy
current_user_session.destroy
flash[:notice] = "You have been logged out"
redirect_to root_path
end
is not sufficient - it doesn't delete user_credentials cookie set for a non-wildcard domain yourdomain.com by default. So you should add cookies.delete :user_credentials to the destroy action so it will look like this:
destroy
current_user_session.destroy
cookies.delete :user_credentials
flash[:notice] = "You have been logged out"
redirect_to root_path
end
And that's odd but it should be added after destroying user session despite of cookies[:user_credentials].is_nil? == true at this point. Also there is a problem that after a user logouts and then logins having cookies.delete :user_credentials in the destroy action also makes users to be unable to logout and it should be removed. Does anybody have a solution for this?
Update. Finally I came up to this - I added a boolean flag to User model via migration:
class AddReloginedToUsers < ActiveRecord::Migration
def change
add_column :users, :relogined, :boolean, :default => false
end
end
and changed the destroy action this way:
def destroy
current_user_session.destroy
if !current_user.relogined
current_user.relogined = true
current_user.save
cookies.delete(:user_credentials)
end
session = nil
flash[:notice] = "You have been logged out"
redirect_to root_path
end
Now everything works as expected although that's not a very beautiful solution. I'll be glad if anyone provides something smarter.
The fix is to add this to production.rb:
if config.action_controller.session
config.action_controller.session[:domain] = '.your-site.com'
else
config.action_controller.session = { :domain => '.your-site.com' }
end
I still can't get it to work in development with localhost:3000, but whatever