Many to many join table not updated in Active admin - mysql

I have two models Destination and Package. A package may have multiple Destinations and Destination may contain multiple Packages. In order to maintain the relationship, I have a table called packages_in_destinations. While I tried to insert destination from package form in active admin, it doesn't show up any errors but the packages_in_destinations table is not updated. I am using MySQL for the database.
# app/admin/package.rb
ActiveAdmin.register Package do
permit_params :package_id, :package_title, :description, :featured_image, :duration, :meta_description, :meta_keywords, :published, :package_categories_id, :destinations_id
form do |f|
f.semantic_errors *f.object.errors.keys
f.inputs "Package Details" do
f.input :destination_id, :as => :check_boxes, :collection => Destination.all.collect {|destination| [destination.destination_title, destination.id]}
end
f.actions
end
end
# app/models/package.rb file
class Package < ApplicationRecord
validates_uniqueness_of :package_title, scope: :package_title
validates :package_title, :description, :package_categories_id, :presence => true
belongs_to :package_category
has_and_belongs_to_many :packages_in_destinations
has_many :destinations, through: :packages_in_destinations
end
# app/models/destination.rb file
class Destination < ApplicationRecord
validates_uniqueness_of :destination_title, scope: :destination_title
validates :destination_title, :description, :altitude, :geolocation, :presence=>true
has_and_belongs_to_many :packages_in_destinations
has_many :packages, through: :packages_in_destinations
end
# app/models/packages_in_destination.rb
class PackagesInDestination < ApplicationRecord
has_and_belongs_to_many :destinations, foreign_key: :destination_id, class_name: 'Destination'
has_and_belongs_to_many :packages, foreign_key: :package_id, class_name: 'Package'
end
the relationship between two table has been created from migration file
class CreatePackagesInDestinations < ActiveRecord::Migration[5.0]
def up
create_join_table :packages, :destinations, table_name: :packages_in_destinations do |t|
t.index :package_id
t.index :destination_id
t.timestamps
end
def down
drop_table :packages_in_destinations
end
end
another migration file is created to add a primary key to the table
class AddingPrimaryInPackaesInDestination < ActiveRecord::Migration[5.0]
def change
add_column :packages_in_destinations, :id, :primary_key
end
end
so from all these things no error has been shown but packages saves in packages table but not inserted relationship in the packages_in_destinations table. Please, somebody, suggest what's missing and what's wrong with these?

You need to either use has_and_belongs_to_many or has_many :through. Don't use both at the same time.
1) Example with has_and_belongs_to_many. In this case, you can remove PackagesInDestination model:
# app/models/package.rb file
class Package < ApplicationRecord
# ...
has_and_belongs_to_many :destinations, join_table: :packages_in_destinations
end
# app/models/destination.rb file
class Destination < ApplicationRecord
# ...
has_and_belongs_to_many :packages, join_table: :packages_in_destinations
end
# app/models/packages_in_destination.rb
# removed
2) Example with has_many :through:
# app/models/package.rb file
class Package < ApplicationRecord
# ...
has_many :packages_in_destinations
has_many :destinations, through: :packages_in_destinations
end
# app/models/destination.rb file
class Destination < ApplicationRecord
# ...
has_many :packages_in_destinations
has_many :packages, through: :packages_in_destinations
end
# app/models/packages_in_destination.rb
class PackagesInDestination < ApplicationRecord
belongs_to :destination
belongs_to :package
end

Related

Struggling to add has_many through relationship

I am trying to make a has_many through relationship like this:
#user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :availabilities
has_many :timeslots, :through => availabilities
end
#availability.rb
class Availability < ApplicationRecord
belongs_to :timeslot
belongs_to :user
end
#timeslot.rb
class Timeslot < ApplicationRecord
has_many :availabilities
has_many :timeslots, :through => availabilities
end
I created the two models and than ran rake db:migrate without adding the code in the models (to create the tables).
I made a migration file:
class AddFieldsToTables < ActiveRecord::Migration[5.0]
def change
add_column :users, :availability_id, :integer
add_column :timeslots, :availability_id, :integer
add_column :availabilities, :user_id, :integer
add_column :availabilities, :timeslot_id, :integer
end
end
and ran rake db:migrate
Than I added the code above to all the files.
And then if I try to generate anything it gives me NameError: undefined local variable or method availabilities for User (call 'User.connection' to establish a connection):Class
I am new to Ruby on Rails.
One issue I see is that in your timeslot.rb you have has_many :timeslots, :through => availabilities. I'm guessing you want has_many :users, :through => :availabilites.
Another is in user.rb, you have has_many :timeslots, :through => availabilities but you need the symbol :availabilites. This is what is causing the error you posted, I believe. It should look like this (all I've changed is the second-to-last line):
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :availabilities
has_many :timeslots, :through => :availabilities
end
I see a tiny problem in your code:
#timeslot.rb
class Timeslot < ApplicationRecord
has_many :availabilities
has_many :timeslots, :through => availabilities
end
it should be:
#timeslot.rb
class Timeslot < ApplicationRecord
has_many :availabilities
has_many :users, :through => availabilities
end
I'm not sure if it can solve your problem but your code (exclude the above mistake) sounds fine for me.
In order to setup has_many through relationship between two tables users and timeslots, you need to setup join table availabilities with columns user_id and timeslot_id.
Setup your rails models like below:
# models/user.rb
class User < ApplicationRecord
has_many :availabilities
has_many :timeslots, :through => :availabilities
end
# models/availability.rb
# projects table should have these columns - user_id:integer, timeslot_id:integer
class Availability < ApplicationRecord
belongs_to :timeslot
belongs_to :user
end
# models/timeslot.rb
class Timeslot < ApplicationRecord
has_many :availabilities
has_many :users, :through => :availabilities
end
You need a migration to create availabilities table which acts as a join table for your has_many through relationship between Timeslot Object and User Object. Migration file looks something like this:
class CreateAvailabilities < ActiveRecord::Migration[5.0]
def change
create_table :availabilities do |t|
t.integer :user_id
t.integer :timeslot_id
end
end
end
Access
User.last.timeslots gives 0, 1 or many timeslots associated with User.last
Timeslot.last.users gives 0, 1 or many users associated with Timeslot.last

has_many :through multiple joins table

I'm trying to create a 4 way joins table.
class User < ApplicationRecord
has_many :user_configurations
has_many :teams, through: :user_configurations
has_many :companies, through: :user_configurations
scope :supervisors, -> { joins(:user_configurations).where(user_configurations: { supervisor: true }) }
scope :agents, -> { joins(:user_configurations).where(user_configurations: { supervisor: false }) }
end
class Team < ApplicationRecord
has_many :user_configurations
has_many :users, through: :user_configurations
belongs_to :company_unit
end
class CompanyUnit < ApplicationRecord
has_many :user_configurations
has_many :users, through: :user_configurations
has_many :teams
belongs_to :company
end
class Company < ApplicationRecord
has_many :user_configurations
has_many :users, through: :user_configurations
has_many :company_units
has_many :teams, through: :company_units
end
class UserConfiguration < ApplicationRecord
belongs_to :user, optional: true
belongs_to :team, optional: true
belongs_to :company, optional: true
#supervisor:boolean in this table
end
When I create I get 2 separate entries into the UserConfiguration Table.
Company.first.users.create(team_ids: [1])
id: 1, user_id: 1, team_id: nil, company_id: 1
id: 2, user_id: 1, team_id: 1, company_id: nil
I don't know if it's good practice to attempt something like this any suggestions will be really helpful thanks. Every search results in trying to do a sql join to query data.
EDIT: Decided not to do this and will try and figure out a different approach.
I would set it up with indirect relationships instead:
class User
has_many :employments
has_many :companies, through: :employments
has_many :positions
has_many :teams, through: :positions
end
class Company
has_many :employments
has_many :users, through: :employments
has_many :teams, through: :users
end
class Team
has_many :positions
has_many :users, through: :positions
has_many :companies, through: :users
end
# join model between User and Company
class Employment
belongs_to :user
belongs_to :company
end
# join model between User and Team
class Position
belongs_to :user
belongs_to :team
end
While you could potentially use a single 3 way join model this violates the Single Responsibility Principle and does not map the domain very well.
3 way joins introduce quite a bit of complexity as you cannot simply unlink two records by deleting a row in the join table. And ActiveRecord does not automatically keep track of the foreign key columns.
If you want to add roles to this data model there are a few ways to do it:
1 add an enum to the join table:
# join model between User and Team
class Position
enum role: [:grunt, :supervisor]
belongs_to :user
belongs_to :team
end
2 create a reusable role system.
class User
has_many :user_roles
has_many :roles, through: :user_roles
def has_role?(name, resource = nil)
roles.exists?(name: name, resource: resource)
end
end
class Role
belongs_to :resource, polymorpic: true, optional: true
has_many :user_roles
end
class UserRole
belongs_to :user
belongs_to :role
end
This is a really flexible system lets you attach roles to anything - companies, teams etc. And lets you build systems where the roles can even by defined by end users. Check out the rolify gem for a full example.

Rails - Issue with controllers for creating a user group

I currently have a Rails app that allows users to create a group and allows other users to join the group. The group "creator" is the owner of the group and any that join ON REQUEST are the members. I want a user to be able to create only one group, but belong to many (I think that I've captured that relationship, but I'm a little uncertain). I need a little help understanding what I need to do to show the group associations on the User's page. How should I go about creating a group "show" page and how do I show the group memberships on the User "show" page? I got help from SO and followed the Railscast on self-referential association to help guide me through setting up the relationships.
In this example groups are called Cliqs and membership is controlled by a has_many :through. I used Devise for the User model.
To clarify my question: Am I capturing the relationship that I'm trying to set up? How would I go about allowing the user to view groups that they belong to?
As an aside, I'm not sure if the group creator is being associated as a member of the group. How do I represent that in my model/controller?
Here is my code:
Group Model:
class Cliq < ActiveRecord::Base
belongs_to :owner, class_name: 'User'
has_many :members, through: :cliq_memberships, source: :user
has_many :cliq_memberships
end
Membership Model:
class CliqMembership < ActiveRecord::Base
belongs_to :cliq
belongs_to :user
end
User Model:
class User < ActiveRecord::Base
has_one :owned_group, foreign_key: 'owner_id', class_name: 'Group'
has_many :cliqs, through: :cliq_memberships
has_many :cliq_memberships
.
.
.
end
Group Controller:
class CliqsController < ApplicationController
def show
#cliq = Cliq.find(params[:id])
end
def new
#cliq = Cliq.new(params[:id])
end
def create
#cliq = Cliq.create(cliq_params)
if #cliq.save
redirect_to current_user
else
redirect_to new_cliq_path
end
end
def destroy
end
def cliq_params
params.require(:cliq).permit(:name, :cliq_id)
end
end
Group Membership Controller:
class CliqMembershipsController < ApplicationController
def create
#cliq = cliq.find(params[:cliq_id])
if #cliq_membership.save = current_user.cliq_memberships.build(:cliq_id => params[:cliq_id])
flash[:notice] = "Joined #{#cliq.name}"
else
#Set up multiple error message handler for rejections/already a member
flash[:notice] = "Not able to join Cliq."
end
redirect_to cliq_url
end
def destroy
#cliq = Cliq.find(params[:id])
#cliq_memberships = current_user.cliq_memberships.find(params[cliq_memberships: :cliq_id]).destroy
redirect_to user_path(current_user)
end
end
And my User Show Page:
<h1> <%= #user.username %> </h1>
<h2>Cliqs</h2>
<%= link_to "Create Cliq", new_cliq_path %>
<ul>
<% for cliq_membership in #user.cliq_memberships %>
<li>
<%= cliq_membership.cliq.name %>
(<%= link_to "Leave Cliq", cliq_membership, :method => :delete %>)
</li>
<% end %>
</ul>
<h3>Title:</h3>
<% #uploads.each do |upload| %>
<div>
<%= link_to upload.title, upload_url %>
</div>
<% end %>
And my Migrations:
Cliq:
class CreateCliqs < ActiveRecord::Migration
def change
create_table :cliqs do |t|
t.string :name
t.references :owner
t.integer :cliq_id
t.timestamps null: false
end
end
end
CliqMemberships:
class CreateCliqMemberships < ActiveRecord::Migration
def change
create_table :cliq_memberships do |t|
t.references :user
t.references :cliq
t.timestamps null: false
end
end
end
FULL SOLUTION OF WHAT WORKED BELOW.
Try the following:
Your revised models. Fixed the following issues:
In User model, for has_one :owned_group, you set class_name as Group instead of Cliq.
Declare has_many before has_many :through. It may work otherwise, but it is a good practice and easy for readability.
class User < ActiveRecord::Base
has_one :owned_group, foreign_key: 'owner_id', class_name: 'Cliq'
has_many :cliq_memberships
has_many :cliqs, through: :cliq_memberships
end
class CliqMembership < ActiveRecord::Base
belongs_to :cliq
belongs_to :user
end
class Cliq < ActiveRecord::Base
belongs_to :owner, class_name: 'User'
has_many :cliq_memberships
has_many :members, through: :cliq_memberships, source: :user
end
Your revised controllers. Fixed the following issues:
In the CliqsController, as it is relates to Cliq, you won't get cliq_id while creating it. So removed the cliq_id from the cliq_params. You could add other cliq related attributes in there.
In create, you forgot to assign the current_user as the owner of the cliq. This is addressed by the next note.
As the user is the owner of the cliq, built the cliq using build_owned_group which automatically sets the current_user as the owner.
Try not to do multiple things in the same statement. Like assigning it to a variable as well as doing some operation on the newly assigned variable. For example: In create action of CliqMembershipsController, you were assigning the #cliq_membership as well as calling save on it. Separated those two into two steps.
In destroy of CliqMembershipsController, there is no need to load the #cliq and also fixed the way you are finding the #cliq_membership.
class CliqsController < ApplicationController
def show
#cliq = Cliq.find(params[:id])
end
def new
#cliq = Cliq.new(params[:id])
end
def create
#cliq = current_user.build_owned_group(cliq_params)
if #cliq.save
redirect_to current_user
else
redirect_to new_cliq_path
end
end
private
def cliq_params
params.require(:cliq).permit(:name)
end
end
class CliqMembershipsController < ApplicationController
def create
#cliq = Cliq.find(params[:cliq_id])
#cliq_membership = current_user.cliq_memberships.build(cliq: #cliq)
if #cliq_membership.save
flash[:notice] = "Joined #{#cliq.name}"
else
#Set up multiple error message handler for rejections/already a member
flash[:notice] = "Not able to join Cliq."
redirect_to cliq_url
end
def destroy
#cliq_membership = current_user.cliq_memberships.find(params[:id])
if #cliq_membership.destroy
redirect_to user_path(current_user)
end
end
end
And finally your revised view:
Fixed few things.
Try to use each on the collection to iteration through. This is more ruby way, instead of for loop.
Based on your CliqMemberhipsController code, I assumed you are using nested resources as below. So fixed the link_to to use cliq_cliq_memberhip_path instead of cliq_membership_path.
<h1><%= #user.username %></h1>
<h2>Cliqs</h2>
<%= link_to "Create Cliq", new_cliq_path %>
<ul>
<% #user.cliq_memberships.each do |cliq_membership| %>
<li><%= cliq_membership.cliq.name %>(<%= link_to "Leave Cliq", cliq_cliq_membership_path([cliq, cliq_membership]), method: :delete %>)</li>
<% end %>
</ul>
This assumes you have a routes file with the following:
resources :cliqs do
resources :cliq_memberships
end
Going along the lines of my comment above it seems to me the best thing to do is implement some kind of role attribute in the bridge table.
The Rails docs say this:
You should use has_many :through if you need validations, callbacks, or extra attributes on the join model.
So you might try this in your models:
class Cliq < ActiveRecord::Base
has_many :cliq_memberships
has_many :members, through: :cliq_memberships
def owner
cliq_memberships.where(role: 'owner').user
end
end
# this model is used to access attributes on the bridge table
class CliqMembership < ActiveRecord::Base
belongs_to :cliq
belongs_to :user
attr_accessor :role
end
class User < ActiveRecord::Base
has_many :cliq_memberships
has_many :cliqs, through: :cliq_memberships
# something like this would make it easy to grab the owned cliq
def ownedCliq
cliq_memberships.where(role: 'owner').cliq
end
end
so the bridge table stores role which would be an enum or a string representing 'member', 'owner', and maybe 'admin' or something.
Some example usage:
# say I have a user
u = User.find(1)
# and I want the cliq that he/she owns
owned_cliq = u.ownedCliq
# maybe I have a group:
g = Cliq.find(1)
# and I want the user that owns it:
my_owner = g.owner
# now let's get all the members of the cliq (including the 'owner')
my_members = g.members
More example usage:
# inside the controller...
# say I have a user:
u = User.find(1)
# this user is trying to create a cliq
# pretend we fill it in with its data here...
c = Cliq.new
c.save!
# we'll need to hook the two together:
cm = CliqMembership.new(role: 'owner', user_id: u.id, cliq_id: c.id)
cm.save!
# or we might try something like this:
#cm = CliqMembership.find_or_create_by #...
Also, I found this SO answer which does a good job of explaining things.
So I started with the code in my question above and then worked inward to my answer (through many additional trials). This may help someone in the future so here is what worked. (Taking advice from both answers):
class Cliq < ActiveRecord::Base
belongs_to :owner, class_name: 'User'
has_many :cliq_memberships
has_many :members, through: :cliq_memberships, source: :user
end
class CliqMembership < ActiveRecord::Base
belongs_to :cliq
belongs_to :user
end
class User < ActiveRecord::Base
has_one :owned_cliq, foreign_key: 'owner_id', class_name: 'Cliq'
has_many :cliq_memberships
has_many :cliqs, through: :cliq_memberships
.
.
.
end
class CliqsController < ApplicationController
def show
#cliq = Cliq.find(params[:id])
end
def new
#cliq = Cliq.new(params[:id])
end
def create
#cliq = current_user.build_owned_cliq(cliq_params)
#cliq.members << current_user
if #cliq.save
redirect_to current_user
else
redirect_to new_cliq_path
end
end
def destroy
end
def cliq_params
params.require(:cliq).permit(:name, :cliq_id)
end
end
class UsersController < ApplicationController
def show
#find way to use username instead of id (vanity url?)
#user = User.find(params[:id])
#uploads = Upload.all
#cliq_memberships = CliqMembership.all
#cliqs = Cliq.all
end
end
class CliqMembershipsController < ApplicationController
def show
end
def create
#cliq = Cliq.find(params[:cliq_id])
#cliq_membership = current_user.cliq_memberships.build(cliq: #cliq)
if #cliq_membership.save
flash[:notice] = "Joined #{#cliq.name}"
else
#Set up multiple error message handler for rejections/already a member
flash[:notice] = "Not able to join Cliq."
end
redirect_to cliq_url
end
def destroy
#cliq_membership = current_user.cliq_membership.find(params[:id])
if #cliq_membership.destroy
redirect_to user_path(current_user)
end
end
class CreateCliqs < ActiveRecord::Migration
def change
create_table :cliqs do |t|
t.string :name
t.references :owner
t.timestamps null: false
end
end
end
class CreateCliqMemberships < ActiveRecord::Migration
def change
create_table :cliq_memberships do |t|
t.references :user
t.references :cliq
t.timestamps null: false
end
end
end
Thanks so much for all of the incredible help on this thread!

ActiveRecord Multi-association query

So I've got three models:
User
User Interest
Interest Tags
User Model
class User < ActiveRecord::Base
has_many :user_interests
end
InterestTag Model
class InterestTag < ActiveRecord::Base
has_many :user_interests, :dependent => :destroy
validates :name, :uniqueness => true
end
UserInterest Model
class UserInterest < ActiveRecord::Base
belongs_to :interest_tag
belongs_to :user
end
I'd like to use ActiveRecord to include the name of the user's interests when loading their profile using the following query:
#user = User.find(current_user.id, :include => [{:user_interests => :interest_tags}])
Migrations for interest_tags + user_interests
create_table :interest_tags do |t|
t.string :name, :null => false, :size => 30
t.timestamp :created_at
end
create_table :user_interests do |t|
t.integer :user_id
t.integer :interest_tag_id
end
What am I doing wrong?
You have to add an has_many :through association on User model.
class User < ActiveRecord::Base
has_many :user_interests
has_many :interest_tags, :through => :user_interests
end
class UserInterest < ActiveRecord::Base
belongs_to :interest_tag
belongs_to :user
end
class InterestTag < ActiveRecord::Base
has_many :user_interests, :dependent => :destroy
validates :name, :uniqueness => true
end
Now you can eager load the tags as follows:
User.find(current_user.id, :include => :interest_tags)
Note:
You might want to look at the acts_as_taggable_on gem for your requirement.
I assume you are building a tagging system, like in stackoverflow: every user kann have multiple tags that they are interested in. In that case the user_interests table is only a join table and does not need a model. Just use has_and_belong_to_many on the two real models.
See also this article on different ways to implement tagging with more or less normalized relational databases. You could also use a non-relational database like Mongodb, there you would need only one table to do tagging, see the cookbook.

Create a form for accessing relationships in RoR 3

This is kind of evolution of my previous question (although I changed a lot, including model names so figured I should better create another thread).. and the nature of question changed as well..
Currently I am struggling to create a form for the function
#dot2.link!(#dot)
The function works fine in console.
Here is the info from models / controllers (I tried to keep it minimal):
class User < ActiveRecord::Base
has_many :dots, :dependent => :destroy
....
end
Dot
class Dot < ActiveRecord::Base
belongs_to :user
has_many :linkages
...
def link!(new_dot)
linkages.create!(:end_id => new_dot.id)
end
...
end
Linkages:
class LinkagesController < ApplicationController
...
def create
#dot = current_user.dots.find(params[:linkages][:end_id])
#dot2 = Dot.find(params[:id])
#dot2.link!(#dot)
end
Linkages migration:
class CreateLinkages < ActiveRecord::Migration
def self.up
create_table :linkages do |t|
t.integer :start_id
t.integer :end_id
t.timestamps
end
add_index :linkages, :start_id
add_index :linkages, :end_id
add_index :linkages, [:start_id, :end_id], :unique => true
end
...
end
Now in console if I do
User.first.dots.first.link!(User.second.dots.second)
everything works fine.
How would I create a form for it (using just dot_id) as input??
Thanks!
Look up the collection_select form helper. It renders an association however you like, defaults to a Selectbox.