surveyor_warehouse 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YjkwYWQxMGNhMTJiYzJlNzY4MWM5NjM3N2ZkMDM5MDE5OGY2NDAyMA==
5
+ data.tar.gz: !binary |-
6
+ NDQxMGFlY2EzNjYyY2UxOTVlZGVlMjI2NTY5NWE4ZDgwZTM3NDY0OQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZjE2MmYyNDQ0MGQ3NzNjNmEyN2NkZDRjMzRkOTM0ZGFmMDUxZDY4Y2VjMGM0
10
+ MzRjNDhlYzkwNThhZTc4ZWQ1ZWJiMGNiZWZiODEyMWFmMDI1ZTJmNmE0Njdi
11
+ Y2QzZWJkYWZmMjg4YWFmMzkwZjEyMmU3N2U3YWUwOWQyZWEwY2I=
12
+ data.tar.gz: !binary |-
13
+ MGM2ODIyZmE2MmJlZjc0ZjkxMDc3ZTk0ZjJiZmNiMGIxNmI5ODczMDA1NjA2
14
+ OTU4MWIzYTgxNjMyYTNlYzIyODU1Njg5OGNhOGQ2NzliNTgxMTc5ZDI0Zjhl
15
+ ZTYwNTNhZWQzN2QzMTRhNzc3MDI5NGEyMzYzODFhN2Y4ZmU4YjM=
@@ -0,0 +1,70 @@
1
+ # Surveyor Warehouse
2
+
3
+ Surveyor Warehouse allows you to extract and transform the responses from
4
+ surveyor response sets into an alternate database table structure. This
5
+ can be generally helpful for querying surveyor responses within a table
6
+ structure of one's choice.
7
+
8
+ The alternate database table structure is defined by adding a
9
+ `:data_export_identifier` attribute to each question with the
10
+ format "table.column" (e.g. 'subjects.name').
11
+
12
+ ## Example
13
+
14
+ Given the survey:
15
+ ```
16
+ q 'What is your name?', :data_export_identifier => 'subjects.name'
17
+ a :string
18
+
19
+ q 'What is your age?', :data_export_identifier => 'subjects.age'
20
+ a :integer
21
+
22
+ q 'What is your gender?', :pick => :one, :data_export_identifier => 'subjects.gender'
23
+ a 'Male', :reference_identifier => "male"
24
+ a 'Female', :reference_identifier => "female"
25
+
26
+ q 'Which brand of shoes do you wear?', :pick => :any, :data_export_identifier => 'subjects.shoes'
27
+ a 'Nike', :reference_identifier => 'nike'
28
+ a 'Adidas', :reference_identifier => 'adidas'
29
+ a 'None', :omit, :reference_identifier => 'none'
30
+
31
+ repeater 'List all your siblings' do
32
+ q 'Name', :data_export_identifier => 'siblings.name'
33
+ a :string
34
+ end
35
+ ```
36
+
37
+ Responses would be transformed into:
38
+
39
+ ```
40
+ db=# select * from subjects;
41
+ name | age | gender | shoes | access_code | id
42
+ ---------+-----+--------+---------------+-------------+--------------
43
+ raphael | 18 | male | {nike,adidas} | QUhwHtMPyQ | QUhwHtMPyQ.0
44
+ stephen | 50 | male | {none} | zZu1OV1hmw | zZu1OV1hmw.0
45
+ (2 rows)
46
+
47
+ db=# select * from siblings;
48
+ name | access_code | id
49
+ --------------+-------------+--------------
50
+ leonardo | QUhwHtMPyQ | QUhwHtMPyQ.0
51
+ michelangelo | QUhwHtMPyQ | QUhwHtMPyQ.1
52
+ elizabeth | zZu1OV1hmw | zZu1OV1hmw.0
53
+ james | zZu1OV1hmw | zZu1OV1hmw.1
54
+ (4 rows)
55
+ ```
56
+
57
+
58
+ ## Getting Started
59
+
60
+ 1. Survey questions must have data_export_identifiers with the
61
+ format "table.column" (e.g. 'subjects.name')
62
+ 2. Take the survey
63
+ 3. `bundle exec rake surveyor:warehouse`
64
+ 4. Query the results
65
+
66
+
67
+
68
+ ## Limitations
69
+
70
+ * Currently only works with PostgresSQL
@@ -0,0 +1,47 @@
1
+ require 'surveyor'
2
+ require 'active_support/concern'
3
+
4
+ require 'surveyor_warehouse/normalized_survey_structure'
5
+ require 'surveyor_warehouse/response_bin'
6
+ require 'surveyor_warehouse/response_row'
7
+ require 'surveyor_warehouse/railtie' if defined?(Rails)
8
+ require 'surveyor_warehouse/extensions/question'
9
+ require 'surveyor_warehouse/extensions/survey'
10
+
11
+ module SurveyorWarehouse
12
+ def self.logger
13
+ @logger ||= Logger.new(STDOUT)
14
+ end
15
+
16
+ def self.transform
17
+ Survey.send(:include, SurveyorWarehouse::Extensions::Survey)
18
+
19
+ surveys = Survey.current_versions
20
+
21
+ surveys.each do |s|
22
+ ns = NormalizedSurveyStructure.new(s)
23
+ ns.create!
24
+ s.response_sets.each do |rs|
25
+ # logger.debug("Transforming [ResponseSet id:#{rs.id}] for [Survey id:#{s.id} title:'#{s.title}")
26
+ bins = ResponseBin.bins(rs.responses)
27
+ bins.map(&:rows).flatten.each(&:insert!)
28
+ logger.info("Transformed [ResponseSet id:#{rs.id}] for [Survey id:#{s.id} title:'#{s.title}']")
29
+ end
30
+ end
31
+ end
32
+
33
+ def self.clobber
34
+ Survey.send(:include, SurveyorWarehouse::Extensions::Survey)
35
+
36
+ surveys = Survey.current_versions
37
+
38
+ surveys.map(&:response_sets).flatten.each do |rs|
39
+ ns = NormalizedSurveyStructure.new(rs.survey)
40
+ ns.destroy!
41
+ end
42
+
43
+ end
44
+ end
45
+
46
+ # Survey.send(:include, SurveyorWarehouse::Extensions::Survey)
47
+ # Question.send(:include, SurveyorWarehouse::Extensions::Question)
@@ -0,0 +1,58 @@
1
+ module SurveyorWarehouse
2
+ module DB
3
+ def self.connection
4
+ @connection ||= Sequel.connect(
5
+ :adapter=> adapter,
6
+ :host=>'localhost',
7
+ :database=> database,
8
+ :user=> username,
9
+ :password=> password).extension(:pg_array)
10
+ @connection.extend(SequelExtension::Connection)
11
+ end
12
+
13
+ def self.configurations
14
+ @configurations ||= ::ActiveRecord::Base.configurations[Rails.env]
15
+ end
16
+
17
+ def self.username
18
+ @username ||= configurations['username']
19
+ end
20
+
21
+ def self.password
22
+ @password ||= configurations['password']
23
+ end
24
+
25
+ def self.database
26
+ @database ||= configurations['database']
27
+ end
28
+
29
+ def self.adapter
30
+ @adapter ||=
31
+ if 'postgresql' == configurations['adapter']
32
+ 'postgres'
33
+ else
34
+ raise "Unsupported database adapter: #{db['adapter']}"
35
+ end
36
+ end
37
+
38
+ ##
39
+ # Columns are returned a hash like below:
40
+ #
41
+ # { :column1 => [:primary_key], :column2 => [:not_null] }
42
+ def self.columns(tablename)
43
+ connection.schema(tablename.to_sym).inject({}) do |attrs, (cname, cattrs)|
44
+ attrs.merge(cname => cattrs)
45
+ end
46
+ end
47
+ end
48
+
49
+ module SequelExtension
50
+ module Connection
51
+ def columns(tablename)
52
+ self.schema(tablename.to_sym).inject({}) do |attrs, (cname, cattrs)|
53
+ attrs.merge(cname => cattrs)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_support/concern'
2
+
3
+ module SurveyorWarehouse
4
+ module Extensions
5
+ module Question
6
+ extend ActiveSupport::Concern
7
+ included do
8
+ ##
9
+ # A question's data_export_identifier should follow the
10
+ # format 'table.column' (e.g. 'patients.name')
11
+ def valid_data_export_identifier?
12
+ dei = self.try(:data_export_identifier)
13
+ dei.present? && dei.split('.').size == 2
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_support/concern'
2
+
3
+ module SurveyorWarehouse
4
+ module Extensions
5
+ module Survey
6
+ extend ActiveSupport::Concern
7
+ module ClassMethods
8
+ def current_versions
9
+ group = ::Survey.order("created_at DESC, survey_version DESC").all.group_by(&:access_code)
10
+ group.map { |access_code, surveys| surveys.first }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,96 @@
1
+ require 'surveyor_warehouse/db'
2
+
3
+ module SurveyorWarehouse
4
+ class NormalizedSurveyStructure
5
+ def initialize(survey)
6
+ @survey = survey
7
+ end
8
+
9
+ ##
10
+ # response_classes
11
+ def tables
12
+ questions = @survey.sections_with_questions.map(&:questions).flatten
13
+ Question.send(:include, SurveyorWarehouse::Extensions::Question)
14
+ predefs = questions.select {|q| q.valid_data_export_identifier? }.map do |question|
15
+ dei_tokens = question.data_export_identifier.split('.')
16
+
17
+ table = dei_tokens[0]
18
+ column = dei_tokens[1]
19
+ type = database_type(question)
20
+
21
+ [table, column, type]
22
+ end
23
+
24
+ tables = {}
25
+ predefs.each do |d|
26
+ table_name, column_name, column_type = d
27
+ table = tables[table_name] || TableDefinition.new(table_name)
28
+ table.columns << ColumnDefinition.new(column_name, column_type)
29
+ tables.merge!(table_name => table)
30
+ end
31
+
32
+ # Add primary key and response set access code columns
33
+ tables.each do |name, tdef|
34
+ tdef.columns << ColumnDefinition.new('access_code', 'text')
35
+ tdef.columns << ColumnDefinition.new('id', 'text')
36
+ end
37
+
38
+ @tables ||= tables.values
39
+ end
40
+
41
+ ##
42
+ # Force creates the structure by destroying all the tables first
43
+ #
44
+ def create!
45
+ destroy!
46
+ tables.each(&:create)
47
+ end
48
+
49
+ ##
50
+ # Destroy all the tables defined in the survey data_export_identifiers
51
+ #
52
+ def destroy!
53
+ drop_ddl = tables.map(&:name).map do |t|
54
+ "drop table if exists #{t};\n"
55
+ end.join
56
+ ActiveRecord::Base.connection.execute(drop_ddl)
57
+ end
58
+
59
+ private
60
+ SUPPORTED_TYPES = %w(date datetime decimal float integer string text)
61
+ def database_type(question)
62
+ case question.pick
63
+ when 'none'
64
+ response_classes = question.answers.map(&:response_class).uniq
65
+ raise "Muliple answer types are unsupported: #{response_classes.join(', ')}" if response_classes.size > 1
66
+ type = response_classes[0].tap do |t|
67
+ raise "Unsupported column type '#{t}' for question: #{question.inspect}}" unless SUPPORTED_TYPES.include?(t)
68
+ end
69
+ type == 'string' ? 'text' : type
70
+ when 'one'
71
+ 'text'
72
+ when 'any'
73
+ "text[]"
74
+ else
75
+ raise "Unable to find type for question: #{question.inspect}"
76
+ end
77
+ end
78
+
79
+ class TableDefinition < Struct.new(:name)
80
+ def columns
81
+ @columns ||= []
82
+ end
83
+
84
+ def create
85
+ ActiveRecord::Base.connection.create_table(name.to_sym, :id => false) do |t|
86
+ columns.each do |c|
87
+ t.column c.name, c.type
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ class ColumnDefinition < Struct.new(:name, :type)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,9 @@
1
+ require 'surveyor_warehouse'
2
+ require 'rails'
3
+ module SurveyorWarehouse
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ Dir[File.join(File.dirname(__FILE__),'../tasks/*.rake')].each { |f| load f }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,67 @@
1
+ require 'surveyor_warehouse/response_row'
2
+
3
+ module SurveyorWarehouse
4
+ class ResponseBin < Struct.new(:key, :access_code, :response_group)
5
+ def <<(response)
6
+ q = response.question
7
+ Question.send(:include, SurveyorWarehouse::Extensions::Question)
8
+ if q.valid_data_export_identifier?
9
+ (@responses ||= []) << response
10
+ else
11
+ puts "Ignoring response with data_export_identifier: #{q.data_export_identifier}"
12
+ end
13
+ self
14
+ end
15
+
16
+ def responses
17
+ (@responses || []).dup.freeze
18
+ end
19
+
20
+ ##
21
+ # Groups responses into "bins" using the 'access_code.response_group' as
22
+ # the bin key.
23
+ #
24
+ # See below for an example how the binning behavior works:
25
+ #
26
+ # responses = []
27
+ # responses.push(
28
+ # [Response access_code:'abc' response_group:0 string_value:'Jeff'],
29
+ # [Response access_code:'abc' response_group:0 string_value:'Bezos'],
30
+ # [Response access_code:'xyz' response_group:0 string_value:'Kindle'],
31
+ # [Response access_code:'xyz' response_group:1 string_value:'EC2'])
32
+ #
33
+ # ResponseBinner.new(responses).bins # => Results in the three bins below
34
+ #
35
+ # [ResponseBin key:'abc:0' responses: [
36
+ # [Response access_code:'abc' response_group:0 string_value:'Jeff'],
37
+ # [Response access_code:'abc' response_group:0 string_value:'Bezos']]
38
+ # [ResponseBin key:'xyz:0' responses: [
39
+ # [Response access_code:'xyz' response_group:0 string_value:'Kindle']]
40
+ # [ResponseBin key:'xyz:1' responses: [
41
+ # [Response access_code:'xyz' response_group:1 string_value:'EC2']]
42
+ DEFAULT_RESPONSE_GROUP = 0
43
+ def self.bins(responses)
44
+ bins = {}
45
+ responses.each do |r|
46
+ ac = r.response_set.access_code
47
+ rg = r.response_group || DEFAULT_RESPONSE_GROUP
48
+ key = "#{ac}.#{rg}"
49
+ bin = bins[key] || ResponseBin.new(key, ac, rg)
50
+ bin << r
51
+ bins.merge!(key => bin)
52
+ end
53
+ bins.values
54
+ end
55
+
56
+ def rows
57
+ rows = {}
58
+ @responses.each do |r|
59
+ table_name = r.question.data_export_identifier.split('.')[0]
60
+ table = rows[table_name] || ResponseRow.new(table_name, key, access_code)
61
+ table.responses << r
62
+ rows.merge!(table_name => table)
63
+ end
64
+ rows.values
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,53 @@
1
+ require 'surveyor_warehouse/db'
2
+ require 'sequel'
3
+
4
+ module SurveyorWarehouse
5
+ class ResponseRow < Struct.new(:name, :id, :access_code)
6
+ def responses
7
+ @responses ||= []
8
+ end
9
+
10
+ def name
11
+ super.to_sym
12
+ end
13
+
14
+ def insert!
15
+ column_and_values = {}
16
+ col_and_val = @responses.map do |r|
17
+ column = r.question.data_export_identifier.split('.')[1]
18
+
19
+ value_field = case r.answer.response_class
20
+ when 'datetime'; 'datetime_value'
21
+ when 'date'; 'date_value'
22
+ when 'time'; 'time_value'
23
+ when 'float'; 'float_value'
24
+ when 'integer'; 'integer_value'
25
+ when 'string'; 'string_value'
26
+ when 'text'; 'text_value'
27
+ end
28
+
29
+ value = value_field.present? ? r.send(value_field) : r.answer.try(:reference_identifier)
30
+ [column.to_sym, value]
31
+ end
32
+
33
+ mDB = SurveyorWarehouse::DB.connection
34
+ schema = mDB.schema(name.to_sym).inject({}) { |attrs, (cname, cattrs)| attrs.merge(cname => cattrs) }
35
+
36
+ column_and_values = col_and_val.inject({}) do |accum, (col, val)|
37
+ if schema[col][:db_type] =~ /\[\]/
38
+ accum[col] = Sequel.pg_array((accum[col] || []) << val)
39
+ else
40
+ accum[col] = val
41
+ end
42
+ accum
43
+ end
44
+
45
+ column_and_values.merge!('id' => id, 'access_code' => access_code)
46
+
47
+ ds = mDB[name]
48
+
49
+ ds.insert(column_and_values)
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ require 'surveyor_warehouse'
2
+
3
+ namespace :surveyor do
4
+ desc 'Transforms the survey response sets into the defined warehouse form'
5
+ task :warehouse => [:environment, :clobber] do
6
+ SurveyorWarehouse.transform
7
+ end
8
+
9
+ desc 'Clobber the warehouse'
10
+ task :clobber do
11
+ SurveyorWarehouse.clobber
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
2
+ ENV["RAILS_ENV"] ||= 'test'
3
+ begin
4
+ require File.expand_path("../../testbed/config/environment", __FILE__)
5
+ rescue LoadError => e
6
+ fail "Could not load the testbed app. Have you generated it?\n#{e.class}: #{e}"
7
+ end
8
+
9
+ require 'rspec/autorun'
10
+ require 'database_cleaner'
11
+
12
+ RSpec.configure do |config|
13
+ config.treat_symbols_as_metadata_keys_with_true_values = true
14
+
15
+ # == Mock Framework
16
+ #
17
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
18
+ #
19
+ # config.mock_with :mocha
20
+ # config.mock_with :flexmock
21
+ # config.mock_with :rr
22
+ config.mock_with :rspec
23
+
24
+ ## Database Cleaner
25
+
26
+ config.before(:suite) do
27
+ DatabaseCleaner.strategy = :transaction
28
+ DatabaseCleaner.clean_with(:truncation)
29
+ end
30
+
31
+ config.before(:each, :clean_with_truncation) do
32
+ DatabaseCleaner.strategy = :truncation
33
+ end
34
+
35
+ config.after(:each, :clean_with_truncation) do
36
+ DatabaseCleaner.strategy = :transaction
37
+ end
38
+
39
+ config.before(:each) do
40
+ DatabaseCleaner.start
41
+ end
42
+
43
+ config.after(:each) do
44
+ DatabaseCleaner.clean
45
+ end
46
+ end
47
+
@@ -0,0 +1,144 @@
1
+ require 'spec_helper'
2
+ require 'surveyor_warehouse'
3
+
4
+ describe SurveyorWarehouse::NormalizedSurveyStructure do
5
+ let(:survey) do
6
+ Surveyor::Parser.new.parse(
7
+ <<-SURVEY
8
+ survey "Favorites" do
9
+ section "One" do
10
+ q "What is your favorite name?", :data_export_identifier => 'favorites.name'
11
+ a :string
12
+
13
+ q "What is your favorite color?", :pick => :one, :data_export_identifier => 'favorites.color'
14
+ a "red"
15
+ a "blue"
16
+
17
+ q "Choose the colors you don't like", :pick => :any, :data_export_identifier => 'hated.colors'
18
+ a "red"
19
+ a "blue"
20
+ a "green"
21
+
22
+ end
23
+ end
24
+ SURVEY
25
+ )
26
+ end
27
+
28
+ let(:normalized) { SurveyorWarehouse::NormalizedSurveyStructure.new(survey) }
29
+
30
+ let(:connection) do
31
+ ActiveRecord::Base.connection
32
+ end
33
+
34
+ def drop_tables(*tables)
35
+ tables.each do |t|
36
+ connection.drop_table(t) if connection.table_exists?(t)
37
+ end
38
+ end
39
+
40
+ describe '#create!' do
41
+ after(:each) do
42
+ drop_tables(:favorites, :hated)
43
+ end
44
+
45
+ it 'creates tables with scalar types' do
46
+ normalized.create!
47
+ connection.table_exists?(:favorites).should be(true)
48
+ columns = connection.columns(:favorites).map{ |c| [c.name, c.sql_type] }.sort
49
+ columns.should == [
50
+ %w(access_code text),
51
+ %w(color text),
52
+ %w(id text),
53
+ %w(name text)
54
+ ]
55
+ end
56
+
57
+ it 'creates tables with array types' do
58
+ normalized.create!
59
+ connection.table_exists?(:hated).should be(true)
60
+ columns = connection.columns(:hated).map(&:name).sort
61
+ columns.should == %w(access_code colors id)
62
+ end
63
+ end
64
+
65
+ describe '#destroy!' do
66
+ before(:each) do
67
+ connection.create_table(:foo) do |t|
68
+ t.integer :id
69
+ end
70
+ end
71
+
72
+ after(:each) do
73
+ drop_tables(:favorites, :hated, :foo)
74
+ end
75
+
76
+ it "drops all normalized tables" do
77
+ normalized.create!
78
+ normalized.destroy!
79
+ connection.table_exists?(:favorites).should be(false)
80
+ connection.table_exists?(:hated).should be(false)
81
+ connection.table_exists?(:foo).should be(true)
82
+ end
83
+ end
84
+
85
+ describe '#tables' do
86
+ let (:tables) { normalized.tables }
87
+ let (:columns) { tables.inject({}){ |accum, t| accum.merge(t.name => t.columns) } }
88
+
89
+ it 'has tables from data export identifiers' do
90
+ tables.map(&:name).sort.should == %w(favorites hated)
91
+ end
92
+
93
+ it 'has columns with scalar types' do
94
+ columns['favorites'].map { |col| [col.name, col.type] }.sort.should == [
95
+ ['access_code', 'text'],
96
+ ['color', 'text'],
97
+ ['id', 'text'],
98
+ ['name', 'text']
99
+ ]
100
+ end
101
+
102
+ it 'has columns with array types' do
103
+ columns['hated'].map { |col| [col.name, col.type] }.sort.should == [
104
+ ['access_code', 'text'],
105
+ ['colors', 'text[]'],
106
+ ['id', 'text']
107
+ ]
108
+ end
109
+
110
+ it 'ignores questions without a data export identifier' do
111
+ blank_survey = Surveyor::Parser.new.parse(
112
+ <<-SURVEY
113
+ survey "Pet" do
114
+ section "Name" do
115
+ q "What is your pet's name?"
116
+ a :string
117
+ end
118
+ end
119
+ SURVEY
120
+ )
121
+
122
+ norm = SurveyorWarehouse::NormalizedSurveyStructure.new(blank_survey)
123
+
124
+ expect(norm.tables).to eq([])
125
+ end
126
+
127
+ it 'ignores questions with an incomplete data export identifier' do
128
+ odd_survey = Surveyor::Parser.new.parse(
129
+ <<-SURVEY
130
+ survey "Pet" do
131
+ section "Name" do
132
+ q "What is your pet's name?", :data_export_identifier => 'pet'
133
+ a :string
134
+ end
135
+ end
136
+ SURVEY
137
+ )
138
+
139
+ norm = SurveyorWarehouse::NormalizedSurveyStructure.new(odd_survey)
140
+
141
+ expect(norm.tables).to eq([])
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'SurveyorWarehouse::ResponseBin' do
4
+ let(:color) { Question.new(:data_export_identifier => 'favorite.color') }
5
+ let(:food) { Question.new(:data_export_identifier => 'favorite.food') }
6
+ let(:utencil) { Question.new(:data_export_identifier => 'hated.utencil') }
7
+ let(:tree) { Question.new(:data_export_identifier => 'meh.tree') }
8
+
9
+ def r(q)
10
+ Response.new(:question => q)
11
+ end
12
+
13
+ describe '#tables' do
14
+ let(:bin) { SurveyorWarehouse::ResponseBin.new.tap{ |bin| bin << r(color) << r(food) << r(utencil) << r(tree) } }
15
+
16
+ it 'groups into tables' do
17
+ bin.rows.map(&:name).sort.should == [:favorite, :hated, :meh]
18
+ end
19
+ end
20
+
21
+ describe '::bins' do
22
+ let(:rs_abc) { ResponseSet.new.tap { |r| r.access_code = 'abc' } }
23
+ let(:rs_def) { ResponseSet.new.tap { |r| r.access_code = 'def' } }
24
+
25
+ let(:r0) { Response.new(:response_set => rs_abc, :question => food) }
26
+ let(:r1) { Response.new(:response_set => rs_abc, :question => utencil) }
27
+ let(:r2) { Response.new(:response_set => rs_def, :question => food) }
28
+ let(:r3) { Response.new(:response_set => rs_def, :question => utencil, :response_group => 2) }
29
+
30
+ let(:responses) { [r0, r1, r2, r3] }
31
+
32
+ it 'should bin each access_code and response_group combination' do
33
+ bins = SurveyorWarehouse::ResponseBin.bins(responses)
34
+ bins.map(&:key).sort.should == %w(abc.1 def.1 def.2)
35
+ bins.map(&:access_code).sort.should == %w(abc def def)
36
+ end
37
+
38
+ it 'should ignore responses with invalid data export identifiers' do
39
+ bad_1 = Question.new(:data_export_identifier => '')
40
+ bad_2 = Question.new(:data_export_identifier => 'tree')
41
+
42
+ Response.new(:response_set => rs_abc)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'SurveyorWarehouse::ResponseRow' do
4
+ let(:connection) do
5
+ SurveyorWarehouse::DB.connection
6
+ end
7
+
8
+ let(:favorites) { connection[:favorites] }
9
+ let(:hated) { connection[:hated] }
10
+
11
+ before(:each) do
12
+ connection.create_table!(:favorites) do
13
+ primary_key :id, String
14
+ String :access_code
15
+ String :name
16
+ String :color
17
+ end
18
+ connection.extension :pg_array
19
+ Sequel::Model.db.extension :pg_array
20
+ connection.create_table!(:hated) do
21
+ primary_key :id, String
22
+ String :access_code
23
+ column :colors, 'text[]'
24
+ end
25
+ end
26
+
27
+ describe '#insert!' do
28
+ let(:survey) do
29
+ Surveyor::Parser.new.parse(<<-SURVEY
30
+ survey "Favorites" do
31
+ section "One" do
32
+ q "What is your favorite name?", :data_export_identifier => 'favorites.name'
33
+ a :string
34
+
35
+ q "What is your favorite color?", :pick => :one, :data_export_identifier => 'favorites.color'
36
+ a_3 "red"
37
+ a_5 "green"
38
+ a_7 "blue"
39
+
40
+ q "Choose the colors you don't like", :pick => :any, :data_export_identifier => 'hated.colors'
41
+ a_2 "red"
42
+ a_4 "blue"
43
+ a_6 "green"
44
+
45
+ end
46
+ end
47
+ SURVEY
48
+ )
49
+ end
50
+
51
+ def find_question_by_dei(dei)
52
+ questions = survey.sections.map(&:questions).flatten.select { |q| q.data_export_identifier == dei}
53
+
54
+ if questions.size == 1
55
+ questions.first
56
+ elsif questions.size > 1
57
+ raise "Too many questions found with dei: #{dei}"
58
+ else
59
+ raise "No questions found with dei: #{dei}"
60
+ end
61
+ end
62
+
63
+ def find_answer_by_refid(q, refid)
64
+ answers = q.answers.select { |a| a.reference_identifier == refid}
65
+
66
+ if answers.size == 1
67
+ answers.first
68
+ elsif answers.size > 1
69
+ raise "Too many answers found with refid: #{refid}"
70
+ else
71
+ raise "No answers found with refid: #{refid}"
72
+ end
73
+ end
74
+
75
+ let (:fav_color) { find_question_by_dei('favorites.color') }
76
+ let (:fav_name) { find_question_by_dei('favorites.name') }
77
+ let (:hated_colors) { find_question_by_dei('hated.colors') }
78
+
79
+ it 'inserts string data' do
80
+ r = Response.new(:question => fav_name, :answer => fav_name.answers[0], :string_value => 'homer')
81
+ row = SurveyorWarehouse::ResponseRow.new('favorites', 'abc.1', 'abc')
82
+ row.responses << r
83
+ row.insert!
84
+
85
+ favorites.map(:name).should == ['homer']
86
+ end
87
+
88
+ it 'inserts integer data' do
89
+ r = Response.new(:question => fav_color, :answer => find_answer_by_refid(fav_color, '5'))
90
+ row = SurveyorWarehouse::ResponseRow.new('favorites', 'abc.1', 'abc')
91
+ row.responses << r
92
+ row.insert!
93
+
94
+ favorites.map(:color).should == ['5']
95
+ end
96
+
97
+ it 'inserts array data' do
98
+ r0 = Response.new(:question => hated_colors, :answer => find_answer_by_refid(hated_colors, '4'))
99
+ r1 = Response.new(:question => hated_colors, :answer => find_answer_by_refid(hated_colors, '6'))
100
+ row = SurveyorWarehouse::ResponseRow.new('hated', 'abc.1', 'abc')
101
+ row.responses << r0 << r1
102
+ row.insert!
103
+
104
+ hated.map(:colors).sort.should == [%w(4 6)]
105
+ end
106
+ end
107
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: surveyor_warehouse
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - John Dzak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: surveyor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: haml
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: actionpack
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activerecord
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sequel
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pg
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Transform surveyor responses into an alternate structure
126
+ email: j-dzak@northwestern.edu
127
+ executables: []
128
+ extensions: []
129
+ extra_rdoc_files: []
130
+ files:
131
+ - README.md
132
+ - lib/surveyor_warehouse.rb
133
+ - lib/surveyor_warehouse/db.rb
134
+ - lib/surveyor_warehouse/extensions/question.rb
135
+ - lib/surveyor_warehouse/extensions/survey.rb
136
+ - lib/surveyor_warehouse/normalized_survey_structure.rb
137
+ - lib/surveyor_warehouse/railtie.rb
138
+ - lib/surveyor_warehouse/response_bin.rb
139
+ - lib/surveyor_warehouse/response_row.rb
140
+ - lib/tasks/transform.rake
141
+ - spec/spec_helper.rb
142
+ - spec/surveyor_warehouse/normalized_survey_structure_spec.rb
143
+ - spec/surveyor_warehouse/response_bin_spec.rb
144
+ - spec/surveyor_warehouse/response_row_spec.rb
145
+ homepage: https://github.com/NUBIC/surveyor_warehouse
146
+ licenses:
147
+ - MIT
148
+ metadata: {}
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ! '>='
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubyforge_project:
165
+ rubygems_version: 2.2.2
166
+ signing_key:
167
+ specification_version: 4
168
+ summary: Transform surveyor responses into an alternate structure
169
+ test_files: []