Merge branch 'master' into New-forms-page
This commit is contained in:
commit
b978d7d197
2
Gemfile
2
Gemfile
@ -24,7 +24,7 @@ gem 'bootsnap', '>= 1.4.2', require: false
|
|||||||
gem 'lockbox'
|
gem 'lockbox'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
# gem 'airrecord'
|
gem 'airrecord'
|
||||||
gem 'google-api-client'
|
gem 'google-api-client'
|
||||||
gem 'rack-cors'
|
gem 'rack-cors'
|
||||||
gem 'sentry-raven'
|
gem 'sentry-raven'
|
||||||
|
@ -58,6 +58,9 @@ GEM
|
|||||||
zeitwerk (~> 2.2)
|
zeitwerk (~> 2.2)
|
||||||
addressable (2.7.0)
|
addressable (2.7.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
|
airrecord (1.0.5)
|
||||||
|
faraday (>= 0.10, < 2.0)
|
||||||
|
net-http-persistent (>= 2.9)
|
||||||
aws-eventstream (1.0.3)
|
aws-eventstream (1.0.3)
|
||||||
aws-partitions (1.263.0)
|
aws-partitions (1.263.0)
|
||||||
aws-sdk-core (3.89.1)
|
aws-sdk-core (3.89.1)
|
||||||
@ -80,6 +83,7 @@ GEM
|
|||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
byebug (11.1.1)
|
byebug (11.1.1)
|
||||||
concurrent-ruby (1.1.6)
|
concurrent-ruby (1.1.6)
|
||||||
|
connection_pool (2.2.2)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
declarative (0.0.10)
|
declarative (0.0.10)
|
||||||
declarative-option (0.1.0)
|
declarative-option (0.1.0)
|
||||||
@ -136,6 +140,8 @@ GEM
|
|||||||
multi_json (1.14.1)
|
multi_json (1.14.1)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
|
net-http-persistent (3.1.0)
|
||||||
|
connection_pool (~> 2.2)
|
||||||
nio4r (2.5.2)
|
nio4r (2.5.2)
|
||||||
nokogiri (1.10.9)
|
nokogiri (1.10.9)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
@ -252,6 +258,7 @@ PLATFORMS
|
|||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
airrecord
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
bootsnap (>= 1.4.2)
|
bootsnap (>= 1.4.2)
|
||||||
byebug
|
byebug
|
||||||
|
@ -3,10 +3,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (!demoForm) {
|
if (!demoForm) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
demoForm.addEventListener('submit', function(e) {
|
||||||
|
var submitButton = document.getElementById('demo-submit');
|
||||||
|
submitButton.innerText = 'loading...';
|
||||||
|
submitButton.disabled = 'true'
|
||||||
|
});
|
||||||
|
|
||||||
demoForm.addEventListener('tinyforms:submitted', function(e) {
|
demoForm.addEventListener('tinyforms:submitted', function(e) {
|
||||||
console.log(e);
|
|
||||||
console.log(e.detail);
|
|
||||||
var name = document.getElementById('demo-submission-name');
|
var name = document.getElementById('demo-submission-name');
|
||||||
var demoFields = document.getElementById('demo-fields');
|
var demoFields = document.getElementById('demo-fields');
|
||||||
var demoSucess = document.getElementById('demo-success');
|
var demoSucess = document.getElementById('demo-success');
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
require 'google/apis/sheets_v4'
|
|
||||||
class Form < ApplicationRecord
|
class Form < ApplicationRecord
|
||||||
# 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"})
|
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_many :submissions, dependent: :destroy
|
has_many :submissions, dependent: :destroy
|
||||||
|
|
||||||
|
before_validation :insert_defaults, on: :create
|
||||||
after_create :create_spreadsheet
|
after_create :create_spreadsheet
|
||||||
|
|
||||||
has_secure_token
|
has_secure_token
|
||||||
|
|
||||||
|
encrypts :airtable_api_key
|
||||||
|
encrypts :airtable_app_key
|
||||||
|
|
||||||
validates_presence_of :title
|
validates_presence_of :title
|
||||||
|
validates_inclusion_of :backend_name, in: ['google_sheets', 'airtable']
|
||||||
|
# Airtable validations
|
||||||
|
validates_presence_of :airtable_api_key, if: :airtable?
|
||||||
|
validates_presence_of :airtable_app_key, if: :airtable?
|
||||||
|
validates_presence_of :airtable_table, if: :airtable?
|
||||||
|
|
||||||
# TODO: use counter_cache option on association
|
# TODO: use counter_cache option on association
|
||||||
def submissions_count
|
def submissions_count
|
||||||
@ -28,63 +34,39 @@ class Form < ApplicationRecord
|
|||||||
self.user.active?
|
self.user.active?
|
||||||
end
|
end
|
||||||
|
|
||||||
def google_spreadsheet_url
|
def airtable?
|
||||||
"https://docs.google.com/spreadsheets/d/#{google_spreadsheet_id}/edit" if google_spreadsheet_id.present?
|
backend_name == 'airtable'
|
||||||
|
end
|
||||||
|
|
||||||
|
def google
|
||||||
|
backend_name == 'google'
|
||||||
|
end
|
||||||
|
|
||||||
|
def backend
|
||||||
|
@backend ||= SpreadsheetBackends.const_get(backend_name.camelize).new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def spreadsheet_url
|
||||||
|
backend.url
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_spreadsheet
|
def create_spreadsheet
|
||||||
sheets = Google::Apis::SheetsV4::SheetsService.new
|
backend.create
|
||||||
sheets.authorization = user.google_authorization
|
|
||||||
create_object = Google::Apis::SheetsV4::Spreadsheet.new(properties: { title: title})
|
|
||||||
spreadsheet = sheets.create_spreadsheet(create_object)
|
|
||||||
update(google_spreadsheet_id: spreadsheet.spreadsheet_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def spreadsheet_service
|
def append_to_spreadsheet(data)
|
||||||
@spreadsheet_service ||= Google::Apis::SheetsV4::SheetsService.new.tap do |s|
|
backend.append(data)
|
||||||
s.authorization = user.google_authorization
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def header_values
|
def spreadsheet_headers
|
||||||
@header_values ||= begin
|
backend.headers
|
||||||
values = spreadsheet_service.get_spreadsheet_values(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 append(data)
|
|
||||||
data = data.with_indifferent_access
|
|
||||||
check_spreadsheed_headers!(data)
|
|
||||||
|
|
||||||
values = header_values.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')
|
|
||||||
|
|
||||||
spreadsheet_service.append_spreadsheet_value(google_spreadsheet_id, range, value_range, value_input_option: 'USER_ENTERED')
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_spreadsheed_headers!(data)
|
|
||||||
missing_headers = data.keys.map { |k| k.to_s.strip } - header_values
|
|
||||||
append_missing_headers(missing_headers) unless missing_headers.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
def append_missing_headers(missing_headers)
|
|
||||||
start_column = COLUMN_INDEX_TO_LETTER[header_values.length]
|
|
||||||
end_column = COLUMN_INDEX_TO_LETTER[header_values.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(google_spreadsheet_id, range, value_range, value_input_option: 'USER_ENTERED')
|
|
||||||
@header_values = nil # reset header values to refresh memoization on next access
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_param
|
def to_param
|
||||||
token
|
token
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def insert_defaults
|
||||||
|
self.backend_name ||= airtable_app_key.present? ? 'airtable' : 'google_sheets'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -44,7 +44,7 @@ class Submission < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def append_to_spreadsheet
|
def append_to_spreadsheet
|
||||||
result = form.append(data)
|
form.append_to_spreadsheet(data) &&
|
||||||
update_column(:appended_at, Time.current) if result.updates.updated_rows > 0
|
update_column(:appended_at, Time.current)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="media-right has-text-right is-size-4">
|
<div class="media-right has-text-right is-size-4">
|
||||||
<figure class="image is-48x48 ">
|
<figure class="image is-48x48 ">
|
||||||
<%= link_to form.google_spreadsheet_url, {class: "has-text-success", target: "__blank"} do %>
|
<%= link_to form.spreadsheet_url, {class: "has-text-success", target: "__blank"} do %>
|
||||||
<i class="far fa-file-excel"></i>
|
<i class="far fa-file-excel"></i>
|
||||||
<% end %>
|
<% end %>
|
||||||
</figure>
|
</figure>
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
<%= submission_url(@form) %>
|
<%= submission_url(@form) %>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<%= link_to 'Google spreadsheet', @form.google_spreadsheet_url %>
|
<%= link_to 'Google spreadsheet', @form.spreadsheet_url %>
|
||||||
</p>
|
</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<% @form.header_values.each do |value| %>
|
<% @form.spreadsheet_headers.each do |value| %>
|
||||||
<th><%= value %></th>
|
<th><%= value %></th>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tr>
|
</tr>
|
||||||
@ -16,7 +16,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<% @submissions.each do |submission| %>
|
<% @submissions.each do |submission| %>
|
||||||
<tr>
|
<tr>
|
||||||
<% @form.header_values.each do |column| %>
|
<% @form.spreadsheet_headers.each do |column| %>
|
||||||
<td><%= submission.data[column] %></td>
|
<td><%= submission.data[column] %></td>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -3,9 +3,10 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-one-third">
|
<div class="column is-one-third">
|
||||||
<form class="form demo-form" id="demo-form" action="<%= submission_url(DEMO_FORM) %>" method="POST" enctype="multipart/form-data" data-tinyforms="true">
|
<form class="form demo-form" id="demo-form" action="<%= submission_url(DEMO_FORM) %>" method="POST" enctype="multipart/form-data" data-tinyforms="true">
|
||||||
|
<input type="hidden" name="ID" value="tinyforms_id">
|
||||||
<h3>Demo</h3>
|
<h3>Demo</h3>
|
||||||
<p>
|
<p>
|
||||||
This short form is connected to the embedded <%= link_to 'document', DEMO_FORM.google_spreadsheet_url %> on the right. <br>
|
This short form is connected to the embedded <%= link_to 'document', DEMO_FORM.spreadsheet_url %> on the right. <br>
|
||||||
Submit the form and see the update of the document in realtime.
|
Submit the form and see the update of the document in realtime.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -15,7 +16,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>See your entry in the spreadsheet?!</p>
|
<p>See your entry in the spreadsheet?!</p>
|
||||||
<p>
|
<p>
|
||||||
<%= link_to "Get started now", signup_url, class: 'button' %>
|
<%= link_to "Create your form now!", signup_url, class: 'button' %>
|
||||||
|
<br>
|
||||||
|
or got <%= link_to 'further questions?', contact_url %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -68,7 +71,7 @@
|
|||||||
|
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button is-link">Submit</button>
|
<button class="button is-link" id="demo-submit">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -77,7 +80,7 @@
|
|||||||
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<iframe id="demo-sheet" style="width:100%;height:100%;" src="https://docs.google.com/spreadsheets/d/<%= DEMO_FORM.google_spreadsheet_id %>/edit?usp=sharing&nocache=<%= Time.now.to_i %>#gid=0&range=<%= DEMO_FORM.submissions.count + 1 %>:<%= DEMO_FORM.submissions.count + 2 %>"></iframe>
|
<iframe id="demo-sheet" style="width:100%;height:100%;" src="https://docs.google.com/spreadsheets/d/<%= DEMO_FORM.google_spreadsheet_id %>/edit?usp=sharing&nocache=<%= Time.now.to_i %>#gid=0&range=<%= DEMO_FORM.submissions.count + 1 %>:<%= DEMO_FORM.submissions.count + 2 %>"></iframe>
|
||||||
<p class="has-text-grey has-text-centered">Document not loading? <%= link_to 'Open it here', DEMO_FORM.google_spreadsheet_url %>.</p>
|
<p class="has-text-grey has-text-centered">Document not loading? <%= link_to 'Open it here', DEMO_FORM.spreadsheet_url %>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<div id="navbar-menu" class="navbar-menu">
|
<div id="navbar-menu" class="navbar-menu">
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<a href="https://www.notion.so/Tinyforms-Help-Center-04f13b5908bc46cfb4283079a3cb1149" class="navbar-item">Help</a>
|
<%= link_to 'Help', help_url, class: 'navbar-item' %>
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<% if !logged_in? -%>
|
<% if !logged_in? -%>
|
||||||
<%= link_to "Login", login_url, { :class => "button is-primary"} %>
|
<%= link_to "Login", login_url, { :class => "button is-primary"} %>
|
||||||
|
@ -6,10 +6,10 @@
|
|||||||
<h1 class="title has-text-centered">
|
<h1 class="title has-text-centered">
|
||||||
Login
|
Login
|
||||||
</h1>
|
</h1>
|
||||||
<div class="button google">
|
<%= link_to auth_at_provider_url(provider: 'google'), class: 'button google' do %>
|
||||||
<svg viewBox="0 0 15 15" class="googleLogo" style="width: 14px; height: 14px; display: block; fill: currentcolor; flex-shrink: 0; backface-visibility: hidden; margin-right: 6px;"><path d="M 7.28571 6.4125L 7.28571 9L 11.3929 9C 11.2143 10.0875 10.1429 12.225 7.28571 12.225C 4.78571 12.225 2.78571 10.0875 2.78571 7.5C 2.78571 4.9125 4.82143 2.775 7.28571 2.775C 8.71429 2.775 9.64286 3.4125 10.1786 3.9375L 12.1429 1.9875C 10.8929 0.75 9.25 0 7.28571 0C 3.25 0 0 3.3375 0 7.5C 0 11.6625 3.25 15 7.28571 15C 11.5 15 14.25 11.9625 14.25 7.6875C 14.25 7.2 14.2143 6.825 14.1429 6.45L 7.28571 6.45L 7.28571 6.4125Z"></path></svg>
|
<svg viewBox="0 0 15 15" class="googleLogo" style="width: 14px; height: 14px; display: block; fill: currentcolor; flex-shrink: 0; backface-visibility: hidden; margin-right: 6px;"><path d="M 7.28571 6.4125L 7.28571 9L 11.3929 9C 11.2143 10.0875 10.1429 12.225 7.28571 12.225C 4.78571 12.225 2.78571 10.0875 2.78571 7.5C 2.78571 4.9125 4.82143 2.775 7.28571 2.775C 8.71429 2.775 9.64286 3.4125 10.1786 3.9375L 12.1429 1.9875C 10.8929 0.75 9.25 0 7.28571 0C 3.25 0 0 3.3375 0 7.5C 0 11.6625 3.25 15 7.28571 15C 11.5 15 14.25 11.9625 14.25 7.6875C 14.25 7.2 14.2143 6.825 14.1429 6.45L 7.28571 6.45L 7.28571 6.4125Z"></path></svg>
|
||||||
Login with Google
|
Login with Google
|
||||||
</div>
|
<% end %>
|
||||||
<hr>
|
<hr>
|
||||||
<p>
|
<p>
|
||||||
As tinyforms builds on Google Sheets.<br>
|
As tinyforms builds on Google Sheets.<br>
|
||||||
|
@ -15,6 +15,17 @@ module Tinyform
|
|||||||
# Application configuration can go into files in config/initializers
|
# Application configuration can go into files in config/initializers
|
||||||
# -- all .rb files in that directory are automatically loaded after loading
|
# -- all .rb files in that directory are automatically loaded after loading
|
||||||
# the framework and any gems in your application.
|
# 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
|
config.middleware.insert_before 0, Rack::Cors do
|
||||||
allow do
|
allow do
|
||||||
origins '*'
|
origins '*'
|
||||||
|
@ -152,7 +152,7 @@ Rails.application.config.sorcery.configure do |config|
|
|||||||
#
|
#
|
||||||
config.google.key = ENV['GOOGLE_CLIENT_ID']
|
config.google.key = ENV['GOOGLE_CLIENT_ID']
|
||||||
config.google.secret = ENV['GOOGLE_CLIENT_SECRET']
|
config.google.secret = ENV['GOOGLE_CLIENT_SECRET']
|
||||||
config.google.callback_url = "http://localhost:3000/oauth/callback?provider=google"
|
config.google.callback_url = (ENV['GOOGLE_AUTH_CALLBACK_URL'] || "http://localhost:3000/oauth/callback?provider=google")
|
||||||
config.google.user_info_mapping = {:email => "email", :name => "name", :google_id => "id"}
|
config.google.user_info_mapping = {:email => "email", :name => "name", :google_id => "id"}
|
||||||
config.google.scope = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/spreadsheets"
|
config.google.scope = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/spreadsheets"
|
||||||
config.google.auth_url = '/o/oauth2/auth?access_type=offline&include_granted_scopes=true'
|
config.google.auth_url = '/o/oauth2/auth?access_type=offline&include_granted_scopes=true'
|
||||||
|
@ -22,5 +22,9 @@ Rails.application.routes.draw do
|
|||||||
get '/auth' => 'sessions#auth', as: :auth
|
get '/auth' => 'sessions#auth', as: :auth
|
||||||
|
|
||||||
get '/demo' => 'home#demo', as: :demo
|
get '/demo' => 'home#demo', as: :demo
|
||||||
|
get '/contact' => 'home#contact', as: :contact
|
||||||
|
get '/help', to: redirect('https://www.notion.so/Tinyforms-Help-Center-04f13b5908bc46cfb4283079a3cb1149')
|
||||||
|
|
||||||
|
|
||||||
root 'home#index'
|
root 'home#index'
|
||||||
end
|
end
|
||||||
|
8
db/migrate/20200413152532_add_airtable_support.rb
Normal file
8
db/migrate/20200413152532_add_airtable_support.rb
Normal file
@ -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
|
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -57,6 +57,10 @@ ActiveRecord::Schema.define(version: 2020_04_13_101920) do
|
|||||||
t.string "thank_you_url"
|
t.string "thank_you_url"
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_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
|
end
|
||||||
|
|
||||||
create_table "submissions", force: :cascade do |t|
|
create_table "submissions", force: :cascade do |t|
|
||||||
|
36
lib/spreadsheet_backends/airtable.rb
Normal file
36
lib/spreadsheet_backends/airtable.rb
Normal file
@ -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
|
71
lib/spreadsheet_backends/google_sheets.rb
Normal file
71
lib/spreadsheet_backends/google_sheets.rb
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user