Merge pull request #6593 from betagouv/main

2021-10-27-01
This commit is contained in:
Kara Diaby 2021-10-27 14:40:50 +02:00 committed by GitHub
commit b49606bfb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
123 changed files with 875 additions and 264 deletions

View file

@ -59,7 +59,7 @@ jobs:
- bin/rspec spec/controllers/*_spec.rb
- bin/rspec spec/controllers/[a-l]**/*_spec.rb
- bin/rspec spec/controllers/[m-z]**/*_spec.rb
- bin/rspec spec/features
- bin/rspec spec/system
- bin/rspec spec/helpers spec/lib spec/middlewares
- bin/rspec spec/mailers spec/jobs spec/policies
- bin/rspec spec/models

View file

@ -722,7 +722,7 @@ Rails/DelegateAllowBlank:
Rails/DynamicFindBy:
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "spec/system/**/*.rb"
Rails/EnumUniqueness:
Enabled: true

View file

@ -89,11 +89,10 @@ group :test do
gem 'capybara' # Integration testing
gem 'capybara-email' # Access emails during integration tests
gem 'capybara-screenshot' # Save a dump of the page when an integration test fails
gem 'capybara-selenium'
gem 'database_cleaner'
gem 'factory_bot'
gem 'launchy'
gem 'rails-controller-testing'
gem 'selenium-webdriver'
gem 'shoulda-matchers', require: false
gem 'timecop'
gem 'vcr'

View file

@ -162,9 +162,6 @@ GEM
capybara-screenshot (1.0.25)
capybara (>= 1.0, < 4)
launchy
capybara-selenium (0.0.6)
capybara
selenium-webdriver
case_transform (0.2)
activesupport
caxlsx (3.1.0)
@ -189,12 +186,6 @@ GEM
css_parser (1.9.0)
addressable
daemons (1.3.1)
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.0.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1)
deep_cloneable (3.0.0)
@ -798,13 +789,11 @@ DEPENDENCIES
capybara
capybara-email
capybara-screenshot
capybara-selenium
charlock_holmes
chartkick
chunky_png
clamav-client
daemons
database_cleaner
deep_cloneable
delayed_cron_job
delayed_job_active_record
@ -874,6 +863,7 @@ DEPENDENCIES
sanitize-url
sassc-rails
scss_lint
selenium-webdriver
sentry-delayed_job
sentry-rails
sentry-ruby

View file

@ -2,7 +2,7 @@
# More info at https://github.com/guard/guard#readme
## Uncomment and set this to only include directories you want to watch
# directories %w(app lib config test spec features) \
# directories %w(app lib config test spec system) \
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
## Note: if you are using the `directories` clause above and you are not
@ -77,8 +77,8 @@ guard :rspec, cmd: 'spring rspec' do
watch('app/controllers/application_controller.rb') { "spec/controllers" }
watch('spec/rails_helper.rb') { "spec" }
# Capybara features specs
watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
# Capybara system specs
watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/system/#{m[1]}_spec.rb" }
# Turnip features and steps
watch(%r{^spec/acceptance/(.+)\.feature$})

View file

@ -57,6 +57,9 @@ module NewAdministrateur
],
methods: [
:drop_down_list_value,
:drop_down_other,
:drop_down_secondary_libelle,
:drop_down_secondary_description,
:piece_justificative_template_filename,
:piece_justificative_template_url,
:editable_options
@ -73,6 +76,9 @@ module NewAdministrateur
:parent_id,
:private,
:drop_down_list_value,
:drop_down_other,
:drop_down_secondary_libelle,
:drop_down_secondary_description,
:piece_justificative_template,
editable_options: [
:cadastres,
@ -94,6 +100,9 @@ module NewAdministrateur
:description,
:mandatory,
:drop_down_list_value,
:drop_down_other,
:drop_down_secondary_libelle,
:drop_down_secondary_description,
:piece_justificative_template,
editable_options: [
:cadastres,

View file

@ -337,8 +337,8 @@ module Users
def champs_params
params.permit(dossier: {
champs_attributes: [
:id, :value, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: [],
champs_attributes: [:id, :_destroy, :value, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: []]
:id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: [],
champs_attributes: [:id, :_destroy, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: []]
]
})
end

View file

@ -16,7 +16,7 @@ module Users
.joins(:dossiers)
.find_by!(id: params[:id], dossiers: { user: current_user })
transfer.destroy
transfer.destroy_and_nullify
redirect_to dossiers_path
end

View file

@ -11,9 +11,11 @@ import MoveButton from './MoveButton';
import TypeDeChampCarteOption from './TypeDeChampCarteOption';
import TypeDeChampCarteOptions from './TypeDeChampCarteOptions';
import TypeDeChampDropDownOptions from './TypeDeChampDropDownOptions';
import TypeDeChampDropDownOther from './TypeDeChampDropDownOther';
import TypeDeChampPieceJustificative from './TypeDeChampPieceJustificative';
import TypeDeChampRepetitionOptions from './TypeDeChampRepetitionOptions';
import TypeDeChampTypesSelect from './TypeDeChampTypesSelect';
import TypeDeChampDropDownSecondary from './TypeDeChampDropDownSecondary';
const TypeDeChamp = sortableElement(
({ typeDeChamp, dispatch, idx: index, isFirstItem, isLastItem, state }) => {
@ -22,6 +24,8 @@ const TypeDeChamp = sortableElement(
'multiple_drop_down_list',
'linked_drop_down_list'
].includes(typeDeChamp.type_champ);
const isLinkedDropDown = typeDeChamp.type_champ === 'linked_drop_down_list';
const isSimpleDropDown = typeDeChamp.type_champ === 'drop_down_list';
const isFile = typeDeChamp.type_champ === 'piece_justificative';
const isCarte = typeDeChamp.type_champ === 'carte';
const isExplication = typeDeChamp.type_champ === 'explication';
@ -130,6 +134,15 @@ const TypeDeChamp = sortableElement(
isVisible={isDropDown}
handler={updateHandlers.drop_down_list_value}
/>
<TypeDeChampDropDownSecondary
isVisible={isLinkedDropDown}
libelleHandler={updateHandlers.drop_down_secondary_libelle}
descriptionHandler={updateHandlers.drop_down_secondary_description}
/>
<TypeDeChampDropDownOther
isVisible={isSimpleDropDown}
handler={updateHandlers.drop_down_other}
/>
<TypeDeChampPieceJustificative
isVisible={isFile}
directUploadUrl={state.directUploadUrl}
@ -234,12 +247,15 @@ const OPTIONS_FIELDS = {
export const FIELDS = [
'description',
'drop_down_list_value',
'drop_down_other',
'libelle',
'mandatory',
'parent_id',
'piece_justificative_template',
'private',
'type_champ',
'drop_down_secondary_libelle',
'drop_down_secondary_description',
...Object.keys(OPTIONS_FIELDS)
];

View file

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
function TypeDeChampDropDownOther({ isVisible, handler }) {
if (isVisible) {
return (
<div className="cell">
<label htmlFor={handler.id}>
<input
type="checkbox"
id={handler.id}
name={handler.name}
checked={!!handler.value}
onChange={handler.onChange}
className="small-margin small"
/>
Proposer une option &apos;autre&apos; avec un texte libre
</label>
</div>
);
}
return null;
}
TypeDeChampDropDownOther.propTypes = {
isVisible: PropTypes.bool,
handler: PropTypes.object
};
export default TypeDeChampDropDownOther;

View file

@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function TypeDeChampDropDownSecondary({
isVisible,
libelleHandler,
descriptionHandler
}) {
if (isVisible) {
return (
<div className="cell">
<label htmlFor={libelleHandler.id}>Libellé secondaire</label>
<input
type="text"
id={libelleHandler.id}
name={libelleHandler.name}
value={libelleHandler.value ?? ''}
onChange={libelleHandler.onChange}
className="small-margin small"
/>
<label htmlFor={descriptionHandler.id}>Description secondaire</label>
<textarea
id={descriptionHandler.id}
name={descriptionHandler.name}
value={descriptionHandler.value ?? ''}
onChange={descriptionHandler.onChange}
rows={3}
cols={40}
className="small-margin small"
/>
</div>
);
}
return null;
}
TypeDeChampDropDownSecondary.propTypes = {
isVisible: PropTypes.bool,
libelleHandler: PropTypes.object,
descriptionHandler: PropTypes.object
};

View file

@ -0,0 +1,20 @@
import { delegate, show, hide } from '@utils';
delegate(
'change',
'.editable-champ-drop_down_list select, .editable-champ-drop_down_list input[type="radio"]',
(event) => {
const parent = event.target.closest('.editable-champ-drop_down_list');
const inputGroup = parent?.querySelector('.drop_down_other');
if (inputGroup) {
const input = inputGroup.querySelector('input');
if (event.target.value === '__other__') {
show(inputGroup);
input.disabled = false;
} else {
hide(inputGroup);
input.disabled = true;
}
}
}
);

View file

@ -27,6 +27,7 @@ import '../new_design/dossiers/auto-upload';
import '../new_design/champs/carte';
import '../new_design/champs/linked-drop-down-list';
import '../new_design/champs/repetition';
import '../new_design/champs/drop-down-list';
import {
toggleCondidentielExplanation,

View file

@ -1,16 +1,7 @@
class Cron::DiscardedDossiersDeletionJob < Cron::CronJob
self.schedule_expression = "every day at 2 am"
def perform(*args)
DossierOperationLog.where(dossier: Dossier.discarded_en_construction_expired)
.where.not(operation: DossierOperationLog.operations.fetch(:supprimer))
.destroy_all
DossierOperationLog.where(dossier: Dossier.discarded_termine_expired)
.where.not(operation: DossierOperationLog.operations.fetch(:supprimer))
.destroy_all
Dossier.discarded_brouillon_expired.destroy_all
Dossier.discarded_en_construction_expired.destroy_all
Dossier.discarded_termine_expired.destroy_all
def perform
Dossier.purge_discarded
end
end

View file

@ -2,6 +2,6 @@ class Cron::PurgeStaleTransfersJob < Cron::CronJob
self.schedule_expression = "every day at midnight"
def perform
DossierTransfer.stale.destroy_all
DossierTransfer.destroy_stale
end
end

View file

@ -0,0 +1,8 @@
class DossierRebaseJob < ApplicationJob
# If by the time the job runs the Dossier has been deleted, ignore the rebase
discard_on ActiveRecord::RecordNotFound
def perform(dossier)
dossier.rebase!
end
end

View file

@ -49,6 +49,7 @@ class Avis < ApplicationRecord
scope :for_dossier, -> (dossier_id) { where(dossier_id: dossier_id) }
scope :by_latest, -> { order(updated_at: :desc) }
scope :updated_since?, -> (date) { where('avis.updated_at > ?', date) }
scope :discarded_termine_expired, -> { unscope(:joins).where(dossier: Dossier.discarded_termine_expired) }
# The form allows subtmitting avis requests to several emails at once,
# hence this virtual attribute.

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string
@ -37,9 +38,12 @@ class Champ < ApplicationRecord
:mandatory?,
:description,
:drop_down_list_options,
:drop_down_other,
:drop_down_list_options?,
:drop_down_list_disabled_options,
:drop_down_list_enabled_non_empty_options,
:drop_down_secondary_libelle,
:drop_down_secondary_description,
:exclude_from_export?,
:exclude_from_view?,
:repetition?,

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string
@ -20,6 +21,7 @@
#
class Champs::DropDownListChamp < Champ
THRESHOLD_NB_OPTIONS_AS_RADIO = 5
OTHER = '__other__'
def render_as_radios?
enabled_non_empty_options.size <= THRESHOLD_NB_OPTIONS_AS_RADIO
@ -30,7 +32,15 @@ class Champs::DropDownListChamp < Champ
end
def options
drop_down_list_options
if drop_down_other?
drop_down_list_options + [["Autre", OTHER]]
else
drop_down_list_options
end
end
def selected
other_value_present? ? OTHER : value
end
def disabled_options
@ -40,4 +50,26 @@ class Champs::DropDownListChamp < Champ
def enabled_non_empty_options
drop_down_list_enabled_non_empty_options
end
def other_value_present?
drop_down_other? && value.present? && drop_down_list_options.exclude?(value)
end
def drop_down_other?
drop_down_other
end
def value=(value)
if value != OTHER
write_attribute(:value, value)
end
end
def value_other=(value)
write_attribute(:value, value)
end
def value_other
other_value_present? ? value : ""
end
end

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -0,0 +1,121 @@
module DossierRebaseConcern
extend ActiveSupport::Concern
def rebase!
if brouillon? && revision != procedure.published_revision
transaction do
rebase
end
end
end
private
def rebase
attachments_to_purge = []
geo_areas_to_delete = []
changes_by_type_de_champ = revision.compare(procedure.published_revision).group_by { |change| change[:stable_id] }
changes_by_type_de_champ.each do |stable_id, changes|
type_de_champ = find_type_de_champ_by_stable_id(stable_id)
published_type_de_champ = find_type_de_champ_by_stable_id(stable_id, published: true)
changes.each do |change|
case change[:op]
when :add
add_new_champs_for_revision(published_type_de_champ)
when :remove
delete_champs_for_revision(type_de_champ)
end
end
end
flattened_all_champs.each do |champ|
changes_by_stable_id = (changes_by_type_de_champ[champ.stable_id] || [])
.filter { |change| change[:op] == :update }
update_champ_for_revision(champ) do |update|
changes_by_stable_id.each do |change|
case change[:attribute]
when :type_champ
update[:type] = "Champs::#{change[:to].classify}Champ"
update[:value] = nil
update[:external_id] = nil
update[:data] = nil
geo_areas_to_delete += champ.geo_areas
if champ.piece_justificative_file.attached?
attachments_to_purge << champ.piece_justificative_file
end
when :drop_down_options
update[:value] = nil
when :carte_layers
geo_areas_to_delete += champ.geo_areas
end
update[:rebased_at] = Time.zone.now
end
end
end
self.update_column(:revision_id, procedure.published_revision_id)
attachments_to_purge.each(&:purge_later)
geo_areas_to_delete.each(&:destroy)
end
def add_new_champs_for_revision(published_type_de_champ)
if published_type_de_champ.parent
find_champs_by_stable_id(published_type_de_champ.parent.stable_id).each do |champ_repetition|
champ_repetition.rows.size.times do |row|
champ = published_type_de_champ.champ.build(row: row)
champ_repetition.champs << champ
end
end
else
champ = published_type_de_champ.build_champ
self.champs << champ
end
end
def update_champ_for_revision(champ)
published_type_de_champ = find_type_de_champ_by_stable_id(champ.stable_id, published: true)
return if !published_type_de_champ
update = {}
yield update
if champ.type_de_champ != published_type_de_champ
update[:type_de_champ_id] = published_type_de_champ.id
end
if update.present?
champ.update_columns(update)
end
end
def delete_champs_for_revision(published_type_de_champ)
Champ.where(id: find_champs_by_stable_id(published_type_de_champ.stable_id).map(&:id))
.destroy_all
end
def flattened_all_types_de_champ(published: false)
revision = published ? procedure.published_revision : self.revision
types_de_champ = revision.types_de_champ + revision.types_de_champ_private
(types_de_champ + types_de_champ.filter(&:repetition?).flat_map(&:types_de_champ))
.index_by(&:stable_id)
end
def flattened_all_champs
all_champs = (champs + champs_private)
all_champs + all_champs.filter(&:repetition?).flat_map(&:champs)
end
def find_type_de_champ_by_stable_id(stable_id, published: false)
flattened_all_types_de_champ(published: published)[stable_id]
end
def find_champs_by_stable_id(stable_id)
flattened_all_champs.filter do |champ|
champ.stable_id == stable_id
end
end
end

View file

@ -36,6 +36,7 @@
class Dossier < ApplicationRecord
self.ignored_columns = [:en_construction_conservation_extension]
include DossierFilteringConcern
include DossierRebaseConcern
include Discard::Model
self.discard_column = :hidden_at
@ -677,6 +678,7 @@ class Dossier < ApplicationRecord
end
end
update!(dossier_transfer_id: nil)
discard!
end
@ -958,6 +960,21 @@ class Dossier < ApplicationRecord
user&.locale || I18n.default_locale
end
def self.purge_discarded
discarded_brouillon_expired.destroy_all
transaction do
DossierOperationLog.discarded_en_construction_expired.destroy_all
discarded_en_construction_expired.destroy_all
end
transaction do
DossierOperationLog.discarded_termine_expired.destroy_all
Avis.discarded_termine_expired.destroy_all
discarded_termine_expired.destroy_all
end
end
private
def defaut_groupe_instructeur?

View file

@ -36,6 +36,10 @@ class DossierOperationLog < ApplicationRecord
belongs_to :dossier, optional: true
belongs_to :bill_signature, optional: true
scope :not_deletion, -> { where.not(operation: operations.fetch(:supprimer)) }
scope :discarded_en_construction_expired, -> { where(dossier: Dossier.discarded_en_construction_expired).not_deletion }
scope :discarded_termine_expired, -> { where(dossier: Dossier.discarded_termine_expired).not_deletion }
def self.create_and_serialize(params)
dossier = params.fetch(:dossier)

View file

@ -37,7 +37,7 @@ class DossierTransfer < ApplicationRecord
}
end)
transfer.dossiers.update_all(user_id: current_user.id)
transfer.destroy
transfer.destroy_and_nullify
end
end
@ -45,6 +45,22 @@ class DossierTransfer < ApplicationRecord
User.find_by(email: email)&.locale || I18n.default_locale
end
def destroy_and_nullify
transaction do
# Rails cascading is not working with default scopes. Doing nullify cascade manually.
dossiers.with_discarded.update_all(dossier_transfer_id: nil)
destroy
end
end
def self.destroy_stale
transaction do
# Rails cascading is not working with default scopes. Doing nullify cascade manually.
Dossier.with_discarded.where(transfer: stale).update_all(dossier_transfer_id: nil)
stale.destroy_all
end
end
private
def send_notification

View file

@ -714,6 +714,9 @@ class Procedure < ApplicationRecord
def publish_revision!
update!(draft_revision: create_new_revision, published_revision: draft_revision)
published_revision.touch(:published_at)
dossiers.state_brouillon.find_each do |dossier|
DossierRebaseJob.perform_later(dossier)
end
end
def cnaf_enabled?

View file

@ -134,11 +134,11 @@ class ProcedureRevision < ApplicationRecord
to_sids = to_h.keys
removed = (from_sids - to_sids).map do |sid|
{ op: :remove, label: from_h[sid].libelle, private: from_h[sid].private?, position: from_sids.index(sid) }
{ op: :remove, label: from_h[sid].libelle, private: from_h[sid].private?, position: from_sids.index(sid), stable_id: sid }
end
added = (to_sids - from_sids).map do |sid|
{ op: :add, label: to_h[sid].libelle, private: to_h[sid].private?, position: to_sids.index(sid) }
{ op: :add, label: to_h[sid].libelle, private: to_h[sid].private?, position: to_sids.index(sid), stable_id: sid }
end
kept = from_sids.intersection(to_sids)
@ -147,7 +147,7 @@ class ProcedureRevision < ApplicationRecord
.map { |sid| [sid, from_sids.index(sid), to_sids.index(sid)] }
.filter { |_, from_index, to_index| from_index != to_index }
.map do |sid, from_index, to_index|
{ op: :move, label: from_h[sid].libelle, private: from_h[sid].private?, from: from_index, to: to_index, position: to_index }
{ op: :move, label: from_h[sid].libelle, private: from_h[sid].private?, from: from_index, to: to_index, position: to_index, stable_id: sid }
end
changed = kept
@ -172,7 +172,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.type_champ,
to: to_type_de_champ.type_champ
to: to_type_de_champ.type_champ,
stable_id: from_type_de_champ.stable_id
}
end
if from_type_de_champ.libelle != to_type_de_champ.libelle
@ -182,7 +183,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.libelle,
to: to_type_de_champ.libelle
to: to_type_de_champ.libelle,
stable_id: from_type_de_champ.stable_id
}
end
if from_type_de_champ.description != to_type_de_champ.description
@ -192,7 +194,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.description,
to: to_type_de_champ.description
to: to_type_de_champ.description,
stable_id: from_type_de_champ.stable_id
}
end
if from_type_de_champ.mandatory? != to_type_de_champ.mandatory?
@ -202,7 +205,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.mandatory?,
to: to_type_de_champ.mandatory?
to: to_type_de_champ.mandatory?,
stable_id: from_type_de_champ.stable_id
}
end
if to_type_de_champ.drop_down_list?
@ -213,7 +217,41 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.drop_down_list_options,
to: to_type_de_champ.drop_down_list_options
to: to_type_de_champ.drop_down_list_options,
stable_id: from_type_de_champ.stable_id
}
end
if to_type_de_champ.linked_drop_down_list?
if from_type_de_champ.drop_down_secondary_libelle != to_type_de_champ.drop_down_secondary_libelle
changes << {
op: :update,
attribute: :drop_down_secondary_libelle,
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.drop_down_secondary_libelle,
to: to_type_de_champ.drop_down_secondary_libelle
}
end
if from_type_de_champ.drop_down_secondary_description != to_type_de_champ.drop_down_secondary_description
changes << {
op: :update,
attribute: :drop_down_secondary_description,
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.drop_down_secondary_description,
to: to_type_de_champ.drop_down_secondary_description
}
end
end
if from_type_de_champ.drop_down_other != to_type_de_champ.drop_down_other
changes << {
op: :update,
attribute: :drop_down_other,
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.drop_down_other,
to: to_type_de_champ.drop_down_other,
stable_id: from_type_de_champ.stable_id
}
end
elsif to_type_de_champ.carte?
@ -224,7 +262,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.carte_optional_layers,
to: to_type_de_champ.carte_optional_layers
to: to_type_de_champ.carte_optional_layers,
stable_id: from_type_de_champ.stable_id
}
end
elsif to_type_de_champ.piece_justificative?
@ -235,7 +274,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.piece_justificative_template_filename,
to: to_type_de_champ.piece_justificative_template_filename
to: to_type_de_champ.piece_justificative_template_filename,
stable_id: from_type_de_champ.stable_id
}
end
elsif to_type_de_champ.repetition?

View file

@ -58,7 +58,7 @@ class TypeDeChamp < ApplicationRecord
belongs_to :parent, class_name: 'TypeDeChamp', optional: true
has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', inverse_of: :parent, dependent: :destroy
store_accessor :options, :cadastres, :old_pj, :drop_down_options, :skip_pj_validation, :skip_content_type_pj_validation
store_accessor :options, :cadastres, :old_pj, :drop_down_options, :skip_pj_validation, :skip_content_type_pj_validation, :drop_down_secondary_libelle, :drop_down_secondary_description, :drop_down_other
has_many :revision_types_de_champ, class_name: 'ProcedureRevisionTypeDeChamp', dependent: :destroy, inverse_of: :type_de_champ
has_many :revisions, through: :revision_types_de_champ
@ -331,9 +331,12 @@ class TypeDeChamp < ApplicationRecord
],
methods: [
:drop_down_list_value,
:drop_down_other,
:piece_justificative_template_filename,
:piece_justificative_template_url,
:editable_options
:editable_options,
:drop_down_secondary_libelle,
:drop_down_secondary_description
]
}
TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE

View file

@ -182,17 +182,16 @@ class User < ApplicationRecord
raise "Cannot delete this user because they are also instructeur, expert or administrateur"
end
Invite.where(dossier: dossiers.with_discarded).destroy_all
dossiers.state_en_construction.each do |dossier|
dossier.discard_and_keep_track!(administration, :user_removed)
transaction do
Invite.where(dossier: dossiers.with_discarded).destroy_all
dossiers.state_en_construction.each do |dossier|
dossier.discard_and_keep_track!(administration, :user_removed)
end
DossierOperationLog.where(dossier: dossiers.with_discarded.discarded).not_deletion.destroy_all
dossiers.with_discarded.discarded.destroy_all
dossiers.update_all(deleted_user_email_never_send: email, user_id: nil, dossier_transfer_id: nil)
destroy!
end
DossierOperationLog
.where(dossier: dossiers.with_discarded.discarded)
.where.not(operation: DossierOperationLog.operations.fetch(:supprimer))
.destroy_all
dossiers.with_discarded.discarded.destroy_all
dossiers.update_all(deleted_user_email_never_send: email, user_id: nil)
destroy!
end
private

View file

@ -1,4 +1,32 @@
class ZxcvbnService
@tester_mutex = Mutex.new
class << self
# Returns an Zxcvbn instance cached between classes instances and between threads.
#
# The tester weights ~20 Mo, and we'd like to save some memory so rather
# that storing it in a per-thread accessor, we prefer to use a mutex
# to cache it between threads.
def tester
@tester_mutex.synchronize do
@tester ||= build_tester
end
end
private
# Returns a fully initializer tester from the on-disk dictionary.
#
# This is slow: loading and parsing the dictionary may take around 1s.
def build_tester
dictionaries = YAML.safe_load(File.read(Rails.root.join("config", "initializers", "zxcvbn_dictionnaries.yaml")))
tester = Zxcvbn::Tester.new
tester.add_word_lists(dictionaries)
tester
end
end
def initialize(password)
@password = password
end
@ -18,6 +46,6 @@ class ZxcvbnService
private
def compute_zxcvbn
Zxcvbn.test(@password, [], ZXCVBN_DICTIONNARIES)
self.class.tester.test(@password)
end
end

View file

@ -14,6 +14,10 @@
%li.mb-1= t("update_type_champ#{postfix}", label: change[:label], to: t("activerecord.attributes.type_de_champ.type_champs.#{change[:to]}"), scope: [:new_administrateur, :revision_changes])
- when :description
%li.mb-1= t("update_description#{postfix}", label: change[:label], to: change[:to], scope: [:new_administrateur, :revision_changes])
- when :drop_down_secondary_libelle
%li.mb-1= t("update_drop_down_secondary_libelle#{postfix}", label: change[:label], to: change[:to], scope: [:new_administrateur, :revision_changes])
- when :drop_down_secondary_description
%li.mb-1= t("update_drop_down_secondary_description#{postfix}", label: change[:label], to: change[:to], scope: [:new_administrateur, :revision_changes])
- when :mandatory
- if change[:from] == false
-# i18n-tasks-use t('new_administrateur.revision_changes.update_mandatory.enabled')
@ -37,6 +41,11 @@
%li= t(:add_option, scope: [:new_administrateur, :revision_changes], items: added.map{ |term| "« #{term.strip} »" }.join(", "))
- if removed.present?
%li= t(:remove_option, scope: [:new_administrateur, :revision_changes], items: removed.map{ |term| "« #{term.strip} »" }.join(", "))
- when :drop_down_other
- if change[:from] == false
%li.mb-1= t("new_administrateur.revision_changes.update_drop_down_other#{postfix}.enabled", label: change[:label])
- else
%li.mb-1= t("new_administrateur.revision_changes.update_drop_down_other#{postfix}.disabled", label: change[:label])
- when :carte_layers
- added = change[:to].sort - change[:from].sort
- removed = change[:from].sort - change[:to].sort

View file

@ -0,0 +1,4 @@
.drop_down_other{ class: champ.other_value_present? ? '' : 'hidden' }
.notice
%p Veuillez saisir votre autre choix
= form.text_field :value_other, maxlength: "200", placeholder: "Saisissez ici", disabled: !champ.other_value_present?

View file

@ -5,3 +5,7 @@
- if champ.updated_at.present? && seen_at.present?
%span.updated-at{ class: highlight_if_unseen_class(seen_at, champ.updated_at) }
= "modifié le #{try_format_datetime(champ.updated_at)}"
- if champ.rebased_at.present? && champ.rebased_at > (seen_at || champ.updated_at) && current_user.owns_or_invite?(champ.dossier)
%span.updated-at.highlighted
Le type de ce champ où sa description a été modifiée par l'administration. Vérifier son contenu.

View file

@ -5,13 +5,18 @@
%label
= form.radio_button :value, option
= option
- if !champ.mandatory?
%label.blank-radio
= form.radio_button :value, ''
Non renseigné
- if champ.drop_down_other?
%label
= form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: champ.other_value_present?
Autre
- else
= form.select :value,
champ.options,
disabled: champ.disabled_options,
required: champ.mandatory?
= form.select :value, champ.options, selected: champ.selected, required: champ.mandatory?
- if champ.drop_down_other?
= render partial: "shared/dossiers/drop_down_other_input", locals: { form: form, champ: champ }

View file

@ -4,10 +4,12 @@
{ required: champ.mandatory? },
{ data: { secondary_options: champ.secondary_options } }
%span
= form.label :secondary_value, class: 'hidden' do
Valeur secondaire dépendant de la première
= form.label :secondary_value do
= champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première"
- if champ.mandatory?
%span.mandatory *
- if champ.drop_down_secondary_description.present?
.notice= string_to_html(champ.drop_down_secondary_description)
= form.select :secondary_value,
champ.secondary_options[champ.primary_value],
{ required: champ.mandatory? },

View file

@ -1 +0,0 @@
ZXCVBN_DICTIONNARIES = YAML.safe_load(File.read(Rails.root.join("config", "initializers", "zxcvbn_dictionnaries.yaml")))

View file

@ -9,12 +9,17 @@ fr:
other: Les positions de %{count} champs ont été modifiées
update_libelle: Le libellé du champ « %{label} » a été modifié. Le nouveau libellé est « %{to} »
update_description: La description du champ « %{label} » a été modifiée. La nouvelle description est « %{to} »
update_drop_down_secondary_libelle: Le libellé secondaire du champ « %{label} » a été modifié. Le nouveau libellé est « %{to} »
update_drop_down_secondary_description: La description secondaire du champ « %{label} » a été modifiée. La nouvelle description est « %{to} »
update_type_champ: Le type du champ « %{label} » a été modifié. Il est maintenant de type « %{to} »
update_mandatory:
enabled: Le champ « %{label} » est maintenant obligatoire
disabled: Le champ « %{label} » nest plus obligatoire
update_piece_justificative_template: Le modèle de pièce justificative du champ « %{label} » a été modifié
update_drop_down_options: Les options de sélection du champ « %{label} » ont été modifiées
update_drop_down_other:
enabled: Le champ « %{label} » comporte maintenant un choix « Autre »
disabled: Le champ « %{label} » ne comporte plus de choix « Autre »
update_carte_layers: Les référentiels cartographiques du champ « %{label} » ont été modifiés
add_private: Lannotation privée « %{label} » a été ajoutée
remove_private: Lannotation privée « %{label} » a été supprimée
@ -23,6 +28,8 @@ fr:
other: Les positions de %{count} annotations privées ont été modifiées
update_libelle_private: Le libellé de lannotation privée « %{label} » a été modifié. Le nouveau libellé est « %{to} »
update_description_private: La description de lannotation privée « %{label} » a été modifiée. La nouvelle description est « %{to} »
update_drop_down_secondary_libelle_private: Le libellé secondaire de lannotation « %{label} » a été modifié. Le nouveau libellé est « %{to} »
update_drop_down_secondary_description_private: La description secondaire de lannotation « %{label} » a été modifiée. La nouvelle description est « %{to} »
update_type_champ_private: Le type de lannotation privée « %{label} » a été modifié. Elle est maintenant de type « %{to} »
update_mandatory_private:
enabled: Lannotation privée « %{label} » est maintenant obligatoire

View file

@ -36,30 +36,9 @@ if ENV.fetch("RAILS_ENV") == "production"
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory. If you use this option
# you need to make sure to reconnect any threads in the `on_worker_boot`
# block.
# process behavior so workers use less memory.
#
preload_app!
# If you are preloading your application and using Active Record, it's
# recommended that you close any connections to the database before workers
# are forked to prevent connection leakage.
#
before_fork do
ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
end
# The code in the `on_worker_boot` will be called if you are using
# clustered mode by specifying a number of `workers`. After each worker
# process is booted, this block will be run. If you are using the `preload_app!`
# option, you will want to use this block to reconnect to any threads
# or connections that may have been created at application boot, as Ruby
# cannot share connections between processes.
#
on_worker_boot do
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end
end
# Allow puma to be restarted by `rails restart` command.

View file

@ -0,0 +1,5 @@
class AddRebasedAtToChamps < ActiveRecord::Migration[6.1]
def change
add_column :champs, :rebased_at, :datetime
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_10_20_104237) do
ActiveRecord::Schema.define(version: 2021_10_20_114237) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -190,6 +190,7 @@ ActiveRecord::Schema.define(version: 2021_10_20_104237) do
t.string "external_id"
t.string "fetch_external_data_exceptions", array: true
t.jsonb "value_json"
t.datetime "rebased_at"
t.index ["dossier_id"], name: "index_champs_on_dossier_id"
t.index ["etablissement_id"], name: "index_champs_on_etablissement_id"
t.index ["parent_id"], name: "index_champs_on_parent_id"

View file

@ -61,7 +61,7 @@ describe Manager::UsersController, type: :controller do
context 'and the old account belongs to an instructeur, expert and administrateur' do
let!(:instructeur) { create(:instructeur, user: user) }
let!(:expert) { create(:expert, user: user) }
let!(:administrateur) { create(:administrateur, user: user) }
let!(:administrateur) { create(:administrateur, user: user, instructeur: instructeur) }
it 'transfers instructeur account' do
subject

View file

@ -40,7 +40,7 @@ describe WebhookController, type: :controller do
context 'when there are an associated Instructeur and Administrateur' do
let!(:instructeur) { create(:instructeur, user: user) }
let!(:admin) { create(:administrateur, user: user) }
let!(:admin) { create(:administrateur, user: user, instructeur: instructeur) }
it 'returns a link to the Instructeur profile in the Manager' do
expect(payload).to have_key('html')

View file

@ -1,13 +1,18 @@
FactoryBot.define do
sequence(:administrateur_email) { |n| "admin#{n}@admin.com" }
factory :administrateur do
user { association :user, email: email, password: password }
transient do
email { generate(:administrateur_email) }
password { 'Mon [hien 4im3 {es banane$' }
instructeur { build(:instructeur, user: user) }
end
initialize_with do
User.create_or_promote_to_administrateur(email, password).administrateur
after(:build) do |administrateur, evaluator|
if administrateur.user
administrateur.user.instructeur = evaluator.instructeur
end
end
end

View file

@ -2,13 +2,11 @@ FactoryBot.define do
sequence(:create_expert_email) { |n| "expert#{n}@expert.com" }
factory :expert do
user { association :user, email: email, password: password }
transient do
email { generate(:expert_email) }
password { 'somethingverycomplated!' }
end
initialize_with do
User.create_or_promote_to_expert(email, password).expert
end
end
end

View file

@ -2,13 +2,11 @@ FactoryBot.define do
sequence(:instructeur_email) { |n| "inst#{n}@inst.com" }
factory :instructeur do
user { association :user, email: email, password: password }
transient do
email { generate(:instructeur_email) }
password { 'somethingverycomplated!' }
end
initialize_with do
User.create_or_promote_to_instructeur(email, password).instructeur
end
end
end

View file

@ -24,7 +24,7 @@ FactoryBot.define do
if evaluator.administrateur
procedure.administrateurs = [evaluator.administrateur]
elsif procedure.administrateurs.empty?
procedure.administrateurs = [create(:administrateur)]
procedure.administrateurs = [build(:administrateur)]
end
procedure.draft_revision = build(:procedure_revision, procedure: procedure)

View file

@ -1,7 +1,7 @@
describe Administrateur, type: :model do
let(:administration) { create(:administration) }
describe 'assocations' do
describe 'associations' do
it { is_expected.to have_and_belong_to_many(:instructeurs) }
it { is_expected.to have_many(:procedures) }
end

View file

@ -1,38 +1,44 @@
describe Champs::PhoneChamp do
let(:phone_champ) { build(:champ_phone) }
describe '#valid?' do
it do
expect(build(:champ_phone, value: nil)).to be_valid
expect(build(:champ_phone, value: "0123456789 0123456789")).to_not be_valid
expect(build(:champ_phone, value: "01.23.45.67.89 01.23.45.67.89")).to_not be_valid
expect(build(:champ_phone, value: "3646")).to be_valid
expect(build(:champ_phone, value: "0123456789")).to be_valid
expect(build(:champ_phone, value: "01.23.45.67.89")).to be_valid
expect(build(:champ_phone, value: "0123 45.67.89")).to be_valid
expect(build(:champ_phone, value: "0033 123-456-789")).to be_valid
expect(build(:champ_phone, value: "0033 123-456-789")).to be_valid
expect(build(:champ_phone, value: "0033(0)123456789")).to be_valid
expect(build(:champ_phone, value: "+33-1.23.45.67.89")).to be_valid
expect(build(:champ_phone, value: "+33 - 123 456 789")).to be_valid
expect(build(:champ_phone, value: "+33(0) 123 456 789")).to be_valid
expect(build(:champ_phone, value: "+33 (0)123 45 67 89")).to be_valid
expect(build(:champ_phone, value: "+33 (0)1 2345-6789")).to be_valid
expect(build(:champ_phone, value: "+33(0) - 123456789")).to be_valid
expect(build(:champ_phone, value: "+1(0) - 123456789")).to be_valid
expect(build(:champ_phone, value: "+49 2109 87654321")).to be_valid
expect(build(:champ_phone, value: "012345678")).to be_valid
expect(champ_with_value(nil)).to be_valid
expect(champ_with_value("0123456789 0123456789")).to_not be_valid
expect(champ_with_value("01.23.45.67.89 01.23.45.67.89")).to_not be_valid
expect(champ_with_value("3646")).to be_valid
expect(champ_with_value("0123456789")).to be_valid
expect(champ_with_value("01.23.45.67.89")).to be_valid
expect(champ_with_value("0123 45.67.89")).to be_valid
expect(champ_with_value("0033 123-456-789")).to be_valid
expect(champ_with_value("0033 123-456-789")).to be_valid
expect(champ_with_value("0033(0)123456789")).to be_valid
expect(champ_with_value("+33-1.23.45.67.89")).to be_valid
expect(champ_with_value("+33 - 123 456 789")).to be_valid
expect(champ_with_value("+33(0) 123 456 789")).to be_valid
expect(champ_with_value("+33 (0)123 45 67 89")).to be_valid
expect(champ_with_value("+33 (0)1 2345-6789")).to be_valid
expect(champ_with_value("+33(0) - 123456789")).to be_valid
expect(champ_with_value("+1(0) - 123456789")).to be_valid
expect(champ_with_value("+49 2109 87654321")).to be_valid
expect(champ_with_value("012345678")).to be_valid
# polynesian numbers should not return errors in any way
## landline numbers start with 40 or 45
expect(build(:champ_phone, value: "45187272")).to be_valid
expect(build(:champ_phone, value: "40 473 500")).to be_valid
expect(build(:champ_phone, value: "40473500")).to be_valid
expect(build(:champ_phone, value: "45473500")).to be_valid
expect(champ_with_value("45187272")).to be_valid
expect(champ_with_value("40 473 500")).to be_valid
expect(champ_with_value("40473500")).to be_valid
expect(champ_with_value("45473500")).to be_valid
## +689 is the international indicator
expect(build(:champ_phone, value: "+689 45473500")).to be_valid
expect(build(:champ_phone, value: "0145473500")).to be_valid
expect(champ_with_value("+689 45473500")).to be_valid
expect(champ_with_value("0145473500")).to be_valid
## polynesian mobile numbers start with 87, 88, 89
expect(build(:champ_phone, value: "87473500")).to be_valid
expect(build(:champ_phone, value: "88473500")).to be_valid
expect(build(:champ_phone, value: "89473500")).to be_valid
expect(champ_with_value("87473500")).to be_valid
expect(champ_with_value("88473500")).to be_valid
expect(champ_with_value("89473500")).to be_valid
end
def champ_with_value(number)
phone_champ.tap { |c| c.value = number }
end
end
end

View file

@ -47,8 +47,8 @@ describe Commentaire do
let(:dossier) { create(:dossier, procedure: procedure) }
context 'with a commentaire created by a instructeur' do
let(:instructeur) { create :instructeur, email: 'some_user@exemple.fr' }
let(:commentaire) { build :commentaire, instructeur: instructeur, dossier: dossier }
let(:instructeur) { build :instructeur, email: 'some_user@exemple.fr' }
context 'when the procedure shows instructeurs email' do
before { Flipper.disable(:hide_instructeur_email, procedure) }

View file

@ -1459,4 +1459,71 @@ describe Dossier do
it { expect(dossier.spreadsheet_columns(types_de_champ: [])).to include(["État du dossier", "Brouillon"]) }
end
describe "#rebase" do
let(:procedure) { create(:procedure, :with_type_de_champ_mandatory, :with_yes_no, :with_repetition, :with_datetime) }
let(:dossier) { create(:dossier, procedure: procedure) }
let(:yes_no_type_de_champ) { procedure.types_de_champ.find { |tdc| tdc.type_champ == TypeDeChamp.type_champs.fetch(:yes_no) } }
let(:text_type_de_champ) { procedure.types_de_champ.find(&:mandatory?) }
let(:text_champ) { dossier.champs.find(&:mandatory?) }
let(:rebased_text_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:text) } }
let(:datetime_type_de_champ) { procedure.types_de_champ.find { |tdc| tdc.type_champ == TypeDeChamp.type_champs.fetch(:datetime) } }
let(:datetime_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:datetime) } }
let(:rebased_datetime_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:date) } }
let(:repetition_type_de_champ) { procedure.types_de_champ.find { |tdc| tdc.type_champ == TypeDeChamp.type_champs.fetch(:repetition) } }
let(:repetition_text_type_de_champ) { repetition_type_de_champ.types_de_champ.find { |tdc| tdc.type_champ == TypeDeChamp.type_champs.fetch(:text) } }
let(:repetition_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:repetition) } }
let(:rebased_repetition_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:repetition) } }
before do
procedure.publish!
procedure.draft_revision.add_type_de_champ({
type_champ: TypeDeChamp.type_champs.fetch(:text),
libelle: "Un champ text"
})
procedure.draft_revision.find_or_clone_type_de_champ(text_type_de_champ).update(mandatory: false, libelle: "nouveau libelle")
procedure.draft_revision.find_or_clone_type_de_champ(datetime_type_de_champ).update(type_champ: TypeDeChamp.type_champs.fetch(:date))
procedure.draft_revision.find_or_clone_type_de_champ(repetition_text_type_de_champ).update(libelle: "nouveau libelle dans une repetition")
procedure.draft_revision.add_type_de_champ({
type_champ: TypeDeChamp.type_champs.fetch(:checkbox),
libelle: "oui ou non",
parent_id: repetition_type_de_champ.stable_id
})
procedure.draft_revision.remove_type_de_champ(yes_no_type_de_champ.stable_id)
datetime_champ.update(value: Date.today.to_s)
text_champ.update(value: 'bonjour')
end
it "updates the brouillon champs with the latest revision changes" do
revision_id = dossier.revision_id
libelle = text_type_de_champ.libelle
expect(dossier.revision).to eq(procedure.published_revision)
expect(dossier.champs.size).to eq(4)
expect(repetition_champ.rows.size).to eq(1)
expect(repetition_champ.rows[0].size).to eq(1)
procedure.publish_revision!
perform_enqueued_jobs
procedure.reload
dossier.reload
expect(procedure.revisions.size).to eq(3)
expect(dossier.revision).to eq(procedure.published_revision)
expect(dossier.champs.size).to eq(4)
expect(rebased_text_champ.value).to eq(text_champ.value)
expect(rebased_text_champ.type_de_champ_id).not_to eq(text_champ.type_de_champ_id)
expect(rebased_datetime_champ.type_champ).to eq(TypeDeChamp.type_champs.fetch(:date))
expect(rebased_datetime_champ.value).to be_nil
expect(rebased_repetition_champ.rows.size).to eq(1)
expect(rebased_repetition_champ.rows[0].size).to eq(2)
expect(rebased_text_champ.rebased_at).not_to be_nil
expect(rebased_datetime_champ.rebased_at).not_to be_nil
end
end
end

View file

@ -46,16 +46,36 @@ RSpec.describe DossierTransfer, type: :model do
it { expect(DossierTransfer.with_dossiers.count).to eq(0) }
end
end
end
describe 'dossier relationship' do
let(:transfer) { create(:dossier_transfer) }
let(:dossier) { create(:dossier, user: user, transfer: transfer) }
describe '#destroy_and_nullify' do
let(:transfer) { create(:dossier_transfer) }
let(:dossier) { create(:dossier, user: user, transfer: transfer) }
let(:discarded_dossier) { create(:dossier, user: user, transfer: dossier.transfer) }
it 'nullify transfer relationship on dossier' do
expect(dossier.transfer).to eq(transfer)
transfer.destroy
expect(dossier.reload.transfer).to be_nil
end
before do
discarded_dossier.discard!
end
it 'nullify transfer relationship on dossier' do
expect(dossier.transfer).to eq(transfer)
transfer.destroy_and_nullify
expect(dossier.reload.transfer).to be_nil
end
end
describe '#destroy_stale' do
let(:transfer) { create(:dossier_transfer, created_at: 1.month.ago) }
let(:dossier) { create(:dossier, user: user, transfer: transfer) }
let(:discarded_dossier) { create(:dossier, user: user, transfer: dossier.transfer) }
before do
discarded_dossier.discard!
end
it 'nullify the transfer on discarded dossier' do
DossierTransfer.destroy_stale
expect(DossierTransfer.count).to eq(0)
end
end
end

View file

@ -1,30 +1,30 @@
RSpec.describe GeoArea, type: :model do
describe '#area' do
let(:geo_area) { build(:geo_area, :polygon) }
let(:geo_area) { build(:geo_area, :polygon, champ: nil) }
it { expect(geo_area.area).to eq(103.6) }
end
describe '#area (hourglass polygon)' do
let(:geo_area) { build(:geo_area, :hourglass_polygon) }
let(:geo_area) { build(:geo_area, :hourglass_polygon, champ: nil) }
it { expect(geo_area.area).to eq(32.4) }
end
describe '#length' do
let(:geo_area) { build(:geo_area, :line_string) }
let(:geo_area) { build(:geo_area, :line_string, champ: nil) }
it { expect(geo_area.length).to eq(21.2) }
end
describe '#location' do
let(:geo_area) { build(:geo_area, :point) }
let(:geo_area) { build(:geo_area, :point, champ: nil) }
it { expect(geo_area.location).to eq("46°32'19\"N 2°25'42\"E") }
end
describe '#rgeo_geometry' do
let(:geo_area) { build(:geo_area, :polygon) }
let(:geo_area) { build(:geo_area, :polygon, champ: nil) }
let(:polygon) do
{
"type" => "Polygon",
@ -46,44 +46,47 @@ RSpec.describe GeoArea, type: :model do
it { expect(geo_area.geometry).to eq(polygon) }
context 'polygon_with_extra_coordinate' do
let(:geo_area) { build(:geo_area, :polygon_with_extra_coordinate) }
let(:geo_area) { build(:geo_area, :polygon_with_extra_coordinate, champ: nil) }
it { expect(geo_area.geometry).not_to eq(polygon) }
it { expect(geo_area.safe_geometry).to eq(polygon) }
end
end
describe '#valid?' do
let(:geo_area) { build(:geo_area, :polygon) }
describe 'validations' do
context 'geometry' do
subject! { geo_area.validate }
context 'polygon' do
it { expect(geo_area.valid?).to be_truthy }
end
context 'polygon' do
let(:geo_area) { build(:geo_area, :polygon, champ: nil) }
it { expect(geo_area.errors).not_to have_key(:geometry) }
end
context 'hourglass_polygon' do
let(:geo_area) { build(:geo_area, :hourglass_polygon) }
it { expect(geo_area.valid?).to be_falsey }
end
context 'hourglass_polygon' do
let(:geo_area) { build(:geo_area, :hourglass_polygon, champ: nil) }
it { expect(geo_area.errors).to have_key(:geometry) }
end
context 'line_string' do
let(:geo_area) { build(:geo_area, :line_string) }
it { expect(geo_area.valid?).to be_truthy }
end
context 'line_string' do
let(:geo_area) { build(:geo_area, :line_string, champ: nil) }
it { expect(geo_area.errors).not_to have_key(:geometry) }
end
context 'point' do
let(:geo_area) { build(:geo_area, :point) }
it { expect(geo_area.valid?).to be_truthy }
end
context 'point' do
let(:geo_area) { build(:geo_area, :point, champ: nil) }
it { expect(geo_area.errors).not_to have_key(:geometry) }
end
context 'invalid_right_hand_rule_polygon' do
let(:geo_area) { build(:geo_area, :invalid_right_hand_rule_polygon) }
it { expect(geo_area.valid?).to be_falsey }
context 'invalid_right_hand_rule_polygon' do
let(:geo_area) { build(:geo_area, :invalid_right_hand_rule_polygon, champ: nil) }
it { expect(geo_area.errors).to have_key(:geometry) }
end
end
end
describe "cadastre properties" do
let(:geo_area) { build(:geo_area, :cadastre) }
let(:legacy_geo_area) { build(:geo_area, :legacy_cadastre) }
let(:geo_area) { build(:geo_area, :cadastre, champ: nil) }
let(:legacy_geo_area) { build(:geo_area, :legacy_cadastre, champ: nil) }
it "should be backward compatible" do
expect("#{geo_area.code_dep}#{geo_area.code_com}").to eq(geo_area.commune)
@ -103,7 +106,7 @@ RSpec.describe GeoArea, type: :model do
describe 'description' do
context 'when properties is nil' do
let(:geo_area) { build(:geo_area, properties: nil) }
let(:geo_area) { build(:geo_area, properties: nil, champ: nil) }
it { expect(geo_area.description).to be_nil }
end

View file

@ -174,7 +174,8 @@ describe ProcedureRevision do
{
op: :add,
label: "Un champ text",
private: false
private: false,
stable_id: new_type_de_champ.stable_id
}
])
@ -186,12 +187,14 @@ describe ProcedureRevision do
label: type_de_champ_first.libelle,
private: false,
from: type_de_champ_first.libelle,
to: "modifier le libelle"
to: "modifier le libelle",
stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
private: false
private: false,
stable_id: new_type_de_champ.stable_id
}
])
expect(new_revision.types_de_champ.first.revision).to eq(new_revision)
@ -204,19 +207,22 @@ describe ProcedureRevision do
label: type_de_champ_first.libelle,
private: false,
from: type_de_champ_first.libelle,
to: "modifier le libelle"
to: "modifier le libelle",
stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
private: false
private: false,
stable_id: new_type_de_champ.stable_id
},
{
op: :move,
label: type_de_champ_second.libelle,
private: false,
from: 1,
to: 2
to: 2,
stable_id: type_de_champ_second.stable_id
}
])
expect(new_revision.types_de_champ.last.revision).to eq(revision)
@ -226,12 +232,14 @@ describe ProcedureRevision do
{
op: :remove,
label: type_de_champ_first.libelle,
private: false
private: false,
stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
private: false
private: false,
stable_id: new_type_de_champ.stable_id
}
])
@ -241,12 +249,14 @@ describe ProcedureRevision do
{
op: :remove,
label: type_de_champ_first.libelle,
private: false
private: false,
stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
private: false
private: false,
stable_id: new_type_de_champ.stable_id
},
{
op: :update,
@ -254,7 +264,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: type_de_champ_second.description,
to: "une description"
to: "une description",
stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@ -262,7 +273,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: false,
to: true
to: true,
stable_id: type_de_champ_second.stable_id
}
])
@ -272,12 +284,14 @@ describe ProcedureRevision do
{
op: :remove,
label: type_de_champ_first.libelle,
private: false
private: false,
stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
private: false
private: false,
stable_id: new_type_de_champ.stable_id
},
{
op: :update,
@ -285,7 +299,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: type_de_champ_second.description,
to: "une description"
to: "une description",
stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@ -293,7 +308,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: false,
to: true
to: true,
stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@ -301,7 +317,8 @@ describe ProcedureRevision do
label: "sub type de champ",
private: false,
from: "text",
to: "drop_down_list"
to: "drop_down_list",
stable_id: new_revision.types_de_champ.last.types_de_champ.first.stable_id
},
{
op: :update,
@ -309,7 +326,8 @@ describe ProcedureRevision do
label: "sub type de champ",
private: false,
from: [],
to: ["one", "two"]
to: ["one", "two"],
stable_id: new_revision.types_de_champ.last.types_de_champ.first.stable_id
}
])
@ -319,12 +337,14 @@ describe ProcedureRevision do
{
op: :remove,
label: type_de_champ_first.libelle,
private: false
private: false,
stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
private: false
private: false,
stable_id: new_type_de_champ.stable_id
},
{
op: :update,
@ -332,7 +352,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: type_de_champ_second.description,
to: "une description"
to: "une description",
stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@ -340,7 +361,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: false,
to: true
to: true,
stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@ -348,7 +370,8 @@ describe ProcedureRevision do
label: "sub type de champ",
private: false,
from: "text",
to: "carte"
to: "carte",
stable_id: new_revision.types_de_champ.last.types_de_champ.first.stable_id
},
{
op: :update,
@ -356,7 +379,8 @@ describe ProcedureRevision do
label: "sub type de champ",
private: false,
from: [],
to: [:cadastres, :znieff]
to: [:cadastres, :znieff],
stable_id: new_revision.types_de_champ.last.types_de_champ.first.stable_id
}
])
end

View file

@ -372,7 +372,7 @@ describe User, type: :model do
end
context 'for administrateurs' do
let(:user) { build(:user, email: 'admin@exemple.fr', password: password, administrateur: build(:administrateur)) }
let(:user) { build(:user, email: 'admin@exemple.fr', password: password, administrateur: create(:administrateur, user: nil)) }
context 'when the password is too short' do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }

View file

@ -52,7 +52,7 @@ describe ChampPolicy do
end
context 'when the user also has instruction rights' do
let(:instructeur) { create(:instructeur, email: signed_in_user.email, password: signed_in_user.password) }
let(:instructeur) { create(:instructeur, user: signed_in_user) }
let(:account) { { user: signed_in_user, instructeur: instructeur } }
context 'as the dossier instructeur and owner' do

View file

@ -58,7 +58,7 @@ RSpec.configure do |config|
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = false
config.use_transactional_fixtures = true
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
@ -127,4 +127,5 @@ RSpec.configure do |config|
config.include Shoulda::Matchers::ActiveModel, type: :model
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :view
config.include Devise::Test::IntegrationHelpers, type: :system
end

View file

@ -0,0 +1,47 @@
describe ZxcvbnService do
let(:password) { 'medium-strength-password' }
subject(:service) { ZxcvbnService.new(password) }
describe '#score' do
it 'returns the password complexity score' do
expect(service.score).to eq 3
end
end
describe '#complexity' do
it 'returns the password score, vulnerability and length' do
expect(service.complexity).to eq [3, 'medium, strength, password', 24]
end
end
describe 'caching' do
it 'lazily caches the tester between calls and instances' do
allow(Zxcvbn::Tester).to receive(:new).and_call_original
allow(YAML).to receive(:safe_load).and_call_original
first_service = ZxcvbnService.new('some-password')
first_service.score
first_service.complexity
other_service = ZxcvbnService.new('other-password')
other_service.score
other_service.complexity
expect(Zxcvbn::Tester).to have_received(:new).at_most(:once)
expect(YAML).to have_received(:safe_load).at_most(:once)
end
it 'lazily caches the tester between threads' do
allow(Zxcvbn::Tester).to receive(:new).and_call_original
threads = 1.upto(4).map do
Thread.new do
ZxcvbnService.new(password).score
end
end.map(&:join)
scores = threads.map(&:value)
expect(scores).to eq([3, 3, 3, 3])
expect(Zxcvbn::Tester).to have_received(:new).at_most(:once)
end
end
end

View file

@ -3,9 +3,6 @@ require 'capybara-screenshot/rspec'
require 'capybara/email/rspec'
require 'selenium/webdriver'
Capybara.javascript_driver = :headless_chrome
Capybara.ignore_hidden_elements = false
Capybara.register_driver :chrome do |app|
Capybara::Selenium::Driver.new(app, browser: :chrome)
end
@ -34,11 +31,10 @@ Capybara.register_driver :headless_chrome do |app|
end
end
# FIXME: remove this line when https://github.com/rspec/rspec-rails/issues/1897 has been fixed
Capybara.server = :puma, { Silent: true }
Capybara.default_max_wait_time = 2
Capybara.ignore_hidden_elements = false
# Save a snapshot of the HTML page when an integration test fails
Capybara::Screenshot.autosave_on_failure = true
# Keep only the screenshots generated from the last failing test suite
@ -49,13 +45,21 @@ Capybara::Screenshot.register_driver :headless_chrome do |driver, path|
end
RSpec.configure do |config|
# Set the user preferred language before Javascript feature specs.
config.before(:each, type: :system) do
driven_by :rack_test
end
config.before(:each, type: :system, js: true) do
driven_by :headless_chrome
end
# Set the user preferred language before Javascript system specs.
#
# Features specs without Javascript run in a Rack stack, and respect the Accept-Language value.
# System specs without Javascript run in a Rack stack, and respect the Accept-Language value.
# However specs using Javascript are run into a Headless Chrome, which doesn't support setting
# the default Accept-Language value reliably.
# So instead we set the locale cookie explicitly before each Javascript test.
config.before(:each, js: true) do
config.before(:each, type: :system, js: true) do
visit '/' # Webdriver needs visiting a page before setting the cookie
Capybara.current_session.driver.browser.manage.add_cookie(
name: :locale,

View file

@ -1,23 +0,0 @@
RSpec.configure do |config|
expect_list = []
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation, except: expect_list)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, js: true) do
DatabaseCleaner.strategy = :deletion, { except: expect_list }
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end

View file

@ -1,4 +1,4 @@
module FeatureHelpers
module SystemHelpers
include ActiveJob::TestHelper
def login_admin
@ -177,5 +177,5 @@ module FeatureHelpers
end
RSpec.configure do |config|
config.include FeatureHelpers, type: :feature
config.include SystemHelpers, type: :system
end

View file

@ -1,4 +1,4 @@
feature 'wcag rules for usager', js: true do
describe 'wcag rules for usager', js: true do
let(:procedure) { create(:procedure, :with_type_de_champ, :with_all_champs, :with_service, :for_individual, :published) }
let(:password) { 'a very complicated password' }
let(:litteraire_user) { create(:user, password: password) }

View file

@ -1,4 +1,4 @@
feature 'As an administrateur', js: true do
describe 'As an administrateur', js: true do
let(:super_admin) { create(:super_admin) }
let(:admin_email) { 'new_admin@gouv.fr' }
let(:new_admin) { Administrateur.by_email(admin_email) }

View file

@ -1,6 +1,6 @@
require 'features/admin/procedure_spec_helper'
require 'system/admin/procedure_spec_helper'
feature 'As an administrateur I wanna clone a procedure', js: true do
describe 'As an administrateur I wanna clone a procedure', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }

View file

@ -1,6 +1,6 @@
require 'features/admin/procedure_spec_helper'
require 'system/admin/procedure_spec_helper'
feature 'As an administrateur I wanna create a new procedure', js: true do
describe 'As an administrateur I wanna create a new procedure', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur, :with_procedure) }

View file

@ -1,4 +1,4 @@
feature 'procedure locked' do
describe 'procedure locked' do
let(:administrateur) { create(:administrateur) }
before do

View file

@ -1,6 +1,6 @@
require 'features/admin/procedure_spec_helper'
require 'system/admin/procedure_spec_helper'
feature 'Publication de démarches', js: true do
describe 'Publication de démarches', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }

View file

@ -1,6 +1,6 @@
require 'features/admin/procedure_spec_helper'
require 'system/admin/procedure_spec_helper'
feature 'Administrateurs can edit procedures', js: true do
describe 'Administrateurs can edit procedures', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }

View file

@ -1,4 +1,4 @@
feature 'fetch API Particulier Data', js: true do
describe 'fetch API Particulier Data', js: true do
let(:administrateur) { create(:administrateur) }
let(:expected_token) { 'd7e9c9f4c3ca00caadde31f50fd4521a' }

View file

@ -1,4 +1,4 @@
feature 'Inviting an expert:' do
describe 'Inviting an expert:' do
include ActiveJob::TestHelper
include ActionView::Helpers

View file

@ -1,4 +1,4 @@
feature 'Protecting against request forgeries:', :allow_forgery_protection, :show_exception_pages do
describe 'Protecting against request forgeries:', :allow_forgery_protection, :show_exception_pages do
let(:user) { create(:user, password: password) }
let(:password) { 'ThisIsTheUserPassword' }

View file

@ -1,4 +1,4 @@
feature 'France Connect Particulier Connexion' do
describe 'France Connect Particulier Connexion' do
let(:code) { 'plop' }
let(:given_name) { 'titi' }
let(:family_name) { 'toto' }

Some files were not shown because too many files have changed in this diff Show more