diff --git a/.rubocop.yml b/.rubocop.yml index e5eac1a9b..9a84c37d4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,15 +1,15 @@ require: - - rubocop/rspec/focused + - rubocop-performance + - rubocop-rails + - rubocop-rspec - ./lib/cops/add_concurrent_index.rb - ./lib/cops/application_name.rb - ./lib/cops/unscoped.rb -inherit_gem: - rubocop-rails_config: - - config/rails.yml - AllCops: - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.0 + DisabledByDefault: true + SuggestExtensions: false Exclude: - "db/schema.rb" - "db/migrate/20190730153555_recreate_structure.rb" @@ -644,6 +644,9 @@ Performance/RedundantMatch: Performance/RedundantMerge: Enabled: true +Style/HashTransformValues: + Enabled: true + Style/RedundantSortBy: Enabled: true @@ -874,7 +877,7 @@ Rails/WhereExists: Rails/WhereNot: Enabled: true -RSpec/Focused: +RSpec/Focus: Enabled: true Security/Eval: @@ -1373,4 +1376,3 @@ Style/YodaCondition: Style/ZeroLengthPredicate: Enabled: true - diff --git a/Gemfile b/Gemfile index 3377bc729..b56231cea 100644 --- a/Gemfile +++ b/Gemfile @@ -111,8 +111,9 @@ group :development do gem 'rack-mini-profiler' gem 'rails-erd', require: false # generates `doc/database_models.pdf` gem 'rubocop', require: false - gem 'rubocop-rails_config' - gem 'rubocop-rspec-focused', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false gem 'scss_lint', require: false gem 'web-console' end diff --git a/Gemfile.lock b/Gemfile.lock index 2ea2134bc..dad5cc90b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -620,8 +620,6 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.4.1) parser (>= 2.7.1.5) - rubocop-packaging (0.5.1) - rubocop (>= 0.89, < 2.0) rubocop-performance (1.9.2) rubocop (>= 0.90.0, < 2.0) rubocop-ast (>= 0.4.0) @@ -629,15 +627,9 @@ GEM activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 0.90.0, < 2.0) - rubocop-rails_config (1.3.1) - railties (>= 5.0) - rubocop (>= 1.8) - rubocop-ast (>= 1.0.1) - rubocop-packaging (~> 0.5) - rubocop-performance (~> 1.3) - rubocop-rails (~> 2.0) - rubocop-rspec-focused (1.0.0) - rubocop (>= 0.51) + rubocop-rspec (2.4.0) + rubocop (~> 1.0) + rubocop-ast (>= 1.1.0) ruby-graphviz (1.2.5) rexml ruby-progressbar (1.11.0) @@ -874,8 +866,9 @@ DEPENDENCIES rspec-rails rspec_junit_formatter rubocop - rubocop-rails_config - rubocop-rspec-focused + rubocop-performance + rubocop-rails + rubocop-rspec ruby-saml-idp sanitize-url sassc-rails diff --git a/app/assets/stylesheets/landing.scss b/app/assets/stylesheets/landing.scss index 589cdbb01..124e9ee2a 100644 --- a/app/assets/stylesheets/landing.scss +++ b/app/assets/stylesheets/landing.scss @@ -295,7 +295,6 @@ $users-breakpoint: 950px; font-size: 24px; font-weight: bold; margin-top: 13px; - color: #FFFFFF; &.grey { color: $g700; @@ -303,15 +302,19 @@ $users-breakpoint: 950px; } .cta-panel-explanation { - font-size: 24px; + font-size: 22px; margin-bottom: 10px; - color: #FFFFFF; &.grey { color: $g700; } } +.half .cta-panel-title, +.half .cta-panel-explanation { + text-align: center; +} + .role-panel-title { font-size: 30px; font-weight: bold; @@ -357,7 +360,7 @@ $cta-panel-button-border-size: 2px; @include vertical-padding(15px); display: block; border-radius: 100px; - font-size: 24px; + font-size: 22px; text-align: center; cursor: pointer; margin-top: 20px; diff --git a/app/controllers/administrateurs/attestation_templates_controller.rb b/app/controllers/administrateurs/attestation_templates_controller.rb index aff0bb793..d1f66ebad 100644 --- a/app/controllers/administrateurs/attestation_templates_controller.rb +++ b/app/controllers/administrateurs/attestation_templates_controller.rb @@ -3,12 +3,17 @@ module Administrateurs before_action :retrieve_procedure def edit - @attestation_template = @procedure.attestation_template || AttestationTemplate.new(procedure: @procedure) + @attestation_template = build_attestation end def update - attestation_template = @procedure.attestation_template + attestation_template = @procedure.draft_attestation_template.revise! + if attestation_template.update(activated_attestation_params) + AttestationTemplate + .where(id: @procedure.revisions.pluck(:attestation_template_id).compact) + .update_all(activated: attestation_template.activated?) + flash.notice = "L'attestation a bien été modifiée" else flash.alert = attestation_template.errors.full_messages.join('
') @@ -18,7 +23,7 @@ module Administrateurs end def create - attestation_template = AttestationTemplate.new(activated_attestation_params.merge(procedure_id: @procedure.id)) + attestation_template = build_attestation(activated_attestation_params) if attestation_template.save flash.notice = "L'attestation a bien été sauvegardée" @@ -30,14 +35,19 @@ module Administrateurs end def preview - attestation = @procedure.attestation_template || AttestationTemplate.new - @attestation = attestation.render_attributes_for({}) + @attestation = build_attestation.render_attributes_for({}) render 'administrateurs/attestation_templates/show', formats: [:pdf] end private + def build_attestation(attributes = {}) + attestation_template = @procedure.draft_attestation_template || @procedure.draft_revision.build_attestation_template + attestation_template.attributes = attributes + attestation_template + end + def activated_attestation_params # cache result to avoid multiple uninterlaced computations if @activated_attestation_params.nil? diff --git a/app/lib/balancer_delivery_method.rb b/app/lib/balancer_delivery_method.rb new file mode 100644 index 000000000..85f468ae3 --- /dev/null +++ b/app/lib/balancer_delivery_method.rb @@ -0,0 +1,38 @@ +# A Mail delivery method that randomly balances the actual delivery between different +# methods. +# +# Usage: +# +# ```ruby +# ActionMailer::Base.add_delivery_method :balancer, BalancerDeliveryMethod +# config.action_mailer.balancer_settings = { +# smtp: 25, +# sendmail: 75 +# } +# config.action_mailer.delivery_method = :balancer +# ``` +# +# Be sure to restart your server when you modify this file. +class BalancerDeliveryMethod + # Allows configuring the random number generator used for selecting a delivery method, + # mostly for testing purposes. + mattr_accessor :random, default: Random.new + + def initialize(settings) + @delivery_methods = settings + end + + def deliver!(mail) + balanced_delivery_method = delivery_method(mail) + ApplicationMailer.wrap_delivery_behavior(mail, balanced_delivery_method) + mail.deliver + end + + private + + def delivery_method(mail) + @delivery_methods + .flat_map { |delivery_method, weight| [delivery_method] * weight } + .sample(random: self.class.random) + end +end diff --git a/app/models/attestation_template.rb b/app/models/attestation_template.rb index 6062589e8..deab7b0a2 100644 --- a/app/models/attestation_template.rb +++ b/app/models/attestation_template.rb @@ -15,7 +15,8 @@ class AttestationTemplate < ApplicationRecord include ActionView::Helpers::NumberHelper include TagsSubstitutionConcern - belongs_to :procedure, optional: false + belongs_to :procedure, optional: true + has_many :revisions, class_name: 'ProcedureRevision', inverse_of: :attestation_template, dependent: :nullify has_one_attached :logo has_one_attached :signature @@ -103,6 +104,25 @@ class AttestationTemplate < ApplicationRecord } end + def revise! + if revisions.size > 1 + attestation_template = dup + attestation_template.save! + revisions + .last + .procedure + .draft_revision + .update!(attestation_template: attestation_template) + attestation_template + else + self + end + end + + def procedure + revisions.last&.procedure || super + end + private def used_tags diff --git a/app/models/dossier.rb b/app/models/dossier.rb index c0f11c577..83a91032a 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -720,9 +720,11 @@ class Dossier < ApplicationRecord { lon: lon, lat: lat, zoom: zoom } end - def unspecified_attestation_champs - attestation_template = procedure.attestation_template + def attestation_template + revision.attestation_template + end + def unspecified_attestation_champs if attestation_template&.activated? attestation_template.unspecified_champs_for_dossier(self) else @@ -731,8 +733,8 @@ class Dossier < ApplicationRecord end def build_attestation - if procedure.attestation_template&.activated? - procedure.attestation_template.attestation_for(self) + if attestation_template&.activated? + attestation_template.attestation_for(self) end end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index dec7d8a27..f3894815c 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -77,12 +77,15 @@ class Procedure < ApplicationRecord has_many :published_types_de_champ_private, through: :published_revision, source: :types_de_champ_private has_many :draft_types_de_champ, through: :draft_revision, source: :types_de_champ has_many :draft_types_de_champ_private, through: :draft_revision, source: :types_de_champ_private + has_one :draft_attestation_template, through: :draft_revision, source: :attestation_template + has_one :published_attestation_template, through: :published_revision, source: :attestation_template has_many :experts_procedures, dependent: :destroy has_many :experts, through: :experts_procedures has_one :module_api_carto, dependent: :destroy has_one :attestation_template, dependent: :destroy + has_many :attestation_templates, through: :revisions, source: :attestation_template belongs_to :parent_procedure, class_name: 'Procedure', optional: true belongs_to :canonical_procedure, class_name: 'Procedure', optional: true @@ -438,7 +441,8 @@ class Procedure < ApplicationRecord }, revision_types_de_champ_private: { type_de_champ: :types_de_champ - } + }, + attestation_template: [] } } include_list[:groupe_instructeurs] = :instructeurs if !is_different_admin @@ -576,13 +580,17 @@ class Procedure < ApplicationRecord touch(:whitelisted_at) end + def active_attestation_template + published_attestation_template || draft_attestation_template + end + def closed_mail_template_attestation_inconsistency_state # As an optimization, don’t check the predefined templates (they are presumed correct) if closed_mail.present? tag_present = closed_mail.body.to_s.include?("--lien attestation--") - if attestation_template&.activated? && !tag_present + if active_attestation_template&.activated? && !tag_present :missing_tag - elsif !attestation_template&.activated? && tag_present + elsif !active_attestation_template&.activated? && tag_present :extraneous_tag end end diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb index 7c0a19c48..da60bb296 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -2,15 +2,17 @@ # # Table name: procedure_revisions # -# id :bigint not null, primary key -# published_at :datetime -# created_at :datetime not null -# updated_at :datetime not null -# procedure_id :bigint not null +# id :bigint not null, primary key +# published_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# attestation_template_id :bigint +# procedure_id :bigint not null # class ProcedureRevision < ApplicationRecord self.implicit_order_column = :created_at belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false + belongs_to :attestation_template, inverse_of: :revisions, optional: true, dependent: :destroy has_many :dossiers, inverse_of: :revision, foreign_key: :revision_id @@ -125,6 +127,10 @@ class ProcedureRevision < ApplicationRecord ) end + def attestation_template + super || procedure.attestation_template + end + private def compare_types_de_champ(from_tdc, to_tdc) diff --git a/app/views/administrateurs/procedures/show.html.haml b/app/views/administrateurs/procedures/show.html.haml index c0985cf4e..04dc0bb42 100644 --- a/app/views/administrateurs/procedures/show.html.haml +++ b/app/views/administrateurs/procedures/show.html.haml @@ -138,7 +138,7 @@ .procedure-grid = link_to edit_admin_procedure_attestation_template_path(@procedure), class: 'card-admin' do - - if @procedure.attestation_template.present? && @procedure.attestation_template.activated + - if @procedure.draft_attestation_template&.activated? %div %span.icon.accept %p.card-admin-status-accept Activée diff --git a/config/env.example b/config/env.example index d2f9de017..8f3e332f8 100644 --- a/config/env.example +++ b/config/env.example @@ -70,28 +70,33 @@ MATOMO_ENABLED="disabled" MATOMO_ID="" MATOMO_HOST="matomo.organisme.fr" -# SMTP Provider: Send In Blue +# Default SMTP Provider: Mailjet +MAILJET_API_KEY="" +MAILJET_SECRET_KEY="" + +# Alternate SMTP Provider: SendInBlue SENDINBLUE_ENABLED="disabled" -SENDINBLUE_BALANCING="" -SENDINBLUE_BALANCING_VALUE="" SENDINBLUE_CLIENT_KEY="" SENDINBLUE_SMTP_KEY="" SENDINBLUE_USER_NAME="" # SENDINBLUE_LOGIN_URL="https://app.sendinblue.com/account/saml/login/truc" -# SMTP Provider: Mailjet -MAILJET_API_KEY="" -MAILJET_SECRET_KEY="" +# Ratio of emails sent using SendInBlue +# When enabled, N % of emails will be sent using SendInBlue +# (and the others using the default SMTP provider) +SENDINBLUE_BALANCING="disabled" +SENDINBLUE_BALANCING_VALUE="50" + +# Alternate SMTP Provider: Mailtrap (mail catcher for staging environments) +# When enabled, all emails will be sent using this provided +MAILTRAP_ENABLED="disabled" +MAILTRAP_USERNAME="" +MAILTRAP_PASSWORD="" # External service: live chat for admins (specific to démarches-simplifiées.fr) CRISP_ENABLED="disabled" CRISP_CLIENT_KEY="" -# External service: mail catcher for staging environments (specific to démarches-simplifiées.fr) -MAILTRAP_ENABLED="disabled" -MAILTRAP_USERNAME="" -MAILTRAP_PASSWORD="" - # API Entreprise credentials # https://api.gouv.fr/api/api-entreprise.html API_ENTREPRISE_KEY="" diff --git a/config/environments/development.rb b/config/environments/development.rb index 2b0f92cbc..e0be95cc4 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -75,40 +75,13 @@ Rails.application.configure do config.assets.raise_runtime_errors = true # Action Mailer settings + config.action_mailer.delivery_method = :letter_opener - if ENV['SENDINBLUE_ENABLED'] == 'enabled' - config.action_mailer.delivery_method = :smtp - config.action_mailer.smtp_settings = { - user_name: Rails.application.secrets.sendinblue[:username], - password: Rails.application.secrets.sendinblue[:smtp_key], - address: 'smtp-relay.sendinblue.com', - domain: 'smtp-relay.sendinblue.com', - port: '587', - authentication: :cram_md5 - } - else - # https://usehelo.com - if ENV['HELO_ENABLED'] == 'enabled' - config.action_mailer.delivery_method = :smtp - config.action_mailer.smtp_settings = { - user_name: 'demarches-simplifiees', - password: '', - address: '127.0.0.1', - domain: '127.0.0.1', - port: ENV.fetch('HELO_PORT', '2525'), - authentication: :plain - } - else - config.action_mailer.delivery_method = :letter_opener_web - end - - config.action_mailer.default_url_options = { - host: 'localhost', - port: 3000 - } - - config.action_mailer.asset_host = "http://" + ENV['APP_HOST'] - end + config.action_mailer.default_url_options = { + host: 'localhost', + port: 3000 + } + config.action_mailer.asset_host = "http://" + ENV['APP_HOST'] Rails.application.routes.default_url_options = { host: 'localhost', diff --git a/config/environments/production.rb b/config/environments/production.rb index cdfeaed9b..912426727 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,4 +1,5 @@ require "active_support/core_ext/integer/time" +require Rails.root.join("app/lib/balancer_delivery_method") Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -76,25 +77,19 @@ Rails.application.configure do # config.action_mailer.raise_delivery_errors = false if ENV['MAILTRAP_ENABLED'] == 'enabled' - config.action_mailer.delivery_method = :smtp - config.action_mailer.smtp_settings = { - user_name: Rails.application.secrets.mailtrap[:username], - password: Rails.application.secrets.mailtrap[:password], - address: 'smtp.mailtrap.io', - domain: 'smtp.mailtrap.io', - port: '2525', - authentication: :cram_md5 + config.action_mailer.delivery_method = :mailtrap + + elsif ENV['SENDINBLUE_ENABLED'] == 'enabled' && ENV['SENDINBLUE_BALANCING'] == 'enabled' + ActionMailer::Base.add_delivery_method :balancer, BalancerDeliveryMethod + config.action_mailer.balancer_settings = { + sendinblue: ENV.fetch('SENDINBLUE_BALANCING_VALUE').to_i, + mailjet: 100 - ENV.fetch('SENDINBLUE_BALANCING_VALUE').to_i } + config.action_mailer.delivery_method = :balancer + elsif ENV['SENDINBLUE_ENABLED'] == 'enabled' - config.action_mailer.delivery_method = :smtp - config.action_mailer.smtp_settings = { - user_name: Rails.application.secrets.sendinblue[:username], - password: Rails.application.secrets.sendinblue[:smtp_key], - address: 'smtp-relay.sendinblue.com', - domain: 'smtp-relay.sendinblue.com', - port: '587', - authentication: :cram_md5 - } + config.action_mailer.delivery_method = :sendinblue + else config.action_mailer.delivery_method = :mailjet end diff --git a/config/initializers/urls.rb b/config/initializers/02_urls.rb similarity index 93% rename from config/initializers/urls.rb rename to config/initializers/02_urls.rb index 732c583f2..6385e38e9 100644 --- a/config/initializers/urls.rb +++ b/config/initializers/02_urls.rb @@ -1,7 +1,9 @@ # rubocop:disable DS/ApplicationName # API URLs +API_ADRESSE_URL = ENV.fetch("API_ADRESSE_URL", "https://api-adresse.data.gouv.fr") API_ENTREPRISE_URL = ENV.fetch("API_ENTREPRISE_URL", "https://entreprise.api.gouv.fr/v2") API_EDUCATION_URL = ENV.fetch("API_EDUCATION_URL", "https://data.education.gouv.fr/api/records/1.0") +API_GEO_URL = ENV.fetch("API_GEO_URL", "https://geo.api.gouv.fr") API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api") HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2") PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1") @@ -11,7 +13,7 @@ UNIVERSIGN_API_URL = ENV.fetch("UNIVERSIGN_API_URL", "https://ws.universign.eu/t FEATURE_UPVOTE_URL = ENV.fetch("FEATURE_UPVOTE_URL", "https://demarches-simplifiees.featureupvote.com") # Internal URLs -FOG_BASE_URL = "https://static.demarches-simplifiees.fr" +FOG_OPENSTACK_URL = ENV.fetch("FOG_OPENSTACK_URL", "https://static.demarches-simplifiees.fr") # External services URLs WEBINAIRE_URL = "https://app.livestorm.co/demarches-simplifiees" diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 17aa4aef0..73d7177a4 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -4,30 +4,45 @@ # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy -# rubocop:disable DS/ApplicationName Rails.application.config.content_security_policy do |policy| # Whitelist image - policy.img_src :self, "*.openstreetmap.org", "static.demarches-simplifiees.fr", "*.cloud.ovh.net", "stats.data.gouv.fr", "*", :data, :blob + images_whitelist = ["*.openstreetmap.org", "*.cloud.ovh.net", "*"] + images_whitelist << URI(FOG_OPENSTACK_URL).host if FOG_OPENSTACK_URL.present? + images_whitelist << URI(MATOMO_IFRAME_URL).host if MATOMO_IFRAME_URL.present? + policy.img_src(:self, :data, :blob, *images_whitelist) + # Whitelist JS: nous, sendinblue et matomo # miniprofiler et nous avons quelques boutons inline :( - policy.script_src :self, "stats.data.gouv.fr", "*.sendinblue.com", "*.crisp.chat", "crisp.chat", "*.sibautomation.com", "sibautomation.com", 'cdn.jsdelivr.net', 'maxcdn.bootstrapcdn.com', 'code.jquery.com', :unsafe_eval, :unsafe_inline, :blob + scripts_whitelist = ["*.sendinblue.com", "*.crisp.chat", "crisp.chat", "*.sibautomation.com", "sibautomation.com", "cdn.jsdelivr.net", "maxcdn.bootstrapcdn.com", "code.jquery.com"] + scripts_whitelist << URI(MATOMO_IFRAME_URL).host if MATOMO_IFRAME_URL.present? + policy.script_src(:self, :unsafe_eval, :unsafe_inline, :blob, *scripts_whitelist) + # Pour les CSS, on a beaucoup de style inline et quelques balises