- This short form is connected to the embedded <%= link_to 'document', DEMO_FORM.google_spreadsheet_url %> on the right.
+ This short form is connected to the embedded <%= link_to 'document', DEMO_FORM.spreadsheet_url %> on the right.
Submit the form and see the update of the document in realtime.
@@ -80,7 +80,7 @@
-
Document not loading? <%= link_to 'Open it here', DEMO_FORM.google_spreadsheet_url %>.
+
Document not loading? <%= link_to 'Open it here', DEMO_FORM.spreadsheet_url %>.
diff --git a/config/application.rb b/config/application.rb
index 2dae959..ffe45c8 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -15,6 +15,17 @@ module Tinyform
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
+ config.generators do |generate|
+ generate.helper false
+ generate.javascript_engine false
+ generate.request_specs false
+ generate.routing_specs false
+ generate.stylesheets false
+ generate.view_specs false
+ end
+
+ config.autoload_paths << Rails.root.join('lib')
+
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
diff --git a/db/migrate/20200413152532_add_airtable_support.rb b/db/migrate/20200413152532_add_airtable_support.rb
new file mode 100644
index 0000000..a5465dc
--- /dev/null
+++ b/db/migrate/20200413152532_add_airtable_support.rb
@@ -0,0 +1,8 @@
+class AddAirtableSupport < ActiveRecord::Migration[6.0]
+ def change
+ add_column :forms, :airtable_app_key_ciphertext, :string
+ add_column :forms, :airtable_api_key_ciphertext, :string
+ add_column :forms, :airtable_table, :string
+ add_column :forms, :backend_name, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 153d317..0c9af99 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_04_13_101920) do
+ActiveRecord::Schema.define(version: 2020_04_13_152532) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -57,6 +57,10 @@ ActiveRecord::Schema.define(version: 2020_04_13_101920) do
t.string "thank_you_url"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
+ t.string "airtable_app_key_ciphertext"
+ t.string "airtable_api_key_ciphertext"
+ t.string "airtable_table"
+ t.string "backend_name"
end
create_table "submissions", force: :cascade do |t|
diff --git a/lib/spreadsheet_backends/airtable.rb b/lib/spreadsheet_backends/airtable.rb
new file mode 100644
index 0000000..047a8f8
--- /dev/null
+++ b/lib/spreadsheet_backends/airtable.rb
@@ -0,0 +1,36 @@
+require 'airrecord'
+module SpreadsheetBackends
+ class Airtable
+
+ attr_accessor :form, :user
+
+ def initialize(form)
+ @form = form
+ @user = form.user
+ end
+
+ def url
+ "https://airtable.com/#{form.airtable_app_key}"
+ end
+
+ def append(data)
+ result = table.create(data, typecast: true)
+ result.id.present?
+ rescue Airrecord::Error => e
+ return false
+ end
+
+ def create
+ # Airtable must already exist
+ # TODO: maybe add validation here?
+ end
+
+ def headers
+ table.records.first&.fields&.keys # we only know the headers once we have at least one record
+ end
+
+ def table
+ @table ||= Airrecord.table(form.airtable_api_key, form.airtable_app_key, form.airtable_table)
+ end
+ end
+end
diff --git a/lib/spreadsheet_backends/google_sheets.rb b/lib/spreadsheet_backends/google_sheets.rb
new file mode 100644
index 0000000..d2a6ab3
--- /dev/null
+++ b/lib/spreadsheet_backends/google_sheets.rb
@@ -0,0 +1,71 @@
+require 'google/apis/sheets_v4'
+module SpreadsheetBackends
+ class GoogleSheets
+ # Hash to translate an index to a A1 notation. e.g. 1 => 'B', 27 => 'AA'
+ COLUMN_INDEX_TO_LETTER = Hash.new {|hash,key| hash[key] = hash[key - 1].next }.merge({0 => "A"})
+
+ attr_accessor :form, :user
+
+ def initialize(form)
+ @form = form
+ @user = form.user
+ end
+
+ def url
+ "https://docs.google.com/spreadsheets/d/#{form.google_spreadsheet_id}/edit" if form.google_spreadsheet_id.present?
+ end
+
+ def append(data)
+ data = data.with_indifferent_access
+ check_spreadsheed_headers!(data)
+
+ values = headers.map { |key| data[key] }
+ range = "A1:A#{COLUMN_INDEX_TO_LETTER[values.length]}1"
+ value_range = Google::Apis::SheetsV4::ValueRange.new(values: [values], major_dimension: 'ROWS')
+
+ result = spreadsheet_service.append_spreadsheet_value(form.google_spreadsheet_id, range, value_range, value_input_option: 'USER_ENTERED')
+ result.updates.updated_rows > 0
+ end
+
+ def create
+ sheets = Google::Apis::SheetsV4::SheetsService.new
+ sheets.authorization = user.google_authorization
+ create_object = Google::Apis::SheetsV4::Spreadsheet.new(properties: { title: form.title})
+ spreadsheet = sheets.create_spreadsheet(create_object)
+ form.update(google_spreadsheet_id: spreadsheet.spreadsheet_id)
+ end
+
+ def headers
+ @headers ||= begin
+ values = spreadsheet_service.get_spreadsheet_values(form.google_spreadsheet_id, 'A1:An').values
+ # if there are no headers yet, return an empty array
+ if values
+ values[0].map(&:strip)
+ else
+ []
+ end
+ end
+ end
+
+ def spreadsheet_service
+ @spreadsheet_service ||= Google::Apis::SheetsV4::SheetsService.new.tap do |s|
+ s.authorization = user.google_authorization
+ end
+ end
+
+ def check_spreadsheed_headers!(data)
+ missing_headers = data.keys.map { |k| k.to_s.strip } - headers
+ append_missing_headers(missing_headers) unless missing_headers.empty?
+ end
+
+ def append_missing_headers(missing_headers)
+ start_column = COLUMN_INDEX_TO_LETTER[headers.length]
+ end_column = COLUMN_INDEX_TO_LETTER[headers.length + missing_headers.length]
+ range = "#{start_column}1:#{end_column}1"
+ value_range = Google::Apis::SheetsV4::ValueRange.new(values: [missing_headers], major_dimension: 'ROWS')
+ spreadsheet_service.update_spreadsheet_value(form.google_spreadsheet_id, range, value_range, value_input_option: 'USER_ENTERED')
+ @headers = nil # reset header values to refresh memoization on next access
+ end
+
+ end
+end