diff --git a/app/catalogs/gad7_catalog.rb b/app/catalogs/gad7_catalog.rb new file mode 100644 index 0000000..252d8b7 --- /dev/null +++ b/app/catalogs/gad7_catalog.rb @@ -0,0 +1,203 @@ +module Gad7Catalog + extend ActiveSupport::Concern + + ### Over the last two weeks, how often have you been bothered by any of the following problems? + + DEFINITION = [ + + ### Feeling nervous, anxious or on edge? + [{ + # 0 Not at all + # 1 Several days + # 2 More than half the days + # 3 Nearly every day + + name: :anxiety, + kind: :select, + inputs: [ + {value: 0, label: "not_at_all", meta_label: "", helper: nil}, + {value: 1, label: "several_days", meta_label: "", helper: nil}, + {value: 2, label: "more_than_half_the_days", meta_label: "", helper: nil}, + {value: 3, label: "nearly_every_day", meta_label: "", helper: nil}, + ] + }], + + ### Not being able to stop or control worrying? + [{ + # 0 Not at all + # 1 Several days + # 2 More than half the days + # 3 Nearly every day + + name: :uncontrollable_worry, + kind: :select, + inputs: [ + {value: 0, label: "not_at_all", meta_label: "", helper: nil}, + {value: 1, label: "several_days", meta_label: "", helper: nil}, + {value: 2, label: "more_than_half_the_days", meta_label: "", helper: nil}, + {value: 3, label: "nearly_every_day", meta_label: "", helper: nil}, + ] + }], + + ### Worrying too much about different things? + [{ + # 0 Not at all + # 1 Several days + # 2 More than half the days + # 3 Nearly every day + + name: :excessive_worry, + kind: :select, + inputs: [ + {value: 0, label: "not_at_all", meta_label: "", helper: nil}, + {value: 1, label: "several_days", meta_label: "", helper: nil}, + {value: 2, label: "more_than_half_the_days", meta_label: "", helper: nil}, + {value: 3, label: "nearly_every_day", meta_label: "", helper: nil}, + ] + }], + + ### Trouble relaxing? + [{ + # 0 Not at all + # 1 Several days + # 2 More than half the days + # 3 Nearly every day + + name: :trouble_relaxing, + kind: :select, + inputs: [ + {value: 0, label: "not_at_all", meta_label: "", helper: nil}, + {value: 1, label: "several_days", meta_label: "", helper: nil}, + {value: 2, label: "more_than_half_the_days", meta_label: "", helper: nil}, + {value: 3, label: "nearly_every_day", meta_label: "", helper: nil}, + ] + }], + + ### Being so restless that it is hard to sit still? + [{ + # 0 Not at all + # 1 Several days + # 2 More than half the days + # 3 Nearly every day + + name: :restlessness, + kind: :select, + inputs: [ + {value: 0, label: "not_at_all", meta_label: "", helper: nil}, + {value: 1, label: "several_days", meta_label: "", helper: nil}, + {value: 2, label: "more_than_half_the_days", meta_label: "", helper: nil}, + {value: 3, label: "nearly_every_day", meta_label: "", helper: nil}, + ] + }], + + ### Becoming easily annoyed or irritable? + [{ + # 0 Not at all + # 1 Several days + # 2 More than half the days + # 3 Nearly every day + + name: :irritability, + kind: :select, + inputs: [ + {value: 0, label: "not_at_all", meta_label: "", helper: nil}, + {value: 1, label: "several_days", meta_label: "", helper: nil}, + {value: 2, label: "more_than_half_the_days", meta_label: "", helper: nil}, + {value: 3, label: "nearly_every_day", meta_label: "", helper: nil}, + ] + }], + + ### Feeling afraid as if something awful might happen? + [{ + # 0 Not at all + # 1 Several days + # 2 More than half the days + # 3 Nearly every day + + name: :impending_doom, + kind: :select, + inputs: [ + {value: 0, label: "not_at_all", meta_label: "", helper: nil}, + {value: 1, label: "several_days", meta_label: "", helper: nil}, + {value: 2, label: "more_than_half_the_days", meta_label: "", helper: nil}, + {value: 3, label: "nearly_every_day", meta_label: "", helper: nil}, + ] + }], + + ] + + SCORE_COMPONENTS = %i( anxiety uncontrollable_worry excessive_worry trouble_relaxing restlessness irritability impending_doom ) + QUESTIONS = DEFINITION.map{|questions| questions.map{|question| question[:name] }}.flatten + COMPLICATIONS = DEFINITION[4].map{|question| question[:name] }.flatten + + included do |base_class| + validate :gad7_response_ranges + def gad7_response_ranges + ranges = [ + [:anxiety, [nil,*0..3]], + [:uncontrollable_worry, [nil,*0..3]], + [:excessive_worry, [nil,*0..3]], + [:trouble_relaxing, [nil,*0..3]], + [:restlessness, [nil,*0..3]], + [:irritability, [nil,*0..3]], + [:impending_doom, [nil,*0..3]], + ] + + ranges.each do |range| + response = gad7_responses.detect{|r| r.name.to_sym == range[0]} + if response and not range[1].include?(response.value) + # TODO add catalog namespace here + # self.errors.messages ||= {} + # self.errors.messages[range[0]] = "no_within_allowed_values" + self.errors.add range[0], "not_within_allowed_values" + end + end + + end + + # validate :hbi_response_booleans + # def hbi_response_booleans + # HbiCatalog::COMPLICATIONS.each do |name| + # response = hbi_responses.detect{|r| r.name.to_sym == name} + + # if response and not [0,1].include? response.value.to_i + # # self.errors.messages ||= {} + # # self.errors.messages[name.to_sym] = "must_be_boolean" + # self.errors.add name.to_sym, "must_be_boolean" + # end + # end + # end + + end + + def gad7_responses + responses.select{|r| r.catalog == "gad7"} + end + + # def valid_hbi_entry? + # return false unless last_6_entries.count == 6 + # !last_6_entries.map{|e| e.filled_hbi_entry?}.include?(false) + # end + def filled_gad7_entry? + valid_responses = gad7_responses.reduce([]) do |accu, response| + accu << response.name.to_sym if response.name and response.value.present? + accu + end + (QUESTIONS-valid_responses) == [] + end + + def complete_gad7_entry? + filled_gad7_entry? + end + + # def setup_hbi_scoring + # end + + def gad7_complications_score + COMPLICATIONS.reduce(0) do |sum, question_name| + sum + (self.send("gad7_#{question_name}").to_i) + end.to_f + end + + +end \ No newline at end of file diff --git a/app/catalogs/sf20_catalog.rb b/app/catalogs/sf20_catalog.rb new file mode 100644 index 0000000..9504f6b --- /dev/null +++ b/app/catalogs/sf20_catalog.rb @@ -0,0 +1,573 @@ +module Sf20Catalog + extend ActiveSupport::Concern + + DEFINITION = [ + + ### SECTION A + + ### In general, would you say your health is: + [{ + # 1 Excellent + # 2 Very good + # 3 Good + # 4 Fair + # 5 Poor + + name: :general_health, + kind: :select, + inputs: [ + {value: 1, label: "excellent", meta_label: "", helper: nil}, + {value: 2, label: "very_good", meta_label: "", helper: nil}, + {value: 3, label: "good", meta_label: "", helper: nil}, + {value: 4, label: "fair", meta_label: "", helper: nil}, + {value: 5, label: "poor", meta_label: "", helper: nil}, + ] + }], + + + ### SECTION B + ### For how long (if at all) has your *health limited you* in *each* of the following activities? + + ### The kinds or amounts of *vigorous* activities you can do, like lifting heavy objects, running or participating in strenuous sports. + [{ + # 1 Limited for more than 3 months + # 2 Limited for 3 months or less + # 3 Not limited at all + + name: :limit_vigorous_activity, + kind: :select, + inputs: [ + {value: 1, label: "limited_more_than_3_months", meta_label: "", helper: nil}, + {value: 2, label: "limited_3_months_or_less", meta_label: "", helper: nil}, + {value: 3, label: "not_limited_at_all", meta_label: "", helper: nil}, + ] + }], + + ### The kinds or amounts of moderate activities you can do, like moving a table, carrying groceries or bowling. + [{ + # 1 Limited for more than 3 months + # 2 Limited for 3 months or less + # 3 Not limited at all + + name: :limit_moderate_activity, + kind: :select, + inputs: [ + {value: 1, label: "limited_more_than_3_months", meta_label: "", helper: nil}, + {value: 2, label: "limited_3_months_or_less", meta_label: "", helper: nil}, + {value: 3, label: "not_limited_at_all", meta_label: "", helper: nil}, + ] + }], + + + ### Walking uphill or climbing a few flights of stairs. + [{ + # 1 Limited for more than 3 months + # 2 Limited for 3 months or less + # 3 Not limited at all + + name: :limit_climbing_stairs, + kind: :select, + inputs: [ + {value: 1, label: "limited_more_than_3_months", meta_label: "", helper: nil}, + {value: 2, label: "limited_3_months_or_less", meta_label: "", helper: nil}, + {value: 3, label: "not_limited_at_all", meta_label: "", helper: nil}, + ] + }], + + ### Bending, lifting, or stooping? + [{ + # 1 Limited for more than 3 months + # 2 Limited for 3 months or less + # 3 Not limited at all + + name: :limit_bending, + kind: :select, + inputs: [ + {value: 1, label: "limited_more_than_3_months", meta_label: "", helper: nil}, + {value: 2, label: "limited_3_months_or_less", meta_label: "", helper: nil}, + {value: 3, label: "not_limited_at_all", meta_label: "", helper: nil}, + ] + }], + + ### Walking one block? + [{ + # 1 Limited for more than 3 months + # 2 Limited for 3 months or less + # 3 Not limited at all + + name: :limit_walking, + kind: :select, + inputs: [ + {value: 1, label: "limited_more_than_3_months", meta_label: "", helper: nil}, + {value: 2, label: "limited_3_months_or_less", meta_label: "", helper: nil}, + {value: 3, label: "not_limited_at_all", meta_label: "", helper: nil}, + ] + }], + + ### Eating, dressing, bathing, or using the toilet? + [{ + # 1 Limited for more than 3 months + # 2 Limited for 3 months or less + # 3 Not limited at all + + name: :limit_basic_activity, + kind: :select, + inputs: [ + {value: 1, label: "limited_more_than_3_months", meta_label: "", helper: nil}, + {value: 2, label: "limited_3_months_or_less", meta_label: "", helper: nil}, + {value: 3, label: "not_limited_at_all", meta_label: "", helper: nil}, + ] + }], + + ### SECTION C + ### How much *bodily* pain have you had *during the past 4 weeks*? + + [{ + # 1 None + # 2 Very mild + # 3 Mild + # 4 Moderate + # 5 Severe + # 6 Very severe + + name: :bodily_pain, + kind: :select, + inputs: [ + {value: 1, label: "none", meta_label: "", helper: nil}, + {value: 2, label: "very_mild", meta_label: "", helper: nil}, + {value: 3, label: "mild", meta_label: "", helper: nil}, + {value: 4, label: "moderate", meta_label: "", helper: nil}, + {value: 5, label: "severe", meta_label: "", helper: nil}, + {value: 6, label: "very_severe", meta_label: "", helper: nil}, + ] + }], + + ### SECTION D + ### Does your health *keep* you from working at a job, doing work around the house, or going to school? + [{ + # 1 Yes, for more than three months + # 2 Yes, for three months or less + # 3 No + + name: :prevent_working, + kind: :select, + inputs: [ + {value: 1, label: "yes_more_than_3_months", meta_label: "", helper: nil}, + {value: 2, label: "yes_3_months_or_less", meta_label: "", helper: nil}, + {value: 3, label: "no", meta_label: "", helper: nil}, + ] + }], + + ### SECTION E + ### Have you been unable to do *certain kinds or amounts* of work, housework, or schoolwork because of your health? + [{ + # 1 Yes, for more than three months + # 2 Yes, for three months or less + # 3 No + + name: :prevent_certain_kinds_work, + kind: :select, + inputs: [ + {value: 1, label: "yes_more_than_3_months", meta_label: "", helper: nil}, + {value: 2, label: "yes_3_months_or_less", meta_label: "", helper: nil}, + {value: 3, label: "no", meta_label: "", helper: nil}, + ] + }], + + ### SECTION F + ### For *each* of the following questions, please mark the circle for the *one* answer that comes *closest* to the way you have been feeling *during the past month*. + + ### How much of the time, during the past month, has your *health limited your social activities* (like visiting with friends or close relatives)? + [{ + # 1 All of the time + # 2 Most of the time + # 3 A good bit of the time + # 4 Some of the time + # 5 A little of the time + # 6 None of the time + + name: :limit_social_activities, + kind: :select, + inputs: [ + {value: 1, label: "all_of_the_time", meta_label: "", helper: nil}, + {value: 2, label: "most_of_the_time", meta_label: "", helper: nil}, + {value: 3, label: "a_good_bit_of_the_time", meta_label: "", helper: nil}, + {value: 4, label: "some_of_the_time", meta_label: "", helper: nil}, + {value: 5, label: "a_little_of_the_time", meta_label: "", helper: nil}, + {value: 6, label: "none_of_the_time", meta_label: "", helper: nil}, + ] + }], + + ### How much of the time, during the past month, have you been a very nervous person? + [{ + # 1 All of the time + # 2 Most of the time + # 3 A good bit of the time + # 4 Some of the time + # 5 A little of the time + # 6 None of the time + + name: :nervousness, + kind: :select, + inputs: [ + {value: 1, label: "all_of_the_time", meta_label: "", helper: nil}, + {value: 2, label: "most_of_the_time", meta_label: "", helper: nil}, + {value: 3, label: "a_good_bit_of_the_time", meta_label: "", helper: nil}, + {value: 4, label: "some_of_the_time", meta_label: "", helper: nil}, + {value: 5, label: "a_little_of_the_time", meta_label: "", helper: nil}, + {value: 6, label: "none_of_the_time", meta_label: "", helper: nil}, + ] + }], + + ### During the past month, how much of the time have you felt calm and peaceful? + [{ + # 1 All of the time + # 2 Most of the time + # 3 A good bit of the time + # 4 Some of the time + # 5 A little of the time + # 6 None of the time + + name: :calmness, + kind: :select, + inputs: [ + {value: 1, label: "all_of_the_time", meta_label: "", helper: nil}, + {value: 2, label: "most_of_the_time", meta_label: "", helper: nil}, + {value: 3, label: "a_good_bit_of_the_time", meta_label: "", helper: nil}, + {value: 4, label: "some_of_the_time", meta_label: "", helper: nil}, + {value: 5, label: "a_little_of_the_time", meta_label: "", helper: nil}, + {value: 6, label: "none_of_the_time", meta_label: "", helper: nil}, + ] + }], + + ### How much of the time, during the past month, have you felt downhearted and blue? + [{ + # 1 All of the time + # 2 Most of the time + # 3 A good bit of the time + # 4 Some of the time + # 5 A little of the time + # 6 None of the time + + name: :downheartedness, + kind: :select, + inputs: [ + {value: 1, label: "all_of_the_time", meta_label: "", helper: nil}, + {value: 2, label: "most_of_the_time", meta_label: "", helper: nil}, + {value: 3, label: "a_good_bit_of_the_time", meta_label: "", helper: nil}, + {value: 4, label: "some_of_the_time", meta_label: "", helper: nil}, + {value: 5, label: "a_little_of_the_time", meta_label: "", helper: nil}, + {value: 6, label: "none_of_the_time", meta_label: "", helper: nil}, + ] + }], + + ### During the past month, how much of the time have you been a happy person? + [{ + # 1 All of the time + # 2 Most of the time + # 3 A good bit of the time + # 4 Some of the time + # 5 A little of the time + # 6 None of the time + + name: :happiness, + kind: :select, + inputs: [ + {value: 1, label: "all_of_the_time", meta_label: "", helper: nil}, + {value: 2, label: "most_of_the_time", meta_label: "", helper: nil}, + {value: 3, label: "a_good_bit_of_the_time", meta_label: "", helper: nil}, + {value: 4, label: "some_of_the_time", meta_label: "", helper: nil}, + {value: 5, label: "a_little_of_the_time", meta_label: "", helper: nil}, + {value: 6, label: "none_of_the_time", meta_label: "", helper: nil}, + ] + }], + + ### How often, during the past month, have you felt so down in the dumps that nothing could cheer you up? + [{ + # 1 All of the time + # 2 Most of the time + # 3 A good bit of the time + # 4 Some of the time + # 5 A little of the time + # 6 None of the time + + name: :despair, + kind: :select, + inputs: [ + {value: 1, label: "all_of_the_time", meta_label: "", helper: nil}, + {value: 2, label: "most_of_the_time", meta_label: "", helper: nil}, + {value: 3, label: "a_good_bit_of_the_time", meta_label: "", helper: nil}, + {value: 4, label: "some_of_the_time", meta_label: "", helper: nil}, + {value: 5, label: "a_little_of_the_time", meta_label: "", helper: nil}, + {value: 6, label: "none_of_the_time", meta_label: "", helper: nil}, + ] + }], + + ### SECTION G + ### Please mark the circle that *best* describes whether *each* of the following statements is *true* or *false* for you. + + ### I am somewhat ill. + [{ + # 1 Definitely true + # 2 Mostly true + # 3 Not sure + # 4 Mostly false + # 5 Definitely false + + name: :somewhat_ill, + kind: :select, + inputs: [ + {value: 1, label: "definitely_true", meta_label: "", helper: nil}, + {value: 2, label: "mostly_true", meta_label: "", helper: nil}, + {value: 3, label: "not_sure", meta_label: "", helper: nil}, + {value: 4, label: "mostly_false", meta_label: "", helper: nil}, + {value: 5, label: "definitely_false", meta_label: "", helper: nil}, + ] + }], + + ### I am as healthy as anybody I know. + [{ + # 1 Definitely true + # 2 Mostly true + # 3 Not sure + # 4 Mostly false + # 5 Definitely false + + name: :healthy_as_anybody, + kind: :select, + inputs: [ + {value: 1, label: "definitely_true", meta_label: "", helper: nil}, + {value: 2, label: "mostly_true", meta_label: "", helper: nil}, + {value: 3, label: "not_sure", meta_label: "", helper: nil}, + {value: 4, label: "mostly_false", meta_label: "", helper: nil}, + {value: 5, label: "definitely_false", meta_label: "", helper: nil}, + ] + }], + + ### My health is excellent. + [{ + # 1 Definitely true + # 2 Mostly true + # 3 Not sure + # 4 Mostly false + # 5 Definitely false + + name: :excellent_health, + kind: :select, + inputs: [ + {value: 1, label: "definitely_true", meta_label: "", helper: nil}, + {value: 2, label: "mostly_true", meta_label: "", helper: nil}, + {value: 3, label: "not_sure", meta_label: "", helper: nil}, + {value: 4, label: "mostly_false", meta_label: "", helper: nil}, + {value: 5, label: "definitely_false", meta_label: "", helper: nil}, + ] + }], + + ### I have been feeling bad lately. + [{ + # 1 Definitely true + # 2 Mostly true + # 3 Not sure + # 4 Mostly false + # 5 Definitely false + + name: :feeling_bad_lately, + kind: :select, + inputs: [ + {value: 1, label: "definitely_true", meta_label: "", helper: nil}, + {value: 2, label: "mostly_true", meta_label: "", helper: nil}, + {value: 3, label: "not_sure", meta_label: "", helper: nil}, + {value: 4, label: "mostly_false", meta_label: "", helper: nil}, + {value: 5, label: "definitely_false", meta_label: "", helper: nil}, + ] + }], + + ] + + QUESTIONS = %i( general_health limit_vigorous_activity limit_moderate_activity limit_climbing_stairs limit_bending limit_walking limit_basic_activity bodily_pain prevent_working prevent_certain_kinds_work limit_social_activities nervousness calmness downheartedness happiness despair somewhat_ill healthy_as_anybody excellent_health feeling_bad_lately ) + SCORE_COMPONENTS = %i( section_a section_b section_c section_d section_e section_f section_g) + SCORE_COMPONENT_QUESTIONS = [1,6,1,1,1,6,4] + QUESTIONS = DEFINITION.map{|questions| questions.map{|question| question[:name] }}.flatten + COMPLICATIONS = DEFINITION[4].map{|question| question[:name] }.flatten + SECTION_SCALE_MAX = 3 + + included do |base_class| + validate :sf20_response_ranges + def sf20_response_ranges + ranges = [ + [:general_health, [nil,*1..5]], + [:limit_vigorous_activity, [nil,*1..3]], + [:limit_moderate_activity, [nil,*1..3]], + [:limit_climbing_stairs, [nil,*1..3]], + [:limit_bending, [nil,*1..3]], + [:limit_walking, [nil,*1..3]], + [:limit_basic_activity, [nil,*1..3]], + [:bodily_pain, [nil,*1..6]], + [:prevent_working, [nil,*1..3]], + [:prevent_certain_kinds_work, [nil,*1..3]], + [:limit_social_activities, [nil,*1..6]], + [:nervousness, [nil,*1..6]], + [:calmness, [nil,*1..6]], + [:downheartedness, [nil,*1..6]], + [:happiness, [nil,*1..6]], + [:despair, [nil,*1..6]], + [:somewhat_ill, [nil,*1..5]], + [:healthy_as_anybody, [nil,*1..5]], + [:excellent_health, [nil,*1..5]], + [:feeling_bad_lately, [nil,*1..5]], + ] + + ranges.each do |range| + response = sf20_responses.detect{|r| r.name.to_sym == range[0]} + if response and not range[1].include?(response.value) + # TODO add catalog namespace here + # self.errors.messages ||= {} + # self.errors.messages[range[0]] = "no_within_allowed_values" + self.errors.add range[0], "not_within_allowed_values" + end + end + + end + + # validate :hbi_response_booleans + # def hbi_response_booleans + # HbiCatalog::COMPLICATIONS.each do |name| + # response = hbi_responses.detect{|r| r.name.to_sym == name} + + # if response and not [0,1].include? response.value.to_i + # # self.errors.messages ||= {} + # # self.errors.messages[name.to_sym] = "must_be_boolean" + # self.errors.add name.to_sym, "must_be_boolean" + # end + # end + # end + + end + + def sf20_responses + responses.select{|r| r.catalog == "sf20"} + end + + # def valid_hbi_entry? + # return false unless last_6_entries.count == 6 + # !last_6_entries.map{|e| e.filled_hbi_entry?}.include?(false) + # end + def filled_sf20_entry? + valid_responses = sf20_responses.reduce([]) do |accu, response| + accu << response.name.to_sym if response.name and response.value.present? + accu + end + (QUESTIONS-valid_responses) == [] + end + + def complete_sf20_entry? + filled_sf20_entry? + end + + # def setup_hbi_scoring + # end + + def invert(n, domain_min, domain_max) + domain_max + domain_min - n + end + + def questions_for_section(section_name) + questions = QUESTIONS + SCORE_COMPONENT_QUESTIONS.reduce([]) do |memo,length| + section = questions[0..length-1] + memo << section + questions = questions - section + memo + end[('a'..'z').to_a.index(section_name)] + end + + def scale_score(score, domain_min, domain_max, range_max) + domain_extent = domain_max - domain_min + range_max * (score - domain_min) / domain_extent + end + + def sf20_section_a_score + general_health_recoding = {1 => 1, 2 => 1.64, 3 => 2.59, 4 => 4.01, 5 => 5} # they made the items not equidistant + + questions = questions_for_section("a") + avg = questions.reduce(0) do |sum, name| + sum + general_health_recoding[self.send("sf20_#{name}").round] # round because we need to use it as a hash key + end / questions.length + + scaled = scale_score(avg, 1, 5, SECTION_SCALE_MAX) + scaled.round + end + + def sf20_section_b_score + questions = questions_for_section("b") + avg = questions.reduce(0) do |sum, name| + sum + self.send("sf20_#{name}") + end / questions.length + + scaled = scale_score(avg, 1, 3, SECTION_SCALE_MAX) + scaled.round + end + + def sf20_section_c_score + questions = questions_for_section("c") + avg = questions.reduce(0) do |sum, name| + sum + self.send("sf20_#{name}") + end / questions.length + + scaled = scale_score(avg, 1, 6, SECTION_SCALE_MAX) + scaled.round + end + + def sf20_section_d_score + questions = questions_for_section("d") + avg = questions.reduce(0) do |sum, name| + sum + invert(self.send("sf20_#{name}"), 1, 3) + end / questions.length + + scaled = scale_score(avg, 1, 3, SECTION_SCALE_MAX) + scaled.round + end + + def sf20_section_e_score + questions = questions_for_section("e") + avg = questions.reduce(0) do |sum, name| + sum + invert(self.send("sf20_#{name}"), 1, 3) + end / questions.length + + scaled = scale_score(avg, 1, 3, SECTION_SCALE_MAX) + scaled.round + end + + def sf20_section_f_score + questions_to_invert = [:calmness, :happiness] + + questions = questions_for_section("f") + avg = questions.reduce(0) do |sum, name| + if questions_to_invert.include?(name) + sum + invert(self.send("sf20_#{name}"), 1, 6) + else + sum + self.send("sf20_#{name}") + end + end / questions.length + + scaled = scale_score(avg, 1, 6, SECTION_SCALE_MAX) + scaled.round + end + + def sf20_section_g_score + questions_to_invert = [:healthy_as_anybody, :excellent_health] + + questions = questions_for_section("g") + avg = questions.reduce(0) do |sum, name| + if questions_to_invert.include?(name) + sum + invert(self.send("sf20_#{name}"), 1, 5) + else + sum + self.send("sf20_#{name}") + end + end / questions.length + + scaled = scale_score(avg, 1, 5, SECTION_SCALE_MAX) + scaled.round + end + +end \ No newline at end of file diff --git a/config/initializers/catalog_conditions.rb b/config/initializers/catalog_conditions.rb index 935b339..d3fc291 100644 --- a/config/initializers/catalog_conditions.rb +++ b/config/initializers/catalog_conditions.rb @@ -1,5 +1,6 @@ CATALOG_CONDITIONS = { "Crohn's disease" => "hbi", "Rheumatoid arthritis" => "rapid3", - "Depression" => "phq9" + "Depression" => "phq9", + "Generalized anxiety disorder" => "gad7", } \ No newline at end of file diff --git a/config/locales/en/catalogs/gad7.yml b/config/locales/en/catalogs/gad7.yml new file mode 100644 index 0000000..7ec48f7 --- /dev/null +++ b/config/locales/en/catalogs/gad7.yml @@ -0,0 +1,36 @@ +catalog_description: "Generalized Anxiety Disorder Assessment (GAD-7)" + +section_1_description: "Feeling nervous, anxious or on edge?" +section_1_prompt: "Feeling nervous, anxious or on edge?" +section_1_header: "Over the past two weeks, how often have you been bothered by ..." +anxiety: "Anxiety" + +section_2_description: "Not being able to stop or control worrying?" +section_2_prompt: "Not being able to stop or control worrying?" +section_2_header: "Over the past two weeks, how often have you been bothered by ..." +uncontrollable_worry: "Uncontrollable worry" + +section_3_description: "Worrying too much about different things?" +section_3_prompt: "Worrying too much about different things?" +section_3_header: "Over the past two weeks, how often have you been bothered by ..." +excessive_worry: "Excessive worry" + +section_4_description: "Trouble relaxing?" +section_4_prompt: "Trouble relaxing?" +section_4_header: "Over the past two weeks, how often have you been bothered by ..." +trouble_relaxing: "Trouble relaxing" + +section_5_description: "Being so restless that it is hard to sit still?" +section_5_prompt: "Being so restless that it is hard to sit still?" +section_5_header: "Over the past two weeks, how often have you been bothered by ..." +restlessness: "Restlessness" + +section_6_description: "Becoming easily annoyed or irritable?" +section_6_prompt: "Becoming easily annoyed or irritable?" +section_6_header: "Over the past two weeks, how often have you been bothered by ..." +irritability: "Irritability" + +section_7_description: "Feeling afraid as if something awful might happen?" +section_7_prompt: "Feeling afraid as if something awful might happen?" +section_7_header: "Over the past two weeks, how often have you been bothered by ..." +impending_doom: "A sense of impending doom" diff --git a/config/locales/en/catalogs/sf20.yml b/config/locales/en/catalogs/sf20.yml new file mode 100644 index 0000000..c4d7b0e --- /dev/null +++ b/config/locales/en/catalogs/sf20.yml @@ -0,0 +1,102 @@ +catalog_description: "Medical Outcomes Study (MOS) 20-Item Short Form Survey (SF-20)" + +section_1_description: "In general, would you say your health is:" +section_1_prompt: "In general, would you say your health is:" +section_1_header: "" +general_health: "General health" + +section_2_description: "The kinds or amounts of vigorous activities you can do, like lifting heavy objects, running or participating in strenuous sports?" +section_2_prompt: "The kinds or amounts of vigorous activities you can do, like lifting heavy objects, running or participating in strenuous sports?" +section_2_header: "For how long (if at all) has your health limited you in ..." +limit_vigorous_activity: "Limits on vigorous activity" + +section_3_description: "The kinds or amounts of moderate activities you can do, like moving a table, carrying groceries or bowling?" +section_3_prompt: "The kinds or amounts of moderate activities you can do, like moving a table, carrying groceries or bowling?" +section_3_header: "For how long (if at all) has your health limited you in ..." +limit_moderate_activity: "Limits on moderate activity" + +section_4_description: "Walking uphill or climbing a few flights of stairs?" +section_4_prompt: "Walking uphill or climbing a few flights of stairs?" +section_4_header: "For how long (if at all) has your health limited you in ..." +limit_climbing_stairs: "Limits on climbing stairs" + +section_5_description: "Bending, lifting, or stooping?" +section_5_prompt: "Bending, lifting, or stooping?" +section_5_header: "For how long (if at all) has your health limited you in ..." +limit_bending: "Limits on bending" + +section_6_description: "Walking one block?" +section_6_prompt: "Walking one block?" +section_6_header: "For how long (if at all) has your health limited you in ..." +limit_walking: "Limits on walking" + +section_7_description: "Eating, dressing, bathing, or using the toilet?" +section_7_prompt: "Eating, dressing, bathing, or using the toilet?" +section_7_header: "For how long (if at all) has your health limited you in ..." +limit_basic_activity: "Limits on basic activities" + +section_8_description: "How much bodily pain have you had during the past 4 weeks?" +section_8_prompt: "How much bodily pain have you had during the past 4 weeks?" +section_8_header: "" +bodily_pain: "Bodily pain" + +section_9_description: "Does your health keep you from working at a job, doing work around the house, or going to school?" +section_9_prompt: "Does your health keep you from working at a job, doing work around the house, or going to school?" +section_9_header: "" +prevent_working: "Interfering with working" + +section_10_description: "Have you been unable to do certain kinds or amounts of work, housework, or schoolwork because of your health?" +section_10_prompt: "Have you been unable to do certain kinds or amounts of work, housework, or schoolwork because of your health?" +section_10_header: "" +prevent_certain_kinds_work: "Interfering with certain kinds of work" + +section_11_description: "How much of the time, during the past month, has your health limited your social activities (like visiting with friends or close relatives)?" +section_11_prompt: "How much of the time, during the past month, has your health limited your social activities (like visiting with friends or close relatives)?" +section_11_header: "Which answer comes closest to the way you have been feeling during the past month?" +limit_social_activities: "Limits on social activities" + +section_12_description: "How much of the time, during the past month, have you been a very nervous person?" +section_12_prompt: "How much of the time, during the past month, have you been a very nervous person?" +section_12_header: "Which answer comes closest to the way you have been feeling during the past month?" +nervousness: "Nervousness" + +section_13_description: "During the past month, how much of the time have you felt calm and peaceful?" +section_13_prompt: "During the past month, how much of the time have you felt calm and peaceful?" +section_13_header: "Which answer comes closest to the way you have been feeling during the past month?" +calmness: "Calmness" + +section_14_description: "How much of the time, during the past month, have you felt downhearted and blue?" +section_14_prompt: "How much of the time, during the past month, have you felt downhearted and blue?" +section_14_header: "Which answer comes closest to the way you have been feeling during the past month?" +downheartedness: "Downheartedness" + +section_15_description: "During the past month, how much of the time have you been a happy person?" +section_15_prompt: "During the past month, how much of the time have you been a happy person?" +section_15_header: "Which answer comes closest to the way you have been feeling during the past month?" +happiness: "Happiness" + +section_16_description: "How often, during the past month, have you felt so down in the dumps that nothing could cheer you up?" +section_16_prompt: "How often, during the past month, have you felt so down in the dumps that nothing could cheer you up?" +section_16_header: "Which answer comes closest to the way you have been feeling during the past month?" +despair: "Despair" + +section_17_description: "I am somewhat ill." +section_17_prompt: "I am somewhat ill." +section_17_header: "Is this statement true or false for you?" +somewhat_ill: "Somewhat ill" + +section_18_description: "I am as healthy as anybody I know." +section_18_prompt: "I am as healthy as anybody I know." +section_18_header: "Is this statement true or false for you?" +healthy_as_anybody: "Healthy as anyone I know" + +section_19_description: "My health is excellent." +section_19_prompt: "My health is excellent." +section_19_header: "Is this statement true or false for you?" +excellent_health: "Excellent health" + +section_20_description: "I have been feeling bad lately." +section_20_prompt: "I have been feeling bad lately." +section_20_header: "Is this statement true or false for you?" +feeling_bad_lately: "Feeling bad lately" + diff --git a/config/locales/en/en_base.yml b/config/locales/en/en_base.yml index 220177f..3e965b4 100644 --- a/config/locales/en/en_base.yml +++ b/config/locales/en/en_base.yml @@ -268,6 +268,11 @@ en: severe: "Severe" extreme: "Extreme" + excellent: "Excellent" + very_good: "Very good" + good: "Good" + fair: "Fair" + dubious: "Dubious" definite: "Definite" definite_and_tender: "Definite and tender" @@ -277,6 +282,27 @@ en: more_than_half_the_days: "More than half the days" nearly_every_day: "Nearly every day" + limited_more_than_3_months: "Limited for more than 3 months" + limited_3_months_or_less: "Limited for 3 months or less" + not_limited_at_all: "Not limited at all" + + yes_more_than_3_months: "Yes, for more than 3 months" + yes_3_months_or_less: "Yes, for 3 months or less" + no: "No" + + all_of_the_time: "All of the time" + most_of_the_time: "Most of the time" + a_good_bit_of_the_time: "A good bit of the time" + some_of_the_time: "Some of the time" + a_little_of_the_time: "A little of the time" + none_of_the_time: "None of the time" + + definitely_true: "Definitely true" + mostly_true: "Mostly true" + not_sure: "Not sure" + mostly_false: "Mostly false" + definitely_false: "Definitely false" + helpers: basic_0: "Not active" basic_1: "Not very active" diff --git a/spec/catalogs/sf20_spec.rb b/spec/catalogs/sf20_spec.rb new file mode 100644 index 0000000..1387e83 --- /dev/null +++ b/spec/catalogs/sf20_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' + +describe Sf20Catalog do + + describe "custom score thing" do + let(:user) { create :user } + let!(:entry) { create :sf20_entry } + it "outputs a custom score for section a" do + expect(entry.sf20_section_a_score).to eq 0 + end + it "outputs a custom score for section b" do + expect(entry.sf20_section_b_score).to eq 1 + end + it "outputs a custom score for section c" do + expect(entry.sf20_section_c_score).to eq 3 + end + it "outputs a custom score for section d" do + expect(entry.sf20_section_d_score).to eq 3 + end + it "outputs a custom score for section e" do + expect(entry.sf20_section_e_score).to eq 3 + end + it "outputs a custom score for section f" do + expect(entry.sf20_section_f_score).to eq 1 + end + it "outputs a custom score for section g" do + expect(entry.sf20_section_g_score).to eq 3 + end + end + + describe "#score" do + let(:user) { create :user } + + describe "General" do + let!(:entry) { create :sf20_entry } + + it "should have a score queue in resque" do + expect(Entry).to have_queue_size_of(1) + end + + it "should validate responses" do + # Range + range_response = entry.responses.detect{|q| q.name == "calmness"} + range_response.value = 999 + expect(entry).to be_invalid + range_response.value = 1 + expect(entry).to be_valid + + # Nils + range_response.value = nil + expect(entry).to be_valid + + # # Boolean + # boolean_response = entry.responses.detect{|q| q.name == "complication_abscess"} + # boolean_response.value = 2 + # expect(entry).to be_invalid + # boolean_response.value = 1 + # expect(entry).to be_valid + end + + it "isn't complete if nils responses are present" do + response = entry.responses.detect{|q| q.name == "general_health"} + response.value = nil + expect(entry.complete_sf20_entry?).to be_false + end + end + + # describe "Scoring" do + # let(:entry) { create :hbi_entry, user: user } + # it "has a score if all responses are present" do + # expect(entry.responses.count).to eq HbiCatalog::QUESTIONS.count + # expect(entry.hbi_score).to be > 0 + # end + + # it "reverts to -1 (incomplete) if any responses are removed" do + # entry.responses.delete entry.responses.first + # with_resque{ entry.save }; entry.reload + + # expect(entry.responses.count).to be < HbiCatalog::QUESTIONS.count + # expect(entry.reload.hbi_score).to eql -1.0 + # end + # end + + # describe "Calculations" do + # let(:entry) { build :hbi_entry, user: user } + # it "sample entry scores match calculated scores" do + # entry.responses << build(:response, {catalog: "hbi", name: :general_wellbeing , value: 1}) + # entry.responses << build(:response, {catalog: "hbi", name: :ab_pain , value: 2}) + # entry.responses << build(:response, {catalog: "hbi", name: :stools , value: 3}) + # entry.responses << build(:response, {catalog: "hbi", name: :ab_mass , value: 0}) + + # entry.responses << build(:response, {catalog: "hbi", name: :complication_arthralgia , value: 0}) + # entry.responses << build(:response, {catalog: "hbi", name: :complication_uveitis , value: 0}) + # entry.responses << build(:response, {catalog: "hbi", name: :complication_erythema_nodosum , value: 0}) + # entry.responses << build(:response, {catalog: "hbi", name: :complication_aphthous_ulcers , value: 1}) + # entry.responses << build(:response, {catalog: "hbi", name: :complication_anal_fissure , value: 0}) + # entry.responses << build(:response, {catalog: "hbi", name: :complication_fistula , value: 0}) + # entry.responses << build(:response, {catalog: "hbi", name: :complication_abscess , value: 1}) + + # entry.save + # Entry.perform entry.id, false + # entry.reload + + # expect(entry.hbi_complications_score).to eql 2.0 + # expect(entry.hbi_score).to eql 8.0 + # end + # end + + + end +end diff --git a/spec/factories/entry_factory.rb b/spec/factories/entry_factory.rb index 191b146..8bfbdee 100644 --- a/spec/factories/entry_factory.rb +++ b/spec/factories/entry_factory.rb @@ -77,6 +77,55 @@ def random_boolean end end +FactoryGirl.define do + factory :sf20_entry, class: Entry do + user + catalogs ["sf20"] + sequence(:date) {|n| (n-1).days.from_now.to_date} + responses [] + + before(:create) do |entry| + # section A + entry.responses << build(:response, {catalog: "sf20", name: :general_health, value: 2}) + # section B + entry.responses << build(:response, {catalog: "sf20", name: :limit_vigorous_activity, value: 2}) + entry.responses << build(:response, {catalog: "sf20", name: :limit_moderate_activity, value: 3}) + entry.responses << build(:response, {catalog: "sf20", name: :limit_climbing_stairs, value: 2}) + entry.responses << build(:response, {catalog: "sf20", name: :limit_bending, value: 1}) + entry.responses << build(:response, {catalog: "sf20", name: :limit_walking, value: 2}) + entry.responses << build(:response, {catalog: "sf20", name: :limit_basic_activity, value: 1}) + # section C + entry.responses << build(:response, {catalog: "sf20", name: :bodily_pain, value: 6}) + # section D + entry.responses << build(:response, {catalog: "sf20", name: :prevent_working, value: 1}) + # section E + entry.responses << build(:response, {catalog: "sf20", name: :prevent_certain_kinds_work, value: 1}) + # section F + entry.responses << build(:response, {catalog: "sf20", name: :limit_social_activities, value: 1}) + entry.responses << build(:response, {catalog: "sf20", name: :nervousness, value: 2}) + entry.responses << build(:response, {catalog: "sf20", name: :calmness, value: 3}) + entry.responses << build(:response, {catalog: "sf20", name: :downheartedness, value: 4}) + entry.responses << build(:response, {catalog: "sf20", name: :happiness, value: 5}) + entry.responses << build(:response, {catalog: "sf20", name: :despair, value: 6}) + # section G + entry.responses << build(:response, {catalog: "sf20", name: :somewhat_ill, value: 5}) + entry.responses << build(:response, {catalog: "sf20", name: :healthy_as_anybody, value: 1}) + entry.responses << build(:response, {catalog: "sf20", name: :excellent_health, value: 1}) + entry.responses << build(:response, {catalog: "sf20", name: :feeling_bad_lately, value: 5}) + + # entry.responses << build(:response, {catalog: "rapid3", name: :global_estimate , value: (0..10).step(0.5).to_a.sample}) + + Entry.class_eval{ include Sf20Catalog } + end + after(:create) do |entry| + Entry.skip_callback(:save, :after, :enqueue) + Entry.perform entry.id, false + entry.reload + end + + end +end + FactoryGirl.define do factory :symptom_entry, class: Entry do user