Merge branch 'master' into New-forms-page
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
//= link_tree ../images
|
||||
//= link_directory ../stylesheets .css
|
||||
//= link application.css
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
@import "bulma/sass/utilities/initial-variables";
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Lobster&family=Comfortaa:wght@400;500;600;700&display=swap');
|
||||
@import url('https://use.fontawesome.com/releases/v5.12.0/css/all.css');
|
||||
@@ -40,6 +25,8 @@ $light: #fff;
|
||||
|
||||
@import 'bulma/bulma';
|
||||
|
||||
@import './demo';
|
||||
|
||||
.is-font-logo {
|
||||
font-family: 'Lobster', cursive;
|
||||
}
|
||||
@@ -60,4 +47,9 @@ $light: #fff;
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.button.google {
|
||||
background: rgb(225, 98, 89);
|
||||
color: white;
|
||||
}
|
||||
16
app/assets/stylesheets/demo.css.scss
Normal file
16
app/assets/stylesheets/demo.css.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
section.demo {
|
||||
height: 90vh;
|
||||
min-height: 600px;
|
||||
}
|
||||
.demo-form {
|
||||
border: 1px solid $grey-light;
|
||||
padding: 1em;
|
||||
height: 100%;
|
||||
}
|
||||
section.demo .container, section.demo .columns, section.demo .column {
|
||||
height: 100%;
|
||||
}
|
||||
#demo-success {
|
||||
text-align: center;
|
||||
padding-top: 1em;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ class FileUploadsController < ApplicationController
|
||||
def show
|
||||
@form = Form.find_by!(token: params[:form_id])
|
||||
@submission = @form.submissions.find(params[:submission_id])
|
||||
@file_upload = @submission.files_attachments.find(params[:id])
|
||||
@file_upload = @submission.files_attachments.find_by!(token: params[:id])
|
||||
redirect_to url_for(@file_upload)
|
||||
end
|
||||
end
|
||||
|
||||
34
app/controllers/oauths_controller.rb
Normal file
34
app/controllers/oauths_controller.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
class OauthsController < ApplicationController
|
||||
|
||||
# Sends the user on a trip to the provider,
|
||||
# and after authorizing there back to the callback url.
|
||||
def oauth
|
||||
login_at(params[:provider])
|
||||
end
|
||||
|
||||
def callback
|
||||
provider = params[:provider]
|
||||
if @user = login_from(provider)
|
||||
redirect_to root_path, :notice => "Logged in from #{provider.titleize}!"
|
||||
else
|
||||
begin
|
||||
@user = create_from(provider)
|
||||
if authentication = @user.authentications.find_by(provider: provider)
|
||||
authentication.update({
|
||||
access_token: @access_token.token,
|
||||
refresh_token: @access_token.refresh_token,
|
||||
expires_at: Time.at(@access_token.expires_at)
|
||||
})
|
||||
end
|
||||
|
||||
reset_session
|
||||
auto_login(@user)
|
||||
redirect_to root_path, :notice => "Logged in from #{provider.titleize}!"
|
||||
rescue
|
||||
Rails.logger.error("Failed to login from #{provider}")
|
||||
redirect_to root_path, :alert => "Failed to login from #{provider.titleize}!"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,29 +1,7 @@
|
||||
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
|
||||
@@ -31,18 +9,4 @@ class SessionsController < ApplicationController
|
||||
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
|
||||
|
||||
@@ -14,7 +14,7 @@ class SubmissionsController < ApplicationController
|
||||
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 }) }
|
||||
format.json { render(json: { success: true, submission: @submission.data }) }
|
||||
else
|
||||
format.html { redirect_to(@form.thank_you_url) if @form.thank_you_url.present? }
|
||||
format.json { render(json: { error: @submission.errors }, status: 422) }
|
||||
|
||||
17
app/javascript/demo.js
Normal file
17
app/javascript/demo.js
Normal file
@@ -0,0 +1,17 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var demoForm = document.getElementById('demo-form');
|
||||
if (!demoForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
demoForm.addEventListener('tinyforms:submitted', function(e) {
|
||||
console.log(e);
|
||||
console.log(e.detail);
|
||||
var name = document.getElementById('demo-submission-name');
|
||||
var demoFields = document.getElementById('demo-fields');
|
||||
var demoSucess = document.getElementById('demo-success');
|
||||
demoFields.style.display = 'none';
|
||||
demoSucess.style.display = 'block';
|
||||
name.innerText = e.detail.submission.Name;
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,8 @@ require("@rails/activestorage").start()
|
||||
require("channels")
|
||||
|
||||
require('burger_menu');
|
||||
require('tinyforms');
|
||||
require('demo');
|
||||
|
||||
// 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' %>)
|
||||
|
||||
51
app/javascript/tinyforms.js
Normal file
51
app/javascript/tinyforms.js
Normal file
@@ -0,0 +1,51 @@
|
||||
window.tinyforms = {
|
||||
init: function() {
|
||||
var forms = document.querySelectorAll('form[data-tinyforms]');
|
||||
forms.forEach(function(form) {
|
||||
form.addEventListener('submit', tinyforms.onSubmit);
|
||||
});
|
||||
},
|
||||
|
||||
submitForm: function(form) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var action = form.getAttribute('action');
|
||||
var formData = new FormData(form);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', action, true);
|
||||
xhr.setRequestHeader("Accept", "application/json");
|
||||
xhr.onload = function () {
|
||||
var response;
|
||||
try {
|
||||
response = JSON.parse(xhr.responseText);
|
||||
} catch (e) {
|
||||
response = xhr.responseText;
|
||||
}
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(response);
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
reject(xhr.responseText)
|
||||
};
|
||||
xhr.send(formData);
|
||||
});
|
||||
},
|
||||
|
||||
onSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
return tinyforms.submitForm(this)
|
||||
.then((response) => {
|
||||
this.dispatchEvent(new CustomEvent('tinyforms:submitted', {detail: { submission: response.submission, response: response }}));
|
||||
})
|
||||
.catch((response) => {
|
||||
this.dispatchEvent(new CustomEvent('tinyforms:error', {detail: { submission: response.submission, response: response }}));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
tinyforms.init();
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
class Authentication < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
scope :for, -> (provider) { where(provider: provider) }
|
||||
|
||||
encrypts :access_token
|
||||
encrypts :refresh_token
|
||||
|
||||
@@ -9,6 +11,7 @@ class Authentication < ApplicationRecord
|
||||
end
|
||||
|
||||
def google_authorization
|
||||
return nil unless provider == 'google'
|
||||
@google_authorization ||= CLIENT_SECRETS.to_authorization.tap do |c|
|
||||
c.access_token = self.access_token
|
||||
c.refresh_token = self.refresh_token
|
||||
|
||||
@@ -13,11 +13,11 @@ class Form < ApplicationRecord
|
||||
|
||||
# TODO: use counter_cache option on association
|
||||
def submissions_count
|
||||
submissions.count
|
||||
@submissions_count ||= submissions.count
|
||||
end
|
||||
|
||||
def last_submission_date
|
||||
submissions.order(created_at: :desc).first&.created_at
|
||||
@last_submission_date ||= submissions.order(created_at: :desc).first&.created_at
|
||||
end
|
||||
|
||||
def deactivate!(reason = nil)
|
||||
|
||||
@@ -2,6 +2,8 @@ class Submission < ApplicationRecord
|
||||
belongs_to :form
|
||||
has_many_attached :files
|
||||
|
||||
acts_as_sequenced scope: :form_id
|
||||
|
||||
validates_presence_of :data, if: :appended_at?
|
||||
|
||||
def process_data(submitted_data)
|
||||
@@ -21,6 +23,10 @@ class Submission < ApplicationRecord
|
||||
JSON.dump(value)
|
||||
when 'tinyforms_now'
|
||||
Time.now.utc.to_formatted_s(:rfc822)
|
||||
when 'tinyforms_token'
|
||||
form.token
|
||||
when 'tinyforms_id'
|
||||
sequential_id
|
||||
when ActionDispatch::Http::UploadedFile
|
||||
# manually create the ActiveStorage attachment because we need the ID of the Attachment to create the URL
|
||||
# first the file needs to be uplaoded then we can create an Attachment
|
||||
@@ -31,7 +37,7 @@ class Submission < ApplicationRecord
|
||||
attachment = ActiveStorage::Attachment.new(record: self, name: 'files', blob: create_one.blob)
|
||||
attachment.save
|
||||
# return the URL that we use to show in the Spreadsheet
|
||||
Rails.application.routes.url_helpers.file_upload_url(form_id: form, submission_id: self, id: attachment.id, host: DEFAULT_HOST)
|
||||
Rails.application.routes.url_helpers.file_upload_url(form_id: form, submission_id: self, id: attachment.token, host: DEFAULT_HOST)
|
||||
else
|
||||
value.to_s
|
||||
end
|
||||
|
||||
@@ -1,40 +1,18 @@
|
||||
class User < ApplicationRecord
|
||||
authenticates_with_sorcery!
|
||||
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)
|
||||
authentication = user.authentications.last
|
||||
authentication.access_token = auth_client.access_token if auth_client.access_token.present?
|
||||
authentication.refresh_token = auth_client.refresh_token if auth_client.refresh_token.present?
|
||||
authentication.expires_at = Time.at(auth_client.expires_at) if auth_client.expires_at.present?
|
||||
authentication.save
|
||||
return user, authentication
|
||||
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 deactivate!(reason = nil)
|
||||
# currently we only use deactivate if we get an authentication exception appending data to a spreadsheet
|
||||
authentications.last&.update(expires_at: Time.current)
|
||||
end
|
||||
|
||||
def active?
|
||||
authentications.last.present? && !authentications.last.expired?
|
||||
authentications.any? { |a| !a.expired? }
|
||||
end
|
||||
|
||||
def google_authorization
|
||||
authentications.last.google_authorization
|
||||
authentications.for(:google).last.google_authorization
|
||||
end
|
||||
end
|
||||
|
||||
85
app/views/home/demo.html.erb
Normal file
85
app/views/home/demo.html.erb
Normal file
@@ -0,0 +1,85 @@
|
||||
<section class="section demo">
|
||||
<div class="container content">
|
||||
<div class="columns">
|
||||
<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">
|
||||
<h3>Demo</h3>
|
||||
<p>
|
||||
This short form is connected to the embedded <%= link_to 'document', DEMO_FORM.google_spreadsheet_url %> on the right. <br>
|
||||
Submit the form and see the update of the document in realtime.
|
||||
</p>
|
||||
|
||||
<div id="demo-success" style="display:none;">
|
||||
<p class="is-size-4 has-text-success">
|
||||
<span id="demo-submission-name"></span>, thanks for your submission!
|
||||
</p>
|
||||
<p>See your entry in the spreadsheet?!</p>
|
||||
<p>
|
||||
<%= link_to "Get started now", signup_url, class: 'button' %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="demo-fields">
|
||||
<div class="field">
|
||||
<label class="label">What's your name?*</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="Name" required="true" placeholder="Your name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Where are you from?*</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="Continent">
|
||||
<option value="Africa">Africa</option>
|
||||
<option value="Antartica">Antartica</option>
|
||||
<option value="Asia">Asia</option>
|
||||
<option value="Australia">Australia</option>
|
||||
<option value="Europe">Europe</option>
|
||||
<option value="North America">North America</option>
|
||||
<option value="South America">South America</option>
|
||||
<option value="Australia">Australia</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Do you like Pizza with Pinapples?*</label>
|
||||
<div class="control">
|
||||
<label class="radio">
|
||||
<input type="radio" name="Pinapples Pizza?" required value="yes">
|
||||
Yes
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="Pinapples Pizza?" required value="no">
|
||||
Hell, no!
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">What's your favorit cat picture?</label>
|
||||
<div class="control">
|
||||
<input class="input" type="file" name="Cat Picture">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-link">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p class="has-text-grey has-text-centered">Document not loading? <%= link_to 'Open it here', DEMO_FORM.google_spreadsheet_url %>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
29
app/views/layouts/_header.html.erb
Normal file
29
app/views/layouts/_header.html.erb
Normal file
@@ -0,0 +1,29 @@
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item is-size-4 has-text-black has-text-weight-bold is-font-logo" href="/">
|
||||
TinyForms
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
|
||||
data-target="navbar-menu">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbar-menu" class="navbar-menu">
|
||||
<div class="navbar-end">
|
||||
<a href="https://www.notion.so/Tinyforms-Help-Center-04f13b5908bc46cfb4283079a3cb1149" class="navbar-item">Help</a>
|
||||
<div class="navbar-item">
|
||||
<% if !logged_in? -%>
|
||||
<%= link_to "Login", login_url, { :class => "button is-primary"} %>
|
||||
<% else -%>
|
||||
<%= link_to "Logout", logout_url, { :class => "button is-light"} %>
|
||||
<% end -%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -11,49 +11,12 @@
|
||||
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Wrapper -->
|
||||
<div id="wrapper" class="has-text-centered-mobile">
|
||||
<body class="has-text-centered-mobile">
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="hero is-medium is-light">
|
||||
<div class="hero-head">
|
||||
<div class="container">
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item is-size-4 has-text-black has-text-weight-bold is-font-logo" href="/">
|
||||
TinyForms
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
|
||||
data-target="navbar">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbar" class="navbar-menu">
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<% if !logged_in? -%>
|
||||
<%= link_to "Login", login_url, { :class => "button is-primary"} %>
|
||||
<% else -%>
|
||||
<%= link_to "Logout", logout_url, { :class => "button is-light"} %>
|
||||
<% end -%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main>
|
||||
<%= yield %>
|
||||
</main>
|
||||
</div>
|
||||
<%= render 'layouts/header' %>
|
||||
<main>
|
||||
<%= yield %>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
22
app/views/sessions/new.html.erb
Normal file
22
app/views/sessions/new.html.erb
Normal file
@@ -0,0 +1,22 @@
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="container has-text-centered">
|
||||
<h1 class="title has-text-centered">
|
||||
Login
|
||||
</h1>
|
||||
<div class="button google">
|
||||
<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
|
||||
</div>
|
||||
<hr>
|
||||
<p>
|
||||
As tinyforms builds on Google Sheets.<br>
|
||||
Simply login with your Google account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user