I'm working on an engine where any model can have a has_many association with Permit as Permissible:
class Permit < ActiveRecord::Base
belongs_to :permissible, polymorphic: true
end
module Permissible
def self.included(base)
base.class_eval do
has_many :permits, as: :permissible
end
end
class Group < ActiveRecord::Base
include Permissible
end
class GroupAllocation < ActiveRecord::Base
belongs_to :person
belongs_to :group
end
class Person < ActiveRecord::Base
include Permissible
has_many :group_allocations
has_many :groups, through: :group_allocations
end
class User < ActiveRecord::Base
belongs_to :person
end
So, Group has_many :permits and Person has_many :permits. What I am trying to do is dynamically create associations on User that uses the permits association as a source, and chain associations on other models down to User by doing the same. This can be done manually (in rails 3.1+) with:
class Person
has_many :group_permits, through: :person, source: :permits
end
class User
has_many :person_permits, through: :person, source: :permits, class_name: Permit
has_many :person_group_permits, through: :person, source: :group_permits, class_name: Permit
end
However, in practice, Permissible will be included on many models, so I'm trying to write a class method on User (actually within another module, but no need to confuse things more) that can traverse User.reflect_on_all_associations and create an array of new associations, which may be many associations deep each.
Looking for input on how to do this cleanly in rails 3.2.8.
Here is how I did it (implementation code varies slightly from the details given in the question):
module Authorisable
def self.included(base)
base.class_eval do
base.extend ClassMethods
end
end
module ClassMethods
class PermissionAssociationBuilder
def build_permissions_associations(klass)
chains = build_chains_from(klass)
chains.select! {|c| c.last.klass.included_modules.include? DistributedAuthorisation::Permissible}
permissions_associations = []
chains.each do |chain|
source_name = :permissions
chain.reverse.each do |r|
assoc_name = :"#{r.name}_#{source_name}"
r.active_record.has_many assoc_name, through: r.name.to_sym, source: source_name, class_name: DistributedAuthorisation::Permission
source_name = assoc_name
end
permissions_associations << source_name
end
return permissions_associations
end
private
def build_chains_from(klass)
chains = reflections_to_follow(klass).map {|r| [r]}
chains.each do |chain|
models = chain.map {|r| r.klass}.unshift klass
reflections_to_follow(models.last).each do |r|
chains << (chain.clone << r) unless models.include? r.klass
end
end
end
def reflections_to_follow(klass)
refs = klass.reflect_on_all_associations
refs.reject {|r| r.options[:polymorphic] or r.is_a? ActiveRecord::Reflection::ThroughReflection}
end
end
def permissions_associations
#permissions_associations ||= PermissionAssociationBuilder.new.build_permissions_associations(self)
end
end
Probably not the most efficient method, but it adds the chains I'm after with Klass.permissions_associations, and stores their symbols in class instance variable.
I'd be happy to hear suggestions on how to improve it.
Related
I have a collection of models: Users, Cvs, Skills, Educations, & Experiences.
# models/user.rb
class User < ApplicationRecord
has_one :cv
has_many :skills, through: :cv, inverse_of: :user
has_many :educations, through: :cv, inverse_of: :user
has_many :experiences, through: :cv, inverse_of: :user
...
end
# models/cv.rb
class Cv < ApplicationRecord
belongs_to :user
has_many :skills
has_many :educations
has_many :experiences
...
end
# models/skill.rb
class Skill < ApplicationRecord
belongs_to :cv
belongs_to :user, inverse_of: :skill
end
# models/education.rb
class Education < ApplicationRecord
belongs_to :cv
belongs_to :user, inverse_of: :education
end
# models/experience.rb
class Experience < ApplicationRecord
belongs_to :cv
belongs_to :user, inverse_of: :experience
end
So, basically, a user has 1 cv and many skills, educations, and experiences that are nested in that cv
A Cv has many attributes other than the nested skills, educations, and experiences.
How can I iterate through all of the attributes in a Cv, including the nested ones? I tried this:
#user = User.new
#user.build_cv
#user.cv.educations.build
#user.cv.experiences.build
#user.cv.skills.build
#user.cv.attributes.each_pair do |attr, value|
puts "#{attr}: #{value}"
end
But this just lists the attributes that are directly in the Cv model and not the attributes for the Cv's education, skill, and experience. What I eventually need to do is iterate through the Cv attributes and it's nested attributes searching for blank values in order to make sure a Cv has been completed. I could do that with a simple .blank? on each attribute if I could just figure out how to iterate through them all.
So far, this is the most concise way I came up with to do it, I was just hoping that there was a built-in helper method for something like this:
def is_complete?
user = current_user
user.cv.attributes.each do |attr|
return false if attr.blank?
end
user.cv.skills.each do |skill|
skill.attributes.each do |sa|
return false if sa.blank?
end
end
user.cv.educations.each do |edu|
edu.attributes.each do |ea|
return false if ea.blank?
end
end
user.cv.experiences.each do |exp|
exp.attributes.each do |exa|
return false if edu.blank?
end
end
true
end
You can use reflect_on_all_associations
Cv.reflect_on_all_associations(:has_many).map(&:name).each do |attr|
if #user.cv.send(attr).blank?
# do something
end
end
If you want to check individual attribute elements...
Cv.reflect_on_all_associations(:has_many).map(&:name).each do |attr|
#user.cv.send(attr).each do |item|
if item.blank?
# do something
end
end
end
or if you're looking at checking each attribute of each item...
If you want to check individual attribute elements...
Cv.reflect_on_all_associations(:has_many).map(&:name).each do |attr|
#user.cv.send(attr).each do |item|
item.attributes.each_pair do |subattr, value|
puts "#{item.class}.#{subattr}: #{value}"
end
end
end
But a more natural way to do this would be to put validations on the association models.
I would like to know if there is a more elegant way to chain has_many through relationships. In my example I have a user whom can have multiple roles. Each role has multiple permissions. So a user has multiple permissions. The code below works fine, but I am wonder if there is a better way to do this.
class User < ActiveRecord::Base
has_many :role_user_mappings
has_many :roles, through: :role_user_mappings
def permissions
permitted_actions = []
self.roles.each do |role|
role.permissions.each do |permission|
permitted_actions << permission
end
end
permitted_actions
end
end
class Role < ActiveRecord::Base
has_many :permission_role_mappings
has_many :permissions, through: :permission_role_mappings
end
class Permission < ActiveRecord::Base
end
class PermissionRoleMapping < ActiveRecord::Base
belongs_to :permission
belongs_to :role
end
class RoleUserMapping < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
I would like to be able to do this.
user.permissions
EDIT: Tried
On thing that I tried that at least DRYs the User model a little bit is adding the function as a concern
module Permittable
extend ActiveSupport::Concern
def permissions
permitted_actions = []
self.roles.each do |role|
role.permissions.each do |permission|
permitted_actions << permission
end
end
permitted_actions
end
end
Have you tried..
class User < ActiveRecord::Base
has_many :role_user_mappings
has_many :roles, through: :role_user_mappings
has_many :permissions, through: roles
That should give you
user.permissions
I'm not sure when the HMT via HMT feature was made available, I know it was missing in earlier versions of rails, but it works for me on Rails 5.
If you do this:
class Permission < ActiveRecord::Base
has_many :permission_role_mappings
end
Then you should be able to do this:
class User < ActiveRecord::Base
has_many :role_user_mappings
has_many :roles, through: :role_user_mappings
def permissions
Permission.
joins(:permission_role_mappings).
where(permission_role_mappings: {role: roles})
end
end
By the way, you may already know this and it may be why you're asking the question... but this is going to give you an N+1 query:
permitted_actions = []
self.roles.each do |role|
role.permissions.each do |permission|
permitted_actions << permission
end
end
permitted_actions
Also, FWIW, when wanting an array back from a collection, you don't need to do:
permitted_actions = []
self.roles.each do |role|
...
end
permitted_actions
You can just do:
roles.map do |role|
...
end
Since map returns an array.
I spent an hour debugging a very strange rails behavior.
Given:
app/models/user.rb
class User < ApplicationRecord
...
has_many :images
has_many :videos
...
has_many :tags
...
end
app/models/image.rb
class Image < ApplicationRecord
...
belongs_to :user
...
has_and_belongs_to_many :tags
...
include TagsFunctions
...
end
app/models/video.rb
class Video < ApplicationRecord
...
include TagsFunctions
...
belongs_to :user
...
has_and_belongs_to_many :tags
...
end
app/models/tag.rb
class Tag < ApplicationRecord
belongs_to :user
validates :text, uniqueness: {scope: :user}, presence: true
before_create :set_code
def set_code
return if self[:code].present?
loop do
self[:code] = [*'A'..'Z'].sample(8).join
break if Tag.find_by(code: self[:code]).nil?
end
end
end
app/models/concerns/tags_functions.rb
module TagsFunctions
extend ActiveSupport::Concern
# hack for new models
included do
attr_accessor :tags_after_creation
after_create -> { self.tags_string = tags_after_creation if tags_after_creation.present? }
end
def tags_string
tags.pluck(:text).join(',')
end
def tags_string=(value)
unless user
#tags_after_creation = value
return
end
#tags_after_creation = ''
self.tags = []
value.to_s.split(',').map(&:strip).each do |tag_text|
tag = user.tags.find_or_create_by(text: tag_text)
self.tags << tag
end
end
end
If I execute such code:
user = User.first
tags_string = 'test'
image = user.images.create(tags_string: tags_string)
video = user.videos.create(tags_string: tags_string)
It will give 1 item in image.tags, but 2 duplicate items in video.tags
But if we change code this way:
user = User.first
tags_string = 'test'
image = Image.create(user: user, tags_string: tags_string)
video = Video.create(user: user, tags_string: tags_string)
everything works fine, 1 tag for an image and 1 tag for a video
And even more...
If we move include TagsFunctions below has_and_belongs_to_many :tags, in a video.rb file, both code examples work fine.
I thought I know rails pretty well, but this behavior is really unclear for me.
Rails version: 5.1.1
It seems like you may want to check these 2 lines
tag = user.tags.find_or_create_by(text: tag_text)
self.tags << tag
What seems to be happening here is a tag is being create for the user but also the actual video record. But it is hard to know without seeing if there is anything in the tag model. It may be good to avoid associating the tag association with the user in the tags function.
I think what you have is an X and Y problem since the domain can be modeled better in the first place:
# rails g model tag name:string:uniq
class Tag < ApplicationRecord
has_many :taggings
has_many :tagged_items, through: :taggings, source: :resource
has_many :videos, through: :taggings, source: :resource, source_type: 'Video'
has_many :images, through: :taggings, source: :resource, source_type: 'Image'
end
# rails g model tagging tag:belongs_to tagger:belongs_to resource:belongs_to:polymorphic
class Tagging < ApplicationRecord
belongs_to :tag
belongs_to :tagger, class_name: 'User'
belongs_to :resource, polymorpic: true
end
class User < ApplicationRecord
has_many :taggings, foreign_key: 'tagger_id'
has_many :tagged_items, through: :taggings, source: :resource
has_many :tagged_videos, through: :taggings, source: :resource, source_type: 'Video'
has_many :tagged_images, through: :taggings, source: :resource, source_type: 'Image'
end
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :resource
has_many :tags, through: :taggings
end
# example
# #video.tag!('#amazeballs', '#cooking', tagger: current_user)
def tag!(*names, tagger:)
names.each do |name|
tag = Tag.find_or_create_by(name: name)
taggnings.create(tag: tag, tagger: tagger)
end
end
end
This creates a normalized tags table that we can use for lookup instead of comparing string values. has_and_belongs_to_many is only really useful in the simplest of cases where you are just joining two tables and never will need to query the join table directly (in other words using HABTM is a misstake 90% of the time).
Using callbacks to "hack into" HABTM just makes it worse.
You can use a bit of metaprogramming to cut the duplication when setting up different taggable classes.
class Video < ApplicationRecord
include Taggable
end
class Image< ApplicationRecord
include Taggable
end
I have a site that allows users to log in via multiple services (LinkedIn, Email, Twitter, etc..).
I have the below structure set up to model a User and their multiple identities. Basically a user can have multiple identieis, but only one of a given type (e.g. can't have 2 Twitter identiteis).
I decided to set it up as a polymorphic relationship, as drawn below. Basically there's a middle table identities that maps a User entry to multiple *_identity tables.
The associations are as follows (shown only for LinkedInIdentity, but can be extrapolated)
# /app/models/user.rb
class User < ActiveRecord::Base
has_many :identities
has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"
...
end
# /app/models/identity
class Identity < ActiveRecord::Base
belongs_to :user
belongs_to :identity, polymorphic: true
...
end
# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
has_one :identity, as: :identity
has_one :user, through: :identity
...
end
The problem I'm running into is with the User model. Since it can have multiple identities, I use has_many :identities. However, for a given identity type (e.g. LinkedIn), I used has_one :linkedin_identity ....
The problem is that the has_one statement is through: :identity, and there's no singular association called :identity. There's only a plural :identities
> User.first.linkedin_identity
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :identity in model User
Any way around this?
I would do it like so - i've changed the relationship name between Identity and the others to external_identity, since saying identity.identity is just confusing, especially when you don't get an Identity record back. I'd also put a uniqueness validation on Identity, which will prevent the creation of a second identity of the same type for any user.
class User < ActiveRecord::Base
has_many :identities
has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"
end
# /app/models/identity
class Identity < ActiveRecord::Base
#fields: user_id, external_identity_id
belongs_to :user
belongs_to :external_identity, polymorphic: true
validates_uniqueness_of :external_identity_type, :scope => :user_id
...
end
# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
# Force the table name to be singular
self.table_name = "linkedin_identity"
has_one :identity
has_one :user, through: :identity
...
end
EDIT - rather than make the association for linkedin_identity, you could always just have a getter and setter method.
#User
def linkedin_identity
(identity = self.identities.where(external_identity_type: "LinkedinIdentity").includes(:external_identity)) && identity.external_identity
end
def linkedin_identity_id
(li = self.linkedin_identity) && li.id
end
def linkedin_identity=(linkedin_identity)
self.identities.build(external_identity: linkedin_identity)
end
def linkedin_identity_id=(li_id)
self.identities.build(external_identity_id: li_id)
end
EDIT2 - refactored the above to be more form-friendly: you can use the linkedin_identity_id= method as a "virtual attribute", eg if you have a form field like "user[linkedin_identity_id]", with the id of a LinkedinIdentity, you can then do #user.update_attributes(params[:user]) in the controller in the usual way.
Here is an idea that has worked wonderfully over here for such as case. (My case is a tad diffferent since all identites are in the same table, subclasses of the same base type).
class EmailIdentity < ActiveRecord::Base
def self.unique_for_user
false
end
def self.to_relation
'emails'
end
end
class LinkedinIdentity < ActiveRecord::Base
def self.unique_for_user
true
end
def self.to_relation
'linkedin'
end
end
class User < ActiveRecord::Base
has_many :identities do
[LinkedinIdentity EmailIdentity].each do |klass|
define_method klass.to_relation do
res = proxy_association.select{ |identity| identity.is_a? klass }
res = res.first if klass.unique_for_user
res
end
end
end
end
You can then
#user.identities.emails
#user.identities.linkedin
For example, let us say we have
class User < ActiveRecord::Base
has_many :networks, through: user_networks
has_many :user_networks
end
class Network< ActiveRecord::Base
has_many :users, through: user_networks
has_many :user_networks
end
class UserNetwork < ActiveRecord::Base
belongs_to :user
belongs_to :network
end
Is there a shortcut for doing the following in a controller:
#network = Network.create(params[:network])
UserNetwork.create(user_id: current_user.id, network_id: #network.id)
Just curious and I doubt it.
This should work:
current_user.networks.create(params[:network])
But your code implies you are not using strong_parameters, or checking the validation of your objects. Your controller should contain:
def create
#network = current_user.networks.build(network_params)
if #network.save
# good response
else
# bad response
end
end
private
def network_params
params.require(:network).permit(:list, :of, :safe, :attributes)
end