Merge pull request #5607 from betagouv/dev

2020-09-22-01
This commit is contained in:
Paul Chavard 2020-09-22 17:35:02 +02:00 committed by GitHub
commit 402622cb02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 493 additions and 127 deletions

View file

@ -13,6 +13,10 @@ select {
line-height: 1.42857143; line-height: 1.42857143;
} }
dt {
margin-bottom: 0.5em;
}
.page-wrapper { .page-wrapper {
position: relative; position: relative;
min-height: 100%; min-height: 100%;

View file

@ -97,12 +97,12 @@ module Instructeurs
end end
def archive def archive
dossier.update(archived: true) dossier.archiver!(current_instructeur)
redirect_back(fallback_location: instructeur_procedures_url) redirect_back(fallback_location: instructeur_procedures_url)
end end
def unarchive def unarchive
dossier.update(archived: false) dossier.desarchiver!(current_instructeur)
redirect_back(fallback_location: instructeur_procedures_url) redirect_back(fallback_location: instructeur_procedures_url)
end end

View file

@ -15,7 +15,7 @@ module NewAdministrateur
end end
def update 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) if type_de_champ.update(type_de_champ_update_params)
reset_procedure reset_procedure
@ -26,13 +26,13 @@ module NewAdministrateur
end end
def move 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 head :no_content
end end
def destroy 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 reset_procedure
head :no_content head :no_content
@ -67,7 +67,7 @@ module NewAdministrateur
end end
def type_de_champ_create_params 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, :libelle,
:description, :description,
:mandatory, :mandatory,
@ -77,12 +77,6 @@ module NewAdministrateur
:piece_justificative_template, :piece_justificative_template,
:cadastres, :cadastres,
:mnhn) :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 end
def type_de_champ_update_params def type_de_champ_update_params

View file

@ -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é quune fois le traitement terminé"] }
end
end
def authorized?(dossier:, instructeur:)
instructeur.is_a?(Instructeur) && instructeur.dossiers.exists?(id: dossier.id)
end
end
end

View file

@ -457,6 +457,38 @@ type DossierAccepterPayload {
errors: [ValidationError!] 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 Autogenerated input type of DossierChangerGroupeInstructeur
""" """
@ -713,8 +745,12 @@ type Effectif {
type Entreprise { type Entreprise {
attestationFiscaleAttachment: File attestationFiscaleAttachment: File
attestationSocialeAttachment: File attestationSocialeAttachment: File
"""
capital social de lentreprise. -1 si inconnu.
"""
capitalSocial: BigInt! capitalSocial: BigInt!
codeEffectifEntreprise: String! codeEffectifEntreprise: String
dateCreation: ISO8601Date! dateCreation: ISO8601Date!
""" """
@ -921,6 +957,11 @@ type Mutation {
""" """
dossierAccepter(input: DossierAccepterInput!): DossierAccepterPayload dossierAccepter(input: DossierAccepterInput!): DossierAccepterPayload
"""
Archiver le dossier.
"""
dossierArchiver(input: DossierArchiverInput!): DossierArchiverPayload
""" """
Changer le grope instructeur du dossier. Changer le grope instructeur du dossier.
""" """
@ -1011,10 +1052,10 @@ type PersonneMorale implements Demandeur {
localite: String! localite: String!
naf: String! naf: String!
nomVoie: String! nomVoie: String!
numeroVoie: String! numeroVoie: String
siegeSocial: Boolean! siegeSocial: Boolean!
siret: String! siret: String!
typeVoie: String! typeVoie: String
} }
type PersonnePhysique implements Demandeur { type PersonnePhysique implements Demandeur {

View file

@ -7,6 +7,7 @@ module Types
field :dossier_classer_sans_suite, mutation: Mutations::DossierClasserSansSuite field :dossier_classer_sans_suite, mutation: Mutations::DossierClasserSansSuite
field :dossier_refuser, mutation: Mutations::DossierRefuser field :dossier_refuser, mutation: Mutations::DossierRefuser
field :dossier_accepter, mutation: Mutations::DossierAccepter field :dossier_accepter, mutation: Mutations::DossierAccepter
field :dossier_archiver, mutation: Mutations::DossierArchiver
field :dossier_changer_groupe_instructeur, mutation: Mutations::DossierChangerGroupeInstructeur field :dossier_changer_groupe_instructeur, mutation: Mutations::DossierChangerGroupeInstructeur
end end
end end

View file

@ -7,14 +7,14 @@ module Types
end end
field :siren, String, null: false 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 lentreprise. -1 si inconnu."
field :numero_tva_intracommunautaire, String, null: false field :numero_tva_intracommunautaire, String, null: false
field :forme_juridique, String, null: false field :forme_juridique, String, null: false
field :forme_juridique_code, String, null: false field :forme_juridique_code, String, null: false
field :nom_commercial, String, null: false field :nom_commercial, String, null: false
field :raison_sociale, String, null: false field :raison_sociale, String, null: false
field :siret_siege_social, 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_mensuel, EffectifType, null: true, description: "effectif pour un mois donné"
field :effectif_annuel, EffectifType, null: true, description: "effectif moyen d'une année" field :effectif_annuel, EffectifType, null: true, description: "effectif moyen d'une année"
field :date_creation, GraphQL::Types::ISO8601Date, null: false field :date_creation, GraphQL::Types::ISO8601Date, null: false
@ -41,6 +41,17 @@ module Types
end end
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 def effectif_annuel
if object.effectif_annuel.present? if object.effectif_annuel.present?
{ {
@ -76,8 +87,8 @@ module Types
field :naf, String, null: false field :naf, String, null: false
field :libelle_naf, String, null: false field :libelle_naf, String, null: false
field :adresse, String, null: false field :adresse, String, null: false
field :numero_voie, String, null: false field :numero_voie, String, null: true
field :type_voie, String, null: false field :type_voie, String, null: true
field :nom_voie, String, null: false field :nom_voie, String, null: false
field :complement_adresse, String, null: false field :complement_adresse, String, null: false
field :code_postal, String, null: false field :code_postal, String, null: false

View file

@ -45,12 +45,20 @@ module ChampHelper
"Culture : #{geo_area.culture} - Surface : #{geo_area.surface} ha" "Culture : #{geo_area.culture} - Surface : #{geo_area.surface} ha"
when GeoArea.sources.fetch(:selection_utilisateur) when GeoArea.sources.fetch(:selection_utilisateur)
if geo_area.polygon? if geo_area.polygon?
if geo_area.area.present?
capture do capture do
concat "Une aire de surface #{geo_area.area} m" concat "Une aire de surface #{geo_area.area} m"
concat content_tag(:sup, "2") concat content_tag(:sup, "2")
end end
else
"Une aire de surface inconnue"
end
elsif geo_area.line? elsif geo_area.line?
if geo_area.length.present?
"Une ligne longue de #{geo_area.length} m" "Une ligne longue de #{geo_area.length} m"
else
"Une ligne de longueur inconnue"
end
elsif geo_area.point? elsif geo_area.point?
"Un point situé à #{geo_area.location}" "Un point situé à #{geo_area.location}"
end end

View file

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import ReactMapboxGl, { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl'; import ReactMapboxGl, { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl';
import DrawControl from 'react-mapbox-gl-draw'; 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 '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
import { getJSON, ajax, fire } from '@utils'; import { getJSON, ajax, fire } from '@utils';
@ -11,7 +10,11 @@ import { getJSON, ajax, fire } from '@utils';
import { getMapStyle, SwitchMapStyle } from '../MapStyles'; import { getMapStyle, SwitchMapStyle } from '../MapStyles';
import SearchInput from './SearchInput'; import SearchInput from './SearchInput';
import { polygonCadastresFill, polygonCadastresLine } from './utils'; import {
polygonCadastresFill,
polygonCadastresLine,
readGeoFile
} from './utils';
import { import {
noop, noop,
filterFeatureCollection, filterFeatureCollection,
@ -149,19 +152,7 @@ function MapEditor({ featureCollection, url, preview, options }) {
} }
const onFileImport = (e, inputId) => { const onFileImport = (e, inputId) => {
const isGpxFile = e.target.files[0].name.includes('.gpx'); readGeoFile(e.target.files[0]).then(async (featureCollection) => {
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')
));
const resultFeatureCollection = await getJSON( const resultFeatureCollection = await getJSON(
`${url}/import`, `${url}/import`,
featureCollection, featureCollection,
@ -196,7 +187,7 @@ function MapEditor({ featureCollection, url, preview, options }) {
updateFeaturesList(resultFeatureCollection.features); updateFeaturesList(resultFeatureCollection.features);
setImportInputs(setInputs); setImportInputs(setInputs);
setBbox(resultFeatureCollection.bbox); setBbox(resultFeatureCollection.bbox);
}; });
}; };
const addInputFile = (e) => { const addInputFile = (e) => {

View file

@ -1,3 +1,5 @@
import { gpx, kml } from '@tmcw/togeojson/dist/togeojson.es.js';
export const polygonCadastresFill = { export const polygonCadastresFill = {
'fill-color': '#EC3323', 'fill-color': '#EC3323',
'fill-opacity': 0.3 'fill-opacity': 0.3
@ -8,3 +10,81 @@ export const polygonCadastresLine = {
'line-width': 4, 'line-width': 4,
'line-dasharray': [1, 1] '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');
});
}

View file

@ -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() { class ButtonExpand {
const contactSelect = document.querySelector('#contact-form #type'); constructor(domNode) {
if (contactSelect) { this.domNode = domNode;
const type = contactSelect.value;
const visibleElements = `[data-contact-type-only="${type}"]`;
const hiddenElements = `[data-contact-type-only]:not([data-contact-type-only="${type}"])`;
document.querySelectorAll(visibleElements).forEach(show); this.keyCode = Object.freeze({
document.querySelectorAll(hiddenElements).forEach(hide); 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); /* Initialize Hide/Show Buttons */
delegate('change', '#contact-form #type', updateContactElementsVisibility);
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
);
}

View file

@ -3,10 +3,10 @@ class Helpscout::FormAdapter
def self.options def self.options
[ [
[I18n.t(TYPE_INFO, scope: [:support, :question]), TYPE_INFO], [I18n.t(TYPE_INFO, scope: [:support, :question]), TYPE_INFO, FAQ_CONTACTER_SERVICE_EN_CHARGE_URL],
[I18n.t(TYPE_PERDU, scope: [:support, :question]), TYPE_PERDU], [I18n.t(TYPE_PERDU, scope: [:support, :question]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL],
[I18n.t(TYPE_INSTRUCTION, scope: [:support, :question]), TYPE_INSTRUCTION], [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], [I18n.t(TYPE_AMELIORATION, scope: [:support, :question]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL],
[I18n.t(TYPE_AUTRE, scope: [:support, :question]), TYPE_AUTRE] [I18n.t(TYPE_AUTRE, scope: [:support, :question]), TYPE_AUTRE]
] ]
end end

View file

@ -416,6 +416,16 @@ class Dossier < ApplicationRecord
end end
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 def text_summary
if brouillon? if brouillon?
parts = [ parts = [
@ -812,7 +822,7 @@ class Dossier < ApplicationRecord
end end
def send_web_hook 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( WebHookJob.perform_later(
procedure, procedure,
self self

View file

@ -26,7 +26,9 @@ class DossierOperationLog < ApplicationRecord
supprimer: 'supprimer', supprimer: 'supprimer',
restaurer: 'restaurer', restaurer: 'restaurer',
modifier_annotation: 'modifier_annotation', modifier_annotation: 'modifier_annotation',
demander_un_avis: 'demander_un_avis' demander_un_avis: 'demander_un_avis',
archiver: 'archiver',
desarchiver: 'desarchiver'
} }
has_one_attached :serialized has_one_attached :serialized

View file

@ -79,13 +79,13 @@ class GeoArea < ApplicationRecord
def area def area
if polygon? && RGeo::Geos.supported? if polygon? && RGeo::Geos.supported?
rgeo_geometry.area.round(1) rgeo_geometry.area&.round(1)
end end
end end
def length def length
if line? && RGeo::Geos.supported? if line? && RGeo::Geos.supported?
rgeo_geometry.length.round(1) rgeo_geometry.length&.round(1)
end end
end end

View file

@ -56,7 +56,7 @@ class Procedure < ApplicationRecord
MAX_DUREE_CONSERVATION = 36 MAX_DUREE_CONSERVATION = 36
MAX_DUREE_CONSERVATION_EXPORT = 3.hours 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 :draft_revision, class_name: 'ProcedureRevision', optional: false
belongs_to :published_revision, class_name: 'ProcedureRevision', optional: true belongs_to :published_revision, class_name: 'ProcedureRevision', optional: true
has_many :deleted_dossiers, dependent: :destroy has_many :deleted_dossiers, dependent: :destroy
@ -90,7 +90,10 @@ class Procedure < ApplicationRecord
has_many :groupe_instructeurs, dependent: :destroy has_many :groupe_instructeurs, dependent: :destroy
has_many :instructeurs, through: :groupe_instructeurs 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 :initiated_mail, class_name: "Mails::InitiatedMail", dependent: :destroy
has_one :received_mail, class_name: "Mails::ReceivedMail", dependent: :destroy has_one :received_mail, class_name: "Mails::ReceivedMail", dependent: :destroy

View file

@ -160,7 +160,7 @@ class ProcedurePresentation < ApplicationRecord
.filter_ilike(table, column, values) .filter_ilike(table, column, values)
when 'groupe_instructeur' when 'groupe_instructeur'
dossiers dossiers
.includes(table) .joins(:groupe_instructeur)
.filter_ilike(table, column, values) .filter_ilike(table, column, values)
end.pluck(:id) end.pluck(:id)
end.reduce(:&) end.reduce(:&)

View file

@ -11,6 +11,8 @@ class ProcedureRevision < ApplicationRecord
self.implicit_order_column = :created_at self.implicit_order_column = :created_at
belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false 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, -> { 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 :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 has_many :types_de_champ, through: :revision_types_de_champ, source: :type_de_champ

View file

@ -17,8 +17,6 @@
# stable_id :bigint # stable_id :bigint
# #
class TypeDeChamp < ApplicationRecord class TypeDeChamp < ApplicationRecord
self.ignored_columns = ['procedure_id']
enum type_champs: { enum type_champs: {
text: 'text', text: 'text',
textarea: 'textarea', textarea: 'textarea',
@ -309,21 +307,6 @@ class TypeDeChamp < ApplicationRecord
end end
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 private
def parse_drop_down_list_value(value) def parse_drop_down_list_value(value)

View file

@ -25,31 +25,21 @@
= label_tag :type do = label_tag :type do
= t('your_question', scope: [:support, :question]) = t('your_question', scope: [:support, :question])
%span.mandatory * %span.mandatory *
= select_tag :type, options_for_select(@options, params[:type]), include_blank: t('choose_question', scope: [:support, :question]), required: true = hidden_field_tag :type, params[:type]
%dl
.support.card.featured.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_INFO } } - @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 .card-title
= t('our_answer', scope: [:support, :response]) = t('our_answer', scope: [:support, :response])
.card-content .card-content
= t('procedure_info_html', scope: [:support, :response], link_procedure_info: FAQ_CONTACTER_SERVICE_EN_CHARGE_URL) = t("#{question_type}_html", scope: [:support, :response], base_url: APPLICATION_BASE_URL, "link_#{question_type}": link)
.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)
.contact-champ .contact-champ
= label_tag :dossier_id, t('file_number', scope: [:utils]) = label_tag :dossier_id, t('file_number', scope: [:utils])

View file

@ -16,7 +16,7 @@ en:
<p>If you have questions about the information requested, contact the service in charge of the procedure.</p> <p>If you have questions about the information requested, contact the service in charge of the procedure.</p>
<p><a href=%{link_procedure_info}>Find more information</a></p>" <p><a href=%{link_procedure_info}>Find more information</a></p>"
instruction_info_html: "<p>If you have questions about the instruction of your application (response delay for example), contact directly the instructor via our mail system.</p> instruction_info_html: "<p>If you have questions about the instruction of your application (response delay for example), contact directly the instructor via our mail system.</p>
<p><a href=%{link_instruction}>Find more information</a></p> <p><a href=%{link_instruction_info}>Find more information</a></p>
<br> <br>
<p>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.</p>" <p>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.</p>"
product_html: "<p>Got an idea? Please check our <strong>enhancement dashboard</strong></p> product_html: "<p>Got an idea? Please check our <strong>enhancement dashboard</strong></p>

View file

@ -16,7 +16,7 @@ fr:
<p>Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche.</p> <p>Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche.</p>
<p><a href=%{link_procedure_info}>En savoir plus</a></p>" <p><a href=%{link_procedure_info}>En savoir plus</a></p>"
instruction_info_html: "<p>Si vous avez des questions sur linstruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie.</p> instruction_info_html: "<p>Si vous avez des questions sur linstruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie.</p>
<p><a href=%{link_instruction}>En savoir plus</a></p> <p><a href=%{link_instruction_info}>En savoir plus</a></p>
<br> <br>
<p>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.</p>" <p>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.</p>"
product_html: "<p>Une idée ? Pensez à consulter notre <strong>tableau de bord des améliorations</strong></p> product_html: "<p>Une idée ? Pensez à consulter notre <strong>tableau de bord des améliorations</strong></p>

View file

@ -5,6 +5,7 @@ namespace :after_party do
geometry_collections = GeoArea.where("geometry -> 'type' = '\"GeometryCollection\"'") geometry_collections = GeoArea.where("geometry -> 'type' = '\"GeometryCollection\"'")
multi_polygons = GeoArea.where("geometry -> 'type' = '\"MultiPolygon\"'") multi_polygons = GeoArea.where("geometry -> 'type' = '\"MultiPolygon\"'")
multi_line_strings = GeoArea.where("geometry -> 'type' = '\"MultiLineString\"'")
def valid_geometry?(geometry) def valid_geometry?(geometry)
RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory) RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory)
@ -26,6 +27,24 @@ namespace :after_party do
end end
progress.finish 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) progress = ProgressReport.new(multi_polygons.count)
multi_polygons.find_each do |multi_polygon| multi_polygons.find_each do |multi_polygon|
multi_polygon.geometry['coordinates'].each do |coordinates| multi_polygon.geometry['coordinates'].each do |coordinates|

View file

@ -396,16 +396,51 @@ describe API::V2::GraphqlController do
... on PersonneMorale { ... on PersonneMorale {
siret siret
siegeSocial siegeSocial
numeroVoie
typeVoie
entreprise { entreprise {
siren siren
dateCreation dateCreation
capitalSocial capitalSocial
codeEffectifEntreprise
} }
} }
} }
} }
}" }"
end end
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 it "should be returned" do
expect(gql_errors).to eq(nil) expect(gql_errors).to eq(nil)
@ -420,16 +455,20 @@ describe API::V2::GraphqlController do
id: dossier.etablissement.to_typed_id, id: dossier.etablissement.to_typed_id,
siret: dossier.etablissement.siret, siret: dossier.etablissement.siret,
siegeSocial: dossier.etablissement.siege_social, siegeSocial: dossier.etablissement.siege_social,
numeroVoie: nil,
typeVoie: nil,
entreprise: { entreprise: {
siren: dossier.etablissement.entreprise_siren, siren: dossier.etablissement.entreprise_siren,
dateCreation: dossier.etablissement.entreprise_date_creation.iso8601, dateCreation: dossier.etablissement.entreprise_date_creation.iso8601,
capitalSocial: dossier.etablissement.entreprise_capital_social.to_s capitalSocial: '-1',
codeEffectifEntreprise: nil
} }
} }
}) })
end end
end end
end end
end
context "groupeInstructeur" do context "groupeInstructeur" do
let(:groupe_instructeur) { procedure.groupe_instructeurs.first } let(:groupe_instructeur) { procedure.groupe_instructeurs.first }
@ -846,6 +885,48 @@ describe API::V2::GraphqlController do
end end
end 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é quune 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
end end

View file

@ -813,6 +813,22 @@ describe Dossier do
}.to_not have_enqueued_job(WebHookJob) }.to_not have_enqueued_job(WebHookJob)
end 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 it 'should call webhook' do
dossier.procedure.update_column(:web_hook_url, '/webhook.json') dossier.procedure.update_column(:web_hook_url, '/webhook.json')

View file

@ -734,7 +734,7 @@ describe ProcedurePresentation do
let!(:gi_3) { procedure.groupe_instructeurs.create(label: '3') } let!(:gi_3) { procedure.groupe_instructeurs.create(label: '3') }
let!(:kept_dossier) { create(:dossier, procedure: procedure) } 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) } it { is_expected.to contain_exactly(kept_dossier.id) }
@ -746,7 +746,7 @@ describe ProcedurePresentation do
] ]
end 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 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) is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)