Ruby on Rails 5: double association between two models - ruby-on-rails-5

I've been working on this issue for a few days now, and the other Stack Overflow posts I've looked at are either really old, or not quite what I need.
Problem:
There are three models involved:
Trip - represents a trip and its associated information, NOTE: does not include any information about crew members.
Milestone - represents an action taken on a trip (for the purposes of this problem, when a crew member embarks or disembarks).
CrewMember - represents a single crew member on the trip, each crew member has an embark and disembark Milestone which may be shared with other crew members if they embark or disembark at the same place. CrewMembers are linked to Milestones by both their embark_milestone_id and disembark_milestone_id.
Now I'm working on a form where a user can edit all the information from a trip, this includes which CrewMembers are present on the trip, what their roles are, and where they get embark/disembark. The trip is being edited, so the whole page is wrapped in a form_for builder.
The segment of the page that edits CrewMembers is wrapped in a fields_for builder. The Trips model is configured to accept nested attributes for the CrewMember model. The number of crew members is variable, and each needs to have their information edited separately (so I can't use select_tags as far as I know). This works fine to display the crew members in the view, but does not work when I actually try to update, because CrewMembers are associated with Milestones through both embark_milestone_id and disembark_milestone_id instead of just milestone_id.
Question:
So with all that in mind, is there a way to 'teach' rails to recognize CrewMembers through Milestones from a trip form by either embark_milestone_id, disembark_milestone_id or preferably both?
My impression is that this is not the 'Rails' way of doing something like this, but I wanted to post the idea to the community before moving to a messier work-around.
Any advice is greatly appreciated. Thank you for your time.
Model Code as Requested:
class CrewMember < ApplicationRecord
belongs_to :guide
belongs_to :milestone
end
class Trip < ApplicationRecord
belongs_to :launch_slot, required: true
has_many :milestones
has_many :places, through: :milestones
has_many :crew_members, through: :milestones, foreign_key: embark_milestone_id
accepts_nested_attributes_for :milestones
accepts_nested_attributes_for :crew_members
end
class Milestone < ApplicationRecord
belongs_to :trip
belongs_to :place
has_many :crew_members
end

It sounds like you want two belongs_to relationships from CrewMember to Milestone. You’ll need to give these relationships different names, and then override the default class name using the class_name option (see docs):
class CrewMember < ApplicationRecord
belongs_to :guide
belongs_to :embark_milestone, class_name: :Milestone
belongs_to :disembark_milestone, class_name: :Milestone
end
Then from the Milestone class you’ll need to define two has_many relationships back to the CrewMember class. This time you’ll also need to specify the foreign_key, as that can’t be inferred either:
class Milestone < ApplicationRecord
belongs_to :trip
has_many :embarking_crew_members, class_name: :CrewMember,
foreign_key: :embark_milestone_id
has_many :disembarking_crew_members, class_name: :CrewMember,
foreign_key: :disembark_milestone_id
end
Your next question then is how to get a collection of allCrewMembers from the Trip model. You suggested that you'd want to return either crew members connected via the embark_milestone_id OR the disembark_milestone_id. There’s no straightforward way of doing this – you'd have to write your own method which both the embarking_crew_members and disembarking_crew_members relationships, joins them together, and then removes any duplicates. However, is it really possible to have a CrewMember who disembarks but didn't embark? ;-)

Related

Conditionally Saving has_many_through Relationships

In Rails, how would one conditionally associated records on a has_many_through relationship? Using the following Rails docs example:
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
Suppose I wanted to have an appointment reference exactly two physicians. That is, there will not be any appointment record one there is less than two physicians assigned. However, how could that appointment then reference each physician?
Example
Basically, I want to keep track of users liking other users and mutual likes between them. A connection is established when both users like each other. But I don't want a connection when only one user likes another but it is not reciprocal.
When User A likes User B. A "like" is created.
When User B likes User A. A "like" is created. A "connection" is also created.
The connection should be able to call:
connection.users
The user should be able to call:
user.likes
user.connections
The problem that I'm having is how can that relationship table know when it is mutual?
For the original question, a connection doesnt make a difference between the two users, so i would model it as a one to many relationship and validate it only has two users.
A like has two users, the liker (giver of the like) and the likee (receiver of the like). Every time you create new like, you should check if the likee also likes the liker. If likee.likes.where(likee: liker)? If yes, then create the new connection with both users.
class User < ApplicationRecord
has_many :likes
has_many :connections
end
class Like < ApplicationRecord
belongs_to :user, :foreign_key => 'liker_id'
belongs_to :user, :foreign_key => 'likee_id'
end
class Connection < ApplicationRecord
has_many :likes
has_many :users, through: likes
end
I want to add that i am not 100% sure of this as i am currently learning Rails myself. But this is what I came up with and hopefully its useful (and correct).

How to manually join two different table with different attribute name in Ruby on Rails controller

I am currently making a website that runs on Ruby on Rails. I am facing some issues while I was trying to join two tables, Rates and Locations, that I have with two different attributes name.
Rates: id rater_id rateable_id (and a few more attributes in this table)
Locations: id title body user_id (and a few more attributes in this table)
Here is the query that I am trying to do in SQL.
SELECT *
FROM rates, locations
WHERE rates.rater_id = locations.user_id AND rates.rateable_id = locations.id
I have read the official active record documents that provided by rubyonrails.org. I have tried doing these, but it does not work. Here is the code that I am trying to implant in app\controllers\users_controller.rb
#join_rating = Rate.joins(:locations).where("rates.rateable_id = locations.id AND rates.rater_id = locations.id")
#all_rating = #all_rating.where(rater_id: #user)
#count_all_rating = #all_rating.count
#join_rating, is trying to join the attributes with different names.
#all_rating, is trying to filter which location to show using the user ID
#join_rating, is trying to calculate the total numbers of locations that are rated by the user
Assume that everything is setup correctly and the only error is in the query that I am trying to do, how should I rewrite the statement so that I am able to show the locations that the user has rated using #all_rating.
Thank you!
A few points:
When in ActiveRecord you're starting a statement with the Rate class, it means the result is going to be a collection of Rate objects. So if you're trying to show locations, you should start with a Location class.
#locations_user_rated = Location.joins('INNER JOIN rates ON
rates.rateable_id = locations.id').where('rates.rater_id' => #user)
And if your ActiveRecord associations are well defined, you could simply do:
#locations_user_rated = Location.joins(:rates).where('rates.rater_id' => #user)
"Well defined" simply means you'll need to do something like the following. Note that I am not sure I understand your model relationships correctly. I assume below that every location has multiple rates, and that the reason your Rate model has the field called rateable_id instead of a location_id is because you want :rateable to be polymorphic. This means you probably also have a rateable_type field in rates table.
class Location < ActiveRecord::Base
has_many :rates, as: :rateable
end
class Rate < ActiveRecord::Base
belongs_to :rateable, polymorphic: true
end
If this polymorphism is not the case, things should actually be simpler, and I highly recommend that you follow Rails's conventions and simply name the relationship field location_id on your Rate model instead of rateable_id. Then you can do:
class Location < ActiveRecord::Base
has_many :rates
end
class Rate < ActiveRecord::Base
belongs_to :location
end
If still you are not convinced about the field name, you can customize things and do:
class Location < ActiveRecord::Base
has_many :rates, foreign_key: :rateable_id
end
class Rate < ActiveRecord::Base
belongs_to :location, foreign_key: :rateable_id
end
You can find more about how to customize associations here, and here.
I highly recommend taking advantage of ActiveRecord's has_many, belongs_to, and has_many through: functionality.
If you set up a model for each of these tables, with the correct relationships:
class User < ActiveRecord::Base
has_many :ratings, foreign_key: :rater_id
has_many :rated_locations, through: ratings, class_name: Location.name, source: :rater
end
class Rating < ActiveRecord::Base
belongs_to :rater, class_name: User.name
belongs_to :location
end
class Location < ActiveRecord::Base
has_many :ratings
end
Then to access the locaitons that a user has rated, you just call
user.rated_locations

has_many association ignores restricting condition

I'm working on a Rails 3.0.x application (actually it's Hobo 1.3.x but that's not material to this question). Among the models, there are GraphPanes, GraphLabels, and LabelSets. A GraphPane can have GraphLabels and LabelSets. GraphLabels can belong to GraphPanes or LabelSets, but not both. So if a GraphLabel belongs to a LabelSet, I'd like to keep it from being associated to a GraphPane.
I am trying to enforce that with this code in the GraphPane model:
has_many :graph_labels, :conditions => 'label_set_id = NULL'
However, I'm still able to associate GraphLabels with not-null label_set_id with GraphPanes. Why? How can I stop this?
This question is superficially similar, but my relationship isn't polymorphic, so the nominal solution there doesn't help me.
The functionality of :conditions on has_many is to filter the results that are passed back via the graph_labels, not to protect objects from being added to the association.
If you add a graph_label with no label_set_id, the association will build, but if you then ask for graph_pane.graph_labels, it will not return that non-condition-matching graph_label.
The has_many/belongs_to relationship is saved on the belongs_to model, graph_label, and so the parent/has_many/graph_pane does not stop the graph_label from writing whatever it wants to its graph_pane_id attribute. This delegation of responsibility is correct, although frustrating, I agree.
Now, as for how to stop this, I'm not sure. It sounds like you need some sort of validation on the graph_label object, something along the lines of not allowing a graph_pane_id to be set on a graph_label if that graph_label's label_set_id is nil. Since the has_many/belongs_to relationship is saved on the graph_label, you should write the validation on the graph_label. That way, the graph_label will not be able to be saved with a new graph_panel_id unless it fulfills the condition.
Thoughts? Questions?
Reference:
has_many
Alternate Solution
I've reread your question and I think want you want here is a polymorphic association.
def GraphPane < ActiveRecord::Base
has_many :label_sets
has_many :graph_labels, as: :parent
end
def LabelSet < ActiveRecord::Base
belongs_to :graph_pane
has_many :graph_labels, as: :parent
end
def GraphLabel < ActiveRecord::Base
belongs_to :parent, polymorphic: true
end
That way, a GraphLabel can only have a single parent, which is what your “spec” above requires. Is there any reason not to implement the relations in this way?

Rails 3: Object chaining with has_many :through associations

I'm trying to get a grasp of how to work with associations in Rails, specifically, when and when not to write explicit SQL code.
In my application, I have four models, which are defined as follows:
class User < ActiveRecord::Base
has_many :comments
has_many :geographies
has_many :communities, through: :geographies
class Comment < ActiveRecord::Base
belongs_to :user
class Community < ActiveRecord::Base
has_many :geographies
has_many :users
class Geography < ActiveRecord::Base
belongs_to :user
belongs_to :community
Users can post comments, and are associated to one or more communities through the geography table (the geography table stores user_id and community_id).
I have an index action listing all comments, and I would like to filter by community. Given a comment object, I can get the user object via comment.user, but I can't chain beyond that (i.e., something like comment.user.geography(0).community doesn't work).
It seems this object chaining is a key feature of rails, but does it work with has_many :through associations? Given my example, is it possible to get the community object from the comment object by using object chaining, or would I need to write the SQL to get anything other than the user when given the comment object?
Since User is associated with multiple communities, you will need to tell ActiveRecord (or raw SQL) which community you want:
comment.user.communities #=> should give you all the communities
If you don't particularly care for getting all communities and just want to get any community
comment.user.communities.first #=> should give you the first community
But, generally you will be interested in one particular community, based on a condition.
comment.user.communities.where(name: 'Europe') #=> should give you the European community.
I don't think you need the geographies table.
Try
class Community < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
belongs_to :community
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :user
end
Then you can access a comment's user's community like
#comment.user.community

Building an active record with associations to two belongs_to relationships

Going back to an example I had previously asked about, I'll try and make this question as simple as possible.
Supposed I have User and Document models.
A User has_many Documents and a Document belongs_to a User. This relationship works fine today.
I want to introduce an Edits model that belongs_to a Document and belongs_to a User. The User can be any user, not necessary the one who created the document. With this new model, the Document now has_many :edits and the User has_many :edits.
So far it would look like:
# user.rb
class User < ActiveRecord::Base
has_many :edits
has_many :documents
end
# document.rb
class Document < ActiveRecord::Base
belongs_to :user
has_many :edits
end
# edit.rb
class Edit < ActiveRecord::Base
belongs_to :user
belongs_to :document
end
When I create a Document through a User, the association between the User and Document are fine in both directions (user.documents and document.user)
Now when I want to create an Edit, the edit should be against a Document but should also be associated with the user who generated the edit (edit.user).
When I'm building this in my RSpec tests I'm struggling to get the associations correct using the "standard" association methods. If I do #user.edits.build({...}) it will associate the user in the returned Edit object, but not the Document. Likewise when I do #document.edits.build({...}) it will associate the Document but not the User.
I supposed I could expose the user_id and post_id in the attr_accessible declaration but won't this but I fear this is not a best way of doing this. I have no real reason to fear other than the attributes are now accessible through mass assignment (from what I understand).
Am I going about this the wrong way or is there a better way to create and test all the associations?
class User < ActiveRecord::Base
has_many :edits, :through => :documents
has_many :documents
end