1
1
# 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.
12
3
module Exercises
13
4
module Import
14
5
class Assessments
@@ -25,79 +16,170 @@ def exec(filename:, book_uuid:)
25
16
author = User . find ( AUTHOR_ID )
26
17
copyright_holder = User . find ( COPYRIGHT_HOLDER_ID ) rescue author # So it works in the dev env with only 1 user
27
18
19
+ initialized = false
20
+
21
+ question_stem_index = nil
28
22
uuid_index = nil
23
+ section_index = nil
24
+
25
+ page_uuid_by_book_location = { }
26
+
27
+ nickname_index = nil
28
+
29
29
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
+
31
40
answer_choice_indices = nil
32
41
correct_answer_index = nil
42
+ feedback_indices = nil
43
+
33
44
detailed_solution_index = nil
45
+
46
+ row_number = nil
47
+
48
+ multi_step = nil
49
+ exercise = nil
34
50
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?
37
53
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!
40
57
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
46
62
end
63
+ end
47
64
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?
50
71
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?
55
76
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
60
82
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' ) }
64
84
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?
67
87
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' ) }
70
90
71
- row_number = row_index + 1
91
+ raise_id_index ||= headers . index { | header | header &. include? ( 'raise' ) }
72
92
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' ) }
78
98
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
82
117
end
83
118
84
- exercise = Exercise . new
119
+ row_number = row_index + 1
85
120
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
91
165
92
166
question = Question . new
167
+ question . sort_position = row [ block_index ] . to_i + 1 unless block_index . nil?
93
168
exercise . questions << question
94
169
95
170
stem = Stem . new ( content : parse ( row [ question_stem_index ] , exercise ) )
96
171
stem . stylings << Styling . new ( style : ::Style ::MULTIPLE_CHOICE )
97
172
question . stems << stem
98
173
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?
101
183
102
184
correct_answer = row [ correct_answer_index ] . downcase . strip . each_byte . first - 97
103
185
answer_choice_indices . each_with_index do |row_index , answer_index |
@@ -113,26 +195,9 @@ def exec(filename:, book_uuid:)
113
195
feedback : parse ( feedback , exercise )
114
196
)
115
197
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
135
198
end
199
+
200
+ save . call ( exercise , row_number )
136
201
end
137
202
end
138
203
end
0 commit comments