11# Imports exercies from a spreadsheet for Assessments
2- # The first row contains column headers. Required columns:
3- # UUID (page UUID)
4- # Pre or Post
5- # Question Stem
6- # Answer Choice A
7- # Answer Choice B
8- # Answer Choice C
9- # Answer Choice D
10- # Correct Answer (A, B, C or D)
11- # Detailed Solution
2+ # The first row contains column headers. The only required column is Question Stem.
123module Exercises
134 module Import
145 class Assessments
@@ -25,79 +16,170 @@ def exec(filename:, book_uuid:)
2516 author = User . find ( AUTHOR_ID )
2617 copyright_holder = User . find ( COPYRIGHT_HOLDER_ID ) rescue author # So it works in the dev env with only 1 user
2718
19+ initialized = false
20+
21+ question_stem_index = nil
2822 uuid_index = nil
23+ section_index = nil
24+
25+ page_uuid_by_book_location = { }
26+
27+ nickname_index = nil
28+
2929 pre_or_post_index = nil
30- question_stem_index = nil
30+
31+ teks_index = nil
32+ machine_teks_index = nil
33+
34+ raise_id_index = nil
35+
36+ background_index = nil
37+ multi_step_index = nil
38+ block_index = nil
39+
3140 answer_choice_indices = nil
3241 correct_answer_index = nil
42+ feedback_indices = nil
43+
3344 detailed_solution_index = nil
45+
46+ row_number = nil
47+
48+ multi_step = nil
49+ exercise = nil
3450 record_failures do |failures |
35- ProcessSpreadsheet . call ( filename : filename , headers : :downcase ) do | headers , row , row_index |
36- uuid_index ||= headers . index { | header | header == 'uuid' || header == 'page uuid' }
51+ save = -> ( exercise , row_number ) do
52+ return if exercise . nil? || row_number . nil?
3753
38- section_index ||= headers . index { |header | header == 'section' }
39- raise ArgumentError , 'Could not find "UUID" or "Section" columns' if uuid_index . nil? && section_index . nil?
54+ begin
55+ exercise . save!
56+ exercise . publication . publish . save!
4057
41- unless section_index . nil?
42- book = OpenStax ::Content ::Abl . new . approved_books . find { |book | book . uuid == book_uuid }
43- page_uuid_by_book_location = { }
44- book . all_pages . each { |page | page_uuid_by_book_location [ page . book_location ] = page . uuid }
45- raise ArgumentError , "Could not find book with UUID #{ book_uuid } in the ABL" if book . nil?
58+ Rails . logger . info { "Imported row ##{ row_number } - New exercise ID: #{ exercise . uid } " }
59+ rescue StandardError => error
60+ Rails . logger . error { "Failed to import row ##{ row_number } - #{ error . message } " }
61+ failures [ row_number ] = error . to_s
4662 end
63+ end
4764
48- pre_or_post_index ||= headers . index { |header | header &.start_with? ( 'pre' ) && header . end_with? ( 'post' ) }
49- raise ArgumentError , 'Could not find "Pre or Post" column' if pre_or_post_index . nil?
65+ ProcessSpreadsheet . call ( filename : filename , headers : :downcase ) do |headers , row , row_index |
66+ unless initialized
67+ question_stem_index ||= headers . index do |header |
68+ header &.start_with? ( 'question' ) || header &.end_with? ( 'stem' )
69+ end
70+ raise ArgumentError , 'Could not find "Question Stem" column' if question_stem_index . nil?
5071
51- question_stem_index ||= headers . index do |header |
52- header &. start_with? ( 'question' ) | | header &. end_with? ( 'stem' )
53- end
54- raise ArgumentError , 'Could not find "Question Stem" column' if question_stem_index . nil?
72+ uuid_index ||= headers . index { |header | header == 'uuid' || header == 'page uuid' }
73+ section_index ||= headers . index { | header | header == 'section' }
74+ Rails . logger . warn { 'Could not find "UUID" or "Section" columns' } \
75+ if uuid_index . nil? && section_index . nil?
5576
56- answer_choice_indices ||= headers . filter_map . with_index do |header , index |
57- index if ( header &.start_with? ( 'answer' ) || header &.end_with? ( 'choice' ) ) && !header . include? ( 'feedback' )
58- end
59- raise ArgumentError , 'Could not find "Answer Choice" columns' if answer_choice_indices . empty?
77+ unless section_index . nil?
78+ book = OpenStax ::Content ::Abl . new . approved_books . find { |book | book . uuid == book_uuid }
79+ book . all_pages . each { |page | page_uuid_by_book_location [ page . book_location ] = page . uuid }
80+ raise ArgumentError , "Could not find book with UUID #{ book_uuid } in the ABL" if book . nil?
81+ end
6082
61- feedback_indices ||= headers . filter_map . with_index do |header , index |
62- index if header &.include? ( 'feedback' )
63- end
83+ nickname_index ||= headers . index { |header | header &.include? ( 'nickname' ) }
6484
65- correct_answer_index ||= headers . index { |header | header &.start_with? ( 'correct ' ) }
66- raise ArgumentError , 'Could not find "Correct Answer " column' if correct_answer_index . nil?
85+ pre_or_post_index ||= headers . index { |header | header &.start_with? ( 'pre' ) && header . end_with? ( 'post ') }
86+ Rails . logger . warn { 'Could not find "Pre or Post " column' } if pre_or_post_index . nil?
6787
68- detailed_solution_index ||= headers . index { |header | header &.end_with ?( 'solution ' ) }
69- raise ArgumentError , 'Could not find "Detailed Solution" column' if detailed_solution_index . nil?
88+ teks_index ||= headers . index { |header | header &.include ?( 'teks' ) && ! header . include? ( 'machine ') }
89+ machine_teks_index ||= headers . index { | header | header &. include? ( 'machine' ) && header . include? ( 'teks' ) }
7090
71- row_number = row_index + 1
91+ raise_id_index ||= headers . index { | header | header &. include? ( 'raise' ) }
7292
73- page_uuid = if uuid_index . nil? || row [ uuid_index ] . blank?
74- page_uuid_by_book_location [ row [ section_index ] . split ( '.' ) . map ( & :to_i ) ] unless row [ section_index ] . blank?
75- else
76- row [ uuid_index ]
77- end
93+ background_index ||= headers . index { | header | header &. include? ( 'background' ) }
94+
95+ multi_step_index ||= headers . index { | header | header &. include? ( 'multi' ) && header . include? ( 'step' ) }
96+
97+ block_index ||= headers . index { | header | header &. include? ( 'block' ) }
7898
79- if page_uuid . blank?
80- Rails . logger . info { "Skipped row ##{ row_number } with blank Section or Page UUID" }
81- next
99+ answer_choice_indices ||= headers . filter_map . with_index do |header , index |
100+ index if (
101+ header &.start_with? ( 'answer' ) || header &.start_with? ( 'option' ) || header &.end_with? ( 'choice' )
102+ ) && !header . include? ( 'feedback' )
103+ end
104+ Rails . logger . warn { 'Could not find "Answer Choice" columns' } if answer_choice_indices . empty?
105+
106+ correct_answer_index ||= headers . index { |header | header &.start_with? ( 'correct' ) }
107+ Rails . logger . warn { 'Could not find "Correct Answer" column' } if correct_answer_index . nil?
108+
109+ feedback_indices ||= headers . filter_map . with_index do |header , index |
110+ index if header &.include? ( 'feedback' )
111+ end
112+
113+ detailed_solution_index ||= headers . index { |header | header &.end_with? ( 'solution' ) }
114+ Rails . logger . warn { 'Could not find "Detailed Solution" column' } if detailed_solution_index . nil?
115+
116+ initialized = true
82117 end
83118
84- exercise = Exercise . new
119+ row_number = row_index + 1
85120
86- exercise . tags = [
87- "assessment:#{ row [ pre_or_post_index ] . downcase == 'pre' ? 'preparedness' : 'practice'
88- } :https://openstax.org/orn/book:page/#{ book_uuid } :#{ page_uuid } ",
89- "context-cnxmod:#{ page_uuid } "
90- ]
121+ # Using row_index here because dealing with the previous row
122+ if multi_step_index . nil? || multi_step . nil? || row [ multi_step_index ] != multi_step
123+ save . call ( exercise , row_index )
124+
125+ multi_step = row [ multi_step_index ] unless multi_step_index . nil?
126+
127+ exercise = Exercise . new
128+
129+ page_uuid = if uuid_index . nil? || row [ uuid_index ] . blank?
130+ page_uuid_by_book_location [ row [ section_index ] . split ( '.' ) . map ( &:to_i ) ] \
131+ unless section_index . nil? || row [ section_index ] . blank?
132+ else
133+ row [ uuid_index ]
134+ end
135+ if page_uuid . blank?
136+ Rails . logger . warn { "Row ##{ row_number } has no associated page in the book" } \
137+ unless uuid_index . nil? && section_index . nil?
138+ else
139+ exercise . tags << "context-cnxmod:#{ page_uuid } "
140+ exercise . tags << "assessment:#{ row [ pre_or_post_index ] . downcase == 'pre' ? 'preparedness' : 'practice'
141+ } :https://openstax.org/orn/book:page/#{ book_uuid } :#{ page_uuid } " \
142+ unless pre_or_post_index . nil? || row [ pre_or_post_index ] . blank?
143+ end
144+
145+ unless teks_index . nil? || row [ teks_index ] . blank?
146+ teks = row [ teks_index ] . split ( /,|;/ ) . map ( &:strip )
147+ teks . each { |tek | exercise . tags << "teks:#{ tek } " }
148+ end
149+ unless machine_teks_index . nil? || row [ machine_teks_index ] . blank?
150+ machine_teks = row [ machine_teks_index ] . split ( /,|;/ ) . map ( &:strip )
151+ machine_teks . each { |tek | exercise . tags << "machine-teks:#{ tek } " }
152+ end
153+ exercise . tags << "raise-content-id:#{ row [ raise_id_index ] } " \
154+ unless raise_id_index . nil? || row [ raise_id_index ] . blank?
155+
156+ exercise . publication . authors << Author . new ( user : author )
157+ exercise . publication . copyright_holders << CopyrightHolder . new ( user : copyright_holder )
158+
159+ exercise . publication . publication_group . nickname = row [ nickname_index ] unless nickname_index . nil?
160+
161+ exercise . stimulus = parse ( row [ background_index ] , exercise ) unless background_index . nil?
162+ else
163+ Rails . logger . info { "Imported row ##{ row_index } - Multi-step exercise" }
164+ end
91165
92166 question = Question . new
167+ question . sort_position = row [ block_index ] . to_i + 1 unless block_index . nil?
93168 exercise . questions << question
94169
95170 stem = Stem . new ( content : parse ( row [ question_stem_index ] , exercise ) )
96171 stem . stylings << Styling . new ( style : ::Style ::MULTIPLE_CHOICE )
97172 question . stems << stem
98173
99- exercise . publication . authors << Author . new ( user : author )
100- exercise . publication . copyright_holders << CopyrightHolder . new ( user : copyright_holder )
174+ unless detailed_solution_index . nil? || row [ detailed_solution_index ] . blank?
175+ solution = CollaboratorSolution . new (
176+ solution_type : SolutionType ::DETAILED ,
177+ content : parse ( row [ detailed_solution_index ] , exercise )
178+ )
179+ question . collaborator_solutions << solution
180+ end
181+
182+ next if correct_answer_index . nil? || row [ correct_answer_index ] . blank?
101183
102184 correct_answer = row [ correct_answer_index ] . downcase . strip . each_byte . first - 97
103185 answer_choice_indices . each_with_index do |row_index , answer_index |
@@ -113,26 +195,9 @@ def exec(filename:, book_uuid:)
113195 feedback : parse ( feedback , exercise )
114196 )
115197 end
116-
117- detailed_solution = row [ detailed_solution_index ]
118- if detailed_solution . present?
119- solution = CollaboratorSolution . new (
120- solution_type : SolutionType ::DETAILED ,
121- content : parse ( detailed_solution , exercise )
122- )
123- question . collaborator_solutions << solution
124- end
125-
126- begin
127- exercise . save!
128- exercise . publication . publish . save!
129-
130- Rails . logger . info { "Imported row ##{ row_number } - New exercise ID: #{ exercise . uid } " }
131- rescue StandardError => error
132- Rails . logger . error { "Failed to import row ##{ row_number } - #{ error . message } " }
133- failures [ row_number ] = error . to_s
134- end
135198 end
199+
200+ save . call ( exercise , row_number )
136201 end
137202 end
138203 end
0 commit comments