Skip to content

Instantly share code, notes, and snippets.

Created July 20, 2010 20:47
Show Gist options
  • Save anonymous/483563 to your computer and use it in GitHub Desktop.
Save anonymous/483563 to your computer and use it in GitHub Desktop.
class Answer < ActiveRecord::Base
###############################
# #
# extend/include declarations #
# #
###############################
include DateUtils
include AuditMixin
extend AnswerSearchExt
####################################
# #
# Non ActiveRecord meta statements #
# #
####################################
#this is present in ActivitySearchExt
#defines search metadata
add_search_metadata
schema_validations :only => []
##########################
# #
# Attribute declarations #
# #
##########################
AUDIT_DELETE = "delete"
AUDIT_CREATE = "create"
AUDIT_UPDATE = "update"
AUDIT_OLD_FILE_MESSAGE = "old file changed"
AUDIT_NEW_FILE_MESSAGE = "new file changed"
VALUE_COLUMN_NAMES = %w(value date_value boolean_value integer_value float_value)
############################
# #
# Association declarations #
# #
############################
belongs_to :question
belongs_to :form_response, :inverse_of => :sti_answers
has_one :folder, :as => :attached_to, :dependent => :destroy
has_many :word_clouds, :dependent => :destroy
#########################
# #
# Callback declarations #
# #
#########################
# IMPORTANT - For this group of callbacks, the auditing callbacks must occur upon creation, update or deletion (unless these are prevented via a callback)
# or auditing will not behave properly. If a CRUD activity moves forward, any callback in the chain must then not prevent the auditing
# callbacks from occurring.
after_save :create_word_cloud
before_save :clear_unrelated_values
before_update :check_for_updating_freshness_record_on_update
after_update :audit_update
before_create :check_for_updating_freshness_record_on_create
after_create :audit_creation
before_destroy :check_for_updating_freshness_record_on_destroy
after_destroy :audit_deletion
after_create :generate_folder
###########################
# #
# Validation declarations #
# #
###########################
validates_presence_of :tabular_data_id
validates_presence_of :question_id
############################
# #
# Named Scope declarations #
# #
############################
named_scope :by_position, :order => :tabular_data_id
named_scope :in_question_group, lambda { | question_group |
{
:conditions => { :questions => {:question_group_id => question_group } },
:joins =>:question
}
}
named_scope :with_form_response, lambda { | form_response |
{ :conditions => { :form_response_id => form_response } }
}
named_scope :in_row, lambda { | row_number |
{
:conditions => { :tabular_data_id => row_number }
}
}
named_scope :for_question, lambda { | question |
{
:conditions => { :question_id => question }
}
}
named_scope :for_person, lambda { |person|
{
:conditions => { :form_response_id => person.custom_fields_response }
}
}
named_scope :in_question_groups, lambda { | group_ids |
{
:joins => %Q{
INNER JOIN questions ON questions.id = answers.question_id
INNER JOIN form_responses ON ( answers.form_response_id = form_responses.id AND
( owner_type <> 'OnboardingRequest' OR owner_type is null))
LEFT OUTER JOIN question_choices ON answers.value = question_choices.value
AND answers.question_id = question_choices.question_id
},
:order => "questions.question_group_id,question_id,tabular_data_id",
:select=> %Q{answers.*,questions.question_group_id as group_id,
question_choices.label as label,questions.type as question_type,answers.tabular_data_id as row_number
},
:conditions => {
:questions => { :question_group_id => group_ids }
}
}
}
# Folder is created on creation
private :create_folder
class << self
%w(edit view).each do |action|
define_method action do | user |
group_ids = QuestionGroupPermission.send("#{action}able_by",user).collect(&:question_group_id)
in_question_groups(group_ids)
end
end
end
def value_object
case self.question
when DateQuestion
value.nil? ? date_value : value
when NumberQuestion
value.nil? ? float_value : value
when FileDownloadQuestion
integer_value
else
value
end
end
def to_export_value
value
end
def float_value
raw_value = read_attribute(:float_value)
raw_value.try(:to_i).try(:to_f) == raw_value ? raw_value.try(:to_i) : raw_value
end
def float_value=(raw_value)
self[:value] = raw_value
self[:float_value] = is_float?(raw_value) ? raw_value : nil
end
def is_float?(raw_value)
Kernel.Float(raw_value) rescue false
end
def date_value=(value)
self[:value] = value
self[:date_value] = value
end
def date_value_ui
return @date_value_ui if @date_value_ui
date_value.to_s($Form_Date_Format) if self.date_value
end
def date_value_ui=(input)
@date_value_ui = input
if input.blank?
self.date_value = nil
elsif(input.is_a?(Date))
self.date_value = input
elsif(input.is_a?(String))
self.date_value = parse_the_date(input.strip)
end
rescue ArgumentError
@due_date_invalid = true
end
def value=(value)
self[:value] = value
self[value_column] = value
end
def jqgrid_value
case self.question
when DateQuestion
date_value.try(:strftime,'%m/%d/%Y')
when NumberQuestion
float_value.to_s
when FileUploadQuestion
file = folder.try(:uploaded_files).try(:first)
return nil unless file
{ :name => file.filename, :url => "/folders/#{file.folder.id}/uploaded_files/#{file.id}" }
else
value
end
end
def value_column
self.class.value_column_for(question)
end
def self.value_column_for(question)
case question
when NumberQuestion
:float_value
when DateQuestion
:date_value
when FileDownloadQuestion
:integer_value
else
:value
end
end
def unrelated_column_names
VALUE_COLUMN_NAMES.reject {|name| name == value_column.to_s }
end
# TODO - Eventually this should be removed. We should keep the text version of the answer always. See the answer subclasses.
def clear_unrelated_values
unrelated_column_names.each {|name| self[name] = nil }
end
def folder_and_file_paths(parent_path)
paths = []
if (form_response.form.anonymous? || form_response.owner.nil?)
answer_by = ""
else
answer_by = " &nbsp &nbsp #{form_response.owner.full_name}"
end
if folder && folder.uploaded_files # uploaded files
folder.uploaded_files.each do |file|
paths << {:path =>parent_path, :file => file, :answer_by => answer_by}
end
else # downloaded files
begin
file = UploadedFile.find(integer_value)
paths << {:path =>parent_path, :file => file, :answer_by => answer_by}
rescue
logger.debug("Downloaded file #{integer_value} not found")
end
end
paths
end
def self.question_line_graph_data(question_id, total)
raw_question = Question.find_by_id(question_id)
case raw_question
when NumberQuestion,DateQuestion
klass_name = raw_question.class.to_s.tableize.singularize
Answer.send("#{klass_name}_line_graph_data",question_id,total)
else
[]
end
end
def self.date_question_line_graph_data(question_id, total)
max = Answer.for_question(question_id).maximum(:date_value)
min = Answer.for_question(question_id).minimum(:date_value)
result = {}
#y labels for graph
y_slice = (total/4.to_f).round
y_labels=[0,y_slice, 2*y_slice, 3*y_slice, 4*y_slice ]
result["y_labels"] = y_labels
result["y_max"] = 4*y_slice
format = '%m/%d/%y' # date format
if (min == max)
result["x_labels"] = [ min.try(:strftime,format) ]
result["data"] = [ Answer.for_question(question_id).scoped_by_date_value(min).count ]
return result
end
slice = (DateUtils.seconds_for(min,max)/5).to_f.round
# calculate x data points
x1 = min.since(slice)
x2 = min.since(2*slice)
x3 = min.since(3*slice)
x4 = min.since(4*slice)
x5 = min.since(5*slice)
# X axis chart labels
labels = []
labels << min.since((slice/2).round).strftime(format)
labels << x1.since((slice/2).round).strftime(format)
labels << x2.since((slice/2).round).strftime(format)
labels << x3.since((slice/2).round).strftime(format)
labels << x4.since((slice/2).round).strftime(format)
result["x_labels"] = labels
# calculate y data points
y1 = Answer.find_date_answer_count_for(question_id, [min, x1])
y2 = Answer.find_date_answer_count_for(question_id, [x1,x2])
y3 = Answer.find_date_answer_count_for( question_id, [x2,x3])
y4 = Answer.find_date_answer_count_for(question_id, [x3,x4])
#hack: adding 1 to take care of the case when x5 = max value and query checks for value < max
# if I don't add, last value won't be counted.
# If I change query to <=, then result will be counted in 2 bar chart
y5 = Answer.find_date_answer_count_for(question_id, [x4, x5+1.day])
result["data"] = [y1,y2,y3,y4,y5]
result["range"] = [min,max]
result
end
# Calculates data for number question/answer frequency chart
def self.number_question_line_graph_data(question_id, total)
max = Answer.for_question(question_id).maximum(:float_value)
min = Answer.for_question(question_id).minimum(:float_value)
result = {}
#y labels for graph
y_slice = (total/4.to_f).round
y_labels=[0,y_slice, 2*y_slice, 3*y_slice, 4*y_slice ]
result["y_labels"]=y_labels
result["y_max"] = 4*y_slice
if (min == max)
result["x_labels"] = [min]
result["data"] = [ Answer.for_question(question_id).scoped_by_float_value(max).count ]
return result
end
# calculate equal slice
slice = (((max-min)/5).to_f).round
x1 = min+slice
x2 = min+2*slice
x3 = min+3*slice
x4 = min+4*slice
x5 = min+5*slice
# x labels fpr graph
labels = [(min+slice/2).round,
(x1+(slice/2)).round,
(x2+(slice/2)).round,
(x3+(slice/2)).round,
(x4+(slice/2)).round ]
result["x_labels"]=labels
# calculate y data points
y1 = Answer.find_number_answer_count_for(question_id, [min, x1])
y2 = Answer.find_number_answer_count_for(question_id, [x1,x2])
y3 = Answer.find_number_answer_count_for( question_id, [x2,x3])
y4 = Answer.find_number_answer_count_for(question_id, [x3,x4])
#hack: adding 1 to take care of the case when x5 = max value and query checks for value < max
# if I don't add, last value won't be counted.
# If I change query to <=, then result will be counted in 2 bar chart
y5 = Answer.find_number_answer_count_for(question_id, [x4,x5+1])
result["data"] = [y1,y2,y3,y4,y5]
result["range"] = [min,max]
result
end
def self.find_number_answer_count_for(question_id, range)
Answer.scoped_by_question_id(question_id).scoped({
:conditions =>['float_value >= ? and float_value < ?',range[0],range[1]]
}).count
end
def self.find_date_answer_count_for(question_id, range)
Answer.scoped_by_question_id(question_id).scoped({
:conditions =>[ 'date_value >= ? and date_value < ?',range[0],range[1]]
}).count
end
# Delete existing words for the answer and create new words for answers
def create_word_cloud
# DEV-3934: THIS IS KILLING PERFORMANCE!!!
# Solution: We don't need word clouds for issue/line item answers
if (question.is_a?(TextAreaQuestion) || question.is_a?(TextFieldQuestion)) && !form_response.form.issue?
self.word_clouds.destroy_all
WordCloud.to_words(value).each { |word| self.word_clouds.create(:name => word) }
end
end
def get_actual_ans(format=nil)
case self.question
when DropListQuestion,MultipleChoiceQuestion,RadioListQuestion:
# Use the relationship so callers have a fighting chance to prefetch the relations...
# QuestionChoice.scoped_by_question_id_and_value(question_id,value).first.try(:label)
question.read_only_choices.find_by_value(value).try(:label)
when NumberQuestion:
float_value
when DateQuestion:
date_value.try(:strftime,(format || "%d/%m/%Y"))
when FileUploadQuestion:
folder.try(:uploaded_files).try(:first).try(:filename)
when FileDownloadQuestion:
check_file_downloaded
else
value
end.try(:to_s)
end
def check_file_downloaded
file_status = ''
if question.folder and integer_value != 0
question.folder.uploaded_files.each do|file|
file_status = "#{file.filename } (downloaded)" if file.id == integer_value
end
end
return file_status
end
def to_export_hash
HashWithIndifferentAccess.new(question.export_question_label => value_object)
end
private
#------------------------- AUDIT START -------------------------
def audit_deletion
audit_cf_common(AUDIT_DELETE)
end
def audit_creation
audit_cf_common(AUDIT_CREATE)
end
def audit_update
audit_cf_common(AUDIT_UPDATE)
end
def audit_cf_common(operation)
return if !auditing_necessary?
the_old_value = old_value
the_new_value = new_value(operation)
return if the_old_value == the_new_value
#audit_record = create_audit_record(operation == AUDIT_DELETE ? AUDIT_DELETE : AUDIT_UPDATE)
audit_record = create_audit_record(AUDIT_UPDATE)
AuditRecordField.create(:audit_record_id => audit_record.id, :attribute_name => question.label, :old_value => the_old_value, :new_value => the_new_value,
:old_choice_label => prepare_label(the_old_value), :new_choice_label => prepare_label(the_new_value),
:question_group_label => question.question_group.name,
:question_id => question.id, :question_group_id => question.question_group.id)
end
def associated_with_audited_model?
return false if form_response.nil?
form_response.form_id == 1 || form_response.form_id == 3
end
def audit_entity_class
form_response.owner_type
end
def audit_entity_desc_update
audit_entity.audit_entity_desc_update
end
def audit_entity_id
audit_entity.id
end
def audit_entity_desc_create_or_delete
audit_entity.audit_entity_desc_create_or_delete
end
def audit_entity
klass = Object.const_get(audit_entity_class)
entity = klass.send(:find, form_response.owner_id)
end
def old_value
determine_value(true)
end
def new_value(operation)
operation == AUDIT_DELETE ? nil : determine_value(false)
end
def convert_to_number(raw_value)
raw_value.try(:to_i).try(:to_f) == raw_value ? raw_value.try(:to_i) : raw_value
end
def determine_value(use_was)
case question
when NumberQuestion
use_was ? convert_to_number(float_value_was) : convert_to_number(float_value)
when DateQuestion
use_was ? date_value_was : date_value
when FileUploadQuestion
use_was ? AUDIT_OLD_FILE_MESSAGE : AUDIT_NEW_FILE_MESSAGE
else
use_was ? value_was : value
end.try(:to_s)
end
def prepare_label(the_value)
return nil unless self.question.try(:can_have_choices?)
# Use the relationship so callers have a fighting chance to prefetch the relations...
#QuestionChoice.scoped_by_question_id(self.question_id).scoped_by_value(the_value).collect(&:label).first
question.read_only_choices.find_by_value(value).try(:label)
end
def auditing_necessary?
Community.auditing_enabled? && associated_with_audited_model? && audit_entity_class != "OnboardingRequest"
end
#--------------------------------- AUDIT END -----------------------
def check_for_updating_freshness_record_on_update
record_fresh_data if should_this_record_be_monitored_for_freshness? && has_something_changed?
end
def check_for_updating_freshness_record_on_create
record_fresh_data if should_this_record_be_monitored_for_freshness?
end
def check_for_updating_freshness_record_on_destroy
record_fresh_data if should_this_record_be_monitored_for_freshness?
end
def should_this_record_be_monitored_for_freshness?
self.form_response && Form.is_custom_form?(self.form_response.form_id) && !Rails.env.test?
end
def record_fresh_data
return nil unless self.question
Freshness.make_fresh(self.question.question_group,self.form_response.owner)
end
def has_something_changed?
VALUE_COLUMN_NAMES.select { |field| self.send("#{field}_changed?") }.nil?
end
def required_field
case self.question
when NumberQuestion
"float_value"
when DateQuestion
"date_value"
when FileUploadQuestion,FileDownloadQuestion
nil
else
"value"
end
end
def validate
return true if self.question.instance_of?(Question)
msg = case question
when FileUploadQuestion
unless folder.nil?
invalid_folders = folder.uploaded_files.reject { |file| file.valid? }
invalid_folders.collect { | file| file.errors.full_messages }.flatten.join(" ")
end
else
question.validate_answer_and_report(self.form_response,value_object)
end
errors.add_to_base(msg) unless msg.blank?
end
def generate_folder
if question.respond_to?(:folder) #Only answers for questions with folders need folders
create_folder(:folder => question.folder)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment