Merge branch 'main' into 8827-user-path-translations-fix
This commit is contained in:
commit
e6004d83f3
189 changed files with 2112 additions and 607 deletions
127
.rubocop.yml
127
.rubocop.yml
|
@ -14,6 +14,7 @@ AllCops:
|
|||
TargetRubyVersion: 3.1
|
||||
DisabledByDefault: true
|
||||
SuggestExtensions: false
|
||||
NewCops: enable
|
||||
Include:
|
||||
- "app/**/*.prawn"
|
||||
Exclude:
|
||||
|
@ -683,9 +684,18 @@ Performance/UriDefaultParser:
|
|||
Rails:
|
||||
Enabled: true
|
||||
|
||||
Rails/ActionControllerFlashBeforeRender:
|
||||
Enabled: true
|
||||
|
||||
Rails/ActionControllerTestCase:
|
||||
Enabled: true
|
||||
|
||||
Rails/ActionFilter:
|
||||
Enabled: true
|
||||
|
||||
Rails/ActionOrder:
|
||||
Enabled: false
|
||||
|
||||
Rails/ActiveRecordAliases:
|
||||
Enabled: true
|
||||
|
||||
|
@ -695,18 +705,24 @@ Rails/ActiveRecordCallbacksOrder:
|
|||
Rails/ActiveSupportAliases:
|
||||
Enabled: true
|
||||
|
||||
Rails/ActiveSupportOnLoad:
|
||||
Enabled: true
|
||||
|
||||
Rails/AddColumnIndex:
|
||||
Enabled: true
|
||||
|
||||
Rails/AfterCommitOverride:
|
||||
Enabled: true
|
||||
|
||||
Rails/ApplicationController:
|
||||
Enabled: false
|
||||
|
||||
Rails/ApplicationJob:
|
||||
Enabled: true
|
||||
|
||||
Rails/ApplicationRecord:
|
||||
Enabled: true
|
||||
|
||||
Rails/ApplicationController:
|
||||
Enabled: false
|
||||
|
||||
Rails/AttributeDefaultBlockValue:
|
||||
Enabled: true
|
||||
|
||||
|
@ -716,6 +732,9 @@ Rails/Blank:
|
|||
Rails/BulkChangeTable:
|
||||
Enabled: false
|
||||
|
||||
Rails/CompactBlank:
|
||||
Enabled: true
|
||||
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Enabled: true
|
||||
Exclude:
|
||||
|
@ -734,10 +753,28 @@ Rails/Delegate:
|
|||
Rails/DelegateAllowBlank:
|
||||
Enabled: true
|
||||
|
||||
Rails/DeprecatedActiveModelErrorsMethods:
|
||||
Enabled: false # re-enable in rails 7
|
||||
|
||||
Rails/DotSeparatedKeys:
|
||||
Enabled: true
|
||||
|
||||
Rails/DuplicateAssociation:
|
||||
Enabled: true
|
||||
|
||||
Rails/DuplicateScope:
|
||||
Enabled: true
|
||||
|
||||
Rails/DurationArithmetic:
|
||||
Enabled: true
|
||||
|
||||
Rails/DynamicFindBy:
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "spec/system/**/*.rb"
|
||||
- spec/system/**/*.rb
|
||||
|
||||
Rails/EagerEvaluationLogMessage:
|
||||
Enabled: true
|
||||
|
||||
Rails/EnumUniqueness:
|
||||
Enabled: true
|
||||
|
@ -748,6 +785,9 @@ Rails/EnvironmentComparison:
|
|||
Rails/Exit:
|
||||
Enabled: true
|
||||
|
||||
Rails/ExpandedDateRange:
|
||||
Enabled: true
|
||||
|
||||
Rails/FilePath:
|
||||
Enabled: false
|
||||
|
||||
|
@ -760,6 +800,9 @@ Rails/FindById:
|
|||
Rails/FindEach:
|
||||
Enabled: true
|
||||
|
||||
Rails/FreezeTime:
|
||||
Enabled: true
|
||||
|
||||
Rails/HasAndBelongsToMany:
|
||||
Enabled: false
|
||||
|
||||
|
@ -772,6 +815,18 @@ Rails/HttpPositionalArguments:
|
|||
Rails/HttpStatus:
|
||||
Enabled: false
|
||||
|
||||
Rails/I18nLazyLookup:
|
||||
Enabled: true
|
||||
|
||||
Rails/I18nLocaleAssignment:
|
||||
Enabled: true
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
|
||||
Rails/IgnoredColumnsAssignment:
|
||||
Enabled: true
|
||||
|
||||
Rails/Inquiry:
|
||||
Enabled: true
|
||||
|
||||
|
@ -787,6 +842,9 @@ Rails/MailerName:
|
|||
Rails/MatchRoute:
|
||||
Enabled: true
|
||||
|
||||
Rails/MigrationClassName:
|
||||
Enabled: true
|
||||
|
||||
Rails/NegateInclude:
|
||||
Enabled: false
|
||||
|
||||
|
@ -823,9 +881,15 @@ Rails/ReadWriteAttribute:
|
|||
Rails/RedundantAllowNil:
|
||||
Enabled: false
|
||||
|
||||
Rails/RedundantPresenceValidationOnBelongsTo:
|
||||
Enabled: true
|
||||
|
||||
Rails/RedundantReceiverInWithOptions:
|
||||
Enabled: true
|
||||
|
||||
Rails/RedundantTravelBack:
|
||||
Enabled: true
|
||||
|
||||
Rails/RelativeDateConstant:
|
||||
Enabled: true
|
||||
|
||||
|
@ -838,9 +902,21 @@ Rails/RenderPlainText:
|
|||
Rails/RequestReferer:
|
||||
Enabled: true
|
||||
|
||||
Rails/ResponseParsedBody:
|
||||
Enabled: true
|
||||
|
||||
Rails/ReversibleMigration:
|
||||
Enabled: false
|
||||
|
||||
Rails/RootJoinChain:
|
||||
Enabled: true
|
||||
|
||||
Rails/RootPathnameMethods:
|
||||
Enabled: true
|
||||
|
||||
Rails/RootPublicPath:
|
||||
Enabled: true
|
||||
|
||||
Rails/SaveBang:
|
||||
Enabled: false
|
||||
|
||||
|
@ -856,9 +932,37 @@ Rails/SkipsModelValidations:
|
|||
Rails/SquishedSQLHeredocs:
|
||||
Enabled: true
|
||||
|
||||
Rails/StripHeredoc:
|
||||
Enabled: true
|
||||
|
||||
Rails/ThreeStateBooleanColumn:
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- db/migrate/2019*
|
||||
- db/migrate/2020*
|
||||
- db/migrate/2021*
|
||||
- db/migrate/2022*
|
||||
- db/migrate/202301*
|
||||
- db/migrate/202303*
|
||||
|
||||
Rails/TimeZone:
|
||||
EnforcedStyle: strict
|
||||
|
||||
Rails/TimeZoneAssignment:
|
||||
Enabled: true
|
||||
|
||||
Rails/ToFormattedS:
|
||||
Enabled: true
|
||||
|
||||
Rails/ToSWithArgument:
|
||||
Enabled: true
|
||||
|
||||
Rails/TopLevelHashWithIndifferentAccess:
|
||||
Enabled: true
|
||||
|
||||
Rails/TransactionExitStatement:
|
||||
Enabled: true
|
||||
|
||||
Rails/UniqBeforePluck:
|
||||
Enabled: true
|
||||
|
||||
|
@ -868,6 +972,9 @@ Rails/UniqueValidationWithoutIndex:
|
|||
Rails/UnknownEnv:
|
||||
Enabled: false
|
||||
|
||||
Rails/UnusedIgnoredColumns:
|
||||
Enabled: true
|
||||
|
||||
Rails/Validation:
|
||||
Enabled: true
|
||||
|
||||
|
@ -877,9 +984,15 @@ Rails/WhereEquals:
|
|||
Rails/WhereExists:
|
||||
Enabled: true
|
||||
|
||||
Rails/WhereMissing:
|
||||
Enabled: true
|
||||
|
||||
Rails/WhereNot:
|
||||
Enabled: true
|
||||
|
||||
Rails/WhereNotWithMultipleConditions:
|
||||
Enabled: true
|
||||
|
||||
RSpec/Focus:
|
||||
Enabled: true
|
||||
|
||||
|
@ -1039,6 +1152,12 @@ Style/EvenOdd:
|
|||
Style/ExpandPathArguments:
|
||||
Enabled: true
|
||||
|
||||
Style/FileRead:
|
||||
Enabled: true
|
||||
|
||||
Style/FileWrite:
|
||||
Enabled: true
|
||||
|
||||
Style/For:
|
||||
Enabled: true
|
||||
|
||||
|
|
38
Gemfile.lock
38
Gemfile.lock
|
@ -473,9 +473,9 @@ GEM
|
|||
validate_url
|
||||
webfinger (>= 1.0.1)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.22.1)
|
||||
parallel (1.23.0)
|
||||
parsby (1.1.1)
|
||||
parser (3.1.2.1)
|
||||
parser (3.2.2.0)
|
||||
ast (~> 2.4.1)
|
||||
pdf-core (0.9.0)
|
||||
pg (1.2.3)
|
||||
|
@ -577,7 +577,7 @@ GEM
|
|||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redcarpet (3.6.0)
|
||||
regexp_parser (2.7.0)
|
||||
regexp_parser (2.8.0)
|
||||
request_store (1.5.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.0.1)
|
||||
|
@ -621,31 +621,33 @@ GEM
|
|||
rspec-support (3.10.2)
|
||||
rspec_junit_formatter (0.4.1)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (1.36.0)
|
||||
rubocop (1.50.2)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.1.2.1)
|
||||
parser (>= 3.2.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.20.1, < 2.0)
|
||||
rubocop-ast (>= 1.28.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.21.0)
|
||||
parser (>= 3.1.1.0)
|
||||
rubocop-performance (1.9.2)
|
||||
rubocop (>= 0.90.0, < 2.0)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.28.0)
|
||||
parser (>= 3.2.1.0)
|
||||
rubocop-capybara (2.17.1)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-performance (1.17.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.9.1)
|
||||
rubocop-rails (2.19.1)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.90.0, < 2.0)
|
||||
rubocop-rspec (2.4.0)
|
||||
rubocop (~> 1.0)
|
||||
rubocop-ast (>= 1.1.0)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-rspec (2.20.0)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-capybara (~> 2.17)
|
||||
ruby-graphviz (1.2.5)
|
||||
rexml
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
|
@ -756,7 +758,7 @@ GEM
|
|||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (2.3.0)
|
||||
unicode-display_width (2.4.2)
|
||||
uri_template (0.7.0)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
|
|
|
@ -1,17 +1,7 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
.conditionnel {
|
||||
|
||||
.condition-error {
|
||||
background: $background-red;
|
||||
margin: ($default-spacer) (-$default-spacer);
|
||||
|
||||
ul {
|
||||
padding: $default-spacer;
|
||||
}
|
||||
}
|
||||
|
||||
form.form > .conditionnel {
|
||||
.condition-table {
|
||||
table-layout: fixed;
|
||||
|
||||
|
|
12
app/assets/stylesheets/errors_summary.scss
Normal file
12
app/assets/stylesheets/errors_summary.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
.errors-summary {
|
||||
background: $background-red;
|
||||
margin: ($default-spacer) (-$default-spacer);
|
||||
|
||||
ul {
|
||||
padding: $default-spacer;
|
||||
}
|
||||
}
|
||||
|
|
@ -438,14 +438,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: inline-block;
|
||||
color: $blue-france-500;
|
||||
font-size: 30px;
|
||||
margin-bottom: 3 * $default-padding;
|
||||
border-bottom: 3px solid $blue-france-500;
|
||||
}
|
||||
|
||||
.header-subsection {
|
||||
font-size: 22px;
|
||||
color: $blue-france-500;
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
.groupe-instructeur {
|
||||
.actions {
|
||||
width: 200px;
|
||||
.setup {
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: center;
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
.dossiers-table {
|
||||
margin-top: $default-spacer;
|
||||
margin-bottom: 3 * $default-spacer;
|
||||
overflow: visible; // remove DSFR hidden overflow because of dropdown
|
||||
}
|
||||
|
||||
.procedure-actions {
|
||||
|
|
|
@ -29,6 +29,10 @@
|
|||
|
||||
.value {
|
||||
width: 200px;
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,4 +60,13 @@
|
|||
border-color: $dark-red;
|
||||
}
|
||||
}
|
||||
|
||||
.form.defaut-groupe {
|
||||
padding: $default-spacer;
|
||||
|
||||
label {
|
||||
width: 600px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,63 @@
|
|||
.counter-start-header-section {
|
||||
counter-reset: headerSectionCounter;
|
||||
}
|
||||
counter-reset: h1 h2 h3 h4 h5 h6;
|
||||
|
||||
.header-section {
|
||||
counter-increment: headerSectionCounter;
|
||||
.reset-h1 {
|
||||
counter-reset: h2;
|
||||
}
|
||||
|
||||
&.header-section-counter::before {
|
||||
content: counter(headerSectionCounter) ". ";
|
||||
.reset-h2 {
|
||||
counter-reset: h3;
|
||||
}
|
||||
|
||||
.reset-h3 {
|
||||
counter-reset: h4;
|
||||
}
|
||||
|
||||
.reset-h4 {
|
||||
counter-reset: h5;
|
||||
}
|
||||
|
||||
.reset-h5 {
|
||||
counter-reset: h6;
|
||||
}
|
||||
|
||||
|
||||
.header-section.fr-h1::before {
|
||||
counter-increment: h1;
|
||||
content: counter(h1) ". ";
|
||||
}
|
||||
|
||||
.header-section.fr-h2::before {
|
||||
counter-increment: h2;
|
||||
content: counter(h1) "."counter(h2) ". ";
|
||||
}
|
||||
|
||||
.header-section.fr-h3::before {
|
||||
counter-increment: h3;
|
||||
content: counter(h1) "."counter(h2) "." counter(h3) ". ";
|
||||
}
|
||||
|
||||
.header-section.fr-h4::before {
|
||||
counter-increment: h4;
|
||||
content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) ". ";
|
||||
}
|
||||
|
||||
.header-section.fr-h5::before {
|
||||
counter-increment: h5;
|
||||
content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". ";
|
||||
}
|
||||
|
||||
.header-section.fr-h6::before {
|
||||
counter-increment: h6;
|
||||
content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". ";
|
||||
}
|
||||
|
||||
.repetition {
|
||||
counter-reset: repetition;
|
||||
|
||||
.block-id::after {
|
||||
counter-increment: repetition;
|
||||
content: counter(repetition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
- if !@champ.mandatory?
|
||||
%label.blank-radio
|
||||
= @form.radio_button :value, ''
|
||||
= @form.radio_button :value, '', checked: @champ.value.blank? && !@champ.other?
|
||||
Non renseigné
|
||||
|
||||
- if @champ.drop_down_other?
|
||||
%label
|
||||
= @form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: @champ.other_value_present?
|
||||
= @form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: @champ.other?
|
||||
Autre
|
||||
- else
|
||||
= @form.select :value, @champ.options_without_empty_value_when_mandatory(@champ.options), { selected: @champ.selected }, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id }
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class EditableChamp::DropDownOtherInputComponent < EditableChamp::EditableChampBaseComponent
|
||||
def render?
|
||||
@champ.other?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.drop_down_other{ class: @champ.other_value_present? ? '' : 'hidden' }
|
||||
.drop_down_other
|
||||
.notice
|
||||
%label{ for: dom_id(@champ, :value_other) } Veuillez saisir votre autre choix
|
||||
= @form.text_field :value_other, maxlength: 200, size: nil, id: dom_id(@champ, :value_other), disabled: !@champ.other_value_present?
|
||||
= @form.text_field :value_other, maxlength: 200, size: nil, id: dom_id(@champ, :value_other)
|
||||
|
|
|
@ -6,7 +6,11 @@ class EditableChamp::EditableChampComponent < ApplicationComponent
|
|||
private
|
||||
|
||||
def has_label?(champ)
|
||||
types_without_label = [TypeDeChamp.type_champs.fetch(:header_section), TypeDeChamp.type_champs.fetch(:explication)]
|
||||
types_without_label = [
|
||||
TypeDeChamp.type_champs.fetch(:header_section),
|
||||
TypeDeChamp.type_champs.fetch(:explication),
|
||||
TypeDeChamp.type_champs.fetch(:repetition)
|
||||
]
|
||||
!types_without_label.include?(@champ.type_champ)
|
||||
end
|
||||
|
||||
|
@ -30,11 +34,6 @@ class EditableChamp::EditableChampComponent < ApplicationComponent
|
|||
# This is an editable champ. Lets find what controllers it might need.
|
||||
controllers = ['autosave']
|
||||
|
||||
# This is a dropdown champ. Activate special behaviours it might have.
|
||||
if @champ.simple_drop_down_list? || @champ.linked_drop_down_list?
|
||||
controllers << 'champ-dropdown'
|
||||
end
|
||||
|
||||
controllers.join(' ')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
.editable-champ{ html_options }
|
||||
- if @champ.block?
|
||||
%h3.header-subsection= @champ.libelle
|
||||
- if @champ.description.present?
|
||||
.notice= render SimpleFormatComponent.new(@champ.description, allow_a: true)
|
||||
|
||||
- elsif has_label?(@champ)
|
||||
- if has_label?(@champ)
|
||||
= render EditableChamp::ChampLabelComponent.new form: @form, champ: @champ, seen_at: @seen_at
|
||||
- if @champ.titre_identite?
|
||||
%p.notice= t('.titre_identite_notice')
|
||||
|
|
|
@ -1,2 +1,25 @@
|
|||
class EditableChamp::HeaderSectionComponent < EditableChamp::EditableChampBaseComponent
|
||||
class EditableChamp::HeaderSectionComponent < ApplicationComponent
|
||||
def initialize(form: nil, champ:, seen_at: nil)
|
||||
@champ = champ
|
||||
end
|
||||
|
||||
def level
|
||||
@champ.level
|
||||
end
|
||||
|
||||
def libelle
|
||||
@champ.libelle
|
||||
end
|
||||
|
||||
def header_section_classnames
|
||||
class_names(
|
||||
"fr-h#{level}": true,
|
||||
'header-section': @champ.dossier.auto_numbering_section_headers_for?(@champ),
|
||||
'hidden': !@champ.visible?
|
||||
)
|
||||
end
|
||||
|
||||
def tag_for_depth
|
||||
"h#{level + 1}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
%h2.header-section{ class: @champ.dossier.auto_numbering_section_headers_for?(@champ) ? "header-section-counter" : nil }
|
||||
= @champ.libelle
|
||||
= tag.send(tag_for_depth, class: header_section_classnames, id: @champ.input_group_id) do
|
||||
= libelle
|
||||
|
|
|
@ -1,2 +1,15 @@
|
|||
class EditableChamp::LinkedDropDownListComponent < EditableChamp::EditableChampBaseComponent
|
||||
private
|
||||
|
||||
def secondary_label
|
||||
secondary_label_text + secondary_label_mandatory
|
||||
end
|
||||
|
||||
def secondary_label_text
|
||||
@champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première"
|
||||
end
|
||||
|
||||
def secondary_label_mandatory
|
||||
@champ.mandatory? ? tag.span(' *', class: 'mandatory') : ''
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
- if @champ.options?
|
||||
= @form.select :primary_value,
|
||||
@champ.primary_options,
|
||||
{},
|
||||
{ data: { secondary_options: @champ.secondary_options }, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id } }
|
||||
|
||||
.secondary{ class: @champ.has_secondary_options_for_primary? ? '' : 'hidden' }
|
||||
= @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do
|
||||
- sanitize((@champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première") + (@champ.mandatory? ? tag.span(' *', class: 'mandatory') : ''))
|
||||
- if @champ.drop_down_secondary_description.present?
|
||||
.notice{ id: "#{@champ.describedby_id}-secondary" }= render SimpleFormatComponent.new(@champ.drop_down_secondary_description, allow_a: true)
|
||||
= @form.select :secondary_value,
|
||||
@champ.secondary_options[@champ.primary_value],
|
||||
{},
|
||||
{ data: { secondary: true }, disabled: !@champ.has_secondary_options_for_primary?, required: @champ.required?, id: "#{@champ.input_id}-secondary", aria: { describedby: "#{@champ.describedby_id}-secondary" } }
|
||||
= @form.hidden_field :secondary_value, value: '', disabled: @champ.has_secondary_options_for_primary?
|
||||
= @form.select :primary_value, @champ.primary_options, {}, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id }
|
||||
- if @champ.has_secondary_options_for_primary?
|
||||
.secondary
|
||||
= @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do
|
||||
- sanitize(secondary_label)
|
||||
- if @champ.drop_down_secondary_description.present?
|
||||
.notice{ id: "#{@champ.describedby_id}-secondary" }
|
||||
= render SimpleFormatComponent.new(@champ.drop_down_secondary_description, allow_a: true)
|
||||
= @form.select :secondary_value, @champ.secondary_options[@champ.primary_value], {}, required: @champ.required?, id: "#{@champ.input_id}-secondary", aria: { describedby: "#{@champ.describedby_id}-secondary" }
|
||||
- else
|
||||
= @form.hidden_field :secondary_value, value: ''
|
||||
|
|
|
@ -1,2 +1,9 @@
|
|||
class EditableChamp::RepetitionComponent < EditableChamp::EditableChampBaseComponent
|
||||
def legend_params
|
||||
@champ.description.present? ? { describedby: dom_id(@champ, :repetition) } : {}
|
||||
end
|
||||
|
||||
def notice_params
|
||||
@champ.description.present? ? { id: dom_id(@champ, :repetition) } : {}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
.repetition{ id: dom_id(@champ, :rows) }
|
||||
- @champ.rows.each do |champs|
|
||||
= render EditableChamp::RepetitionRowComponent.new(form: @form, champ: @champ, row: champs, seen_at: @seen_at)
|
||||
%fieldset
|
||||
%legend.header-subsection{ legend_params }= @champ.libelle
|
||||
- if @champ.description.present?
|
||||
.notice{ notice_params }= render SimpleFormatComponent.new(@champ.description, allow_a: true)
|
||||
|
||||
.actions{ 'data-turbo': 'true' }
|
||||
= render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id), http_method: :create, opt: { class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-circle-line fr-mb-3w", title: t(".add_title", libelle: @champ.libelle), id: dom_id(@champ, :create_repetition)}) do
|
||||
= t(".add", libelle: @champ.libelle)
|
||||
|
||||
.repetition{ id: dom_id(@champ, :rows) }
|
||||
- @champ.rows.each do |champs|
|
||||
= render EditableChamp::RepetitionRowComponent.new(form: @form, champ: @champ, row: champs, seen_at: @seen_at)
|
||||
|
||||
.actions{ 'data-turbo': 'true' }
|
||||
= render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id), http_method: :create, opt: { class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-circle-line fr-mb-3w", title: t(".add_title", libelle: @champ.libelle), id: dom_id(@champ, :create_repetition)}) do
|
||||
= t(".add", libelle: @champ.libelle)
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
- row_id = "safe-row-selector-#{@row.first.row_id}"
|
||||
.row{ id: row_id }
|
||||
- @row.each do |champ|
|
||||
= fields_for champ.input_name, champ do |form|
|
||||
= render EditableChamp::EditableChampComponent.new form: form, champ: champ, seen_at: @seen_at
|
||||
- if @row.size > 1
|
||||
%fieldset
|
||||
%legend.block-id= "#{@champ.libelle} "
|
||||
= render EditableChamp::SectionComponent.new(champs: @row)
|
||||
- else
|
||||
= render EditableChamp::SectionComponent.new(champs: @row)
|
||||
|
||||
.flex.row-reverse{ 'data-turbo': 'true' }
|
||||
= render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id, row_id: @row.first.row_id), http_method: :delete, opt: { class: "fr-btn fr-btn--sm fr-btn--tertiary fr-text-action-high--red-marianne", title: t(".delete_title", row_number: @champ.rows.find_index(@row))}) do
|
||||
|
|
64
app/components/editable_champ/section_component.rb
Normal file
64
app/components/editable_champ/section_component.rb
Normal file
|
@ -0,0 +1,64 @@
|
|||
class EditableChamp::SectionComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include TreeableConcern
|
||||
|
||||
def initialize(nodes: nil, champs: nil)
|
||||
if (nodes.nil?)
|
||||
nodes = to_tree(champs:)
|
||||
end
|
||||
@nodes = to_fieldset(nodes:)
|
||||
end
|
||||
|
||||
def render_within_fieldset?
|
||||
first_champ_is_an_header_section? && any_champ_fillable?
|
||||
end
|
||||
|
||||
def header_section
|
||||
return @nodes.first if @nodes.first.is_a?(Champs::HeaderSectionChamp)
|
||||
end
|
||||
|
||||
def splitted_tail
|
||||
tail.map { split_section_champ(_1) }
|
||||
end
|
||||
|
||||
def tail
|
||||
return @nodes if !first_champ_is_an_header_section?
|
||||
_, *rest_of_champ = @nodes
|
||||
|
||||
rest_of_champ
|
||||
end
|
||||
|
||||
def tag_for_depth
|
||||
"h#{header_section.level + 1}"
|
||||
end
|
||||
|
||||
# if two headers follows each others [h1, [h2, c]]
|
||||
# the first one must not be contained in fieldset
|
||||
# so we make the tree not fillable
|
||||
def fillable?
|
||||
false
|
||||
end
|
||||
|
||||
def split_section_champ(node)
|
||||
case node
|
||||
when EditableChamp::SectionComponent
|
||||
[node, nil]
|
||||
else
|
||||
[nil, node]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_fieldset(nodes:)
|
||||
nodes.map { _1.is_a?(Array) ? EditableChamp::SectionComponent.new(nodes: _1) : _1 }
|
||||
end
|
||||
|
||||
def first_champ_is_an_header_section?
|
||||
header_section.present?
|
||||
end
|
||||
|
||||
def any_champ_fillable?
|
||||
tail.any? { _1&.fillable? }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
- if render_within_fieldset?
|
||||
= tag.fieldset(class: "reset-#{tag_for_depth}") do
|
||||
= tag.legend do
|
||||
= render EditableChamp::HeaderSectionComponent.new(champ: header_section)
|
||||
- splitted_tail.each do |section, champ|
|
||||
- if section.present?
|
||||
= render section
|
||||
- else
|
||||
= fields_for champ.input_name, champ do |form|
|
||||
= render EditableChamp::EditableChampComponent.new form: ,champ:
|
||||
- else
|
||||
- if header_section
|
||||
= render EditableChamp::HeaderSectionComponent.new(champ: header_section)
|
||||
- splitted_tail.each do |section, champ|
|
||||
- if section.present?
|
||||
= render section
|
||||
- else
|
||||
= fields_for champ.input_name, champ do |form|
|
||||
= render EditableChamp::EditableChampComponent.new form: ,champ:
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
fr:
|
||||
select: Sélectionner
|
||||
apply_routing_rules: Appliquer des règles de routage
|
||||
apply_routing_rules: Règles de routage
|
||||
routing_rules_notice_html: |
|
||||
<p>Ajoutez des règles de routage à partir de champs « choix simple » créés dans le <a href="%{path}">formulaire</a>.</p>
|
||||
<p>Les dossiers seront routées vers le premier groupe affiché dont la règle correspond.</p>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.card#routing-rules
|
||||
.card-title
|
||||
= t('.apply_routing_rules')
|
||||
%h2.card-title= t('.apply_routing_rules')
|
||||
- if can_route?
|
||||
.notice
|
||||
= t('.routing_rules_notice_html', path: champs_admin_procedure_path(@procedure_id))
|
||||
|
@ -28,5 +27,14 @@
|
|||
%td.target= targeted_champ_tag(targeted_champ, row_index)
|
||||
%td.operator est égal à
|
||||
%td.value= value_tag(targeted_champ, value, row_index)
|
||||
|
||||
= form_tag admin_procedure_update_defaut_groupe_instructeur_path(@procedure_id),
|
||||
class: 'form flex align-baseline defaut-groupe',
|
||||
data: { controller: 'autosave' } do
|
||||
= label_tag :defaut_groupe_instructeur_id, 'Et si aucune règle ne correspond, router vers :'
|
||||
= select_tag :defaut_groupe_instructeur_id,
|
||||
options_for_select(@groupe_instructeurs.pluck(:label, :id), selected: @revision.procedure.defaut_groupe_instructeur.id),
|
||||
class: 'width-100'
|
||||
|
||||
- else
|
||||
.notice= t('.routing_rules_warning_html', path: champs_admin_procedure_path(@procedure_id))
|
||||
|
|
|
@ -21,12 +21,22 @@ class SimpleFormatComponent < ApplicationComponent
|
|||
no_images: true
|
||||
}
|
||||
|
||||
SIMPLE_URL_REGEX = %r{https?://\S+}
|
||||
EMAIL_IN_TEXT_REGEX = Regexp.new(Devise.email_regexp.source.gsub(/\\A|\\z/, '\b'))
|
||||
|
||||
def initialize(text, allow_a: true, class_names_map: {})
|
||||
@allow_a = allow_a
|
||||
|
||||
@text = (text || "").gsub(/\R/, "\n\n") # force double \n otherwise a single one won't split paragraph
|
||||
.split("\n\n") #
|
||||
.map(&:lstrip) # this block prevent redcarpet to consider " text" as block code by lstriping
|
||||
.join("\n\n") #
|
||||
@allow_a = allow_a
|
||||
.join("\n\n")
|
||||
.gsub(EMAIL_IN_TEXT_REGEX) { _1.gsub('_', '\\_') } # Workaround for redcarpet bug on autolink email having _. Cf tests
|
||||
|
||||
if !@allow_a
|
||||
@text = @text.gsub(SIMPLE_URL_REGEX) { _1.gsub('_', '\\_') } # Escape underscores in URLs
|
||||
end
|
||||
|
||||
@renderer = Redcarpet::Markdown.new(
|
||||
Redcarpet::BareRenderer.new(class_names_map:),
|
||||
REDCARPET_EXTENSIONS.merge(autolink: @allow_a)
|
||||
|
|
|
@ -48,6 +48,11 @@
|
|||
.cell.mt-1
|
||||
= form.label :description, "Description du champ (optionnel)", for: dom_id(type_de_champ, :description)
|
||||
= form.text_area :description, class: 'small-margin small width-100', rows: 3, id: dom_id(type_de_champ, :description)
|
||||
- if type_de_champ.header_section?
|
||||
.cell.mt-1
|
||||
= render TypesDeChampEditor::HeaderSectionComponent.new(form: form, tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ))
|
||||
|
||||
|
||||
|
||||
.flex.justify-start.mt-1
|
||||
- if type_de_champ.drop_down_list?
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.flex.justify-start.section{ id: dom_id(@tdc.stable_self, :conditions) }
|
||||
.flex.justify-start.section{ id: dom_id(@tdc.stable_self, :condition) }
|
||||
= form_tag admin_procedure_condition_path(@procedure_id, @tdc.stable_id), method: :patch, class: 'form width-100' do
|
||||
.conditionnel.mt-2.width-100
|
||||
.flex
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
.condition-error
|
||||
.errors-summary
|
||||
= errors
|
||||
|
|
|
@ -3,16 +3,32 @@ class TypesDeChampEditor::ErrorsSummary < ApplicationComponent
|
|||
@revision = revision
|
||||
end
|
||||
|
||||
def invalid?
|
||||
@revision.invalid?
|
||||
end
|
||||
|
||||
def condition_errors?
|
||||
@revision.errors.include?(:condition)
|
||||
end
|
||||
|
||||
def header_section_errors?
|
||||
@revision.errors.include?(:header_section)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def error_message
|
||||
@revision.errors
|
||||
def errors_for(key)
|
||||
@revision.errors.filter { _1.attribute == key }
|
||||
end
|
||||
|
||||
def error_message_for(key)
|
||||
errors_for(key)
|
||||
.map { |error| error.options[:type_de_champ] }
|
||||
.map { |tdc| tag.li(tdc_anchor(tdc)) }
|
||||
.map { |tdc| tag.li(tdc_anchor(tdc, key)) }
|
||||
.then { |lis| tag.ul(lis.reduce(&:+)) }
|
||||
end
|
||||
|
||||
def tdc_anchor(tdc)
|
||||
tag.a(tdc.libelle, href: champs_admin_procedure_path(@revision.procedure_id, anchor: dom_id(tdc.stable_self, :conditions)), data: { turbo: false })
|
||||
def tdc_anchor(tdc, key)
|
||||
tag.a(tdc.libelle, href: champs_admin_procedure_path(@revision.procedure_id, anchor: dom_id(tdc.stable_self, key)), data: { turbo: false })
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
fr:
|
||||
fix:
|
||||
one: 'Corrigez le champ suivant :'
|
||||
other: 'Corrigez les champs suivants :'
|
||||
fix_conditional:
|
||||
one: 'La logique conditionnelle du champ suivant est invalide, veuillez la corriger :'
|
||||
other: 'La logique conditionnelle des champs suivants sont invalides, veuillez les corriger :'
|
||||
|
||||
fix_header_section:
|
||||
one: 'Le titre de section suivant est invalide, veuillez le corriger :'
|
||||
other: 'Les titres de section suivants sont invalides, veuillez les corriger :'
|
||||
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
#errors-summary
|
||||
- if @revision.invalid?
|
||||
- if invalid?
|
||||
.card.warning
|
||||
.card-title La logique conditionnelle est devenue invalide
|
||||
.card-title Le formulaire contient des erreurs
|
||||
|
||||
%p.mb-2= t('.fix', count: @revision.errors.count)
|
||||
= error_message
|
||||
- if condition_errors?
|
||||
%p.mb-2= t('.fix_conditional', count: errors_for(:condition).size)
|
||||
= error_message_for(:condition)
|
||||
|
||||
- if header_section_errors?
|
||||
%p.mb-2= t('.fix_header_section', count: errors_for(:header_section).size)
|
||||
= error_message_for(:header_section)
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
class TypesDeChampEditor::HeaderSectionComponent < ApplicationComponent
|
||||
MAX_LEVEL = 3
|
||||
|
||||
def initialize(form:, tdc:, upper_tdcs:)
|
||||
@form = form
|
||||
@tdc = tdc
|
||||
@upper_tdcs = upper_tdcs
|
||||
end
|
||||
|
||||
def header_section_options_for_select
|
||||
closest_level = @tdc.previous_section_level(@upper_tdcs)
|
||||
next_level = [closest_level + 1, MAX_LEVEL].min
|
||||
|
||||
available_levels = (1..next_level).map(&method(:option_for_level))
|
||||
disabled_levels = errors? ? (next_level + 1..MAX_LEVEL).map(&method(:option_for_level)) : []
|
||||
options_for_select(
|
||||
available_levels + disabled_levels,
|
||||
disabled: disabled_levels.map(&:second),
|
||||
selected: @tdc.header_section_level_value
|
||||
)
|
||||
end
|
||||
|
||||
def errors
|
||||
@tdc.check_coherent_header_level(@upper_tdcs)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def option_for_level(level)
|
||||
[translate(".select_option", level: level), level]
|
||||
end
|
||||
|
||||
def errors?
|
||||
!errors.empty?
|
||||
end
|
||||
|
||||
def to_html_list(messages)
|
||||
messages
|
||||
.map { |message| tag.li(message) }
|
||||
.then { |lis| tag.ul(lis.reduce(&:+)) }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
en:
|
||||
select_option: "Header section %{level}"
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
fr:
|
||||
select_option: "Titre de niveau %{level}"
|
|
@ -0,0 +1,5 @@
|
|||
%div{ id: dom_id(@tdc.stable_self, :header_section) }
|
||||
- if errors?
|
||||
.errors-summary= to_html_list(errors)
|
||||
= @form.label :header_section_level, "Niveau du titre", for: dom_id(@tdc, :header_section_level)
|
||||
= @form.select :header_section_level, header_section_options_for_select, {}, id: dom_id(@tdc, :header_section_level)
|
|
@ -48,7 +48,10 @@ module Administrateurs
|
|||
def update
|
||||
@groupe_instructeur = groupe_instructeur
|
||||
|
||||
if @groupe_instructeur.update(groupe_instructeur_params)
|
||||
if closed_params? && @groupe_instructeur.id == procedure.defaut_groupe_instructeur.id
|
||||
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur),
|
||||
alert: "Il est impossible de désactiver le groupe d’instructeurs par défaut."
|
||||
elsif @groupe_instructeur.update(groupe_instructeur_params)
|
||||
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur),
|
||||
notice: "Le nom est à présent « #{@groupe_instructeur.label} »."
|
||||
else
|
||||
|
@ -68,6 +71,8 @@ module Administrateurs
|
|||
flash[:alert] = "Impossible de supprimer un groupe avec des dossiers. Il faut le réaffecter avant"
|
||||
elsif procedure.groupe_instructeurs.one?
|
||||
flash[:alert] = "Suppression impossible : il doit y avoir au moins un groupe instructeur sur chaque procédure"
|
||||
elsif @groupe_instructeur.id == procedure.defaut_groupe_instructeur.id
|
||||
flash[:alert] = "Suppression impossible : le groupe « #{@groupe_instructeur.label} » est le groupe par défaut."
|
||||
else
|
||||
@groupe_instructeur.destroy!
|
||||
if procedure.groupe_instructeurs.active.one?
|
||||
|
@ -258,6 +263,10 @@ module Administrateurs
|
|||
|
||||
private
|
||||
|
||||
def closed_params?
|
||||
groupe_instructeur_params[:closed] == "1"
|
||||
end
|
||||
|
||||
def procedure
|
||||
current_administrateur
|
||||
.procedures
|
||||
|
|
|
@ -219,7 +219,7 @@ module Administrateurs
|
|||
|
||||
def restore
|
||||
procedure = current_administrateur.procedures.with_discarded.discarded.find(params[:id])
|
||||
procedure.restore_procedure(current_administrateur)
|
||||
procedure.restore(current_administrateur)
|
||||
flash.notice = t('administrateurs.index.restored', procedure_id: procedure.id)
|
||||
redirect_to admin_procedures_path
|
||||
end
|
||||
|
|
|
@ -12,6 +12,11 @@ module Administrateurs
|
|||
groupe_instructeur.update!(routing_rule: ds_eq(left, right))
|
||||
end
|
||||
|
||||
def update_defaut_groupe_instructeur
|
||||
new_defaut = @procedure.groupe_instructeurs.find(defaut_groupe_instructeur_id)
|
||||
@procedure.update!(defaut_groupe_instructeur: new_defaut)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def targeted_champ_changed?
|
||||
|
@ -19,11 +24,11 @@ module Administrateurs
|
|||
end
|
||||
|
||||
def targeted_champ
|
||||
Logic.from_json(routing_params[:targeted_champ])
|
||||
Logic.from_json(params[:targeted_champ])
|
||||
end
|
||||
|
||||
def value
|
||||
Logic.from_json(routing_params[:value])
|
||||
Logic.from_json(params[:value])
|
||||
end
|
||||
|
||||
def groupe_instructeur
|
||||
|
@ -31,11 +36,11 @@ module Administrateurs
|
|||
end
|
||||
|
||||
def groupe_instructeur_id
|
||||
routing_params[:groupe_instructeur_id]
|
||||
params[:groupe_instructeur_id]
|
||||
end
|
||||
|
||||
def routing_params
|
||||
params.permit(:targeted_champ, :value, :groupe_instructeur_id)
|
||||
def defaut_groupe_instructeur_id
|
||||
params[:defaut_groupe_instructeur_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -128,6 +128,7 @@ module Administrateurs
|
|||
:drop_down_secondary_description,
|
||||
:collapsible_explanation_enabled,
|
||||
:collapsible_explanation_text,
|
||||
:header_section_level,
|
||||
editable_options: [
|
||||
:cadastres,
|
||||
:unesco,
|
||||
|
|
|
@ -14,7 +14,7 @@ class API::Public::V1::StatsController < API::Public::V1::BaseController
|
|||
private
|
||||
|
||||
def retrieve_procedure
|
||||
@procedure = Procedure.publiees_ou_brouillons.opendata.find_by(id: params[:id])
|
||||
@procedure = Procedure.opendata.find_by(id: params[:id])
|
||||
render_not_found("procedure", params[:id]) if @procedure.blank?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,13 @@ class API::V2::DossiersController < API::V2::BaseController
|
|||
|
||||
private
|
||||
|
||||
def append_info_to_payload(payload)
|
||||
super
|
||||
if dossier.present?
|
||||
payload.merge!(ds_dossier_id: dossier.id.to_s, ds_procedure_id: dossier.procedure.id.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_dossier_present
|
||||
if dossier.blank?
|
||||
head :unauthorized
|
||||
|
|
|
@ -5,7 +5,9 @@ class API::V2::GraphqlController < API::V2::BaseController
|
|||
|
||||
render json: result
|
||||
rescue GraphQL::ParseError, JSON::ParserError => exception
|
||||
handle_parse_error(exception)
|
||||
handle_parse_error(exception, :graphql_parse_failed)
|
||||
rescue ArgumentError => exception
|
||||
handle_parse_error(exception, :bad_request)
|
||||
rescue => exception
|
||||
if Rails.env.production?
|
||||
handle_error_in_production(exception)
|
||||
|
@ -33,7 +35,12 @@ class API::V2::GraphqlController < API::V2::BaseController
|
|||
rescue ActionDispatch::Http::Parameters::ParseError => exception
|
||||
render json: {
|
||||
errors: [
|
||||
{ message: exception.cause.message }
|
||||
{
|
||||
message: exception.cause.message,
|
||||
extensions: {
|
||||
code: :bad_request
|
||||
}
|
||||
}
|
||||
],
|
||||
data: nil
|
||||
}, status: 400
|
||||
|
@ -75,10 +82,13 @@ class API::V2::GraphqlController < API::V2::BaseController
|
|||
end
|
||||
end
|
||||
|
||||
def handle_parse_error(exception)
|
||||
def handle_parse_error(exception, code)
|
||||
render json: {
|
||||
errors: [
|
||||
{ message: exception.message }
|
||||
{
|
||||
message: exception.message,
|
||||
extensions: { code: }
|
||||
}
|
||||
],
|
||||
data: nil
|
||||
}, status: 400
|
||||
|
@ -90,22 +100,32 @@ class API::V2::GraphqlController < API::V2::BaseController
|
|||
|
||||
render json: {
|
||||
errors: [
|
||||
{ message: exception.message, backtrace: exception.backtrace }
|
||||
{
|
||||
message: exception.message,
|
||||
extensions: {
|
||||
code: :internal_server_error,
|
||||
backtrace: exception.backtrace
|
||||
}
|
||||
}
|
||||
],
|
||||
data: nil
|
||||
}, status: 500
|
||||
end
|
||||
|
||||
def handle_error_in_production(exception)
|
||||
extra = { exception_id: SecureRandom.uuid }
|
||||
Sentry.capture_exception(exception, extra:)
|
||||
exception_id = SecureRandom.uuid
|
||||
Sentry.with_scope do |scope|
|
||||
scope.set_tags(exception_id:)
|
||||
Sentry.capture_exception(exception)
|
||||
end
|
||||
|
||||
render json: {
|
||||
errors: [
|
||||
{
|
||||
message: "Internal Server Error",
|
||||
extensions: {
|
||||
exception: { id: extra[:exception_id] }
|
||||
code: :internal_server_error,
|
||||
exception_id:
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -326,7 +326,7 @@ module Instructeurs
|
|||
|
||||
def champs_private_params
|
||||
champs_params = params.require(:dossier).permit(champs_private_attributes: [
|
||||
:id, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, :value, value: [],
|
||||
:id, :value, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, value: [],
|
||||
champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, value: []]
|
||||
])
|
||||
champs_params[:champs_private_all_attributes] = champs_params.delete(:champs_private_attributes) || {}
|
||||
|
|
|
@ -89,6 +89,7 @@ module Instructeurs
|
|||
.per(ITEMS_PER_PAGE)
|
||||
|
||||
@projected_dossiers = DossierProjectionService.project(@filtered_sorted_paginated_ids, procedure_presentation.displayed_fields)
|
||||
@disable_checkbox_all = @projected_dossiers.all? { _1.batch_operation_id.present? }
|
||||
|
||||
assign_exports
|
||||
@batch_operations = BatchOperation.joins(:groupe_instructeurs)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class PingController < ApplicationController
|
||||
def index
|
||||
Rails.logger.silence do
|
||||
status_code = if File.file?(Rails.root.join("maintenance"))
|
||||
status_code = if Rails.root.join("maintenance").file?
|
||||
# See https://cbonte.github.io/haproxy-dconv/2.0/configuration.html#4.2-http-check%20disable-on-404
|
||||
:not_found
|
||||
elsif (ActiveRecord::Base.connection.execute('select 1 as test;').first['test'] == 1)
|
||||
|
|
|
@ -83,7 +83,12 @@ class RootController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
@dossier.association(:revision).target = @dossier.procedure.build_draft_revision
|
||||
draft_revision = @dossier.procedure.build_draft_revision(types_de_champ_public: all_champs.map(&:type_de_champ))
|
||||
@dossier.association(:revision).target = draft_revision
|
||||
@dossier.champs_public.map(&:type_de_champ).map do |tdc|
|
||||
tdc.association(:revision_type_de_champ).target = tdc.build_revision_type_de_champ(revision: draft_revision)
|
||||
tdc.association(:revision).target = draft_revision
|
||||
end
|
||||
end
|
||||
|
||||
def suivi
|
||||
|
|
|
@ -73,7 +73,7 @@ class SupportController < ApplicationController
|
|||
[params[:tags], params[:type]].flatten.compact
|
||||
.map { |tag| tag.split(',') }
|
||||
.flatten
|
||||
.reject(&:blank?).uniq
|
||||
.compact_blank.uniq
|
||||
end
|
||||
|
||||
def browser_name
|
||||
|
|
|
@ -11,6 +11,7 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
published_types_de_champ_public: TypesDeChampCollectionField,
|
||||
published_types_de_champ_private: TypesDeChampCollectionField,
|
||||
path: ProcedureLinkField,
|
||||
aasm_state: ProcedureStateField,
|
||||
dossiers: Field::HasMany,
|
||||
administrateurs: Field::HasMany,
|
||||
id: Field::Number.with_options(searchable: true),
|
||||
|
@ -40,6 +41,7 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
max_duree_conservation_dossiers_dans_ds: Field::Number,
|
||||
estimated_duration_visible: Field::Boolean,
|
||||
piece_justificative_multiple: Field::Boolean,
|
||||
replaced_by_procedure_id: Field::String,
|
||||
tags: Field::Text
|
||||
}.freeze
|
||||
|
||||
|
@ -56,7 +58,7 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
:service,
|
||||
:dossiers,
|
||||
:published_at,
|
||||
:unpublished_at
|
||||
:aasm_state
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
|
@ -64,6 +66,7 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
SHOW_PAGE_ATTRIBUTES = [
|
||||
:id,
|
||||
:path,
|
||||
:aasm_state,
|
||||
:administrateurs,
|
||||
:libelle,
|
||||
:description,
|
||||
|
@ -78,6 +81,7 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
:whitelisted_at,
|
||||
:hidden_at,
|
||||
:closed_at,
|
||||
:unpublished_at,
|
||||
:published_types_de_champ_public,
|
||||
:published_types_de_champ_private,
|
||||
:for_individual,
|
||||
|
@ -92,7 +96,8 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
:duree_conservation_dossiers_dans_ds,
|
||||
:max_duree_conservation_dossiers_dans_ds,
|
||||
:estimated_duration_visible,
|
||||
:piece_justificative_multiple
|
||||
:piece_justificative_multiple,
|
||||
:replaced_by_procedure_id
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
|
@ -103,7 +108,8 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
:duree_conservation_dossiers_dans_ds,
|
||||
:max_duree_conservation_dossiers_dans_ds,
|
||||
:estimated_duration_visible,
|
||||
:piece_justificative_multiple
|
||||
:piece_justificative_multiple,
|
||||
:replaced_by_procedure_id
|
||||
].freeze
|
||||
|
||||
# Overwrite this method to customize how procedures are displayed
|
||||
|
|
7
app/fields/procedure_state_field.rb
Normal file
7
app/fields/procedure_state_field.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require "administrate/field/base"
|
||||
|
||||
class ProcedureStateField < Administrate::Field::String
|
||||
def name
|
||||
"Statut"
|
||||
end
|
||||
end
|
|
@ -25,8 +25,6 @@ class API::V2::Schema < GraphQL::Schema
|
|||
|
||||
def self.object_from_id(id, ctx)
|
||||
ApplicationRecord.record_from_typed_id(id)
|
||||
rescue => e
|
||||
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
||||
end
|
||||
|
||||
def self.resolve_type(type_definition, object, ctx)
|
||||
|
@ -50,7 +48,7 @@ class API::V2::Schema < GraphQL::Schema
|
|||
when GroupeInstructeur
|
||||
Types::GroupeInstructeurType
|
||||
else
|
||||
raise GraphQL::ExecutionError.new("Unexpected object: #{object}")
|
||||
type_definition
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -129,8 +127,13 @@ class API::V2::Schema < GraphQL::Schema
|
|||
super
|
||||
end
|
||||
|
||||
rescue_from(ActiveRecord::RecordNotFound) do |_error, _object, _args, _ctx, field|
|
||||
raise GraphQL::ExecutionError.new("#{field.type.unwrap.graphql_name} not found", extensions: { code: :not_found })
|
||||
end
|
||||
|
||||
class Timeout < GraphQL::Schema::Timeout
|
||||
def handle_timeout(error, query)
|
||||
error.extensions = { code: :timeout }
|
||||
Sentry.capture_exception(error, extra: query.context.query_info)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ class API::V2::StoredQuery
|
|||
when 'introspection'
|
||||
GraphQL::Introspection::INTROSPECTION_QUERY
|
||||
else
|
||||
raise GraphQL::ExecutionError.new("No query with id \"#{query_id}\"")
|
||||
raise GraphQL::ExecutionError.new("No query with id \"#{query_id}\"", extensions: { code: :bad_request })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -23,18 +23,18 @@ class API::V2::StoredQuery
|
|||
$revision: ID
|
||||
$createdSince: ISO8601DateTime
|
||||
$updatedSince: ISO8601DateTime
|
||||
$deletedOrder: Order
|
||||
$deletedFirst: Int
|
||||
$deletedAfter: String
|
||||
$deletedSince: ISO8601DateTime
|
||||
$pendingDeletedOrder: Order
|
||||
$pendingDeletedFirst: Int
|
||||
$pendingDeletedAfter: String
|
||||
$pendingDeletedSince: ISO8601DateTime
|
||||
$deletedOrder: Order
|
||||
$deletedFirst: Int
|
||||
$deletedAfter: String
|
||||
$deletedSince: ISO8601DateTime
|
||||
$includeGroupeInstructeurs: Boolean = false
|
||||
$includeDossiers: Boolean = false
|
||||
$includeDeletedDossiers: Boolean = false
|
||||
$includePendingDeletedDossiers: Boolean = false
|
||||
$includeDeletedDossiers: Boolean = false
|
||||
$includeRevision: Boolean = false
|
||||
$includeService: Boolean = false
|
||||
$includeChamps: Boolean = true
|
||||
|
@ -118,11 +118,16 @@ class API::V2::StoredQuery
|
|||
$revision: ID
|
||||
$createdSince: ISO8601DateTime
|
||||
$updatedSince: ISO8601DateTime
|
||||
$pendingDeletedOrder: Order
|
||||
$pendingDeletedFirst: Int
|
||||
$pendingDeletedAfter: String
|
||||
$pendingDeletedSince: ISO8601DateTime
|
||||
$deletedOrder: Order
|
||||
$deletedFirst: Int
|
||||
$deletedAfter: String
|
||||
$deletedSince: ISO8601DateTime
|
||||
$includeDossiers: Boolean = false
|
||||
$includePendingDeletedDossiers: Boolean = false
|
||||
$includeDeletedDossiers: Boolean = false
|
||||
$includeChamps: Boolean = true
|
||||
$includeAnotations: Boolean = true
|
||||
|
@ -157,6 +162,19 @@ class API::V2::StoredQuery
|
|||
...DossierFragment
|
||||
}
|
||||
}
|
||||
pendingDeletedDossiers(
|
||||
order: $pendingDeletedOrder
|
||||
first: $pendingDeletedFirst
|
||||
after: $pendingDeletedAfter
|
||||
deletedSince: $pendingDeletedSince
|
||||
) @include(if: $includePendingDeletedDossiers) {
|
||||
pageInfo {
|
||||
...PageInfoFragment
|
||||
}
|
||||
nodes {
|
||||
...DeletedDossierFragment
|
||||
}
|
||||
}
|
||||
deletedDossiers(
|
||||
order: $deletedOrder
|
||||
first: $deletedFirst
|
||||
|
@ -240,7 +258,7 @@ class API::V2::StoredQuery
|
|||
...FileFragment
|
||||
}
|
||||
pdf {
|
||||
url
|
||||
...FileFragment
|
||||
}
|
||||
usager {
|
||||
email
|
||||
|
@ -250,14 +268,9 @@ class API::V2::StoredQuery
|
|||
}
|
||||
demandeur {
|
||||
__typename
|
||||
... on PersonnePhysique {
|
||||
civilite
|
||||
nom
|
||||
prenom
|
||||
dateDeNaissance
|
||||
}
|
||||
... on PersonneMoraleIncomplete { siret }
|
||||
...PersonnePhysiqueFragment
|
||||
...PersonneMoraleFragment
|
||||
...PersonneMoraleIncompleteFragment
|
||||
}
|
||||
demarche {
|
||||
revision {
|
||||
|
@ -368,24 +381,6 @@ class API::V2::StoredQuery
|
|||
collapsibleExplanationEnabled
|
||||
collapsibleExplanationText
|
||||
}
|
||||
... on PaysChampDescriptor {
|
||||
options {
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
... on RegionChampDescriptor {
|
||||
options {
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
... on DepartementChampDescriptor {
|
||||
options {
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment AvisFragment on Avis {
|
||||
|
@ -502,8 +497,7 @@ class API::V2::StoredQuery
|
|||
}
|
||||
... on EpciChamp {
|
||||
epci {
|
||||
name
|
||||
code
|
||||
...EpciFragment
|
||||
}
|
||||
departement {
|
||||
...DepartementFragment
|
||||
|
@ -524,14 +518,12 @@ class API::V2::StoredQuery
|
|||
}
|
||||
... on RegionChamp {
|
||||
region {
|
||||
name
|
||||
code
|
||||
...RegionFragment
|
||||
}
|
||||
}
|
||||
... on PaysChamp {
|
||||
pays {
|
||||
name
|
||||
code
|
||||
...PaysFragment
|
||||
}
|
||||
}
|
||||
... on SiretChamp {
|
||||
|
@ -579,6 +571,17 @@ class API::V2::StoredQuery
|
|||
}
|
||||
}
|
||||
|
||||
fragment PersonneMoraleIncompleteFragment on PersonneMoraleIncomplete {
|
||||
siret
|
||||
}
|
||||
|
||||
fragment PersonnePhysiqueFragment on PersonnePhysique {
|
||||
civilite
|
||||
nom
|
||||
prenom
|
||||
}
|
||||
|
||||
|
||||
fragment FileFragment on File {
|
||||
filename
|
||||
contentType
|
||||
|
@ -602,11 +605,26 @@ class API::V2::StoredQuery
|
|||
regionCode
|
||||
}
|
||||
|
||||
fragment PaysFragment on Pays {
|
||||
name
|
||||
code
|
||||
}
|
||||
|
||||
fragment RegionFragment on Region {
|
||||
name
|
||||
code
|
||||
}
|
||||
|
||||
fragment DepartementFragment on Departement {
|
||||
name
|
||||
code
|
||||
}
|
||||
|
||||
fragment EpciFragment on Epci {
|
||||
name
|
||||
code
|
||||
}
|
||||
|
||||
fragment CommuneFragment on Commune {
|
||||
name
|
||||
code
|
||||
|
|
|
@ -13,7 +13,7 @@ module Loaders
|
|||
|
||||
def perform(keys)
|
||||
query(keys).each { |record| fulfill(record.stable_id, [record].compact) }
|
||||
keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
|
||||
keys.each { |key| fulfill(key, []) unless fulfilled?(key) }
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -21,7 +21,7 @@ module Loaders
|
|||
fulfilled_value = @array ? [record].compact : record
|
||||
fulfill(record.public_send(@column), fulfilled_value)
|
||||
end
|
||||
keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
|
||||
keys.each { |key| fulfill(key, @array ? [] : nil) unless fulfilled?(key) }
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1070,7 +1070,7 @@ enum DemarcheState {
|
|||
close
|
||||
|
||||
"""
|
||||
Depubliee
|
||||
Dépubliée
|
||||
"""
|
||||
depubliee
|
||||
|
||||
|
@ -1921,12 +1921,12 @@ enum DossierState {
|
|||
accepte
|
||||
|
||||
"""
|
||||
En construction
|
||||
En construction
|
||||
"""
|
||||
en_construction
|
||||
|
||||
"""
|
||||
En instruction
|
||||
En instruction
|
||||
"""
|
||||
en_instruction
|
||||
|
||||
|
@ -1936,7 +1936,7 @@ enum DossierState {
|
|||
refuse
|
||||
|
||||
"""
|
||||
Classé sans suite
|
||||
Classé sans suite
|
||||
"""
|
||||
sans_suite
|
||||
}
|
||||
|
@ -2478,6 +2478,41 @@ type GroupeInstructeurWithDossiers {
|
|||
Le numero du groupe instructeur.
|
||||
"""
|
||||
number: Int!
|
||||
|
||||
"""
|
||||
Liste de tous les dossiers en attente de suppression définitive d’un groupe instructeur.
|
||||
"""
|
||||
pendingDeletedDossiers(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Dossiers en attente de suppression depuis la date.
|
||||
"""
|
||||
deletedSince: ISO8601DateTime
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
|
||||
"""
|
||||
L’ordre des dossiers en attente de suppression.
|
||||
"""
|
||||
order: Order = ASC
|
||||
): DeletedDossierConnection!
|
||||
}
|
||||
|
||||
type HeaderSectionChampDescriptor implements ChampDescriptor {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
module Types
|
||||
class BaseObject < GraphQL::Schema::Object
|
||||
field_class BaseField
|
||||
|
||||
class InvalidNullError < GraphQL::InvalidNullError
|
||||
def to_h
|
||||
super.merge(extensions: { code: :invalid_null })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -143,7 +143,10 @@ module Types
|
|||
{
|
||||
filename: "dossier-#{object.id}.pdf",
|
||||
content_type: 'application/pdf',
|
||||
url: Rails.application.routes.url_helpers.api_v2_dossier_pdf_url(id: sgid)
|
||||
url: Rails.application.routes.url_helpers.api_v2_dossier_pdf_url(id: sgid),
|
||||
byte_size: 0,
|
||||
byte_size_big_int: '0',
|
||||
checksum: ''
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -152,7 +155,10 @@ module Types
|
|||
{
|
||||
filename: "dossier-#{object.id}-features.json",
|
||||
content_type: 'application/json',
|
||||
url: Rails.application.routes.url_helpers.api_v2_dossier_geojson_url(id: sgid)
|
||||
url: Rails.application.routes.url_helpers.api_v2_dossier_geojson_url(id: sgid),
|
||||
byte_size: 0,
|
||||
byte_size_big_int: '0',
|
||||
checksum: ''
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -18,6 +18,11 @@ module Types
|
|||
argument :deleted_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers supprimés depuis la date."
|
||||
end
|
||||
|
||||
field :pending_deleted_dossiers, Types::DeletedDossierType.connection_type, "Liste de tous les dossiers en attente de suppression définitive d’un groupe instructeur.", null: false do
|
||||
argument :order, Types::Order, default_value: :asc, required: false, description: "L’ordre des dossiers en attente de suppression."
|
||||
argument :deleted_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers en attente de suppression depuis la date."
|
||||
end
|
||||
|
||||
def dossiers(updated_since: nil, created_since: nil, state: nil, archived: nil, revision: nil, max_revision: nil, min_revision: nil, order:, lookahead:)
|
||||
dossiers = object
|
||||
.dossiers
|
||||
|
@ -70,5 +75,15 @@ module Types
|
|||
|
||||
dossiers.order(deleted_at: order)
|
||||
end
|
||||
|
||||
def pending_deleted_dossiers(deleted_since: nil, order:)
|
||||
dossiers = object.dossiers.hidden_for_administration
|
||||
|
||||
if deleted_since.present?
|
||||
dossiers = dossiers.hidden_since(deleted_since)
|
||||
end
|
||||
|
||||
dossiers.order(hidden_by_user_at: order, hidden_by_administration_at: order)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,13 +26,11 @@ module Types
|
|||
demarche_number = demarche.number.presence || ApplicationRecord.id_from_typed_id(demarche.id)
|
||||
Procedure
|
||||
.includes(draft_revision: :procedure, published_revision: :procedure)
|
||||
.find_by(id: demarche_number)
|
||||
.find(demarche_number)
|
||||
end
|
||||
|
||||
def demarche(number:)
|
||||
Procedure.for_api_v2.find(number)
|
||||
rescue => e
|
||||
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
||||
end
|
||||
|
||||
def dossier(number:)
|
||||
|
@ -42,14 +40,10 @@ module Types
|
|||
Dossier.visible_by_administration.for_api_v2.find(number)
|
||||
end
|
||||
DossierPreloader.load_one(dossier)
|
||||
rescue => e
|
||||
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
||||
end
|
||||
|
||||
def groupe_instructeur(number:)
|
||||
GroupeInstructeur.for_api_v2.find(number)
|
||||
rescue => e
|
||||
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
||||
end
|
||||
|
||||
def self.accessible?(context)
|
||||
|
|
|
@ -84,10 +84,10 @@ module DossierHelper
|
|||
|
||||
def deletion_reason_badge(reason)
|
||||
if reason.present?
|
||||
status_text = I18n.t(reason, scope: [:activerecord, :attributes, :deleted_dossier, :reason])
|
||||
status_text = I18n.t(reason, scope: 'activerecord.attributes.deleted_dossier.reason')
|
||||
status_class = reason.tr('_', '-')
|
||||
else
|
||||
status_text = I18n.t(:unknown, scope: [:activerecord, :attributes, :deleted_dossier, :reason])
|
||||
status_text = I18n.t('activerecord.attributes.deleted_dossier.reason.unknown')
|
||||
status_class = 'unknown'
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ export class BatchOperationController extends ApplicationController {
|
|||
this.inputTargets.forEach((e) => (e.checked = target.checked));
|
||||
this.toggleSubmitButtonWhenNeeded();
|
||||
|
||||
const pagination = document.querySelector('tfoot .pagination');
|
||||
const pagination = document.querySelector('tfoot .fr-pagination');
|
||||
if (pagination) {
|
||||
displayNotice(this.inputTargets);
|
||||
}
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
import {
|
||||
isSelectElement,
|
||||
isCheckboxOrRadioInputElement,
|
||||
show,
|
||||
hide,
|
||||
enable,
|
||||
disable
|
||||
} from '@utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ApplicationController } from './application_controller';
|
||||
|
||||
export class ChampDropdownController extends ApplicationController {
|
||||
connect() {
|
||||
this.on('change', (event) => this.onChange(event));
|
||||
}
|
||||
|
||||
private onChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (!target.disabled) {
|
||||
if (isSelectElement(target) || isCheckboxOrRadioInputElement(target)) {
|
||||
this.toggleOtherInput(target);
|
||||
this.toggleLinkedSelect(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private toggleOtherInput(target: HTMLSelectElement | HTMLInputElement) {
|
||||
const parent = target.closest('.editable-champ-drop_down_list');
|
||||
const inputGroup = parent?.querySelector<HTMLElement>('.drop_down_other');
|
||||
if (inputGroup) {
|
||||
const input = inputGroup.querySelector('input');
|
||||
if (input) {
|
||||
if (target.value == '__other__') {
|
||||
show(inputGroup);
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
} else {
|
||||
hide(inputGroup);
|
||||
input.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private toggleLinkedSelect(target: HTMLSelectElement | HTMLInputElement) {
|
||||
const secondaryOptions = target.dataset.secondaryOptions;
|
||||
if (isSelectElement(target) && secondaryOptions) {
|
||||
const parent = target.closest('.editable-champ-linked_drop_down_list');
|
||||
const secondary = parent?.querySelector<HTMLSelectElement>(
|
||||
'select[data-secondary]'
|
||||
);
|
||||
if (secondary) {
|
||||
const options = parseOptions(secondaryOptions);
|
||||
this.setSecondaryOptions(secondary, options[target.value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setSecondaryOptions(
|
||||
secondarySelectElement: HTMLSelectElement,
|
||||
options: string[]
|
||||
) {
|
||||
const wrapper = secondarySelectElement.closest('.secondary');
|
||||
const hidden = wrapper?.nextElementSibling as HTMLInputElement | null;
|
||||
|
||||
secondarySelectElement.innerHTML = '';
|
||||
|
||||
if (options.length) {
|
||||
disable(hidden);
|
||||
|
||||
if (secondarySelectElement.required) {
|
||||
secondarySelectElement.appendChild(makeOption(''));
|
||||
}
|
||||
for (const option of options) {
|
||||
secondarySelectElement.appendChild(makeOption(option));
|
||||
}
|
||||
|
||||
secondarySelectElement.selectedIndex = 0;
|
||||
enable(secondarySelectElement);
|
||||
show(wrapper);
|
||||
} else {
|
||||
hide(wrapper);
|
||||
disable(secondarySelectElement);
|
||||
enable(hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SecondaryOptions = z.record(z.string().array());
|
||||
|
||||
function parseOptions(options: string) {
|
||||
return SecondaryOptions.parse(JSON.parse(options));
|
||||
}
|
||||
|
||||
function makeOption(option: string) {
|
||||
const element = document.createElement('option');
|
||||
element.textContent = option;
|
||||
element.value = option;
|
||||
return element;
|
||||
}
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::AccountByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
User.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count
|
||||
User.where(created_at: 1.month.ago.all_month).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::AdministrateurByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
Administrateur.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count
|
||||
Administrateur.where(created_at: 1.month.ago.all_month).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::FileByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
Dossier.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count
|
||||
Dossier.where(created_at: 1.month.ago.all_month).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::FileDeposeByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
Dossier.where(depose_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count
|
||||
Dossier.where(depose_at: 1.month.ago.all_month).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::InstructeurByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
Instructeur.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count
|
||||
Instructeur.where(created_at: 1.month.ago.all_month).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::InstructeurConnectedByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
Instructeur.joins(:user).where(user: { last_sign_in_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month }).count
|
||||
Instructeur.joins(:user).where(user: { last_sign_in_at: 1.month.ago.all_month }).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::ProcedureByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
Procedure.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count
|
||||
Procedure.where(created_at: 1.month.ago.all_month).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::ProcedureClosedByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
Procedure.where(closed_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count
|
||||
Procedure.where(closed_at: 1.month.ago.all_month).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::ProcedureDeletedByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
Procedure.where(hidden_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count
|
||||
Procedure.where(hidden_at: 1.month.ago.all_month).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,6 @@ class Cron::Datagouv::UserConnectedWithFranceConnectByMonthJob < Cron::CronJob
|
|||
end
|
||||
|
||||
def data
|
||||
User.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month, loged_in_with_france_connect: "particulier").count
|
||||
User.where(created_at: 1.month.ago.all_month, loged_in_with_france_connect: "particulier").count
|
||||
end
|
||||
end
|
||||
|
|
14
app/jobs/cron/stalled_declarative_procedures_job.rb
Normal file
14
app/jobs/cron/stalled_declarative_procedures_job.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class Cron::StalledDeclarativeProceduresJob < Cron::CronJob
|
||||
self.schedule_expression = "every 10 minute"
|
||||
|
||||
def perform(*args)
|
||||
Procedure.declarative.find_each do |procedure|
|
||||
begin
|
||||
procedure.process_stalled_dossiers!
|
||||
rescue => e
|
||||
Sentry.set_tags(procedure: procedure.id)
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,6 +4,7 @@ class Dolist::API
|
|||
CONTACT_URL = "https://apiv9.dolist.net/v1/contacts/read?AccountID=%{account_id}"
|
||||
EMAIL_LOGS_URL = "https://apiv9.dolist.net/v1/statistics/email/sendings/transactional/search?AccountID=%{account_id}"
|
||||
EMAIL_KEY = 7
|
||||
STATUS_KEY = 72
|
||||
DOLIST_WEB_DASHBOARD = "https://campaign.dolist.net/#/%{account_id}/contacts/%{contact_id}/sendings"
|
||||
EMAIL_MESSAGES_ADRESSES_REPLIES = "https://apiv9.dolist.net/v1/email/messages/addresses/replies?AccountID=%{account_id}"
|
||||
EMAIL_MESSAGES_ADRESSES_PACKSENDERS = "https://apiv9.dolist.net/v1/email/messages/addresses/packsenders?AccountID=%{account_id}"
|
||||
|
@ -13,6 +14,18 @@ class Dolist::API
|
|||
|
||||
class_attribute :limit_remaining, :limit_reset_at
|
||||
|
||||
# those code are just undocumented
|
||||
IGNORABLE_API_ERROR_CODE = [
|
||||
"458",
|
||||
"402"
|
||||
]
|
||||
|
||||
# see: https://usercampaign.dolist.net/wp-content/uploads/2022/12/Comprendre-les-Opt-out-tableau-v2.pdf
|
||||
IGNORABLE_CONTACT_STATUSES = [
|
||||
"4", # Le serveur distant n'accepte pas le mail car il identifie que l’adresse e-mail est en erreur.
|
||||
"7" # Suite à un envoi, le serveur distant accepte le mail dans un premier temps mais envoie une erreur définitive car l’adresse e-mail est en erreur. L'adresse e-mail n’existe pas ou n'existe plus.
|
||||
]
|
||||
|
||||
class << self
|
||||
def save_rate_limit_headers(headers)
|
||||
self.limit_remaining = headers["X-Rate-Limit-Remaining"].to_i
|
||||
|
@ -124,8 +137,43 @@ class Dolist::API
|
|||
get format_url(EMAIL_MESSAGES_ADRESSES_REPLIES)
|
||||
end
|
||||
|
||||
# Une adresse e-mail peut ne pas être adressable pour différentes raisons (injoignable, plainte pour spam, blocage d’un FAI).
|
||||
# Dans ce cas l’API d’envoi transactionnel renvoie différentes erreurs.
|
||||
# Pour connaitre exactement le statut d’une adresse, je vous invite à récupérer le champ 72 du contact à partir de son adresse e-mail avec la méthode https://api.dolist.com/documentation/index.html#/40e7751d00dc3-rechercher-un-contact
|
||||
#
|
||||
# La liste des différents statuts est disponible sur https://usercampaign.dolist.net/wp-content/uploads/2022/12/Comprendre-les-Opt-out-tableau-v2.pdf
|
||||
def fetch_contact_status(email_address)
|
||||
url = format(Dolist::API::CONTACT_URL, account_id: account_id)
|
||||
body = {
|
||||
Query: {
|
||||
FieldValueList: [{ ID: 7, Value: email_address }],
|
||||
OutputFieldIDList: [72]
|
||||
}
|
||||
}.to_json
|
||||
|
||||
post(url, body)["FieldList"].find { _1['ID'] == 72 }['Value']
|
||||
end
|
||||
|
||||
def ignorable_error?(response, mail)
|
||||
error_code = response&.dig("ResponseStatus", "ErrorCode")
|
||||
invalid_contact_status = if ignorable_api_error_code?(error_code)
|
||||
fetch_contact_status(mail.to.first)
|
||||
else
|
||||
nil
|
||||
end
|
||||
[error_code, invalid_contact_status]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ignorable_api_error_code?(api_error_code)
|
||||
IGNORABLE_API_ERROR_CODE.include?(api_error_code)
|
||||
end
|
||||
|
||||
def ignorable_contact_status?(contact_status)
|
||||
IGNORABLE_CONTACT_STATUSES.include?(contact_status)
|
||||
end
|
||||
|
||||
def format_url(base)
|
||||
format(base, account_id: account_id)
|
||||
end
|
||||
|
|
|
@ -4,7 +4,6 @@ module Redcarpet
|
|||
include ApplicationHelper
|
||||
|
||||
# won't use rubocop tag method because it is missing output buffer
|
||||
# rubocop:disable Rails/ContentTag
|
||||
def list(content, list_type)
|
||||
tag = list_type == :ordered ? :ol : :ul
|
||||
content_tag(tag, content, { class: @options[:class_names_map].fetch(:list) {} }, false)
|
||||
|
@ -27,12 +26,11 @@ module Redcarpet
|
|||
when :url
|
||||
link(link, nil, link)
|
||||
when :email
|
||||
# NOTE: As of Redcarpet 3.6.0, autolinking email containing is broken https://github.com/vmg/redcarpet/issues/402
|
||||
content_tag(:a, link, { href: "mailto:#{link}" })
|
||||
else
|
||||
link
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Rails/ContentTag
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,12 +19,18 @@ module MailerMonitoringConcern
|
|||
end
|
||||
end
|
||||
|
||||
rescue_from Dolist::IgnorableError, with: :log_delivery_error
|
||||
|
||||
def log_and_raise_delivery_error(exception)
|
||||
EmailEvent.create_from_message!(message, status: "dispatch_error")
|
||||
log_delivery_error(exception)
|
||||
Sentry.capture_exception(exception, extra: { to: message.to, subject: message.subject })
|
||||
|
||||
# re-raise another error so job will retry later
|
||||
raise MailDeliveryError.new(exception)
|
||||
end
|
||||
|
||||
def log_delivery_error(exception)
|
||||
EmailEvent.create_from_message!(message, status: "dispatch_error")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -42,7 +42,6 @@ class Avis < ApplicationRecord
|
|||
size: { less_than: FILE_MAX_SIZE }
|
||||
|
||||
validates :email, format: { with: Devise.email_regexp, message: "n'est pas valide" }, allow_nil: true
|
||||
validates :claimant, presence: true
|
||||
validates :question_answer, inclusion: { in: [true, false] }, on: :update, if: -> { question_label.present? }
|
||||
validates :piece_justificative_file, size: { less_than: FILE_MAX_SIZE }
|
||||
validates :introduction_file, size: { less_than: FILE_MAX_SIZE }
|
||||
|
|
|
@ -48,6 +48,8 @@ class Champ < ApplicationRecord
|
|||
:drop_down_secondary_description,
|
||||
:collapsible_explanation_enabled?,
|
||||
:collapsible_explanation_text,
|
||||
:header_section_level_value,
|
||||
:current_section_level,
|
||||
:exclude_from_export?,
|
||||
:exclude_from_view?,
|
||||
:repetition?,
|
||||
|
|
|
@ -52,6 +52,10 @@ class Champs::CommuneChamp < Champs::TextChamp
|
|||
code_postal.present?
|
||||
end
|
||||
|
||||
def code_postal=(value)
|
||||
super(value&.gsub(/[[:space:]]/, ''))
|
||||
end
|
||||
|
||||
alias postal_code code_postal
|
||||
|
||||
def name
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
# type_de_champ_id :integer
|
||||
#
|
||||
class Champs::DropDownListChamp < Champ
|
||||
store_accessor :value_json, :other
|
||||
THRESHOLD_NB_OPTIONS_AS_RADIO = 5
|
||||
OTHER = '__other__'
|
||||
delegate :options_without_empty_value_when_mandatory, to: :type_de_champ
|
||||
|
@ -44,7 +45,7 @@ class Champs::DropDownListChamp < Champ
|
|||
end
|
||||
|
||||
def selected
|
||||
other_value_present? ? OTHER : value
|
||||
other? ? OTHER : value
|
||||
end
|
||||
|
||||
def disabled_options
|
||||
|
@ -55,22 +56,28 @@ class Champs::DropDownListChamp < Champ
|
|||
drop_down_list_enabled_non_empty_options
|
||||
end
|
||||
|
||||
def other_value_present?
|
||||
drop_down_other? && value.present? && drop_down_list_options.exclude?(value)
|
||||
def other?
|
||||
drop_down_other? && (other || (value.present? && drop_down_list_options.exclude?(value)))
|
||||
end
|
||||
|
||||
def value=(value)
|
||||
if value != OTHER
|
||||
if value == OTHER
|
||||
self.other = true
|
||||
write_attribute(:value, nil)
|
||||
else
|
||||
self.other = false
|
||||
write_attribute(:value, value)
|
||||
end
|
||||
end
|
||||
|
||||
def value_other=(value)
|
||||
write_attribute(:value, value)
|
||||
if other?
|
||||
write_attribute(:value, value)
|
||||
end
|
||||
end
|
||||
|
||||
def value_other
|
||||
other_value_present? ? value : ""
|
||||
other? ? value : ""
|
||||
end
|
||||
|
||||
def in?(options)
|
||||
|
|
|
@ -21,6 +21,16 @@
|
|||
# type_de_champ_id :integer
|
||||
#
|
||||
class Champs::HeaderSectionChamp < Champ
|
||||
def level
|
||||
if parent.present?
|
||||
header_section_level_value.to_i + parent.current_section_level
|
||||
elsif header_section_level_value
|
||||
header_section_level_value.to_i
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def search_terms
|
||||
# The user cannot enter any information here so it doesn’t make much sense to search
|
||||
end
|
||||
|
|
|
@ -118,7 +118,7 @@ class Champs::MultipleDropDownListChamp < Champ
|
|||
private
|
||||
|
||||
def values_are_in_options
|
||||
json = selected_options.reject(&:blank?)
|
||||
json = selected_options.compact_blank
|
||||
return if json.empty?
|
||||
return if (json - enabled_non_empty_options).empty?
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
class Commentaire < ApplicationRecord
|
||||
include Discard::Model
|
||||
|
||||
self.ignored_columns = [:user_id]
|
||||
belongs_to :dossier, inverse_of: :commentaires, touch: true, optional: false
|
||||
|
||||
belongs_to :instructeur, inverse_of: :commentaires, optional: true
|
||||
|
|
|
@ -2,7 +2,7 @@ module BlobVirusScannerConcern
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.ignored_columns = [:lock_version]
|
||||
self.ignored_columns += [:lock_version]
|
||||
before_create :set_pending
|
||||
end
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ module ProcedureStatsConcern
|
|||
end
|
||||
|
||||
def last_considered_processed_at
|
||||
(Time.zone.now - 1.month).end_of_month
|
||||
(1.month.ago).end_of_month
|
||||
end
|
||||
|
||||
def convert_seconds_in_days(seconds)
|
||||
|
|
37
app/models/concerns/treeable_concern.rb
Normal file
37
app/models/concerns/treeable_concern.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
module TreeableConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
MAX_DEPTH = 6 # deepest level for header_sections is 3.
|
||||
# but a repetition can be nested an header_section, so 3+3=6=MAX_DEPTH
|
||||
|
||||
included do
|
||||
# as we progress in the list of ordered champs
|
||||
# we keep a reference to each level of nesting (walk)
|
||||
# when we encounter an header_section, it depends of its own depth of nesting minus 1, ie:
|
||||
# h1 belongs to prior (rooted_tree)
|
||||
# h2 belongs to prior h1
|
||||
# h3 belongs to prior h2
|
||||
# h1 belongs to prior (rooted_tree)
|
||||
# then, each and every champs which are not an header_section
|
||||
# are added to the current_tree
|
||||
# given a root_depth at 0, we build a full tree
|
||||
# given a root_depth > 0, we build a partial tree (aka, a repetition)
|
||||
def to_tree(champs:)
|
||||
rooted_tree = []
|
||||
walk = Array.new(MAX_DEPTH)
|
||||
walk[0] = rooted_tree
|
||||
current_tree = rooted_tree
|
||||
|
||||
champs.each do |champ|
|
||||
if champ.header_section?
|
||||
new_tree = [champ]
|
||||
walk[champ.header_section_level_value - 1].push(new_tree)
|
||||
current_tree = walk[champ.header_section_level_value] = new_tree
|
||||
else
|
||||
current_tree.push(champ)
|
||||
end
|
||||
end
|
||||
rooted_tree
|
||||
end
|
||||
end
|
||||
end
|
|
@ -46,7 +46,6 @@
|
|||
# user_id :integer
|
||||
#
|
||||
class Dossier < ApplicationRecord
|
||||
self.ignored_columns = [:en_construction_conservation_extension]
|
||||
include DossierFilteringConcern
|
||||
include DossierPrefillableConcern
|
||||
include DossierRebaseConcern
|
||||
|
@ -281,13 +280,13 @@ class Dossier < ApplicationRecord
|
|||
scope :processed_in_month, -> (date) do
|
||||
date = date.to_datetime
|
||||
state_termine
|
||||
.where(processed_at: date.beginning_of_month..date.end_of_month)
|
||||
.where(processed_at: date.all_month)
|
||||
end
|
||||
scope :ordered_for_export, -> {
|
||||
order(depose_at: 'asc')
|
||||
}
|
||||
scope :en_cours, -> { not_archived.state_en_construction_ou_instruction }
|
||||
scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
|
||||
scope :without_followers, -> { where.missing(:follows) }
|
||||
scope :with_followers, -> { left_outer_joins(:follows).where.not(follows: { id: nil }) }
|
||||
scope :with_champs, -> {
|
||||
includes(champs_public: [
|
||||
|
|
|
@ -144,7 +144,7 @@ class Etablissement < ApplicationRecord
|
|||
"#{numero_voie} #{type_voie} #{nom_voie}",
|
||||
complement_adresse,
|
||||
"#{code_postal} #{localite}"
|
||||
].reject(&:blank?).join(', ').squeeze(' ')
|
||||
].compact_blank.join(', ').squeeze(' ')
|
||||
end
|
||||
|
||||
def association?
|
||||
|
|
|
@ -21,6 +21,8 @@ class GroupeInstructeur < ApplicationRecord
|
|||
has_and_belongs_to_many :exports, dependent: :destroy
|
||||
has_and_belongs_to_many :bulk_messages, dependent: :destroy
|
||||
|
||||
has_one :defaut_procedure, -> { with_discarded }, class_name: 'Procedure', foreign_key: :defaut_groupe_instructeur_id, dependent: :nullify, inverse_of: :defaut_groupe_instructeur
|
||||
|
||||
validates :label, presence: true, allow_nil: false
|
||||
validates :label, uniqueness: { scope: :procedure }
|
||||
validates :closed, acceptance: { accept: [false] }, if: -> { closed_changed? && self.procedure.groupe_instructeurs.active.one? }
|
||||
|
|
|
@ -208,7 +208,7 @@ class Instructeur < ApplicationRecord
|
|||
h = {
|
||||
nb_en_construction: groupe.dossiers.visible_by_administration.en_construction.count,
|
||||
nb_en_instruction: groupe.dossiers.visible_by_administration.en_instruction.count,
|
||||
nb_accepted: Traitement.where(dossier: groupe.dossiers.accepte, processed_at: Time.zone.yesterday.beginning_of_day..Time.zone.yesterday.end_of_day).count,
|
||||
nb_accepted: Traitement.where(dossier: groupe.dossiers.accepte, processed_at: Time.zone.yesterday.all_day).count,
|
||||
nb_notification: nb_notification
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ class Procedure < ApplicationRecord
|
|||
|
||||
include Discard::Model
|
||||
self.discard_column = :hidden_at
|
||||
self.ignored_columns = [:direction, :durees_conservation_required, :cerfa_flag, :test_started_at, :lien_demarche]
|
||||
self.ignored_columns += [:direction, :durees_conservation_required, :cerfa_flag, :test_started_at, :lien_demarche]
|
||||
|
||||
default_scope -> { kept }
|
||||
|
||||
|
@ -208,7 +208,7 @@ class Procedure < ApplicationRecord
|
|||
has_one :refused_mail, class_name: "Mails::RefusedMail", dependent: :destroy
|
||||
has_one :without_continuation_mail, class_name: "Mails::WithoutContinuationMail", dependent: :destroy
|
||||
|
||||
has_one :defaut_groupe_instructeur, -> { active.order(id: :asc) }, class_name: 'GroupeInstructeur', inverse_of: false
|
||||
belongs_to :defaut_groupe_instructeur, class_name: 'GroupeInstructeur', inverse_of: false, optional: true
|
||||
|
||||
has_one_attached :logo
|
||||
has_one_attached :notice
|
||||
|
@ -453,6 +453,10 @@ class Procedure < ApplicationRecord
|
|||
publiee? || brouillon?
|
||||
end
|
||||
|
||||
def replaced_by_procedure?
|
||||
replaced_by_procedure_id.present?
|
||||
end
|
||||
|
||||
def dossier_can_transition_to_en_construction?
|
||||
accepts_new_dossiers? || depubliee?
|
||||
end
|
||||
|
@ -479,6 +483,23 @@ class Procedure < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def process_stalled_dossiers!
|
||||
case declarative_with_state
|
||||
when Procedure.declarative_with_states.fetch(:en_instruction)
|
||||
dossiers
|
||||
.state_en_construction
|
||||
.where(declarative_triggered_at: nil)
|
||||
.find_each(&:passer_automatiquement_en_instruction!)
|
||||
when Procedure.declarative_with_states.fetch(:accepte)
|
||||
dossiers
|
||||
.state_en_construction
|
||||
.where(declarative_triggered_at: nil)
|
||||
.find_each do |dossier|
|
||||
dossier.accepter_automatiquement! if dossier.can_accepter_automatiquement?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def feature_enabled?(feature)
|
||||
Flipper.enabled?(feature, self)
|
||||
end
|
||||
|
@ -559,6 +580,9 @@ class Procedure < ApplicationRecord
|
|||
procedure.draft_revision.types_de_champ_public.each { |tdc| tdc.options&.delete(:old_pj) }
|
||||
end
|
||||
|
||||
new_defaut_groupe = procedure.groupe_instructeurs.find_by(label: defaut_groupe_instructeur.label)
|
||||
procedure.update!(defaut_groupe_instructeur: new_defaut_groupe)
|
||||
|
||||
procedure
|
||||
end
|
||||
|
||||
|
@ -741,15 +765,6 @@ class Procedure < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def restore_procedure(author)
|
||||
if discarded?
|
||||
undiscard
|
||||
self.dossiers.hidden_by_administration.each do |dossier|
|
||||
dossier.restore(author)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def flipper_id
|
||||
"Procedure;#{id}"
|
||||
end
|
||||
|
@ -926,4 +941,13 @@ class Procedure < ApplicationRecord
|
|||
def stable_ids_used_by_routing_rules
|
||||
@stable_ids_used_by_routing_rules ||= groupe_instructeurs.flat_map { _1.routing_rule&.sources }.compact
|
||||
end
|
||||
|
||||
# We need this to unfuck administrate + aasm
|
||||
def self.human_attribute_name(attribute, options = {})
|
||||
if attribute == :aasm_state
|
||||
'Statut'
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,6 +31,7 @@ class ProcedureRevision < ApplicationRecord
|
|||
scope :ordered, -> { order(:created_at) }
|
||||
|
||||
validate :conditions_are_valid?
|
||||
validate :header_sections_are_valid?
|
||||
|
||||
delegate :path, to: :procedure, prefix: true
|
||||
|
||||
|
@ -401,4 +402,24 @@ class ProcedureRevision < ApplicationRecord
|
|||
.filter { |_tdc, errors| errors.present? }
|
||||
.each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) }
|
||||
end
|
||||
|
||||
def header_sections_are_valid?
|
||||
public_tdcs = types_de_champ_public.to_a
|
||||
|
||||
root_tdcs_errors = errors_for_header_sections_order(public_tdcs)
|
||||
repetition_tdcs_errors = public_tdcs
|
||||
.filter_map { _1.repetition? ? children_of(_1) : nil }
|
||||
.map { errors_for_header_sections_order(_1) }
|
||||
|
||||
repetition_tdcs_errors + root_tdcs_errors
|
||||
end
|
||||
|
||||
def errors_for_header_sections_order(tdcs)
|
||||
tdcs
|
||||
.map.with_index
|
||||
.filter_map { |tdc, i| tdc.header_section? ? [tdc, i] : nil }
|
||||
.map { |tdc, i| [tdc, tdc.check_coherent_header_level(tdcs.take(i))] }
|
||||
.filter { |_tdc, errors| errors.present? }
|
||||
.each { |tdc, message| errors.add(:header_section, message, type_de_champ: tdc) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,7 +22,8 @@ class Traitement < ApplicationRecord
|
|||
includes(:dossier)
|
||||
.termine
|
||||
.where(dossier: procedure.dossiers.visible_by_administration)
|
||||
.where.not('dossiers.depose_at' => nil, processed_at: nil)
|
||||
.where.not('dossiers.depose_at' => nil)
|
||||
.where.not(processed_at: nil)
|
||||
.order(:processed_at)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
# stable_id :bigint
|
||||
#
|
||||
class TypeDeChamp < ApplicationRecord
|
||||
self.ignored_columns = [:migrated_parent, :revision_id, :parent_id, :order_place]
|
||||
self.ignored_columns += [:migrated_parent, :revision_id, :parent_id, :order_place]
|
||||
|
||||
FILE_MAX_SIZE = 200.megabytes
|
||||
FEATURE_FLAGS = {}
|
||||
|
@ -119,7 +119,8 @@ class TypeDeChamp < ApplicationRecord
|
|||
:drop_down_secondary_description,
|
||||
:drop_down_other,
|
||||
:collapsible_explanation_enabled,
|
||||
:collapsible_explanation_text
|
||||
:collapsible_explanation_text,
|
||||
:header_section_level
|
||||
|
||||
has_many :revision_types_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', dependent: :destroy, inverse_of: :type_de_champ
|
||||
has_one :revision_type_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', inverse_of: false
|
||||
|
@ -411,6 +412,39 @@ class TypeDeChamp < ApplicationRecord
|
|||
self.drop_down_options = parse_drop_down_list_value(value)
|
||||
end
|
||||
|
||||
def header_section_level_value
|
||||
if header_section_level.presence
|
||||
header_section_level.to_i
|
||||
else
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
def previous_section_level(upper_tdcs)
|
||||
previous_header_section = upper_tdcs.reverse.find(&:header_section?)
|
||||
|
||||
return 0 if !previous_header_section
|
||||
previous_header_section.header_section_level_value.to_i
|
||||
end
|
||||
|
||||
def check_coherent_header_level(upper_tdcs)
|
||||
errs = []
|
||||
previous_level = previous_section_level(upper_tdcs)
|
||||
|
||||
current_level = header_section_level_value.to_i
|
||||
difference = current_level - previous_level
|
||||
if current_level > previous_level && difference != 1
|
||||
errs << I18n.t('activerecord.errors.type_de_champ.attributes.header_section_level.gap_error', level: current_level - previous_level - 1)
|
||||
end
|
||||
errs
|
||||
end
|
||||
|
||||
def current_section_level
|
||||
tdcs = private? ? revision.type_champs_private.to_a : revision.types_de_champ_public.to_a
|
||||
|
||||
previous_section_level(tdcs.take(tdcs.find_index(self)))
|
||||
end
|
||||
|
||||
def self.options_for_select?(type_champs)
|
||||
[
|
||||
TypeDeChamp.type_champs.fetch(:departements),
|
||||
|
@ -431,7 +465,7 @@ class TypeDeChamp < ApplicationRecord
|
|||
# then rails decided to add this blank ("") option when the select is required
|
||||
# so we revert this change
|
||||
def options_without_empty_value_when_mandatory(options)
|
||||
mandatory? ? options.reject(&:blank?) : options
|
||||
mandatory? ? options.compact_blank : options
|
||||
end
|
||||
|
||||
def drop_down_list_options?
|
||||
|
@ -503,7 +537,9 @@ class TypeDeChamp < ApplicationRecord
|
|||
when type_champs.fetch(:epci),
|
||||
type_champs.fetch(:communes),
|
||||
type_champs.fetch(:multiple_drop_down_list),
|
||||
type_champs.fetch(:dossier_link)
|
||||
type_champs.fetch(:dossier_link),
|
||||
type_champs.fetch(:linked_drop_down_list),
|
||||
type_champs.fetch(:drop_down_list)
|
||||
true
|
||||
else
|
||||
false
|
||||
|
|
|
@ -6,7 +6,7 @@ class TypesDeChamp::PrefillRepetitionTypeDeChamp < TypesDeChamp::PrefillTypeDeCh
|
|||
[
|
||||
I18n.t("views.prefill_descriptions.edit.possible_values.#{type_champ}_html"),
|
||||
subchamps_all_possible_values
|
||||
].join("</br>").html_safe # rubocop:disable Rails/OutputSafety
|
||||
].join("</br>").html_safe
|
||||
end
|
||||
|
||||
def example_value
|
||||
|
@ -18,7 +18,7 @@ class TypesDeChamp::PrefillRepetitionTypeDeChamp < TypesDeChamp::PrefillTypeDeCh
|
|||
|
||||
value.map.with_index do |repetition, index|
|
||||
PrefillRepetitionRow.new(champ, repetition, index, @revision).to_assignable_attributes
|
||||
end.reject(&:blank?)
|
||||
end.compact_blank
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -83,6 +83,6 @@ class TypesDeChamp::PrefillTypeDeChamp < SimpleDelegator
|
|||
end
|
||||
|
||||
def description
|
||||
@description ||= I18n.t("views.prefill_descriptions.edit.possible_values.#{type_champ}_html", default: nil)&.html_safe # rubocop:disable Rails/OutputSafety
|
||||
@description ||= I18n.t("views.prefill_descriptions.edit.possible_values.#{type_champ}_html", default: nil)&.html_safe
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ class ZxcvbnService
|
|||
#
|
||||
# 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")))
|
||||
dictionaries = YAML.safe_load(Rails.root.join("config", "initializers", "zxcvbn_dictionnaries.yaml").read)
|
||||
|
||||
tester = Zxcvbn::Tester.new
|
||||
tester.add_word_lists(dictionaries)
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
= link_to_prev_page resources, t("views.pagination.previous"), param_name: "_page"
|
||||
= link_to_next_page resources, t("views.pagination.next"), param_name: "_page"
|
||||
- else
|
||||
= paginate resources, param_name: "_page"
|
||||
= paginate resources, param_name: "_page", views_prefix: 'shared'
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
- rendered = render @condition_component
|
||||
|
||||
- if rendered.present?
|
||||
= turbo_stream.replace dom_id(@tdc.stable_self, :conditions) do
|
||||
= turbo_stream.replace dom_id(@tdc.stable_self, :condition) do
|
||||
- rendered
|
||||
- else
|
||||
= turbo_stream.remove dom_id(@tdc.stable_self, :conditions)
|
||||
= turbo_stream.remove dom_id(@tdc.stable_self, :condition)
|
||||
|
|
|
@ -11,8 +11,7 @@
|
|||
= f.submit t('.button.rename'), class: 'button primary send'
|
||||
|
||||
.card
|
||||
.card-title
|
||||
= t('.group_management.title')
|
||||
%h2.card-title= t('.group_management.title')
|
||||
|
||||
= form_for :groupe_instructeur, html: { class: 'form' } do |f|
|
||||
= f.label :label do
|
||||
|
@ -46,13 +45,13 @@
|
|||
%tr
|
||||
// i18n-tasks-use t('.existing_groupe')
|
||||
%th{ colspan: 2 }= t(".existing_groupe", count: groupes_instructeurs.total_count)
|
||||
%th
|
||||
%th.actions
|
||||
= link_to "Exporter au format CSV", export_groupe_instructeurs_admin_procedure_groupe_instructeurs_path(procedure, format: :csv)
|
||||
%tbody
|
||||
- groupes_instructeurs.each do |group|
|
||||
%tr
|
||||
%td= group.label
|
||||
%td.actions= link_to t('.set_up'), admin_procedure_groupe_instructeur_path(procedure, group)
|
||||
%td.setup= link_to t('.set_up'), admin_procedure_groupe_instructeur_path(procedure, group)
|
||||
- if group.can_delete?
|
||||
%td.actions
|
||||
= link_to admin_procedure_groupe_instructeur_path(procedure, group), { method: :delete, class: 'button', data: { confirm: t('.group_management.delete_confirmation', group_name: group.label) }} do
|
||||
|
@ -65,4 +64,4 @@
|
|||
= t('.group_management.move_files', count: group.dossiers.visible_by_administration.size)
|
||||
|
||||
|
||||
= paginate groupes_instructeurs
|
||||
= paginate groupes_instructeurs, views_prefix: 'shared'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue