diff --git a/app/assets/stylesheets/new_design/common.scss b/app/assets/stylesheets/new_design/common.scss index 4b6e2f772..8128e3c79 100644 --- a/app/assets/stylesheets/new_design/common.scss +++ b/app/assets/stylesheets/new_design/common.scss @@ -13,6 +13,10 @@ select { line-height: 1.42857143; } +dt { + margin-bottom: 0.5em; +} + .page-wrapper { position: relative; min-height: 100%; diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 78529a9ba..85bb998bf 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -97,12 +97,12 @@ module Instructeurs end def archive - dossier.update(archived: true) + dossier.archiver!(current_instructeur) redirect_back(fallback_location: instructeur_procedures_url) end def unarchive - dossier.update(archived: false) + dossier.desarchiver!(current_instructeur) redirect_back(fallback_location: instructeur_procedures_url) end diff --git a/app/controllers/new_administrateur/types_de_champ_controller.rb b/app/controllers/new_administrateur/types_de_champ_controller.rb index 655950b0a..2f31b0118 100644 --- a/app/controllers/new_administrateur/types_de_champ_controller.rb +++ b/app/controllers/new_administrateur/types_de_champ_controller.rb @@ -15,7 +15,7 @@ module NewAdministrateur end def update - type_de_champ = @procedure.draft_revision.find_or_clone_type_de_champ(TypeDeChamp.to_stable_id(params[:id])) + type_de_champ = @procedure.draft_revision.find_or_clone_type_de_champ(params[:id]) if type_de_champ.update(type_de_champ_update_params) reset_procedure @@ -26,13 +26,13 @@ module NewAdministrateur end def move - @procedure.draft_revision.move_type_de_champ(TypeDeChamp.to_stable_id(params[:id]), (params[:position] || params[:order_place]).to_i) + @procedure.draft_revision.move_type_de_champ(params[:id], (params[:position] || params[:order_place]).to_i) head :no_content end def destroy - @procedure.draft_revision.remove_type_de_champ(TypeDeChamp.to_stable_id(params[:id])) + @procedure.draft_revision.remove_type_de_champ(params[:id]) reset_procedure head :no_content @@ -67,7 +67,7 @@ module NewAdministrateur end def type_de_champ_create_params - type_de_champ_params = params.required(:type_de_champ).permit(:type_champ, + params.required(:type_de_champ).permit(:type_champ, :libelle, :description, :mandatory, @@ -77,12 +77,6 @@ module NewAdministrateur :piece_justificative_template, :cadastres, :mnhn) - - if type_de_champ_params[:parent_id].present? - type_de_champ_params[:parent_id] = TypeDeChamp.to_stable_id(type_de_champ_params[:parent_id]) - end - - type_de_champ_params end def type_de_champ_update_params diff --git a/app/graphql/mutations/dossier_archiver.rb b/app/graphql/mutations/dossier_archiver.rb new file mode 100644 index 000000000..aad9e79e0 --- /dev/null +++ b/app/graphql/mutations/dossier_archiver.rb @@ -0,0 +1,25 @@ +module Mutations + class DossierArchiver < Mutations::BaseMutation + description "Archiver le dossier." + + argument :dossier_id, ID, "Dossier ID", required: true, loads: Types::DossierType + argument :instructeur_id, ID, "Instructeur qui prend la décision sur le dossier.", required: true, loads: Types::ProfileType + + field :dossier, Types::DossierType, null: true + field :errors, [Types::ValidationErrorType], null: true + + def resolve(dossier:, instructeur:) + if dossier.termine? + dossier.archiver!(instructeur) + + { dossier: dossier } + else + { errors: ["Un dossier ne peut être archivé qu’une fois le traitement terminé"] } + end + end + + def authorized?(dossier:, instructeur:) + instructeur.is_a?(Instructeur) && instructeur.dossiers.exists?(id: dossier.id) + end + end +end diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index a81e39392..e8cad479c 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -457,6 +457,38 @@ type DossierAccepterPayload { errors: [ValidationError!] } +""" +Autogenerated input type of DossierArchiver +""" +input DossierArchiverInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Dossier ID + """ + dossierId: ID! + + """ + Instructeur qui prend la décision sur le dossier. + """ + instructeurId: ID! +} + +""" +Autogenerated return type of DossierArchiver +""" +type DossierArchiverPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + dossier: Dossier + errors: [ValidationError!] +} + """ Autogenerated input type of DossierChangerGroupeInstructeur """ @@ -713,8 +745,12 @@ type Effectif { type Entreprise { attestationFiscaleAttachment: File attestationSocialeAttachment: File + + """ + capital social de l’entreprise. -1 si inconnu. + """ capitalSocial: BigInt! - codeEffectifEntreprise: String! + codeEffectifEntreprise: String dateCreation: ISO8601Date! """ @@ -921,6 +957,11 @@ type Mutation { """ dossierAccepter(input: DossierAccepterInput!): DossierAccepterPayload + """ + Archiver le dossier. + """ + dossierArchiver(input: DossierArchiverInput!): DossierArchiverPayload + """ Changer le grope instructeur du dossier. """ @@ -1011,10 +1052,10 @@ type PersonneMorale implements Demandeur { localite: String! naf: String! nomVoie: String! - numeroVoie: String! + numeroVoie: String siegeSocial: Boolean! siret: String! - typeVoie: String! + typeVoie: String } type PersonnePhysique implements Demandeur { diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 1a0e8e32f..1651ed5fd 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -7,6 +7,7 @@ module Types field :dossier_classer_sans_suite, mutation: Mutations::DossierClasserSansSuite field :dossier_refuser, mutation: Mutations::DossierRefuser field :dossier_accepter, mutation: Mutations::DossierAccepter + field :dossier_archiver, mutation: Mutations::DossierArchiver field :dossier_changer_groupe_instructeur, mutation: Mutations::DossierChangerGroupeInstructeur end end diff --git a/app/graphql/types/personne_morale_type.rb b/app/graphql/types/personne_morale_type.rb index 769554ce6..bef7c6d02 100644 --- a/app/graphql/types/personne_morale_type.rb +++ b/app/graphql/types/personne_morale_type.rb @@ -7,14 +7,14 @@ module Types end field :siren, String, null: false - field :capital_social, GraphQL::Types::BigInt, null: false + field :capital_social, GraphQL::Types::BigInt, null: false, description: "capital social de l’entreprise. -1 si inconnu." field :numero_tva_intracommunautaire, String, null: false field :forme_juridique, String, null: false field :forme_juridique_code, String, null: false field :nom_commercial, String, null: false field :raison_sociale, String, null: false field :siret_siege_social, String, null: false - field :code_effectif_entreprise, String, null: false + field :code_effectif_entreprise, String, null: true field :effectif_mensuel, EffectifType, null: true, description: "effectif pour un mois donné" field :effectif_annuel, EffectifType, null: true, description: "effectif moyen d'une année" field :date_creation, GraphQL::Types::ISO8601Date, null: false @@ -41,6 +41,17 @@ module Types end end + def capital_social + # capital_social is defined as a BigInt, so we can't return an empty string when value is unknown + # 0 could appear to be a legitimate value, so a negative value helps to ensure the value is not known + object.capital_social || '-1' + end + + def code_effectif_entreprise + # we need this in order to bypass Hashie::Dash deserialization issue on nil values + object.code_effectif_entreprise + end + def effectif_annuel if object.effectif_annuel.present? { @@ -76,8 +87,8 @@ module Types field :naf, String, null: false field :libelle_naf, String, null: false field :adresse, String, null: false - field :numero_voie, String, null: false - field :type_voie, String, null: false + field :numero_voie, String, null: true + field :type_voie, String, null: true field :nom_voie, String, null: false field :complement_adresse, String, null: false field :code_postal, String, null: false diff --git a/app/helpers/champ_helper.rb b/app/helpers/champ_helper.rb index 2af656d69..a9c25965b 100644 --- a/app/helpers/champ_helper.rb +++ b/app/helpers/champ_helper.rb @@ -45,12 +45,20 @@ module ChampHelper "Culture : #{geo_area.culture} - Surface : #{geo_area.surface} ha" when GeoArea.sources.fetch(:selection_utilisateur) if geo_area.polygon? - capture do - concat "Une aire de surface #{geo_area.area} m" - concat content_tag(:sup, "2") + if geo_area.area.present? + capture do + concat "Une aire de surface #{geo_area.area} m" + concat content_tag(:sup, "2") + end + else + "Une aire de surface inconnue" end elsif geo_area.line? - "Une ligne longue de #{geo_area.length} m" + if geo_area.length.present? + "Une ligne longue de #{geo_area.length} m" + else + "Une ligne de longueur inconnue" + end elsif geo_area.point? "Un point situé à #{geo_area.location}" end diff --git a/app/javascript/components/MapEditor/index.js b/app/javascript/components/MapEditor/index.js index c025f5fb3..d3ab35a1c 100644 --- a/app/javascript/components/MapEditor/index.js +++ b/app/javascript/components/MapEditor/index.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import mapboxgl from 'mapbox-gl'; import ReactMapboxGl, { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl'; import DrawControl from 'react-mapbox-gl-draw'; -import { gpx, kml } from '@tmcw/togeojson/dist/togeojson.es.js'; import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; import { getJSON, ajax, fire } from '@utils'; @@ -11,7 +10,11 @@ import { getJSON, ajax, fire } from '@utils'; import { getMapStyle, SwitchMapStyle } from '../MapStyles'; import SearchInput from './SearchInput'; -import { polygonCadastresFill, polygonCadastresLine } from './utils'; +import { + polygonCadastresFill, + polygonCadastresLine, + readGeoFile +} from './utils'; import { noop, filterFeatureCollection, @@ -149,19 +152,7 @@ function MapEditor({ featureCollection, url, preview, options }) { } const onFileImport = (e, inputId) => { - const isGpxFile = e.target.files[0].name.includes('.gpx'); - let reader = new FileReader(); - reader.readAsText(e.target.files[0], 'UTF-8'); - reader.onload = async (event) => { - let featureCollection; - isGpxFile - ? (featureCollection = gpx( - new DOMParser().parseFromString(event.target.result, 'text/xml') - )) - : (featureCollection = kml( - new DOMParser().parseFromString(event.target.result, 'text/xml') - )); - + readGeoFile(e.target.files[0]).then(async (featureCollection) => { const resultFeatureCollection = await getJSON( `${url}/import`, featureCollection, @@ -196,7 +187,7 @@ function MapEditor({ featureCollection, url, preview, options }) { updateFeaturesList(resultFeatureCollection.features); setImportInputs(setInputs); setBbox(resultFeatureCollection.bbox); - }; + }); }; const addInputFile = (e) => { diff --git a/app/javascript/components/MapEditor/utils.js b/app/javascript/components/MapEditor/utils.js index 30311a836..955c92859 100644 --- a/app/javascript/components/MapEditor/utils.js +++ b/app/javascript/components/MapEditor/utils.js @@ -1,3 +1,5 @@ +import { gpx, kml } from '@tmcw/togeojson/dist/togeojson.es.js'; + export const polygonCadastresFill = { 'fill-color': '#EC3323', 'fill-opacity': 0.3 @@ -8,3 +10,81 @@ export const polygonCadastresLine = { 'line-width': 4, 'line-dasharray': [1, 1] }; + +export function normalizeFeatureCollection(featureCollection) { + const features = []; + for (const feature of featureCollection.features) { + switch (feature.geometry.type) { + case 'MultiPoint': + for (const coordinates of feature.geometry.coordinates) { + features.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates + }, + properties: feature.properties + }); + } + break; + case 'MultiLineString': + for (const coordinates of feature.geometry.coordinates) { + features.push({ + type: 'Feature', + geometry: { + type: 'LineString', + coordinates + }, + properties: feature.properties + }); + } + break; + case 'MultiPolygon': + for (const coordinates of feature.geometry.coordinates) { + features.push({ + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates + }, + properties: feature.properties + }); + } + break; + case 'GeometryCollection': + for (const geometry of feature.geometry.geometries) { + features.push({ + type: 'Feature', + geometry, + properties: feature.properties + }); + } + break; + default: + features.push(feature); + } + } + + featureCollection.features = features; + return featureCollection; +} + +export function readGeoFile(file) { + const isGpxFile = file.name.includes('.gpx'); + const reader = new FileReader(); + + return new Promise((resolve) => { + reader.onload = (event) => { + const xml = new DOMParser().parseFromString( + event.target.result, + 'text/xml' + ); + const featureCollection = normalizeFeatureCollection( + isGpxFile ? gpx(xml) : kml(xml) + ); + + resolve(featureCollection); + }; + reader.readAsText(file, 'UTF-8'); + }); +} diff --git a/app/javascript/new_design/support.js b/app/javascript/new_design/support.js index 1c637ed88..8c590ae00 100644 --- a/app/javascript/new_design/support.js +++ b/app/javascript/new_design/support.js @@ -1,16 +1,121 @@ -import { show, hide, delegate } from '@utils'; +// +// This content is inspired by w3c aria example +// https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-faq.html +// -function updateContactElementsVisibility() { - const contactSelect = document.querySelector('#contact-form #type'); - if (contactSelect) { - const type = contactSelect.value; - const visibleElements = `[data-contact-type-only="${type}"]`; - const hiddenElements = `[data-contact-type-only]:not([data-contact-type-only="${type}"])`; +class ButtonExpand { + constructor(domNode) { + this.domNode = domNode; - document.querySelectorAll(visibleElements).forEach(show); - document.querySelectorAll(hiddenElements).forEach(hide); + this.keyCode = Object.freeze({ + RETURN: 13 + }); + + this.allButtons = []; + this.controlledNode = false; + + var id = this.domNode.getAttribute('aria-controls'); + + if (id) { + this.controlledNode = document.getElementById(id); + } + + this.domNode.setAttribute('aria-expanded', 'false'); + this.hideContent(); + + this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); + this.domNode.addEventListener('click', this.handleClick.bind(this)); + this.domNode.addEventListener('focus', this.handleFocus.bind(this)); + this.domNode.addEventListener('blur', this.handleBlur.bind(this)); + } + + showContent() { + this.domNode.setAttribute('aria-expanded', 'true'); + this.domNode.classList.add('primary'); + if (this.controlledNode) { + this.controlledNode.classList.remove('hidden'); + } + this.formInput.value = this.domNode.getAttribute('data-question-type'); + + this.allButtons.forEach((b) => { + if (b != this) { + b.hideContent(); + } + }); + } + + hideContent() { + this.domNode.setAttribute('aria-expanded', 'false'); + this.domNode.classList.remove('primary'); + if (this.controlledNode) { + this.controlledNode.classList.add('hidden'); + } + } + + toggleExpand() { + if (this.domNode.getAttribute('aria-expanded') === 'true') { + this.hideContent(); + } else { + this.showContent(); + } + } + + setAllButtons(buttons) { + this.allButtons = buttons; + } + + setFormInput(formInput) { + this.formInput = formInput; + } + + handleKeydown() { + switch (event.keyCode) { + case this.keyCode.RETURN: + this.toggleExpand(); + + event.stopPropagation(); + event.preventDefault(); + break; + + default: + break; + } + } + + handleClick() { + event.stopPropagation(); + event.preventDefault(); + this.toggleExpand(); + } + + handleFocus = function () { + this.domNode.classList.add('focus'); + }; + + handleBlur() { + this.domNode.classList.remove('focus'); } } -addEventListener('ds:page:update', updateContactElementsVisibility); -delegate('change', '#contact-form #type', updateContactElementsVisibility); +/* Initialize Hide/Show Buttons */ + +if (document.querySelector('#contact-form')) { + window.addEventListener( + 'ds:page:update', + function () { + var buttons = document.querySelectorAll( + 'button[aria-expanded][aria-controls], button.button-without-hint' + ); + var expandButtons = []; + var formInput = document.querySelector('form input#type'); + + buttons.forEach((button) => { + var be = new ButtonExpand(button); + expandButtons.push(be); + }); + expandButtons.forEach((button) => button.setAllButtons(expandButtons)); + expandButtons.forEach((button) => button.setFormInput(formInput)); + }, + false + ); +} diff --git a/app/lib/helpscout/form_adapter.rb b/app/lib/helpscout/form_adapter.rb index d9aaea738..6e4dcdaac 100644 --- a/app/lib/helpscout/form_adapter.rb +++ b/app/lib/helpscout/form_adapter.rb @@ -3,10 +3,10 @@ class Helpscout::FormAdapter def self.options [ - [I18n.t(TYPE_INFO, scope: [:support, :question]), TYPE_INFO], - [I18n.t(TYPE_PERDU, scope: [:support, :question]), TYPE_PERDU], - [I18n.t(TYPE_INSTRUCTION, scope: [:support, :question]), TYPE_INSTRUCTION], - [I18n.t(TYPE_AMELIORATION, scope: [:support, :question]), TYPE_AMELIORATION], + [I18n.t(TYPE_INFO, scope: [:support, :question]), TYPE_INFO, FAQ_CONTACTER_SERVICE_EN_CHARGE_URL], + [I18n.t(TYPE_PERDU, scope: [:support, :question]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL], + [I18n.t(TYPE_INSTRUCTION, scope: [:support, :question]), TYPE_INSTRUCTION, FAQ_OU_EN_EST_MON_DOSSIER_URL], + [I18n.t(TYPE_AMELIORATION, scope: [:support, :question]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL], [I18n.t(TYPE_AUTRE, scope: [:support, :question]), TYPE_AUTRE] ] end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 9fe7a7242..f3546e7cb 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -416,6 +416,16 @@ class Dossier < ApplicationRecord end end + def archiver!(author) + update!(archived: true) + log_dossier_operation(author, :archiver) + end + + def desarchiver!(author) + update!(archived: false) + log_dossier_operation(author, :desarchiver) + end + def text_summary if brouillon? parts = [ @@ -812,7 +822,7 @@ class Dossier < ApplicationRecord end def send_web_hook - if saved_change_to_state? && !brouillon? && procedure.web_hook_url + if saved_change_to_state? && !brouillon? && procedure.web_hook_url.present? WebHookJob.perform_later( procedure, self diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index 301dd1b3a..f52254f31 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -26,7 +26,9 @@ class DossierOperationLog < ApplicationRecord supprimer: 'supprimer', restaurer: 'restaurer', modifier_annotation: 'modifier_annotation', - demander_un_avis: 'demander_un_avis' + demander_un_avis: 'demander_un_avis', + archiver: 'archiver', + desarchiver: 'desarchiver' } has_one_attached :serialized diff --git a/app/models/geo_area.rb b/app/models/geo_area.rb index 3682dc8d8..9bc30fc67 100644 --- a/app/models/geo_area.rb +++ b/app/models/geo_area.rb @@ -79,13 +79,13 @@ class GeoArea < ApplicationRecord def area if polygon? && RGeo::Geos.supported? - rgeo_geometry.area.round(1) + rgeo_geometry.area&.round(1) end end def length if line? && RGeo::Geos.supported? - rgeo_geometry.length.round(1) + rgeo_geometry.length&.round(1) end end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 9f31c345b..af7bc9a9d 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -56,7 +56,7 @@ class Procedure < ApplicationRecord MAX_DUREE_CONSERVATION = 36 MAX_DUREE_CONSERVATION_EXPORT = 3.hours - has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure, dependent: :destroy + has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure belongs_to :draft_revision, class_name: 'ProcedureRevision', optional: false belongs_to :published_revision, class_name: 'ProcedureRevision', optional: true has_many :deleted_dossiers, dependent: :destroy @@ -90,7 +90,10 @@ class Procedure < ApplicationRecord has_many :groupe_instructeurs, dependent: :destroy has_many :instructeurs, through: :groupe_instructeurs - has_many :dossiers, through: :groupe_instructeurs, dependent: :restrict_with_exception + # This relationship is used in following dossiers through. We can not use revisions relationship + # as order scope introduces invalid sql in some combinations. + has_many :unordered_revisions, class_name: 'ProcedureRevision', inverse_of: :procedure, dependent: :destroy + has_many :dossiers, through: :unordered_revisions, dependent: :restrict_with_exception has_one :initiated_mail, class_name: "Mails::InitiatedMail", dependent: :destroy has_one :received_mail, class_name: "Mails::ReceivedMail", dependent: :destroy diff --git a/app/models/procedure_presentation.rb b/app/models/procedure_presentation.rb index ed086a8c9..64515f196 100644 --- a/app/models/procedure_presentation.rb +++ b/app/models/procedure_presentation.rb @@ -160,7 +160,7 @@ class ProcedurePresentation < ApplicationRecord .filter_ilike(table, column, values) when 'groupe_instructeur' dossiers - .includes(table) + .joins(:groupe_instructeur) .filter_ilike(table, column, values) end.pluck(:id) end.reduce(:&) diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb index b97564455..54fe8b729 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -11,6 +11,8 @@ class ProcedureRevision < ApplicationRecord self.implicit_order_column = :created_at belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false + has_many :dossiers, inverse_of: :revision, foreign_key: :revision_id + has_many :revision_types_de_champ, -> { public_only.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :revision_types_de_champ_private, -> { private_only.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :types_de_champ, through: :revision_types_de_champ, source: :type_de_champ diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 9974ef1f2..7a5835616 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -17,8 +17,6 @@ # stable_id :bigint # class TypeDeChamp < ApplicationRecord - self.ignored_columns = ['procedure_id'] - enum type_champs: { text: 'text', textarea: 'textarea', @@ -309,21 +307,6 @@ class TypeDeChamp < ApplicationRecord end end - # FIXME: We are changing how id is exposed to the editor. - # We used to expose type_de_champ.id as primary key to the editor. With revisions - # we need primary key to be type_de_champ.stable_id because any update can create - # a new version but we do not want editor to know about this. - # This is only needed for a clean migration without downtime. We want to ensure - # that if editor send a simple id because it was loaded before deployment - # we would still do the right thing. - def self.to_stable_id(id_or_stable_id) - if id_or_stable_id.to_s =~ /^stable:/ - id_or_stable_id.to_s.gsub(/^stable:/, '') - else - id_or_stable_id - end - end - private def parse_drop_down_list_value(value) diff --git a/app/views/support/index.html.haml b/app/views/support/index.html.haml index 28729416f..5f5f23806 100644 --- a/app/views/support/index.html.haml +++ b/app/views/support/index.html.haml @@ -25,31 +25,21 @@ = label_tag :type do = t('your_question', scope: [:support, :question]) %span.mandatory * - = select_tag :type, options_for_select(@options, params[:type]), include_blank: t('choose_question', scope: [:support, :question]), required: true - - .support.card.featured.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_INFO } } - .card-title - = t('our_answer', scope: [:support, :response]) - .card-content - = t('procedure_info_html', scope: [:support, :response], link_procedure_info: FAQ_CONTACTER_SERVICE_EN_CHARGE_URL) - - .support.card.featured.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_PERDU } } - .card-title - = t('our_answer', scope: [:support, :response]) - .card-content - = t('lost_user_html', scope: [:support, :response], base_url: APPLICATION_BASE_URL, link_lost_user: LISTE_DES_DEMARCHES_URL) - - .support.card.featured.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_INSTRUCTION } } - .card-title - = t('our_answer', scope: [:support, :response]) - .card-content - = t('instruction_info_html', scope: [:support, :response], link_instruction: FAQ_OU_EN_EST_MON_DOSSIER_URL) - - .support.card.featured.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AMELIORATION } } - .card-title - = t('our_answer', scope: [:support, :response]) - .card-content - = t('product_html', scope: [:support, :response], link_product: FEATURE_UPVOTE_URL) + = hidden_field_tag :type, params[:type] + %dl + - @options.each do |(question, question_type, link)| + %dt + - if link.present? + %button.button{ 'aria-expanded': 'false', 'aria-controls': question_type, data: { 'question-type': question_type } }= question + - else + %button.button.button-without-hint{ data: { 'question-type': question_type } }= question + - if link.present? + %dd + .support.card.featured.hidden{ id: question_type } + .card-title + = t('our_answer', scope: [:support, :response]) + .card-content + = t("#{question_type}_html", scope: [:support, :response], base_url: APPLICATION_BASE_URL, "link_#{question_type}": link) .contact-champ = label_tag :dossier_id, t('file_number', scope: [:utils]) diff --git a/config/locales/views/support/index.en.yml b/config/locales/views/support/index.en.yml index 8d92f06f3..f804c581c 100644 --- a/config/locales/views/support/index.en.yml +++ b/config/locales/views/support/index.en.yml @@ -16,7 +16,7 @@ en:
If you have questions about the information requested, contact the service in charge of the procedure.
" instruction_info_html: "If you have questions about the instruction of your application (response delay for example), contact directly the instructor via our mail system.
- +If you are facing technical issues on the website, use the form below. We will not be able to inform you about the instruction of your application.
" product_html: "Got an idea? Please check our enhancement dashboard
diff --git a/config/locales/views/support/index.fr.yml b/config/locales/views/support/index.fr.yml index 8c09f38e1..0043277ca 100644 --- a/config/locales/views/support/index.fr.yml +++ b/config/locales/views/support/index.fr.yml @@ -16,7 +16,7 @@ fr:Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche.
" instruction_info_html: "Si vous avez des questions sur l’instruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie.
- +Si vous souhaitez poser une question pour un problème technique sur le site, utilisez le formulaire ci-dessous. Nous ne pourrons pas vous renseigner sur l'instruction de votre dossier.
" product_html: "Une idée ? Pensez à consulter notre tableau de bord des améliorations
diff --git a/lib/tasks/deployment/20200813111957_fix_geo_areas_geometry.rake b/lib/tasks/deployment/20200813111957_fix_geo_areas_geometry.rake index 1b8e162ab..3969494b9 100644 --- a/lib/tasks/deployment/20200813111957_fix_geo_areas_geometry.rake +++ b/lib/tasks/deployment/20200813111957_fix_geo_areas_geometry.rake @@ -5,6 +5,7 @@ namespace :after_party do geometry_collections = GeoArea.where("geometry -> 'type' = '\"GeometryCollection\"'") multi_polygons = GeoArea.where("geometry -> 'type' = '\"MultiPolygon\"'") + multi_line_strings = GeoArea.where("geometry -> 'type' = '\"MultiLineString\"'") def valid_geometry?(geometry) RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory) @@ -26,6 +27,24 @@ namespace :after_party do end progress.finish + progress = ProgressReport.new(multi_line_strings.count) + multi_line_strings.find_each do |multi_line_string| + multi_line_string.geometry['coordinates'].each do |coordinates| + geometry = { + type: 'LineString', + coordinates: coordinates + } + + if valid_geometry?(geometry) + multi_line_string.champ.geo_areas.create!(geometry: geometry, source: 'selection_utilisateur') + end + end + + multi_line_string.destroy + progress.inc + end + progress.finish + progress = ProgressReport.new(multi_polygons.count) multi_polygons.find_each do |multi_polygon| multi_polygon.geometry['coordinates'].each do |coordinates| diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb index b1c200c2f..02aa269b5 100644 --- a/spec/controllers/api/v2/graphql_controller_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_spec.rb @@ -396,37 +396,76 @@ describe API::V2::GraphqlController do ... on PersonneMorale { siret siegeSocial + numeroVoie + typeVoie entreprise { siren dateCreation capitalSocial + codeEffectifEntreprise } } } } }" end - - it "should be returned" do - expect(gql_errors).to eq(nil) - expect(gql_data).to eq(dossier: { - id: dossier.to_typed_id, - number: dossier.id, - usager: { - id: dossier.user.to_typed_id, - email: dossier.user.email - }, - demandeur: { - id: dossier.etablissement.to_typed_id, - siret: dossier.etablissement.siret, - siegeSocial: dossier.etablissement.siege_social, - entreprise: { - siren: dossier.etablissement.entreprise_siren, - dateCreation: dossier.etablissement.entreprise_date_creation.iso8601, - capitalSocial: dossier.etablissement.entreprise_capital_social.to_s + context "in the nominal case" do + it "should be returned" do + expect(gql_errors).to eq(nil) + expect(gql_data).to eq(dossier: { + id: dossier.to_typed_id, + number: dossier.id, + usager: { + id: dossier.user.to_typed_id, + email: dossier.user.email + }, + demandeur: { + id: dossier.etablissement.to_typed_id, + siret: dossier.etablissement.siret, + siegeSocial: dossier.etablissement.siege_social, + numeroVoie: dossier.etablissement.numero_voie.to_s, + typeVoie: dossier.etablissement.type_voie.to_s, + entreprise: { + siren: dossier.etablissement.entreprise_siren, + dateCreation: dossier.etablissement.entreprise_date_creation.iso8601, + capitalSocial: dossier.etablissement.entreprise_capital_social.to_s, + codeEffectifEntreprise: dossier.etablissement.entreprise_code_effectif_entreprise.to_s + } } - } - }) + }) + end + end + + context "when there are missing data" do + before do + dossier.etablissement.update!(entreprise_code_effectif_entreprise: nil, entreprise_capital_social: nil, + numero_voie: nil, type_voie: nil) + end + + it "should be returned" do + expect(gql_errors).to eq(nil) + expect(gql_data).to eq(dossier: { + id: dossier.to_typed_id, + number: dossier.id, + usager: { + id: dossier.user.to_typed_id, + email: dossier.user.email + }, + demandeur: { + id: dossier.etablissement.to_typed_id, + siret: dossier.etablissement.siret, + siegeSocial: dossier.etablissement.siege_social, + numeroVoie: nil, + typeVoie: nil, + entreprise: { + siren: dossier.etablissement.entreprise_siren, + dateCreation: dossier.etablissement.entreprise_date_creation.iso8601, + capitalSocial: '-1', + codeEffectifEntreprise: nil + } + } + }) + end end end end @@ -846,6 +885,48 @@ describe API::V2::GraphqlController do end end end + + describe 'dossierArchiver' do + let(:query) do + "mutation { + dossierArchiver(input: { + dossierId: \"#{dossier.to_typed_id}\", + instructeurId: \"#{instructeur.to_typed_id}\" + }) { + dossier { + archived + } + errors { + message + } + } + }" + end + + it "validation error" do + expect(gql_errors).to eq(nil) + + expect(gql_data).to eq(dossierArchiver: { + dossier: nil, + errors: [{ message: "Un dossier ne peut être archivé qu’une fois le traitement terminé" }] + }) + end + + context "should archive dossier" do + let(:dossier) { create(:dossier, :sans_suite, :with_individual, procedure: procedure) } + + it "change made" do + expect(gql_errors).to eq(nil) + + expect(gql_data).to eq(dossierArchiver: { + dossier: { + archived: true + }, + errors: nil + }) + end + end + end end end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index ff6cffc32..8a6780d37 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -813,6 +813,22 @@ describe Dossier do }.to_not have_enqueued_job(WebHookJob) end + it 'should not call webhook with empty value' do + dossier.procedure.update_column(:web_hook_url, '') + + expect { + dossier.accepte! + }.to_not have_enqueued_job(WebHookJob) + end + + it 'should not call webhook with blank value' do + dossier.procedure.update_column(:web_hook_url, ' ') + + expect { + dossier.accepte! + }.to_not have_enqueued_job(WebHookJob) + end + it 'should call webhook' do dossier.procedure.update_column(:web_hook_url, '/webhook.json') diff --git a/spec/models/procedure_presentation_spec.rb b/spec/models/procedure_presentation_spec.rb index 6f9415bf2..1c118c7ad 100644 --- a/spec/models/procedure_presentation_spec.rb +++ b/spec/models/procedure_presentation_spec.rb @@ -734,7 +734,7 @@ describe ProcedurePresentation do let!(:gi_3) { procedure.groupe_instructeurs.create(label: '3') } let!(:kept_dossier) { create(:dossier, procedure: procedure) } - let!(:discarded_dossier) { create(:dossier, groupe_instructeur: gi_2) } + let!(:discarded_dossier) { create(:dossier, procedure: procedure, groupe_instructeur: gi_2) } it { is_expected.to contain_exactly(kept_dossier.id) } @@ -746,7 +746,7 @@ describe ProcedurePresentation do ] end - let!(:other_kept_dossier) { create(:dossier, groupe_instructeur: gi_3) } + let!(:other_kept_dossier) { create(:dossier, procedure: procedure, groupe_instructeur: gi_3) } it 'returns every dossier that matches any of the search criteria for a given column' do is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)