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.
Related
My app has many interrelationships like:
# Company
has_many :programs
has_many :projects
has_many :users
# Project
has_many :users
has_many :programs
belongs_to :company
# User
belongs_to :project
has_many :programs
belongs_to :company
# Program
belongs_to :project
belongs_to :user
belongs_to :company
Every program must belong to a project and user, BOTH OF WHICH belong to current_user.company.
Approach 1 - controller upon create/update
#program = Program.new(program_params)
#program.company = current_user.company
#allowed_projects = current_user.company.projects
unless #allowed_projects.include? #program.project
raise Exception
end
Approach 2 - model-based validation
before_save :ensure_all_allowed
def ensure_all_allowed
current_user = ???
self.company_id = current_user.company_id
# Then a similar validation to above for self.project_id
end
I feel these are both awkward and not 'the Rails way'.
I assume Approach 2 is the better method because it'll save all this awkward controller code and hold better to the MVC standard.
How can I validate these items correctly?
It's actually somewhat problematic to access current user in a model. It's not impossible, but it requires an around_action that will load the current user in the model class in a thread safe way.
Better would be to assign the current user in the controller
#program.user = current_user
#program.company = #program.user.company
Then do the validation in the model
validate :project_must_be_allowed
def project_must_be_allowed
unless company.projects include project
errors.add(:project, "Project is not valid for company.")
end
end
However, it would be a more normalized setup if you did through relationships
class Company
has_many :users
has_many :projects, through: :users
That way your 'projects' table doesn't need a company_id
You could still do the validation as I described but you'd have to add one method to the model...
def company
user.company
end
or more simply...
delegate :company, to: :user
Since Program has a belongs to relation to both user a and project you can setup some simple validations without worrying about the current_user. This is desirable from a MVC standpoint models should not be aware of the session or the request.
class Program < ActiveRecord::Base
# ...
validates_presence_of :user, :company, :project
# the unless conditions are there avoid the program blowing
# up with nil errors - but the presence validation above covers
# those scenarios
validate :user_must_belong_to_company,
unless: -> { company.nil? || user.nil? }
validate :project_must_belong_to_company,
unless: -> { company.nil? || project.nil? }
def user_must_belong_to_company
unless self.company == self.user.company
errors.add(:user, "must belong to same company as user.")
end
end
def project_must_belong_to_company
unless self.company == self.project.company
errors.add(:company, "must belong to same company as project.")
end
end
end
But I'm thinking that this is just a symtom of some bad relation design choices.
What you probably need is a series of many to many relations - it does not seem very realistic that a project can only have one user or a program either for that part.
class Company
has_many :users
has_many :projects
has_many :assignments, through :projects
has_many :programs, through :projects
end
class User
belongs_to :company
has_many :projects, through: :assignments
end
class Project
has_many :assignments, class_name: 'ProjectAssignment'
has_many :users, through: :assignments
belongs_to :company
end
# you can really call this whatever floats you boat
class ProjectAssignment
belongs_to :user
belongs_to :project
end
class Program
belongs_to :project
has_one :company, through: :project
has_many :assignments, class_name: 'ProgramAssignment'
has_many :users, through: :assignments
end
# you can really call this whatever floats you boat
class ProgramAssignment
belongs_to :user
belongs_to :program
end
That would automatically eliminate the problem with the company since it gets it through a parent relation.
The second problem that a user should not be able to create programs in a project he / she is not a member of sounds like something which should instead be handled on the authorization level - not in a validation.
Pundit example:
class ProgramPolicy < ApplicationPolicy
# ...
def create?
record.project.users.include?(user)
end
end
CanCanCan example:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
can :create, Program do |p|
p.project.users.include?(user)
end
end
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
So the way I did things for these model set ups is a bit different then what you might actually do. How ever I did things like this:
Post Model
class Post < ActiveRecord::Base
belongs_to :blog
belongs_to :user
has_and_belongs_to_many :tags, join_table: 'tags_posts', :dependent => :destroy
has_and_belongs_to_many :categories, join_table: 'categories_posts', :dependent => :destroy
has_many :comments, :dependent => :destroy
validates :title, presence: true
def has_tag?(tag_name)
tags.where(name: tag_name).any?
end
def tag_names
tags.pluck(:name)
end
def tag_names=(names)
self.tags = names.map{ |name| Tag.where(name: name).first }
end
def tag_name=(tag_name)
single_tag = [tag_name]
self.tag_names = single_tag
end
def has_category?(category_name)
categories.where(name: category_name).any?
end
def category_names
categories.pluck(:name)
end
def category_names=(names)
self.categories = names.map{ |name| Category.where(name: name).first }
end
def category_name=(category_name)
single_category_name = [category_name]
self.category_names = single_category_name
end
def user=(id)
user = User.find_by(id: id)
self.user_id = user.id if user
end
end
The above allows us to assign tags and categories and a post to a user (the last part is being refactored out as we speak). You can also get all tags and categories for a post and see if that post has a particular category.
Now what I want to do, in the tags model (for now) is get all the posts that a tag belongs to. But I am not sure how to do that ...
this is my tags model:
Tags Model
class Tag < ActiveRecord::Base
belongs_to :blog
validates :name, presence: true, uniqueness: true
end
How do I accomplish what I want?
I am not sure how to do this with has_and_belong_to_many. However, it would be pretty easy using has many through. By Rails conventions, the same of your join table should be tag_posts or post_tags (the first model is singular).
In your Post model:
has_many :tag_posts
has_many :tags, :through => :tag_posts
Then in your Tag model, a similar setup:
has_many :tag_posts
has_many :posts, :through => :tag_posts
Finally, you would create a TagPost model
belongs_to :tag
belongs_to :post
After that, calling tag.posts should return all posts for a given tag.
There has to be a better way to do this. My Favorite model belongs to User while Applicant belongs to both Gig and User. I am trying to efficiently determine whether a user has applied for Gig that was favorited (<% if #application.present? %>).
I tried chaining the collection by using something like #favorites.each.gig to no avail. While the below index action for Favorites seems to work, it's really verbose and inefficient. What is a more succinct way of doing this?
def index
#favorites = Favorite.where(:candidate_id => current_candidate)
#applications = Applicant.where(:candidate_id => current_candidate)
#favorites.each do |favorite|
#applications.each do |application|
if favorite.gig.id == application.id
#application = application
end
end
end
end
class User
has_many :applicants
has_many :gigs, :through => :applicants
has_many :favorites
end
class Favorite < ActiveRecord::Base
belongs_to :candidate
belongs_to :gig
end
class Applicant < ActiveRecord::Base
belongs_to :gig
belongs_to :candidate
end
class Candidate < ActiveRecord::Base
has_many :applicants
has_many :gigs, :through => :applicants
has_many :favorites
end
class Gig < ActiveRecord::Base
belongs_to :employer
has_many :applicants
has_many :favorites
has_many :users, :through => :applicants
end
For lack of a better answer, here's my idea:
--
User
Your user model should be structured as such (I just highlighted foreign keys, which I imagine you'd have anyway):
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :applicants
has_many :gigs, :through => :applicants, foreign_key: "candidate_id"
has_many :favorites, foreign_key: "candidate_id"
end
This means you'll be able to call:
current_candidate.favorites
current_candidate.applicants
This will remove the need for your #applications and #favorites queries
--
Favorite
You basically want to return a boolean of whether applicant is part of the favorite model or not. In essence, for each favorite the candidate has made, you'll be able to check if it's got an application
I would do this by setting an instance method on your favorites method using an ActiveRecord Association Extension, like so:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :favorites do
def applied?
self.applicant.exists? proxy_association.owner.gig.id
end
end
end
This will allow you to call:
<%= for favorite in current_candidate.favorites do %>
<%= if favorite.applied? %>
<% end %>
This is untested & highly speculative. I hope it gives you some ideas, though!
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.