Skip to content

Commit

Permalink
Merge pull request #20 from weedySeaDragon/201220-eager-load-all-qa-d…
Browse files Browse the repository at this point in the history
…ependencies

201220 eager load all qa dependencies
  • Loading branch information
weedySeaDragon authored Dec 20, 2020
2 parents e0f0e9a + af420ca commit a6fa6a6
Show file tree
Hide file tree
Showing 10 changed files with 61 additions and 77 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
surveyor (1.6.7)
surveyor (1.6.8)
formtastic (>= 2.2.1)
haml (>= 4.0)
mustache (~> 0.99)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ based on the HeH version of the surveyor gem, these are my modifications to upda

### Status

current version: 1.6.7
current version: 1.6.8

## Requirements

Expand Down
2 changes: 1 addition & 1 deletion lib/surveyor/models/answer_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module AnswerMethods
# Associations
belongs_to :question
has_many :responses
has_many :validations, :dependent => :destroy
has_many :validations, -> { includes validations: :validation_conditions }, :dependent => :destroy
attr_accessible *PermittedParams.new.answer_attributes if defined? ActiveModel::MassAssignmentSecurity

# Validations
Expand Down
2 changes: 1 addition & 1 deletion lib/surveyor/models/question_group_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module QuestionGroupMethods

included do

has_many :questions
has_many :questions, -> { includes answers: { validations: :validation_conditions } }

has_one :dependency

Expand Down
10 changes: 7 additions & 3 deletions lib/surveyor/models/question_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module QuestionMethods
# Associations
belongs_to :survey_section
belongs_to :question_group, dependent: :destroy, required: false
has_many :answers, -> {includes :responses}, dependent: :destroy # it might not always have answers
has_many :answers, -> { includes validations: :validation_conditions } , dependent: :destroy # it might not always have answers
has_one :dependency, dependent: :destroy
belongs_to :correct_answer, class_name: "Answer", dependent: :destroy, required: false
attr_accessible *PermittedParams.new.question_attributes if defined? ActiveModel::MassAssignmentSecurity
Expand Down Expand Up @@ -59,17 +59,21 @@ def mandatory?


def dependent?
self.dependency != nil
self.dependency != nil # FIXME: change to self.dependency.present?
end


# FIXME: replace with self.dependency&.is_met?(response_set)
def triggered?(response_set)
dependent? ? self.dependency.is_met?(response_set) : true
end


def css_class(response_set)
[(dependent? ? "q_dependent" : nil), (triggered?(response_set) ? nil : "q_hidden"), custom_class].compact.join(" ")
puts " ------ "
puts " css_class: #{response_set.inspect}"
puts " q: #{self.inspect}"
puts " ------ "
end


Expand Down
92 changes: 34 additions & 58 deletions lib/surveyor/models/response_set_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ module ResponseSetMethods
belongs_to :survey
belongs_to :user

has_many :responses, -> {includes :answer}, :dependent => :destroy

has_many :responses, -> { includes :answer }, :dependent => :destroy

accepts_nested_attributes_for :responses, :allow_destroy => true
attr_accessible *PermittedParams.new.response_set_attributes if defined? ActiveModel::MassAssignmentSecurity
Expand All @@ -31,152 +30,131 @@ module ClassMethods
def has_blank_value?(hash)
return true if hash["answer_id"].blank?
return false if (q = Question.find_by_id(hash["question_id"])) and q.pick == "one"
hash.any? {|k, v| v.is_a?(Array) ? v.all? {|x| x.to_s.blank?} : v.to_s.blank?}
hash.any? { |k, v| v.is_a?(Array) ? v.all? { |x| x.to_s.blank? } : v.to_s.blank? }
end
end


def ensure_start_timestamp
self.started_at ||= Time.now
end


def ensure_identifiers
self.access_code ||= Surveyor::Common.make_tiny_code
self.api_id ||= Surveyor::Common.generate_api_id
self.api_id ||= Surveyor::Common.generate_api_id
end


def to_csv(access_code = false, print_header = true)
result = Surveyor::Common.csv_impl.generate do |csv|
if print_header
csv << (access_code ? ["response set access code"] : []) +
csv_question_columns.map {|qcol| "question.#{qcol}"} +
csv_answer_columns.map {|acol| "answer.#{acol}"} +
csv_response_columns.map {|rcol| "response.#{rcol}"}
csv_question_columns.map { |qcol| "question.#{qcol}" } +
csv_answer_columns.map { |acol| "answer.#{acol}" } +
csv_response_columns.map { |rcol| "response.#{rcol}" }
end
responses.each do |response|
csv << (access_code ? [self.access_code] : []) +
csv_question_columns.map {|qcol| response.question.send(qcol)} +
csv_answer_columns.map {|acol| response.answer.send(acol)} +
csv_response_columns.map {|rcol| response.send(rcol)}
csv_question_columns.map { |qcol| response.question.send(qcol) } +
csv_answer_columns.map { |acol| response.answer.send(acol) } +
csv_response_columns.map { |rcol| response.send(rcol) }
end
end
result
end


%w(question answer response).each do |model|
define_method "csv_#{model}_columns" do
model.capitalize.constantize.content_columns.map(&:name) - (model == "response" ? [] : %w(created_at updated_at))
end
end


def as_json(options = nil)
template_paths = ActionController::Base.view_paths.collect(&:to_path)
Rabl.render(self, 'surveyor/show.json', :view_path => template_paths, :format => "hash")
end


def complete!
self.completed_at = Time.now
end


def complete?
!completed_at.nil?
end


def correct?
responses.all?(&:correct?)
end


def correctness_hash
{ :questions => Survey.where(id: self.survey_id).includes(sections: :questions).first.sections.map(&:questions).flatten.compact.size,
:responses => responses.compact.size,
:correct => responses.find_all(&:correct?).compact.size
:correct => responses.find_all(&:correct?).compact.size
}
end


def mandatory_questions_complete?
progress_hash[:triggered_mandatory] == progress_hash[:triggered_mandatory_completed]
end


def progress_hash
qs = Survey.where(id: self.survey_id).includes(sections: :questions).first.sections.map(&:questions).flatten
ds = dependencies(qs.map(&:id))
triggered = qs - ds.select {|d| !d.is_met?(self)}.map(&:question)
{ :questions => qs.compact.size,
:triggered => triggered.compact.size,
:triggered_mandatory => triggered.select {|q| q.mandatory?}.compact.size,
:triggered_mandatory_completed => triggered.select {|q| q.mandatory? and is_answered?(q)}.compact.size
qs = Survey.where(id: self.survey_id).includes(sections: :questions).first.sections.map(&:questions).flatten
ds = dependencies(qs.map(&:id))
triggered = qs - ds.select { |d| !d.is_met?(self) }.map(&:question)
{ :questions => qs.compact.size,
:triggered => triggered.compact.size,
:triggered_mandatory => triggered.select { |q| q.mandatory? }.compact.size,
:triggered_mandatory_completed => triggered.select { |q| q.mandatory? and is_answered?(q) }.compact.size
}
end


def is_answered?(question)
%w(label image).include?(question.display_type) or !is_unanswered?(question)
end


def is_unanswered?(question)
self.responses.detect {|r| r.question_id == question.id}.nil?
self.responses.detect { |r| r.question_id == question.id }.nil?
end


def is_group_unanswered?(group)
group.questions.any? {|question| is_unanswered?(question)}
group.questions.any? { |question| is_unanswered?(question) }
end


# Returns the number of response groups (count of group responses enterted) for this question group
def count_group_responses(questions)
questions.map {|q|
responses.select {|r|
questions.map { |q|
responses.select { |r|
(r.question_id.to_i == q.id.to_i) && !r.response_group.nil?
}.group_by(&:response_group).size
}.max
end


def unanswered_dependencies
unanswered_question_dependencies + unanswered_question_group_dependencies
end


def unanswered_question_dependencies
dependencies.select {|d| d.question && self.is_unanswered?(d.question) && d.is_met?(self)}.map(&:question)
dependencies.select { |d| d.question && self.is_unanswered?(d.question) && d.is_met?(self) }.map(&:question)
end


def unanswered_question_group_dependencies
dependencies.
select {|d| d.question_group && self.is_group_unanswered?(d.question_group) && d.is_met?(self)}.
map(&:question_group)
select { |d| d.question_group && self.is_group_unanswered?(d.question_group) && d.is_met?(self) }.
map(&:question_group)
end


def all_dependencies(question_ids = nil)
arr = dependencies(question_ids).partition {|d| d.is_met?(self)}
arr = dependencies(question_ids).partition { |d| d.is_met?(self) }
{
:show => arr[0].map {|d| d.question_group_id.nil? ? "q_#{d.question_id}" : "g_#{d.question_group_id}"},
:hide => arr[1].map {|d| d.question_group_id.nil? ? "q_#{d.question_id}" : "g_#{d.question_group_id}"}
# :invalid => invalid_hash # :question_id => "message" - the questions which are invalid
:show => arr[0].map { |d| d.question_group_id.nil? ? "q_#{d.question_id}" : "g_#{d.question_group_id}" },
:hide => arr[1].map { |d| d.question_group_id.nil? ? "q_#{d.question_id}" : "g_#{d.question_group_id}" }
# :invalid => invalid_hash # :question_id => "message" - the questions which are invalid
}
end


# Check existence of responses to questions from a given survey_section
def no_responses_for_section?(section)
!responses.any? {|r| r.survey_section_id == section.id}
!responses.any? { |r| r.survey_section_id == section.id }
end


# ui_hash = parameters
# the surveryor_gui gem created an answer type: a grid of checkboxes where many checked boxes for 1 questions are allowed
# ( = the grid answer types)
Expand All @@ -190,16 +168,16 @@ def update_from_ui_hash(ui_hash)
api_id = response_hash['api_id']
fail "api_id missing from response #{ord}" unless api_id

existing = Response.where(:api_id => api_id).first
existing = Response.where(:api_id => api_id).first

if self.class.has_blank_value?(response_hash)
existing.destroy if existing
else

updateable_attributes = response_hash.reject {|k, v| k == 'api_id'}
updateable_attributes = response_hash.reject { |k, v| k == 'api_id' }

# this is where we check for an clean up malformed attributes for the answer_id:
cleaned_up_answer_attributes = fix_malformed_answer_ids updateable_attributes
cleaned_up_answer_attributes = fix_malformed_answer_ids updateable_attributes
cleaned_up_answer_attributes['survey_section_id'] = Question.find(cleaned_up_answer_attributes['question_id']).survey_section.id

if existing
Expand All @@ -223,7 +201,6 @@ def update_from_ui_hash(ui_hash)
end
end


protected

def fix_malformed_answer_ids(attribute_params)
Expand All @@ -240,13 +217,12 @@ def fix_malformed_answer_ids(attribute_params)
fixed_params
end


def dependencies(question_ids = nil)
question_ids = survey.sections.map(&:questions).flatten.map(&:id) if responses.blank? and question_ids.blank?
deps = Dependency.includes(:dependency_conditions).where({ :dependency_conditions => { :question_id => question_ids || responses.map(&:question_id) } })
deps = Dependency.joins(:dependency_conditions).where({ dependency_conditions: { question_id: question_ids || responses.map(&:question_id) } })
# this is a work around for a bug in active_record in rails 2.3 which incorrectly eager-loads associatins when a
# condition clause includes an association limiter
deps.each {|d| d.dependency_conditions.reload}
deps.each { |d| d.dependency_conditions.reload }
deps
end
end
Expand Down
16 changes: 9 additions & 7 deletions lib/surveyor/models/survey_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ module SurveyMethods
included do

# Associations
has_many :sections, -> { includes(questions: {answers: :responses}) }, class_name: 'SurveySection', :dependent => :destroy
has_many :sections,
-> { includes(questions: { answers: { validations: :validation_conditions }, dependency: :dependency_conditions } ) },
class_name: 'SurveySection', dependent: :destroy
has_many :response_sets
has_many :translations, :class_name => "SurveyTranslation"
has_many :translations, class_name: "SurveyTranslation"
attr_accessible *PermittedParams.new.survey_attributes if defined? ActiveModel::MassAssignmentSecurity

# Validations
validates_presence_of :title
validates_uniqueness_of :survey_version, :scope => :access_code, :message => "survey with matching access code and version already exists"
validates_uniqueness_of :survey_version, scope: :access_code, message: "survey with matching access code and version already exists"

# Derived attributes
before_save :generate_access_code
Expand Down Expand Up @@ -68,7 +70,7 @@ def deactivate!

def as_json(options = nil)
template_paths = ActionController::Base.view_paths.collect(&:to_path)
Rabl.render(filtered_for_json, 'surveyor/export.json', :view_path => template_paths, :format => "hash")
Rabl.render(filtered_for_json, 'surveyor/export.json', view_path: template_paths, format: "hash")
end

##
Expand All @@ -87,7 +89,7 @@ def generate_access_code
end

def increment_version
surveys = self.class.select(:survey_version).where(:access_code => access_code).order("survey_version DESC")
surveys = self.class.select(:survey_version).where(access_code: access_code).order("survey_version DESC")
next_version = surveys.any? ? surveys.first.survey_version.to_i + 1 : 0

self.survey_version = next_version
Expand All @@ -97,8 +99,8 @@ def increment_version
def translation(locale_symbol)
# TODO cache the counter for translations; need a column
if self.translations.where(locale: locale_symbol.to_s).count > 0
t = self.translations.where(:locale => locale_symbol.to_s).first
{:title => self.title, :description => self.description}.with_indifferent_access.merge(
t = self.translations.where(locale: locale_symbol.to_s).first
{title: self.title, description: self.description}.with_indifferent_access.merge(
t ? YAML.load(t.translation || "{}").with_indifferent_access : {}
)
else
Expand Down
10 changes: 6 additions & 4 deletions lib/surveyor/models/survey_section_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ module SurveySectionMethods

included do
# Associations
has_many :questions, -> { includes answers: :responses }, :dependent => :destroy
has_many :questions,
-> { includes answers: { validations: :validation_conditions }, dependency: :dependency_conditions },
dependent: :destroy
belongs_to :survey
attr_accessible *PermittedParams.new.survey_section_attributes if defined? ActiveModel::MassAssignmentSecurity

Expand All @@ -27,9 +29,9 @@ def default_args

def questions_and_groups
qs = []
questions.each_with_index.map do |q,i|
questions.each_with_index.map do |q, i|
if q.part_of_group?
if (i+1 >= questions.size) or (q.question_group_id != questions[i+1].question_group_id)
if (i + 1 >= questions.size) or (q.question_group_id != questions[i + 1].question_group_id)
q.question_group
end
else
Expand All @@ -39,7 +41,7 @@ def questions_and_groups
end

def translation(locale)
{:title => self.title, :description => self.description}.with_indifferent_access.merge(
{ title: self.title, description: self.description }.with_indifferent_access.merge(
(self.survey.translation(locale)[:survey_sections] || {})[self.reference_identifier] || {}
)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/surveyor/version.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Surveyor
# uses ruby version 2.5.8
VERSION = '1.6.7'
VERSION = '1.6.8'
end
Binary file added surveyor-1.6.8.gem
Binary file not shown.

0 comments on commit a6fa6a6

Please sign in to comment.