Skip to content

Commit

Permalink
Fleshout metadata system and sentiment analysis
Browse files Browse the repository at this point in the history
- Remove Question#config and QuestionType#settings
  favor using SelfDescribing instead
- Add Dragnet::TextSentiment integrate with Answer
  and to generate score for long answers that have been
  marked for calculation
  • Loading branch information
delonnewman committed Aug 13, 2023
1 parent b2aec87 commit ad732e8
Show file tree
Hide file tree
Showing 21 changed files with 235 additions and 123 deletions.
2 changes: 1 addition & 1 deletion app/generators/answer_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def call(*)
def value(question)
case question.question_type.ident
when :text
ShortAnswer.generate
question.settings.long_answer? ? LongAnswer.generate : ShortAnswer.generate
when :choice
QuestionOptionAnswer[question].generate
when :number
Expand Down
2 changes: 2 additions & 0 deletions app/generators/question_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ def call(*)
count.times do |i|
q.question_options << QuestionOption[question: q, weight: i - (count / 2)].generate
end
when :text
q.settings = { long_answer: true, countable: true } if Faker::Boolean.boolean(true_ratio: 0.3)
end
end
end
Expand Down
7 changes: 7 additions & 0 deletions app/models/answer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ class Answer < ApplicationRecord
self.question_type = question.question_type if question
end

before_save do
# TODO: this should be another perspective to it can be extensible
if question.settings.long_answer? && question.settings.countable?
self.float_value = Dragnet::TextSentiment.new(long_text_value).score
end
end

def evaluation
Perspectives::AnswerEvaluation.get(question_type)
end
Expand Down
96 changes: 0 additions & 96 deletions app/models/concerns/self_describable.rb

This file was deleted.

6 changes: 6 additions & 0 deletions app/models/perspectives/answer_occurrence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@ def collect(reportable, question)
end
end
end

class Text < self
def collect(reportable, question)
reportable.answers.where(question: question).group(:float_value).count
end
end
end
end
14 changes: 14 additions & 0 deletions app/models/perspectives/answer_stats.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ def collect(reportable, question)
end
end

class Text < self
def collect(reportable, question)
column = Answer.arel_table[:float_value]

data =
reportable
.answers
.where(question: question)
.pick(min(column), max(column), sum(column), avg(column), stddev(column))

project_answer_stats(data)
end
end

private

def project_answer_stats(data)
Expand Down
1 change: 0 additions & 1 deletion app/models/question.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class Question < ApplicationRecord

validates :text, presence: true

serialize :config # TODO: use meta data instead
with Settings, delegating: %i[setting? setting]

belongs_to :survey
Expand Down
6 changes: 3 additions & 3 deletions app/models/question/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ def setting?(setting)
end

def setting(setting)
default = question.question_type.setting_default(setting)
return default unless question.config
default = question.question_type.get_meta(setting)
return default unless question.meta_data?

question.config.fetch(setting, default)
question.get_meta(setting, default)
end
end
3 changes: 0 additions & 3 deletions app/models/question_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ class QuestionType < ApplicationRecord

delegate :to_s, to: :name

# TODO: move these to meta data
serialize :settings

def self.get(ident)
QuestionType.find_by(slug: ident)
end
Expand Down
68 changes: 68 additions & 0 deletions app/models/self_describable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

module SelfDescribable
extend ActiveSupport::Concern

included do
has_many :meta_data_records, class_name: 'MetaDatum', as: :self_describable
with Parsing
end

# @param [String, Symbol] key
# @param alt
def get_meta(key, alt = nil)
records = meta_data_records.where(key: key)
return alt if records.empty?

parsing.parse_meta(records)
end
alias get_setting get_meta

# @return [Hash{Symbol => Object}]
def meta_data
return @meta_data if @meta_data

grouped = meta_data_records.group_by { _1.key.to_sym }
@meta_data = grouped.transform_values! { parsing.parse_meta(_1, grouped) }
end
alias settings meta_data

def meta_data?
!meta_data_records.empty?
end

# @param [String, Symbol] key
# @param value
#
# @return [Array<MetaDatum>]
def create_meta_datum!(key, value)
meta_data_records.create!(Evaluation.meta_attributes(key, value))
end

# @param [String, Symbol] key
# @param value
#
# @return [MetaDatum]
def build_meta_datum(key, value)
meta_data_records.build(Evaluation.meta_attributes(key, value))
end

# @param [String, Symbol] key
#
# @return [MetaDatum]
def destroy_meta_datum(key)
meta_data_records.where(key: key).destroy
end

# @param [Hash] data_hash
#
# @return [void]
def meta_data=(data_hash)
self.meta_data_records = data_hash.flat_map do |key, value|
Evaluation.meta_attributes(key, value).map do |attributes|
MetaDatum.new(attributes)
end
end
end
alias settings= meta_data=
end
43 changes: 43 additions & 0 deletions app/models/self_describable/evaluation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module SelfDescribable
# @api private
module Evaluation
module_function

def meta_attributes(key, value)
case value
when Array
multi_meta_attributes(key, value)
when Hash
ref_meta_attributes(key, value)
else
[single_meta_attributes(key, value)]
end
end

def multi_meta_attributes(key, values)
values.map do |value|
single_meta_attributes(key, value)
end
end

def ref_meta_attributes(key, deref)
deref.reduce([]) do |a, (k, v)|
a << single_meta_attributes(key, k, type: :ref)
if v.is_a?(Hash)
ref_meta_attributes(k, v).each do |attr|
a << attr
end
a
else
a << single_meta_attributes(k, v)
end
end
end

def single_meta_attributes(key, value, type: value.class)
{ key: key, value: String(value), key_type: type.name }
end
end
end
57 changes: 57 additions & 0 deletions app/models/self_describable/parsing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

module SelfDescribable
class Parsing < Dragnet::Advice
advises SelfDescribable

def parse_meta(records, grouped = nil)
return if records.empty?

if records.count == 1
parse_single_meta(records.first)
elsif records.first.key_type != 'ref'
parse_multi_meta(records)
else
parse_ref_meta(records, grouped)
end
end

def parse_ref_meta(records, grouped)
records.each_with_object({}) do |record, h|
key = record.value.to_sym
records = grouped ? grouped[key] : self_describable.meta_data_records.where(key: key)
h[key] = parse_meta(records, grouped)
grouped&.delete(key) # remove referenced keys from grouping
end
end

def parse_multi_meta(records)
records.map do |record|
parse_single_meta(record)
end
end

class Error < RuntimeError; end

def parse_single_meta(datum)
case datum.key_type
when 'String'
datum.value
when 'Symbol'
datum.value.to_sym
when 'TrueClass'
true
when 'FalseClass'
false
when 'Integer', 'Float', 'Rational'
Kernel.public_send(datum.key_type, datum.value)
when 'Date'
Date.parse(datum.value)
when 'Time'
Time.zone.parse(datum.value)
else
raise Error, "unable to parse values of type #{datum.key_type}"
end
end
end
end
File renamed without changes.
2 changes: 1 addition & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module Dragnet
class Application < Rails::Application
config.load_defaults 7.0

config.time_zone = 'Central Time (US & Canada)'
config.time_zone = 'Mountain Time (US & Canada)'

# Don't generate system test files.
config.generators.system_tests = nil
Expand Down
4 changes: 0 additions & 4 deletions db/migrate/20220514023941_initial.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ def change
t.string :slug, null: false, index: true
t.string :icon

t.string :settings

t.uuid :parent_type_id, index: true
end

Expand All @@ -72,8 +70,6 @@ def change
t.uuid :survey_id, index: true, null: false
t.foreign_key :surveys, column: :survey_id, primary_key: :id, on_delete: :cascade

t.string :config # TODO: remove

# Required for FollowupQuestions
t.uuid :question_id, index: true
t.belongs_to :question_option, index: true
Expand Down
Loading

0 comments on commit ad732e8

Please sign in to comment.