Merge branch 'master' into New-forms-page

This commit is contained in:
Yannick
2020-04-13 19:59:25 +02:00
29 changed files with 887 additions and 125 deletions

View File

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

View File

@@ -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;
}

View 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;
}

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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
View 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;
});
});

View File

@@ -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' %>)

View 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();
});

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View 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>

View 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>

View File

@@ -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>

View 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>