Merge pull request #6320 from betagouv/main

2021-07-06-01
This commit is contained in:
Paul Chavard 2021-07-06 11:32:45 +02:00 committed by GitHub
commit 12ecafb67a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 518 additions and 303 deletions

View file

@ -170,7 +170,7 @@ GEM
coffee-script-source coffee-script-source
execjs execjs
coffee-script-source (1.12.2) coffee-script-source (1.12.2)
concurrent-ruby (1.1.8) concurrent-ruby (1.1.9)
connection_pool (2.2.3) connection_pool (2.2.3)
crack (0.4.5) crack (0.4.5)
rexml rexml
@ -244,7 +244,7 @@ GEM
execjs (2.7.0) execjs (2.7.0)
factory_bot (6.1.0) factory_bot (6.1.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
faraday (1.4.2) faraday (1.4.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)

View file

@ -1,4 +1,5 @@
@import "colors"; @import "colors";
@import "constants";
.areas-title { .areas-title {
font-weight: bold; font-weight: bold;
@ -38,6 +39,24 @@
left: 5px; left: 5px;
} }
} }
.map-style-panel {
z-index: 99;
padding: $default-spacer;
margin-bottom: $default-spacer;
ul {
list-style: none;
padding: $default-spacer;
padding-bottom: 0;
margin-bottom: -$default-spacer;
label {
font-size: 12px;
font-weight: normal;
}
}
}
} }
.cadastres-selection-control { .cadastres-selection-control {

View file

@ -1,11 +1,15 @@
@import "constants"; @import "constants";
.m-0 {
margin: 0px !important;
}
.mb-1 { .mb-1 {
margin-bottom: $default-spacer; margin-bottom: $default-spacer;
} }
.mr-1 { .mr-1 {
margin-right: $default-spacer; margin-right: $default-spacer !important;
} }
.mb-4 { .mb-4 {
@ -20,6 +24,10 @@
padding-left: 0px !important; padding-left: 0px !important;
} }
.p-0 {
padding: 0px !important;
}
.bold { .bold {
font-weight: bold; font-weight: bold;
} }

View file

@ -4,7 +4,7 @@ module Experts
before_action :authenticate_expert!, except: [:sign_up, :update_expert] before_action :authenticate_expert!, except: [:sign_up, :update_expert]
before_action :check_if_avis_revoked, only: [:show] before_action :check_if_avis_revoked, only: [:show]
before_action :redirect_if_no_sign_up_needed, only: [:sign_up] before_action :redirect_if_no_sign_up_needed, only: [:sign_up, :update_expert]
before_action :set_avis_and_dossier, only: [:show, :instruction, :messagerie, :create_commentaire, :update] before_action :set_avis_and_dossier, only: [:show, :instruction, :messagerie, :create_commentaire, :update]
A_DONNER_STATUS = 'a-donner' A_DONNER_STATUS = 'a-donner'
@ -82,12 +82,11 @@ module Experts
email = params[:email] email = params[:email]
password = params[:user][:password] password = params[:user][:password]
# Not perfect because the password will not be changed if the user already exists
user = User.create_or_promote_to_expert(email, password) user = User.create_or_promote_to_expert(email, password)
user.reset_password(password, password)
if user.valid? if user.valid?
sign_in(user) sign_in(user)
redirect_to url_for(expert_all_avis_path) redirect_to url_for(expert_all_avis_path)
else else
flash[:alert] = user.errors.full_messages flash[:alert] = user.errors.full_messages
@ -128,11 +127,9 @@ module Experts
if current_expert.present? if current_expert.present?
# an expert is authenticated ... lets see if it can view the dossier # an expert is authenticated ... lets see if it can view the dossier
redirect_to expert_avis_url(avis.procedure, avis) redirect_to expert_avis_url(avis.procedure, avis)
elsif avis.expert&.email == params[:email] && avis.expert.user.active?.present? elsif avis.expert&.email == params[:email] && avis.expert.user.active?.present?
# The expert already used the sign-in page to change their password: ask them to sign-in instead.
redirect_to new_user_session_url redirect_to new_user_session_url
end end
end end

View file

@ -7,7 +7,6 @@ module Instructeurs
@average_dossier_weight = procedure.average_dossier_weight @average_dossier_weight = procedure.average_dossier_weight
@count_dossiers_termines_by_month = Traitement.count_dossiers_termines_by_month(groupe_instructeurs) @count_dossiers_termines_by_month = Traitement.count_dossiers_termines_by_month(groupe_instructeurs)
@nb_dossiers_termines = @count_dossiers_termines_by_month.sum { |count_by_month| count_by_month["count"] }
@archives = Archive @archives = Archive
.for_groupe_instructeur(groupe_instructeurs) .for_groupe_instructeur(groupe_instructeurs)

View file

@ -27,7 +27,7 @@ module NewAdministrateur
end end
def reset_procedure def reset_procedure
if @procedure.brouillon? if @procedure.brouillon? || @procedure.draft_changed?
@procedure.reset! @procedure.reset!
end end
end end

View file

@ -55,11 +55,8 @@ module NewAdministrateur
def show def show
@procedure = current_administrateur.procedures.find(params[:id]) @procedure = current_administrateur.procedures.find(params[:id])
@current_administrateur = current_administrateur @current_administrateur = current_administrateur
if @procedure.brouillon? @procedure_lien = commencer_url(path: @procedure.path)
@procedure_lien = commencer_test_url(path: @procedure.path) @procedure_lien_test = commencer_test_url(path: @procedure.path)
else
@procedure_lien = commencer_url(path: @procedure.path)
end
end end
def edit def edit
@ -142,11 +139,8 @@ module NewAdministrateur
end end
def publication def publication
if @procedure.brouillon? @procedure_lien = commencer_url(path: @procedure.path)
@procedure_lien = commencer_test_url(path: @procedure.path) @procedure_lien_test = commencer_test_url(path: @procedure.path)
else
@procedure_lien = commencer_url(path: @procedure.path)
end
@procedure.path = @procedure.suggested_path(current_administrateur) @procedure.path = @procedure.suggested_path(current_administrateur)
@current_administrateur = current_administrateur @current_administrateur = current_administrateur
end end

View file

@ -5,13 +5,15 @@ module Users
def commencer def commencer
@procedure = retrieve_procedure @procedure = retrieve_procedure
return procedure_not_found if @procedure.blank? || @procedure.brouillon? return procedure_not_found if @procedure.blank? || @procedure.brouillon?
@revision = @procedure.published_revision
render 'commencer/show' render 'commencer/show'
end end
def commencer_test def commencer_test
@procedure = retrieve_procedure @procedure = retrieve_procedure
return procedure_not_found if @procedure.blank? || @procedure.publiee? return procedure_not_found if @procedure.blank? || (@procedure.publiee? && !@procedure.draft_changed?)
@revision = @procedure.draft_revision
render 'commencer/show' render 'commencer/show'
end end
@ -20,14 +22,14 @@ module Users
@procedure = retrieve_procedure @procedure = retrieve_procedure
return procedure_not_found if @procedure.blank? || @procedure.brouillon? return procedure_not_found if @procedure.blank? || @procedure.brouillon?
generate_empty_pdf(@procedure) generate_empty_pdf(@procedure.published_revision)
end end
def dossier_vide_pdf_test def dossier_vide_pdf_test
@procedure = retrieve_procedure @procedure = retrieve_procedure
return procedure_not_found if @procedure.blank? || @procedure.publiee? return procedure_not_found if @procedure.blank? || (@procedure.publiee? && !@procedure.draft_changed?)
generate_empty_pdf(@procedure) generate_empty_pdf(@procedure.draft_revision)
end end
def sign_in def sign_in
@ -80,10 +82,10 @@ module Users
store_location_for(:user, helpers.procedure_lien(procedure)) store_location_for(:user, helpers.procedure_lien(procedure))
end end
def generate_empty_pdf(procedure) def generate_empty_pdf(revision)
@dossier = procedure.active_revision.new_dossier @dossier = revision.new_dossier
s = render_to_string(template: 'dossiers/dossier_vide', formats: [:pdf]) s = render_to_string(template: 'dossiers/dossier_vide', formats: [:pdf])
send_data(s, :filename => "#{procedure.libelle}.pdf") send_data(s, :filename => "#{revision.procedure.libelle}.pdf")
end end
end end
end end

View file

@ -242,18 +242,18 @@ module Users
erase_user_location! erase_user_location!
begin begin
if params[:brouillon] procedure = if params[:brouillon]
procedure = Procedure.brouillon.find(params[:procedure_id]) Procedure.publiees.or(Procedure.brouillons).find(params[:procedure_id])
else else
procedure = Procedure.publiees.find(params[:procedure_id]) Procedure.publiees.find(params[:procedure_id])
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
flash.alert = t('errors.messages.procedure_not_found') flash.alert = t('errors.messages.procedure_not_found')
return redirect_to url_for dossiers_path return redirect_to dossiers_path
end end
dossier = Dossier.new( dossier = Dossier.new(
revision: procedure.active_revision, revision: params[:brouillon] ? procedure.draft_revision : procedure.active_revision,
groupe_instructeur: procedure.defaut_groupe_instructeur_for_new_dossier, groupe_instructeur: procedure.defaut_groupe_instructeur_for_new_dossier,
user: current_user, user: current_user,
state: Dossier.states.fetch(:brouillon) state: Dossier.states.fetch(:brouillon)
@ -303,7 +303,7 @@ module Users
end end
def show_demarche_en_test_banner def show_demarche_en_test_banner
if @dossier.present? && @dossier.procedure.brouillon? if @dossier.present? && @dossier.revision.draft?
flash.now.alert = "Ce dossier est déposé sur une démarche en test. Toute modification de la démarche par l'administrateur (ajout dun champ, publication de la démarche...) entraînera sa suppression." flash.now.alert = "Ce dossier est déposé sur une démarche en test. Toute modification de la démarche par l'administrateur (ajout dun champ, publication de la démarche...) entraînera sa suppression."
end end
end end

View file

@ -25,12 +25,8 @@ module DossierHelper
end end
end end
def url_for_new_dossier(procedure) def url_for_new_dossier(revision)
if procedure.brouillon? new_dossier_url(procedure_id: revision.procedure.id, brouillon: revision.draft? ? true : nil)
new_dossier_url(procedure_id: procedure.id, brouillon: true)
else
new_dossier_url(procedure_id: procedure.id)
end
end end
def dossier_form_class(dossier) def dossier_form_class(dossier)

View file

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ReactMapboxGl, { ZoomControl } from 'react-mapbox-gl'; import ReactMapboxGl, { ZoomControl } from 'react-mapbox-gl';
import DrawControl from 'react-mapbox-gl-draw'; import DrawControl from 'react-mapbox-gl-draw';
import { MapIcon } from '@heroicons/react/outline'; import { CursorClickIcon } from '@heroicons/react/outline';
import 'mapbox-gl/dist/mapbox-gl.css'; import 'mapbox-gl/dist/mapbox-gl.css';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
@ -36,7 +36,13 @@ function MapEditor({ featureCollection, url, options, preview }) {
enabled: !preview, enabled: !preview,
cadastreEnabled cadastreEnabled
}); });
const [style, setStyle] = useMapStyle(options.layers, { const {
style,
layers,
setStyle,
setLayerEnabled,
setLayerOpacity
} = useMapStyle(options.layers, {
onStyleChange, onStyleChange,
cadastreEnabled cadastreEnabled
}); });
@ -133,7 +139,13 @@ function MapEditor({ featureCollection, url, options, preview }) {
}} }}
/> />
)} )}
<MapStyleControl style={style.id} setStyle={setStyle} /> <MapStyleControl
style={style.id}
layers={layers}
setStyle={setStyle}
setLayerEnabled={setLayerEnabled}
setLayerOpacity={setLayerOpacity}
/>
<ZoomControl /> <ZoomControl />
{options.layers.includes('cadastres') && ( {options.layers.includes('cadastres') && (
<div className="cadastres-selection-control mapboxgl-ctrl-group"> <div className="cadastres-selection-control mapboxgl-ctrl-group">
@ -145,7 +157,7 @@ function MapEditor({ featureCollection, url, options, preview }) {
title="Sélectionner les parcelles cadastrales" title="Sélectionner les parcelles cadastrales"
className={cadastreEnabled ? 'on' : ''} className={cadastreEnabled ? 'on' : ''}
> >
<MapIcon className="icon-size" /> <CursorClickIcon className="icon-size" />
</button> </button>
</div> </div>
)} )}

View file

@ -20,7 +20,13 @@ const MapReader = ({ featureCollection, options }) => {
onMouseEnter, onMouseEnter,
onMouseLeave onMouseLeave
} = useMapbox(featureCollection); } = useMapbox(featureCollection);
const [style, setStyle] = useMapStyle(options.layers, { onStyleChange }); const {
style,
layers,
setStyle,
setLayerEnabled,
setLayerOpacity
} = useMapStyle(options.layers, { onStyleChange });
if (!isSupported) { if (!isSupported) {
return ( return (
@ -36,7 +42,7 @@ const MapReader = ({ featureCollection, options }) => {
<Mapbox <Mapbox
onStyleLoad={(map) => onLoad(map)} onStyleLoad={(map) => onLoad(map)}
style={style} style={style}
containerStyle={{ height: '400px' }} containerStyle={{ height: '500px' }}
> >
<SelectionUtilisateurPolygonLayer <SelectionUtilisateurPolygonLayer
featureCollection={featureCollection} featureCollection={featureCollection}
@ -54,7 +60,13 @@ const MapReader = ({ featureCollection, options }) => {
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
/> />
<MapStyleControl style={style.id} setStyle={setStyle} /> <MapStyleControl
style={style.id}
layers={layers}
setStyle={setStyle}
setLayerEnabled={setLayerEnabled}
setLayerOpacity={setLayerOpacity}
/>
<ZoomControl /> <ZoomControl />
</Mapbox> </Mapbox>
); );

View file

@ -1,35 +1,28 @@
import React, { useMemo, useState, useEffect } from 'react'; import React, { useMemo, useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Popover, RadioGroup } from '@headlessui/react';
import { usePopper } from 'react-popper';
import { MapIcon } from '@heroicons/react/outline';
import { Slider } from '@reach/slider';
import '@reach/slider/styles.css';
import { getMapStyle } from './styles'; import { getMapStyle, getLayerName, NBS } from './styles';
import ortho from './styles/images/preview-ortho.png';
import vector from './styles/images/preview-vector.png';
const STYLES = { const STYLES = {
ortho: { ortho: 'Satellite',
title: 'Satellite', vector: 'Vectoriel',
preview: ortho, ign: 'Carte IGN'
color: '#fff'
},
vector: {
title: 'Vectoriel',
preview: vector,
color: '#000'
},
ign: {
title: 'Carte IGN',
preview: vector,
color: '#000'
}
}; };
function getNextStyle(style) { function optionalLayersMap(optionalLayers) {
const styleNames = Object.keys(STYLES); return Object.fromEntries(
const index = styleNames.indexOf(style) + 1; optionalLayers
if (index === styleNames.length) { .filter((layer) => layer != 'cadastres')
return styleNames[0]; .map((layer) => [
} layer,
return styleNames[index]; { enabled: true, opacity: 70, name: getLayerName(layer) }
])
);
} }
export function useMapStyle( export function useMapStyle(
@ -37,33 +30,149 @@ export function useMapStyle(
{ onStyleChange, cadastreEnabled } { onStyleChange, cadastreEnabled }
) { ) {
const [styleId, setStyle] = useState('ortho'); const [styleId, setStyle] = useState('ortho');
const style = useMemo(() => getMapStyle(styleId, optionalLayers), [ const [layers, setLayers] = useState(() => optionalLayersMap(optionalLayers));
styleId, const setLayerEnabled = (layer, enabled) =>
optionalLayers setLayers((optionalLayers) => {
]); optionalLayers[layer].enabled = enabled;
return { ...optionalLayers };
});
const setLayerOpacity = (layer, opacity) =>
setLayers((optionalLayers) => {
optionalLayers[layer].opacity = opacity;
return { ...optionalLayers };
});
const enabledLayers = Object.entries(layers).filter(
([, { enabled }]) => enabled
);
const style = useMemo(
() =>
getMapStyle(
styleId,
enabledLayers.map(([layer]) => layer),
Object.fromEntries(
enabledLayers.map(([layer, { opacity }]) => [layer, opacity])
)
),
[
styleId,
enabledLayers.map(([layer, { opacity }]) => `${layer}-${opacity}`)
]
);
useEffect(() => onStyleChange(), [styleId, cadastreEnabled]); useEffect(() => onStyleChange(), [styleId, cadastreEnabled]);
return [style, setStyle]; return { style, layers, setStyle, setLayerEnabled, setLayerOpacity };
} }
function MapStyleControl({ style, setStyle }) { function MapStyleControl({
const nextStyle = getNextStyle(style); style,
const { title, preview, color } = STYLES[nextStyle]; layers,
setStyle,
setLayerEnabled,
setLayerOpacity
}) {
const [buttonElement, setButtonElement] = useState();
const [panelElement, setPanelElement] = useState();
const { styles, attributes } = usePopper(buttonElement, panelElement, {
placement: 'bottom-end'
});
return ( return (
<div className="map-style-control"> <div className="form map-style-control mapboxgl-ctrl-group">
<button type="button" onClick={() => setStyle(nextStyle)}> <Popover>
<img alt={title} src={preview} /> <Popover.Button
<div style={{ color }}>{title}</div> ref={setButtonElement}
</button> className="map-style-button"
title="Sélectionner les couches cartographiques"
>
<MapIcon className="icon-size" />
</Popover.Button>
<Popover.Panel
className="flex map-style-panel mapboxgl-ctrl-group"
ref={setPanelElement}
style={styles.popper}
{...attributes.popper}
>
<RadioGroup
value={style}
onChange={setStyle}
className="styles-list"
as="ul"
>
{Object.entries(STYLES).map(([style, title]) => (
<RadioGroup.Option
key={style}
value={style}
as="li"
className="flex"
>
{({ checked }) => (
<>
<input
type="radio"
key={`${style}-${checked}`}
defaultChecked={checked}
name="map-style"
className="m-0 p-0 mr-1"
/>
<RadioGroup.Label>
{title.replace(/\s/g, ' ')}
</RadioGroup.Label>
</>
)}
</RadioGroup.Option>
))}
</RadioGroup>
{Object.keys(layers).length ? (
<ul className="layers-list">
{Object.entries(layers).map(
([layer, { enabled, opacity, name }]) => (
<li key={layer}>
<div className="flex mb-1">
<input
className="m-0 p-0 mr-1"
type="checkbox"
checked={enabled}
onChange={(event) => {
setLayerEnabled(layer, event.target.checked);
}}
/>
<label className="m-0">{name}</label>
</div>
<Slider
min={10}
max={100}
step={5}
value={opacity}
onChange={(value) => {
setLayerOpacity(layer, value);
}}
className="mb-1"
title={`Réglage de lopacité de la couche «${NBS}${name}${NBS}»`}
getAriaLabel={() =>
`Réglage de lopacité de la couche «${NBS}${name}${NBS}»`
}
getAriaValueText={(value) =>
`Lopacité de la couche «${NBS}${name}${NBS}» est à ${value}${NBS}%`
}
/>
</li>
)
)}
</ul>
) : null}
</Popover.Panel>
</Popover>
</div> </div>
); );
} }
MapStyleControl.propTypes = { MapStyleControl.propTypes = {
style: PropTypes.string, style: PropTypes.string,
setStyle: PropTypes.func layers: PropTypes.object,
setStyle: PropTypes.func,
setLayerEnabled: PropTypes.func,
setLayerOpacity: PropTypes.func
}; };
export default MapStyleControl; export default MapStyleControl;

View file

@ -119,9 +119,9 @@ const OPTIONAL_LAYERS = [
function buildSources() { function buildSources() {
return Object.fromEntries( return Object.fromEntries(
OPTIONAL_LAYERS.filter(({ id }) => id !== 'cadastres') OPTIONAL_LAYERS.filter(({ id }) => id !== 'cadastres')
.flatMap(({ layers }) => layers) .flatMap(({ layers }) => layers.map(([, code]) => code))
.map(([, code]) => [ .map((code) => [
code.toLowerCase().replace(/\./g, '-'), getLayerCode(code),
rasterSource([ignServiceURL(code)], 'IGN-F/Géoportail/MNHN') rasterSource([ignServiceURL(code)], 'IGN-F/Géoportail/MNHN')
]) ])
); );
@ -138,25 +138,40 @@ function rasterSource(tiles, attribution) {
}; };
} }
function rasterLayer(source) { function rasterLayer(source, opacity) {
return { return {
id: source, id: source,
source, source,
type: 'raster', type: 'raster',
paint: { 'raster-resampling': 'linear' } paint: { 'raster-resampling': 'linear', 'raster-opacity': opacity }
}; };
} }
export function buildOptionalLayers(ids) { export function buildOptionalLayers(ids, opacity) {
return OPTIONAL_LAYERS.filter(({ id }) => ids.includes(id)) return OPTIONAL_LAYERS.filter(({ id }) => ids.includes(id))
.flatMap(({ layers }) => layers) .flatMap(({ layers, id }) =>
.flatMap(([, code]) => layers.map(([, code]) => [code, opacity[id] / 100])
)
.flatMap(([code, opacity]) =>
code === 'CADASTRE' code === 'CADASTRE'
? cadastreLayers ? cadastreLayers
: [rasterLayer(code.toLowerCase().replace(/\./g, '-'))] : [rasterLayer(getLayerCode(code), opacity)]
); );
} }
export const NBS = ' ';
export function getLayerName(layer) {
return OPTIONAL_LAYERS.find(({ id }) => id == layer).label.replace(
/\s/g,
NBS
);
}
function getLayerCode(code) {
return code.toLowerCase().replace(/\./g, '-');
}
export default { export default {
version: 8, version: 8,
metadat: { metadat: {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,9 +1,11 @@
import baseStyle, { buildOptionalLayers } from './base'; import baseStyle, { buildOptionalLayers, getLayerName, NBS } from './base';
import orthoStyle from './layers/ortho'; import orthoStyle from './layers/ortho';
import vectorStyle from './layers/vector'; import vectorStyle from './layers/vector';
import ignLayers from './layers/ign'; import ignLayers from './layers/ign';
export function getMapStyle(id, optionalLayers) { export { getLayerName, NBS };
export function getMapStyle(id, layers, opacity) {
const style = { ...baseStyle, id }; const style = { ...baseStyle, id };
switch (id) { switch (id) {
@ -21,7 +23,7 @@ export function getMapStyle(id, optionalLayers) {
break; break;
} }
style.layers = style.layers.concat(buildOptionalLayers(optionalLayers)); style.layers = style.layers.concat(buildOptionalLayers(layers, opacity));
return style; return style;
} }

View file

@ -307,9 +307,7 @@ class Procedure < ApplicationRecord
end end
def reset! def reset!
if locked? if !locked? || draft_changed?
raise "Can not reset a locked procedure."
else
draft_revision.dossiers.destroy_all draft_revision.dossiers.destroy_all
end end
end end
@ -426,7 +424,9 @@ class Procedure < ApplicationRecord
} }
} }
include_list[:groupe_instructeurs] = :instructeurs if !is_different_admin include_list[:groupe_instructeurs] = :instructeurs if !is_different_admin
procedure = self.deep_clone(include: include_list, &method(:clone_attachments)) procedure = self.deep_clone(include: include_list) do |original, kopy|
PiecesJustificativesService.clone_attachments(original, kopy)
end
procedure.path = SecureRandom.uuid procedure.path = SecureRandom.uuid
procedure.aasm_state = :brouillon procedure.aasm_state = :brouillon
procedure.closed_at = nil procedure.closed_at = nil
@ -472,29 +472,6 @@ class Procedure < ApplicationRecord
procedure procedure
end end
def clone_attachments(original, kopy)
if original.is_a?(TypeDeChamp)
clone_attachment(:piece_justificative_template, original, kopy)
elsif original.is_a?(Procedure)
clone_attachment(:logo, original, kopy)
clone_attachment(:notice, original, kopy)
clone_attachment(:deliberation, original, kopy)
end
end
def clone_attachment(attribute, original, kopy)
original_attachment = original.send(attribute)
if original_attachment.attached?
kopy.send(attribute).attach({
io: StringIO.new(original_attachment.download),
filename: original_attachment.filename,
content_type: original_attachment.content_type,
# we don't want to run virus scanner on cloned file
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
})
end
end
def whitelisted? def whitelisted?
whitelisted_at.present? whitelisted_at.present?
end end

View file

@ -249,7 +249,9 @@ class ProcedureRevision < ApplicationRecord
def revise_type_de_champ(type_de_champ) def revise_type_de_champ(type_de_champ)
types_de_champ_association = type_de_champ.private? ? :revision_types_de_champ_private : :revision_types_de_champ types_de_champ_association = type_de_champ.private? ? :revision_types_de_champ_private : :revision_types_de_champ
association = send(types_de_champ_association).find_by!(type_de_champ: type_de_champ) association = send(types_de_champ_association).find_by!(type_de_champ: type_de_champ)
cloned_type_de_champ = type_de_champ.deep_clone(include: [:types_de_champ], &type_de_champ.method(:clone_attachments)) cloned_type_de_champ = type_de_champ.deep_clone(include: [:types_de_champ]) do |original, kopy|
PiecesJustificativesService.clone_attachments(original, kopy)
end
cloned_type_de_champ.revision = self cloned_type_de_champ.revision = self
association.update!(type_de_champ: cloned_type_de_champ) association.update!(type_de_champ: cloned_type_de_champ)
cloned_type_de_champ cloned_type_de_champ

View file

@ -369,23 +369,4 @@ class TypeDeChamp < ApplicationRecord
types_de_champ.destroy_all types_de_champ.destroy_all
end end
end end
def clone_attachments(original, kopy)
if original.is_a?(TypeDeChamp)
clone_attachment(:piece_justificative_template, original, kopy)
end
end
def clone_attachment(attribute, original, kopy)
original_attachment = original.send(attribute)
if original_attachment.attached?
kopy.send(attribute).attach({
io: StringIO.new(original_attachment.download),
filename: original_attachment.filename,
content_type: original_attachment.content_type,
# we don't want to run virus scanner on cloned file
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
})
end
end
end end

View file

@ -49,6 +49,30 @@ class PiecesJustificativesService
end end
end end
def self.clone_attachments(original, kopy)
if original.is_a?(TypeDeChamp)
clone_attachment(original.piece_justificative_template, kopy.piece_justificative_template)
elsif original.is_a?(Procedure)
clone_attachment(original.logo, kopy.logo)
clone_attachment(original.notice, kopy.notice)
clone_attachment(original.deliberation, kopy.deliberation)
end
end
def self.clone_attachment(original_attachment, copy_attachment)
if original_attachment.attached?
original_attachment.open do |tempfile|
copy_attachment.attach({
io: File.open(tempfile.path),
filename: original_attachment.filename,
content_type: original_attachment.content_type,
# we don't want to run virus scanner on cloned file
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
})
end
end
end
class FakeAttachment < Hashie::Dash class FakeAttachment < Hashie::Dash
property :filename property :filename
property :name property :name

View file

@ -11,12 +11,12 @@
= link_to 'Jai déjà un compte', commencer_sign_in_path(path: @procedure.path), class: ['button large expand'] = link_to 'Jai déjà un compte', commencer_sign_in_path(path: @procedure.path), class: ['button large expand']
- else - else
- dossiers = current_user.dossiers.where(revision: @procedure.revisions) - dossiers = current_user.dossiers.where(revision: @revision.draft? ? @revision : @procedure.revisions.where.not(id: @procedure.draft_revision_id))
- drafts = dossiers.merge(Dossier.state_brouillon) - drafts = dossiers.merge(Dossier.state_brouillon)
- not_drafts = dossiers.merge(Dossier.state_not_brouillon) - not_drafts = dossiers.merge(Dossier.state_not_brouillon)
- if dossiers.count == 0 - if dossiers.count == 0
= link_to 'Commencer la démarche', url_for_new_dossier(@procedure), class: ['button large expand primary'] = link_to 'Commencer la démarche', url_for_new_dossier(@revision), class: ['button large expand primary']
- elsif drafts.count == 1 && not_drafts.count == 0 - elsif drafts.count == 1 && not_drafts.count == 0
- dossier = drafts.first - dossier = drafts.first
@ -25,7 +25,7 @@
Il y a <strong>#{time_ago_in_words(dossier.created_at)}</strong>, Il y a <strong>#{time_ago_in_words(dossier.created_at)}</strong>,
vous avez commencé à remplir un dossier sur la démarche « #{dossier.procedure.libelle} ». vous avez commencé à remplir un dossier sur la démarche « #{dossier.procedure.libelle} ».
= link_to 'Continuer à remplir mon dossier', brouillon_dossier_path(dossier), class: ['button large expand primary'] = link_to 'Continuer à remplir mon dossier', brouillon_dossier_path(dossier), class: ['button large expand primary']
= link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand'] = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@revision), class: ['button large expand']
- elsif not_drafts.count == 1 - elsif not_drafts.count == 1
- dossier = not_drafts.first - dossier = not_drafts.first
@ -34,17 +34,15 @@
Il y a <strong>#{time_ago_in_words(dossier.en_construction_at)}</strong>, Il y a <strong>#{time_ago_in_words(dossier.en_construction_at)}</strong>,
vous avez déposé un dossier sur la démarche « #{dossier.procedure.libelle} ». vous avez déposé un dossier sur la démarche « #{dossier.procedure.libelle} ».
= link_to 'Voir mon dossier déposé', dossier_path(dossier), class: ['button large expand primary'] = link_to 'Voir mon dossier déposé', dossier_path(dossier), class: ['button large expand primary']
= link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand'] = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@revision), class: ['button large expand']
- else - else
%h2.huge-title Vous avez déjà des dossiers pour cette démarche %h2.huge-title Vous avez déjà des dossiers pour cette démarche
= link_to 'Voir mes dossiers en cours', dossiers_path, class: ['button large expand primary'] = link_to 'Voir mes dossiers en cours', dossiers_path, class: ['button large expand primary']
= link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand'] = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@revision), class: ['button large expand']
- if @procedure.feature_enabled?(:dossier_pdf_vide) - if @procedure.feature_enabled?(:dossier_pdf_vide)
- pdf_link = commencer_dossier_vide_path(path: @procedure.path) - pdf_link = @revision.draft? ? commencer_dossier_vide_test_path(path: @procedure.path) : commencer_dossier_vide_path(path: @procedure.path)
- if @procedure.brouillon?
- pdf_link = commencer_dossier_vide_test_path(path: @procedure.path)
%hr %hr
%p %p
Vous souhaitez effectuer une demande par papier ? Vous pouvez télécharger un dossier vide au format PDF, Vous souhaitez effectuer une demande par papier ? Vous pouvez télécharger un dossier vide au format PDF,

View file

@ -30,31 +30,6 @@
%th.center Télécharger %th.center Télécharger
%tbody %tbody
%tr
- matching_archive = @archives.find { |archive| archive.time_span_type == 'everything' }
- weight = estimate_weight(matching_archive, @nb_dossiers_termines, @average_dossier_weight)
%td
Tous les dossiers
%td.text-right
= @nb_dossiers_termines
%td.text-right
= number_to_human_size(weight)
%td.center
- if matching_archive.try(&:available?)
= link_to url_for(matching_archive.file), class: 'button primary' do
%span.icon.download-white
= t(:archive_ready_html, generated_period: time_ago_in_words(matching_archive.updated_at), scope: [:instructeurs, :procedure])
- elsif matching_archive.try(&:pending?)
%span.icon.retry
= t(:archive_pending_html, created_period: time_ago_in_words(matching_archive.created_at), scope: [:instructeurs, :procedure])
- elsif @nb_dossiers_termines == 0
Rien à télécharger !
- elsif weight < 1.gigabyte
= link_to instructeur_archives_path(@procedure, type: 'everything'), method: :post, class: "button" do
%span.icon.new-folder
Demander la création
- else
Archive trop volumineuse
- @count_dossiers_termines_by_month.each do |count_by_month| - @count_dossiers_termines_by_month.each do |count_by_month|
- month = count_by_month["month"].to_date - month = count_by_month["month"].to_date
- nb_dossiers_termines = count_by_month["count"] - nb_dossiers_termines = count_by_month["count"]

View file

@ -25,11 +25,7 @@
%li %li
= active_link_to "Démarches", instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'tab-link' = active_link_to "Démarches", instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'tab-link'
- if current_instructeur.user.expert && current_expert.avis_summary[:total] > 0 - if current_instructeur.user.expert && current_expert.avis_summary[:total] > 0
%li = render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert }
= active_link_to expert_all_avis_path, active: controller_name == 'avis', class: 'tab-link' do
Avis
- if current_expert.avis_summary[:unanswered] > 0
%span.badge.warning= current_expert.avis_summary[:unanswered]
- if nav_bar_profile == :expert && expert_signed_in? - if nav_bar_profile == :expert && expert_signed_in?
%ul.header-tabs %ul.header-tabs
@ -38,16 +34,14 @@
= active_link_to "Démarches", instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'tab-link' = active_link_to "Démarches", instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'tab-link'
- if current_expert.avis_summary[:total] > 0 - if current_expert.avis_summary[:total] > 0
%li = render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert }
= active_link_to expert_all_avis_path, active: controller_name == 'avis', class: 'tab-link' do
Avis
- if current_expert.avis_summary[:unanswered] > 0
%span.badge.warning= current_expert.avis_summary[:unanswered]
- if nav_bar_profile == :user - if nav_bar_profile == :user
%ul.header-tabs %ul.header-tabs
%li %li
= active_link_to "Dossiers", dossiers_path, active: :inclusive, class: 'tab-link' = active_link_to "Dossiers", dossiers_path, active: :inclusive, class: 'tab-link'
- if current_user.expert && current_expert.avis_summary[:total] > 0
= render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert }
%ul.header-right-content %ul.header-right-content
- if params[:controller] == 'recherche' - if params[:controller] == 'recherche'

View file

@ -0,0 +1,5 @@
%li
= active_link_to expert_all_avis_path, active: controller_name == 'avis', class: 'tab-link' do
Avis
- if current_expert.avis_summary[:unanswered] > 0
%span.badge.warning= current_expert.avis_summary[:unanswered]

View file

@ -28,7 +28,7 @@
%p %p
Cette démarche est actuellement <strong>en test</strong>, Cette démarche est actuellement <strong>en test</strong>,
pour y accéder vous pouvez utiliser le lien : pour y accéder vous pouvez utiliser le lien :
= link_to @procedure_lien, sanitize_url(@procedure_lien), target: :blank, rel: :noopener = link_to @procedure_lien_test, sanitize_url(@procedure_lien_test), target: :blank, rel: :noopener
%p.mb-4 %p.mb-4
Toute personne ayant la connaissance de ce lien pourra ainsi remplir des dossiers de test sur votre démarche. Toute personne ayant la connaissance de ce lien pourra ainsi remplir des dossiers de test sur votre démarche.

View file

@ -8,8 +8,8 @@
%span.icon.preview %span.icon.preview
Prévisualiser Prévisualiser
- if @procedure.brouillon? - if @procedure.brouillon? || @procedure.draft_changed?
= link_to sanitize_url(@procedure_lien), target: :blank, rel: :noopener, class: 'button' do = link_to sanitize_url(@procedure_lien_test), target: :blank, rel: :noopener, class: 'button' do
%span.icon.in-progress %span.icon.in-progress
Tester Tester

View file

@ -7,13 +7,5 @@ Sentry.init do |config|
config.enabled_environments = ['production', secrets[:environment].presence].compact config.enabled_environments = ['production', secrets[:environment].presence].compact
config.breadcrumbs_logger = [:active_support_logger] config.breadcrumbs_logger = [:active_support_logger]
config.traces_sample_rate = secrets[:enabled] ? 0.001 : nil config.traces_sample_rate = secrets[:enabled] ? 0.001 : nil
config.excluded_exceptions += [ config.delayed_job.report_after_job_retries = true
# Ignore exceptions caught by ActiveJob.retry_on
# https://github.com/getsentry/sentry-ruby/issues/1347
'Excon::Error::BadRequest',
'ActiveStorage::IntegrityError',
'VirusScannerJob::FileNotAnalyzedYetError',
'TitreIdentiteWatermarkJob::WatermarkFileNotScannedYetError',
'APIEntreprise::API::Error::TimedOut'
]
end end

View file

@ -1,13 +1,16 @@
{ {
"dependencies": { "dependencies": {
"@babel/preset-react": "^7.12.13", "@babel/preset-react": "^7.12.13",
"@headlessui/react": "^1.3.0",
"@heroicons/react": "^1.0.1", "@heroicons/react": "^1.0.1",
"@mapbox/mapbox-gl-draw": "^1.2.2", "@mapbox/mapbox-gl-draw": "^1.2.2",
"@popperjs/core": "^2.9.2",
"@rails/actiontext": "^6.0.3", "@rails/actiontext": "^6.0.3",
"@rails/activestorage": "^6.0.3", "@rails/activestorage": "^6.0.3",
"@rails/ujs": "^6.0.3", "@rails/ujs": "^6.0.3",
"@rails/webpacker": "5.1.1", "@rails/webpacker": "5.1.1",
"@reach/combobox": "^0.13.0", "@reach/combobox": "^0.13.0",
"@reach/slider": "^0.15.0",
"@sentry/browser": "^5.15.5", "@sentry/browser": "^5.15.5",
"@tmcw/togeojson": "^4.3.0", "@tmcw/togeojson": "^4.3.0",
"babel-plugin-macros": "^2.8.0", "babel-plugin-macros": "^2.8.0",
@ -28,6 +31,7 @@
"react-intersection-observer": "^8.31.0", "react-intersection-observer": "^8.31.0",
"react-mapbox-gl": "^5.1.1", "react-mapbox-gl": "^5.1.1",
"react-mapbox-gl-draw": "^2.0.4", "react-mapbox-gl-draw": "^2.0.4",
"react-popper": "^2.2.5",
"react-query": "^3.9.7", "react-query": "^3.9.7",
"react-sortable-hoc": "^1.11.0", "react-sortable-hoc": "^1.11.0",
"react_ujs": "^2.6.1", "react_ujs": "^2.6.1",

View file

@ -1,67 +1,59 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang='fr'>
<head> <head>
<title>The change you wanted was rejected (422)</title> <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta content='IE=edge' http-equiv='X-UA-Compatible'>
<meta content='no-cache' name='turbolinks-cache-control'>
<meta content='width=device-width, initial-scale=1' name='viewport'>
<title>
Erreur 422 · demarches-simplifiees.fr
</title>
<style> <style>
body { html,body,div,span,h1,p,a,img,table,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;}
background-color: #EFEFEF; html,body{height:100%;}
color: #2E2F30; body{line-height:1;font-family:"Muli";color:#333333;font-size:16px;line-height:1.42857143;}
text-align: center; html{box-sizing:border-box;}
font-family: arial, sans-serif; *,*::before,*::after{box-sizing:inherit;}
margin: 0; a{text-decoration:none;}
} .page-wrapper{position:relative;min-height:100%;}
.container{padding-left:16px;padding-right:16px;max-width:700px;margin:0 auto;}
div.dialog { .flex{display:flex;}
width: 95%; .flex.align-center{align-items:center;}
max-width: 33em; .header-inner-content{padding-left:16px;padding-right:16px;max-width:1072px;margin:0 auto;display:flex;justify-content:space-between;min-height:72px;}
margin: 4em auto 0; .new-header{background-color:#FFFFFF;}
} .new-header-with-border{border-bottom:1px solid #CCCCCC;}
.header-logo{display:inline-block;margin-right:32px;}
div.dialog > div { .header-logo img{height:34px;}
border: 1px solid #CCC; @media (max-width: 1040px){
border-right-color: #999; .header-logo img{height:18px;}
border-left-color: #999; }
border-bottom-color: #BBB; .new-h1{color:#333333;text-align:center;font-weight:bold;margin-bottom:60px;font-size:41px;margin-top:60px;}
border-top: #B00100 solid 4px; .description{font-size: 18px; margin-bottom: 8px;}
border-top-left-radius: 9px; /*! CSS Used fontfaces */
border-top-right-radius: 9px; @font-face{font-family:"Muli";src:url(/fonts/muli/Muli-Regular.woff) format("woff");font-weight:normal;font-style:normal;}
background-color: white;
padding: 7px 12% 0;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style> </style>
</head> </head>
<body> <body>
<!-- This file lives in public/422.html --> <div class='page-wrapper'>
<div class="dialog"> <div class='new-header new-header-with-border'>
<div> <div class='header-inner-content'>
<h1>The change you wanted was rejected.</h1> <div class='flex align-center'>
<p>Maybe you tried to change something you didn't have access to.</p> <a class="header-logo" href="/"><img alt='demarches-simplifiees.fr' src='/logo-ds.svg'>
</a>
</div>
</div>
</div>
<div class='container'>
<h1 class='new-h1'>Laction demandée a été rejetée.</h1>
<p class='description'>
Pas de panique, cest probablement temporaire.
</p>
<p class='description'>
Essayez de
<a href="javascript:window.location=document.referrer;">recharger la page précédente</a>,
et tout devrait rentrer dans lordre.
</p>
</div> </div>
<p>If you are the application owner check the logs for more information.</p>
</div> </div>
</body> </body>
</html> </html>

View file

@ -300,54 +300,83 @@ describe Experts::AvisController, type: :controller do
end end
context 'without an expert signed in' do context 'without an expert signed in' do
let(:claimant) { create(:instructeur) }
let(:expert) { create(:expert) }
let(:experts_procedure) { create(:experts_procedure, expert: expert, procedure: procedure) }
let(:dossier) { create(:dossier) }
let(:avis) { create(:avis, dossier: dossier, experts_procedure: experts_procedure, claimant: claimant) }
let(:procedure) { dossier.procedure }
describe '#sign_up' do describe '#sign_up' do
let(:invited_email) { 'invited@avis.com' } subject do
let(:claimant) { create(:instructeur) } get :sign_up, params: { id: avis.id, procedure_id: procedure.id, email: avis.expert.email }
let(:expert) { create(:expert) }
let(:experts_procedure) { create(:experts_procedure, expert: expert, procedure: procedure) }
let(:dossier) { create(:dossier) }
let(:procedure) { dossier.procedure }
let!(:avis) { create(:avis, experts_procedure: experts_procedure, claimant: claimant, dossier: dossier) }
let(:invitations_email) { true }
context 'when the expert has already signed up and belongs to the invitation' do
let!(:avis) { create(:avis, dossier: dossier, experts_procedure: experts_procedure, claimant: claimant) }
context 'when the expert is authenticated' do
before do
sign_in(expert.user)
expert.user.update(last_sign_in_at: Time.zone.now)
expert.user.reload
get :sign_up, params: { id: avis.id, procedure_id: procedure.id, email: avis.expert.email }
end
it { is_expected.to redirect_to expert_avis_url(avis.procedure, avis) }
end
context 'when the expert is not authenticated' do
before do
sign_in(expert.user)
expert.user.update(last_sign_in_at: Time.zone.now)
expert.user.reload
sign_out(expert.user)
get :sign_up, params: { id: avis.id, procedure_id: procedure.id, email: avis.expert.email }
end
it { is_expected.to redirect_to new_user_session_url }
end
end end
context 'when the expert has already signed up / is authenticated and does not belong to the invitation' do context 'when the expert hasnt signed up yet' do
let(:expert) { create(:expert) } before { expert.user.update(last_sign_in_at: nil) }
let!(:avis) { create(:avis, email: invited_email, dossier: dossier, experts_procedure: experts_procedure) }
before do it { is_expected.to have_http_status(:success) }
sign_in(expert.user) end
get :sign_up, params: { id: avis.id, procedure_id: procedure.id, email: avis.expert.email }
context 'when the expert has already signed up' do
before { expert.user.update(last_sign_in_at: Time.zone.now) }
context 'and the expert belongs to the invitation' do
context 'and the expert is authenticated' do
before { sign_in(expert.user) }
it { is_expected.to redirect_to expert_avis_url(avis.procedure, avis) }
end
context 'and the expert is not authenticated' do
before { sign_out(expert.user) }
it { is_expected.to redirect_to new_user_session_url }
end
end end
# redirected to dossier but then the instructeur gonna be banished ! context 'and the expert does not belong to the invitation' do
it { is_expected.to redirect_to expert_avis_url(avis.procedure, avis) } let(:avis) { create(:avis, email: 'another_expert@avis.com', dossier: dossier, experts_procedure: experts_procedure) }
before { sign_in(expert.user) }
# redirected to dossier but then the instructeur gonna be banished !
it { is_expected.to redirect_to expert_avis_url(avis.procedure, avis) }
end
end
end
describe '#update_expert' do
subject do
post :update_expert, params: {
id: avis.id,
procedure_id: procedure.id,
email: avis.expert.email,
user: {
password: 'my-s3cure-p4ssword'
}
}
end
context 'when the expert hasnt signed up yet' do
before { expert.user.update(last_sign_in_at: nil) }
it 'saves the expert new password' do
subject
expect(expert.user.reload.valid_password?('my-s3cure-p4ssword')).to be true
end
it { is_expected.to redirect_to expert_all_avis_path }
end
context 'when the expert has already signed up' do
before { expert.user.update(last_sign_in_at: Time.zone.now) }
it 'doesnt change the expert password' do
subject
expect(expert.user.reload.valid_password?('my-s3cure-p4ssword')).to be false
end
it { is_expected.to redirect_to new_user_session_url }
end end
end end
end end

View file

@ -25,7 +25,6 @@ describe Instructeurs::ArchivesController, type: :controller do
it 'displays archives' do it 'displays archives' do
get :index, { params: { procedure_id: procedure1.id } } get :index, { params: { procedure_id: procedure1.id } }
expect(assigns(:nb_dossiers_termines).size).to eq(8)
expect(assigns(:archives)).to eq([archive1]) expect(assigns(:archives)).to eq([archive1])
end end
end end

View file

@ -13,6 +13,7 @@ describe Users::CommencerController, type: :controller do
expect(subject.status).to eq(200) expect(subject.status).to eq(200)
expect(subject).to render_template('show') expect(subject).to render_template('show')
expect(assigns(:procedure)).to eq published_procedure expect(assigns(:procedure)).to eq published_procedure
expect(assigns(:revision)).to eq published_procedure.published_revision
end end
end end
@ -43,6 +44,7 @@ describe Users::CommencerController, type: :controller do
expect(subject.status).to eq(200) expect(subject.status).to eq(200)
expect(subject).to render_template('show') expect(subject).to render_template('show')
expect(assigns(:procedure)).to eq draft_procedure expect(assigns(:procedure)).to eq draft_procedure
expect(assigns(:revision)).to eq draft_procedure.draft_revision
end end
end end

View file

@ -11,16 +11,30 @@ feature 'Inviting an expert:' do
let(:avis) { create(:avis, dossier: dossier, claimant: instructeur, experts_procedure: experts_procedure, confidentiel: true) } let(:avis) { create(:avis, dossier: dossier, claimant: instructeur, experts_procedure: experts_procedure, confidentiel: true) }
context 'when I dont already have an account' do context 'when I dont already have an account' do
scenario 'I can sign up' do let(:password) { 'This is an expert password' }
before 'Signing up' do
visit sign_up_expert_avis_path(avis.dossier.procedure, avis, email: avis.expert.email) visit sign_up_expert_avis_path(avis.dossier.procedure, avis, email: avis.expert.email)
expect(page).to have_field('Email', with: avis.expert.email, disabled: true) expect(page).to have_field('Email', with: avis.expert.email, disabled: true)
fill_in 'Mot de passe', with: 'This is a very complicated password !' fill_in 'Mot de passe', with: password
click_on 'Créer un compte' click_on 'Créer un compte'
end
scenario 'I can see the avis after signing up' do
expect(page).to have_current_path(expert_all_avis_path) expect(page).to have_current_path(expert_all_avis_path)
expect(page).to have_text('1 avis à donner') expect(page).to have_text('1 avis à donner')
end end
scenario 'I can sign-in again afterwards' do
click_on 'Se déconnecter'
visit new_user_session_path
sign_in_with avis.expert.email, password
expect(page).to have_content('Connecté(e).')
expect(page).to have_current_path(dossiers_path) # Ideally we'd want `expert_all_avis_path` instead
end
end end
context 'when I already have an existing account' do context 'when I already have an existing account' do

View file

@ -5,6 +5,7 @@ RSpec.describe 'commencer/show.html.haml', type: :view do
before do before do
assign(:procedure, procedure) assign(:procedure, procedure)
assign(:revision, procedure.published_revision)
if user if user
sign_in user sign_in user
end end

View file

@ -5,6 +5,7 @@ describe 'new_administrateur/procedures/show.html.haml', type: :view do
before do before do
assign(:procedure, procedure) assign(:procedure, procedure)
assign(:procedure_lien, commencer_url(path: procedure.path)) assign(:procedure_lien, commencer_url(path: procedure.path))
assign(:procedure_lien_test, commencer_test_url(path: procedure.path))
allow(view).to receive(:current_administrateur).and_return(procedure.administrateurs.first) allow(view).to receive(:current_administrateur).and_return(procedure.administrateurs.first)
end end

View file

@ -1005,6 +1005,11 @@
enabled "2.0.x" enabled "2.0.x"
kuler "^2.0.0" kuler "^2.0.0"
"@headlessui/react@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.3.0.tgz#62287c92604923e5dfb394483e5ec2463e1baea6"
integrity sha512-2gqTO6BQ3Jr8vDX1B67n1gl6MGKTt6DBmR+H0qxwj0gTMnR2+Qpktj8alRWxsZBODyOiBb77QSQpE/6gG3MX4Q==
"@heroicons/react@^1.0.1": "@heroicons/react@^1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd" resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd"
@ -1567,6 +1572,11 @@
dependencies: dependencies:
"@types/node" ">= 8" "@types/node" ">= 8"
"@popperjs/core@^2.9.2":
version "2.9.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
"@rails/actiontext@^6.0.3": "@rails/actiontext@^6.0.3":
version "6.0.3" version "6.0.3"
resolved "https://registry.yarnpkg.com/@rails/actiontext/-/actiontext-6.0.3.tgz#71dacd49df7c16a363e20aa89a53efcad8fcecde" resolved "https://registry.yarnpkg.com/@rails/actiontext/-/actiontext-6.0.3.tgz#71dacd49df7c16a363e20aa89a53efcad8fcecde"
@ -1638,6 +1648,14 @@
"@reach/utils" "0.13.0" "@reach/utils" "0.13.0"
tslib "^2.0.0" tslib "^2.0.0"
"@reach/auto-id@0.15.0":
version "0.15.0"
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.15.0.tgz#f46afebfc140b2099b95c7aec1f049d167d3833d"
integrity sha512-iACaCcZeqAhDf4OOwJpmHHS/LaRj9z3Ip8JmlhpCrFWV2YOIiiZk42amlBZX6CKH66Md+eriYZQk3TyAjk6Oxg==
dependencies:
"@reach/utils" "0.15.0"
tslib "^2.1.0"
"@reach/combobox@^0.13.0": "@reach/combobox@^0.13.0":
version "0.13.0" version "0.13.0"
resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.13.0.tgz#16bbdae42fc84f28025e6aa8552f1035cb15a365" resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.13.0.tgz#16bbdae42fc84f28025e6aa8552f1035cb15a365"
@ -1694,6 +1712,17 @@
prop-types "^15.7.2" prop-types "^15.7.2"
tslib "^2.0.0" tslib "^2.0.0"
"@reach/slider@^0.15.0":
version "0.15.0"
resolved "https://registry.yarnpkg.com/@reach/slider/-/slider-0.15.0.tgz#6b871e913bf10aa8f1ee5a4090478deff71d9c39"
integrity sha512-8dYJiclMpjHJ/wc0pIUxbwf8d3oO9ojnbMO31HPdqCqa+2KK8MoYzVdD32VsEa+gznHjoG9c05PPsrAjQd1yYg==
dependencies:
"@reach/auto-id" "0.15.0"
"@reach/utils" "0.15.0"
prop-types "^15.7.2"
tiny-warning "^1.0.3"
tslib "^2.1.0"
"@reach/utils@0.13.0": "@reach/utils@0.13.0":
version "0.13.0" version "0.13.0"
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.13.0.tgz#2da775a910d8894bb34e1e94fe95842674f71844" resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.13.0.tgz#2da775a910d8894bb34e1e94fe95842674f71844"
@ -1703,6 +1732,14 @@
tslib "^2.0.0" tslib "^2.0.0"
warning "^4.0.3" warning "^4.0.3"
"@reach/utils@0.15.0":
version "0.15.0"
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.15.0.tgz#5b183d668f9bb900b2dec7a33c028a2a828d27b2"
integrity sha512-JHHN7T5ucFiuQbqkgv8ECbRWKfRiJxrO/xHR3fHf+f2C7mVs/KkJHhYtovS1iEapR4silygX9PY0+QUmHPOTYw==
dependencies:
tiny-warning "^1.0.3"
tslib "^2.1.0"
"@sentry/browser@^5.15.5": "@sentry/browser@^5.15.5":
version "5.15.5" version "5.15.5"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.5.tgz#d9a51f1388581067b50d30ed9b1aed2cbb333a36" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.5.tgz#d9a51f1388581067b50d30ed9b1aed2cbb333a36"
@ -10436,6 +10473,11 @@ react-dom@^17.0.1:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler "^0.20.1" scheduler "^0.20.1"
react-fast-compare@^3.0.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-intersection-observer@^8.31.0: react-intersection-observer@^8.31.0:
version "8.31.0" version "8.31.0"
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.31.0.tgz#0ed21aaf93c4c0475b22b0ccaba6169076d01605" resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.31.0.tgz#0ed21aaf93c4c0475b22b0ccaba6169076d01605"
@ -10462,6 +10504,14 @@ react-mapbox-gl@^5.1.1:
deep-equal "1.0.1" deep-equal "1.0.1"
supercluster "^7.0.0" supercluster "^7.0.0"
react-popper@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96"
integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==
dependencies:
react-fast-compare "^3.0.1"
warning "^4.0.2"
react-query@^3.9.7: react-query@^3.9.7:
version "3.9.7" version "3.9.7"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.9.7.tgz#324c697f418827c129c8c126d233c6052bb1e35e" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.9.7.tgz#324c697f418827c129c8c126d233c6052bb1e35e"
@ -12052,6 +12102,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinyqueue@^2.0.3: tinyqueue@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08"
@ -12208,6 +12263,11 @@ tslib@^2.0.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
tslib@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tsutils@^3.17.1: tsutils@^3.17.1:
version "3.17.1" version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
@ -12680,7 +12740,7 @@ wait-port@^0.2.2:
commander "^3.0.2" commander "^3.0.2"
debug "^4.1.1" debug "^4.1.1"
warning@^4.0.3: warning@^4.0.2, warning@^4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==