Skip to content

Instantly share code, notes, and snippets.

@mattlong
Last active November 15, 2018 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattlong/0afa0ad08ddc3babf83b2e16b8c0d717 to your computer and use it in GitHub Desktop.
Save mattlong/0afa0ad08ddc3babf83b2e16b8c0d717 to your computer and use it in GitHub Desktop.
Rails / ActiveRecord has_one and has_many to same model
# From https://github.com/rails/rails/issues/20606#issuecomment-113323102
# Almost good, but I don't think it handles the case of promoting an
# existing location to being the primary location correctly in some cases
class Organization < ActiveRecord::Base
has_many :locations, dependent: :destroy, autosave: true # autosave necessary for the importer
has_one :primary_location, -> { where(locations: { primary: true }) }, class_name: "Location", autosave: true
# Override getter to fix issue with Rails not reloading the primary_location after resetting it to nil
def primary_location
@primary_location || locations.detect{ |l| l.primary? }
end
# Override the builder to handle removale of existing primary locations and building a new one with the proper attributes
def build_primary_location(attributes = {})
handle_existing_primary_location
@primary_location = self.locations.build(attributes.merge({primary: true}))
end
# Override the primary_location setter to work around Rails' conflicting behaviour between the has_one :primary_location and has_many :locations
# Rails is unable to re-assign a primary_location as it tries to clear the organization_id foreign_key on the location, which isn't possible because of NOT NULL
# +location_or_id+ supports passing a location object OR a location ID (workaround for inline-creation using select_or_create which was designed to work with belongs_to)
def primary_location=(location_or_id)
if location_or_id
new_primary_location = location_or_id.is_a?(Location) ? location_or_id : self.locations.find(location_or_id)
new_primary_location.try(:assign_attributes, primary: true, organization: self)
end
handle_existing_primary_location
# if this is a new location, make sure it's added to self.locations so that it gets saved alongside the organization
# use the target method here, as it allows us to add to the locations (and have it saved on organization save) w/o saving the location immediately
self.locations.target << new_primary_location if new_primary_location && new_primary_location.new_record?
@primary_location = new_primary_location
end
private
def handle_existing_primary_location
if self.primary_location
if self.primary_location.new_record?
self.locations.detect{ |l| l.primary? }.mark_for_destruction
end
self.locations.detect{ |l| l.primary? }.try(:assign_attributes, {primary: false})
end
end
end
describe Organization do
describe "#primary_location=" do
let(:new_location) { Location.new(name: "Test Location") }
context "new organization" do
let(:organization) { build(:organization) }
it "creates a new primary via attributes when there is no primary_location" do
o = organization
o.assign_attributes primary_location_attributes: { name: "New Location", address: "123 W Pender St" }
o.save
expect(o.primary_location.name).to eq "New Location"
end
it "creates a new primary via attributes when a primary_location exists" do
o = organization
o.primary_location = new_location
o.save
o.reload
o.assign_attributes primary_location_attributes: { name: "New Location", address: "123 W Pender St" }
o.save
expect(o.primary_location.name).to eq "New Location"
end
it "the built primary_location is accessible after being set but before save" do
o = organization
o.assign_attributes primary_location_attributes: { name: "New Location", address: "123 W Pender St" }
expect(o.primary_location.name).to eq "New Location"
end
it "the primary_location is accessible after being set but before save" do
o = organization
o.primary_location = new_location
expect(o.primary_location).to eq new_location
expect(o.primary_location).to_not be_persisted
end
it "allows the primary_location to be reset after being set but before the organization is persisted" do
o = organization
o.primary_location = new_location
expect(o.primary_location).to eq new_location
o.primary_location = nil
expect(o.primary_location).to be_nil
o.save
expect(o.reload.primary_location).to be_nil
end
it "persists the primary_location on save of the organization" do
o = organization
o.primary_location = new_location
o.save
o.reload
expect(o.primary_location.name).to eq new_location.name
expect(o.locations.first.primary?).to be_true
end
end
context "existing organization" do
let(:organization) { create(:organization) }
context "new location" do
it "the primary_location is accessible after being set but before save" do
o = organization
expect(o.primary_location).to be_nil
o.primary_location = new_location
expect(o.primary_location).to eq new_location
end
it "assigns the primary_location immediately" do
o = organization
expect(o.primary_location).to be_nil
o.primary_location = new_location
expect(o.primary_location.name).to eq new_location.name
end
it "doesn't save new locations on assignment" do
o = organization
o.primary_location = new_location
expect(o.primary_location.name).to eq new_location.name
expect(o.primary_location.id).to eq nil
end
it "doesn't save extra locations after subsequent primary_location sets with new locations" do
o = organization
o.primary_location = Location.new name: "test"
o.primary_location = new_location
o.save
expect(o.reload.primary_location).to eq new_location.reload
expect(o.locations.count).to eq 1
end
it "doesn't save extra locations after subsequent build_primary_location calls" do
o = organization
o.build_primary_location name: "test"
o.build_primary_location name: "test part two"
o.save
expect(o.reload.primary_location.name).to eq "test part two"
expect(o.locations.count).to eq 1
end
end
context "existing location" do
let(:location1) { create(:location, organization: organization) }
let(:location2) { create(:location, organization: organization) }
context "primary_location first assignment" do
it "assigns the primary_location immediately when given a location" do
o = organization
expect(o.primary_location).to be_nil
o.primary_location = location1
expect(o.reload.primary_location).to eq location1
end
it "assigns the primary_location immediately when given a location ID" do
o = organization
expect(o.primary_location).to be_nil
o.primary_location = location1.id
expect(o.reload.primary_location).to eq location1
end
end
context "primary_location reassignment" do
it "resets the primary location to nil after a new one is built and an old one exists" do
o = organization
o.primary_location = location1
o.save
o.build_primary_location name: "test built"
o.primary_location = nil
o.save
expect(o.reload.primary_location).to eq nil
end
it "reassigns the primary_location immediately when given a location" do
o = organization
o.primary_location = location1
expect(o.reload.primary_location).to eq location1
o.primary_location = location2
expect(o.reload.primary_location).to eq location2
end
it "reassigns the primary_location immediately when given a location ID" do
o = organization
o.primary_location = location1.id
expect(o.reload.primary_location).to eq location1
o.primary_location = location2.id
expect(o.reload.primary_location).to eq location2
end
end
context "primary_location reset" do
it "resets the primary_location immediately" do
o = organization
o.primary_location = location1
expect(o.reload.primary_location).to eq location1
o.primary_location = nil
expect(o.primary_location).to be_nil
expect(o.reload.primary_location).to be_nil
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment