Merge pull request #6777 from betagouv/main

2021-12-23-01
This commit is contained in:
mfo 2022-01-03 14:27:29 +01:00 committed by GitHub
commit 7c4e26d1b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 2971 additions and 4369 deletions

View file

@ -415,7 +415,7 @@ GEM
railties (>= 4)
request_store (~> 1.0)
logstash-event (1.2.02)
loofah (2.12.0)
loofah (2.13.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -432,7 +432,7 @@ GEM
mini_magick (4.11.0)
mini_mime (1.1.2)
mini_portile2 (2.6.1)
minitest (5.14.4)
minitest (5.15.0)
momentjs-rails (2.20.1)
railties (>= 3.1)
msgpack (1.4.2)

View file

@ -12,4 +12,9 @@ $contact-padding: $default-space * 2;
.hidden {
display: none;
}
ul {
margin-bottom: $default-space;
}
}

View file

@ -4,6 +4,7 @@
html,
body {
height: 100%;
background-color: $white;
}
html {

View file

@ -565,3 +565,8 @@
margin-bottom: 16px;
}
}
input::placeholder,
textarea::placeholder {
color: $dark-grey;
}

View file

@ -14,14 +14,15 @@
vertical-align: middle;
}
th {
th,
td.libelle {
text-align: left;
font-weight: bold;
padding: (3 * $default-spacer) 2px;
}
th.padded {
padding-left: (2 * $default-spacer);
&.padded {
padding-left: (2 * $default-spacer);
}
}
&.hoverable {
@ -38,7 +39,8 @@
border-top: none;
}
th {
th,
td.libelle {
@include vertical-padding($default-spacer);
&.header-section {

View file

@ -125,7 +125,9 @@ module Administrateurs
end
if procedure.routee?
groupe_instructeur.instructeurs << instructeurs
instructeurs.each do |instructeur|
groupe_instructeur.add(instructeur)
end
GroupeInstructeurMailer
.add_instructeurs(groupe_instructeur, instructeurs, current_user.email)
@ -139,7 +141,9 @@ module Administrateurs
else
if instructeurs.present?
procedure.defaut_groupe_instructeur.instructeurs << instructeurs
instructeurs.each do |instructeur|
procedure.defaut_groupe_instructeur.add(instructeur)
end
flash[:notice] = "Les instructeurs ont bien été affectés à la démarche"
end
end
@ -158,7 +162,7 @@ module Administrateurs
else
instructeur = Instructeur.find(instructeur_id)
if procedure.routee?
if instructeur.remove_from_groupe_instructeur(groupe_instructeur)
if groupe_instructeur.remove(instructeur)
flash[:notice] = "Linstructeur « #{instructeur.email} » a été retiré du groupe."
GroupeInstructeurMailer
.remove_instructeur(groupe_instructeur, instructeur, current_user.email)
@ -167,7 +171,7 @@ module Administrateurs
flash[:alert] = "Linstructeur « #{instructeur.email} » nest pas dans le groupe."
end
else
if instructeur.remove_from_groupe_instructeur(procedure.defaut_groupe_instructeur)
if procedure.defaut_groupe_instructeur.remove(instructeur)
flash[:notice] = "Linstructeur a bien été désaffecté de la démarche"
else
flash[:alert] = "Linstructeur nest pas affecté à la démarche"

View file

@ -20,7 +20,7 @@ module Instructeurs
if groupe_instructeur.instructeurs.include?(instructeur)
flash[:alert] = "Linstructeur « #{instructeur_email} » est déjà dans le groupe."
else
groupe_instructeur.instructeurs << instructeur
groupe_instructeur.add(instructeur)
flash[:notice] = "Linstructeur « #{instructeur_email} » a été affecté au groupe."
GroupeInstructeurMailer
.add_instructeurs(groupe_instructeur, [instructeur], current_user.email)
@ -35,7 +35,7 @@ module Instructeurs
flash[:alert] = "Suppression impossible : il doit y avoir au moins un instructeur dans le groupe"
else
instructeur = Instructeur.find(instructeur_id)
if instructeur.remove_from_groupe_instructeur(groupe_instructeur)
if groupe_instructeur.remove(instructeur)
flash[:notice] = "Linstructeur « #{instructeur.email} » a été retiré du groupe."
GroupeInstructeurMailer
.remove_instructeur(groupe_instructeur, instructeur, current_user.email)

View file

@ -216,11 +216,13 @@ module Instructeurs
def email_notifications
@procedure = procedure
@assign_to = assign_to
@assign_to = assign_tos.first
end
def update_email_notifications
assign_to.update!(assign_to_params)
assign_tos.each do |assign_to|
assign_to.update!(assign_to_params)
end
flash.notice = 'Vos notifications sont enregistrées.'
redirect_to instructeur_procedure_path(procedure)
end
@ -290,10 +292,6 @@ module Instructeurs
@exports = Export.find_for_groupe_instructeurs(groupe_instructeur_ids)
end
def assign_to
current_instructeur.assign_to.joins(:groupe_instructeur).find_by(groupe_instructeurs: { procedure: procedure })
end
def assign_tos
@assign_tos ||= current_instructeur
.assign_to

View file

@ -0,0 +1,50 @@
module Manager
class ZonesController < Manager::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
def find_resource(param)
if param == "nil"
NullZone.new
else
Zone.find(param)
end
end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -14,9 +14,7 @@ module Users
def update_email
requested_user = User.find_by(email: requested_email)
if requested_user.present?
current_user.ask_for_merge(requested_user)
if requested_user.present? && current_user.ask_for_merge(requested_user)
current_user.update(unconfirmed_email: nil)
flash.notice = t('devise.registrations.update_needs_confirmation')

View file

@ -16,6 +16,7 @@ class ProcedureDashboard < Administrate::BaseDashboard
id: Field::Number.with_options(searchable: true),
libelle: Field::String,
description: Field::String,
zone: Field::BelongsTo,
lien_site_web: Field::String, # TODO: use Field::Url when administrate-v0.12 will be released
organisation: Field::String,
direction: Field::String,
@ -47,6 +48,7 @@ class ProcedureDashboard < Administrate::BaseDashboard
:id,
:created_at,
:libelle,
:zone,
:service,
:dossiers,
:published_at,
@ -64,6 +66,7 @@ class ProcedureDashboard < Administrate::BaseDashboard
:lien_site_web,
:organisation,
:direction,
:zone,
:service,
:created_at,
:updated_at,

View file

@ -0,0 +1,53 @@
require "administrate/base_dashboard"
class ZoneDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
procedures: Field::HasMany,
id: Field::Number,
acronym: Field::String,
label: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = [:procedures, :id, :acronym, :label].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = [:procedures, :id, :acronym, :label, :created_at, :updated_at].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = [:procedures, :acronym, :label].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how zones are displayed
# across all pages of the admin dashboard.
#
def display_resource(zone)
"Zone #{zone.label}"
end
end

View file

@ -1,24 +0,0 @@
import React, { Suspense, lazy } from 'react';
import PropTypes from 'prop-types';
const Loader = () => <div className="spinner left" />;
function LazyLoad({ component: Component, ...props }) {
return (
<Suspense fallback={<Loader />}>
<Component {...props} />
</Suspense>
);
}
LazyLoad.propTypes = {
component: PropTypes.object
};
export default function Loadable(loader) {
const LazyComponent = lazy(loader);
return function PureComponent(props) {
return <LazyLoad component={LazyComponent} {...props} />;
};
}

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/Chartkick'));

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/ComboAdresseSearch'));

View file

@ -1,5 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() =>
import('../components/ComboAnnuaireEducationSearch')
);

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/ComboCommunesSearch'));

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/ComboDepartementsSearch'));

View file

@ -1,5 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() =>
import('../components/ComboMultipleDropdownList')
);

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/ComboPaysSearch'));

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/ComboRegionsSearch'));

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/MapEditor'));

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/MapReader'));

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/Trix'));

View file

@ -1,3 +0,0 @@
import Loadable from '../components/Loadable';
export default Loadable(() => import('../components/TypesDeChampEditor'));

View file

@ -2,7 +2,6 @@ import '../shared/polyfills';
import Rails from '@rails/ujs';
import * as ActiveStorage from '@rails/activestorage';
import 'whatwg-fetch'; // window.fetch polyfill
import ReactRailsUJS from 'react_ujs';
import '../shared/page-update-event';
import '../shared/activestorage/ujs';
@ -48,6 +47,38 @@ import {
showNewAccountPasswordConfirmation
} from '../new_design/fc-fusion';
import {
registerReactComponents,
Loadable
} from '../shared/register-react-components';
registerReactComponents({
Chartkick: Loadable(() => import('../components/Chartkick')),
ComboAdresseSearch: Loadable(() =>
import('../components/ComboAdresseSearch')
),
ComboAnnuaireEducationSearch: Loadable(() =>
import('../components/ComboAnnuaireEducationSearch')
),
ComboCommunesSearch: Loadable(() =>
import('../components/ComboCommunesSearch')
),
ComboDepartementsSearch: Loadable(() =>
import('../components/ComboDepartementsSearch')
),
ComboMultipleDropdownList: Loadable(() =>
import('../components/ComboMultipleDropdownList')
),
ComboPaysSearch: Loadable(() => import('../components/ComboPaysSearch')),
ComboRegionsSearch: Loadable(() =>
import('../components/ComboRegionsSearch')
),
MapEditor: Loadable(() => import('../components/MapEditor')),
MapReader: Loadable(() => import('../components/MapReader')),
Trix: Loadable(() => import('../components/Trix')),
TypesDeChampEditor: Loadable(() => import('../components/TypesDeChampEditor'))
});
// This is the global application namespace where we expose helpers used from rails views
const DS = {
fire: (eventName, data) => Rails.fire(document, eventName, data),
@ -69,7 +100,3 @@ ActiveStorage.start();
// Expose globals
window.DS = window.DS || DS;
// eslint-disable-next-line no-undef, react-hooks/rules-of-hooks
ReactRailsUJS.useContext(require.context('loaders', true));
addEventListener('ds:page:update', ReactRailsUJS.handleMount);

View file

@ -0,0 +1,108 @@
import React, { Suspense, lazy, createElement } from 'react';
import { render } from 'react-dom';
// This attribute holds the name of component which should be mounted
// example: `data-react-class="MyApp.Items.EditForm"`
const CLASS_NAME_ATTR = 'data-react-class';
// This attribute holds JSON stringified props for initializing the component
// example: `data-react-props="{\"item\": { \"id\": 1, \"name\": \"My Item\"} }"`
const PROPS_ATTR = 'data-react-props';
// A unique identifier to identify a node
const CACHE_ID_ATTR = 'data-react-cache-id';
const CLASS_NAME_SELECTOR = `[${CLASS_NAME_ATTR}]`;
// helper method for the mount and unmount methods to find the
// `data-react-class` DOM elements
function findDOMNodes(searchSelector) {
const [selector, parent] = getSelector(searchSelector);
return parent.querySelectorAll(selector);
}
function getSelector(searchSelector) {
switch (typeof searchSelector) {
case 'undefined':
return [CLASS_NAME_SELECTOR, document];
case 'object':
return [CLASS_NAME_SELECTOR, searchSelector];
case 'string':
return [
['', ' ']
.map(
(separator) => `${searchSelector}${separator}${CLASS_NAME_SELECTOR}`
)
.join(', '),
document
];
}
}
class ReactComponentRegistry {
#cache = {};
#components;
constructor(components) {
this.#components = components;
}
getConstructor(className) {
return this.#components[className];
}
mountComponents(searchSelector) {
const nodes = findDOMNodes(searchSelector);
for (const node of nodes) {
const className = node.getAttribute(CLASS_NAME_ATTR);
const ComponentClass = this.getConstructor(className);
const propsJson = node.getAttribute(PROPS_ATTR);
const props = propsJson && JSON.parse(propsJson);
const cacheId = node.getAttribute(CACHE_ID_ATTR);
if (!ComponentClass) {
const message = `Cannot find component: "${className}"`;
console?.log(
`%c[react-rails] %c${message} for element`,
'font-weight: bold',
'',
node
);
throw new Error(
`${message}. Make sure your component is available to render.`
);
} else {
let component = this.#cache[cacheId];
if (!component) {
this.#cache[cacheId] = component = createElement(
ComponentClass,
props
);
}
render(component, node);
}
}
}
}
const Loader = () => <div className="spinner left" />;
export function Loadable(loader) {
const LazyComponent = lazy(loader);
return function PureComponent(props) {
return (
<Suspense fallback={<Loader />}>
<LazyComponent {...props} />
</Suspense>
);
};
}
export function registerReactComponents(components) {
const registry = new ReactComponentRegistry(components);
addEventListener('ds:page:update', () => registry.mountComponents());
}

View file

@ -7,12 +7,19 @@ if (enabled) {
const trackerUrl = `${url}piwik.php`;
const jsUrl = `${url}piwik.js`;
//
// Configure Matomo analytics
//
window._paq.push(['setCookieDomain', '*.www.demarches-simplifiees.fr']);
window._paq.push(['setDomains', ['*.www.demarches-simplifiees.fr']]);
// Dont store any cookies or send any tracking request when the "Do Not Track" browser setting is enabled.
window._paq.push(['setDoNotTrack', true]);
// When enabling external link tracking, consider that it will also report links to attachments.
// Youll want to exclude links to attachments from being tracked for instance using Matomo's
// `setCustomRequestProcessing` callback.
// window._paq.push(['enableLinkTracking']);
window._paq.push(['trackPageView']);
window._paq.push(['enableLinkTracking']);
// Load script from Matomo
window._paq.push(['setTrackerUrl', trackerUrl]);

View file

@ -24,4 +24,21 @@ class GroupeInstructeur < ApplicationRecord
scope :without_group, -> (group) { where.not(id: group) }
scope :for_api_v2, -> { includes(procedure: [:administrateurs]) }
def add(instructeur)
return if in?(instructeur.groupe_instructeurs)
default_notification_settings = instructeur.notification_settings(procedure_id)
instructeur.assign_to.create(groupe_instructeur: self, **default_notification_settings)
end
def remove(instructeur)
return if !in?(instructeur.groupe_instructeurs)
instructeur.groupe_instructeurs.destroy(self)
instructeur.follows
.joins(:dossier)
.where(dossiers: { groupe_instructeur: self })
.update_all(unfollowed_at: Time.zone.now)
end
end

View file

@ -81,14 +81,13 @@ class Instructeur < ApplicationRecord
end
end
def remove_from_groupe_instructeur(groupe_instructeur)
if groupe_instructeur.in?(groupe_instructeurs)
groupe_instructeurs.destroy(groupe_instructeur)
follows
.joins(:dossier)
.where(dossiers: { groupe_instructeur: groupe_instructeur })
.update_all(unfollowed_at: Time.zone.now)
end
NOTIFICATION_SETTINGS = [:daily_email_notifications_enabled, :instant_email_dossier_notifications_enabled, :instant_email_message_notifications_enabled, :weekly_email_notifications_enabled]
def notification_settings(procedure_id)
assign_to
.joins(:groupe_instructeur)
.find_by(groupe_instructeurs: { procedure_id: procedure_id })
&.slice(*NOTIFICATION_SETTINGS) || {}
end
def last_week_overview

31
app/models/null_zone.rb Normal file
View file

@ -0,0 +1,31 @@
class NullZone
include ActiveModel::Model
def procedures
Procedure.where(zone: nil).where.not(published_at: nil).order(published_at: :desc)
end
def self.reflect_on_association(association)
OpenStruct.new(class_name: "Procedure") if association == :procedures
end
def label
"non renseignée"
end
def id
-1
end
def acronym
"NA"
end
def created_at
"NA"
end
def updated_at
"NA"
end
end

View file

@ -63,6 +63,8 @@ class User < ApplicationRecord
before_validation -> { sanitize_email(:email) }
validate :does_not_merge_on_self, if: :requested_merge_into_id_changed?
def validate_password_complexity?
administrateur?
end
@ -223,12 +225,21 @@ class User < ApplicationRecord
end
def ask_for_merge(requested_user)
update(requested_merge_into: requested_user)
UserMailer.ask_for_merge(self, requested_user.email).deliver_later
if update(requested_merge_into: requested_user)
UserMailer.ask_for_merge(self, requested_user.email).deliver_later
return true
else
return false
end
end
private
def does_not_merge_on_self
return if requested_merge_into_id != self.id
errors.add(:requested_merge_into, :same)
end
def link_invites!
Invite.where(email: email).update_all(user_id: id)
end

View file

@ -10,4 +10,5 @@
#
class Zone < ApplicationRecord
validates :acronym, presence: true, uniqueness: true
has_many :procedures, -> { order(published_at: :desc) }, inverse_of: :zone
end

View file

@ -1,6 +1,5 @@
.header-search{ role: 'search' }
= form_tag "#{search_endpoint}", method: :get, class: "form" do
= label_tag :q, "Numéro de dossier", class: 'hidden'
= text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: "Rechercher un dossier"
= text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: "Rechercher un dossier", title: "Rechercher un dossier"
%button{ title: "Rechercher" }
= image_tag "icons/search-blue.svg", alt: 'Rechercher', 'aria-hidden':'true', width: 24, height: 24, loading: 'lazy'

View file

@ -0,0 +1,70 @@
<%#
# Index
This view is the template for the index page.
It is responsible for rendering the search bar, header and pagination.
It renders the `_table` partial to display details about the resources.
## Local variables:
- `page`:
An instance of [Administrate::Page::Collection][1].
Contains helper methods to help display a table,
and knows which attributes should be displayed in the resource's table.
- `resources`:
An instance of `ActiveRecord::Relation` containing the resources
that match the user's search criteria.
By default, these resources are passed to the table partial to be displayed.
- `search_term`:
A string containing the term the user has searched for, if any.
- `show_search_bar`:
A boolean that determines if the search bar should be shown.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
%>
<% content_for(:title) do %>
<%= display_resource_name(page.resource_name) %>
<% end %>
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title">
<%= content_for(:title) %>
</h1>
<div>
<%= link_to "Procedures sans zone renseignée", manager_zone_path(id: 'nil') %>
</div>
<% if show_search_bar %>
<%= render(
"search",
search_term: search_term,
resource_name: display_resource_name(page.resource_name)
) %>
<% end %>
<div>
<%= link_to(
t(
"administrate.actions.new_resource",
name: display_resource_name(page.resource_name, singular: true).downcase
),
[:new, namespace, page.resource_path.to_sym],
class: "button",
) if valid_action?(:new) && show_action?(:new, new_resource) %>
</div>
</header>
<section class="main-content__body main-content__body--flush">
<%= render(
"collection",
collection_presenter: page,
collection_field_name: resource_name,
page: page,
resources: resources,
table_title: "page-title"
) %>
<%= paginate resources, param_name: '_page' %>
</section>

View file

@ -1,19 +1,19 @@
- champs.reject(&:exclude_from_view?).each do |c|
- if c.type_champ == TypeDeChamp.type_champs.fetch(:repetition)
%tr
%th.libelle.repetition{ colspan: 3 }
%td.libelle.repetition{ colspan: 3 }
= "#{c.libelle} :"
- c.rows.each do |champs|
= render partial: "shared/dossiers/champ_row", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile, repetition: true }
%tr
%th{ colspan: 4 }
%td.libelle{ colspan: 4 }
- else
%tr
- if c.type_champ == TypeDeChamp.type_champs.fetch(:header_section)
%th.header-section{ colspan: 3 }
= c.libelle
- else
%th.libelle{ class: repetition ? 'padded' : '' }
%td.libelle{ class: repetition ? 'padded' : '' }
= "#{c.libelle} :"
%td.rich-text
%div{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) }

View file

@ -1,7 +1,7 @@
%table.table.vertical.dossier-champs
%tbody
- if dossier.show_groupe_instructeur_details?
%th= dossier.procedure.routing_criteria_name
%td.libelle= dossier.procedure.routing_criteria_name
%td{ class: highlight_if_unseen_class(demande_seen_at, dossier.groupe_instructeur_updated_at) }= dossier.groupe_instructeur.label
%td.updated-at
%span{ class: highlight_if_unseen_class(demande_seen_at, dossier.groupe_instructeur_updated_at) }

View file

@ -5,59 +5,59 @@
%td= t('warning_for_private_info', scope: 'views.shared.dossiers.identite_entreprise', etablissement: raison_sociale_or_name(etablissement))
- else
%tr
%th.libelle Dénomination :
%td.libelle Dénomination :
%td= raison_sociale_or_name(etablissement)
%tr
%th.libelle SIRET :
%td.libelle SIRET :
%td= etablissement.siret
- if etablissement.siret != etablissement.entreprise.siret_siege_social
%tr
%th.libelle SIRET du siège social:
%td.libelle SIRET du siège social:
%td= etablissement.entreprise.siret_siege_social
%tr
%th.libelle Forme juridique :
%td.libelle Forme juridique :
%td= sanitize(etablissement.entreprise.forme_juridique)
%tr
%th.libelle Libellé NAF :
%td.libelle Libellé NAF :
%td= etablissement.libelle_naf
%tr
%th.libelle Code NAF :
%td.libelle Code NAF :
%td= etablissement.naf
%tr
%th.libelle Date de création :
%td.libelle Date de création :
%td= try_format_date(etablissement.entreprise.date_creation)
- if profile == 'instructeur'
%tr
%th.libelle
%td.libelle
Effectif mensuel
= try_format_mois_effectif(etablissement)
(URSSAF) :
%td= etablissement.entreprise_effectif_mensuel
%tr
%th.libelle
%td.libelle
Effectif moyen annuel
= etablissement.entreprise_effectif_annuel_annee
(URSSAF) :
%td= etablissement.entreprise_effectif_annuel
%tr
%th.libelle Effectif de l'organisation (INSEE) :
%td.libelle Effectif de l'organisation (INSEE) :
%td
= effectif(etablissement)
%tr
%th.libelle Numéro de TVA intracommunautaire :
%td.libelle Numéro de TVA intracommunautaire :
%td= etablissement.entreprise.numero_tva_intracommunautaire
%tr
%th.libelle Adresse :
%td.libelle Adresse :
%td
- etablissement.adresse.split("\n").each do |line|
= line
%br
%tr
%th.libelle Capital social :
%td.libelle Capital social :
%td= pretty_currency(etablissement.entreprise.capital_social)
%tr
%th.libelle Chiffre daffaires :
%td.libelle Chiffre daffaires :
%td
- if profile == 'instructeur'
%details
@ -83,7 +83,7 @@
= render partial: 'shared/dossiers/identite_entreprise_bilan_detail',
locals: { libelle: 'Besoin en fonds de roulement', key: 'besoin_en_fonds_de_roulement', etablissement: etablissement }
%tr
%th.libelle
%td.libelle
Chiffres financiers clés (Banque de France)
= "en #{pretty_currency_unit(etablissement.entreprise_bilans_bdf_monnaie)} :"
@ -105,12 +105,12 @@
= link_to "au format ods", bilans_bdf_instructeur_dossier_path(procedure_id: @dossier.procedure.id, dossier_id: @dossier.id, format: 'ods')
- else
%tr
%th.libelle
%td.libelle
Bilans Banque de France :
%td Les 3 derniers bilans connus de votre entreprise par la Banque de France ont été joints à votre dossier.
- if etablissement.entreprise_attestation_sociale.attached?
%tr
%th.libelle Attestation sociale :
%td.libelle Attestation sociale :
- if profile == 'instructeur'
%td= link_to "Consulter l'attestation", url_for(etablissement.entreprise_attestation_sociale)
- else
@ -118,7 +118,7 @@
- if etablissement.entreprise_attestation_fiscale.attached?
%tr
%th.libelle Attestation fiscale :
%td.libelle Attestation fiscale :
- if profile == 'instructeur'
%td= link_to "Consulter l'attestation", url_for(etablissement.entreprise_attestation_fiscale)
- else
@ -126,22 +126,22 @@
- if etablissement.association?
%tr
%th.libelle Numéro RNA :
%td.libelle Numéro RNA :
%td= etablissement.association_rna
%tr
%th.libelle Titre :
%td.libelle Titre :
%td= etablissement.association_titre
%tr
%th.libelle Objet :
%td.libelle Objet :
%td= etablissement.association_objet
%tr
%th.libelle Date de création :
%td.libelle Date de création :
%td= try_format_date(etablissement.association_date_creation)
%tr
%th.libelle Date de publication :
%td.libelle Date de publication :
%td= try_format_date(etablissement.association_date_publication)
%tr
%th.libelle Date de déclaration :
%td.libelle Date de déclaration :
%td= try_format_date(etablissement.association_date_declaration)
%p

View file

@ -1,5 +1,5 @@
%tr
%th.libelle
%td.libelle
= "#{libelle} :"
%td
%details

View file

@ -1,15 +1,15 @@
%table.table.vertical.dossier-champs
%tbody
%tr
%th.libelle Civilité :
%td.libelle Civilité :
%td= individual.gender
%tr
%th.libelle Prénom :
%td.libelle Prénom :
%td= individual.prenom
%tr
%th.libelle Nom :
%td.libelle Nom :
%td= individual.nom
- if individual.birthdate.present?
%tr
%th.libelle Date de naissance :
%td.libelle Date de naissance :
%td= try_format_date(individual.birthdate)

View file

@ -1,11 +1,11 @@
%table.table.vertical.dossier-champs
%tbody
%tr
%th.libelle Déposé le :
%td.libelle Déposé le :
%td= l(dossier.depose_at, format: '%d %B %Y')
- if dossier.justificatif_motivation.attached?
%tr
%th.libelle Justificatif :
%td.libelle Justificatif :
%td
.action
= render partial: 'shared/attachment/show', locals: { attachment: dossier.justificatif_motivation.attachment }

View file

@ -1,5 +1,5 @@
%table.table.vertical.dossier-champs
%tbody
%tr
%th.libelle Email :
%td.libelle Email :
%td= user_deleted ? "#{email} (lusager a supprimé son compte)" : email

View file

@ -3,7 +3,7 @@
- placeholder = t('views.shared.dossiers.messages.form.write_message_to_administration_placeholder')
- if instructeur_signed_in? || administrateur_signed_in?
- placeholder = t('views.shared.dossiers.messages.form.write_message_placeholder')
= f.text_area :body, rows: 5, placeholder: placeholder, required: true, class: 'message-textarea persisted-input'
= f.text_area :body, rows: 5, placeholder: placeholder, title: placeholder, required: true, class: 'message-textarea persisted-input'
.flex.justify-between.wrap
- disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false
%div

View file

@ -19,12 +19,11 @@
= label_tag :email do
Email
%span.mandatory *
= text_field_tag :email, params[:email], required: true
= text_field_tag :email, params[:email], required: true, autocomplete: 'email'
.contact-champ
= label_tag :type do
= t('.your_question')
%span.mandatory *
= hidden_field_tag :type, params[:type]
%dl
- @options.each do |(question, question_type, link)|

View file

@ -244,9 +244,16 @@ en:
one: User
other: Users
attributes:
default_attributes: &default_attributes
password: 'password'
requested_merge_into: 'new email address'
user:
siret: 'SIRET number'
password: 'password'
<< : *default_attributes
instructeur:
<< : *default_attributes
super_admin:
<< : *default_attributes
instructeur:
password: 'password'
errors:
@ -268,6 +275,8 @@ en:
too_short: 'is too short'
password_confirmation:
confirmation: ': The two passwords do not match'
requested_merge_into:
same: "can't be the same as the old one"
invite:
attributes:
email:

View file

@ -244,6 +244,7 @@ fr:
attributes:
default_attributes: &default_attributes
password: 'Le mot de passe'
requested_merge_into: 'La nouvelle adresse email'
user:
siret: 'Numéro SIRET'
<< : *default_attributes
@ -273,6 +274,8 @@ fr:
not_strong: 'nest pas assez complexe'
password_confirmation:
confirmation: ': Les deux mots de passe ne correspondent pas'
requested_merge_into:
same: "ne peut être identique à lancienne"
invite:
attributes:
email:

View file

@ -50,6 +50,6 @@ fr:
mesri:
show:
not_filled: non renseigné
fetching_data: "La récupération automatique des données pour l'INE %{ine} et en cours."
fetching_data: "La récupération automatique des données pour l'INE %{ine} est en cours."
data_fetched: "Des données concernant %{sources} liées à l'INE %{ine} ont été reçues depuis le MESRI."
data_fetched_title: "Données obtenues du MESRI"

View file

@ -21,8 +21,8 @@ en:
product:
question: I have an idea to improve the website
answer_html: "<p>Got an idea? Please check our <strong>enhancement dashboard</strong></p>
<p><ul><li>Vote for your priority improvements</li>
<li>Share your own ideas</li></ul></p>
<ul><li>Vote for your priority improvements</li>
<li>Share your own ideas</li></ul>
<p><strong><a href=%{link_product}>➡ Access the enhancement dashboard</a></strong></p>"
lost_user:
question: I am having trouble finding the procedure I am looking for

View file

@ -21,8 +21,8 @@ fr:
product:
question: Jai une idée damélioration pour votre site
answer_html: "<p>Une idée ? Pensez à consulter notre <strong>tableau de bord des améliorations</strong></p>
<p><ul><li>Votez pour vos améliorations prioritaires;</li>
<li>Proposez votre propre idée.</li></ul></p>
<ul><li>Votez pour vos améliorations prioritaires;</li>
<li>Proposez votre propre idée.</li></ul>
<p><strong><a href=%{link_product}>➡ Accéder au tableau des améliorations</a></strong></p>"
lost_user:
question: Je ne trouve pas la démarche que je veux faire

View file

@ -54,6 +54,8 @@ Rails.application.routes.draw do
resources :super_admins, only: [:index, :show, :destroy]
resources :zones, only: [:index, :show]
post 'demandes/create_administrateur'
post 'demandes/refuse_administrateur'

View file

@ -36,7 +36,6 @@
"react-popper": "^2.2.5",
"react-query": "^3.9.7",
"react-sortable-hoc": "^1.11.0",
"react_ujs": "^2.6.1",
"trix": "^1.2.3",
"use-debounce": "^5.2.0",
"webpack": "^4.46.0",
@ -51,10 +50,10 @@
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.25.1",
"eslint-plugin-react-hooks": "^4.2.0",
"netlify-cli": "^2.61.2",
"netlify-cli": "^8.3.0",
"prettier": "^2.3.2",
"webpack-bundle-analyzer": "^3.7.0",
"webpack-dev-server": "^3"
"webpack-dev-server": "^4.6.0"
},
"scripts": {
"lint:js": "eslint --ext .js,.jsx,.ts,.tsx ./app/javascript ./config/webpack",

View file

@ -48,6 +48,14 @@ describe Users::ProfilController, type: :controller do
end
describe 'PATCH #update_email' do
context 'when email is same as user' do
it 'fails' do
patch :update_email, params: { user: { email: user.email } }
expect(response).to have_http_status(302)
expect(flash[:alert]).to eq(["La nouvelle adresse email ne peut être identique à lancienne"])
end
end
context 'when everything is fine' do
let(:previous_request) { create(:user) }
@ -69,7 +77,7 @@ describe Users::ProfilController, type: :controller do
before do
user.update(unconfirmed_email: 'unconfirmed@mail.com')
expect_any_instance_of(User).to receive(:ask_for_merge).with(existing_user)
expect(UserMailer).to receive(:ask_for_merge).with(user, existing_user.email).and_return(double(deliver_later: true))
perform_enqueued_jobs do
patch :update_email, params: { user: { email: existing_user.email } }

View file

@ -3,6 +3,9 @@ describe 'populate_zones' do
subject(:run_task) do
rake_task.invoke
end
after(:each) do
rake_task.reenable
end
it 'populates zones' do
run_task

View file

@ -1,5 +1,17 @@
describe GroupeInstructeur, type: :model do
let(:procedure) { create(:procedure) }
let(:admin) { create :administrateur }
let(:procedure) { create :procedure, :published, administrateur: admin }
let(:procedure_2) { create :procedure, :published, administrateur: admin }
let(:procedure_3) { create :procedure, :published, administrateur: admin }
let(:instructeur) { create :instructeur, administrateurs: [admin] }
let(:procedure_assign) { assign(procedure) }
before do
procedure_assign
assign(procedure_2)
procedure_3
end
subject { GroupeInstructeur.new(label: label, procedure: procedure) }
context 'with no label provided' do
@ -33,4 +45,62 @@ describe GroupeInstructeur, type: :model do
it { is_expected.to be_invalid }
end
describe "#add" do
let(:another_groupe_instructeur) { create(:groupe_instructeur, procedure: procedure) }
subject { another_groupe_instructeur.add(instructeur) }
it 'adds the instructeur to the groupe instructeur' do
subject
expect(another_groupe_instructeur.reload.instructeurs).to include(instructeur)
end
context 'when joining another groupe instructeur on the same procedure' do
before do
procedure_assign.update(daily_email_notifications_enabled: true)
subject
end
it 'copies notifications settings from a previous group' do
expect(instructeur.assign_to.last.daily_email_notifications_enabled).to be_truthy
end
end
end
describe "#remove" do
subject { procedure_to_remove.defaut_groupe_instructeur.remove(instructeur) }
context "with an assigned procedure" do
let(:procedure_to_remove) { procedure }
let!(:procedure_presentation) { procedure_assign.procedure_presentation }
it { is_expected.to be_truthy }
describe "consequences" do
before do
procedure_assign.build_procedure_presentation
procedure_assign.save
subject
end
it "removes the assign_to and procedure_presentation" do
expect(AssignTo.where(id: procedure_assign).count).to eq(0)
expect(ProcedurePresentation.where(assign_to_id: procedure_assign.id).count).to eq(0)
end
end
end
context "with an already unassigned procedure" do
let(:procedure_to_remove) { procedure_3 }
it { is_expected.to be_falsey }
end
end
private
def assign(procedure_to_assign, instructeur_assigne: instructeur)
create :assign_to, instructeur: instructeur_assigne, procedure: procedure_to_assign, groupe_instructeur: procedure_to_assign.defaut_groupe_instructeur
end
end

View file

@ -1,13 +1,15 @@
describe Instructeur, type: :model do
let(:admin) { create :administrateur }
let!(:procedure) { create :procedure, :published, administrateur: admin }
let!(:procedure_2) { create :procedure, :published, administrateur: admin }
let!(:procedure_3) { create :procedure, :published, administrateur: admin }
let(:procedure) { create :procedure, :published, administrateur: admin }
let(:procedure_2) { create :procedure, :published, administrateur: admin }
let(:procedure_3) { create :procedure, :published, administrateur: admin }
let(:instructeur) { create :instructeur, administrateurs: [admin] }
let!(:procedure_assign) { assign(procedure) }
let(:procedure_assign) { assign(procedure) }
before do
procedure_assign
assign(procedure_2)
procedure_3
end
describe 'follow' do
@ -84,36 +86,6 @@ describe Instructeur, type: :model do
end
end
describe "#remove_from_groupe_instructeur" do
subject { instructeur.remove_from_groupe_instructeur(procedure_to_remove.defaut_groupe_instructeur) }
context "with an assigned procedure" do
let(:procedure_to_remove) { procedure }
let!(:procedure_presentation) { procedure_assign.procedure_presentation }
it { is_expected.to be_truthy }
describe "consequences" do
before do
procedure_assign.build_procedure_presentation
procedure_assign.save
subject
end
it "removes the assign_to and procedure_presentation" do
expect(AssignTo.where(id: procedure_assign).count).to eq(0)
expect(ProcedurePresentation.where(assign_to_id: procedure_assign.id).count).to eq(0)
end
end
end
context "with an already unassigned procedure" do
let(:procedure_to_remove) { procedure_3 }
it { is_expected.to be_falsey }
end
end
describe 'last_week_overview' do
let!(:instructeur2) { create(:instructeur) }
subject { instructeur2.last_week_overview }

View file

@ -8,10 +8,10 @@ describe ExpiredDossiersDeletionService do
let(:reference_date) { Date.parse("March 8") }
describe '#process_expired_dossiers_brouillon' do
let(:today) { Time.zone.now.at_midnight }
let(:date_close_to_expiration) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 2.weeks }
let(:date_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months - 6.days }
let(:date_not_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 2.months }
let(:today) { Time.zone.now.at_beginning_of_day }
let(:date_close_to_expiration) { today - procedure.duree_conservation_dossiers_dans_ds.months + 13.days }
let(:date_expired) { today - procedure.duree_conservation_dossiers_dans_ds.months - 6.days }
let(:date_not_expired) { today - procedure.duree_conservation_dossiers_dans_ds.months + 2.months }
context 'send messages for dossiers expiring soon and delete expired' do
let!(:expired_brouillon) { create(:dossier, procedure: procedure, created_at: date_expired, brouillon_close_to_expiration_notice_sent_at: today - (warning_period + 3.days)) }

View file

@ -1,10 +1,10 @@
describe UpdateZoneToProceduresService do
before(:all) do
before(:each) do
Rake::Task['zones:populate_zones'].invoke
end
after(:all) do
Zone.destroy_all
after(:each) do
Rake::Task['zones:populate_zones'].reenable
end
describe '#call' do

6560
yarn.lock

File diff suppressed because it is too large Load diff