has_and_belongs_to_many validations - ruby-on-rails-3

What is the most straightforward way to check to make sure a creation of a new record includes the creation of a related record via has_and_belongs_to_many? For example, I have:
class Person < ActiveRecord::Base
has_and_belongs_to_many :groups
end
class Group < ActiveRecord::Base
has_and_belongs_to_many :people
end
I want a validation to fire on the creation of a new Person to make sure they belong to at least one group.
Also, how would I build this out in the controller? Right now I have:
def create
#person = current_user.people.new(params[:person])
end
I'd like params to include a group hash as well, to act as a sort of nested resource.
I've looked through the Rails documentation and I haven't been able to find anything on this particular case. If someone could explain this to me or point me in the right direction, I'd be very happy. Thanks!

If you want to give the user the option of creating one or more groups during the creation of a person, and then validate that those groups were created, please specify. Otherwise the remainder of this answer will be dedicated to creating a Person and validating that it is associated with at least one existing group.
If you're asking how to verify the existence of an Person-Group association on the groups_people join table, this could be done with weird sql queries and is inadvisable. Just trust that the well tested ActiveRecord works properly.
You can, however, validate the existence of one or more groups on a Person record before it is saved.
As long as you've migrated a join table called groups_people:
# db/migrate/xxxxxxxxxxxxxx_create_groups_people
class CreateGroupsPeople < ActiveRecord::Migration
def change
create_table :groups_people, :id => false do |t|
t.string :group_id, :null => false
t.string :person_id, :null => false
end
end
end
# $ rake db:migrate
, and your controller is correct:
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
def new
#groups = Group.all
#person = Person.new
end
def create
#person= Person.new(params[:person])
if #person.save
# render/redirect_to and/or flash stuff
else
# render and/or flash stuff
end
end
end
, and you've all existing group options as checkboxes:
# app/views/people/new.html.erb
<%= form_for #person do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
# same for other person attributes
<% #groups.each do |g| %>
<%= check_box_tag 'person[group_ids][]', g.id, false, :id => g.group_name_attr %>
<%= label_tag g.group_name_attr %>
<% end %>
<%= f.submit 'Create!' %>
<% end %>
, then you can validate the presence of groups on your Person record:
class Person < ActiveRecord::Base
validates_presence_of :groups
has_and_belongs_to_many :groups
end
There is validates_associated helper, but wouldn't be necessary in this case, where you show Group.all as checkboxed options.
No accepts_nested_attributes_for is necessary for this. It would be if you were creating a Group for a Person while creating a Person. Again, please specify if this is the case.
Just a note: validating an incoming form that includes Group.all as options and gives the option of creating a group along with the person is possible but complicated. It would involve bypassing existing validations on the Group model, if any, which there probably is.

Related

Rails - Eager load 2 tables, but filter one - Fixing N+1

I have three models:
class User < ActiveRecord::Base
has_many :destinations, :through => :trips
has_many :destination_reviews
class Destination < ActiveRecord::Base
has_many :users, :through => :trips
has_many :destination_reviews
class DestinationReview < ActiveRecord::Base
belongs_to :user
belongs_to :destination
Different users can leave reviews for the same destination.
We're building a table that shows all destinations belonging to a user. The user has left reviews for some (but not all) of those destinations.
Controller:
#user = User.find_by_id(params[:id])
#user_reviews = #user.destination_reviews
View:
<% #user.destinations.each do |destination| %>
<% review = #user_reviews.find_by_destination_id(dest.id) %>
<% if review %>
<div class="review>
...show the review
</div>
<% else %>
<div class="add-review>
...prompt user to add a review
</div>
<% end %>
<% end %>
The N+1 is happening at #user_reviews.find_by_destination_id(dest.id), where #user_reviews is a preloaded list of the user's reviews.
How can I eager load all destinations belonging to this user, including any reviews the user has left for these destinations?
I'm looking for some query like:
dest_ids = current_user.destinations.pluck(:id)`
Destination.includes(:destination_reviews).where(id: dest_ids).where('destination_reviews.user_id = ?', user_id)
But that throws the error:
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "destination_reviews"
Alternatively, if there was a way I could write #user_reviews.find_by_destination_id(dest.id) without having it re-query the database, that would work.
Please let me know if that is clear or if I should add more details
The easiest way is to use group_by:
#user_reviews = #user.desintation_reviews.group_by(&:destination_id)
then in the view
<% review = #user_reviews[dest.id] %>
That gives you one query to get the user's destinations, and another to get their reviews.
Or you could get it down to a single query with some joins plus using select to create some pseudo-attributes on the Destination, and then you can just pull everything you want from dest. It's going to be more complicated though, especially since you're going through a join table. It's going to be something like this (not tested):
current_user.destinations.
joins("LEFT OUTER JOIN destination_reviews r " +
"ON r.user_id = trips.user_id " +
"AND r.destination_id = destinations.id").
select("destinations.*, r.foo AS review_foo, r.bar AS review_bar")
and then this:
<% if destination.review_foo %>

has_many_through association not inserting values in params to join table instead inserting null

EDIT
I am new to rails and got stuck at this step.Researched whatever I could on the internet but could not fix this. Please help! I am using Rails 3.2.13.
This is how my models look now. Excuse me for the typo, if any, as this is a made up example. cleaned up a bit but again the same problem. Could be bug not sure.
I have 3 Models:
1.Cuisine (example Thai/Mexican/Italian)
class Cuisine < ActiveRecord::Base
has_many :testers, :through => :ratings
has_many :ratings, :inverse_of => :cuisine
2.Testers
class Tester < ActiveRecord::Base
has_many :cuisines, :through => :ratings
has_many :ratings, :inverse_of => :tester
3.Rating (note:had the inverse_of here too but did not work)
class Rating < ActiveRecord::Base
belongs_to :tester
belongs_to :cuisine
testers_controller
class TestersController < ApplicationController
def update ##TO DO: SAVE (tester + cuisine IDS) IN THE ratings/JOIN TABLE
#tester = Tester.find(params[:id])
#tester.ratings.create
render text: 'Success'
end
This is form in the view. I am not using / rendering any partials for this exercise.
<%= form_for :rating, url: ratings_path do |f| %>
<h3>Review:</h3>
<% for cuisine in Cuisine.find(:all) %>
<div>
<%= check_box_tag("tester[cuisine_ids][]", cuisine.id) %>
<%= cuisine.cuisine_name %>
<% end %>
</div>
<p><%= f.submit %></p>
<% end %>
The development log shows as below.
Started PUT "/testers/3" for 127.0.0.1 at 2014-11-27 16:53:31 -0700
Processing by TestersController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"5awCMjqwUSHaByj1XFDs5UKZUjyvMoigB88NZCFWgSE=", "tester"=> {"cuisine_ids"=>["3", "6"]}, "commit"=>"Update Tester", "id"=>"3"}
User Load (0.3ms) SELECT `testers`.* FROM `testers` WHERE `testers`.`id` = 3 LIMIT 1
Cuisine Load (0.4ms) SELECT `cuisines`.* FROM `cuisines` WHERE `cuisines`.`id` = 3 LIMIT 1
(0.1ms) BEGIN
SQL (0.2ms) INSERT INTO `ratings` (`created_at`, `created_by`, `cuisine_id`, `updated_at`, `tester`, `tester_id`) VALUES ('2014-11-27 23:53:31', NULL, NULL, '2014-11-27 23:53:31', NULL, 3)
(0.4ms) COMMIT
Rendered text template (0.0ms)
Completed 200 OK in 15ms (Views: 0.3ms | ActiveRecord: 3.8ms)
Couple of issues here.
1. Cuisine_ids are not getting inserted in the ratings table
2. If I have combination of tester_id =1 and cuisine_ids = [2,3] already in the join table then it does nothing. I would like to insert again the same values. that is I would like to allow insert statement to work for inserting duplicate entries. That is how my ratings work.
3. If I have combination of tester_id= and cuisine_ids= [1,2,3] and if I select cuisine_ids=[2,3], then it somehow, rails deletes the cuisine_ids[1,2,3] and again inserts [2,3]. so firstly it executes
1.DELETE from ratings where tester_id=1 (and then runs the insert again)
All I want to do is to save the records which the users select using check boxes in the join table. I want to allow duplicates (for tester_id and cuisine_ids combination) in that join table. This join table might resemble to transaction tables i.e. like one product might have repeated/recurring orders by same customer. However, the entire row will still remain unique because rails has its own primary key on each table.
Let me know if you need more information. Someone please help!!!!
Thanks
I need to know more about your data model but I can provide some general guidance here.
class Cuisine < ActiveRecord::Base
attr_accessible :cuisine_name, :id, :cuisine_id
attr_accessor :tester, :cuisine_ids
has_many :testers, :through => :ratings
has_many :ratings
First, adding ":id" to attr_accessible doesn't make sense. You don't need it as it would give people the ability to update a record's id. I'm not sure what "cuisine_id" is, it doesn't make sense that it would be in the cuisines table.
You definitely don't need attr_accessor. And you should add inverse relationships.
So here's where we're left:
class Cuisine < ActiveRecord::Base
attr_accessible :cuisine_name
has_many :ratings, :inverse_of => :cuisine
has_many :testers, :through => :ratings
Next up is Tester:
class Tester < ActiveRecord::Base
attr_accessible :tester_name, :id, :tester_id
attr_accessor :cusines
has_many :cusines, :through => :ratings
has_many :ratings
Again, your attr_accessible is confusing. You also have misspelled "cuisine". Finally, you don't need the attr_accessor and, again, it will actually "hide" the real "cuisines" relationship. Here's what we end up with:
class Tester < ActiveRecord::Base
attr_accessible :tester_name
has_many :ratings, :inverse_of => :tester
has_many :cuisines, :through => :ratings
Finally, ratings. This is simple and actually looks good, but it strikes me that there's probably at least some sort of "rating" attribute that should be in attr_accessors. Since I don't know what's there, let's just add inverse relationships:
class Rating < ActiveRecord::Base
belongs_to :tester, :inverse_of => :ratings
belongs_to :cuisine, :inverse_of => :ratings
Having those correct will make fixing the rest of it possible.
Looking at your ratings controller, it's kind of confusing. I have no idea what the "ratings" action should do. You're mixing plurals and singulars and all that. You have "cuisine" misspelled in another way there. Let's start with the view:
<h3>Review:</h3>
<% for cuisine in Cuisine.find(:all) %>
<div>
<%= check_box_tag("tester[cuisine_ids][]", cuisine.id) %>
<%= cuisine.cuisine_name %>
</div>
<% end %>
<p><%= f.submit %></p>
<% end %>
Presumably there's a "form_for" that you didn't include. I have done the minimal needed to get this to at least render properly. It's still not exactly what you want as it won't allow for updates.
Back to the controller, what is "ratings" supposed to do? Is that your "new" action?
class RatingsController < ApplicationController
def ratings
#ratings=Rating.new
end
def create
#tester= Tester.new(params[:tester])
params[:tester][:cuisine_ids].each do |tester|
#cus=#tester.ratings.build(:tester_id => tester)
#cus.save
end
end
That's a general cleanup. You still aren't saving "#tester" or doing anything else after the create. If you start with a general resource scaffold for testers you can do this pretty easily. But with these issues fixed you can at least start making some headway.
As I said in the comment above, though, you would be wise to put this aside, go through a tutorial, and then revisit this.

Rails cache not updating properly

I am using the cache-digests gem and following the instructions as per the Railscast, it creates and reads from a cache as you would expect, but the cache does not seem to be updating properly in relation to an associated record.
When moving a listing from one category to another, the category.live_entries count stays the same for the category I move it from, but goes up for the one I move it to.
So it sounds like I need a touch: all type method so it touches the one I am moving it from as well as the one it is moving to?
_category.html.erb
<% cache category do %>
<li>
<%= link_to category.name, category %>
<% if category.live_entries > 0 %>
(<%= category.live_entries %>)
<% end %>
- <%= category.desc %>
</li>
<% end %>
category.rb
class Category < ActiveRecord::Base
has_many :listings
def live_entries
listings.where(verified: true).count
end
end
listing.rb
class Listing < ActiveRecord::Base
belongs_to :category, touch: true
Any ideas on how to tackle this?
Guess I could create a before_update callback to touch the old category - but is there a better way?
Ok just adding this as an answer - but if anyone has a better solution please feel free to share.
I just added an after_update to touch the old category:
def touch_old_category(listing)
cat = listing.category_id_was
Category.find(cat).touch if cat
end

Accessing attributes from different associated models rails 3

I am looking to get a better understanding of Active Model/Record relationships and how to call attributes dependent upon where the attributes are located (models) and where I am calling them. So for example I can access the attribute dish_name from within the recipe controller like so
def all_recipes
#recipes = Recipe.all
end
In the view
<% #recipes.each do |r| %>
<%= r.dish_name %>
<% end %>
Now say i want to access a recipe attribute from within my controller called worldrecipes and i have just written a method that returns all recipes with the same country. a country has many recipes as a relation
So my method is
def self.top_countries
joins(:recipes).
select('countries.*, count(*) AS recipes_count').
group('countries.id').
order('recipes_count DESC')
end
My controller
#worldrecipes = Country.where(:name => params[:name])
and view
<% #worldrecipes.each do |r| %>
<%= r.name %>
<% end %>
so accessing the country name attribute is easy as its in the country model and thats where my query results are being returned from (I think)...My question is how do i access the dish_name attribute from my recipe model to that links to the country name
Hope that makes sense, does anyone have a guide on how to work this out or some golden rules for this
Thank you
I think what you need is:
#country=Country.where(:name=>params[:name]).first
#worldrecipes=#country.recipes
And in the view:
<% #worldrecipes.each do |r| %>
<%= r.dish_name %>
<% end %>
This would print the dish names of the recipes of the country with name provided by params[:name]
EDIT:
Ok Let me clear this up for you :)
Your model relationship is setup such that each country has many recipes. i.e a country has many recipes.
So you have,
has_many :recipes
in country.rb and
belongs_to :country
in recipe.rb
Now when you want to access all the recipes belonging to a country, what you do is, you call country_record.recipes (country_record being the object of the country record you need).
And when you call,
Country.where(:name=>params[:name])
What you actually get is the active record object representing the COUNTRY itself and not the recipes of the country and that is why Italy was printed.
Hope this helped you.
For starters you want to make sure you have the association setup in your models:
country.rb
class Country < ActiveRecord::Base
has_many :recipes
end
recipe.rb
class Recipe < ActiveRecord::Base
belongs_to :country
end
If you haven't already done so, add a foreign_key attribute to your recipe model by running the following migration:
rails g migration add_country_id_to_recipe country_id:integer
Now that your associations are in place you can easily query for a countries respective recipes. In your controller:
#worldrecipes = Country.where(:name => params[:name])
Then in your view:
<% #worldrecipes.each do |c| %>
<% c.recipes.each do |r| %>
<%= r.dish_name %>
<% end %>
<% end %>
In regards to 'golden rules' I highly recommend you check out Association Basics. This is the go-to place for an overview of what you can do with associations.

Preloading count of multiple named_scope in Rails

I have a the following classes
class Service < ActiveRecord::Base
has_many :incidents
end
class Incident < ActiveRecord::Base
belongs_to :service
named_scope :active, lambda ...
named_scope :inactive, lambda ...
end
I'm trying to preload the counts of each incidents class so that my view can do
something like:
<% Service.all.each do |s| %>
<%= s.active.count =>
<%= s.inactive.count =>
<% end %>
Without making a SQL query for each count. I'm trying an approach with select
but I'm not having much luck. Here's what I have so far:
# In Service class
def self.fetch_incident_counts
select('services.*, count(incidents.id) as acknowledged_incident_count').
joins('left outer join incidents on incidents.service_id = services.id').
where('incidents.service_id = services.id AND ... same conditions as active scope').
group('services.id')
end
That will prefetch one count, but I'm not sure how to do it for two different
named_scope.
Using counter_cache is not an option because the database is being written to by non-rails code.
Any help welcome.
I ended up writing a gem for this. https://github.com/smathieu/preload_counts