class Quby::Compiler::Services::QubyProxy

Constants

HEADERS

Attributes

options[R]
questionnaire[R]

Public Class Methods

new(questionnaire, options) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 20
def initialize(questionnaire, options)
  @questionnaire = questionnaire
  @options = options
end

Private Class Methods

keys_for_score(score) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 406
def self.keys_for_score(score)
  score.map { |subscore| subscore[:key] }
end

Public Instance Methods

generate(seed) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 25
def generate(seed)
  question_titles = generate_question_titles
  d_qtypes = {}
  vars = []
  @hidden_questions = {} # hash containing questions hidden by other questions

  for question in questions_flat
    if question.hidden && question.type != :check_box
      d_qtypes[question.key.to_s] = { depends: :present } unless options[:without_depends]
    end
    unless question.hidden && (question.type == :check_box || question.type == :hidden)
      vars << question.key.to_s
    end

    case question.type
    when :radio, :scale
      handle_scale(question, question_titles, d_qtypes, vars)
    when :select
      d_qtypes[question.key.to_s] = { type: :discrete }
      for option in question.options
        d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "") unless option.placeholder
      end
      update_hidden_questions_for(question)
    when :check_box
      d_qtypes[question.key.to_s] = { type: :check_box }
      question.options.each do |option|
        next if option.inner_title
        vars << option.key.to_s
        if question.hidden
          question_titles[option.key.to_s] = strip_tags question.context_free_title
        end
        value = 1
        option_type = { type: :discrete }
        option_type[value.to_s] = (option.context_free_description || "")
        option_type[:depends] = { values: [value, value.to_s].uniq, variable: option.key.to_s } unless options[:without_depends]
        d_qtypes[option.key.to_s] = option_type
        values = [value, value.to_s].uniq
        handle_subquestions(question, question_titles, d_qtypes, vars, option, values, option.key.to_s)
      end
      update_hidden_questions_for(question, for_checkbox: true)
    when :textarea
      d_qtypes[question.key.to_s] = { type: :text_field }
    when :string, :integer, :float
      handle_textfield(question, d_qtypes)
    when :date
      d_qtypes[question.key.to_s] = question.components.each_with_object({ type: :date }) do |component, hash|
        key = question.send("#{component}_key")
        vars << key.to_s
        hash[component] = key.to_s
      end
    when :hidden
      if question.options.blank? # string
        question_titles[question.key.to_s] = strip_tags question.context_free_title
        vars << question.key.to_s unless vars.include? question.key.to_s
        d_qtypes[question.key.to_s] = { type: :text }
        d_qtypes[question.key.to_s][:depends] = :present unless options[:without_depends]
      else
        no_keys = true
        values = []
        question.options.each do |option|
          if option.value # scale or radio
            vars << question.key.to_s unless vars.include? question.key.to_s
            next if option.inner_title
            d_qtypes[question.key.to_s] ||= { type: :scale }
            values << option.value.to_s
            d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "")
            # TODO: missing sub-questions
          else # check_box
            d_qtypes[question.key.to_s] ||= { type: :check_box }
            no_keys = false
            question_titles[option.key.to_s] = strip_tags question.context_free_title
            vars << option.key.to_s
            value = option.value || 1
            option_type = { type: :discrete }
            option_type[value.to_s] = (option.context_free_description || "")
            option_type[:depends] = { values: [value, value.to_s].uniq, variable: option.key.to_s } unless options[:without_depends]
            d_qtypes[option.key.to_s] = option_type
            # TODO: missing sub-questions
          end
        end
        if no_keys # scale or radio
          d_qtypes[question.key.to_s][:depends] = { values: values, variable: question.key.to_s } unless options[:without_depends]
          question_titles[question.key.to_s] = strip_tags question.context_free_title
        end
      end
    else
      fail "WARNING: Unimplemented type #{question.type}."
    end

    update_dqtypes_depends(d_qtypes, question, options)
  end

  strip_question_number_slashes(question_titles)
  seed["quests"] = sort_nested_hash(question_titles)
  seed["d_qtypes"] = sort_nested_hash(d_qtypes)
  seed["name"] = questionnaire.title
  seed["short_description"] = questionnaire.short_description unless questionnaire.short_description.blank?
  seed["description"] = questionnaire.description unless questionnaire.description.blank?

  # this approach preserves the order of vars as much as possible, adding new vars to the end of the list
  old_vars = (seed["vars"]&.split(",") || []).map(&:to_s)
  new_vars = vars.map(&:to_s)
  seed["vars"] = ((old_vars & new_vars) | new_vars).join(",")

  scores = process_scores

  seed["properties"] ||= {}
  # headers outcome (humanized)
  seed["properties"][:score_headers] = scores[:headers]
  # headers data-export
  seed["properties"][:score_keys] = scores[:keys]
  # score names outcome (humanized)
  seed["properties"][:score_labels] = scores[:labels]

  seed["properties"].merge!(@options[:properties]) if @options.key?(:properties)
  seed["properties"] = sort_nested_hash(seed["properties"])

  data = {"key" => seed["key"] || options[:roqua_key] || questionnaire.key, "remote_id" => questionnaire.key}
  attrs = %w(name vars quests d_qtypes properties short_description)
  attrs.sort.each do |name|
    data[name] = seed[name]
  end

  data
end
generate_question_titles() click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 188
def generate_question_titles
  question_titles = {}

  for question in questions_flat
    unless question.hidden && (question.type == :check_box || question.type == :hidden)
      title = question.context_free_title || question.description || ""
      question_titles[question.key.to_s] = strip_tags(title)
    end
  end

  question_titles
end
handle_scale(question, quests, d_qtypes, vars) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 271
def handle_scale(question, quests, d_qtypes, vars)
  d_qtypes[question.key.to_s] = { type: :scale }
  values = []
  update_hidden_questions_for(question)
  for option in question.options
    next if option.inner_title
    d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "")
    values << option.value.to_s
    key = question.key.to_s
    handle_subquestions(question, quests, d_qtypes, vars, option, [option.value.to_s], key)
  end
end
handle_subquestions(question, quests, d_qtypes, vars, option, values, key) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 207
def handle_subquestions(question, quests, d_qtypes, vars, option, values, key)
  option.questions.each do |quest|
    if quest.presentation == :next_to_title && ![:string, :integer, :float].include?(quest.type)
      fail "unsupported title question type"
    end
    case quest.type
    when :string, :integer, :float
      subquestion(question, quests, d_qtypes, vars, quest, values, key)
    when :textarea
      sub_textfield(question, quests, d_qtypes, vars, quest, values, key)
    when :radio
      sub_radio(question, quests, d_qtypes, vars, quest, values, key)
    when :date
      sub_date(question, quests, d_qtypes, vars, quest, values, key)
    else
      fail "Unimplemented type #{quest.type} for sub_question"
    end
  end
end
handle_textfield(question, d_qtypes) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 284
def handle_textfield(question, d_qtypes)
  d_qtypes[question.key.to_s] = { type: :text }
  d_qtypes[question.key.to_s][:label] = question.unit unless question.unit.blank?
end
process_scores() click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 299
def process_scores
  scores_from_schemas
end
questions_flat() click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 201
def questions_flat
  @questions_flat ||= questionnaire.panels.map do |panel|
    panel.items.select { |item| item.is_a? Quby::Compiler::Entities::Question }
  end.flatten.compact
end
scores_from_schemas() click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 303
def scores_from_schemas
  score_headers = [] # headers outcome (humanized name for subscores)
  score_keys    = [] # headers data-export (not all of it, just the score_subscore part, shortened)
  score_labels  = [] # score names outcome (humanized name for score as a whole)

  questionnaire.score_schemas.values.each do |score_schema|
    score_labels << score_schema.label
    score_keys << score_schema.subscore_schemas.map do |subschema|
      hash = {
        key: subschema.key,
        header: subschema.export_key.to_s # a shortened key used as PART OF the csv export column headers
      }
      if subschema.only_for_export
        hash.merge(hidden: true)
      else
        hash
      end
    end

    headers = score_schema.subscore_schemas.map(&:label)
    score_headers += headers - score_headers
  end

  {
    headers: score_headers,
    keys: score_keys,
    labels: score_labels
  }
end
sort_nested_hash(obj) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 390
def sort_nested_hash(obj)
  case obj
  when Hash
    obj.transform_values { |v| sort_nested_hash(v) }
       .sort_by_alphanum { |k, _v| k.to_s }
       .to_h
  when Array
    obj.map { |v| sort_nested_hash(v) }
  else
    obj
  end
end
strip_p_tag(text) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 289
def strip_p_tag(text)
  text.gsub /^<p>(.*)<\/p>\n?$/, '\1'
end
strip_question_number_slashes(quests) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 293
def strip_question_number_slashes(quests)
  quests.transform_values! do |value|
    value&.gsub /^(\s*\d+)\\/, '\1'
  end
end
sub_date(question, quests, d_qtypes, vars, quest, values, key) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 262
def sub_date(question, quests, d_qtypes, vars, quest, values, key)
  d_qtypes[quest.key.to_s] = quest.components.each_with_object({ type: :date }) do |component, hash|
    key = quest.send("#{component}_key")
    vars << key
    hash[component] = key.to_s
  end
  quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
end
sub_radio(question, quests, d_qtypes, vars, quest, values, key) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 250
def sub_radio(question, quests, d_qtypes, vars, quest, values, key)
  d_qtypes[quest.key.to_s] = { type: :scale }
  d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key } unless options[:without_depends]
  quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
  for option in quest.options
    next if option.inner_title
    d_qtypes[quest.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "")
  end
  vars << quest.key.to_s
  update_hidden_questions_for(quest)
end
sub_textfield(question, quests, d_qtypes, vars, quest, values, key) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 243
def sub_textfield(question, quests, d_qtypes, vars, quest, values, key)
  d_qtypes[quest.key.to_s] = { type: :text_field }
  d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key } unless options[:without_depends]
  quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
  vars << quest.key.to_s
end
subquestion(question, quests, d_qtypes, vars, quest, values, key) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 227
def subquestion(question, quests, d_qtypes, vars, quest, values, key)
  d_qtypes[quest.key.to_s] = { type: :text }
  unless options[:without_depends]
    if quest.presentation == :next_to_title
      # make title questons dependent on themselves so we don't have to dig into quby's depends relations
      # which sometimes refer to some of the parent's options, but not always the correct ones
      d_qtypes[quest.key.to_s][:depends] = :present
    else
      d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key }
    end
  end
  d_qtypes[quest.key.to_s][:label] = quest.unit unless quest.unit.blank?
  quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
  vars << quest.key.to_s
end
update_dqtypes_depends(d_qtypes, question, options) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 182
def update_dqtypes_depends(d_qtypes, question, options)
  if hidden = @hidden_questions[question.key.to_s]
    d_qtypes[question.key.to_s][:depends] ||= hidden unless options[:without_depends]
  end
end
update_hidden_questions_for(question, for_checkbox: false) click to toggle source
# File lib/quby/compiler/services/quby_proxy.rb, line 151
def update_hidden_questions_for(question, for_checkbox: false)
  shows = question.options.each_with_object({}) do |option, shows|
    next if option.inner_title
    for key in option.shows_questions
      skey = key.to_s
      if for_checkbox
        # is another checkbox option already showing the target question?
        if shows.key?(skey)
          # then set the target's depends on :present, since we cannot represent depending on multiple variables
          shows[skey] = :present
        else
          shows[skey] = { values: ["1", 1], variable: option.key.to_s }
        end
      else
        shows[skey] ||= { values: [], variable: question.key.to_s }
        shows[skey][:values] |= [option.value.to_s, option.value]
      end
    end
  end
  for skey, show in shows
    # if a different question is already showing the same question, we cannot register a dependency on both questions
    # (the 'variable' attribute accepts only 1 key). Thus it is better to show the question based on presence of
    # an answer instead of on the depended question's answers.
    if @hidden_questions.has_key?(skey)
      @hidden_questions[skey] = :present
    else
      @hidden_questions[skey] = show
    end
  end
end