/
question_answered_event_extractor.rb
109 lines (96 loc) · 3.96 KB
/
question_answered_event_extractor.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# frozen_string_literal: true
#
# Copyright (C) 2014 - 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/>.
module Quizzes::LogAuditing
# @class QuestionAnsweredEventExtractor
#
# Extracts EVT_QUESTION_ANSWERED events from a submission data construct.
class QuestionAnsweredEventExtractor
EVENT_TYPE = Quizzes::QuizSubmissionEvent::EVT_QUESTION_ANSWERED
RE_QUESTION_ANSWER_FIELD = /^question_(\d+)_?/
SQL_FIND_PREDECESSORS =
<<~SQL.squish
created_at >= :started_at
AND created_at <= :created_at
AND quiz_submission_id = :quiz_submission_id
AND attempt = :attempt
AND event_type = '#{EVENT_TYPE}'
SQL
# Main API. Extract, optimize, and persist an answer event from a given
# submission data construct.
#
# @param [Hash] submission_data
# Similar to what you pass for generating snapshots, which is the payload
# that gets sent by the quiz-taking front-end. This is what gets pushed
# to the /backup endpoint in the quiz sub controller then goes through
# Quizzes::QuizSubmission#backup_submission_data.
#
# @param [Quizzes::QuizSubmission] quiz_submission
#
# @return [Quizzes::QuizSubmissionEvent|NilClass]
# Nothing will be returned/saved if the event is empty after optimizing;
# e.g, it contains no new answers.
def create_event!(submission_data, quiz_submission)
event = build_event(submission_data, quiz_submission)
predecessors = Quizzes::QuizSubmissionEvent.where(SQL_FIND_PREDECESSORS, {
quiz_submission_id: quiz_submission.id,
attempt: event.attempt,
started_at: quiz_submission.started_at,
created_at: event.created_at
}).order("created_at DESC")
if predecessors.any?
optimizer = Quizzes::LogAuditing::QuestionAnsweredEventOptimizer.new
optimizer.run!(event.answers, predecessors)
end
if event.answers.any?
event.tap(&:save!)
end
end
# @internal
def build_event(submission_data, quiz_submission)
submission_data.stringify_keys!
Quizzes::QuizSubmissionEvent.new.tap do |event|
event.event_type = EVENT_TYPE
event.event_data = extract_answers(submission_data, quiz_submission.quiz_data)
event.created_at = Time.now
event.quiz_submission = quiz_submission
event.attempt = submission_data["attempt"]
end
end
protected
def extract_answers(submission_data, quiz_data)
quiz_questions = begin
quiz_question_ids = submission_data.keys.filter_map do |key|
if key =~ RE_QUESTION_ANSWER_FIELD
$1
end
end.uniq
quiz_data.select do |qq|
quiz_question_ids.include?(qq["id"].to_s)
end.map(&:symbolize_keys)
end
quiz_questions.reduce([]) do |answers, qq|
serializer = Quizzes::QuizQuestion::AnswerSerializers.serializer_for(qq)
serializer.override_question_data(qq)
answers << {
"quiz_question_id" => qq[:id].to_s,
"answer" => serializer.deserialize(submission_data, full: true)
}
end
end # extract_answers
end
end