Using Rails 6 and CanCanCan. Here are my models:
class Shop < ApplicationRecord
has_many :shop_reviews, dependent: :destroy
end
class ShopReview < ApplicationRecord
belongs_to :shop, counter_cache: true
belongs_to :user_profile, counter_cache: true
end
class User < ApplicationRecord
has_one :user_profile, dependent: :destroy
has_many :shop_reviews, through: :user_profile
end
class UserProfile < ApplicationRecord
belongs_to :user
has_many :shop_reviews
end
ShopReview table:
id, shop_id, user_profile_id, review
Association:
1. A shop can have many reviews
2. Only one User Profile can have one review on a shop
Abilities I want to define:
1. User Profile can edit, update, destroy his own review of a shop
2. User Profile cannot create a new review if a review of his exist on that shop
What I tried:
class Ability
include CanCan::Ability
def initialize(user)
can :read, :all
if user.present?
can [:update, :destroy], ShopReview, user_profile_id: user.user_profile.id
can :create, ShopReview do
!ShopReview.exists?(user_profile_id: user.user_profile.id, shop_id: :shop_id)
end
end
end
end
But I can't seem to pass in the shop_id on visiting /shops/11/shop_reviews/new. Here's the log:
Started GET "/shops/11/shop_reviews/new" for 127.0.0.1 at 2020-01-04 00:23:10 +0800
Processing by ShopReviewsController#new as HTML
Parameters: {"shop_id"=>"11"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 9], ["LIMIT", 1]]
UserProfile Load (0.2ms) SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = $1 LIMIT $2 [["user_id", 9], ["LIMIT", 1]]
↳ app/models/ability.rb:9:in `initialize'
ShopReview Exists? (0.3ms) SELECT 1 AS one FROM "shop_reviews" WHERE "shop_reviews"."user_profile_id" = $1 AND "shop_reviews"."shop_id" = $2 LIMIT $3 [["user_profile_id", 8], ["shop_id", nil], ["LIMIT", 1]]
↳ app/models/ability.rb:11:in `block in initialize'
Shop Load (0.2ms) SELECT "shops".* FROM "shops" WHERE "shops"."id" = $1 ORDER BY "shops"."created_at" DESC LIMIT $2 [["id", 11], ["LIMIT", 1]]
You should use gem pundit to create authorizations in your app. Pundit is easy to scale than gem cancancan. Because pundit allows your app to create authorization on each model. You can search more about two gems and choose what is most suitable for you.
I have a Rails (5.2.2) app (Postgres database) with some Models related to different geographies:
- districts (have many sectors)
-- sectors (have many cells, have one district)
--- cells (have many villages, have one sector)
---- villages (have many facilities, have one cell)
----- facilities (have one village)
I also have a Report Model, which, for context, records the quantity of specific technology distributed in a specific location.
#<Report id: nil, date: nil, technology_id: nil, user_id: nil, contract_id: nil, model_gid: nil, distributed: nil, checked: nil, created_at: nil, updated_at: nil, people: nil, households: nil>
This location can be any of the geography models. So I'm using GlobalID stored as a string in model_gid on the Report record.
e.g.:
#<Report id: 1, ... model_gid: "gid://liters-tracker/Village/64", ...>
Then I wrote some scopes that work fine:
scope :only_districts, -> { where('model_gid ILIKE ?', '%/District/%') }
scope :only_sectors, -> { where('model_gid ILIKE ?', '%/Sector/%') }
scope :only_cells, -> { where('model_gid ILIKE ?', '%/Cell/%') }
scope :only_villages, -> { where('model_gid ILIKE ?', '%/Village/%') }
scope :only_facilities, -> { where('model_gid ILIKE ?', '%/Facility/%') }
I thought this was a good approach because my report.model method works:
def model
GlobalID::Locator.locate model_gid
end
e.g.:
2.4.5 :001 > Report.first.model
Report Load (0.5ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Village Load (0.4ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
=> #<Village id: 64, name: "Ruhanga", cell_id: 11, gis_id: 13080406, latitude: -2.00828333333333, longitude: 30.1708, population: 518, households: 179, created_at: "2019-01-21 22:53:06", updated_at: "2019-01-21 22:53:06">
I opted to do this string field instead of a polymorphic association because GlobalID::Locator methods can accept strings and parse out the model and ID from it. So why hassle with the association? Maybe this is the fundamental flaw in my thinking?
Because finding records based upon the model_gid seems to fail:
2.4.5 :045 > Report.all.where(model_gid: Report.first.model_gid)
Report Load (0.4ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Report Load (0.5ms) SELECT "reports".* FROM "reports" WHERE "reports"."model_gid" = $1 LIMIT $2 [["model_gid", "--- gid://liters-tracker/Village/64\n"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
(honestly not sure why the SQL is morphed into "--- gid://liters-tracker/Village/64\n" and if this is actually my problem)
2.4.5 :046 > Report.all.where("model_gid ILIKE ?", Report.first.model_gid)
Report Load (0.5ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Report Load (3.2ms) SELECT "reports".* FROM "reports" WHERE (model_gid ILIKE 'gid://liters-tracker/Village/64') LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
2.4.5 :049 > Report.all.where("model_gid = ?", Report.first.model_gid)
Report Load (0.3ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Report Load (0.6ms) SELECT "reports".* FROM "reports" WHERE (model_gid = 'gid://liters-tracker/Village/64') LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
I'm trying to get this method to work:
def self.related_to(record)
where(model_gid: record.to_global_id.to_s)
end
And I really don't understand why it's not working:
2.4.5 :010 > Report.first.model_gid
Report Load (0.6ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> "gid://liters-tracker/Village/64"
2.4.5 :011 > Village.find(64).to_global_id.to_s
Village Load (0.5ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
=> "gid://liters-tracker/Village/64"
2.4.5 :012 > Report.first.model_gid == Village.find(64).to_global_id.to_s
Report Load (0.4ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Village Load (0.3ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
=> true
2.4.5 :013 > Report.all.where(model_gid: Village.find(64).to_global_id.to_s)
Village Load (0.4ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
Report Load (0.4ms) SELECT "reports".* FROM "reports" WHERE "reports"."model_gid" = $1 LIMIT $2 [["model_gid", "--- gid://liters-tracker/Village/64\n"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
If I mimic the scopes, it does work:
def self.related_to(record)
where('model_gid ILIKE ?', "%#{record.to_global_id.to_s}%")
end
But, in the example records I've been showing, this would match Village #64 and Village #640, so it's not a good solution.
UPDATE
I thought maybe the special characters were throwing things off. But things work as expected when I use another string column on another Model:
2.4.5 :052 > Village.first.update(name: "gid://liters-tracker/Village/64")
Village Load (0.5ms) SELECT "villages".* FROM "villages" ORDER BY "villages"."id" ASC LIMIT $1 [["LIMIT", 1]]
(0.2ms) BEGIN
Cell Load (0.2ms) SELECT "cells".* FROM "cells" WHERE "cells"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Village Exists (0.3ms) SELECT 1 AS one FROM "villages" WHERE "villages"."gis_id" = $1 AND "villages"."id" != $2 LIMIT $3 [["gis_id", 11070101], ["id", 1], ["LIMIT", 1]]
Village Update (0.3ms) UPDATE "villages" SET "name" = $1, "updated_at" = $2 WHERE "villages"."id" = $3 [["name", "gid://liters-tracker/Village/64"], ["updated_at", "2019-07-06 22:16:38.585563"], ["id", 1]]
(1.2ms) COMMIT
=> true
2.4.5 :053 > Village.where(name: "gid://liters-tracker/Village/64")
Village Load (0.3ms) SELECT "villages".* FROM "villages" WHERE "villages"."name" = $1 LIMIT $2 [["name", "gid://liters-tracker/Village/64"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Village id: 1, name: "gid://liters-tracker/Village/64", cell_id: 1, gis_id: 11070101, latitude: -2.054922, longitude: 30.0912883, population: 513, households: 110, created_at: "2019-01-21 22:53:04", updated_at: "2019-07-06 22:16:38">]>
I thought maybe I needed an index on the Report.model_gid field. But it hasn't made a difference.
class AddModelGidIndexToReports < ActiveRecord::Migration[5.2]
def change
add_index :reports, :model_gid
end
end
UPDATE 2
(this is based upon my own provided 'answer', but since it's a question, I put it here)
#MichaelChaney:
Just so I'm clear, are you suggesting something like this:
class Report < ApplicationRecord
belongs_to :technology, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :contract, inverse_of: :reports
enum geography: { district: 'district', sector: 'sector', cell: 'cell', village: 'village', facility: 'facility' }
At this point, should I just add a geography_id integer column and stop using GlobalID?
What about just going polymorphic instead?
For the sake of closing the record, I switched to a polymorphic association. Probably not quite as fast as the Enum solution that #MichaelChaney suggests in the comments on the previous answer, but fast enough for my in-house app and creates an association known to my app.
class Report < ApplicationRecord
belongs_to :technology, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :contract, inverse_of: :reports
# serialize :model_gid #<-- this was real bad as #MichaelChaney points out
# enum geography: { district: 'district', sector: 'sector', cell: 'cell', village: 'village', facility: 'facility' } #<-- this is probably the fastest option
belongs_to :reportable, polymorphic: true #<-- this is probably the middle ground, as the :reportable_id and :reportable_type columns are indexed together
And this was partnered with the following on all my Geography models, e.g.:
class Facility < ApplicationRecord
belongs_to :village, inverse_of: :facilities
has_one :cell, through: :village, inverse_of: :facilities
has_one :sector, through: :cell, inverse_of: :facilities
has_one :district, through: :sector, inverse_of: :facilities
has_many :reports, as: :reportable, inverse_of: :reportable #<-- tadaa
So now I don't even need my initial method as I can compare the results of reports.reportable with the record I have to see if they are associated.
The lesson I learned: in the early stages, I need to think more about RdBMS and what associations I'll care a lot about so I don't try doing dumb Regex searches across my dB.
The other lesson: keep better notes in my code base, so when I change strategies I can correctly un-wind things I implemented.
Ugh. This is a face-palm moment.
class Report < ApplicationRecord
belongs_to :technology, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :contract, inverse_of: :reports
serialize :model_gid
scope :only_districts, -> { where('model_gid ILIKE ?', '%/District/%') }
...
Report.model_gid is serialized, which I did before I discovered GlobalID. I think I was planning to save some key-value hash like {model: 'Village', id: '64'}.
Now to figure out how to un-serialize a column.
In my controllers/common/roles_controller.rb I would like to check if particular role (ID) belongs to current_user company users and if not, redirect to errors_path:
def correct_role
role_user = Role.where(:id => params[:id]).select('user_id').first
company_user = current_user.companies.includes(:users)
redirect_to(errors_path) unless company_user.include? role_user.id
end
Definitions:
role_user - finds user ID for particular role ID ("user_id" is column of roles table)
company_user - finds all user ID who belong to companies, which belong to current_user
models/role.rb
belongs_to :user, optional: true, inverse_of: :roles
accepts_nested_attributes_for :user
enum general: { seller: 1, buyer: 2, seller_buyer: 3}, _suffix: true
enum dashboard: { denied: 0, viewer: 1, editer: 2, creater: 3, deleter: 4}, _suffix: true
models/user.rb
#User has roles
has_many :roles
accepts_nested_attributes_for :roles, reject_if: proc { |attributes| attributes[:name].blank? }
# User has many companies
has_many :accounts, dependent: :destroy
has_many :companies, through: :accounts
models/account.rb
class Account < ApplicationRecord
belongs_to :company
belongs_to :user
accepts_nested_attributes_for :company, :user
end
models/company.rb
has_many :accounts, dependent: :destroy
has_many :users, through: :accounts
At the moment with role_user and company_user I can find both ID, however I cannot do the checking part. How do I do that correctly, please? Thank you for any help!
Update
#sajan code give this in console when I open /common/roles/1/edit (current_user ID=1 and should be allowed to edit):
Parameters: {"id"=>"1"}
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.6ms) SELECT COUNT(*) FROM "companies" INNER JOIN "accounts" ON "companies"."id" = "accounts"."company_id" WHERE "accounts"."user_id" = ? [["user_id", 1]]
(0.3ms) SELECT "roles".id FROM "roles" WHERE "roles"."user_id" = ? [["user_id", 1]]
#Abhishek Kumar code in console gives:
Parameters: {"id"=>"1"}
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Company Load (0.5ms) SELECT "companies".* FROM "companies" INNER JOIN "accounts" ON "companies"."id" = "accounts"."company_id" WHERE "accounts"."user_id" = ? [["user_id", 1]]
(0.4ms) SELECT "users".id FROM "users" INNER JOIN "accounts" ON "users"."id" = "accounts"."user_id" WHERE "accounts"."company_id" = ? [["company_id", 13]]
Role Load (0.2ms) SELECT "roles"."user_id" FROM "roles" WHERE "roles"."id" = ? ORDER BY "roles"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
Update v2
So I'm trying to use this code:
def correct_role
company_user_ids = current_user.companies.map(&:user_ids)
role_user = Role.where(:id => params[:id]).select('user_id').first
unless role_user.user_id.in?(company_user_ids)
redirect_to(errors_path)
end
end
however it redirects to errors_path in any case, this is what I have in console:
Parameters: {"id"=>"1"}
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Company Load (0.5ms) SELECT "companies".* FROM "companies" INNER JOIN "accounts" ON "companies"."id" = "accounts"."company_id" WHERE "accounts"."user_id" = ? [["user_id", 1]]
(0.4ms) SELECT "users".id FROM "users" INNER JOIN "accounts" ON "users"."id" = "accounts"."user_id" WHERE "accounts"."company_id" = ? [["company_id", 13]]
Role Load (0.3ms) SELECT "roles"."user_id" FROM "roles" WHERE "roles"."id" = ? ORDER BY "roles"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
It seems that a better logic would be to define on the model for the current_user an association through companies and users to roles.
Then you can check:
def correct_role
redirect_to(errors_path) unless current_user.company_user_roles.where(id: params[:id]).exists?
end
It would be a single SQL statement that would return zero or one rows, and execute very quickly with the appropriate indexes in place.
Edit:
To company.rb, add:
has_many :user_roles, through: :users, source: :roles
To user.rb (assuming that current_user is an instance of this model) add:
has_many :company_user_roles, through: :companies, source: :user_roles
Maybe you could do like this:
def correct_role
unless current_user.companies.count > 0 && current_user.role_ids.include?(params[:id])
redirect_to(errors_path)
end
end
I'm trying to make will_paginate's :order to work with this array:
#posts = current_user.subscribed_tags.map(&:posts).flatten.paginate(:page => params[:page],
per_page => 5,
:order => "created_at DESC")
Right now, it doesn't matter what value I give to :order, posts are ordered by tag (I think the date of creation of the tag). I want them to appear according to the posts creation date. I think the problem is that will_paginate is using the tags as reference and not the posts.
How to solve this issue (Maybe I have to define #posts in another way)?
Additional Information:
Users can subscribe to tags (So in the index page the user only sees posts with tags he or she is subscribe to).
Models:
User model:
class User < ActiveRecord::Base
(Devise)
has_many :posts, :dependent => :destroy
has_many :subscriptions
has_many :subscribed_tags, :source => :tag, :through => :subscriptions
attr_writer :subscribed_tag_names
after_save :assign_subscribed_tags
def subscribed_tag_names
#subscribed_tag_names || subscribed_tags.map(&:name).join(' ')
end
private
def assign_subscribed_tags
#self.subscribed_tags = []
return if #subscribed_tag_names.blank?
#subscribed_tag_names.split(" ").each do |name|
subscribed_tag = Tag.find_or_create_by_name(name)
self.subscribed_tags << subscribed_tag unless subscribed_tags.include?(subscribed_tag)
end
end
end
Tag model:
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :posts, :through => :taggings
has_many :subscriptions
has_many :subscribed_users, :source => :user, :through => :subscriptions
def tag_posts_count
"#{self.name} (#{self.posts.count})"
end
end
Generated SQL:
Processing by PostsController#index as HTML
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
Tag Load (0.5ms) SELECT "tags".* FROM "tags" INNER JOIN "subscriptions" ON "tags"."id" = "subscriptions"."tag_id" WHERE "subscriptions"."user_id" = 2
Post Load (0.7ms) SELECT "posts".* FROM "posts" INNER JOIN "taggings" ON "posts"."id" = "taggings"."post_id" WHERE "taggings"."tag_id" = 6
Post Load (0.6ms) SELECT "posts".* FROM "posts" INNER JOIN "taggings" ON "posts"."id" = "taggings"."post_id" WHERE "taggings"."tag_id" = 10
Post Load (0.7ms) SELECT "posts".* FROM "posts" INNER JOIN "taggings" ON "posts"."id" = "taggings"."post_id" WHERE "taggings"."tag_id" = 1
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
Tag Load (0.5ms) SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."post_id" = 42
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = 4 LIMIT 1
Tag Load (0.5ms) SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."post_id" = 38
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
Comment Load (0.6ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 52 ORDER BY "comments"."id" DESC LIMIT 1
CACHE (0.0ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 52 ORDER BY "comments"."id" DESC LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
CACHE (0.0ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 52 ORDER BY "comments"."id" DESC LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
Tag Load (0.5ms) SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."post_id" = 52
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = 5 LIMIT 1
Tag Load (0.5ms) SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."post_id" = 55
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 5 LIMIT 1
Comment Load (0.7ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 57 ORDER BY "comments"."id" DESC LIMIT 1
CACHE (0.0ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 57 ORDER BY "comments"."id" DESC LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
CACHE (0.0ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 57 ORDER BY "comments"."id" DESC LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
Tag Load (0.5ms) SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."post_id" = 57
Rendered layouts/_sidebar.html.erb (12.6ms)
Rendered posts/index.html.erb within layouts/application (314.3ms)
Rendered layouts/_header.html.erb (5.2ms)
Rendered layouts/_footer.html.erb (0.1ms)
Completed 200 OK in 1242ms (Views: 392.8ms | ActiveRecord: 12.5ms)
You are calling paginate on a regular array rather than on a sql result set.
[1,2,3,4].paginate(:page => 1, :per_page => 2) # [1, 2]
current_user.subscribed_tags.map(&:posts) # returns an array
If you are on Rails 3.1 and above:
class User < ActiveRecord::Base
has_many :posts, :dependent => :destroy
has_many :subscriptions
has_many :subscribed_tags, :source => :tag, :through => :subscriptions
has_many :subscribed_posts, :source => :posts, :through => :subscribed_tags
end
class Subscription
belongs_to :user
belongs_to :tag
end
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :posts, :through => :taggings
has_many :subscriptions
end
Now you can
current_user.subscribed_posts.paginate(:page => params[:page],
per_page => 5,
:order => "created_at DESC")