diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 1f9ca01..2ffec71 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -22,7 +22,7 @@ def index # conversations with the client, we're looking to only export classic (as you can migrate a # classic question to new format). This filename is another "helpful clue" and introduces # later considerations for what the file format might be. - filename = "questions-#{now.strftime('%Y-%m-%d_%H:%M:%S:%L')}.classic-question-canvas.qti.xml" + filename = "#{export_filename(now)}.classic-question-canvas.qti.xml" @questions = Question.filter(**filter_values) if any_question_has_images? @@ -35,14 +35,35 @@ def index end end - def text_download - questions = Question.where(id: Bookmark.select(:question_id)) - content = questions.map { |question| PlainTextFormatterService.new(question).format }.join('') - send_data content, filename: 'questions.txt', type: 'text/plain' + # download bookmarked questions + def download + @questions = Question.where(id: Bookmark.select(:question_id)) + case params[:format] + when 'md' + md_download + when 'txt' + text_download + else + redirect_to authenticated_root_path, alert: t('.alert') + end end private + def export_filename(now = Time.current) + "questions-#{now.strftime('%Y-%m-%d_%H:%M:%S:%L')}" + end + + def text_download + content = @questions.map { |question| QuestionFormatter::PlainTextService.new(question).format_content }.join('') + send_data content, filename: "#{export_filename}.txt", type: 'text/plain' + end + + def md_download + content = @questions.map { |question| QuestionFormatter::MarkdownService.new(question).format_content }.join('') + send_data content, filename: "#{export_filename}.md", type: 'text/plain' + end + def any_question_has_images? @questions.any? { |question| question.images.any? } end diff --git a/app/javascript/components/ui/Search/SearchBar.jsx b/app/javascript/components/ui/Search/SearchBar.jsx index 5f4741f..da169d5 100644 --- a/app/javascript/components/ui/Search/SearchBar.jsx +++ b/app/javascript/components/ui/Search/SearchBar.jsx @@ -90,13 +90,21 @@ const SearchBar = (props) => { View Bookmarks Export as Plain Text + + Export as Markdown + `bookmarked_question_ids[]=${encodeURIComponent(id)}`).join('&')}`} className={`btn btn-primary p-2 m-2 ${!hasBookmarks ? 'disabled' : ''}`} diff --git a/app/models/question/bow_tie.rb b/app/models/question/bow_tie.rb index 0ec7f67..7133b03 100644 --- a/app/models/question/bow_tie.rb +++ b/app/models/question/bow_tie.rb @@ -32,7 +32,7 @@ # => true class Question::BowTie < Question self.type_name = "Bow Tie" - self.model_exporter = 'bow_tie' + self.model_exporter = 'bowtie_type' # NOTE: We're not storing this in a JSONB data type, but instead favoring a text field. The need # for the data to be used in the application, beyond export of data, is minimal. diff --git a/app/models/question/categorization.rb b/app/models/question/categorization.rb index dc53adf..49e879a 100644 --- a/app/models/question/categorization.rb +++ b/app/models/question/categorization.rb @@ -8,7 +8,7 @@ class Question::Categorization < Question include MatchingQuestionBehavior self.type_name = "Categorization" - self.model_exporter = 'categorization' + self.model_exporter = 'categorization_type' self.export_as_xml = true self.choice_cardinality_is_multiple = true diff --git a/app/models/question/drag_and_drop.rb b/app/models/question/drag_and_drop.rb index b0f9eb8..25d9008 100644 --- a/app/models/question/drag_and_drop.rb +++ b/app/models/question/drag_and_drop.rb @@ -21,7 +21,7 @@ # data: [{ answer: "Aardvark", correct: true }, { answer: "Blue", correct: false }, { answer: "Yellow", correct:false }, { answer: "Cat", correct: true }]) class Question::DragAndDrop < Question self.type_name = "Drag and Drop" - self.model_exporter = 'drag_and_drop' + self.model_exporter = 'traditional_type' ## # Represents the mapping process of a CSV Row to the underlying {Question::DragAndDrop}. diff --git a/app/models/question/essay.rb b/app/models/question/essay.rb index 06aaea5..1298859 100644 --- a/app/models/question/essay.rb +++ b/app/models/question/essay.rb @@ -9,7 +9,7 @@ class Question::Essay < Question include MarkdownQuestionBehavior self.type_name = "Essay" - self.model_exporter = 'essay' + self.model_exporter = 'essay_type' self.export_as_xml = true class ImportCsvRow < MarkdownQuestionBehavior::ImportCsvRow diff --git a/app/models/question/matching.rb b/app/models/question/matching.rb index 1377d33..de158af 100644 --- a/app/models/question/matching.rb +++ b/app/models/question/matching.rb @@ -8,7 +8,7 @@ class Question::Matching < Question include MatchingQuestionBehavior self.type_name = "Matching" - self.model_exporter = 'matching' + self.model_exporter = 'matching_type' self.export_as_xml = true self.choice_cardinality_is_multiple = false diff --git a/app/models/question/select_all_that_apply.rb b/app/models/question/select_all_that_apply.rb index 89fa5d9..40f9312 100644 --- a/app/models/question/select_all_that_apply.rb +++ b/app/models/question/select_all_that_apply.rb @@ -7,7 +7,7 @@ # duplication than more complicated inheritance. class Question::SelectAllThatApply < Question self.type_name = "Select All That Apply" - self.model_exporter = 'select_all_that_apply' + self.model_exporter = 'traditional_type' ## # Represents the mapping process of a CSV Row to the underlying {Question::SelectAllThatApply}. diff --git a/app/models/question/stimulus_case_study.rb b/app/models/question/stimulus_case_study.rb index d8a1636..1eaeab9 100644 --- a/app/models/question/stimulus_case_study.rb +++ b/app/models/question/stimulus_case_study.rb @@ -5,7 +5,7 @@ class Question::StimulusCaseStudy < Question self.type_label = "Case Study" self.type_name = "Stimulus Case Study" - self.model_exporter = 'stimulus_case_study' + self.model_exporter = 'stimulus_type' self.has_parts = true has_many :as_parent_question_aggregations, diff --git a/app/models/question/traditional.rb b/app/models/question/traditional.rb index c362919..9e4f656 100644 --- a/app/models/question/traditional.rb +++ b/app/models/question/traditional.rb @@ -10,7 +10,7 @@ class Question::Traditional < Question # # @see https://github.com/notch8/viva/issues/261 self.type_name = "Multiple Choice" - self.model_exporter = 'multiple_choice' + self.model_exporter = 'traditional_type' ## # Represents the mapping process of a CSV Row to the underlying {Question::Traditional}. diff --git a/app/models/question/upload.rb b/app/models/question/upload.rb index 4a36ef3..6a4aedd 100644 --- a/app/models/question/upload.rb +++ b/app/models/question/upload.rb @@ -10,7 +10,7 @@ class Question::Upload < Question include MarkdownQuestionBehavior self.type_name = "Upload" - self.model_exporter = 'upload' + self.model_exporter = 'essay_type' self.export_as_xml = true class ImportCsvRow < MarkdownQuestionBehavior::ImportCsvRow diff --git a/app/services/base_formatter_service.rb b/app/services/base_formatter_service.rb deleted file mode 100644 index 4662764..0000000 --- a/app/services/base_formatter_service.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true -require 'nokogiri' - -## -# Service to handle formatting questions for downloads - -class BaseFormatterService - def initialize(question, subq = false) - @question = question - @subq = subq - end - - def format - format_by_type + content_divider - end - - protected - - def content_divider - raise NotImplementedError, "Subclasses must implement content_divider" - end - - def format_by_type - method = @question.class.model_exporter - send(method) - end - - def format_question_header - raise NotImplementedError, "Subclasses must implement format_question_header" - end - - def essay - format_question_header + format_essay_content - end - - def upload - format_question_header + format_essay_content - end - - def multiple_choice - format_question_header + format_answers(@question.data) { |answer, index| format_traditional_answer(answer, index) } - end - - def select_all_that_apply - format_question_header + format_answers(@question.data) { |answer, index| format_traditional_answer(answer, index) } - end - - def drag_and_drop - format_question_header + format_answers(@question.data) { |answer, index| format_traditional_answer(answer, index) } - end - - def matching - format_question_header + format_answers(@question.data) { |answer, index| format_matching_answer(answer, index) } - end - - def categorization - format_question_header + format_categories(@question.data) - end - - def bow_tie - format_question_header + format_bow_tie_sections - end - - def stimulus_case_study - output = @question.child_questions.map { |sub_question| format_sub_question(sub_question) } - # removes additional line breaks from the sub-questions - output[-1] = output[-1].chomp if output.any? - "#{format_question_header}#{output.join('')}" - end - - def format_sub_question(sub_question) - if sub_question.type == "Question::Scenario" - "Scenario: #{sub_question.text}\n\n" - else - "#{self.class.new(sub_question, true).format_by_type}\n" - end - end - - private - - def question_type - @question.class.type_name.titleize - end - - def format_essay_content - plain_text = format_html(@question.data['html']) - "Text: #{plain_text}\n" - end - - def format_answers(data) - data.map.with_index { |answer, index| yield(answer, index) }.join('') - end - - def format_traditional_answer(answer, index) - "#{index + 1}) #{answer['correct'] ? 'Correct' : 'Incorrect'}: #{answer['answer']}\n" - end - - def format_matching_answer(answer, index) - "#{index + 1}) #{answer['answer']}\n Correct Match: #{answer['correct'].first}\n" - end - - def format_categories(data) - data.map do |category| - items = category['correct'].map.with_index { |item, index| "#{index + 1}) #{item}\n" }.join('') - "Category: #{category['answer']}\n#{items}" - end.join("\n") - end - - def format_bow_tie_sections - sections = ['center', 'left', 'right'].map do |section| - answers = @question.data[section]['answers'].map.with_index do |answer, index| - format_traditional_answer(answer, index) - end.join('') - "#{section.capitalize}\n#{answers}" - end - sections.join("\n") - end - - def format_html(html) - rich_text = Nokogiri::HTML(html) - rich_text.css('a').each { |link| link.replace("#{link.text} (#{link['href']})") } - rich_text.css('p').each { |p| p.replace("#{p.text}\n") } - rich_text.css('li').each { |li| li.replace("- #{li.text}\n") } - rich_text.text.strip - end -end diff --git a/app/services/plain_text_formatter_service.rb b/app/services/plain_text_formatter_service.rb deleted file mode 100644 index eadeb79..0000000 --- a/app/services/plain_text_formatter_service.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -## -# Service to handle formatting questions into plain text - -class PlainTextFormatterService < BaseFormatterService - protected - - def content_divider - "\n==========\n\n" - end - - def format_question_header - return "QUESTION TYPE: #{question_type}\nQUESTION: #{@question.text}\n\n" unless @subq - "Subquestion Type: #{question_type}\nSubquestion: #{@question.text}\n\n" - end -end diff --git a/app/services/question_formatter/base_service.rb b/app/services/question_formatter/base_service.rb new file mode 100644 index 0000000..49dcf01 --- /dev/null +++ b/app/services/question_formatter/base_service.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true +require 'nokogiri' + +module QuestionFormatter + class BaseService + ## + # Service to handle formatting questions for downloads + class_attribute :output_format, default: nil + attr_reader :question, :subq + + def initialize(question, subq = false) + @question = question + @subq = subq + end + + def format_content + format_by_type + divider_line + end + + # These methods are protected instead of private so they can be called by other instances + protected + + def essay_type + format_question_header + format_essay_content + end + + def traditional_type + format_question_header + format_answers(@question.data) { |answer, index| format_traditional_answer(answer, index) } + end + + def matching_type + format_question_header + format_answers(@question.data) { |answer, index| format_matching_answer(answer, index) } + end + + def categorization_type + format_question_header + format_categories(@question.data) + end + + def bowtie_type + format_question_header + format_bowtie_sections + end + + def stimulus_type + output = @question.child_questions.map { |sub_question| format_sub_question(sub_question) } + # remove extra line breaks + output[-1] = output[-1].chomp if output.any? + "#{format_question_header}#{output.join('')}" + end + + def format_sub_question(sub_question) + case sub_question.type + when "Question::Scenario" + format_scenario(sub_question) + else + "#{self.class.new(sub_question, true).format_by_type}\n" + end + end + + def format_by_type + method = @question.class.model_exporter + send(method) + end + + private + + def divider_line + raise NotImplementedError, "Subclasses must implement divider_line" + end + + def format_scenario(question) + raise NotImplementedError, "Subclasses must implement format_scenario" + end + + def format_question_header + raise NotImplementedError, "Subclasses must implement format_question_header" + end + + def format_essay_content + plain_text = format_html(@question.data['html']) + "Text: #{plain_text}\n" + end + + def format_answers(data) + data.map.with_index { |answer, index| yield(answer, index) }.join('') + end + + def format_traditional_answer(answer, index) + "#{index + 1}) #{answer['correct'] ? 'Correct' : 'Incorrect'}: #{answer['answer']}\n" + end + + def format_matching_answer(answer, index) + "#{index + 1}) #{answer['answer']}\n Correct Match: #{answer['correct'].first}\n" + end + + def format_categories(data) + raise NotImplementedError, "Subclasses must implement format_categories" + end + + def format_bowtie_sections + sections = ['center', 'left', 'right'].map do |section| + answers = @question.data[section]['answers'].map.with_index do |answer, index| + format_traditional_answer(answer, index) + end.join('') + "#{section.capitalize}\n#{answers}" + end + sections.join("\n") + end + + def question_type + @question.class.type_name + end + + def format_html(html) + rich_text = Nokogiri::HTML(html) + rich_text.css('a').each { |link| link.replace("#{link.text} (#{link['href']})") } + rich_text.css('p').each { |p| p.replace("#{p.text}\n") } + rich_text.css('li').each { |li| li.replace("- #{li.text}\n") } + rich_text.text.strip + end + end +end diff --git a/app/services/question_formatter/markdown_service.rb b/app/services/question_formatter/markdown_service.rb new file mode 100644 index 0000000..9b88923 --- /dev/null +++ b/app/services/question_formatter/markdown_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +## +# Service to handle formatting questions into markdown +module QuestionFormatter + class MarkdownService < BaseService + self.output_format = 'md' + + private + + def divider_line + "\n---\n\n" + end + + def format_scenario(sub_question) + "**Scenario:** #{sub_question.text}\n\n" + end + + def format_question_header + headers = case @subq + when true + "### Subquestion Type: #{question_type}\n**Subquestion:** #{@question.text}\n\n" + else + "## QUESTION TYPE: #{question_type}\n**QUESTION:** #{@question.text}\n\n" + end + headers + end + + def format_essay_content + plain_text = format_html(@question.data['html']) + "**Text:** #{plain_text}\n" + end + + def format_categories(data) + data.map do |category| + items = category['correct'].map.with_index { |item, index| "#{index + 1}) #{item}\n" }.join('') + "**Category:** #{category['answer']}\n#{items}" + end.join("\n") + end + end +end diff --git a/app/services/question_formatter/plain_text_service.rb b/app/services/question_formatter/plain_text_service.rb new file mode 100644 index 0000000..e1742ea --- /dev/null +++ b/app/services/question_formatter/plain_text_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +## +# Service to handle formatting questions into plain text +module QuestionFormatter + class PlainTextService < BaseService + self.output_format = 'txt' + + private + + def divider_line + "\n==========\n\n" + end + + def format_scenario(sub_question) + "Scenario: #{sub_question.text}\n\n" + end + + def format_question_header + return "QUESTION TYPE: #{question_type}\nQUESTION: #{@question.text}\n\n" unless @subq + "Subquestion Type: #{question_type}\nSubquestion: #{@question.text}\n\n" + end + + def format_categories(data) + data.map do |category| + items = category['correct'].map.with_index { |item, index| "#{index + 1}) #{item}\n" }.join('') + "Category: #{category['answer']}\n#{items}" + end.join("\n") + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index e50aaeb..4d6ebb8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -49,3 +49,6 @@ en: success: "Settings updated successfully." update_password: success: "Password updated successfully." + search: + download: + alert: "Invalid format. Please select a valid format." diff --git a/config/routes.rb b/config/routes.rb index 5a74de9..228b89b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,8 +13,8 @@ # Necessary for downloading the XML file from the search result. get '/(.:format)', to: 'search#index' - # download the bookmarked questions in a plain text file - get 'questions/text_download', to: 'search#text_download' + # download the bookmarked questions in a text file + get 'questions/download', to: 'search#download', as: 'download_questions' # settings page routes get '/settings', to: 'settings#index', as: 'settings' diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index b0a2754..bb0ad82 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -142,21 +142,39 @@ end end - context 'plain text download' do + context 'downloading question text' do let(:question) { FactoryBot.create(:question_traditional) } let(:user) { FactoryBot.create(:user) } + let(:bookmark) { Bookmark.create!(user:, question:) } - it 'returns a text file' do - get :text_download - expect(response.content_type).to eq('text/plain') - expect(response.headers['Content-Disposition']).to include('questions.txt') + before do + bookmark end - it 'includes bookmarked questions in the response' do - Bookmark.create!(user:, question:) + context 'downloading as plain text' do + it 'returns a txt file' do + get :download, format: :txt + expect(response.content_type).to eq('text/plain') + expect(response.headers['Content-Disposition']).to match(/questions-.*\.txt/) + end + + it 'includes bookmarked questions in the response' do + get :download, format: :txt + expect(response.body).to include(question.text) + end + end - get :text_download - expect(response.body).to include(question.text) + context 'downloading as markdown' do + it 'returns a md file' do + get :download, format: :md + expect(response.content_type).to eq('text/plain') + expect(response.headers['Content-Disposition']).to match(/questions-.*\.md/) + end + + it 'includes bookmarked questions in the response' do + get :download, format: :md + expect(response.body).to include(question.text) + end end end end diff --git a/spec/services/question_formatter/base_service_spec.rb b/spec/services/question_formatter/base_service_spec.rb new file mode 100644 index 0000000..4361abd --- /dev/null +++ b/spec/services/question_formatter/base_service_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe QuestionFormatter::BaseService do + let(:subject) { described_class.new(question) } + let(:question) do + create(:question_essay, + text: 'Sample essay question', + data: { 'html' => '

Essay prompt

Link' }) + end + + it 'has the correct public methods' do + expect(subject).to respond_to(:format_content) + end +end diff --git a/spec/services/question_formatter/markdown_service_spec.rb b/spec/services/question_formatter/markdown_service_spec.rb new file mode 100644 index 0000000..1146528 --- /dev/null +++ b/spec/services/question_formatter/markdown_service_spec.rb @@ -0,0 +1,299 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe QuestionFormatter::MarkdownService do + let(:service) { described_class.new(question) } + + describe '#format' do + subject { service.format_content } + + context 'with an essay question' do + let(:question) do + create(:question_essay, + text: 'Sample essay question', + data: { 'html' => '

Essay prompt

Link' }) + end + + it 'formats the question correctly' do + expected_output = <<~TEXT + ## QUESTION TYPE: Essay + **QUESTION:** Sample essay question + + **Text:** Essay prompt + - Point 1 + Link (https://example.com) + + --- + + TEXT + expect(subject).to eq(expected_output) + end + end + + context 'with an upload question' do + let(:question) do + create(:question_upload, + text: 'Sample upload question', + data: { 'html' => '

Upload instructions

Guidelines' }) + end + + it 'formats the question correctly' do + expected_output = <<~TEXT + ## QUESTION TYPE: Upload + **QUESTION:** Sample upload question + + **Text:** Upload instructions + - File type: PDF + Guidelines (https://example.com) + + --- + + TEXT + expect(subject).to eq(expected_output) + end + end + + context 'with a multiple choice question' do + let(:question) do + create(:question_traditional, + text: 'Sample multiple choice', + data: [ + { 'answer' => 'Option A', 'correct' => true }, + { 'answer' => 'Option B', 'correct' => false } + ]) + end + + it 'formats the question correctly' do + expected_output = <<~TEXT + ## QUESTION TYPE: Multiple Choice + **QUESTION:** Sample multiple choice + + 1) Correct: Option A + 2) Incorrect: Option B + + --- + + TEXT + expect(subject).to eq(expected_output) + end + end + + context 'with a select all that apply question' do + let(:question) do + create(:question_select_all_that_apply, + text: 'Sample select all question', + data: [ + { 'answer' => 'Option A', 'correct' => true }, + { 'answer' => 'Option B', 'correct' => true }, + { 'answer' => 'Option C', 'correct' => false } + ]) + end + + it 'formats the question correctly' do + expected_output = <<~TEXT + ## QUESTION TYPE: Select All That Apply + **QUESTION:** Sample select all question + + 1) Correct: Option A + 2) Correct: Option B + 3) Incorrect: Option C + + --- + + TEXT + expect(subject).to eq(expected_output) + end + end + + context 'with a drag and drop question' do + let(:question) do + create(:question_drag_and_drop, + text: 'Sample drag and drop question', + data: [ + { 'answer' => 'Item 1', 'correct' => true }, + { 'answer' => 'Item 2', 'correct' => false }, + { 'answer' => 'Item 3', 'correct' => true } + ]) + end + + it 'formats the question correctly' do + expected_output = <<~TEXT + ## QUESTION TYPE: Drag and Drop + **QUESTION:** Sample drag and drop question + + 1) Correct: Item 1 + 2) Incorrect: Item 2 + 3) Correct: Item 3 + + --- + + TEXT + expect(subject).to eq(expected_output) + end + end + + context 'with a matching question' do + let(:question) do + create(:question_matching, + text: 'Sample matching question', + data: [ + { + 'answer' => 'Term 1', + 'correct' => ['Definition 1'] + }, + { + 'answer' => 'Term 2', + 'correct' => ['Definition 2'] + }, + { + 'answer' => 'Term 3', + 'correct' => ['Definition 3'] + } + ]) + end + + it 'formats the question correctly' do + expected_output = <<~TEXT + ## QUESTION TYPE: Matching + **QUESTION:** Sample matching question + + 1) Term 1 + Correct Match: Definition 1 + 2) Term 2 + Correct Match: Definition 2 + 3) Term 3 + Correct Match: Definition 3 + + --- + + TEXT + expect(subject).to eq(expected_output) + end + end + + context 'with a categorization question' do + let(:question) do + create(:question_categorization, + text: 'Sample categorization', + data: [ + { 'answer' => 'Category 1', 'correct' => ['Item 1', 'Item 2'] }, + { 'answer' => 'Category 2', 'correct' => ['Item 3'] } + ]) + end + + it 'formats the question correctly' do + expected_output = <<~TEXT + ## QUESTION TYPE: Categorization + **QUESTION:** Sample categorization + + **Category:** Category 1 + 1) Item 1 + 2) Item 2 + + **Category:** Category 2 + 1) Item 3 + + --- + + TEXT + expect(subject).to eq(expected_output) + end + end + + context 'with a bow tie question' do + let(:question) do + create(:question_bow_tie, + text: 'Sample bow tie question', + data: { + 'center' => { + 'label' => 'Center Label', + 'answers' => [ + { 'answer' => 'Center Answer 1', 'correct' => true }, + { 'answer' => 'Center Answer 2', 'correct' => false } + ] + }, + 'left' => { + 'label' => 'Left Label', + 'answers' => [ + { 'answer' => 'Left Answer 1', 'correct' => true }, + { 'answer' => 'Left Answer 2', 'correct' => false } + ] + }, + 'right' => { + 'label' => 'Right Label', + 'answers' => [ + { 'answer' => 'Right Answer 1', 'correct' => true }, + { 'answer' => 'Right Answer 2', 'correct' => false } + ] + } + }) + end + + it 'formats the question correctly' do + expected_output = <<~TEXT + ## QUESTION TYPE: Bow Tie + **QUESTION:** Sample bow tie question + + Center + 1) Correct: Center Answer 1 + 2) Incorrect: Center Answer 2 + + Left + 1) Correct: Left Answer 1 + 2) Incorrect: Left Answer 2 + + Right + 1) Correct: Right Answer 1 + 2) Incorrect: Right Answer 2 + + --- + + TEXT + expect(subject).to eq(expected_output) + end + end + + context 'with a stimulus case study question' do + let(:scenario) { create(:question_scenario, text: 'Sample scenario') } + let(:sub_question_essay) { create(:question_essay, text: 'Sub question', data: { 'html' => '

Essay prompt

' }) } + let(:sub_question_mc) do + create(:question_traditional, + text: 'Multiple choice sub question', + data: [ + { 'answer' => 'Option A', 'correct' => true }, + { 'answer' => 'Option B', 'correct' => false } + ]) + end + let(:question) do + create(:question_stimulus_case_study, + text: 'Main question', + child_questions: [scenario, sub_question_essay, sub_question_mc]) + end + + it 'formats the question correctly' do + expected_output = <<~TEXT + ## QUESTION TYPE: Stimulus Case Study + **QUESTION:** Main question + + **Scenario:** Sample scenario + + ### Subquestion Type: Essay + **Subquestion:** Sub question + + **Text:** Essay prompt + + ### Subquestion Type: Multiple Choice + **Subquestion:** Multiple choice sub question + + 1) Correct: Option A + 2) Incorrect: Option B + + --- + + TEXT + expect(subject).to eq(expected_output) + end + end + end +end diff --git a/spec/services/plain_text_formatter_service_spec.rb b/spec/services/question_formatter/plain_text_service_spec.rb similarity index 98% rename from spec/services/plain_text_formatter_service_spec.rb rename to spec/services/question_formatter/plain_text_service_spec.rb index d4a6712..55ade23 100644 --- a/spec/services/plain_text_formatter_service_spec.rb +++ b/spec/services/question_formatter/plain_text_service_spec.rb @@ -2,11 +2,11 @@ require 'rails_helper' -RSpec.describe PlainTextFormatterService do +RSpec.describe QuestionFormatter::PlainTextService do let(:service) { described_class.new(question) } describe '#format' do - subject { service.format } + subject { service.format_content } context 'with an essay question' do let(:question) do @@ -119,7 +119,7 @@ it 'formats the question correctly' do expected_output = <<~TEXT - QUESTION TYPE: Drag And Drop + QUESTION TYPE: Drag and Drop QUESTION: Sample drag and drop question 1) Correct: Item 1