Last active
November 15, 2018 16:38
-
-
Save mattlong/0afa0ad08ddc3babf83b2e16b8c0d717 to your computer and use it in GitHub Desktop.
Rails / ActiveRecord has_one and has_many to same model
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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