Skip to content

Commit

Permalink
Merge pull request #387 from openstax/books_breakdown
Browse files Browse the repository at this point in the history
Books breakdown
  • Loading branch information
Dantemss authored Feb 3, 2023
2 parents 8801cde + ebda89c commit 753752c
Show file tree
Hide file tree
Showing 4 changed files with 2,309 additions and 1 deletion.
1 change: 0 additions & 1 deletion app/controllers/api/v1/books_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ def show
render json: tree
end


protected

def abl
Expand Down
98 changes: 98 additions & 0 deletions lib/tasks/books/breakdown.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
namespace :books do
# Creates a CSV file listing the number of Exercises of each type per section of the given book
# Arguments are, in order:
# book_uuid, [filename]
# Example: rake books:breakdown[14fb4ad7-39a1-4eee-ab6e-3ef2482e3e22]
# will create 14fb4ad7-39a1-4eee-ab6e-3ef2482e3e22.csv containing book Exercise info for A&P
desc 'creates a CSV file listing the number of Exercises of each type per section of the given book'
task :breakdown, [:book_uuid, :filename] => :environment do |t, args|
# Output logging info to the console (except in the test environment)
original_logger = Rails.logger

begin
Rails.logger = ActiveSupport::Logger.new(STDOUT) unless Rails.env.test?

book_uuid = args[:book_uuid]
filename = args[:filename] || "#{book_uuid}.csv"

book = OpenStax::Content::Abl.new.approved_books.find { |book| book.uuid == book_uuid }
root_book_part = loop do
begin
break book.root_book_part
rescue StandardError => exception
# Sometimes books in the ABL fail to load
# Retry with an earlier version of archive, if possible
previous_version = book.archive.previous_version

# break from the loop if there are no more archive versions to try
raise exception if previous_version.nil?

book = OpenStax::Content::Book.new(
archive: OpenStax::Content::Archive.new(version: previous_version),
uuid: book.uuid,
version: book.version,
slug: book.slug,
style: book.style,
min_code_version: book.min_code_version,
committed_at: book.committed_at
)

raise exception unless book.valid?
end
end

def recursive_exercise_counts(book_part, type)
results = book_part.parts.flat_map do |part|
next recursive_exercise_counts(part, 'Unit/Chapter') unless part.is_a?(OpenStax::Content::Page)

exercises = Exercise.chainable_latest.published.joins(:tags).where(
tags: { name: "context-cnxmod:#{part.uuid}" }
).joins(questions: {stems: :stylings}).distinct

mc_tf_exercises = exercises.dup.where(
questions: { stems: { stylings: { style: [ Style::MULTIPLE_CHOICE, Style::TRUE_FALSE ] } } }
).pluck(:id)
fr_exercises = exercises.dup.where(
questions: { stems: { stylings: { style: Style::FREE_RESPONSE } } }
).pluck(:id)

[
[
part.uuid,
'Page',
part.book_location.join('.'),
ActionView::Base.full_sanitizer.sanitize(part.title).gsub(/\s+/, ' ').strip,
exercises.count,
(mc_tf_exercises & fr_exercises).size,
(mc_tf_exercises - fr_exercises).size,
(fr_exercises - mc_tf_exercises).size
]
]
end

direct_child_uuids = book_part.parts.map(&:uuid)
direct_child_results = results.filter { |result| direct_child_uuids.include? result.first }

[
[
book_part.uuid,
type,
book_part.book_location.join('.'),
ActionView::Base.full_sanitizer.sanitize(book_part.title).gsub(/\s+/, ' ').strip
] + direct_child_results.map { |result| result[4..-1] }.transpose.map(&:sum)
] + results
end

CSV.open(filename, 'w') do |csv|
csv << [
'UUID', 'Type', 'Number', 'Title', 'Total Exercises', '2-step MC/TF', 'MC/TF only', 'FR only'
]

recursive_exercise_counts(root_book_part, 'Book').each { |row| csv << row }
end
ensure
# Restore original logger
Rails.logger = original_logger
end
end
end
2,109 changes: 2,109 additions & 0 deletions spec/cassettes/books_breakdown/returns_correct_exercise_counts.yml

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions spec/lib/tasks/books/breakdown.rake_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
require 'vcr_helper'
require 'rake'

RSpec.describe 'books breakdown', type: :rake, vcr: VCR_OPTS do
before :all do
Rake.application.rake_require 'tasks/books/breakdown'
Rake::Task.define_task :environment
end

before { Rake::Task['books:breakdown'].reenable }

let(:book_uuid) { '14fb4ad7-39a1-4eee-ab6e-3ef2482e3e22' }
let(:filename) { "#{Rails.root}/tmp/test-#{book_uuid}.csv" }

it 'returns correct exercise counts' do
# Disable set_slug_tags!
allow_any_instance_of(Exercise).to receive(:set_slug_tags!)

ex1 = FactoryBot.create :exercise, tags: [ 'context-cnxmod:ada35081-9ec4-4eb8-98b2-3ce350d5427f' ]
ex1.questions.first.stems.first.stylings.first.update_attribute :style, Style::MULTIPLE_CHOICE
FactoryBot.create :styling, stylable: ex1.questions.first.stems.first, style: Style::FREE_RESPONSE
ex1.publication.publish.save!

ex2 = FactoryBot.create :exercise, tags: [ 'context-cnxmod:5e1ff6e7-0980-4ae0-bc8a-4b591a7c1760' ]
ex2.questions.first.stems.first.stylings.first.update_attribute :style, Style::TRUE_FALSE
FactoryBot.create :styling, stylable: ex2.questions.first.stems.first, style: Style::FREE_RESPONSE
ex2.publication.publish.save!

ex3 = FactoryBot.create :exercise, tags: [ 'context-cnxmod:5e1ff6e7-0980-4ae0-bc8a-4b591a7c1760' ]
ex3.questions.first.stems.first.stylings.first.update_attribute :style, Style::MULTIPLE_CHOICE
ex3.publication.publish.save!

ex4 = FactoryBot.create :exercise, tags: [ 'context-cnxmod:59221da8-5fb6-4b3e-9450-079cd616385b' ]
ex4.questions.first.stems.first.stylings.first.update_attribute :style, Style::TRUE_FALSE
ex4.publication.publish.save!

ex5 = FactoryBot.create :exercise, tags: [ 'context-cnxmod:59221da8-5fb6-4b3e-9450-079cd616385b' ]
ex5.questions.first.stems.first.stylings.first.update_attribute :style, Style::FREE_RESPONSE
ex5.publication.publish.save!

ex6 = FactoryBot.create :exercise, tags: [ 'context-cnxmod:59221da8-5fb6-4b3e-9450-079cd616385b' ]
ex6.questions.first.stems.first.stylings.first.update_attribute :style, Style::MULTIPLE_CHOICE
ex6.publication.publish.save!

Rake.application.invoke_task "books:breakdown[#{book_uuid},#{filename}]"

rows = CSV.read filename
expect(rows.first).to eq([
'UUID', 'Type', 'Number', 'Title', 'Total Exercises', '2-step MC/TF', 'MC/TF only', 'FR only'
])
expect(rows.second).to eq([
'14fb4ad7-39a1-4eee-ab6e-3ef2482e3e22', 'Book', '', 'Anatomy and Physiology', '6', '2', '3', '1'
])
expect(rows.third).to eq([
'7c42370b-c3ad-48ac-9620-d15367b882c6', 'Page', '', 'Preface', '0', '0', '0', '0'
])
expect(rows.fourth).to eq([
'd3ad443b-78fa-551e-a67f-182bd0cb4c77', 'Unit/Chapter', '1', 'Unit 1 Levels of Organization', '6', '2', '3', '1'
])
expect(rows.fifth).to eq([
'5ae6cc38-7b7b-5e9c-a7a4-5d8251baac7f',
'Unit/Chapter',
'1',
'Chapter 1 An Introduction to the Human Body',
'6',
'2',
'3',
'1'
])
expect(rows[5]).to eq([
'ccc4ed14-6c87-408b-9934-7a0d279d853a', 'Page', '', 'Introduction', '0', '0', '0', '0'
])
expect(rows[6]).to eq([
'ada35081-9ec4-4eb8-98b2-3ce350d5427f',
'Page',
'1.1',
'1.1 Overview of Anatomy and Physiology',
'1',
'1',
'0',
'0'
])
expect(rows[7]).to eq([
'5e1ff6e7-0980-4ae0-bc8a-4b591a7c1760',
'Page',
'1.2',
'1.2 Structural Organization of the Human Body',
'2',
'1',
'1',
'0'
])
expect(rows[8]).to eq([
'59221da8-5fb6-4b3e-9450-079cd616385b', 'Page', '1.3', '1.3 Functions of Human Life', '3', '0', '2', '1'
])
rows[9..-1].each do |row|
expect(row[4..-1]).to eq(['0', '0', '0', '0'])
end
ensure
FileUtils.rm_f filename
end
end

0 comments on commit 753752c

Please sign in to comment.