multiple habtm relationships on one model - ruby-on-rails

I am trying to figure out the best way to accomplish my problem. I've got a pages table, and a user_types table. I am trying to specify multiple user types on a page. They will act as permission groups. I need to do this twice however. Once for read permissions, and once for edit permissions. Here is an example:
Home page has 3 user types that can read it - admin, super admin, public
It has 2 user types that can edit it - admin, super admin
I have one user_types table:
admin
super admin
public
etc
I have created two mapping tables (one for read, and one for edit):
pages_user_read_types
pages_user_edit_types
they both have page_id, and user_type_id
Is there a better way to accomplish this? If this is the best way, I need help figuring out the relationships for the models. I have this for one relationship
has_and_belongs_to_many :user_types, :join_table => :pages_user_read_types
How do i specify two relationships for seperate fields?
Thanks

The HABTM relationship in Rails has seemed to fall out of favor over the last couple of years with Rails developers to the has_many :through relationship. The only time you should use HABTM is when you have no need for any additional information about the relationship between two models. In your case, you are trying to emulate this by creating two HABTM relationships when you could effectively accomplish by having a join model with a editable attribute.
In code, it would look something like this:
class Page < ActiveRecord::Base
has_many :page_permissions
has_many :user_types, :through => page_permissions
def editable_user_types
page_permissions.includes(:user_types).where(:editable => true).map(&:user_type)
end
def read_only_user_types
page_permissions.includes(:user_types).where(:editable => false).map(&:user_type)
end
end
class PagePermission < ActiveRecord::Base
belongs_to :page
belongs_to :user_type
# When you create this model, you should have a boolean attribute for editable
end
class UserType < ActiveRecord::Base
has_many :page_permissions
has_many :pages, :through => :page_permissions
end
I think following this approach will allow you to consolidate to one join table which will be better in the future if you need to add additional attributes to the relationship (PagePermission) between Page and UserType.

At the very least, you probably want to add a Permission model. If it ever gets more complicated than what you've described, I would also recommend using CanCan.
class Permission < ActiveRecord::Base
#table is id, page_id, user_type_id, and permission_type (string).
belongs_to :page
belongs_to :user_type
end
In your controller, you can construct a filter chain like this:
class PagesController < ApplicationController
before_filter :load_page
before_filter :authorize_view!, only: [ :show ]
before_filter :authorize_edit!, only: [ :edit ]
def show
end
def edit
end
private
def load_page
#page = Page.find(params[:id])
end
def authorize_view!
if !#page.permissions.where(user_type_id: current_user.user_type_id, permission_type: "view").exists?
flash[:notice] = "You do not have permission to view that page."
redirect to root_path
end
end
def authorize_edit!
if !#page.permissions.where(user_type_id: current_user.user_type_id, permission_type: "edit").exists?
flash[:notice] = "You do not have permission to edit that page."
redirect to root_path
end
end
end
(This assumes you have a current_user method in your app).

Related

Rails - has_many :through association

I’ve setup a has_many :through association between a User and Organisation model, using a Membership model as the join.
class Organisation < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
end
class User < ActiveRecord::Base
. . .
has_many :memberships
has_many :organisations, :through => memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :organisation
end
When a User creates an organisation, I want a membership to automatically be created linking the user to that organisation.
Where is the best place to attack this?
Options I’ve been investigating:
Use an after_create callback on the organisation
Move this process into a separate Ruby class.
In the organisations Controller, create action.
?
How would you recommend I go about it?
Is there somewhere in the Rails Guides where it outlines best practices for this kind of thing?
Rails 4.2.5.
#config/routes.rb
resources :organizations #-> url.com/organizations/new
#app/controllers/organizations_controller.rb
class OrganizationsController < ApplicationController
before_action :authenticate_user!
def new
#organization = current_user.organizations.new
end
def create
#organization = current_user.organizations.new organization_params
#organization.save
end
private
def organization_params
params.require(:organization).permit(:x, :y, :z) #-> membership automatically created
end
end
The above will automatically create the associated membership; assuming you're using Devise & have access to the current_user method.
--
The best practice is the most succinct; there is no way you're "meant" to do it.
One of the biggest fallacies I see in Rails is people trying to find the most acceptable way to do something (as if there's a rulebook). The best thing you can do is get it working then refactor the code.
As you progress through your app, you'll find that certain patterns can be changed, some removed and many combined. The more "DRY" you make your code, the better it is (as a rule).
My idea is way 3. Normaly, when set up many - many association in models, we should do creating temp table record auto through controller.
For example in controller you can write:
#organisation = current_user.organisations.build organisation_params
if #organisation.save
....
So that if #organisation is save then after that memberships record auto generate.
You can see this tutorial to see that:
http://blog.teamtreehouse.com/what-is-a-has_many-through-association-in-ruby-on-rails-treehouse-quick-tip
I think you should just be able to do something like:
org = Organisation.new
org.otherstuff = "populate other stuff"
org.users = [user_who_created]
org.save
After that the two should be related...? If you wanted to encapsulate this behavior you could do something like have a class method on Organization like create_org_for_user(name, user) and then do this logic in there, or you could do it in the controller action that handles the creation.
If there isn't any additional logic other than creating an organization and membership, I would just do #3. However, if you are planning on adding more logic for creating a new organization in the future, I would create a new service (#2).
Where is the best place to attack this?
I would like to say you should write this in OrganisationsController's create action to make a DRY on update action as well(use strong parameter) .Because form attribute that you are getting is from outer worlds and it is best to use permit method on required params using Strong Parametre concept.
def create
#organisation = Organisation.new(organisation_params)
...
end
def organisation_params
# here you could write all the params which you want to permit from outer worlds
end
Nore more info about Strong Parameters

has_and_belongs_to_many_relationship but only for non-admin users

I have a users model and a companies model. Many users to many companies. This works great for normal users. It's not so great for users with admin abilities. Admins need to see all companies. I'd rather not have to assign all companies to the admins.
Currently, I'm using methods inside my model like so:
class Company
def self.for_user
if User.current.is_admin?
Company.all
else
User.companies
end
end
end
Is there a way to make the has_and_belongs_to_many relationship contingent on a condition?
I don't think you can define a relationship conditionally by passing any kind of option to the has_and_belongs_to_many method.
I am a big fat fan of STI so I'd probably do something like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :companies
end
class Admin < User
def companies
Companies.all
end
end
thanks to this question/answer, I figured it out
Modify the behavior of has_many or use scope?
so, my association becomes
class User
has_and_belongs_to_many :companies do
def visible
if proxy_owner.can? "company:view_all"
Company.scoped
else
self
end
end
end
end
Then you can call it with
User.find(x).companies.visible

rails associations - How would you represent this relationship?

I am trying to figure out a best way to represent the following relationship.
Newspaper has_many Articles
Newspaper has_many Subscribers
Subscribers are allowed to save the articles for their personal page.
Two Questions:
1) How would the relationship look like in rails? How would the action 'save' look like?
The following using has_many does not seem right to me:
class ArticleController < ApplicationController
def save
a = Article.find(101)
#user.saved_articles << a
end
end
2) Do I need a join table Saved_Articles that looked like this?
Saved_Articles
----------------
user_id, article_id
What you're describing is a many-to-many relationship. In terms of Rails associations, this is one way of defining it:
class Newspaper
has_many :articles
has_many :subscribers
end
class Subscriber
belongs_to :newspaper
has_and_belongs_to_many :articles
end
class Article
belongs_to :newspaper
has_and_belongs_to_many :subscribers
end
By using has_and_belongs_to_many, you will need a join table, but it would need to be called articles_subscribers and would have subscriber_id and article_id fields:
articles_subscribers
--------------------
article_id
subscriber_id
Your save action would then look something like the following, assuming #user was an instance of Subscriber:
class ArticlesController < ApplicationController
def save
#user.articles << Article.find(params[:id])
# handle the response - render or redirect
end
end
See http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many for more information on has_and_belongs_to_many
Answer QUestion #1:
in models/newspaper.rb:
has_many :articles
has_many :subscribers
in controllers/newspapers_controller.rb
def create
#newspaper = NewsPaper.new(params[:newspaper])
if #newspaper.save
redirect_to
else
render :new
end
end
Answer for Question #2:
You don't need a join table for this. Just 3 separate tables (newspapers, articles, and subscribers) would be fine. The foreign keys for newspaper_id would be in articles and subscribers.

How to implement "business rules" in Rails?

What is the way to implement "business rules" in Rails?
Let us say I have a car and want to sell it:
car = Cars.find(24)
car.sell
car.sell method will check a few things:
does current_user own the car?
check: car.user_id == current_user.id
is the car listed for sale in the sales catalog?
check: car.catalogs.ids.include? car.id
if all o.k. then car is marked as sold.
I was thinking of creating a class called Rules:
class Rules
def initialize(user,car)
#user = user
#car = car
end
def can_sell_car?
#car.user_id == #user.id && #car.catalogs.ids.include? #car.id
end
end
And using it like this:
def Car
def sell
if Rules.new(current_user,self).can_sell_car
..sell the car...
else
#error_message = "Cannot sell this car"
nil
end
end
end
As for getting the current_user, I was thinking of storing it in a global variable?
I think that whenever a controller action is called, it's always a "fresh" call right? If so then storing the current user as a global variable should not introduce any risks..(like some other user being able to access another user's details)
Any insights are appreciated!
UPDATE
So, the global variable route is out! Thanks to PeterWong for pointing out that global variables persist!
I've now thinking of using this way:
class Rules
def self.can_sell_car?(current_user, car)
......checks....
end
end
And then calling Rules.can_sell_car?(current_user,#car) from the controller action.
Any thoughts on this new way?
I'd use the following tables:
For buyers and sellers:
people(id:int,name:string)
class Person << ActiveRecord::Base
has_many :cars, :as => :owner
has_many :sales, :as => :seller, :class_name => 'Transfer'
has_many :purchases, :as => :buyer, :class_name => 'Transfer'
end
cars(id:int,owner_id:int, vin:string, year:int,make:string,model:string,listed_at:datetime)
listed_at is the flag to see if a Car is for sale or not
class Car << ActiveRecord::Base
belongs_to :owner, :class_name => 'Person'
has_many :transfers
def for_sale?
not listed_at.nil?
end
end
transfers(id:int,car_id:int,seller_id:int,buyer_id:int)
class Transfer << ActiveRecord::Base
belongs_to :car
belongs_to :seller, :class_name => 'Person'
belongs_to :buyer, :class_name => 'Person'
validates_with Transfer::Validator
def car_owned_by_seller?
seller_id == car.owner_id
end
end
Then you can use this custom validator to setup your rules.
class Transfer::Validator << ActiveModel::Validator
def validate(transfer)
transfer.errors[:base] = "Seller doesn't own car" unless transfer.car_owned_by_seller?
transfer.errors[:base] = "Car isn't for sale" unless transfer.car.for_sale?
end
end
First, the standard rails practice is to keep all business logic in the models, not the controllers. It looks like you're heading that direction, so that's good -- BUT: be aware, there isn't a good clean way to get to the current_user from the model.
I wouldn't make a new Rules model (although you can if you really want to do it that way), I would just involve the user model and the car. So, for instance:
class User < ActiveRecord::Base
...
def sell_car( car )
if( car.user_id == self.id && car.for_sale? )
# sell car
end
end
...
end
class Car < ActiveRecord::Base
...
def for_sale?
!catalog_id.nil?
end
...
end
Obviously I'm making assumptions about how your Catalog works, but if cars that are for_sale belong_to a catalog, then that method would work - otherwise just adjust the method as necessary to check if the car is listed in a catalog or not. Honestly it would probably be a good idea to set a boolean value on the Car model itself, this way users could simply toggle the car being for sale or not for sale whenever you want them to ( either by marking the car for sale, or by adding the car to a catalog, etc. ).
I hope this gives you some direction! Please feel free to ask questions.
EDIT: Another way to do this would be to have methods in your models like:
user.buy_car( car )
car.transfer_to( user )
There are many ways to do it putting the logic in the object its interacting with.
I would think this would a prime candidate for using a database, and then you could use Ruby to query the different tables.
You might take a look at the declarative authorization gem - https://github.com/stffn/declarative_authorization
While it's pre-configured for CRUD actions, you can easily add your own actions (buy, sell) and put their business logic in the authorization_rules.rb config file. Then, in your controllers, views, and even models!, you can easily ask permitted_to? :buy, #car
I'm doing something similar with users and what they can do with photo galleries. I'm using devise for users and authentication, and then I set up several methods in the user model that determine if the user has various permissions (users have many galleries through permissions) to act on that gallery. I think it looks like the biggest problem you are having is with determining your current user, which can be handled quite easily with Devise, and then you can add a method to the user model and check current_user.can_sell? to authorized a sale.

How to associate a new model with existing models using has_and_belongs_to_many

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.

Resources