Hello tinyforms

This commit is contained in:
2020-04-05 23:29:32 +02:00
commit 917051c7ea
101 changed files with 9649 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
//= link_tree ../images
//= link_directory ../stylesheets .css

0
app/assets/images/.keep Normal file
View File

View File

@@ -0,0 +1,15 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View File

@@ -0,0 +1,10 @@
class ApplicationController < ActionController::Base
def require_login
redirect_to login_url unless current_user.present?
end
def current_user
session[:user_id] && User.find_by(id: session[:user_id])
end
end

View File

View File

@@ -0,0 +1,33 @@
require 'google/apis/sheets_v4'
require 'google/api_client/client_secrets'
class FormsController < ApplicationController
before_action :require_login
def new
@form = current_user.forms.build
end
def show
@form = current_user.forms.find_by!(token: params[:id])
@submissions = @form.submissions
end
def index
@forms = current_user.forms
end
def create
@form = current_user.forms.build(form_params)
if @form.save
redirect_to form_url(@form)
else
render :new
end
end
private
def form_params
params.require(:form).permit(:title, :thank_you_url)
end
end

View File

@@ -0,0 +1,4 @@
class HomeController < ApplicationController
def index; end
end

View File

@@ -0,0 +1,48 @@
require 'google/apis/oauth2_v2'
class SessionsController < ApplicationController
def new
reset_session
redirect_to auth_client.authorization_uri.to_s
end
def auth
reset_session
if params[:error]
flash[:error] = 'Login failed'
redirect_to root_url
else
auth_client.code = params[:code]
auth_client.fetch_access_token!
@user, @authentication = User.find_by_oauth_info(auth_client)
if @user.persisted? && @authentication.persisted?
session[:user_id] = @user.id.to_s
redirect_to forms_url
else
flash[:error] = 'Login failed'
redirect_to root_url
end
end
end
def destroy
reset_session
redirect_to root_url
end
private
def auth_client
@auth_client ||= CLIENT_SECRETS.to_authorization.tap do |c|
c.update!(
scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/spreadsheets',
redirect_uri: auth_url,
additional_parameters: {
'access_type' => 'offline', # offline access
'include_granted_scopes' => 'true' # incremental auth
}
)
end
end
end

View File

@@ -0,0 +1,25 @@
require 'google/apis/sheets_v4'
class SubmissionsController < ApplicationController
skip_before_action :verify_authenticity_token
def create
@form = Form.find_by!(token: params[:form_id])
@submission = @form.submissions.build(data: data_params)
respond_to do |format|
if @submission.save
format.html { redirect_to(@form.thank_you_url) if @form.thank_you_url.present? }
format.json { render(json: { success: true, data: @submission.data }) }
else
format.html
format.json { render(json: { error: @submission.errors }, status: 422) }
end
end
end
private
def data_params
request.request_parameters
end
end

View File

@@ -0,0 +1,2 @@
module ApplicationHelper
end

View File

@@ -0,0 +1,6 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
import { createConsumer } from "@rails/actioncable"
export default createConsumer()

View File

@@ -0,0 +1,5 @@
// Load all the channels within this directory and all subdirectories.
// Channel files must be named *_channel.js.
const channels = require.context('.', true, /_channel\.js$/)
channels.keys().forEach(channels)

View File

@@ -0,0 +1,17 @@
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
// Uncomment to copy all static images under ../images to the output folder and reference
// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
// or the `imagePath` JavaScript helper below.
//
// const images = require.context('../images', true)
// const imagePath = (name) => images(name, true)

View File

@@ -0,0 +1,7 @@
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end

View File

@@ -0,0 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
end

View File

@@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

View File

@@ -0,0 +1,22 @@
class Authentication < ApplicationRecord
belongs_to :user
def expired?
expires_at <= Time.current
end
def google_authorization
@google_authorization ||= CLIENT_SECRETS.to_authorization.tap do |c|
c.access_token = self.access_token
c.refresh_token = self.refresh_token
c.expires_at = self.expires_at
if expires_at < 1.minute.from_now
c.refresh!
self.access_token = c.access_token if c.access_token.present?
self.refresh_token = c.refresh_token if c.refresh_token.present?
self.expires_at = Time.at(c.expires_at) if c.expires_at.present?
end
end
end
end

View File

69
app/models/form.rb Normal file
View File

@@ -0,0 +1,69 @@
require 'google/apis/sheets_v4'
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
has_many :submissions, dependent: :destroy
after_create :create_spreadsheet
has_secure_token
validates_presence_of :title
def create_spreadsheet
sheets = Google::Apis::SheetsV4::SheetsService.new
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
def spreadsheet_service
@spreadsheet_service ||= Google::Apis::SheetsV4::SheetsService.new.tap do |s|
s.authorization = user.google_authorization
end
end
def header_values
@header_values ||= begin
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
def to_param
token
end
end

18
app/models/submission.rb Normal file
View File

@@ -0,0 +1,18 @@
class Submission < ApplicationRecord
belongs_to :form
after_create :append_to_spreadsheet
validates_presence_of :data
def data=(value)
sanitized_data = {}
value.each do |key, value|
sanitized_data[key] = value.to_s
end
write_attribute(:data, sanitized_data)
end
def append_to_spreadsheet
result = form.append(data)
update_column(:appended_at, Time.current) if result.updates.updated_rows > 0
end
end

26
app/models/user.rb Normal file
View File

@@ -0,0 +1,26 @@
class User < ApplicationRecord
has_many :authentications, dependent: :destroy
has_many :forms, dependent: :destroy
def self.find_by_oauth_info(auth_client)
oauth = Google::Apis::Oauth2V2::Oauth2Service.new
oauth.authorization = auth_client
user_info = oauth.get_userinfo
if user = User.find_by(google_id: user_info.id)
return user, user.authentications.last
else
user = User.create(name: user_info.name, email: user_info.email, google_id: user_info.id)
authentication = user.authentications.create(
access_token: auth_client.access_token,
refresh_token: auth_client.refresh_token,
expires_at: Time.at(auth_client.expires_at)
)
return user, authentication
end
end
def google_authorization
authentications.last.google_authorization
end
end

View File

@@ -0,0 +1,5 @@
<%= link_to "new form", new_form_url %>
<hr>
<% @forms.each do |form| %>
<%= link_to form.title, form_url(form) %>
<% end %>

View File

@@ -0,0 +1,5 @@
<%= form_for @form do |f| %>
<%= f.text_field :title %>
<%= f.text_field :thank_you_url %>
<%= f.submit %>
<% end %>

View File

@@ -0,0 +1,18 @@
<table>
<thead>
<tr>
<% @form.header_values.each do |value| %>
<th><%= value %></th>
<% end %>
</tr>
</thead>
<tbody>
<% @submissions.each do |submission| %>
<tr>
<% @form.header_values.each do |column| %>
<td><%= submission.data[column] %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>

View File

@@ -0,0 +1,3 @@
<h1>Welcome</h1>
<%= link_to "Login", login_url %>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Tinyform</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<%= yield %>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<%= yield %>
</body>
</html>

View File

@@ -0,0 +1 @@
<%= yield %>

View File

@@ -0,0 +1 @@
Thanks