Relationship modeling and routing - ruby-on-rails-3

Consider these two resources: user and group.
Rules:
A group is owned by an user;
A group contains many users;
A user can have many groups;
A user can attend to many groups;
What I have:
class User
has_many :groups, :foreign_key => "owner_id"
has_and_belongs_to_many :attended_groups,
:class_name => "Group",
:join_table => "groups_members",
:foreign_key => "member_id"
end
class Group
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_and_belongs_to_many :members, :class_name => "User",
:join_table => "groups_members",
:association_foreign_key => "member_id"
end
My question is: what is the best (elegant?) solution to add actions in group controller, and also routes to, while owner sees his group (and all members), let it see who is not there and maybe add it. Something like: /groups/1/add_member/2. Same thing for a user to add a group, while he sees its page.
I've managed to make it work, but I would like to see how it should be. The problem is too simple to have a solution that complicated as mine. Maybe the way I modeled the problem is not the best way too.
Just for the record, I'm a completely newbie to Rails.
Please comment! And thanks in advance!

You probably want to do a nested resource pattern. It adheres to REST though, so you're going to have to deal with the naming conventions for urls.
resources :groups do
resources :users
end
This is a good resource: http://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default
In order to add an existing user to an existing group I would probably define a new route.
match '/groups/:group/users/add' => 'groups#add'
And this in the controller
#group = Group.find params[:group]
#user = User.find params[:user]
#group.users << #user if #user

Related

Extra Role column in join table in Rails

I have users and companies in a many to many relationship by a join table which has a column for user Role. I'm not sure if this is the best way to have the model set up.
Now each user can have different roles depending on the company, what is the best way to design and access user's role using ActiveRecord associations?
I would like to return via JSON the user's role based on their current company and default to something if their company is nil OR their role has not been set (nil).
Update:
What I've got now after reading Many-to-many relationship with the same model in rails? which is a bit different (many to many on itself).
CompaniesUser
belongs_to :company
belongs_to :user
Company
has_many(:companies_users, :dependent => :destroy)
has_many :users, :through => :companies_users
User
has_one :company
has_many(:companies_users, :dependent => :destroy)
has_many :companies, :through => :companies_users
Appreciate any advice as I'm just starting to learn this!
What you have above is correct, in terms of the ActiveRecord relationships. If you'd like to read more on the subject I believe this is the best source: http://guides.rubyonrails.org/association_basics.html
One problem I see there is that CompaniesUsers should be in singular form: CompanyUser, and then in all cases where you use :companies_users use: :company_users
I am assuming here that the current company of the User is the last one assigned.
Now in order to serialize in JSON format you should add the following in your User ActiveRecord:
def serializable_hash(options = nil)
options ||= {}
h = super(options)
if(defined?self.company_users.last and defined?(self.company_users.last).role)
h[:role] = (self.company_users.last).role
else
h[:role] = 'default_value'
end
end

how do I structure CanCan abilities to control creation based on another model?

Imagine a has_many relationship for memberships for clubs:
end
class Club < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :users, :through => :memberships
validates :name, :is_enrollable, :presence => true
end
class Membership < ActiveRecord::Base
belongs_to :club
end
Assume also that Club has a boolean is_enrollable field in its table. When true, a user can create a Membership associated with that Club. When false, only an admin may create the Membership record.
My question is: how do you set up CanCan's ability.rb to reflect this?
Comment: It's slightly unusual in that a field in the Club table controls the ability to create a Membership record. This cannot work:
can :create, Membership, :club => {:is_enrollable => true}
... since the Membership doesn't exist before its created. Edit: that's not true -- CanCan will work on an unsaved record before authorizing it. See answer below.
(I was planning on withdrawing this question since it's badly posed, but I figured out the answer and thought it might be useful.)
You CAN create a clause in Ability for :create that reads:
can :create, Membership, :club => {:is_enrollable => true}
This works because, in MembershipsController, CanCan will build a new (unsaved) Membership model and and then authorize it, effectively doing:
#membership = Membership.new(params[:membership])
raise <some error> unless Ability.new(user).can?(:create, #membership)
# if we've gotten this far, we can now save the membership
#membership.save
At least, that's what I think is going on.

Searching a has_many :through association including the middle model

First, the topic.
I have three models, which are linked between each other with a has_many :trough association like this:
#User model
has_many :chars_del, :class_name => CharDelegated, :dependent => :destroy
has_many :chars, :through => :chars_del
#CharDelegated model
#has a field owner:integer
belongs_to :char
belongs_to :user
#Char model
#has fields name:string
has_many :chars_del, :class_name => CharDelegated
has_many :users, :through => :chars_del
What I need to do is I need to search from a User Record to find all the Chars that the particular user ownes (:owner field is true) ordered by name. I have been stuck with this for a couple hours now, so I believe that I could have missed a very simple answer... But nothing that I have tried so far did work even a bit.
UPDATE
found something that works:
user.chars.where(:char_delegateds => {:owner => 1}).order('name')
don't know why the :chars_del gave an error, but the full table name did the job.
Andrew, your answer works well too and is a little faster on the database, thans alot.
Does
user.chars.order('name')
not work? (Given user is a single User instance.)
Edit
Given your new information:
CharDelegated.where(user_id: user.id, owner: true).map(&:char)
should work.
In your specific example you don't need to search through the middle table but if you want to see an example of how to use the joining table and search through it for a more complex scenario you can do it this way.
#char = Char.all(:include => :users, :conditions => ["char_delegated.user_id in (?)", user_id]).order('name')

How to give foreign key a name in RoR 3?

How can I give foreign key a name in RoR?
I use following command to give foreign key:
rails generate scaffold Table2 id:integer Table1:references
This command adds foreign key of Table1 in Table2 but with default name that is Table1_id. So how can I give custom name to it for example my_table_f_key instead of Table1_id.
I'm using Ruby 1.9.2 and Rails 3.0.3.
Edit:-
In my project.rb model:
belongs_to :own, :class_name => User
In my user.rb model:
has_many :owned_projects, :class_name => Project, :foreign_key => :owner
how I created my project model
rails generate scaffold Project name:string owner:integer
Now when I access user_id from Project like
project.owner.userid it throws exception.
Based on your responses in the comments, this is one way of implementing what you want to do:
Assuming two models in your app (Users and Questions), and two different relationships:
User asks many Questions, Question belongs_to Asker
User edits many Questions, Question belongs_to Editor
You could implement this structure in the following way:
rails generate scaffold Question asker_id:integer editor_id:integer
Specifying id:integer in your generate command is redundant, as Rails will generate that column for you automatically. It's also conventional to name your foreign keys in terms of the relationship (ie, asker_id).
Then, inside each of your models:
class Question < ActiveRecord::Base
belongs_to :asker, :class_name => User
belongs_to :editor, :class_name => User
end
class User < ActiveRecord::Base
has_many :asked_questions, :class_name => Question, :foreign_key => :asker_id
has_many :edited_questions, :class_name => Question, :foreign_key => :editor_id
end
That way, you can use them together like this:
#question.asker # => User
#question.editor # => User
#user.asked_questions # => [Question, Question, Question]
#user.edited_questions # => [Question, Question]
Hope this helps.
Adding to #Dan's answer, pass the class name as String.
DEPRECATION WARNING: Passing a class to the class_name is deprecated and will raise an ArgumentError in Rails 5.2. It eagerloads more classes than necessary and potentially creates circular dependencies. Please pass the class name as a string
class Question < ActiveRecord::Base
belongs_to :asker, :class_name => User
belongs_to :editor, :class_name => User
end
class User < ActiveRecord::Base
has_many :asked_questions, :class_name => 'Question', :foreign_key => :asker_id
has_many :edited_questions, :class_name => 'Question', :foreign_key => :editor_id
end

Complex associations in ActiveRecord models

I'm trying to understand how ActiveRecord deals with associations that are more complex than simple has_many, belongs_to, and so on.
As an example, consider an application for recording music gigs. Each Gig has a Band, which has a Genre. Each Gig also has a Venue, which has a Region.
In the rough notation of MS Access (which I'm suddenly beginning to feel quite nostalgic for) these relationships would be presented like this
1 ∞ 1 ∞ ∞ 1 ∞ 1
Genre ---- Band ---- Gig ---- Venue ---- Region
I would like to be able to find out, for example, all the bands who've played in a region, or all the venues that host a certain genre.
Ideally, my models would contain this code
class Genre
has_many :bands
has_many :gigs, :through => bands
has_many :venues, :through => :gigs, :uniq => true
has_many :regions, :through => :venues, :uniq => true
end
class Band
belongs_to :genre
has_many :gigs
has_many :venues, :through => :gigs, :uniq => true
has_many :regions, :through => :venues, :uniq => true
end
class Gig
belongs_to :genre, :through => :band
belongs_to :band
belongs_to :venue
belongs_to :region, :through => :venue
end
and so on for Venue and Region.
However, it seems I have to produce something like this instead
class Genre
has_many :bands
has_many :gigs, :through => bands
has_many :venues, :finder_sql => "SELECT DISTINCT venues.* FROM venues " +
"INNER JOIN gigs ON venue.id = gig.venue_id " +
"INNER JOIN bands ON band.id = gig.band_id " +
"WHERE band.genre_id = #{id}"
# something even yuckier for regions
end
class Band
belongs_to :genre
has_many :gigs
has_many :venues, :through => :gigs, :uniq => true
# some more sql for regions
end
class Gig
delegate :genre, :to => :band
belongs_to :band
belongs_to :venue
delegate :region, :to => :venue
end
I have two questions - one general and one particular.
The general:
I would have thought that what I was trying to do would come up fairly often. Is what I have really the best way to do it, or is there something much simpler that I'm overlooking?
The particular:
What I have above doesn't actually quite work! The #{id} in the second genre model actually to return the id of the class. (I think). However, this seems to work here and here
I realise this is a rather epic question, so thank you if you've got this far. Any help would be greatly appreciated!
Associations are designed to be readable and writable. A large part of their value is that you can do something like this:
#band.gigs << Gig.new(:venue => #venue)
It sounds, though, like you want something that's read-only. In other words, you want to associate Venues and Genres, but you'd never do:
#venue.genres << Genre.new("post-punk")
because it wouldn't make sense. A Venue only has a Genre if a Band with that particular Genre has a Gig there.
Associations don't work for that because they have to be writable. Here's how I'd do readonly associations:
class Genre
has_many :bands
def gigs
Gig.find(:all, :include => 'bands',
:conditions => ["band.genre_id = ?", self.id])
end
def venues
Venue.find(:all, :include => {:gigs => :band},
:conditions => ["band.genre_id = ?", self.id])
end
end
You can add conditions and parameters to your associations.
Recent versions of ActiveRecord give the power of named_scopes, which will work on associated records as well.
From a current project
Folder has_many Pages
Page has_many Comments
# In Page
named_scope :commented,
:include => "comments",
:conditions => ["comments.id IS NULL OR comments.id IS NOT NULL"],
:order => "comments.created_at DESC, pages.created_at DESC"
Using this we can say:
folder.pages.commented
Which will scope on the associated records, doing a conditional with the supplied parameters.
Plus! named_scopes are composable.
And more scopes:
named_scope :published, :conditions => ["forum_topics.status = ?", "published"]
And chain them together:
folder.pages.published.commented
For associations like this, you're going to end up writing custom SQL -- there's no real way that you can handle a chain of associations like this without having to do some fairly massive joins, and there really isn't an efficient way for the built-in query generators to handle it with a one-liner.
You can look into the :joins parameter of ActiveRecord as well -- this may do what you want.
Sounds like a job for nested_has_many_through! Great plugin that allows you to do nested has_many :throughs