Merge branch 'master' into setup/pagination
This commit is contained in:
@@ -28,7 +28,7 @@ $weight-bold: 400;
|
||||
|
||||
@import "checkmark-icon";
|
||||
@import 'demo';
|
||||
@import 'highlight';
|
||||
@import 'prism';
|
||||
|
||||
.field_with_errors input {
|
||||
border-color: $red;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
code.html .hljs-tag:first-child .hljs-attr, code.html .hljs-tag:first-child .hljs-string {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
131
app/assets/stylesheets/prism.css.scss
Normal file
131
app/assets/stylesheets/prism.css.scss
Normal file
@@ -0,0 +1,131 @@
|
||||
/* PrismJS 1.20.0
|
||||
https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript&plugins=custom-class */
|
||||
/**
|
||||
* okaidia theme for JavaScript, CSS and HTML
|
||||
* Loosely based on Monokai textmate theme by http://www.monokai.nl/
|
||||
* @author ocodia
|
||||
*/
|
||||
|
||||
code.language-html > .prism-tag:first-child {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #f8f8f2;
|
||||
background: none;
|
||||
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #272822;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.prism-token.prism-comment,
|
||||
.prism-token.prism-prolog,
|
||||
.prism-token.prism-doctype,
|
||||
.prism-token.prism-cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.prism-token.prism-punctuation {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.prism-token.prism-namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.prism-token.prism-property,
|
||||
.prism-token.prism-tag,
|
||||
.prism-token.prism-constant,
|
||||
.prism-token.prism-symbol,
|
||||
.prism-token.prism-deleted {
|
||||
color: #f92672;
|
||||
}
|
||||
|
||||
.prism-token.prism-boolean,
|
||||
.prism-token.prism-number {
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
.prism-token.prism-selector,
|
||||
.prism-token.prism-attr-name,
|
||||
.prism-token.prism-string,
|
||||
.prism-token.prism-char,
|
||||
.prism-token.prism-builtin,
|
||||
.prism-token.prism-inserted {
|
||||
color: #a6e22e;
|
||||
}
|
||||
|
||||
.prism-token.prism-operator,
|
||||
.prism-token.prism-entity,
|
||||
.prism-token.prism-url,
|
||||
.prism-language-css .prism-token.prism-string,
|
||||
.prism-style .prism-token.prism-string,
|
||||
.prism-token.prism-variable {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.prism-token.prism-atrule,
|
||||
.prism-token.prism-attr-value,
|
||||
.prism-token.prism-function,
|
||||
.prism-token.prism-class-name {
|
||||
color: #e6db74;
|
||||
}
|
||||
|
||||
.prism-token.prism-keyword {
|
||||
color: #66d9ef;
|
||||
}
|
||||
|
||||
.prism-token.prism-regex,
|
||||
.token.important {
|
||||
color: #fd971f;
|
||||
}
|
||||
|
||||
.prism-token.prism-important,
|
||||
.prism-token.prism-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.prism-token.prism-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prism-token.prism-entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
helper_method :current_user, :logged_in?
|
||||
include Pagy::Backend
|
||||
|
||||
def require_login
|
||||
redirect_to login_url unless current_user.present?
|
||||
redirect_to login_url if current_user.blank?
|
||||
end
|
||||
|
||||
def current_user
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class FileUploadsController < ApplicationController
|
||||
def show
|
||||
@form = Form.find_by!(token: params[:form_id])
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'google/apis/sheets_v4'
|
||||
require 'google/api_client/client_secrets'
|
||||
class FormsController < ApplicationController
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class HomeController < ApplicationController
|
||||
# frozen_string_literal: true
|
||||
|
||||
class HomeController < ApplicationController
|
||||
def index
|
||||
redirect_to forms_url if logged_in?
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class OauthsController < ApplicationController
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OauthsController < ApplicationController
|
||||
# Sends the user on a trip to the provider,
|
||||
# and after authorizing there back to the callback url.
|
||||
def oauth
|
||||
@@ -9,26 +10,25 @@ class OauthsController < ApplicationController
|
||||
def callback
|
||||
provider = params[:provider]
|
||||
if @user = login_from(provider)
|
||||
redirect_to root_path, :notice => "Logged in from #{provider.titleize}!"
|
||||
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)
|
||||
})
|
||||
access_token: @access_token.token,
|
||||
refresh_token: @access_token.refresh_token,
|
||||
expires_at: Time.zone.at(@access_token.expires_at)
|
||||
})
|
||||
end
|
||||
|
||||
reset_session
|
||||
auto_login(@user)
|
||||
redirect_to root_path, :notice => "Logged in from #{provider.titleize}!"
|
||||
rescue
|
||||
redirect_to root_path, notice: "Logged in from #{provider.titleize}!"
|
||||
rescue StandardError
|
||||
Rails.logger.error("Failed to login from #{provider}")
|
||||
redirect_to root_path, :alert => "Failed to login from #{provider.titleize}!"
|
||||
redirect_to root_path, alert: "Failed to login from #{provider.titleize}!"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class SessionsController < ApplicationController
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SessionsController < ApplicationController
|
||||
def new
|
||||
reset_session
|
||||
end
|
||||
@@ -8,5 +9,4 @@ class SessionsController < ApplicationController
|
||||
reset_session
|
||||
redirect_to root_url
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'google/apis/sheets_v4'
|
||||
class SubmissionsController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token
|
||||
@@ -14,10 +16,14 @@ 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.html do
|
||||
redirect_to(@form.thank_you_url) if @form.thank_you_url.present?
|
||||
end
|
||||
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.html do
|
||||
redirect_to(@form.thank_you_url) if @form.thank_you_url.present?
|
||||
end
|
||||
format.json { render(json: { error: @submission.errors }, status: 422) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationHelper
|
||||
include Pagy::Frontend
|
||||
end
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
document.addEventListener("turbolinks:load", function () {
|
||||
document.querySelectorAll('pre code').forEach(function (block) {
|
||||
if (window.hljs) {
|
||||
hljs.highlightBlock(block);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ require('tinyforms');
|
||||
require('demo');
|
||||
require('backend_typed');
|
||||
require('tabs');
|
||||
require('highlight');
|
||||
require('prism');
|
||||
|
||||
// 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' %>)
|
||||
|
||||
17
app/javascript/prism.js
Normal file
17
app/javascript/prism.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationJob < ActiveJob::Base
|
||||
# Automatically retry jobs that encountered a deadlock
|
||||
# retry_on ActiveRecord::Deadlocked
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionAppendJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
rescue_from(Signet::AuthorizationError, Google::Apis::AuthorizationError) do |exception|
|
||||
submission_id = self.arguments.first
|
||||
rescue_from(Signet::AuthorizationError, Google::Apis::AuthorizationError) do |_exception|
|
||||
submission_id = arguments.first
|
||||
Rails.logger.error("AuthorizationError during SubmissionAppend: submission_id=#{submission_id}")
|
||||
submission = Submission.find(submission_id)
|
||||
submission.form.deactivate!('AuthorizationError')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: 'from@example.com'
|
||||
layout 'mailer'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Authentication < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
scope :for, -> (provider) { where(provider: provider) }
|
||||
scope :for, ->(provider) { where(provider: provider) }
|
||||
|
||||
encrypts :access_token
|
||||
encrypts :refresh_token
|
||||
@@ -10,19 +12,24 @@ class Authentication < ApplicationRecord
|
||||
expires_at <= Time.current
|
||||
end
|
||||
|
||||
def refresh_from(client_secret)
|
||||
client_secret.refresh!
|
||||
self.access_token = client_secret.access_token if client_secret.access_token.present?
|
||||
self.refresh_token = client_secret.refresh_token if client_secret.refresh_token.present?
|
||||
self.expires_at = Time.zone.at(client_secret.expires_at) if client_secret.expires_at.present?
|
||||
save
|
||||
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
|
||||
c.expires_at = self.expires_at
|
||||
c.access_token = access_token
|
||||
c.refresh_token = refresh_token
|
||||
c.expires_at = 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?
|
||||
refresh_from(c)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Form < ApplicationRecord
|
||||
belongs_to :user
|
||||
has_many :submissions, dependent: :destroy
|
||||
@@ -10,12 +12,12 @@ class Form < ApplicationRecord
|
||||
encrypts :airtable_api_key
|
||||
encrypts :airtable_app_key
|
||||
|
||||
validates_presence_of :title
|
||||
validates_inclusion_of :backend_name, in: ['google_sheets', 'airtable']
|
||||
validates :title, presence: true
|
||||
validates :backend_name, inclusion: { in: %w[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?
|
||||
validates :airtable_api_key, presence: { if: :airtable? }
|
||||
validates :airtable_app_key, presence: { if: :airtable? }
|
||||
validates :airtable_table, presence: { if: :airtable? }
|
||||
|
||||
# TODO: use counter_cache option on association
|
||||
def submissions_count
|
||||
@@ -27,12 +29,10 @@ class Form < ApplicationRecord
|
||||
end
|
||||
|
||||
def deactivate!(reason = nil)
|
||||
self.user.deactivate!(reason)
|
||||
user.deactivate!(reason)
|
||||
end
|
||||
|
||||
def active?
|
||||
self.user.active?
|
||||
end
|
||||
delegate :active?, to: :user
|
||||
|
||||
def airtable?
|
||||
backend_name == 'airtable'
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Submission < ApplicationRecord
|
||||
belongs_to :form
|
||||
has_many_attached :files
|
||||
|
||||
acts_as_sequenced scope: :form_id
|
||||
|
||||
validates_presence_of :data, if: :appended_at?
|
||||
validates :data, presence: { if: :appended_at? }
|
||||
|
||||
def process_data(submitted_data)
|
||||
processed_data = {}
|
||||
@@ -12,7 +14,7 @@ class Submission < ApplicationRecord
|
||||
processed_data[key] = submission_value_for(value)
|
||||
end
|
||||
update_attribute(:data, processed_data)
|
||||
SubmissionAppendJob.perform_later(self.id)
|
||||
SubmissionAppendJob.perform_later(id)
|
||||
end
|
||||
|
||||
def submission_value_for(value)
|
||||
@@ -37,7 +39,8 @@ 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.token, host: DEFAULT_HOST, filename: attachment.blob.filename)
|
||||
Rails.application.routes.url_helpers.file_upload_url(form_id: form, submission_id: self, id: attachment.token,
|
||||
host: DEFAULT_HOST, filename: attachment.blob.filename)
|
||||
else
|
||||
value.to_s
|
||||
end
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class User < ApplicationRecord
|
||||
authenticates_with_sorcery!
|
||||
has_many :authentications, dependent: :destroy
|
||||
has_many :forms, dependent: :destroy
|
||||
|
||||
def deactivate!(reason = nil)
|
||||
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
|
||||
|
||||
@@ -6,6 +6,16 @@
|
||||
<%= text_field_tag header %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if form.spreadsheet_headers.blank? %>
|
||||
<p>
|
||||
<label for="Name">Name</label>
|
||||
<input type="text" name="Name" required>
|
||||
</p>
|
||||
<p>
|
||||
<label for="Email">Email</label>
|
||||
<input type="email" name="Email" required>
|
||||
</p>
|
||||
<% end %>
|
||||
...
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
@@ -7,5 +7,4 @@
|
||||
</p>
|
||||
<% end %>
|
||||
...
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<p>
|
||||
Please set your form action as in the example below. Make sure to use proper <code>name</code> attributes.
|
||||
<p>
|
||||
<pre><code class="html">
|
||||
<pre><code class="language-html">
|
||||
<%= html_escape_once render('form_example', form: @form) %>
|
||||
</code></pre>
|
||||
|
||||
@@ -50,12 +50,9 @@
|
||||
<p>
|
||||
Learn more about our <a href="https://www.notion.so/JavaScript-Library-442071548edd4cd1af9c7d8edb3d42cb">JavaScript library</a> and our <a href="https://www.notion.so/Tinyforms-API-7862355e22ce4bbead5de3b635cda55e">API</a> in your help section.
|
||||
</p>
|
||||
<pre><code class="html">
|
||||
<pre><code class="language-html">
|
||||
<%= html_escape_once render('form_example_js', form: @form) %>
|
||||
</code></pre>
|
||||
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.0.0/build/styles/railscasts.min.css">
|
||||
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.0.0/build/highlight.min.js"></script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,22 +24,23 @@
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
|
||||
<h3 class="title">Setup your online form:</h3>
|
||||
<h3 class="title">One more step:</h3>
|
||||
<p>
|
||||
Your form submission URL: <code><%= submission_url(@form) %></code>
|
||||
</p>
|
||||
<p>
|
||||
Set the <code>action</code> of your online form to this URL and make sure your fields have proper <code>name</code> attributes.
|
||||
Set the <code>action</code> of your online form to <code><%= submission_url(@form) %></code> and make sure your fields have proper <code>name</code> attributes.
|
||||
</p>
|
||||
<h4 class="subtitle" style="margin-top:1em">Example:</h4>
|
||||
<pre><code class="html">
|
||||
<pre><code class="language-html">
|
||||
<%= html_escape_once render('form_example', form: @form) %>
|
||||
</code></pre>
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.0.0/build/styles/default.min.css">
|
||||
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.0.0/build/highlight.min.js"></script>
|
||||
|
||||
<p>
|
||||
Let us know if you need <%= link_to 'help', help_url %> - we also build the form for you!
|
||||
If you prefer to submit the form using JavaScript/AJAX have a look at our <a href="https://www.notion.so/JavaScript-Library-442071548edd4cd1af9c7d8edb3d42cb">JavaScript library</a> and our <a href="https://www.notion.so/Tinyforms-API-7862355e22ce4bbead5de3b635cda55e">API</a>.
|
||||
</p>
|
||||
<p>
|
||||
If you need help, <%= link_to "we're here!", help_url %>
|
||||
</p>
|
||||
<p style="margin-top:15px">
|
||||
And if you do not want to deal with your form at all, have a look at our <%= link_to 'form building service', form_building_service_url %>.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<% end %>
|
||||
<hr>
|
||||
<p>
|
||||
Simply login with your existing Google account.
|
||||
Login with your existing Google account, no new credentials needed.
|
||||
<br>
|
||||
Do you need help? <%= link_to 'let us know', help_url %>.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user