Merge branch 'main' of github.com:betagouv/demarches-simplifiees.fr into user-dashboard/add-alert-WIP-last-dossier
This commit is contained in:
commit
26ff82dbe4
456 changed files with 7265 additions and 1491 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
|
||||
|
||||
|
|
40
Gemfile.lock
40
Gemfile.lock
|
@ -458,7 +458,7 @@ GEM
|
|||
net-protocol
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.14.2)
|
||||
nokogiri (1.14.3)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
open4 (1.3.4)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
.types-de-champ-editor {
|
||||
> .types-de-champ-block {
|
||||
padding-bottom: 50px;
|
||||
|
||||
.types-de-champ-errors {
|
||||
background-color: $background-red;
|
||||
padding: $default-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.type-de-champ {
|
||||
|
|
|
@ -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 {
|
||||
|
|
72
app/assets/stylesheets/routing_rules_component.scss
Normal file
72
app/assets/stylesheets/routing_rules_component.scss
Normal file
|
@ -0,0 +1,72 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
#routing-rules {
|
||||
|
||||
.routing-rules-table {
|
||||
table-layout: fixed;
|
||||
|
||||
.far-left {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.if {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.target {
|
||||
width: 350px;
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.operator {
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 200px;
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: $default-spacer;
|
||||
|
||||
}
|
||||
|
||||
td {
|
||||
padding: $default-spacer;
|
||||
|
||||
input,
|
||||
select {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input.alert,
|
||||
select.alert {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
%label.notice{ for: code_postal_input_id }= t('.postal_code')
|
||||
= @form.text_field :code_postal, required: @champ.required?, id: code_postal_input_id, class: "width-33-desktop width-100-mobile small-margin"
|
||||
- if @champ.code_postal_with_fallback?
|
||||
- if @champ.code_postal?
|
||||
- if commune_options.empty?
|
||||
.fr-error-text.mb-4= t('.not_found', postal_code: @champ.code_postal_with_fallback)
|
||||
.fr-error-text.mb-4= t('.not_found', postal_code: @champ.code_postal)
|
||||
- elsif commune_options.size <= 3
|
||||
%fieldset.radios
|
||||
- commune_options.each.with_index do |(option, value), index|
|
||||
|
|
|
@ -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:
|
|
@ -4,18 +4,27 @@ class Procedure::RoutingRulesComponent < ApplicationComponent
|
|||
def initialize(revision:, groupe_instructeurs:)
|
||||
@revision = revision
|
||||
@groupe_instructeurs = groupe_instructeurs
|
||||
@procedure_id = revision.procedure_id
|
||||
end
|
||||
|
||||
def rows
|
||||
@groupe_instructeurs.active.map do |gi|
|
||||
[gi.routing_rule&.left, gi.routing_rule&.right, gi]
|
||||
if gi.routing_rule.present?
|
||||
[gi.routing_rule.left, gi.routing_rule.right, gi]
|
||||
else
|
||||
[empty, empty, gi]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def can_route?
|
||||
available_targets_for_select.present?
|
||||
end
|
||||
|
||||
def targeted_champ_tag(targeted_champ, row_index)
|
||||
select_tag(
|
||||
'targeted_champ',
|
||||
options_for_select(targeted_champs_for_select, selected: targeted_champ&.stable_id),
|
||||
options_for_select(targeted_champs_for_select, selected: targeted_champ.to_json),
|
||||
id: input_id_for('targeted_champ', row_index)
|
||||
)
|
||||
end
|
||||
|
@ -23,7 +32,10 @@ class Procedure::RoutingRulesComponent < ApplicationComponent
|
|||
def value_tag(targeted_champ, value, row_index)
|
||||
select_tag(
|
||||
'value',
|
||||
options_for_select(values_for_select(targeted_champ), selected: value),
|
||||
options_for_select(
|
||||
values_for_select(targeted_champ, row_index),
|
||||
selected: value.to_json
|
||||
),
|
||||
id: input_id_for('value', row_index)
|
||||
)
|
||||
end
|
||||
|
@ -48,16 +60,20 @@ class Procedure::RoutingRulesComponent < ApplicationComponent
|
|||
def available_targets_for_select
|
||||
@revision.types_de_champ_public
|
||||
.filter { |tdc| [:drop_down_list].include?(tdc.type_champ.to_sym) }
|
||||
.map { |tdc| [tdc.libelle, tdc.stable_id] }
|
||||
.map { |tdc| [tdc.libelle, champ_value(tdc.stable_id).to_json] }
|
||||
end
|
||||
|
||||
def available_values_for_select(targeted_champ)
|
||||
return [] if targeted_champ.nil?
|
||||
targeted_champ.options(@revision.types_de_champ_public)
|
||||
return [] if targeted_champ.is_a?(Logic::Empty)
|
||||
targeted_champ
|
||||
.options(@revision.types_de_champ_public)
|
||||
.map { |tdc| [tdc.first, constant(tdc.first).to_json] }
|
||||
end
|
||||
|
||||
def values_for_select(targeted_champ)
|
||||
empty_target_for_select + available_values_for_select(targeted_champ)
|
||||
def values_for_select(targeted_champ, row_index)
|
||||
(empty_target_for_select + available_values_for_select(targeted_champ))
|
||||
# add id to help morph render selected option
|
||||
.map { |(libelle, json)| [libelle, json, { id: "#{row_index}-option-#{libelle}" }] }
|
||||
end
|
||||
|
||||
def input_id_for(name, row_index)
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
---
|
||||
fr:
|
||||
select: Sélectionner
|
||||
apply_routing_rules: Appliquer des règles de routage
|
||||
routing_rules_notice: |
|
||||
Ajoutez des règles de routage à partir de champs créés dans le formulaire.
|
||||
Si les mêmes règles de routage sont appliquées à plusieurs groupes,
|
||||
les dossiers seront routés vers le premier groupe affiché dans la liste.
|
||||
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>
|
||||
routing_rules_warning_html: |
|
||||
<p>Pour appliquer des règles de routage, votre formulaire doit comporter
|
||||
au moins un champ « choix simple ».</p>
|
||||
<p>Ajoutez ce champ dans la page <a href="%{path}">« Configuration des champs »</a>.</p>
|
||||
|
|
|
@ -1,26 +1,40 @@
|
|||
.card#routing-rules
|
||||
.card-title
|
||||
= t('.apply_routing_rules')
|
||||
%p.notice
|
||||
= t('.routing_rules_notice')
|
||||
.conditionnel.mt-2.width-100
|
||||
%table.condition-table.mt-2.width-100
|
||||
%thead
|
||||
%tr
|
||||
%th.far-left
|
||||
%th.target Champ cible du routage
|
||||
%th.operator Opérateur
|
||||
%th.value Valeur
|
||||
%th.delete-column
|
||||
.conditionnel.mt-2.width-100
|
||||
- rows.each.with_index do |(targeted_champ, value, groupe_instructeur), row_index|
|
||||
= form_tag admin_procedure_routing_rules_path, method: :post, class: "form width-100 gi-#{groupe_instructeur.id}" do
|
||||
%table.condition-table.mt-2.width-100
|
||||
%tbody
|
||||
%tr{ data: { controller: 'autosave' } }
|
||||
%td.far-left Router vers « #{groupe_instructeur.label} » si
|
||||
%td.target= targeted_champ_tag(targeted_champ, row_index)
|
||||
%td.operator Est égal à
|
||||
%td.value= value_tag(targeted_champ, value, row_index)
|
||||
%td.delete-column
|
||||
= hidden_groupe_instructeur_tag(groupe_instructeur.id)
|
||||
%h2.card-title= t('.apply_routing_rules')
|
||||
- if can_route?
|
||||
.notice
|
||||
= t('.routing_rules_notice_html', path: champs_admin_procedure_path(@procedure_id))
|
||||
.mt-2.width-100
|
||||
%table.routing-rules-table.mt-2.width-100
|
||||
%thead
|
||||
%tr
|
||||
%th.far-left Router vers
|
||||
%th.if
|
||||
%th.target Champ cible du routage
|
||||
%th.operator
|
||||
%th.value Valeur
|
||||
.mt-2.width-100
|
||||
- rows.each.with_index do |(targeted_champ, value, groupe_instructeur), row_index|
|
||||
= form_tag admin_procedure_routing_rules_path(@procedure_id),
|
||||
method: :post,
|
||||
class: "form width-100 gi-#{groupe_instructeur.id}",
|
||||
data: { controller: 'autosave' } do
|
||||
= hidden_groupe_instructeur_tag(groupe_instructeur.id)
|
||||
%table.routing-rules-table.condition-table.mt-2.width-100
|
||||
%tbody
|
||||
%tr
|
||||
%td.far-left= groupe_instructeur.label
|
||||
%td.if si
|
||||
%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)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
class TypesDeChampEditor::ChampComponent < ApplicationComponent
|
||||
attr_reader :coordinate, :upper_coordinates
|
||||
|
||||
def initialize(coordinate:, upper_coordinates:, focused: false)
|
||||
def initialize(coordinate:, upper_coordinates:, focused: false, errors: '')
|
||||
@coordinate = coordinate
|
||||
@focused = focused
|
||||
@upper_coordinates = upper_coordinates
|
||||
@errors = errors
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
%li.type-de-champ.flex.column.justify-start{ html_options }
|
||||
.flex.justify-start.section.head.hr
|
||||
.flex.justify-between.section.head.hr
|
||||
.handle.small.icon-only.icon.move-handle{ title: "Déplacer le champ vers le haut ou vers le bas" }
|
||||
.flex.justify-start.delete
|
||||
= button_to type_de_champ_path, class: 'button small icon-only danger', method: :delete, form: { data: { turbo_confirm: 'Êtes vous sûr de vouloir supprimer ce champ ?' } } do
|
||||
.icon.delete
|
||||
%span.sr-only Supprimer
|
||||
|
||||
- if coordinate.used_by_routing_rules?
|
||||
.flex.align-center
|
||||
%span
|
||||
utilisé pour
|
||||
= link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules'))
|
||||
- else
|
||||
.flex.justify-start.delete
|
||||
= button_to type_de_champ_path, class: 'button small icon-only danger', method: :delete, form: { data: { turbo_confirm: 'Êtes vous sûr de vouloir supprimer ce champ ?' } } do
|
||||
.icon.delete
|
||||
%span.sr-only Supprimer
|
||||
|
||||
- if @errors.present?
|
||||
.types-de-champ-errors
|
||||
= @errors
|
||||
|
||||
.flex.justify-start.section.ml-1
|
||||
= form_for(type_de_champ, form_options) do |form|
|
||||
|
@ -19,7 +30,7 @@
|
|||
%span.sr-only Déplacer le champ vers le bas
|
||||
.cell.flex.justify-start.column.flex-grow
|
||||
= form.label :type_champ, "Type de champ", for: dom_id(type_de_champ, :type_champ)
|
||||
= form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ)
|
||||
= form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules?
|
||||
.flex.column.justify-start.flex-grow
|
||||
.cell
|
||||
.flex.align-center
|
||||
|
@ -37,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
|
||||
|
|
|
@ -5,34 +5,42 @@ module Administrateurs
|
|||
before_action :retrieve_procedure
|
||||
|
||||
def update
|
||||
left = champ_value(targeted_champ)
|
||||
right = parsed_value
|
||||
left = targeted_champ
|
||||
|
||||
@procedure.groupe_instructeurs.find(groupe_instructeur_id).update!(routing_rule: ds_eq(left, right))
|
||||
right = targeted_champ_changed? ? empty : value
|
||||
|
||||
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?
|
||||
targeted_champ != groupe_instructeur.routing_rule&.left
|
||||
end
|
||||
|
||||
def targeted_champ
|
||||
routing_params[:targeted_champ].to_i
|
||||
Logic.from_json(params[:targeted_champ])
|
||||
end
|
||||
|
||||
def value
|
||||
routing_params[:value]
|
||||
Logic.from_json(params[:value])
|
||||
end
|
||||
|
||||
def parsed_value
|
||||
term = Logic.from_json(value) rescue nil
|
||||
|
||||
term.presence || constant(value)
|
||||
def groupe_instructeur
|
||||
@groupe_instructeur ||= @procedure.groupe_instructeurs.find(groupe_instructeur_id)
|
||||
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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Administrateurs
|
||||
class TypesDeChampController < AdministrateurController
|
||||
before_action :retrieve_procedure
|
||||
after_action :reset_procedure, only: [:create, :update, :destroy]
|
||||
after_action :reset_procedure, only: [:create, :update, :destroy, :piece_justificative_template]
|
||||
|
||||
def create
|
||||
type_de_champ = draft.add_type_de_champ(type_de_champ_create_params)
|
||||
|
@ -20,7 +20,12 @@ module Administrateurs
|
|||
def update
|
||||
type_de_champ = draft.find_and_ensure_exclusive_use(params[:stable_id])
|
||||
|
||||
if type_de_champ.update(type_de_champ_update_params)
|
||||
if type_de_champ.revision_type_de_champ.used_by_routing_rules? && changing_of_type?(type_de_champ)
|
||||
coordinate = draft.coordinate_for(type_de_champ)
|
||||
errors = "« #{type_de_champ.libelle} » est utilisé pour le routage, vous ne pouvez pas modifier son type."
|
||||
@morphed = [champ_component_from(coordinate, focused: false, errors:)]
|
||||
flash.alert = errors
|
||||
elsif type_de_champ.update(type_de_champ_update_params)
|
||||
@coordinate = draft.coordinate_for(type_de_champ)
|
||||
@morphed = champ_components_starting_at(@coordinate)
|
||||
|
||||
|
@ -67,17 +72,29 @@ module Administrateurs
|
|||
end
|
||||
|
||||
def destroy
|
||||
@coordinate = draft.remove_type_de_champ(params[:stable_id])
|
||||
flash.notice = "Formulaire enregistré"
|
||||
coordinate, type_de_champ = draft.coordinate_and_tdc(params[:stable_id])
|
||||
|
||||
if @coordinate.present?
|
||||
@destroyed = @coordinate
|
||||
@morphed = champ_components_starting_at(@coordinate)
|
||||
if coordinate.used_by_routing_rules?
|
||||
errors = "« #{type_de_champ.libelle} » est utilisé pour le routage, vous ne pouvez pas le supprimer."
|
||||
@morphed = [champ_component_from(coordinate, focused: false, errors:)]
|
||||
flash.alert = errors
|
||||
else
|
||||
@coordinate = draft.remove_type_de_champ(params[:stable_id])
|
||||
flash.notice = "Formulaire enregistré"
|
||||
|
||||
if @coordinate.present?
|
||||
@destroyed = @coordinate
|
||||
@morphed = champ_components_starting_at(@coordinate)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def changing_of_type?(type_de_champ)
|
||||
type_de_champ_update_params['type_champ'].present? && (type_de_champ_update_params['type_champ'] != type_de_champ.type_champ)
|
||||
end
|
||||
|
||||
def champ_components_starting_at(coordinate, offset = 0)
|
||||
coordinate
|
||||
.siblings_starting_at(offset)
|
||||
|
@ -85,11 +102,12 @@ module Administrateurs
|
|||
.map { |c| champ_component_from(c) }
|
||||
end
|
||||
|
||||
def champ_component_from(coordinate, focused: false)
|
||||
def champ_component_from(coordinate, focused: false, errors: '')
|
||||
TypesDeChampEditor::ChampComponent.new(
|
||||
coordinate: coordinate,
|
||||
coordinate:,
|
||||
upper_coordinates: coordinate.upper_siblings,
|
||||
focused: focused
|
||||
focused: focused,
|
||||
errors:
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -101,27 +119,28 @@ module Administrateurs
|
|||
|
||||
def type_de_champ_update_params
|
||||
params.required(:type_de_champ).permit(:type_champ,
|
||||
:libelle,
|
||||
:description,
|
||||
:mandatory,
|
||||
:drop_down_list_value,
|
||||
:drop_down_other,
|
||||
:drop_down_secondary_libelle,
|
||||
:drop_down_secondary_description,
|
||||
:collapsible_explanation_enabled,
|
||||
:collapsible_explanation_text,
|
||||
editable_options: [
|
||||
:cadastres,
|
||||
:unesco,
|
||||
:arretes_protection,
|
||||
:conservatoire_littoral,
|
||||
:reserves_chasse_faune_sauvage,
|
||||
:reserves_biologiques,
|
||||
:reserves_naturelles,
|
||||
:natura_2000,
|
||||
:zones_humides,
|
||||
:znieff
|
||||
])
|
||||
:libelle,
|
||||
:description,
|
||||
:mandatory,
|
||||
:drop_down_list_value,
|
||||
:drop_down_other,
|
||||
:drop_down_secondary_libelle,
|
||||
:drop_down_secondary_description,
|
||||
:collapsible_explanation_enabled,
|
||||
:collapsible_explanation_text,
|
||||
:header_section_level,
|
||||
editable_options: [
|
||||
:cadastres,
|
||||
:unesco,
|
||||
:arretes_protection,
|
||||
:conservatoire_littoral,
|
||||
:reserves_chasse_faune_sauvage,
|
||||
:reserves_biologiques,
|
||||
:reserves_naturelles,
|
||||
:natura_2000,
|
||||
:zones_humides,
|
||||
:znieff
|
||||
])
|
||||
end
|
||||
|
||||
def draft
|
||||
|
|
|
@ -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
|
41
app/jobs/migrations/normalize_communes_job.rb
Normal file
41
app/jobs/migrations/normalize_communes_job.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
class Migrations::NormalizeCommunesJob < ApplicationJob
|
||||
def perform(ids)
|
||||
Champs::CommuneChamp.where(id: ids).find_each do |champ|
|
||||
next if champ.external_id.blank?
|
||||
|
||||
value_json = champ.value_json || {}
|
||||
|
||||
if !champ.departement?
|
||||
metro_code = champ.external_id[0..1]
|
||||
drom_com_code = champ.external_id[0..2]
|
||||
|
||||
if metro_code == '97' || metro_code == '98'
|
||||
value_json[:code_departement] = drom_com_code
|
||||
else
|
||||
value_json[:code_departement] = metro_code
|
||||
end
|
||||
end
|
||||
|
||||
if !champ.code_postal? && code_postal_with_fallback(champ).present?
|
||||
value_json[:code_postal] = code_postal_with_fallback(champ)
|
||||
end
|
||||
|
||||
if value_json.present?
|
||||
champ.update_column(:value_json, value_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# We try to extract the postal code from the value, which is the name of the commune and the
|
||||
# postal code in brackets.
|
||||
def code_postal_with_fallback(champ)
|
||||
if champ.value.present?
|
||||
match = champ.value.match(/[^(]\(([^\)]*)\)$/)
|
||||
match[1] if match.present?
|
||||
else
|
||||
nil
|
||||
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
|
||||
|
@ -192,6 +240,7 @@ class Dolist::API
|
|||
post(url, body)["ItemList"]
|
||||
end
|
||||
|
||||
# see: https://api.dolist.com/documentation/index.html#/7edc2948ba01f-creer-un-envoi-transactionnel
|
||||
def prepare_mail_body(mail)
|
||||
{
|
||||
"Type": "TransactionalService",
|
||||
|
@ -207,15 +256,15 @@ class Dolist::API
|
|||
"Name": mail['X-Dolist-Message-Name'].value,
|
||||
"Subject": mail.subject,
|
||||
"SenderID": sender_id,
|
||||
"ForceHttp": true,
|
||||
"ForceHttp": false, # ForceHttp : force le tracking http non sécurisé (True/False).
|
||||
"Format": "html",
|
||||
"DisableOpenTracking": true,
|
||||
"IsTrackingValidated": true
|
||||
"DisableOpenTracking": true, # DisableOpenTracking : désactivation du tracking d'ouverture (True/False).
|
||||
"IsTrackingValidated": true # IsTrackingValidated : est-ce que le tracking de ce message est validé ? (True/False). Passez la valeur True pour un envoi transactionnel.
|
||||
},
|
||||
"MessageContent": {
|
||||
"SourceCode": mail_source_code(mail),
|
||||
"EncodingType": "UTF8",
|
||||
"EnableTrackingDetection": false
|
||||
"EnableTrackingDetection": false # EnableTrackingDetection : booléen pour l’activation du tracking personnalisé des liens des messages utilisés lors des précédents envois (True/False).
|
||||
}
|
||||
}
|
||||
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
|
||||
|
|
|
@ -83,6 +83,8 @@ class Administrateur < ApplicationRecord
|
|||
end
|
||||
|
||||
procedures.with_discarded.each do |procedure|
|
||||
next if procedure.service.nil?
|
||||
|
||||
next_administrateur = procedure.administrateurs.where.not(id: self.id).first
|
||||
procedure.service.update(administrateur: next_administrateur)
|
||||
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 }
|
||||
|
|
|
@ -32,7 +32,7 @@ class Champ < ApplicationRecord
|
|||
# here because otherwise we can't easily use includes in our queries.
|
||||
has_many :geo_areas, -> { order(:created_at) }, dependent: :destroy, inverse_of: :champ
|
||||
belongs_to :etablissement, optional: true, dependent: :destroy
|
||||
has_many :champs, -> { ordered }, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
|
||||
has_many :champs, -> { ordered }, foreign_key: :parent_id, inverse_of: :parent
|
||||
|
||||
delegate :procedure, to: :dossier
|
||||
|
||||
|
@ -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
|
||||
|
@ -64,7 +68,7 @@ class Champs::CommuneChamp < Champs::TextChamp
|
|||
|
||||
def to_s
|
||||
if code?
|
||||
"#{APIGeoService.commune_name(code_departement, code)} (#{code_postal_with_fallback})"
|
||||
"#{APIGeoService.commune_name(code_departement, code)} (#{code_postal})"
|
||||
else
|
||||
value.present? ? value.to_s : ''
|
||||
end
|
||||
|
@ -79,15 +83,15 @@ class Champs::CommuneChamp < Champs::TextChamp
|
|||
end
|
||||
|
||||
def communes
|
||||
if code_postal_with_fallback?
|
||||
APIGeoService.communes_by_postal_code(code_postal_with_fallback)
|
||||
if code_postal?
|
||||
APIGeoService.communes_by_postal_code(code_postal)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def value=(code)
|
||||
if code.blank? || !code_postal_with_fallback?
|
||||
if code.blank? || !code_postal?
|
||||
self.code_departement = nil
|
||||
self.external_id = nil
|
||||
super(nil)
|
||||
|
@ -105,23 +109,6 @@ class Champs::CommuneChamp < Champs::TextChamp
|
|||
end
|
||||
end
|
||||
|
||||
def code_postal_with_fallback?
|
||||
code_postal_with_fallback.present?
|
||||
end
|
||||
|
||||
# We try to extract the postal code from the value, which is the name of the commune and the
|
||||
# postal code in brackets. This is temporary until we do a full data migration.
|
||||
def code_postal_with_fallback
|
||||
if code_postal?
|
||||
code_postal
|
||||
elsif value.present?
|
||||
match = value.match(/[^(]\(([^\)]*)\)$/)
|
||||
match[1] if match.present?
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def on_code_postal_change
|
||||
|
|
|
@ -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
|
||||
|
@ -86,8 +85,11 @@ class Dossier < ApplicationRecord
|
|||
has_one_attached :justificatif_motivation
|
||||
|
||||
has_many :champs
|
||||
has_many :champs_public, -> { root.public_ordered }, class_name: 'Champ', inverse_of: false, dependent: :destroy
|
||||
has_many :champs_private, -> { root.private_ordered }, class_name: 'Champ', inverse_of: false, dependent: :destroy
|
||||
# We have to remove champs in a particular order - champs with a reference to a parent have to be
|
||||
# removed first, otherwise we get a foreign key constraint error.
|
||||
has_many :champs_to_destroy, -> { order(:parent_id) }, class_name: 'Champ', inverse_of: false, dependent: :destroy
|
||||
has_many :champs_public, -> { root.public_ordered }, class_name: 'Champ', inverse_of: false
|
||||
has_many :champs_private, -> { root.private_ordered }, class_name: 'Champ', inverse_of: false
|
||||
has_many :champs_public_all, -> { public_only }, class_name: 'Champ', inverse_of: false
|
||||
has_many :champs_private_all, -> { private_only }, class_name: 'Champ', inverse_of: false
|
||||
has_many :prefilled_champs_public, -> { root.public_only.prefilled }, class_name: 'Champ', inverse_of: false
|
||||
|
@ -278,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
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# canonical_procedure_id :bigint
|
||||
# defaut_groupe_instructeur_id :bigint
|
||||
# draft_revision_id :bigint
|
||||
# parent_procedure_id :bigint
|
||||
# published_revision_id :bigint
|
||||
|
@ -64,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 }
|
||||
|
||||
|
@ -207,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
|
||||
|
@ -452,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
|
||||
|
@ -478,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
|
||||
|
@ -558,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
|
||||
|
||||
|
@ -740,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
|
||||
|
@ -797,6 +813,7 @@ class Procedure < ApplicationRecord
|
|||
end
|
||||
|
||||
def publish_revision!
|
||||
reset!
|
||||
transaction do
|
||||
self.published_revision = draft_revision
|
||||
self.draft_revision = create_new_revision
|
||||
|
@ -810,8 +827,8 @@ class Procedure < ApplicationRecord
|
|||
|
||||
def reset_draft_revision!
|
||||
if published_revision.present? && draft_changed?
|
||||
reset!
|
||||
transaction do
|
||||
reset!
|
||||
draft_revision.types_de_champ.filter(&:only_present_on_draft?).each(&:destroy)
|
||||
draft_revision.update(dossier_submitted_message: nil)
|
||||
draft_revision.destroy
|
||||
|
@ -916,7 +933,21 @@ class Procedure < ApplicationRecord
|
|||
|
||||
def ensure_defaut_groupe_instructeur
|
||||
if self.groupe_instructeurs.empty?
|
||||
groupe_instructeurs.create(label: GroupeInstructeur::DEFAUT_LABEL)
|
||||
gi = groupe_instructeurs.create(label: GroupeInstructeur::DEFAUT_LABEL)
|
||||
self.update(defaut_groupe_instructeur_id: gi.id)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
|
@ -225,6 +226,16 @@ class ProcedureRevision < ApplicationRecord
|
|||
types_de_champ_public.any?(&:carte?)
|
||||
end
|
||||
|
||||
def coordinate_and_tdc(stable_id)
|
||||
return [nil, nil] if stable_id.blank?
|
||||
|
||||
coordinate = revision_types_de_champ
|
||||
.joins(:type_de_champ)
|
||||
.find_by(type_de_champ: { stable_id: stable_id })
|
||||
|
||||
[coordinate, coordinate&.type_de_champ]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_estimated_fill_duration
|
||||
|
@ -245,16 +256,6 @@ class ProcedureRevision < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def coordinate_and_tdc(stable_id)
|
||||
return [nil, nil] if stable_id.blank?
|
||||
|
||||
coordinate = revision_types_de_champ
|
||||
.joins(:type_de_champ)
|
||||
.find_by(type_de_champ: { stable_id: stable_id })
|
||||
|
||||
[coordinate, coordinate&.type_de_champ]
|
||||
end
|
||||
|
||||
def renumber(siblings)
|
||||
siblings.to_a.compact.each.with_index do |sibling, position|
|
||||
sibling.update_column(:position, position)
|
||||
|
@ -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
|
||||
|
|
|
@ -69,4 +69,8 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
|
|||
revision
|
||||
end
|
||||
end
|
||||
|
||||
def used_by_routing_rules?
|
||||
stable_id.in?(procedure.stable_ids_used_by_routing_rules)
|
||||
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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue