commit
5d8b32e972
21 changed files with 183 additions and 37 deletions
1
Gemfile
1
Gemfile
|
@ -44,6 +44,7 @@ gem 'haml-rails'
|
||||||
gem 'hashie'
|
gem 'hashie'
|
||||||
gem 'http_accept_language'
|
gem 'http_accept_language'
|
||||||
gem 'iban-tools'
|
gem 'iban-tools'
|
||||||
|
gem 'image_processing'
|
||||||
gem 'jquery-rails' # Use jquery as the JavaScript library
|
gem 'jquery-rails' # Use jquery as the JavaScript library
|
||||||
gem 'jwt'
|
gem 'jwt'
|
||||||
gem 'kaminari', '1.2.1' # Pagination
|
gem 'kaminari', '1.2.1' # Pagination
|
||||||
|
|
|
@ -357,6 +357,9 @@ GEM
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
iban-tools (1.1.0)
|
iban-tools (1.1.0)
|
||||||
ice_nine (0.11.2)
|
ice_nine (0.11.2)
|
||||||
|
image_processing (1.12.1)
|
||||||
|
mini_magick (>= 4.9.5, < 5)
|
||||||
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
jquery-rails (4.4.0)
|
jquery-rails (4.4.0)
|
||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
|
@ -415,6 +418,7 @@ GEM
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2020.0512)
|
mime-types-data (3.2020.0512)
|
||||||
mimemagic (0.3.5)
|
mimemagic (0.3.5)
|
||||||
|
mini_magick (4.11.0)
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
minitest (5.14.2)
|
minitest (5.14.2)
|
||||||
|
@ -630,6 +634,8 @@ GEM
|
||||||
rexml
|
rexml
|
||||||
ruby-progressbar (1.10.1)
|
ruby-progressbar (1.10.1)
|
||||||
ruby-saml-idp (0.3.5)
|
ruby-saml-idp (0.3.5)
|
||||||
|
ruby-vips (2.0.17)
|
||||||
|
ffi (~> 1.9)
|
||||||
ruby2_keywords (0.0.2)
|
ruby2_keywords (0.0.2)
|
||||||
ruby_parser (3.15.0)
|
ruby_parser (3.15.0)
|
||||||
sexp_processor (~> 4.9)
|
sexp_processor (~> 4.9)
|
||||||
|
@ -827,6 +833,7 @@ DEPENDENCIES
|
||||||
hashie
|
hashie
|
||||||
http_accept_language
|
http_accept_language
|
||||||
iban-tools
|
iban-tools
|
||||||
|
image_processing
|
||||||
jquery-rails
|
jquery-rails
|
||||||
jwt
|
jwt
|
||||||
kaminari (= 1.2.1)
|
kaminari (= 1.2.1)
|
||||||
|
|
BIN
app/assets/images/watermark.png
Normal file
BIN
app/assets/images/watermark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.7 KiB |
|
@ -105,7 +105,7 @@ module Users
|
||||||
sanitized_siret = siret_model.siret
|
sanitized_siret = siret_model.siret
|
||||||
begin
|
begin
|
||||||
etablissement = ApiEntrepriseService.create_etablissement(@dossier, sanitized_siret, current_user.id)
|
etablissement = ApiEntrepriseService.create_etablissement(@dossier, sanitized_siret, current_user.id)
|
||||||
rescue ApiEntreprise::API::RequestFailed
|
rescue ApiEntreprise::API::RequestFailed, ApiEntreprise::API::BadGateway
|
||||||
return render_siret_error(t('errors.messages.siret_network_error'))
|
return render_siret_error(t('errors.messages.siret_network_error'))
|
||||||
end
|
end
|
||||||
if etablissement.nil?
|
if etablissement.nil?
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
class ApiEntreprise::AttestationSocialeJob < ApiEntreprise::Job
|
class ApiEntreprise::AttestationSocialeJob < ApiEntreprise::Job
|
||||||
retry_on ApiEntreprise::API::ServiceUnavailable, wait: 1.day
|
|
||||||
|
|
||||||
def perform(etablissement_id, procedure_id)
|
def perform(etablissement_id, procedure_id)
|
||||||
etablissement = Etablissement.find(etablissement_id)
|
etablissement = Etablissement.find(etablissement_id)
|
||||||
etablissement_params = ApiEntreprise::AttestationSocialeAdapter.new(etablissement.siret, procedure_id).to_params
|
etablissement_params = ApiEntreprise::AttestationSocialeAdapter.new(etablissement.siret, procedure_id).to_params
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
class ApiEntreprise::Job < ApplicationJob
|
class ApiEntreprise::Job < ApplicationJob
|
||||||
queue_as :api_entreprise
|
queue_as :api_entreprise
|
||||||
|
|
||||||
|
retry_on ApiEntreprise::API::ServiceUnavailable,
|
||||||
|
ApiEntreprise::API::BadGateway,
|
||||||
|
wait: 1.day
|
||||||
|
|
||||||
DEFAULT_MAX_ATTEMPTS_API_ENTREPRISE_JOBS = 5
|
DEFAULT_MAX_ATTEMPTS_API_ENTREPRISE_JOBS = 5
|
||||||
|
|
||||||
# If by the time the job runs the Etablissement has been deleted
|
# If by the time the job runs the Etablissement has been deleted
|
||||||
|
|
70
app/jobs/titre_identite_watermark_job.rb
Normal file
70
app/jobs/titre_identite_watermark_job.rb
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
class TitreIdentiteWatermarkJob < ApplicationJob
|
||||||
|
queue_as :active_storage_watermark
|
||||||
|
|
||||||
|
MAX_IMAGE_SIZE = 1500
|
||||||
|
SCALE = 0.9
|
||||||
|
WATERMARK = Rails.root.join("app/assets/images/#{WATERMARK_FILE}")
|
||||||
|
|
||||||
|
def perform(blob)
|
||||||
|
blob.open do |file|
|
||||||
|
watermark = resize_watermark(file)
|
||||||
|
processed = watermark_image(file, watermark)
|
||||||
|
|
||||||
|
blob.metadata[:watermark] = true
|
||||||
|
blob.upload(processed)
|
||||||
|
blob.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def watermark_image(file, watermark)
|
||||||
|
ImageProcessing::MiniMagick
|
||||||
|
.source(file)
|
||||||
|
.convert("png")
|
||||||
|
.resize_to_limit(MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
|
||||||
|
.composite(watermark, mode: "over", gravity: "center")
|
||||||
|
.call
|
||||||
|
end
|
||||||
|
|
||||||
|
def resize_watermark(file)
|
||||||
|
metadata = image_metadata(file)
|
||||||
|
|
||||||
|
width = [metadata[:width], MAX_IMAGE_SIZE].min * SCALE
|
||||||
|
height = [metadata[:height], MAX_IMAGE_SIZE].min * SCALE
|
||||||
|
diagonal = Math.sqrt(height**2 + width**2)
|
||||||
|
angle = Math.asin(height / diagonal) * 180 / Math::PI
|
||||||
|
|
||||||
|
ImageProcessing::MiniMagick
|
||||||
|
.source(WATERMARK)
|
||||||
|
.resize_to_limit(diagonal, diagonal / 2)
|
||||||
|
.rotate(-angle, background: :transparent)
|
||||||
|
.call
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_metadata(file)
|
||||||
|
read_image(file) do |image|
|
||||||
|
if rotated_image?(image)
|
||||||
|
{ width: image.height, height: image.width }
|
||||||
|
else
|
||||||
|
{ width: image.width, height: image.height }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_image(file)
|
||||||
|
require "mini_magick"
|
||||||
|
image = MiniMagick::Image.new(file.path)
|
||||||
|
|
||||||
|
if image.valid?
|
||||||
|
yield image
|
||||||
|
else
|
||||||
|
logger.info "Skipping image analysis because ImageMagick doesn't support the file"
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rotated_image?(image)
|
||||||
|
['RightTop', 'LeftBottom'].include?(image["%[orientation]"])
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,6 +21,9 @@ class ApiEntreprise::API
|
||||||
class BadFormatRequest < StandardError
|
class BadFormatRequest < StandardError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class BadGateway < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
class ServiceUnavailable < StandardError
|
class ServiceUnavailable < StandardError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -97,10 +100,19 @@ class ApiEntreprise::API
|
||||||
raise ResourceNotFound, "url: #{url}"
|
raise ResourceNotFound, "url: #{url}"
|
||||||
elsif response.code == 400
|
elsif response.code == 400
|
||||||
raise BadFormatRequest, "url: #{url}"
|
raise BadFormatRequest, "url: #{url}"
|
||||||
|
elsif response.code == 502
|
||||||
|
raise BadGateway, "url: #{url}"
|
||||||
elsif response.code == 503
|
elsif response.code == 503
|
||||||
raise ServiceUnavailable, "url: #{url}"
|
raise ServiceUnavailable, "url: #{url}"
|
||||||
else
|
else
|
||||||
raise RequestFailed, "HTTP Error Code: #{response.code} for #{url}\nheaders: #{response.headers}\nbody: #{response.body}"
|
raise RequestFailed,
|
||||||
|
<<~TEXT
|
||||||
|
HTTP Error Code: #{response.code} for #{url}
|
||||||
|
headers: #{response.headers}
|
||||||
|
body: #{response.body}
|
||||||
|
curl message: #{response.return_message}
|
||||||
|
timeout: #{response.timed_out?}
|
||||||
|
TEXT
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -42,16 +42,10 @@ class Champs::TitreIdentiteChamp < Champ
|
||||||
end
|
end
|
||||||
|
|
||||||
def for_export
|
def for_export
|
||||||
piece_justificative_file.filename.to_s if piece_justificative_file.attached?
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def for_api
|
def for_api
|
||||||
if piece_justificative_file.attached? && (piece_justificative_file.virus_scanner.safe? || piece_justificative_file.virus_scanner.pending?)
|
nil
|
||||||
piece_justificative_file.service_url
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_skip_pj_validation
|
|
||||||
type_de_champ.update(skip_pj_validation: true)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
23
app/models/concerns/blob_titre_identite_watermark_concern.rb
Normal file
23
app/models/concerns/blob_titre_identite_watermark_concern.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
module BlobTitreIdentiteWatermarkConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
after_update_commit :enqueue_watermark_job
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def titre_identite?
|
||||||
|
attachments.find { |attachment| attachment.record.class.name == 'Champs::TitreIdentiteChamp' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def watermarked?
|
||||||
|
metadata[:watermark]
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_watermark_job
|
||||||
|
if titre_identite? && !watermarked? && analyzed? && virus_scanner.done? && Flipper.enabled?(:titre_identite_watermark)
|
||||||
|
TitreIdentiteWatermarkJob.perform_later(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,7 +4,8 @@
|
||||||
Bonjour,
|
Bonjour,
|
||||||
|
|
||||||
%p
|
%p
|
||||||
Vous avez du nouveau sur #{APPLICATION_NAME}.
|
Vous avez du nouveau sur #{APPLICATION_NAME} depuis
|
||||||
|
= Date.today.monday? ? "vendredi dernier" : "hier"
|
||||||
|
|
||||||
%ul
|
%ul
|
||||||
- @data.each do |datum|
|
- @data.each do |datum|
|
||||||
|
@ -18,10 +19,10 @@
|
||||||
#{datum[:nb_notification]} #{'notification'.pluralize(datum[:nb_notification])}
|
#{datum[:nb_notification]} #{'notification'.pluralize(datum[:nb_notification])}
|
||||||
- if datum[:nb_en_instruction] > 0
|
- if datum[:nb_en_instruction] > 0
|
||||||
%br
|
%br
|
||||||
#{datum[:nb_en_instruction]} #{'dossier'.pluralize(datum[:nb_en_instruction])}
|
#{datum[:nb_en_instruction]} #{'dossier'.pluralize(datum[:nb_en_instruction])} en instruction
|
||||||
- if datum[:nb_accepted] > 0
|
- if datum[:nb_accepted] > 0
|
||||||
%br
|
%br
|
||||||
#{datum[:nb_accepted]} #{'dossier'.pluralize(datum[:nb_accepted])}
|
#{datum[:nb_accepted]} #{'dossier'.pluralize(datum[:nb_accepted])} #{'accepté'.pluralize(datum[:nb_accepted])}
|
||||||
|
|
||||||
|
|
||||||
= render partial: "layouts/mailers/signature"
|
= render partial: "layouts/mailers/signature"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
= render 'shared/attachment/edit',
|
= render 'shared/attachment/edit',
|
||||||
{ form: form,
|
{ form: form,
|
||||||
attached_file: champ.piece_justificative_file,
|
attached_file: champ.piece_justificative_file,
|
||||||
template: champ.type_de_champ.piece_justificative_template, user_can_destroy: true }
|
user_can_destroy: true }
|
||||||
|
|
|
@ -23,3 +23,6 @@ APPLICATION_BASE_URL="https://www.demarches-simplifiees.fr"
|
||||||
|
|
||||||
# Personnalisation d'instance - Page externe "Disponibilité" (status page)
|
# Personnalisation d'instance - Page externe "Disponibilité" (status page)
|
||||||
# STATUS_PAGE_URL=""
|
# STATUS_PAGE_URL=""
|
||||||
|
|
||||||
|
# Personnalisation d'instance - fichier utilisé pour poser un filigrane sur les pièces d'identité
|
||||||
|
# WATERMARK_FILE=""
|
||||||
|
|
|
@ -6,6 +6,7 @@ Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer
|
||||||
ActiveSupport.on_load(:active_storage_blob) do
|
ActiveSupport.on_load(:active_storage_blob) do
|
||||||
include BlobSignedIdConcern
|
include BlobSignedIdConcern
|
||||||
include BlobVirusScannerConcern
|
include BlobVirusScannerConcern
|
||||||
|
include BlobTitreIdentiteWatermarkConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
# When an OpenStack service is initialized it makes a request to fetch
|
# When an OpenStack service is initialized it makes a request to fetch
|
||||||
|
|
|
@ -32,7 +32,8 @@ features = [
|
||||||
:mini_profiler,
|
:mini_profiler,
|
||||||
:xray,
|
:xray,
|
||||||
:carte_ign,
|
:carte_ign,
|
||||||
:localization
|
:localization,
|
||||||
|
:titre_identite_watermark
|
||||||
]
|
]
|
||||||
|
|
||||||
def database_exists?
|
def database_exists?
|
||||||
|
|
1
config/initializers/watermark.rb
Normal file
1
config/initializers/watermark.rb
Normal file
|
@ -0,0 +1 @@
|
||||||
|
WATERMARK_FILE = ENV.fetch('WATERMARK_FILE', 'watermark.png')
|
|
@ -9,7 +9,7 @@ default_user = "test@exemple.fr"
|
||||||
default_password = "this is a very complicated password !"
|
default_password = "this is a very complicated password !"
|
||||||
|
|
||||||
puts "Create test user '#{default_user}'"
|
puts "Create test user '#{default_user}'"
|
||||||
Administration.create!(email: default_user, password: default_password)
|
SuperAdmin.create!(email: default_user, password: default_password)
|
||||||
user = User.create!(
|
user = User.create!(
|
||||||
email: default_user,
|
email: default_user,
|
||||||
password: default_password,
|
password: default_password,
|
||||||
|
|
|
@ -23,20 +23,4 @@ RSpec.describe ApiEntreprise::AttestationSocialeJob, type: :job do
|
||||||
subject
|
subject
|
||||||
expect(Etablissement.find(etablissement.id).entreprise_attestation_sociale).to be_attached
|
expect(Etablissement.find(etablissement.id).entreprise_attestation_sociale).to be_attached
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when ApiEntreprise::API::ServiceUnavailable is raised' do
|
|
||||||
# https://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html#method-i-retry_on
|
|
||||||
# retry on will try 5 times and then bubble up the error
|
|
||||||
it 'makes 5 attempts' do
|
|
||||||
assert_performed_jobs 5 do
|
|
||||||
ServiceUnavailableJob.perform_later rescue ApiEntreprise::API::ServiceUnavailable
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class ServiceUnavailableJob < ApiEntreprise::AttestationSocialeJob
|
|
||||||
def perform
|
|
||||||
raise ApiEntreprise::API::ServiceUnavailable
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
46
spec/jobs/api_entreprise/job_spec.rb
Normal file
46
spec/jobs/api_entreprise/job_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
|
RSpec.describe ApiEntreprise::Job, type: :job do
|
||||||
|
# https://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html#method-i-retry_on
|
||||||
|
context 'when an exception is raised' do
|
||||||
|
subject do
|
||||||
|
assert_performed_jobs(try) do
|
||||||
|
ExceptionJob.perform_later(error) rescue StandardError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is a service_unavaible' do
|
||||||
|
let(:error) { :standard_error }
|
||||||
|
let(:try) { 1 }
|
||||||
|
|
||||||
|
it { subject }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is a service_unavaible' do
|
||||||
|
let(:error) { :service_unavaible }
|
||||||
|
let(:try) { 5 }
|
||||||
|
|
||||||
|
it { subject }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is a bad gateway' do
|
||||||
|
let(:error) { :bad_gateway }
|
||||||
|
let(:try) { 5 }
|
||||||
|
|
||||||
|
it { subject }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ExceptionJob < ApiEntreprise::Job
|
||||||
|
def perform(exception)
|
||||||
|
case exception
|
||||||
|
when :service_unavaible
|
||||||
|
raise ApiEntreprise::API::ServiceUnavailable
|
||||||
|
when :bad_gateway
|
||||||
|
raise ApiEntreprise::API::BadGateway
|
||||||
|
else
|
||||||
|
raise StandardError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,13 +12,13 @@ describe ApiEntreprise::API do
|
||||||
allow_any_instance_of(ApiEntrepriseToken).to receive(:expired?).and_return(false)
|
allow_any_instance_of(ApiEntrepriseToken).to receive(:expired?).and_return(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the service is unavailable' do
|
context 'when the service throws a bad gateaway exception' do
|
||||||
let(:siren) { '111111111' }
|
let(:siren) { '111111111' }
|
||||||
let(:status) { 502 }
|
let(:status) { 502 }
|
||||||
let(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises_unavailable.json') }
|
let(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises_unavailable.json') }
|
||||||
|
|
||||||
it 'raises ApiEntreprise::API::RequestFailed' do
|
it 'raises ApiEntreprise::API::RequestFailed' do
|
||||||
expect { subject }.to raise_error(ApiEntreprise::API::RequestFailed)
|
expect { subject }.to raise_error(ApiEntreprise::API::BadGateway)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ describe ApiEntreprise::EntrepriseAdapter do
|
||||||
|
|
||||||
context "when the service is unavailable" do
|
context "when the service is unavailable" do
|
||||||
let(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises_unavailable.json') }
|
let(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises_unavailable.json') }
|
||||||
let(:status) { 502 }
|
let(:status) { 500 }
|
||||||
|
|
||||||
it 'raises an exception' do
|
it 'raises an exception' do
|
||||||
expect { subject }.to raise_error(ApiEntreprise::API::RequestFailed)
|
expect { subject }.to raise_error(ApiEntreprise::API::RequestFailed)
|
||||||
|
|
Loading…
Reference in a new issue