Skip to content

Commit

Permalink
Merge branch 'main' into 69-create-copy-paste-info-method
Browse files Browse the repository at this point in the history
  • Loading branch information
dsomel21 authored Oct 28, 2023
2 parents 69f56c3 + 660f639 commit 673e4dc
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 3 deletions.
5 changes: 4 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ group :development, :test do
gem 'factory_bot_rails'

gem 'dotenv-rails'

# CLI Stuff
gem 'diffy'
gem 'tty-prompt'
end

group :development do
Expand All @@ -79,6 +83,5 @@ end

gem 'contentful'
gem 'rack-cors'
gem 'tty-prompt'

gem 'dockerfile-rails', '>= 1.4', :group => :development
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ GEM
irb (>= 1.5.0)
reline (>= 0.3.1)
diff-lcs (1.5.0)
diffy (3.4.2)
dockerfile-rails (1.4.1)
rails
domain_name (0.5.20190701)
Expand Down Expand Up @@ -316,6 +317,7 @@ DEPENDENCIES
capybara
contentful
debug
diffy
dockerfile-rails (>= 1.4)
dotenv-rails
factory_bot_rails
Expand Down
29 changes: 29 additions & 0 deletions app/models/book.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require 'csv'
class Book < ApplicationRecord
has_many :chapters, :dependent => :destroy

Expand All @@ -22,4 +23,32 @@ def last_tuk
def number_of_chapters_released
return self.chapters.released.count
end

##
# Create (or update) the a specific chapter via CSV
# HOW IT WORKS:
# 1. Add your CSV file to `lib/imports/#{book.sequence}/#{chapter_number}.csv`
# 2. Pass in a `chapter_number: Integer` e.g. `3`
# 3. Call `@book.import_chapter(3)`.
# EXAMPLE:
# @book = Book.find_by(:sequence => 1)
# @book.import_chapter(3)
# This will search for a CSV file at `lib/imports/1/3.csv` (or raise error)
# The CSV file should have the following columns:
# - Chapter_Number: Integer
# - Chapter_Name: String
# - Chhand_Type: String
# - Tuk: String
# - Pauri_Number: Integer
# - Tuk_Number: Integer
# - Pauri_Translation_EN: String | NULL
# - Tuk_Translation_EN: String | NULL
# - Footnotes: String | NULL
# - Extended_Ref: String | NULL
# - Assigned_Singh: String | NULL
# - Extended_Meaning: String | NULL
##
def import_chapter(chapter_number)
ChapterImporterService.new(self, chapter_number).call
end
end
15 changes: 15 additions & 0 deletions app/models/chapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,19 @@ def copy_paste_info

Rails.logger.debug info
end

def csv_rows
file_path = "lib/imports/#{self.book.sequence}/#{self.number}.csv"

unless File.exist?(file_path)
Rails.logger.debug "CSV file #{file_path} not found. " + Pastel.new.red.on_bright_white.bold("Are you sure you added it to #{file_path}?")
raise "CSV file #{file_path} not found. "
end

rows = []
CSV.foreach(file_path, :headers => true) do |row|
rows << row
end
return rows
end
end
132 changes: 132 additions & 0 deletions app/services/chapter_importer_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# frozen_string_literal: true

class ChapterImporterService
attr_reader :book, :chapter_number, :pastel, :prompt, :chapter

def initialize(book, chapter_number)
@book = book
@chapter_number = chapter_number
@pastel = Pastel.new
@prompt = TTY::Prompt.new
end

def call
ensure_chapter_exists!
ActiveRecord::Base.transaction do
each_csv_row do |row|
process_row(row)
end
end
end

private

def each_csv_row(&)
blob = chapter.csv_rows
blob.each(&)
end

def ensure_chapter_exists!
@chapter = book.chapters.find_by(:number => chapter_number)
raise "Chapter not found: #{chapter_number}" unless @chapter
end

def process_row(row)
validate_and_update_chapter_title(row)
process_pauri(row)
process_tuk(row)
create_tuk_translation(row)
create_pauri_translation(row)
end

def validate_and_update_chapter_title(row)
chapter_name = row['Chapter_Name'].try(:strip)
return if chapter.title == chapter_name

message = "The name in Book #{book.sequence}, Chapter #{chapter_number} is " +
pastel.bold('presently') +
" '#{chapter.title}'. The CSV says '#{chapter_name}'."
prompt.say(message, :color => :yellow)
answer = prompt.yes?("Do you want to continue and update this title to '#{chapter_name}'?")
raise 'Aborted by user' unless answer

chapter.update(:title => chapter_name)
Rails.logger.debug pastel.green("✓ Chapter #{chapter_number}'s title updated to '#{chapter_name}'")
end

def process_pauri(row)
pauri_number = row['Pauri_Number'].to_i
@pauri = chapter.pauris.find_by(:number => pauri_number)

raise "Pauri not found: #{pauri_number}" if @pauri.nil?
end

def process_tuk(row)
tuk = row['Tuk'].try(:strip)
tuk_number = row['Tuk_Number'].to_i
@tuk = @pauri.tuks.find_by(:sequence => tuk_number)

raise "Tuk #{tuk_number} not found: #{tuk}" if @tuk.nil?

return if @tuk.original_content == tuk

diff = Diffy::Diff.new(@tuk.original_content, tuk, :include_diff_info => true).to_s(:color)
Rails.logger.debug diff

choices = [
"Keep the NEW one from the CSV (update the original) :: #{tuk}",
pastel.red("Keep the original :: #{@tuk.original_content}")
]

selected_choice = prompt.select('Choose an option:', choices)
case selected_choice
when choices[0]
@tuk.update(:original_content => tuk)
Rails.logger.debug pastel.green("✓ `Tuk` `original_content` updated to #{tuk} - For #{@pauri.number}.#{tuk_number}")
when choices[1]
Rails.logger.debug pastel.red("x Keeping the original `Tuk` `original_content` - For #{@pauri.number}.#{tuk_number}")
end
end

def create_tuk_translation(row)
tuk_translation_en = row['Tuk_Translation_EN'].try(:strip) || row['Translation_EN'].try(:strip)
return if tuk_translation_en.blank?

# If there's a Pauri translation and a Tuk translation, ask what to do
if @pauri.translation.present?
choices = [
'Continue with the `TukTranslation` and KEEP the `PauriTranslation`, too!',
'Destroy the existing `PauriTranslation`, and continue with only the `TukTranslation`',
pastel.red('Abort')
]
selected_choice = prompt.select('Choose an option:', choices)

case selected_choice
when choices[0]
upsert_tuk_translation(tuk_translation_en, row['Assigned_Singh'].try(:strip))
when choices[1]
upsert_tuk_translation(tuk_translation_en, row['Assigned_Singh'].try(:strip))
@pauri.translation.destroy
when choices[2]
raise 'Aborted by user'
end
else
upsert_tuk_translation(tuk_translation_en, row['Assigned_Singh'].try(:strip))
end
end

def upsert_tuk_translation(translation, translator)
@tuk_translation = @tuk.translation || TukTranslation.new(:tuk_id => @tuk.id)
@tuk_translation.update(:en_translation => translation, :en_translator => translator)
Rails.logger.debug pastel.green("✓ `TukTranslation` created or updated for Tuk #{translation} - Pauri # #{@pauri.number}")
end

def create_pauri_translation(row)
pauri_translation_en = row['Pauri_Translation_EN'].try(:strip)
return if pauri_translation_en.blank?

pauri_translation = @pauri.translation || PauriTranslation.new(:pauri_id => @pauri.id)
pauri_translation.update(:en_translation => pauri_translation_en, :en_translator => row['Assigned_Singh'].try(:strip))
Rails.logger.debug pastel.green("✓ `PauriTranslation` created or updated for Pauri # #{@pauri.number}")
end
end
2 changes: 1 addition & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Application < Rails::Application

config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'https://spg.dev', /\A.*\.netlify\.app\z/
origins 'https://spg.dev', /\A.*\.netlify\.app\z/, 'localhost:3000'

resource '*',
headers: :any,
Expand Down
123 changes: 123 additions & 0 deletions spec/models/book_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# rubocop:disable RSpec/NestedGroups, RSpec/MultipleExpectations
# frozen_string_literal: true

require 'rails_helper'
require 'csv'

RSpec.describe Book do
let(:book) { create(:book) }

describe '#import_chapter' do
context 'when the `chapter.number` is invalid' do
it 'raises an error if the `chapter_number` does not exist in `book`' do
expect { book.import_chapter(99) }.to raise_error(RuntimeError, /Chapter not found: 99/)
end

it 'raises an error if the CSV does not exist' do
# Create the `Chapter` row only! Not the CSV.
create(:chapter, :book => book, :number => 100)
expect { book.import_chapter(100) }.to raise_error(RuntimeError, %r{CSV file lib/imports/1/100\.csv not found})
end
end

context 'when the `chapter_number` is valid' do
csv_content = <<~CSV
Chapter_Number,Chapter_Name,Chhand_Type ,Tuk,Pauri_Number,Tuk_Number,Pauri_Translation_EN,Translation_EN ,Footnotes,Custom_Footnotes,Extended_Ref ,Assigned_Singh,Status,Extended_Meaning
CSV

csv_rows = CSV.parse(csv_content, :headers => true)
let(:chapter) { create(:chapter, :book => book) }

it 'does not raise an error' do
file_path = "lib/imports/#{book.sequence}/#{chapter.number}.csv"

allow(File).to receive(:exist?).with(file_path).and_return(true)
allow(CSV).to receive(:foreach).with(file_path, :headers => true).and_return(csv_rows)

expect { book.import_chapter(chapter.number) }.not_to raise_error
end
end

context 'when given valid input and CSV' do
let(:chapter_number) { 99 }
let(:chapter_title) { 'ਇਸ਼੍ਟ ਦੇਵ-ਸ਼੍ਰੀ ਅਕਾਲ ਪੁਰਖ-ਮੰਗਲ' }
let(:csv_content) do
<<~CSV
Chapter_Number,Chapter_Name,Chhand_Type ,Tuk,Pauri_Number,Tuk_Number,Pauri_Translation_EN,Translation_EN ,Footnotes,Custom_Footnotes,Extended_Ref ,Assigned_Singh,Status,Extended_Meaning
99,ਇਸ਼੍ਟ ਦੇਵ-ਸ਼੍ਰੀ ਅਕਾਲ ਪੁਰਖ-ਮੰਗਲ,ਦੋਹਰਾ,"ਤੀਨੋ ਕਾਲ ਅਲਿਪਤ ਰਹਿ, ਖੋਜੈਂ ਜਾਂਹਿ ਪ੍ਰਬੀਨ",1,1,,,,,,,,
99,ਇਸ਼੍ਟ ਦੇਵ-ਸ਼੍ਰੀ ਅਕਾਲ ਪੁਰਖ-ਮੰਗਲ,ਦੋਹਰਾ,"ਬੀਨਤਿ ਸਚਿਦਾਨੰਦ ਤ੍ਰੈ, ਜਾਨਹਿਂ ਮਰਮ ਰਤੀ ਨ",1,2,,,,,,,,
CSV
end

let(:chapter) { create(:chapter, :number => chapter_number, :book => book, :title => chapter_title) }
let(:chhand_type) { create(:chhand_type, :name => 'ਦੋਹਰਾ') }
let(:chhand) { create(:chhand, :chhand_type => chhand_type, :chapter => chapter) }
let(:pauri) { create(:pauri, :chapter => chapter, :chhand => chhand, :number => 1) }
let(:prompt) { instance_double(TTY::Prompt) }

before do
# Initialize mocks for TTY::Prompt
allow(TTY::Prompt).to receive(:new).and_return(prompt)
allow(prompt).to receive(:say)

# Associations for the chapter - This one reflects out mock `csv_content`
create(:tuk, :pauri => pauri, :chapter => chapter, :original_content => 'ਤੀਨੋ ਕਾਲ ਅਲਿਪਤ ਰਹਿ, ਖੋਜੈਂ ਜਾਂਹਿ ਪ੍ਰਬੀਨ', :sequence => 1)
create(:tuk, :pauri => pauri, :chapter => chapter, :original_content => 'ਬੀਨਤਿ ਸਚਿਦਾਨੰਦ ਤ੍ਰੈ, ਜਾਨਹਿਂ ਮਰਮ ਰਤੀ ਨ', :sequence => 2)

# Mocking the CSV data
allow(book.chapters).to receive(:find_by).with(:number => chapter_number).and_return(chapter)
allow(chapter).to receive(:csv_rows).and_return(CSV.parse(csv_content, :headers => true))
end

context 'when prompted to update `chapter.title`' do
it 'does not prompt user if the `chapter.title` is unchanged' do
allow(prompt).to receive(:yes?)
book.import_chapter(chapter_number)
expect(prompt).not_to have_received(:yes?)

expect(chapter.reload.title).to eq(chapter_title)
end

it 'updates the `chapter.title` if user confirms' do
# Change the `chapter.title` so it is different than the one in CSV
chapter.update(:title => 'Different title')

allow(prompt).to receive(:yes?).and_return(true)
book.import_chapter(chapter_number)

expect(prompt).to have_received(:yes?).with("Do you want to continue and update this title to '#{chapter_title}'?")
expect(chapter.reload.title).to eq(chapter_title)
end

it 'aborts and does not update the chapter title if user declines' do
# Change the `chapter.title` so it is different than the one in CSV
chapter.update(:title => 'Something Else - Suraj Suraj Suraj')
allow(prompt).to receive(:yes?).and_return(false)
expect { book.import_chapter(chapter_number) }.to raise_error(RuntimeError, 'Aborted by user')
end
end

context 'when `chapter` associations do not exist' do
it 'raises an error when `pauri` is nil' do
pauri.destroy
expect { book.import_chapter(chapter_number) }.to raise_error(StandardError, /Pauri not found/)
end

it 'raises an error when `tuks` are nil' do
Tuk.destroy_all
expect { book.import_chapter(chapter_number) }.to raise_error(StandardError, /Tuk 1 not found/)
end

it 'raises an error when 2nd `tuk` is missing' do
chapter.tuks.second.destroy
expect { book.import_chapter(chapter_number) }.to raise_error(StandardError, /Tuk 2 not found/)
end
end

# TODO: Write tests for Translations, Footnotes, etc.
# TODO: and write tests for the TTY stuff
end
end
end

# rubocop:enable RSpec/NestedGroups, RSpec/MultipleExpectations
2 changes: 1 addition & 1 deletion spec/models/chapter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

it 'cannot save when `book_id` does not belong to a real book' do
expect do
Chapter.create!({ :book_id => 1, :number => 2, :title => 'Mangal' })
Chapter.create!({ :book_id => 99, :number => 2, :title => 'Mangal' })
end.to raise_error(ActiveRecord::RecordInvalid)
end
end
Expand Down

0 comments on commit 673e4dc

Please sign in to comment.