I'm trying to make a simple movie database using Rails 4.0.0 as a learning project. I'm particularly interested in using scaffolding as much as possible, as this is one of the features that drew me to RoR in the first place.
Yes, I do realize the potential risks (Box 1.2.Scaffolding: Quicker, easier, more seductive), but I promise I won't have my project go public before I really understand what's going on beneath the hood. Right now I'm more in "evaluating technologies for my next super duper project"-mode.
Here's what I've got so far:
rails g scaffold Person name:string
rails g scaffold Movie name:string
Now, I could've done something like
rails g scaffold Movie name:string person_id:integer
instead, but I want a movie to be associated with both a director and an actor. (Next step is to make an association that relates multiple actors to a single movie, but I'm not quite there yet.)
So, I headed over to the blog post Creating Multiple Associations With the Same Table, describing pretty much what I need. It's a somewhat old post, so things might have changed now - I don't know. Anyway. This how I changed the models:
class Person < ActiveRecord::Base
has_many :movies
end
and
class Movie < ActiveRecord::Base
belongs_to :director_id, :class_name => 'Person', :foreign_key => 'person_id'
belongs_to :actor_id, :class_name => 'Person', :foreign_key => 'actor_id'
end
and finally the magical
rake db:migrate
Starting the WEBrick by running rails s in the console, I open my browser and start registering people
The time has come to start registering movies. According to previous questions here and here I have to make a migration script in order to create the necessary database fields. So this is what I did:
rails g migration AddPersonIdsToMovies director_id:integer actor_id:integer
I also updated the app/views/movies/_form.html.erb to
<%= form_for(#movie) do |f| %>
<% if #movie.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#movie.errors.count, "error") %> prohibited this movie from being saved:</h2>
<ul>
<% #movie.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :director_id %><br>
<%= f.select :director_id, Person.all.collect {|x| [x.name, x.id]}, {}, :multiple => false %>
</div>
<div class="field">
<%= f.label :actor_id %><br>
<%= f.select :actor_id, Person.all.collect {|x| [x.name, x.id]}, {}, :multiple => false %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
When I create a new movie, the view shows up fine and the select inputs works fine. However, the data in the director and actor field isn't persisted. I ran rails console and looked at the newly created movie:
irb(main):004:0> mov = Movie.first
Movie Load (0.2ms) SELECT "movies".* FROM "movies" ORDER BY "movies"."id"
ASC LIMIT 1 => #<Movie id: 1, name: "No such movie", created_at:
"2013-08-02 17:02:12", updated_at: "2013-08-02 17:02:12",
director_id: nil, actor_id: nil>
which is kind'a disappointing with no director or actor info.
Update
Based on #Mattherick's suggesition, I edited the private part of the movies_controller.rb to this:
# Never trust parameters from the scary internet, only allow the white list through.
def movie_params
params.require(:movie).permit(:name, :director_id, :actor_id)
end
Unfortunately, when I post a new movie I get
Person(#70319935588740) expected, got String(#70319918738480)
Extracted source:
# POST /movies.json
def create
#movie = Movie.new(movie_params)
respond_to do |format|
if #movie.save
and the request data goes as
{"utf8"=>"✓",
"authenticity_token"=>"???",
"movie"=>{"name"=>"Romantic Comedy",
"director_id"=>"2",
"actor_id"=>"1"},
"commit"=>"Create Movie"}
Update 2
I tried to create a new Movie in the rails console, like this:
irb(main):001:0> movie = Movie.new(name: "My fine movie", director_id: "1", actor_id: "2")
ActiveRecord::AssociationTypeMismatch: Person(#70311109773080) expected, got String(#70311102311480)
which is what you'd expect from the POST to the controller. This made me test what happened if I excluded the quotation marks for director_id and actor_id. So I did
irb(main):005:0> movie = Movie.new(name: "My fine movie", director_id: 1, actor_id: 2)
ActiveRecord::AssociationTypeMismatch: Person(#70282707507880) expected, got Fixnum(#70282677499540)
Still using the console, I decided to create an actor and a director
director = Person.new(name: "Woody Allen")
director.save
actor = Person.new(name: "Arnold Schwarzenegger")
actor.save
and then I did
movie = Movie.new(name: "I'd like to see that", director_id: director, actor_id: actor)
movie.save
which worked like a charm (output omitted for brevity). So the whole question boils down to "How can I pass a Person as the argument to director_id and actor_id through the web interface?"
If I had a single field in Movies called person_id: integer, I believe that rails would've inferred that I'm not trying to pass a string containing the id of a person, but rather I'm trying to pass an entire person object.
Update 3
I tested my suspicion that rails understands how to deal with posts when the foreign key column is named after the pattern [table]_id. So I created a new project with a Continent model and a Country model, where rails g scaffold Country name:string continent_id:integer. I changed my Country view to include
<div class="field">
<%= f.label :continent_id %><br>
<%= f.select :continent_id, Continent.all.collect {|x| [x.name, x.id]} %>
</div>
instead of the default numeric field. The continent_id is still posted a string:
Started POST "/countries" for 127.0.0.1 at 2013-08-03 10:40:40 +0200
Processing by CountriesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"???", "country"=>{"name"=>"Uganda", "continent_id"=>"4"}, "commit"=>"Create Country"}
yet rails understood that continent_id was the identifier of an entry in the Continent table.
Sadly, the inferrer mechanism doesn't work in my original case, as I have two associations from Movie to the Person table. I need to somehow make sure rails understand that there is a mapping from director_id to a Person.
Update 4
According to some sources, it seems as I need to refine the Person model further. So I did
class Person < ActiveRecord::Base
has_many :directed_movies, :class_name => 'Movie', :foreign_key => 'director_id'
has_many :acted_movies, :class_name => 'Movie', :foreign_key => 'actor_id'
end
but still not fixing my problems.
I'm kind'a lost. Can anyone give me a few pointers on what I'm missing here? Can anyone help me map from director_id to person_id? Thanks!
Ok, so I finally got it. I don't think this is the correct way to go about this, but at least it solved the problem. As I mentioned in update 2, I was able to create a Movie object in the irb, and so I asked the question "How can I pass a Person as the argument to director_id and actor_id through the web interface?"
According to the sources here and elsewhere, rails should've understood the has_many and belongs_to class methods. However, I just can't seem to get it to work.
So I hacked the create method in movies_controller.rb to read like this:
def create
director = Person.where(:id => movie_params[:director_id]).first
actor = Person.where(:id => movie_params[:actor_id]).first
#movie = Movie.new(name: movie_params[:name], director_id: director, actor_id: actor)
respond_to do |format|
if #movie.save
format.html { redirect_to #movie, notice: 'Movie was successfully created.' }
format.json { render action: 'show', status: :created, location: #movie }
else
format.html { render action: 'new' }
format.json { render json: #movie.errors, status: :unprocessable_entity }
end
end
end
This is certainly not as elegant as I'd like it to be, and I don't think it is the RoR way to do things. Unfortunately it is the only thing I got working so far, but if anyone else can make a multiple association to the same model using rails 4, please do give me a heads up! :)
Related
I am having some trouble saving arrays in Rails.
Rails version: 4.2.3 | Ruby version: 2.2.1 | DB: PostgreSQL
In my view, I have a collection of check boxes that shows the conferences that my member attended.
<%= f.fields_for :conferences_member do |conference| %>
<%= collection_check_boxes(:conferences_member, :conference_id, #all_conferences, :id, :name)%>
<% end %>
I put a break point (binding.pry) after the create action in my MembersController, and surprisingly, it shows the selected check boxes:
Processing by Database::MembersController#create as HTML
Parameters: {"utf8"=>"✓","authenticity_token"=>"XYZ==",
[...] "conferences_member"=> {"conference_id"=>["3", "5", ""]}, [...]
Now, if I go to rails c, and type ConferencesMember.last to check what was saved, I get:
pry(main)> ConferencesMember.last
ConferencesMember Load (0.5ms) SELECT "conferences_members".* FROM
"conferences_members" ORDER BY "conferences_members"."id" DESC LIMIT 1
=> nil
These are my associations:
#=> member.rb
has_one :conferences_member
accepts_nested_attributes_for :conferences_member, allow_destroy: true, reject_if: :all_blank
#=> conferences_member.rb
serialize :conference_id, Array
belongs_to :member
#=> members_controller.rb
params.require(:member).permit( [...]
:conference_member_attributes => [:id, :member_id, :conference_id => []],
[...])
I want to thank you in advance. I've tried almost everything here on StackOverflow, but I don't see my error.
Thank you again.
EDIT:
More of my MembersController:
def new
#member = Member.new
#member.build_conferences_member
end
def create
#member = Member.new(member_params)
binding.pry
end
The log doesn't show any error, it just shows that conferences were not saved at all.
First, your field needs to be renamed to nest the :conference_id in :conferences_member_attributes (not in :conferences_member as you do now). Take advantage of the form object yielded by fields_for:
<%= f.fields_for :conferences_member do |conference| %>
<%= conference.collection_check_boxes :conference_id, #all_conferences, :id, :name %>
<% end %>
You also need to actually save the record in the create action: Member.new builds the record but does not save it. Typically, the create action branches based on whether the record saved or did not (due to validations). So you might rewrite this method like so:
def create
#member = Member.new(member_params)
# when #member.save returns true, it saved to the db successfully
if #member.save
redirect_to members_path, notice: "Member #{#member.id} saved!"
# otherwise, it didn't save because of a validation error, so we render the error
# to the user and give them a chance to fix it
else
flash[:error] = "Member didn't save: #{#member.errors.full_messages.to_sentence}"
render :new
end
end
Lastly, to make sure your data gets through your strong parameters, check your logs for any messages that parameters were filtered out. The messages look like:
Unpermitted parameters: your_favorite_attribute
Hi I am trying to use Rails on Rest 2 - movielist tutorial with rails 4 and making adjustments as I go for new rails. I am stuck on associations of Movies Roles. I have added to Movie.rb
class Movie < ActiveRecord::Base
has_many :roles, :dependent => :destroy
has_many :people, :through => :roles
validates_presence_of :title
def new_role=(values)
values.each do |i, hash|
unless hash[:name].blank?
roles.create(:person_id => hash[:person_id], :name => hash[:name])
roles.save
end
end
end
def deleted_roles=(values)
values.each do |role_id|
roles.find(role_id).destroy
end
end
end
and also to show _Form.html.rb that I render (excert below)
<b>Add New People</b><br />
<% (1..3).each do |i| %>
<%= select_tag 'movie_new_role_person_id', options_for_select(#people), {
:name => "movie[new_role][#{i}][person_id]"
} %>
<%= text_field_tag 'movie_new_role_name', '', {
:name => "movie[new_role][#{i}][name]"
} %><br />
<% end %>
</p>
<p>
<%= f.submit "Update" %>
</p>
It renders the list to choose from but when i submit - nothing is written to database table.
If I manually enter data in database then it displays on movielist page ie: "Stephen Spielberg - Director" etc...
Any help appreciated- Driving me nuts at this stage
I thought it might be params driven restriction but I do not have a good example of associations style params filter
Thanks
Alan
After debugging including Chicagogrrl's !flag I investigated the params.permits in the movies_Controller again and figured I would have to add the method types to the list of excepted. I could not find detailed info on syntax for this but trial and error paid off.
app/controllers/movies_controllers.rb (excerpt)
......
# Never trust parameters from the scary internet, only allow the white list through.
def movie_params
params.require(:movie).permit(:title,:description, :rating, :role, :deleted_roles=>[],
:new_role=> ['person_id', 'name'])
end
......
The delete_roles=>[] takes the array params and process to delete_roles method in movies.rb
the new_role=>['person_id', 'name'] takes the new_role individual params.
I Hope this saves somebody else some time andIf anybody needs anymore info just ask thanks again Alan
I get
ActiveRecord::RecordNotFound: Couldn't find Client with ID=3 for Order with ID=
when trying to submit an Order form for an existing client. This happens through the form or the console by typing:
Order.new(:client_attributes => { :id => 3 })
payment_form.html.erb:
<%= semantic_form_for #order, :url => checkout_purchase_url(:secure => true) do |f| %>
<%= f.inputs "Personal Information" do %>
<%= f.semantic_fields_for :client do |ff| %>
<%= ff.input :first_name %>
<%= ff.input :last_name %>
<!-- looks like semantic_fields_for auto-inserts a hidden field for client ID -->
<% end %>
<% end %>
<% end %>
Order.rb:
class Order < ActiveRecord::Base
belongs_to :client
accepts_nested_attributes_for :client, :reject_if => :check_client
def check_client(client_attr)
if _client = Client.find(client_attr['id'])
self.client = _client
return true
else
return false
end
end
end
The reject_if idea came from here but I logged the method and it's not even being called! It doesn't matter what its name is!
Note: Feb 2020
Since I'm starting to get downvotes on this 8 years later, adding this note. While this was the original solution I went with 8 years ago, a better one has been proposed by MatayoshiMariano (5 years after my OP).
My Original Fix
Fixed the issue by overloading the client_attributes= method, as described here:
def client_attributes=(client_attrs)
self.client = Client.find_or_initialize_by_id(client_attrs.delete(:id))
self.client.attributes = client_attrs
end
If you only want a new Order with an existing client, without modifying the client, you need to assign the id.
Order.new(client_id: 3)
This is another way to do this without overloading the client_attributes= method and cleanest
The new Order now has the client with ID 3
If you also want to update ant client's attributes you must add the client_attributes, for example:
Order.new(client_id: 3, client_attributes: { id: 3, last_order_at: Time.current })
See https://github.com/rails/rails/issues/7256 from 2012.
If you have has_many relationship, this will work. Tested on Rails 6.0.2
def clients_attributes =(attributes)
# Get IDs for any clients that already exist.
client_ids = attributes.values.map { |a| a[:id] }.compact
# Now find them all and move them to this section.
clients << Client.find(client_ids)
# Update them with standard `accepts_nested_attributes_for` behaviour.
super attributes
end
Had the same error creating a new Thing for existing model with has_many and belongs_to relations.
Fixed it by adding a hidden field for the id of the existing model, for instance User, to the form.
= form.input :user_id, as: :hidden
Then new Thing was created without the error.
(rails 2.2.2)
I have 2 models, user and subscription. Each user can have one ore more subscriptions (= premium services). Below the attributes:
user: id, username, ...
subscription: id, user_id (FK), type, started_at, ended_at
The classes:
class User < ActiveRecord::Base
..
has_many :subscriptions, :dependent => :destroy
..
end
class Subscription < ActiveRecord::Base
belongs_to :user, :foreign_key => :user_id
end
Now I want to make the UI part where existing users can subscribe in their account for the premium services. Therefore I wanted to make a first simple version where the user can subscribe by clicking on a checkbox. This is what I get so far
<div class = 'wrapper'>
<%= render :partial => "my_account_leftbar" %>
<% form_for #subscription, :url => subscribe_user_path(current_user) do |f| %>
<div class="field">
<%= (f.check_box :type?) %> <!-- add '?'after the symbol, source: https://github.com/justinfrench/formtastic/issues/269 -->
</div>
<div class="actions">
<%= f.submit "Subscribe", :class => "button mr8" %>
</div>
<% end %>
</div>
Problems:
the app inserts a record into the db, but the attribute I defined in the form (type) has not been set (it should set '1' which stands for 'standard subscription') . How to get the app set this attribute?
how to set the FK? I assume that rails should set the FK automatically, is that assumption correct?
how to set the other values 'started_at' and 'ended_at? Those are datetime(timestamp) values...
Just run out of my beginner rails knowledge, any help really appreciated...
'Type' is a ruby on rails reserved word which should only be used when you are using Single Table Inheritance. You should rename your column name to something else.
I could solve the other questions 2 and 3 as well, wrapping it up:
insert the record: as stated in the answer from Wahaj, renaming the column 'type' into e.g. 'subscription_type' helped. I created a seperate migration as described here: How can I rename a database column in a Ruby on Rails migration?
storing the FK: updated the action in the controller. Instead of just writing
#subscription = Subscription.new(params[:subscription])
I wrote the following method to create a 'user's subscription'
#subscription = current_user.subscriptions.build(params[:subscription])
storing the 'started_at': added a method to the controller:
#subscription.update_attributes(:started_at => Time.zone.now)
Simply, a Contact can have various associated Time Windows, which may or may not be Active as a Schedule. To wit:
Models
class Contact < ActiveRecord::Base
has_many :schedules
has_many :time_windows, :through => :schedules
accepts_nested_attributes_for :schedules, :allow_destroy => true
end
class TimeWindow < ActiveRecord::Base
has_many :schedules
has_many :contacts, :through => :schedules
end
class Schedule < ActiveRecord::Base
belongs_to :contact
belongs_to :time_window
end
View
<% TimeWindow.all.each do |tw| %>
<% schedule = Schedule.find_by_contact_id_and_time_window_id(#contact.id, tw.id)
schedule ||= Schedule.new %>
<p>
<%= f.label tw.description %>
<%= hidden_field_tag "contact[schedules_attributes][][id]", schedule.id %>
<%= check_box_tag "contact[schedules_attributes][][time_window_id]",
tw.id, #contact.time_windows.include?(tw) %>
<%= check_box_tag "contact[schedules_attributes][][active]", nil,
schedule.active %>
</p>
<% end %>
This submits something like this:
Parameters: { "commit" => "Update", "contact" => {
"group_ids" => ["2"], "enabled" => "1",
"schedules_attributes" => [ { "time_window_id"=>"1", "id"=>"46"},
{ "time_window_id" => "2", "id" => "42", "active" => "on" },
{ "time_window_id" => "3", "id" => "43"},
{ "time_window_id" => "4", "id" => "44", "active" => "on"}],
"last_name" => ...
The update action in the controller is basically stock, except to handle another instance of another related model which I coded using the "Handling Multiple Models" example from the Advanced Rails Recipes book.
According to this API doc, I think the above ought to work. However, nothing about the Schedules is getting updated. This shows up in the server log:
[4;35;1mSchedule Update (0.2ms)[0m [0mUPDATE `schedules` SET `updated_at` = '2010-09-30 20:39:49', `active` = 0 WHERE `id` = 42[0m
[4;36;1mSchedule Update (0.1ms)[0m [0;1mUPDATE `schedules` SET `updated_at` = '2010-09-30 20:39:49', `active` = 0 WHERE `id` = 44[0m
(NetBeans is giving me those stupid "[0m"'s in the output. I don't know what's wrong there.)
The SQL shows that the "active" boolean field is getting set to 0 where checked. How do I get this to correctly set the active bit?
As a followup, how would I organize this to get rid of the Schedule "connection" at all? I'm thinking I need to submit a :_delete with the Schedule from the form, but how would I do that conditionally when a checkbox is involved?
Thanks for any help you can provide. Rails is turning out to be a vast subject for me, and I want to do it "right." I'm really close here, but there's got to be a way to make this -- not just correct -- but elegant. The view code just feels way too cumbersome to be proper Rails. ;-)
I've kept trying different approaches to this problem, and I've come up with this, which works. Mostly. The only problem is that it doesn't handle NOT having a "Schedule" for each "Time Window". The form will render, and I'll get a disabled check_box (to prevent me from trying to delete something that isn't there), but I don't have a way to add it back, and submitting without it throws off the params hash (and causes Rails to give me an "Expected Hash (got Array)" error)
<% TimeWindow.all.each do |tw| %>
<% schedule = Schedule.find_by_contact_id_and_time_window_id(#contact.id, tw.id)
schedule ||= Schedule.new %>
<% f.fields_for "schedules_attributes[]", schedule do |sf| %>
<p>
<%= sf.label tw.description %>
<%= sf.hidden_field :id %>
<%= sf.check_box :_destroy, :disabled => schedule.new_record? %>
<%= sf.check_box :active %>
</p>
<% end %>
<% end %>
Note that the "schedules_attributes[]" array will automatically give you an existing ID within the braces in your HTML (which is nice), but the _attributes hash is expecting an "id" alongside the other attributes in order to make sense of the sub-hashes.
One of the big lessons I've learned here is that the "check_box_tag" method doesn't (seem to) give me a paired-up hidden field for Rails to parse in the unchecked case. I would have expected this. Adding one in by hand made a mess, which led me to finally giving into the "fields_for" method, and trying many incarnations before finding the appropriate syntax to get what I wanted out of it.
I've realized that my model isn't quite appropriate in this setup, so I'm going to change it, but I was so close to this answer, I wanted to at least get to the point of being able to see the end before I moved on.