Skip to content

Commit

Permalink
Merge pull request #624 from rubyforgood/migrate-multi-listing-submis…
Browse files Browse the repository at this point in the history
…sions

Migrate multi listing submissions
  • Loading branch information
solebared authored Sep 21, 2020
2 parents df39c6b + c0a8b34 commit 176c94e
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 0 deletions.
19 changes: 19 additions & 0 deletions lib/tasks/one_time.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require_relative './support/listing_converter'

namespace :one_time do
desc "split listings with multiple tags into one listing for each tag"
task split_listings: :environment do
converter = ListingConverter.new

relevant_listings = Listing.joins(:taggings).group('listings.id').having('count(taggable_id) > 1')
puts "Processing #{relevant_listings.count.keys.size} listings"

relevant_listings.find_each do |listing|
puts "#, #{listing.id}, #{listing.tag_list}"
new_listings = converter.convert listing

puts "-, #{listing.id}, #{listing.reload.tag_list}"
new_listings.each{ |new_listing| puts "+, #{new_listing.id}, #{new_listing.tag_list}" }
end
end
end
46 changes: 46 additions & 0 deletions lib/tasks/support/listing_converter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
class ListingConverter
def convert listing
Listing.transaction do
first_tag_set, *remaining_tag_sets = splitter.split listing.tag_list

listing.update! tag_list: [first_tag_set]

remaining_tag_sets.map do |tag_set|
listing.dup.tap do |new_listing|
new_listing.update!(
tag_list: tag_set,
created_at: listing.created_at,
)
end
end
end
end

def splitter
@splitter ||= TagSplitter.new
end

class TagSplitter
attr_reader :lineages

def initialize lineages = nil
@lineages = lineages || gather_lineages
end

def gather_lineages
Category.all.map{ |category| [category.name, category.lineage] }.to_h
end

def split tag_list
matching_lineages = lineages
.values_at(*tag_list)
.sort_by{ |lineage| lineage.size }
.reverse

matching_lineages.reduce([]) do |minimal_set, lineage|
with_lineage = minimal_set + [lineage]
with_lineage.flatten.uniq.size > minimal_set.flatten.uniq.size ? with_lineage : minimal_set
end
end
end
end
148 changes: 148 additions & 0 deletions spec/lib/tasks/support/listing_converter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
require 'rails_helper'
require 'tasks/support/listing_converter'

RSpec.describe ListingConverter do
let(:converter) { ListingConverter.new }
let(:convert) { converter.convert listing }

before { Category.destroy_all } # from seeds

context 'without sub-categories' do
let(:tags) { %w[cash errands housing] }
let!(:categories) { tags.map{ |name| create :category, name: name }}
let(:listing) { create :listing, tag_list: tags }

it 'removes all but one tag from the original listing' do
convert
expect(listing.reload.tag_list.size).to be 1
expect(tags).to include listing.tag_list.first
end

it 'creates a listing for each extraneous tag' do
convert
expect(Listing.count).to be 3
end

it 'sets the tag on each new listing' do
new_listings = convert
expect(new_listings.map(&:tag_list).flatten).to match_array(tags - listing.reload.tag_list)
end

it 'copies attributes from original listing except id, tag_list, and updated_at' do
original = listing.attributes.to_a
new_listing = convert.last
copied = new_listing.attributes.to_a
expect((copied - original).to_h.keys).to match_array %w[id tag_list updated_at]
end

it 'keeps the association with a submission' do
listing.update!(submission: create(:submission))
new_listing = convert.last
expect(new_listing.submission).to be_present
expect(new_listing.submission).to eq listing.submission
end

describe 'with matches' do
let!(:provider_match) { create(:match, :with_ask_and_offer, provider: listing) }
let!(:receiver_match) { create(:match, :with_ask_and_offer, receiver: listing) }

it 'keeps matches only on the original listing' do
new_listing = convert.last
expect(listing.matches_as_provider).to_not be_empty
expect(listing.matches_as_receiver).to_not be_empty
expect(new_listing.matches_as_provider).to be_empty
expect(new_listing.matches_as_receiver).to be_empty
end
end
end

context 'with only a parent- and sub-category' do
let!(:parent) { create :category, name: 'food' }
let!(:child) { create :category, name: 'meals', parent: parent }
let!(:listing) { create :listing, tag_list: %w[food meals] }

it 'does not create any new listings' do
expect{ convert }.to_not change(Listing, :count)
end

it 'leaves the listing tags unchanged' do
convert
expect(listing.tag_list).to match_array %w[food meals]
end
end

context 'with a mix of single tags and and nested categories' do
let!(:unrelated) { create :category, name: 'cash' }
let!(:parent) { create :category, name: 'food' }
let!(:child) { create :category, name: 'meals', parent: parent }
let!(:other_child) { create :category, name: 'groceries', parent: parent }
let!(:grand_child) { create :category, name: 'delivery', parent: child }

let!(:listing) { create :listing, tag_list: %w[food meals cash groceries delivery] }

it 'leaves the listing with one of the tag sets' do
convert
expect(listing.tag_list).to match_array %w[food meals delivery]
end

it 'creates listing for the remaining tag sets' do
expect(convert.map(&:tag_list)).to match_array [
%w[cash],
%w[food groceries],
]
end
end

describe 'TagSplitter' do
let(:splitter) { ListingConverter::TagSplitter.new(lineages) }
let(:lineages) {{
'grandchild' => %w[parent child grandchild],
'child' => %w[parent child],
'other_child' => %w[parent other_child],
'parent' => %w[parent],
'orphan' => %w[orphan],
}}

subject { splitter.split tag_list }

context 'single tag' do
let(:tag_list) { %w[orphan] }
it { is_expected.to contain_exactly %w[orphan] }
end

context 'unrelated tags' do
let(:tag_list) { %w[parent orphan] }
it { is_expected.to contain_exactly %w[parent], %w[orphan] }
end

context 'parent, child' do
let(:tag_list) { %w[parent child] }
it { is_expected.to contain_exactly %w[parent child] }
end

context 'when tagged out of order' do
let(:tag_list) { %w[child parent] }
it { is_expected.to contain_exactly %w[parent child] }
end

context 'parent, orphan, child' do
let(:tag_list) { %w[parent child orphan] }
it { is_expected.to contain_exactly %w[parent child], %w[orphan] }
end

context 'parent, child, other_child' do
let(:tag_list) { %w[parent child other_child] }
it { is_expected.to contain_exactly %w[parent child], %w[parent other_child] }
end

context 'parent, child, other_child, grandchild' do
let(:tag_list) { %w[parent child other_child grandchild] }
it { is_expected.to contain_exactly %w[parent child grandchild], %w[parent other_child] }
end

context 'parent, child, other_child, grandchild' do
let(:tag_list) { %w[parent child other_child grandchild] }
it { is_expected.to contain_exactly %w[parent child grandchild], %w[parent other_child] }
end
end
end

0 comments on commit 176c94e

Please sign in to comment.