has_one through and polymorphic associations over multi-table inheritance - ruby-on-rails

In the project i'm currently developing under rails 4.0.0beta1, i had the need for a user based authentication in which each user could be linked to an entity. I'm kinda new to rails and had some troubles doing so.
The model is as following:
class User < ActiveRecord::Base
end
class Agency < ActiveRecord::Base
end
class Client < ActiveRecord::Base
belongs_to :agency
end
What i need is for a user to be able to link to either an agency or a client but not both (those two are what i'll be calling entities). It can have no link at all and at most one link.
First thing i looked for was how to do Mutli-Table inheritance (MTI) in rails. But some things blocked me:
it was not available out of the box
MTI looked kinda hard to implement for a newbie such as me
the gems implementing the solutions seemed old and either too complexe or not complete
the gems would have probably broke under rails4 as they had not been updated for a while
So i looked for another solution and i found polymorphic associations.
I've be on this since yesterday and took some time to make it work even with the help of Rails polymorphic has_many :through and ActiveRecord, has_many :through, and Polymorphic Associations
I managed to make the examples from the question above work but it took a while and i finally have two problems:
How to transform the relations in user into a has_one association and be able to access "blindly" the linked entity ?
How to set a constraint so that no user can have more than one entity ?
Is there a better way to do what i want ?

Here's a fully working example:
The migration file:
class CreateUserEntities < ActiveRecord::Migration
def change
create_table :user_entities do |t|
t.integer :user_id
t.references :entity, polymorphic: true
t.timestamps
end
add_index :user_entities, [:user_id, :entity_id, :entity_type]
end
end
The models:
class User < ActiveRecord::Base
has_one :user_entity
has_one :client, through: :user_entity, source: :entity, source_type: 'Client'
has_one :agency, through: :user_entity, source: :entity, source_type: 'Agency'
def entity
self.user_entity.try(:entity)
end
def entity=(newEntity)
self.build_user_entity(entity: newEntity)
end
end
class UserEntity < ActiveRecord::Base
belongs_to :user
belongs_to :entity, polymorphic: true
validates_uniqueness_of :user
end
class Client < ActiveRecord::Base
has_many :user_entities, as: :entity
has_many :users, through: :user_entities
end
class Agency < ActiveRecord::Base
has_many :user_entities, as: :entity
has_many :users, through: :user_entities
end
As you can see i added a getter and a setter that i named "entity". That's because has_one :entity, through: :user_entity raises the following error:
ActiveRecord::HasManyThroughAssociationPolymorphicSourceError: Cannot have a has_many :through association 'User#entity' on the polymorphic object 'Entity#entity' without 'source_type'. Try adding 'source_type: "Entity"' to 'has_many :through' definition.
Finally, here are the tests i set up. I give them so that everyone understands know ho you can set and access data between those objects. i won't be detailing my FactoryGirl models but they're pretty obvious
require 'test_helper'
class UserEntityTest < ActiveSupport::TestCase
test "access entity from user" do
usr = FactoryGirl.create(:user_with_client)
assert_instance_of client, usr.user_entity.entity
assert_instance_of client, usr.entity
assert_instance_of client, usr.client
end
test "only right entity is set" do
usr = FactoryGirl.create(:user_with_client)
assert_instance_of client, usr.client
assert_nil usr.agency
end
test "add entity to user using the blind rails method" do
usr = FactoryGirl.create(:user)
client = FactoryGirl.create(:client)
usr.build_user_entity(entity: client)
usr.save!
result = UserEntity.where(user_id: usr.id)
assert_equal 1, result.size
assert_equal client.id, result.first.entity_id
end
test "add entity to user using setter" do
usr = FactoryGirl.create(:user)
client = FactoryGirl.create(:client)
usr.client = client
usr.save!
result = UserEntity.where(user_id: usr.id)
assert_equal 1, result.size
assert_equal client.id, result.first.entity_id
end
test "add entity to user using blind setter" do
usr = FactoryGirl.create(:user)
client = FactoryGirl.create(:client)
usr.entity = client
usr.save!
result = UserEntity.where(user_id: usr.id)
assert_equal 1, result.size
assert_equal client.id, result.first.entity_id
end
test "add user to entity" do
usr = FactoryGirl.create(:user)
client = FactoryGirl.create(:client)
client.users << usr
result = UserEntity.where(entity_id: client.id, entity_type: 'client')
assert_equal 1, result.size
assert_equal usr.id, result.first.user_id
end
test "only one entity by user" do
usr = FactoryGirl.create(:user)
client = FactoryGirl.create(:client)
agency = FactoryGirl.create(:agency)
usr.agency = agency
usr.client = client
usr.save!
result = UserEntity.where(user_id: usr.id)
assert_equal 1, result.size
assert_equal client.id, result.first.entity_id
end
test "user uniqueness" do
usr = FactoryGirl.create(:user)
client = FactoryGirl.create(:client)
agency = FactoryGirl.create(:agency)
UserEntity.create!(user: usr, entity: client)
assert_raise(ActiveRecord::RecordInvalid) {
UserEntity.create!(user: usr, entity: agency)
}
end
end
I Hope this can be of some help to someone. I decided to put the whole solution here cause it seems to me like a good one compared to MTI and i think it shouldn't take someone that much time to set something like that up.

The above answer was giving me some trouble. Use a column name instead of a model name when validating uniqueness. Change validates_uniqueness_of :user to validates_uniqueness_of :user_id.

Related

Rails FactoryGirl for model that belongs_to 2 other models

I have 3 following models like this:
# model/timeline.rb
class Timeline
belongs_to :series
belongs_to :creator
end
def series_belongs_to_creator
if creator_id
creator = Creator.find_by id: creator_id
related_series = creator.series.find_by id: series_id
errors.add(:series_id, :not_found_series) unless related_series
end
end
# model/creator.rb
class Creator
has_many :timelines
has_many :series, through: :contents
end
# model/series.rb
class Series
has_many :timelines
has_many :creators, through: :contents
end
This is not many to many relation, timelines table has two fields creator_id and series_id beside another fields. creator_id and series_id must be entered when create Timeline and i have a method series_belongs_to_creator to validates series_id must belong to creator_id to create successful.
So how should I write factory for timeline model if using FactoryGirl. Im so confused about Unit test in Rails.
If you're using Rails 5, you have to keep in mind that belongs_to is no longer optional by default: https://blog.bigbinary.com/2016/02/15/rails-5-makes-belong-to-association-required-by-default.html
So creator_id will always need to be present unless you specify the relation is optional.
For the factories, you're going to end up with something like this (FactoryGirl was recently renamed to FactoryBot):
http://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md#Associations
FactoryBot.define do
factory :timeline do
creator
series
end
end
FactoryBot.define do
factory :creator do
...
end
end
FactoryBot.define do
factory :series do
...
end
end

Creating objects with associations in Rails

In my Rails app I have Clients and Users. And Users can have many Clients.
The models are setup as so:
class Client < ApplicationRecord
has_many :client_users, dependent: :destroy
has_many :users, through: :client_users
end
class User < ApplicationRecord
has_many :client_users, dependent: :destroy
has_many :clients, through: :client_users
end
class ClientUser < ApplicationRecord
belongs_to :user
belongs_to :client
end
So if I wanted to create a new client that had the first two users associated with it how would I do it?
e.g.
Client.create!(name: 'Client1', client_users: [User.first, User.second])
Trying that gives me the error:
ActiveRecord::AssociationTypeMismatch: ClientUser(#70142396623360) expected, got #<User id: 1,...
I also want to do this for an RSpec test. e.g.
user1 = create(:user)
user2 = create(:user)
client1 = create(:client, client_users: [user1, user2])
How do I create a client with associated users for in both the Rails console and in an RSpec test?
If you do not want to accept_nested_attributes for anything, as documented here you can also pass block to create.
Client.create!(name: 'Client1') do |client1|
client1.users << [User.find(1), User.find(2), User.find(3)]
end
Try this. It should work
Client.create!(name: 'Client1').client_users.new([{user_id:
User.first},{user_id: User.second}])
You can do this with the following code:
user1 = create(:user)
user2 = create(:user)
client1 = create(:client, users: [user1, user2])
See ClassMethods/has_many for the documentation
collection=objects
Replaces the collections content by deleting and adding objects as
appropriate. If the :through option is true callbacks in the join
models are triggered except destroy callbacks, since deletion is
direct.
If you are using factory_girl you can add trait :with_users like this:
FactoryGirl.define do
factory :client do
trait :with_two_users do
after(:create) do |client|
client.users = create_list :user, 2
end
end
end
end
Now you can create a client with users in test like this:
client = create :client, :with_two_users
accepts_nested_attributes_for :users
and do as so:
Client.create!(name: 'Client1', users_attributes: { ........ })
hope this would work for you.
You can make use of after_create callback
class Client < ApplicationRecord
has_many :client_users, dependent: :destroy
has_many :users, through: :client_users
after_create :add_users
private def add_users
sef.users << [User.first, User.second]
end
end
Alternatively, A simpler approach would be
Client.create!(name: 'Client1', user_ids: [User.first.id, User.second.id])
The reason you're getting a mismatch is because you're specifying the client_users association that expects ClientUser instances, but you're passing in User instances:
# this won't work
Client.create!(client_users: [User.first, User.second])
Instead, since you already specified a users association, you can do this:
Client.create!(users: [User.first, User.second])
There's a simpler way to handle this, though: ditch the join model and use a has_and_belongs_to_many relationship. You still need a clients_users join table in the database, but you don't need a ClientUser model. Rails will handle this automatically under the covers.
class Client < ApplicationRecord
has_and_belongs_to_many :users
end
class User
has_and_belongs_to_many :clients
end
# Any of these work:
client = Client.new(name: "Kung Fu")
user = client.users.new(name: "Panda")
client.users << User.new(name: "Nemo")
client.save # => this will create two users and a client, and add two records to the `clients_users` join table

Rails: implement sharing blogs; has_many with STI: undefined method 'klass' for nil:NilClass

Just started using STI with a Rails 4 project. Suppose I have User and Blog, and User can share his non-public blogs to some other users as editors or normal viewers.
It doesn't make sense for me to put type column in users table, because in the project, the user is associated with not just blogs, but also things like posts. (The blogs here are more like a platform, and posts are articles. Just an idea here, could be other two things).
So I used another model called BlogUserAssociation to manage the above sharing relationship. Basically this model contains a type column, and I have BlogEditorAssociation and BlogViewerAssociation inherited from it. (Name is a bit clunky.) First question, is this a recommended way to handle the "sharing" situation?
With the above thought, I have:
# blog.rb
class Blog < ActiveRecord::Base
...
has_many :blog_user_associations, dependent: :destroy
has_many :editors, through: :blog_editor_associations, source: :user
has_many :allowed_viewers, through: :blog_viewer_associations, source: :user # STI
...
And
# user.rb
class User < ActiveRecord::Base
...
has_many :blog_user_associations, dependent: :destroy
has_many :editable_blogs, through: :blog_editor_associations, source: :blog
has_many :blogs_shared_for_view, through: :blog_viewer_associations, source: :blog
...
But when I tried to test this with Rspec,
it { should have_many(:editors).through(:blog_editor_associations).source(:user) }
I got the error undefined method 'klass' for nil:NilClass
I believe this is because I didn't say has_many blog_editor_associations in User. But I thought since blog_editor_associations inherits from blog_viewer_associations, I don't have to say has_many again for the sub-model. So is there a reason for not automatically bind has_many to sub-models?
STI seems like overkill for this situation. I prefer to add an attribute to the association model and use scopes to retrieve collections, depending on the value of the attribute. For example, you could name the association model BlogUser, and add a boolean can_edit column. A value of true indicates the user can edit the associated blog.
Then the models look like this:
class Blog < ActiveRecord::Base
has_many :blog_users
has_many :users, through: :blog_users
scope :editable, -> { where(blog_users: {can_edit: true}) }
end
class BlogUser < ActiveRecord::Base
belongs_to :blog
belongs_to :user
end
class User < ActiveRecord::Base
has_many :blog_users
has_many :blogs, through: :blog_users
scope :editors, -> { where(blog_users: {can_edit: true}) }
end
So user.blogs retrieves all blogs associated with the user, and user.blogs.editable retrieves all blogs that the user can edit. blog.users retrieves all users associated with the blog, and blog.users.editors retrieves all users who can edit the blog.
Some tests to demonstrate:
require 'rails_helper'
RSpec.describe User, type: :model do
describe "A user with no associated blogs" do
let(:user) { User.create! }
it "has no blogs" do
expect(user.blogs.empty?).to be true
expect(user.blogs.editable.empty?).to be true
end
end
describe "A user with a non-editable blog association" do
let(:user) { User.create! }
let(:blog) { Blog.create! }
before do
user.blogs << blog
end
it "has one blog" do
expect(user.blogs.count).to eq 1
end
it "has no editable blogs" do
expect(user.blogs.editable.empty?).to be true
end
end
describe "A user with an editable blog association" do
let(:user) { User.create! }
let(:blog) { Blog.create! }
before do
user.blog_users << BlogUser.new(blog: blog, user: user, can_edit: true)
end
it "has one blog" do
expect(user.blogs.count).to eq 1
end
it "has one editable blog" do
expect(user.blogs.editable.count).to eq 1
end
end
end

validation error through FactoryGirl

I have problems on validating my data through FactoryGirl.
For team.rb, the custom validations are has_only_one_leader and belongs_to_same_department
class Team < ActiveRecord::Base
attr_accessible :name, :department, :active
has_many :memberships
has_many :users, through: :memberships
accepts_nested_attributes_for :memberships, :users
validates :department, inclusion: [nil, "Architectural", "Interior Design"]
validates_uniqueness_of :name, scope: :department, conditions: -> { where(active: true) }
validate :has_only_one_leader
validate :belongs_to_same_department
def has_only_one_leader
unless self.users.where!(designation: "Team Leader").size == 1
errors.add(:team, "needs to have exactly one leader")
end
end
def belongs_to_same_department
unless self.users.where!.not(department: self.department).size == 0
errors.add(:users, "should belong to the same department")
end
end
I'll also include membership.rb and user.rb (associations only) just for reference.
class Membership < ActiveRecord::Base
belongs_to :team
belongs_to :user
end
class User < ActiveRecord::Base
has_many :memberships
has_many :teams, through: :memberships
end
Here's my factory for team.rb
FactoryGirl.define do
factory :team do
sequence(:name) {|n| "Team #{n}" }
department "Architectural"
before(:create) do |team|
team.users << FactoryGirl.create(:user, designation: "Team Leader",
department: "Architectural")
team.users << FactoryGirl.create_list(:user, 5,
designation: "CAD Operator", department: "Architectural")
end
end
end
It seems that after I do FactoryGirl.create(:team) in the rails console, it seems that I got the error messages from my validations.
Two things I've noticed when I manually build a team, specifically adding members to the team with 1 leader and 5 members belonging to the same department:
team.users.where!(designation: "Team Leader").size returns 6 although there's only one leader, same goes for changing the designation to CAD Operator, returns 6 although there are only five non leaders in the team.
same goes for checking if the members are in the same department, team.users.where!.not(department: team.department).size returns 6, although all of them belong to the same department.
Are there any modifications to make in my model or in my factory?
Here are the things that I've discovered before getting the answer.
team.users.where(designation: "Team Leader") returns a User::ActiveRelation_AssociationRelation object
team.users returns a User::ActiveRecord_Associations_CollectionProxy object
CollectionProxy has no method where since the this method (in my opinion) is a query to the database (with exception if the team is already saved in the database, you can use where, but in this case, it's only for building)
Therefore, I used the select method accompanied with the count, to return the correct values like so. I'll use my example from the question to illustrate the answer.
team.users.select(:designation) {|user| user.designation == "Team Leader"}.count returns 1
team.users.select(:department) {|user| user.department != team.department}.count returns 0

factory_girl_rails: factory built model instance's has_many association not populated when an associated child is created

I'm using factory_girl_rails instead of fixtures. Here are my models:
class User < ActiveRecord::Base
has_many :tasks
belongs_to :project
end
class Task < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
class Project < ActiveRecord::Base
has_many :users
has_many :tasks
end
Here's the relevant factory:
Factory.define :task do |t|
t.association :user
t.association :project
t.after_create {|t| t.user.tasks << t}
t.after_create {|t| t.project.tasks << t}
end
In an integration test I do this:
scenario "user with tasks from one project is assigned another task from the same project" do
user = Factory.create :user
(1..5).each { Factory.create(:task, :user => user, :project => user.project)}
visit_project_path user.project
correctly_fill_in_new_task_fields
click_button "Create task" #creates a new task for the above user
assert user.tasks.size == 6 #currently fails
end
The problem that I have is that after the scenario runs user.tasks.size == 5, but Task.where(:user_id => user.id).size == 6. I'd appreciate any help.
Actually, this is more likely due to the way ActiveRecord works. Your controller fetches the user from the database and creates a new instance of User. Now your controller and your test have references to two different users, which is why your test never sees the changes.
You'll need to call reload on user before checking how many tasks there are again.
Side-note: some ORMs provide an identity map (special type of registry) to get around this problem (actually... a quick Google seems to indicate Rails 3 recently got an Identity Map added to the source. I don't use AR, so not sure how you enable it).

Resources