Created
July 20, 2010 20:47
-
-
Save anonymous/483563 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = "     #{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