░▒▓████████████████████████████████▓▒░ ░▒▓█ ▓▒░ ░▒▓█ ~ S W A M P ~ ▓▒░ ░▒▓█ ▓▒░ ░▒▓████████████████████████████████▓▒░
pplmvsvm
LOCATION:
/var/www/canvas/spec/models/quizzes/quiz_statistics
☗ ROOT
↻ REFRESH
✎ CARVE FLESH
EDITING: student_analysis_spec.rb
# frozen_string_literal: true # # Copyright (C) 2013 - present Instructure, Inc. # # This file is part of Canvas. # # Canvas is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, version 3 of the License. # # Canvas is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see <http://www.gnu.org/licenses/>. require_relative "common" describe Quizzes::QuizStatistics::StudentAnalysis do def temporary_user_code "tmp_#{Digest::SHA256.hexdigest("#{Time.now.to_i}_#{rand}")}" end def survey_with_logged_out_submission course_with_teacher(active_all: true) @assignment = @course.assignments.create(title: "Test Assignment") @assignment.workflow_state = "available" @assignment.submission_types = "online_quiz" @assignment.save @quiz = Quizzes::Quiz.where(assignment_id: @assignment).first @quiz.anonymous_submissions = false @quiz.quiz_type = "survey" # make questions questions = [{ question_data: { name: "test 1" } }, { question_data: { name: "test 2" } }, { question_data: { name: "test 3" } }, { question_data: { name: "test 4" } }] @questions = questions.map { |q| @quiz.quiz_questions.create!(q) } @quiz.generate_quiz_data @quiz.save! @quiz_submission = @quiz.generate_submission(temporary_user_code) @quiz_submission.mark_completed Quizzes::SubmissionGrader.new(@quiz_submission).grade_submission @quiz_submission.save! end let(:report_type) { "student_analysis" } include_examples "Quizzes::QuizStatistics::Report" before(:once) { course_factory } def csv(opts = {}, quiz = @quiz) opts[:includes_sis_ids] = true unless opts.key?(:includes_sis_ids) stats = quiz.statistics_csv("student_analysis", opts) run_jobs stats.reload_csv_attachment.open.read end it "calculates mean/stddev as expected with no submissions" do q = @course.quizzes.create! stats = q.statistics expect(stats[:submission_score_average]).to be_nil expect(stats[:submission_score_high]).to be_nil expect(stats[:submission_score_low]).to be_nil expect(stats[:submission_score_stdev]).to be_nil end it "calculates mean/stddev as expected with a few submissions" do q = @course.quizzes.create! question = q.quiz_questions.create!({ question_data: { name: "q1", points_possible: 30, question_type: "essay_question", question_text: "ohai mark" } }) q.generate_quiz_data q.save! @user1 = User.create! name: "some_user 1" @user2 = User.create! name: "some_user 2" @user3 = User.create! name: "some_user 2" student_in_course course: @course, user: @user1 student_in_course course: @course, user: @user2 student_in_course course: @course, user: @user3 sub = q.generate_submission(@user1) sub.workflow_state = "complete" sub.submission_data = [{ points: 10, text: "", correct: "undefined", question_id: question.id }] # simulate a positive fudge of 5 points: sub.score = 15 sub.with_versioning(true, &:save!) stats = q.statistics expect(stats[:submission_score_average]).to eq 15 expect(stats[:submission_score_high]).to eq 15 expect(stats[:submission_score_low]).to eq 15 expect(stats[:submission_score_stdev]).to eq 0 expect(stats[:submission_scores]).to eq({ 50 => 1 }) sub = q.generate_submission(@user2) sub.workflow_state = "complete" sub.submission_data = [{ points: 17, text: "", correct: "undefined", question_id: question.id }] sub.score = 17 sub.with_versioning(true, &:save!) stats = q.statistics expect(stats[:submission_score_average]).to eq 16 expect(stats[:submission_score_high]).to eq 17 expect(stats[:submission_score_low]).to eq 15 expect(stats[:submission_score_stdev]).to eq 1 expect(stats[:submission_scores]).to eq({ 50 => 1, 57 => 1 }) sub = q.generate_submission(@user3) sub.workflow_state = "complete" sub.submission_data = [{ points: 20, text: "", correct: "undefined", question_id: question.id }] sub.score = 20 sub.with_versioning(true, &:save!) stats = q.statistics expect(stats[:submission_score_average]).to be_within(0.0000000001).of(17 + (1.0 / 3)) expect(stats[:submission_score_high]).to eq 20 expect(stats[:submission_score_low]).to eq 15 expect(stats[:submission_score_stdev]).to be_within(0.0000000001).of(Math.sqrt(4 + (2.0 / 9))) expect(stats[:submission_scores]).to eq({ 50 => 1, 57 => 1, 67 => 1 }) end it "creates quiz statistics with essay questions and anonymous submissions" do @user1 = User.create! name: "some_user 1" student_in_course course: @course, user: @user1 quiz = @course.quizzes.create! quiz.update(published_at: Time.zone.now, quiz_type: "survey", anonymous_submissions: true) quiz.quiz_questions.create!(question_data: essay_question_data) quiz.generate_quiz_data quiz.save qs = quiz.generate_submission(@user1) qs.submission_data = { "question_#{quiz.quiz_data[0][:id]}" => "Essay response user 1" } qs.workflow_state = "complete" qs.save expect do quiz.quiz_statistics.build(report_type: "student_analysis", includes_all_versions: true, anonymous: true).report.generate(false) end.to_not raise_error end it "creates quiz statistics with logged out users" do survey_with_logged_out_submission expect do @quiz.quiz_statistics.build(report_type: "student_analysis", includes_all_versions: true, anonymous: false).report.generate(false) end.to_not raise_error end context "csv" do before do student_in_course(active_all: true) @quiz = @course.quizzes.create! @quiz.quiz_questions.create!(question_data: { name: "test 1" }) @quiz.generate_quiz_data @quiz.published_at = Time.now @quiz.save! end it "does not include user data for anonymous surveys" do @quiz.update_attribute :anonymous_submissions, true # one complete submission qs = @quiz.generate_submission(@student) Quizzes::SubmissionGrader.new(qs).grade_submission # and one in progress @quiz.generate_submission(@student) stats = CSV.parse(csv(include_all_versions: true)) expect(stats.last.length).to eq 9 stats.first.first == "section" end it "includes sis ids when requested" do qs = @quiz.generate_submission(@student) Quizzes::SubmissionGrader.new(qs).grade_submission stats = CSV.parse(csv(includes_sis_ids: true)) expect(stats.last.length).to eq 12 expect(stats.first).to include "sis_id" expect(stats.first).to include "section_sis_id" end it "does not include sis ids when not requested" do qs = @quiz.generate_submission(@student) Quizzes::SubmissionGrader.new(qs).grade_submission stats = CSV.parse(csv(includes_sis_ids: false)) expect(stats.last.length).to eq 10 expect(stats.first).not_to include "sis_id" expect(stats.first).not_to include "section_sis_id" end it "succeeds with logged-out user submissions" do survey_with_logged_out_submission stats = CSV.parse(csv(include_all_versions: true)) expect(stats.last[0]).to eq "" expect(stats.last[1]).to eq "" expect(stats.last[2]).to eq "" end it "has sections in quiz statistics_csv" do # enroll user in multiple sections pseudonym(@student) @student.pseudonym.sis_user_id = "user_sis_id_01" @student.pseudonym.save! section1 = @course.course_sections.first section1.sis_source_id = "SISSection01" section1.save! section2 = CourseSection.new(course: @course, name: "section2") section2.sis_source_id = "SISSection02" section2.save! @course.enroll_user(@student, "StudentEnrollment", enrollment_state: "active", allow_multiple_enrollments: true, section: section2) @student.save! # one complete submission qs = @quiz.generate_submission(@student) Quizzes::SubmissionGrader.new(qs).grade_submission stats = CSV.parse(csv(include_all_versions: true)) expect(stats.last[0]).to eq "nobody@example.com" expect(stats.last[1]).to eq @student.id.to_s expect(stats.last[2]).to eq "user_sis_id_01" splitter = ->(str) { str.split(",").map(&:strip) } sections = splitter.call(stats.last[3]) expect(sections).to include("section2") expect(sections).to include("Unnamed Course") section_ids = splitter.call(stats.last[4]) expect(section_ids).to include(section2.id.to_s) expect(section_ids).to include(section1.id.to_s) section_sis_ids = splitter.call(stats.last[5]) expect(section_sis_ids).to include("SISSection02") expect(section_sis_ids).to include("SISSection01") end it "uses sections in quiz statistics generate" do # enroll user in multiple sections pseudonym(@student) @student.pseudonym.sis_user_id = "user_sis_id_01" @student.pseudonym.save! section1 = @course.course_sections.first section1.sis_source_id = "SISSection01" section1.save! section2 = CourseSection.new(course: @course, name: "section2") section2.sis_source_id = "SISSection02" section2.save! @course.enroll_user(@student, "StudentEnrollment", enrollment_state: "active", allow_multiple_enrollments: true, section: section2) @student.save! @quiz.quiz_questions.create!({ question_data: { name: "q1", points_possible: 30, question_type: "essay_question", question_text: "ohai mark" } }) @quiz.generate_quiz_data @quiz.save! # one complete submission qs = @quiz.generate_submission(@student) Quizzes::SubmissionGrader.new(qs).grade_submission stats = @quiz.quiz_statistics.build( report_type: "student_analysis", includes_all_versions: true ).report.generate(true, { section_ids: section2.id }) expect(stats[:questions][0][1]["answers"][0]["responses"]).to eq 1 end it "deals with incomplete fill-in-multiple-blanks questions" do @quiz.quiz_questions.create!(question_data: { name: "test 2", question_type: "fill_in_multiple_blanks_question", question_text: "[ans0]", answers: [{ "answer_text" => "foo", "blank_id" => "ans0", "answer_weight" => "100" }] }) @quiz.quiz_questions.create!(question_data: { name: "test 3", question_type: "fill_in_multiple_blanks_question", question_text: "[ans0] [ans1]", answers: [{ "answer_text" => "bar", "blank_id" => "ans0", "answer_weight" => "100" }, { "answer_text" => "baz", "blank_id" => "ans1", "answer_weight" => "100" }] }) @quiz.generate_quiz_data @quiz.save! expect(@quiz.quiz_questions.size).to eq 3 qs = @quiz.generate_submission(@student) # submission will not answer question 2 and will partially answer question 3 qs.submission_data = { "question_#{@quiz.quiz_questions[2].id}_#{AssessmentQuestion.variable_id("ans1")}" => "baz" } Quizzes::SubmissionGrader.new(qs).grade_submission stats = CSV.parse(csv) expect(stats.last.size).to eq 16 # 3 questions * 2 lines + ten more (name, id, sis_id, section, section_id, section_sis_id, submitted, correct, incorrect, score) expect(stats.last[11]).to eq ",baz" end it "contains answers to numerical questions" do @quiz.quiz_questions.create!(question_data: { name: "numerical_question", question_type: "numerical_question", question_text: "[num1]", answers: { "answer_0" => { numerical_answer_type: "exact_answer" } } }) @quiz.quiz_questions.last.question_data[:answers].first[:exact] = 5 @quiz.generate_quiz_data @quiz.save! qs = @quiz.generate_submission(@student) qs.submission_data = { "question_#{@quiz.quiz_questions[1].id}" => 5 } Quizzes::SubmissionGrader.new(qs).grade_submission stats = CSV.parse(csv) expect(stats.last[9]).to eq "5" end it "does not error out when no answers are present in a calculated_question" do @quiz.quiz_questions.create!(question_data: { name: "calculated_question", question_type: "calculated_question", question_text: "[num1]" }) @quiz.generate_quiz_data @quiz.save! qs = @quiz.generate_submission(@student) qs.submission_data = { "question_#{@quiz.quiz_questions[1].id}" => "Pretend this is an essay question!" } Quizzes::SubmissionGrader.new(qs).grade_submission expect { @quiz.statistics_csv("student_analysis", {}) }.not_to raise_error end it "does not error out when answers is null in a text_only_question" do @quiz.quiz_questions.create!(question_data: { name: "text_only_question", question_type: "text_only_question", question_text: "[num1]", answers: nil }) @quiz.generate_quiz_data @quiz.save! qs = @quiz.generate_submission(@student) qs.submission_data = { "question_#{@quiz.quiz_questions[1].id}" => "Pretend this is an essay question!" } Quizzes::SubmissionGrader.new(qs).grade_submission expect { @quiz.statistics_csv("student_analysis", {}) }.not_to raise_error end it "includes primary domain if trust exists" do account2 = Account.create! allow(HostUrl).to receive(:context_host).and_return("school") expect(HostUrl).to receive(:context_host).with(account2).and_return("school1") @student.pseudonyms.scope.delete_all account2.pseudonyms.create!(user: @student, unique_id: "user") { |p| p.sis_user_id = "sisid" } allow_any_instantiation_of(@quiz.context.root_account).to receive(:trust_exists?).and_return(true) allow_any_instantiation_of(@quiz.context.root_account).to receive(:trusted_account_ids).and_return([account2.id]) qs = @quiz.generate_submission(@student) Quizzes::SubmissionGrader.new(qs).grade_submission stats = CSV.parse(csv(include_all_versions: true)) expect(stats[1][3]).to eq "school1" end end it "includes attachment display names for quiz file upload questions" do student_in_course(active_all: true) student = @student student.name = "Not Steve" student.save! student2 = User.create!(name: "Stevie Jeebie") @course.enroll_user(student2, "StudentEnrollment", enrollment_state: :active) q = @course.quizzes.create!(title: "new quiz") q.update_attribute :published_at, Time.now question = q.quiz_questions.create! question_data: { name: "q1", points_possible: 1, question_type: "file_upload_question", question_text: "ohai mark" } q.generate_quiz_data q.save! qs = q.generate_submission student io = fixture_file_upload("docs/doc.doc", "application/msword", true) attach = qs.attachments.create! filename: "doc.doc", display_name: "attachment.png", user: student, uploaded_data: io qs.submission_data["question_#{question.id}".to_sym] = [attach.id.to_s] qs.save! Quizzes::SubmissionGrader.new(qs).grade_submission qs = q.generate_submission student2 qs.submission_data["question_#{question.id}".to_sym] = nil qs.save! Quizzes::SubmissionGrader.new(qs).grade_submission # make student2's submission first qs.updated_at = 3.days.ago qs.save! stats = CSV.parse(csv({ include_all_versions: true }, q.reload)) stats = stats.sort_by { |s| s[1] } # sort by the id expect(stats.first[7]).to eq attach.display_name end it "strips tags from html multiple-choice/multiple-answers" do student_in_course(active_all: true) q = @course.quizzes.create!(title: "new quiz") q.update_attribute(:published_at, Time.now) q.quiz_questions.create!(question_data: { :name => "q1", :points_possible => 1, "question_type" => "multiple_choice_question", "answers" => [{ "answer_text" => "", "answer_html" => "<em>zero</em>", "answer_weight" => "100" }, { "answer_text" => "", "answer_html" => "<p>one</p>", "answer_weight" => "0" }] }) q.quiz_questions.create!(question_data: { :name => "q2", :points_possible => 1, "question_type" => "multiple_answers_question", "answers" => [{ "answer_text" => "", "answer_html" => "<a href='http://example.com/caturday.gif'>lolcats</a>", "answer_weight" => "100" }, { "answer_text" => "lolrus", "answer_weight" => "100" }] }) q.generate_quiz_data q.save qs = q.generate_submission(@student) qs.submission_data = { "question_#{q.quiz_data[0][:id]}" => (q.quiz_data[0][:answers][0][:id]).to_s, "question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][0][:id]}" => "1", "question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][1][:id]}" => "1" } Quizzes::SubmissionGrader.new(qs).grade_submission # visual statistics stats = q.statistics expect(stats[:questions].length).to eq 2 expect(stats[:questions][0].length).to eq 2 expect(stats[:questions][0][0]).to eq "question" expect(stats[:questions][0][1][:answers].length).to eq 2 expect(stats[:questions][0][1][:answers][0][:responses]).to eq 1 expect(stats[:questions][0][1][:answers][0][:text]).to eq "zero" expect(stats[:questions][0][1][:answers][1][:responses]).to eq 0 expect(stats[:questions][0][1][:answers][1][:text]).to eq "one" expect(stats[:questions][1].length).to eq 2 expect(stats[:questions][1][0]).to eq "question" expect(stats[:questions][1][1][:answers].length).to eq 2 expect(stats[:questions][1][1][:answers][0][:responses]).to eq 1 expect(stats[:questions][1][1][:answers][0][:text]).to eq "lolcats" expect(stats[:questions][1][1][:answers][1][:responses]).to eq 1 expect(stats[:questions][1][1][:answers][1][:text]).to eq "lolrus" # csv statistics stats = CSV.parse(csv({}, q)) expect(stats.last[7]).to eq "zero" expect(stats.last[9]).to eq "lolcats,lolrus" end it "does not strip things that look like tags from essay questions" do student_in_course(active_all: true) q = @course.quizzes.create! q.update_attribute(:published_at, Time.zone.now) q.quiz_questions.create!(question_data: essay_question_data) q.generate_quiz_data q.save qs = q.generate_submission(@student) qs.submission_data = { "question_#{q.quiz_data[0][:id]}" => "<p class=\"p1\"><span class=\"s1\"><> < > WHERE rental_duration >= 6 AND rating <> 'R' 1 < 3 > 2</span></p>" } Quizzes::SubmissionGrader.new(qs).grade_submission stats = CSV.parse(csv({}, q)) expect(stats.last[7]).to eq "<> < > WHERE rental_duration >= 6 AND rating <> 'R' 1 < 3 > 2" end it "strips tags from all student-provided answers" do student_in_course(active_all: true) q = @course.quizzes.create! q.update_attribute(:published_at, Time.now) q.quiz_questions.create!(question_data: short_answer_question_data) q.quiz_questions.create!(question_data: fill_in_multiple_blanks_question_one_blank_data) q.quiz_questions.create!(question_data: essay_question_data) q.quiz_questions.create!(question_data: numerical_question_data) q.quiz_questions.create!(question_data: calculated_question_data) q.generate_quiz_data q.save qs = q.generate_submission(@student) qs.submission_data = { "question_#{q.quiz_data[0][:id]}" => "<em>short_answer</em>", "question_#{q.quiz_data[1][:id]}_10ca8479f89652b254a5c6ec90ab9ab8" => "<em>fimb</em>", "question_#{q.quiz_data[2][:id]}" => "<em>essay</em>", "question_#{q.quiz_data[3][:id]}" => "<em>numerical</em>", "question_#{q.quiz_data[4][:id]}" => "<em>calculated</em>", } Quizzes::SubmissionGrader.new(qs).grade_submission stats = CSV.parse(csv({}, q)) expect(stats.last[7]).to eq "short_answer" expect(stats.last[9]).to eq "fimb" expect(stats.last[11]).to eq "essay" expect(stats.last[13]).to eq "numerical" # calculated field also includes the values for the variables, something like: # "x=>4.3,y=>21,calculated" # so we'll match instead expect(stats.last[15]).to match(/,calculated$/) end it "does not count teacher preview submissions" do teacher_in_course(active_all: true) q = @course.quizzes.create! q.update_attribute(:published_at, Time.now) q.quiz_questions.create!(question_data: { :name => "q1", :points_possible => 1, "question_type" => "multiple_choice_question", "answers" => [{ "answer_text" => "", "answer_html" => "<em>zero</em>", "answer_weight" => "100" }, { "answer_text" => "", "answer_html" => "<p>one</p>", "answer_weight" => "0" }] }) q.quiz_questions.create!(question_data: { :name => "q2", :points_possible => 1, "question_type" => "multiple_answers_question", "answers" => [{ "answer_text" => "", "answer_html" => "<a href='http://example.com/caturday.gif'>lolcats</a>", "answer_weight" => "100" }, { "answer_text" => "lolrus", "answer_weight" => "100" }] }) q.generate_quiz_data q.save qs = q.generate_submission(@teacher, true) qs.submission_data = { "question_#{q.quiz_data[0][:id]}" => (q.quiz_data[0][:answers][0][:id]).to_s, "question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][0][:id]}" => "1", "question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][1][:id]}" => "1" } Quizzes::SubmissionGrader.new(qs).grade_submission stats = q.statistics expect(stats[:unique_submission_count]).to eq 0 end it "does not show student names for anonymous submissions" do student_in_course(active_all: true) q = @course.quizzes.create! q.update(published_at: Time.zone.now, quiz_type: "survey", anonymous_submissions: true) q.quiz_questions.create!(question_data: { :name => "q1", :points_possible => 1, "question_type" => "multiple_choice_question", "answers" => [{ "answer_text" => "", "answer_html" => "<em>zero</em>", "answer_weight" => "100" }, { "answer_text" => "", "answer_html" => "<p>one</p>", "answer_weight" => "0" }] }) q.generate_quiz_data q.save qs = q.generate_submission(@student) qs.submission_data = { "question_#{q.quiz_data[0][:id]}" => (q.quiz_data[0][:answers][0][:id]).to_s } Quizzes::SubmissionGrader.new(qs).grade_submission stats = q.statistics expect(stats[:questions].first.last[:user_ids].first).to be_nil end it "does not count student view submissions" do @course = course_factory(active_all: true) fake_student = @course.student_view_student q = @course.quizzes.create! q.update_attribute(:published_at, Time.now) q.quiz_questions.create!(question_data: { :name => "q1", :points_possible => 1, "question_type" => "multiple_choice_question", "answers" => [{ "answer_text" => "", "answer_html" => "<em>zero</em>", "answer_weight" => "100" }, { "answer_text" => "", "answer_html" => "<p>one</p>", "answer_weight" => "0" }] }) q.quiz_questions.create!(question_data: { :name => "q2", :points_possible => 1, "question_type" => "multiple_answers_question", "answers" => [{ "answer_text" => "", "answer_html" => "<a href='http://example.com/caturday.gif'>lolcats</a>", "answer_weight" => "100" }, { "answer_text" => "lolrus", "answer_weight" => "100" }] }) q.generate_quiz_data q.save qs = q.generate_submission(fake_student) qs.submission_data = { "question_#{q.quiz_data[0][:id]}" => (q.quiz_data[0][:answers][0][:id]).to_s, "question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][0][:id]}" => "1", "question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][1][:id]}" => "1" } Quizzes::SubmissionGrader.new(qs).grade_submission stats = q.statistics expect(stats[:unique_submission_count]).to eq 0 end describe "question statistics" do subject { Quizzes::QuizStatistics::StudentAnalysis.new({}) } it "proxies to CanvasQuizStatistics for supported questions" do question_data = { question_type: "essay_question" } responses = [] expect(CanvasQuizStatistics).to receive(:analyze) .with(question_data, responses) .and_return({ some_metric: 5 }) output = subject.send(:stats_for_question, question_data, responses, false) expect(output).to eq({ question_type: "essay_question", some_metric: 5 }) end it "does not proxy if the legacy flag is on" do question_data = { question_type: "essay_question", answers: [] } expect(CanvasQuizStatistics).not_to receive(:analyze) subject.send(:stats_for_question, question_data, []) end end end
CANCEL
Name
Type
Size
Modified
Actions
↩ ..
DIR
—
—
📁 item_analysis/
DIR
—
2023-09-24 03:36
📄 common.rb
RB
1.3 KB
2023-09-24 03:36
EDIT
📄 item_analysis_spec.rb
RB
4.9 KB
2023-09-24 03:36
EDIT
📄 student_analysis_spec.rb
RB
26.1 KB
2023-09-24 03:36
EDIT