mirror of https://github.com/docusealco/docuseal
commit
d86b16de6b
@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: email_message_assets
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# data :text not null
|
||||||
|
# sha1 :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_email_message_assets_on_account_id_and_sha1 (account_id,sha1) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
|
#
|
||||||
|
class EmailMessageAsset < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
before_validation :set_sha1, on: :create
|
||||||
|
|
||||||
|
def set_sha1
|
||||||
|
self.sha1 = Digest::SHA1.hexdigest(data.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateEmailMessageAssets < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
create_table :email_message_assets do |t|
|
||||||
|
t.references :account, null: false, foreign_key: true, index: false
|
||||||
|
t.text :data, null: false
|
||||||
|
t.string :sha1, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :email_message_assets, %i[account_id sha1], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddSubmissionCompletedAt < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
add_column :submissions, :completed_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PopulateSubmissionCompletedAt < ActiveRecord::Migration[8.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
class MigrationSubmission < ApplicationRecord
|
||||||
|
self.table_name = 'submissions'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
max_id = MigrationSubmission.maximum(:id)
|
||||||
|
|
||||||
|
return unless max_id
|
||||||
|
|
||||||
|
max_completed_at =
|
||||||
|
Arel::Nodes::Grouping.new(
|
||||||
|
Submitter.arel_table.project(Submitter.arel_table[:completed_at].maximum)
|
||||||
|
.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
|
||||||
|
.ast
|
||||||
|
)
|
||||||
|
|
||||||
|
(1..max_id).step(10_000) do |start_id|
|
||||||
|
range = start_id...(start_id + 10_000)
|
||||||
|
|
||||||
|
incomplete_submitter =
|
||||||
|
Submitter.where(completed_at: nil, submission_id: range)
|
||||||
|
.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
|
||||||
|
.select(1)
|
||||||
|
|
||||||
|
MigrationSubmission.where(completed_at: nil, id: range)
|
||||||
|
.where.not(incomplete_submitter.arel.exists)
|
||||||
|
.update_all(completed_at: max_completed_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexOnSubmissionsCompletedAt < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
add_index :submissions, %i[account_id completed_at],
|
||||||
|
where: 'completed_at IS NOT NULL AND archived_at IS NULL',
|
||||||
|
name: 'index_submissions_on_account_id_and_completed_at',
|
||||||
|
if_not_exists: true
|
||||||
|
|
||||||
|
return unless connection.supports_partial_index?
|
||||||
|
|
||||||
|
add_index :submissions, %i[account_id id],
|
||||||
|
where: 'completed_at IS NULL AND archived_at IS NULL',
|
||||||
|
name: 'index_submissions_on_account_id_and_id_pending',
|
||||||
|
if_not_exists: true
|
||||||
|
end
|
||||||
|
end
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,70 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module EmailMessages
|
module EmailMessages
|
||||||
|
MIN_BODY_SIZE = 2.kilobytes
|
||||||
|
MIN_ASSET_SIZE = 256.bytes
|
||||||
|
STYLE_REGEXP = %r{<style[^>]*>.*?</style>(?:\s*<style[^>]*>.*?</style>)*}mi
|
||||||
|
BASE64_REGEXP = %r{(data:[^,]*;base64,)([A-Za-z0-9+/=]+)}
|
||||||
|
ASSET_REGEXP = Regexp.union(STYLE_REGEXP, BASE64_REGEXP)
|
||||||
|
ASSET_PREFIX = '[[asset:'
|
||||||
|
PLACEHOLDER_REGEXP = /\[\[asset:(\h{40})\]\]/
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def find_or_create_for_account_user(account, user, subject, body)
|
def find_or_create_for_account_user(account, user, subject, body)
|
||||||
subject = I18n.t(:you_are_invited_to_sign_a_document) if subject.blank?
|
subject = I18n.t(:you_are_invited_to_sign_a_document) if subject.blank?
|
||||||
|
|
||||||
message = account.email_messages.new(author: user, subject:, body:).tap(&:validate)
|
body, assets = maybe_extract_assets(account, body)
|
||||||
|
|
||||||
|
new_message = account.email_messages.new(author: user, subject:, body:).tap(&:validate)
|
||||||
|
|
||||||
|
message = account.email_messages.find_by(sha1: new_message.sha1)
|
||||||
|
|
||||||
|
message ||= new_message.tap do |m|
|
||||||
|
m.save!(validate: false)
|
||||||
|
|
||||||
|
save_new_assets!(account, assets)
|
||||||
|
end
|
||||||
|
|
||||||
|
message
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_new_assets!(account, assets)
|
||||||
|
return if assets.blank?
|
||||||
|
|
||||||
|
existing_assets_sha1 = account.email_message_assets.where(sha1: assets.map(&:sha1)).pluck(:sha1)
|
||||||
|
|
||||||
|
assets.each do |asset|
|
||||||
|
asset.save!(validate: false) if existing_assets_sha1.exclude?(asset.sha1)
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_extract_assets(account, body)
|
||||||
|
return [body, []] if body.blank? || body.bytesize < MIN_BODY_SIZE
|
||||||
|
|
||||||
|
assets_index = {}
|
||||||
|
|
||||||
|
result = body.gsub(ASSET_REGEXP) do
|
||||||
|
match = Regexp.last_match
|
||||||
|
prefix, data = match[1] ? [match[1], match[2]] : ['', match[0]]
|
||||||
|
|
||||||
|
next match[0] if data.blank? || data.bytesize < MIN_ASSET_SIZE
|
||||||
|
|
||||||
|
asset = account.email_message_assets.new(data:).tap(&:validate)
|
||||||
|
assets_index[asset.sha1] = asset
|
||||||
|
|
||||||
|
"#{prefix}#{ASSET_PREFIX}#{asset.sha1}]]"
|
||||||
|
end
|
||||||
|
|
||||||
|
[result, assets_index.values]
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebuild_body_with_assets(account_id, body)
|
||||||
|
shas = body.scan(PLACEHOLDER_REGEXP).flatten.uniq
|
||||||
|
data = EmailMessageAsset.where(account_id:, sha1: shas).pluck(:sha1, :data).to_h
|
||||||
|
|
||||||
account.email_messages.find_by(sha1: message.sha1) || message.tap { |m| m.save!(validate: false) }
|
body.gsub(PLACEHOLDER_REGEXP) { data[Regexp.last_match(1)] || Regexp.last_match(0) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe 'Submission' do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:submission) do
|
||||||
|
create(:submission, :with_submitters, template:, created_by_user: user,
|
||||||
|
archived_at: Time.current, completed_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
submission.submitters.each { |s| s.update!(completed_at: 1.day.ago) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unarchives a completed submission from the download dropdown' do
|
||||||
|
visit submission_path(submission)
|
||||||
|
|
||||||
|
find('label[aria-label="Download"]').click
|
||||||
|
click_button 'Unarchive'
|
||||||
|
|
||||||
|
expect(page).to have_content('Submission has been unarchived.')
|
||||||
|
expect(submission.reload.archived_at).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in new issue