1

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.

T. Cole
  • 191
  • 13
  • So there's a bridge table between Users and Cliqs. For projects I've worked on in the past there was a `role` column in the bridge table that indicated whether a user was an admin, user, or owner of a group. Would such an approach work here? – erapert Mar 11 '16 at 00:25
  • How would I go about implementing that? I'm still somewhat baffled by the HMT association. – T. Cole Mar 11 '16 at 00:27
  • HMT? You mean many-to-many right? Many users can belong to many groups, right? The only restriction is that users may only _own_ one group, right? – erapert Mar 11 '16 at 00:28
  • That's exactly it. – T. Cole Mar 11 '16 at 00:29

3 Answers3

0

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.

Community
  • 1
  • 1
erapert
  • 1,383
  • 11
  • 15
  • I like the way this looks because it separates things well. How would I set it that the creating user is the owner? How would I go about making other users request to join and show the "group belonging" relationship on their own pages? – T. Cole Mar 11 '16 at 00:54
  • I understand the way that you are approaching it. Currently, after testing things a little, I've learned that, with my code, the creating user is only able to create one group (which is good). And when I put "<%= @user.cliq_memberships.name %>" in my "user/show" page all that shows is "CliqMemberships". It would appear that with my code, the correct associations are being made, but there are some problems rendering what I'm wanting. It seems as though the "creating user" isn't being persisted as a member. How do I accomplish that? – T. Cole Mar 11 '16 at 01:18
  • Actually the User IS able to create more than one group. Would a validation fix this problem? – T. Cole Mar 11 '16 at 01:19
0

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 
Dharam Gollapudi
  • 6,328
  • 2
  • 34
  • 42
  • I like this a lot! But I'm getting an "Unknown attribute: 'Owner_ID' for Cliq". – T. Cole Mar 11 '16 at 01:58
  • Here are the migrations: For Cliq: def change create_table :cliqs do |t| t.string :name t.references :owner t.integer :cliq_id t.timestamps null: false – T. Cole Mar 11 '16 at 01:58
  • For Cliq_Membership: def change create_table :cliq_memberships do |t| t.references :user t.references :cliq t.timestamps null: false – T. Cole Mar 11 '16 at 02:00
  • What is happening here? – T. Cole Mar 11 '16 at 02:00
  • Please add all the migrations (formatted) to your question. Also provide the error with the stacktrace to see where the issue is happening. – Dharam Gollapudi Mar 11 '16 at 02:03
  • Added in question. – T. Cole Mar 11 '16 at 02:07
  • The error is happening here: CliqsController#create. – T. Cole Mar 11 '16 at 02:14
  • I don't see issue with migration. Just check in the database and see if cliqs table has owner_id column. If the column exists then provide stacktrace for further review. – Dharam Gollapudi Mar 11 '16 at 02:55
  • For some reason the new records were not persisting to the database. I rolled my database all the way back and re-migrated it. It now will not show routes at all. Trace is posted in question. – T. Cole Mar 11 '16 at 03:36
  • Database issue is fixed (it was a bad MySQL version). I'm still not able to see records of ownership or membership however. – T. Cole Mar 11 '16 at 04:03
  • That stacktrace you provided seems to be not complete. It doesn't list the error in `cliq_controller`. – Dharam Gollapudi Mar 11 '16 at 06:11
  • I discovered my problem. Sorry for the incomplete trace. I'll mark your answer as the right one because of how complete it is. I'll post my answer to what helped me. – T. Cole Mar 11 '16 at 06:38
0

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!

T. Cole
  • 191
  • 13