app/lib/json_resolver.rb
# encoding: utf-8
class JsonResolver
include CacheTools
@@app_controller = ApplicationController.new
def self.resolve_efficiently(question_ids, root_count, current_user, qs_data = [])
question_ids.map.with_index do |q, idx|
# maximum depth of 5 questions. However, avoid going too deep for
# later questions. For example, the last question never will
# present a subquestion, regardless if it has one. Therefore, no
# need to query for them.
c = root_count - idx - 1
meta = qs_data.empty? ? {} : qs_data[q.id.to_s]
r = JsonResolver.new(q, [c, 5].min, meta)
r.current_user = current_user
tmp = r.resolve
# assert the generated data looks reasonable, otherwise skip it
unless tmp.is_a?(Hash)
raise <<-ERR
JSON for Question #{q.id} returned an array when it should be a Hash
#{PP.pp(q, "")}
ERR
end
tmp
end
end
def initialize(question, max_depth = 5, meta = {})
@q = question
@meta = meta
@max_depth = max_depth
@qjson = {}
end
def current_user=(user)
@current_user = user
end
def resolve
cache = cache_check(qkey, Hash)
return cache if cache
json_for_question
end
def require_per_params(params)
@params = params
end
private
# freezeframe = time of first frame, used for thumbnail
def embed_video_str(opts, width=240, height=240, freezeframe="#t=0.5")
s = "<video width=\"%s\", height=\"%s\" controls=\"controls\" preload=\"metadata\"><source src=\"%s\" type=\"video/mp4\"></video>"
return nil if opts['video_file_link'].nil? || opts['width'].nil? || opts['height'].nil?
url = opts['video_file_link'] + freezeframe
w = opts['width'].nil? || opts['width'] > width ? width.to_s : opts['width'].to_s
h = opts['height'].nil? || opts['height'] > height ? height.to_s : opts['height'].to_s
s % [w, h, url]
end
def json_for_question
@qjson = {
answers: answers,
id: @q.id,
video: !@meta.empty? ? embed_video_str(@meta) : nil,
html: add_newlines(@q.text)
}
# only include these, if they evaluate to true to save bandwidth
add_matrix_validate
add_starred
add_hints
# because subquestions are chosen randomly or roulette like, it’s
# not possible to cache the question if there are subquestions.
cachable = @max_depth > 0 && answers.all? { |a| a[:subquestion].nil? }
Rails.cache.write(qkey, @qjson) if cachable
@qjson
end
def add_matrix_validate
@qjson.merge!({ matrix: 1, matrix_solution: @q.matrix_solution }) if @q.matrix_validate?
end
def add_starred
@qjson.merge!({ starred: 1 }) if @current_user && @current_user.has_starred?(@q)
end
def add_hints
@qjson.merge!({ hints: json_for_hints }) if @q.hints.any?
end
def answers
return @answers if @answers
if @max_depth > 0
ans_qry = Rails.cache.fetch(generate_cache_key(@q.id)) {
# the map forces rails to resolve
@q.answers.includes(:questions, :categories).map { |x| x }
}
else
ans_qry = @q.answers
end
@answers = ans_qry.map { |a| json_for_answer(a) }
end
def json_for_answer(a)
key = generate_cache_key(a.id)
cache = Rails.cache.read(key)
return cache if cache
ajson = {
correct: a.correct? ? 1 : 0,
id: a.id,
html: add_newlines(a.text)
}
subq = get_subquestion_for_answer(a)
ajson[:subqestion] = subq if subq
cacheable = @max_depth > 0 && ajson[:subquestion].nil?
Rails.cache.write(key, ajson) if cacheable
ajson
end
def get_subquestion_for_answer(a)
return nil if @max_depth <= 0
qr = QuestionReachability.new(a.get_all_subquestions)
qr.require_per_params(@params) if @params
sq = qr.get_reachable
return nil if sq.size == 0
sq = sq.sample
JsonResolver.new(sq, @max_depth - 1).resolve
end
def json_for_hints
@q.hints.map do |h|
@@app_controller.instance_variable_set("@hint", h)
@@app_controller.render_to_string(partial: '/hints/render')
end
end
def cache_check(key, expected_class)
cache = Rails.cache.read(key)
return nil unless cache
return cache if cache.is_a?(expected_class)
Rails.cache.delete(key)
raise "Retrieved cache #{qkey}, received #{cache.class} instead of #{exepected_class}"
end
def qkey
generate_cache_key(@q.id)
end
def add_newlines(content)
content.gsub(/(\r\n){2,}|\n{2,}|\r{2,}/, '<br/><br/>')
end
end