I have the following:
class Car < ActiveRecord::Base
has_one :driver
end
class Driver < ActiveRecord::Base
belongs_to :car
has_one :license, :as => :licensable
end
class License < ActiveRecord::Base
belongs_to :licensable, :polymorphic => true
end
i.e., Car has one driver who has one license (license is polymorphic - let's just say in this case since it can be associated with other objects).
In routes.rb I have:
resources :cars do
resource :driver do
resource :license
end
end
I would like to show my license. The "show" in the routes file is:
GET /cars/:car_id/driver/license(.:format) {:action=>"show", :controller=>"licenses"}
In my licenses controller I have:
def show
#license = #licensable.licenses.find(params[:id])
# continues.......
The problem is that even though driver has the relation to license, the #licensable is coming across as Car because of the routes. Car has no relation to license so the code doesn't work. I assume I either have to change my controller or more likely my routes.
Because from the URL you only get the car_id this could work:
#car = Car.find(params[:car_id])
#license = #car.driver.licensable
An alternative is to use a less nested REST interface. Nesting is not really necessary if licenses and cars both have unique ID's.
Related
I'm using:
Ruby 2.6.5
Rails 6.0.2.1
PostgreSQL 11
The problem:
I'm trying to create a Scope query for Pundit to check if user has an access to the secret (password) that it wants to visit. I tried to join/include nested users within teams/projects and users which have an access to the password with in such way (writing from what I remember):
What I tried
# secret_policy.rb
class SecretPolicy < ApplicationPolicy
class Scope < Scope
def resolve
test = scope.includes(:projects => {:teams => :users}).includes(:teams =>
{:users}).includes(:users)
test.where(projects: {teams: {users: {id: user}}})
.or(test.where(teams: {users: {id: user}}).or(test.where(users: {id: user})))
end
end
# [...]
end
and then in controller:
# secret_controller.rb
class SecretController < ApplicationController
[...]
def show
#secret = policy_scope(Secret).find(params[:id])
end
[...]
end
which was supposed to join all related entities to Secret model and do nested queries but what was happening at the end was that even tho includes worked properly including all needed data, at the end where clause was using the same join name for running a query: users which is wrong because different join names containing needed data should be called.
More details:
I have a structure like so:
# user.rb
class User < ApplicationRecord
has_and_belongs_to_many :teams
has_and_belongs_to_many :secrets
end
# secret.rb
class Secret < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :teams
has_and_belongs_to_many :users
has_and_belongs_to_many :projects
end
# team.rb
class Team < ApplicationRecord
belongs_to :project
belongs_to :user
has_and_belongs_to_many :users
has_and_belongs_to_many :secrets
end
# project.rb
class Project < ApplicationRecord
belongs_to :user
has_many :teams
has_and_belongs_to_many :secrets
end
I'm kinda new to Rails (ActiveRecords) so my mapping might be a bit wrong. Here is what it supposed to be just to make it clear:
User:
can be owner of passwords
can have access to passwords given by other users
can belong to teams
can belong to projects
Secret:
can belong to 1 user
can be shared with many teams
can be shared with many users (not including owner. Owner has an access already)
can be shared with many projects
Team:
can belong to 1 project
belongs to only 1 user
many users can join the team
many secrets can be shared within the team
Projects:
belongs to only 1 user
can have many teams (uniquely)
many secrets can be shared within the projects
I found many resources talking about the case when you can do it with nested relations but usually it was done with only 1 resource being in where clause, so there was no issue with incorrectly used joins. I was about to give up and try to rewrite is in native SQL, but I believe there must be something.
So let's say I have the following models:
class Building < ActiveRecord::Base
has_many :rooms
has_many :training_rooms, class_name: 'TrainginRoom', source: rooms
has_many :computers, through: :training_rooms
end
class Computer < ActiveRecord::Base
belongs_to :room
end
class Room < ActiveRecord::Base
belongs_to :building
end
class Office < Room
end
class TrainingRoom < Room
has_many :computers
end
And let's also say I am following the jsonapi spec and using the included top-level member to render each related object in a single http call.
So buildings/show looks sort of like this:
json.data do
json.id building.id
json.type building.type
json.attributes do
# render the attributes
end
json.relationships do
# render the relationships
end
end
json.included.do
json.array! building.rooms.each do |room|
json.type room.type
json.id room.id
json.attributes do
# render the attribtues
end
json.relationships do |room|
# render included member's relationships
# N+1 Be here
end
end
end
I have not been able to eagerly load the relationships from the included member, since it is not present on all members of the array.
For instance, in the controller:
#building = Building.eager_load(
{ rooms: :computers }
).find(params[:id])
Will not work if there is an office in the rooms relationship, as it does not have a computers relationship.
#building = Building.eager_load(
:rooms,
traning_rooms: :computers
).find(params[:id])
Also does not work, as the training rooms relationship provides access to the computers to be sideloaded, but is not accessed directly in the rendering code and thus is a useless cache.
Additionally I tried applying a default scope to training room to eager load the computers association, but that also didn't have the desired affect.
The only thing I can think of at this point is to apply the computers relationship to the Room class, but I don't really want to do it, because only training rooms should have computers.
I'd love to hear any thoughts.
Since there is no training_room_id in the Computer model, you will have to explicitly mention the foreign_key while defining the relationship.
class TrainingRoom < Room
has_many :computers, foreign_key: :room_id
end
Now you will be able to eager load the records:
#building = Building.eager_load({ training_rooms: :computers }).where(id: params[:id])
Hope it helps !
Given the following model structures;
class Project < ApplicationRecord
has_many :leads
has_and_belonds_to_many :clients
end
class Lead < ApplicationRecord
belongs_to :project
end
class Client < ApplicationRecord
has_and_belongs_to_many :projects
end
How you would suggest reporting on duplicate leads across a Client?
Right now I am doing something very gnarly with flattens and counts, it feels like there should be a 'Ruby way'.
Ideally I would like the interface to be able to say either Client.first.duplicate_leads or Lead.first.duplicate?.
Current (terrible) solution
#duplicate_leads = Client.all.map(&:duplicate_leads).flatten
Class Client
def duplicate_leads
leads = projects.includes(:leads).map(&:leads).flatten
grouped_leads = leads.group_by(&:email)
grouped_leads.select { |_, v| v.size > 1 }.map { |a| a[1][0] }.flatten
end
end
Environment
Rails 5
Ruby 2.3.1
You could try this.
class Client < ApplicationRecord
has_and_belongs_to_many :projects
has_many :leads, through: :projects
def duplicate_leads
duplicate_ids = leads.group(:email).having("count(email) > 1").count.keys
Lead.where(id: duplicate_ids)
end
end
You could try creating a has_many association from Lead through Project back to Lead, in which you use a lambda to dynamically join based on a match on email between the two records, and on the id not matching. This would mark both records as a duplicate -- if you wanted to mark only one then you can require that the id of one is less than the id of the other.
Some hints: Rails has_many with dynamic conditions
I am totally confused about how I should go about "the rails way" of effectively using my associations.
Here is an example model configuration from a Rails 4 app:
class Film < ActiveRecord::Base
# A movie, documentary, animated short, etc
has_many :roleships
has_many :participants, :through => :roleships
has_many :roles, :through => :roleships
# has_many :writers........ ?
end
class Participant < ActiveRecord::Base
# A human involved in making a movie
has_many :roleships
end
class Role < ActiveRecord::Base
# A person's role in a film. i.e. "Writer", "Actor", "Extra" etc
has_many :roleships
end
class Roleship < ActiveRecord::Base
# The join for connecting different people
# to the different roles they have had in
# different films
belongs_to :participant
belongs_to :film
belongs_to :role
end
Given the above model configuration, the code I wish I had would allow me to add writers directly to a film and in the end have the join setup correctly.
So for example, I'd love to be able to do something like this:
## The Code I WISH I Had
Film.create!(name: "Some film", writers: [Participant.first])
I'm not sure if I'm going about thinking about this totally wrong but it seems impossible. What is the right way to accomplish this? Nested resources? A custom setter + scope? Something else? Virtual attributes? thank you!
I created a sample app based on your question.
https://github.com/szines/hodor_filmdb
I think useful to setup in Participant and in Role model a through association as well, but without this will work. It depends how would you like to use later this database. Without through this query wouldn't work: Participant.find(1).films
class Participant < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
class Role < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
Don't forget to give permit for extra fields (strong_parameters) in your films_controller.rb
def film_params
params.require(:film).permit(:title, :participant_ids, :role_ids)
end
What is strange, that if you create a new film with a participant and a role, two records will be created in the join table.
Update:
You can create a kind of virtual attribute in your model. For example:
def writers=(participant)
#writer_role = Role.find(1)
self.roles << #writer_role
self.participants << participant
end
and you can use: Film.create(title: 'The Movie', writers: [Participant.first])
If you had a normal has_and_belongs_to_many relationship i.e. beween a film and a participant, then you can create a film together with your examples.
As your joining model is more complex, you have to build the roleships separately:
writer= Roleship.create(
participant: Participant.find_by_name('Spielberg'),
role: Role.find_by_name('Director')
)
main_actor= Roleship.create(
participant: Participant.find_by_name('Willis'),
role: Role.find_by_name('Actor')
)
Film.create!(name: "Some film", roleships: [writer, main_actor])
for that, all attributes you use to build roleships and films must be mass assignable, so in a Rails 3.2 you would have to write:
class Roleship < ActiveRecord::Base
attr_accessible :participant, :role
...
end
class Film < ActiveRecord::Base
attr_accessible :name, :roleships
...
end
If you want to user roleship_ids, you have to write
class Film < ActiveRecord::Base
attr_accessible :name, :roleship_ids
...
end
Addendum:
Of cause you could write a setter method
class Film ...
def writers=(part_ids)
writer_role=Role.find_by_name('Writer')
# skiped code to delete existing writers
part_ids.each do |part_id|
self.roleships << Roleship.new(role: writer_role, participant_id: part_id)
end
end
end
but that makes your code depending on the data in your DB (contents of table roles) which is a bad idea.
I have two models with a many to many relationship using has_and_belongs_to_many. Like so:
class Competition < ActiveRecord::Base
has_and_belongs_to_many :teams
accepts_nested_attributes_for :teams
end
class Team < ActiveRecord::Base
has_and_belongs_to_many :competitions
accepts_nested_attributes_for :competitions
end
If we assume that I have already created several Competitions in the database, when I create a new Team, I would like to use a nested form to associate the new Team with any relevant Competitions.
It's at this point onwards that I really do need help (have been stuck on this for hours!) and I think my existing code has already gone about this the wrong way, but I'll show it just in case:
class TeamsController < ApplicationController
def new
#team = Team.new
#competitions.all
#competitions.size.times {#team.competitions.build}
end
def create
#team = Team.new params[:team]
if #team.save
# .. usual if logic on save
end
end
end
And the view... this is where I'm really stuck so I won't both posting my efforts so far. What I'd like it a list of checkboxes for each competition so that the user can just select which Competitions are appropriate, and leave unchecked those that aren't.
I'm really stuck with this one so appreciate any pointing in the right direction you can provide :)
The has_and_belongs_to_many method of joining models together is deprecated in favor of the new has_many ... :through approach. It is very difficult to manage the data stored in a has_and_belongs_to_many relationship, as there are no default methods provided by Rails, but the :through method is a first-class model and can be manipulated as such.
As it relates to your problem, you may want to solve it like this:
class Competition < ActiveRecord::Base
has_many :participating_teams
has_many :teams,
:through => :participating_teams,
:source => :team
end
class Team < ActiveRecord::Base
has_many :participating_teams
has_many :competitions,
:through => :participating_teams,
:source => :competition
end
class ParticipatingTeam < ActiveRecord::Base
belongs_to :competition
belongs_to :team
end
When it comes to creating the teams themselves, you should structure your form so that one of the parameters you receive is sent as an array. Typically this is done by specifying all the check-box fields to be the same name, such as 'competitions[]' and then set the value for each check-box to be the ID of the competition. Then the controller would look something like this:
class TeamsController < ApplicationController
before_filter :build_team, :only => [ :new, :create ]
def new
#competitions = Competitions.all
end
def create
#team.save!
# .. usual if logic on save
rescue ActiveRecord::RecordInvalid
new
render(:action => 'new')
end
protected
def build_team
# Set default empty hash if this is a new call, or a create call
# with missing params.
params[:team] ||= { }
# NOTE: HashWithIndifferentAccess requires keys to be deleted by String
# name not Symbol.
competition_ids = params[:team].delete('competitions')
#team = Team.new(params[:team])
#team.competitions = Competition.find_all_by_id(competition_ids)
end
end
Setting the status of checked or unchecked for each element in your check-box listing is done by something like:
checked = #team.competitions.include?(competition)
Where 'competition' is the one being iterated over.
You can easily add and remove items from your competitions listing, or simply re-assign the whole list and Rails will figure out the new relationships based on it. Your update method would not look that different from the new method, except that you'd be using update_attributes instead of new.