commit
308e48b611
45 changed files with 365 additions and 286 deletions
|
@ -364,4 +364,20 @@ class ApplicationController < ActionController::Base
|
||||||
def set_customizable_view_path
|
def set_customizable_view_path
|
||||||
prepend_view_path "app/custom_views"
|
prepend_view_path "app/custom_views"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Extract a value from params based on the "path"
|
||||||
|
#
|
||||||
|
# params: { dossiers: { champs_attributes: { 1234 => { value: "hello" } } } }
|
||||||
|
#
|
||||||
|
# Usage: read_param_value("dossiers[champs_attributes][1234]", "value")
|
||||||
|
def read_param_value(path, name)
|
||||||
|
parts = path.split(/\[|\]\[|\]/) + [name]
|
||||||
|
parts.reduce(params) do |value, part|
|
||||||
|
if part.to_i != 0
|
||||||
|
value[part.to_i] || value[part]
|
||||||
|
else
|
||||||
|
value[part]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,6 @@ class Champs::CarteController < ApplicationController
|
||||||
before_action :authenticate_logged_user!
|
before_action :authenticate_logged_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@selector = ".carte-#{params[:champ_id]}"
|
|
||||||
@champ = policy_scope(Champ).find(params[:champ_id])
|
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||||
@focus = params[:focus].present?
|
@focus = params[:focus].present?
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,12 +2,7 @@ class Champs::DossierLinkController < ApplicationController
|
||||||
before_action :authenticate_logged_user!
|
before_action :authenticate_logged_user!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@position = params[:position]
|
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||||
|
@linked_dossier_id = read_param_value(@champ.input_name, 'value')
|
||||||
if params[:dossier].key?(:champs_attributes)
|
|
||||||
@dossier_id = params[:dossier][:champs_attributes][params[:position]][:value]
|
|
||||||
else
|
|
||||||
@dossier_id = params[:dossier][:champs_private_attributes][params[:position]][:value]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,13 +3,6 @@ class Champs::RepetitionController < ApplicationController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@champ = policy_scope(Champ).includes(:champs).find(params[:champ_id])
|
@champ = policy_scope(Champ).includes(:champs).find(params[:champ_id])
|
||||||
@position = params[:position]
|
@champs = @champ.add_row
|
||||||
@champ.add_row
|
|
||||||
|
|
||||||
if @champ.private?
|
|
||||||
@attribute = "dossier[champs_private_attributes][#{@position}][champs_attributes]"
|
|
||||||
else
|
|
||||||
@attribute = "dossier[champs_attributes][#{@position}][champs_attributes]"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,9 +2,9 @@ class Champs::SiretController < ApplicationController
|
||||||
before_action :authenticate_logged_user!
|
before_action :authenticate_logged_user!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@position = params[:position]
|
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||||
extract_siret
|
@siret = read_param_value(@champ.input_name, 'value')
|
||||||
find_etablisement
|
@etablissement = @champ.etablissement
|
||||||
|
|
||||||
if @siret.empty?
|
if @siret.empty?
|
||||||
return clear_siret_and_etablissement
|
return clear_siret_and_etablissement
|
||||||
|
@ -34,22 +34,6 @@ class Champs::SiretController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def extract_siret
|
|
||||||
if params[:dossier].key?(:champs_attributes)
|
|
||||||
@siret = params[:dossier][:champs_attributes][@position][:value]
|
|
||||||
@attribute = "dossier[champs_attributes][#{@position}][etablissement_attributes]"
|
|
||||||
else
|
|
||||||
@siret = params[:dossier][:champs_private_attributes][@position][:value]
|
|
||||||
@attribute = "dossier[champs_private_attributes][#{@position}][etablissement_attributes]"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_etablisement
|
|
||||||
@champ = policy_scope(Champ).find(params[:champ_id])
|
|
||||||
@etablissement = @champ.etablissement
|
|
||||||
@procedure_id = @champ.dossier.procedure.id
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_etablissement_with_siret
|
def find_etablissement_with_siret
|
||||||
APIEntrepriseService.create_etablissement(@champ, @siret, current_user.id)
|
APIEntrepriseService.create_etablissement(@champ, @siret, current_user.id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -60,15 +60,6 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_champ(champ)
|
|
||||||
champ_selector = "##{champ.input_group_id}"
|
|
||||||
form_html = render 'shared/dossiers/edit', dossier: champ.dossier
|
|
||||||
champ_html = Nokogiri::HTML.fragment(form_html).at_css(champ_selector).to_s
|
|
||||||
# rubocop:disable Rails/OutputSafety
|
|
||||||
raw("document.querySelector('#{champ_selector}').outerHTML = \"#{escape_javascript(champ_html)}\";")
|
|
||||||
# rubocop:enable Rails/OutputSafety
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_element(selector, timeout: 0, inner: false)
|
def remove_element(selector, timeout: 0, inner: false)
|
||||||
script = "(function() {";
|
script = "(function() {";
|
||||||
script << "var el = document.querySelector('#{selector}');"
|
script << "var el = document.querySelector('#{selector}');"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { getJSON, ajax, fire } from '@utils';
|
import { httpRequest, fire } from '@utils';
|
||||||
import type { Feature, FeatureCollection, Geometry } from 'geojson';
|
import type { Feature, FeatureCollection, Geometry } from 'geojson';
|
||||||
|
|
||||||
export const SOURCE_SELECTION_UTILISATEUR = 'selection_utilisateur';
|
export const SOURCE_SELECTION_UTILISATEUR = 'selection_utilisateur';
|
||||||
|
@ -37,7 +37,9 @@ export function useFeatureCollection(
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: callback(features)
|
features: callback(features)
|
||||||
}));
|
}));
|
||||||
ajax({ url, type: 'GET' }).catch(() => null);
|
httpRequest(url)
|
||||||
|
.js()
|
||||||
|
.catch(() => null);
|
||||||
},
|
},
|
||||||
[url, setFeatureCollection]
|
[url, setFeatureCollection]
|
||||||
);
|
);
|
||||||
|
@ -99,7 +101,10 @@ export function useFeatureCollection(
|
||||||
try {
|
try {
|
||||||
const newFeatures: Feature[] = [];
|
const newFeatures: Feature[] = [];
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
const data = await getJSON(url, { feature, source }, 'post');
|
const data = await httpRequest(url, {
|
||||||
|
method: 'post',
|
||||||
|
json: { feature, source }
|
||||||
|
}).json<{ feature: Feature & { lid?: string | number } }>();
|
||||||
if (data) {
|
if (data) {
|
||||||
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
||||||
data.feature.lid = feature.id;
|
data.feature.lid = feature.id;
|
||||||
|
@ -128,9 +133,15 @@ export function useFeatureCollection(
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
const id = feature.properties?.id;
|
const id = feature.properties?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
await getJSON(`${url}/${id}`, { feature }, 'patch');
|
await httpRequest(`${url}/${id}`, {
|
||||||
|
method: 'patch',
|
||||||
|
json: { feature }
|
||||||
|
}).json();
|
||||||
} else {
|
} else {
|
||||||
const data = await getJSON(url, { feature, source }, 'post');
|
const data = await httpRequest(url, {
|
||||||
|
method: 'post',
|
||||||
|
json: { feature, source }
|
||||||
|
}).json<{ feature: Feature & { lid?: string | number } }>();
|
||||||
if (data) {
|
if (data) {
|
||||||
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
||||||
data.feature.lid = feature.id;
|
data.feature.lid = feature.id;
|
||||||
|
@ -157,7 +168,7 @@ export function useFeatureCollection(
|
||||||
const deletedFeatures = [];
|
const deletedFeatures = [];
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
const id = feature.properties?.id;
|
const id = feature.properties?.id;
|
||||||
await getJSON(`${url}/${id}`, null, 'delete');
|
await httpRequest(`${url}/${id}`, { method: 'delete' }).json();
|
||||||
deletedFeatures.push(feature);
|
deletedFeatures.push(feature);
|
||||||
}
|
}
|
||||||
removeFeatures(deletedFeatures, external);
|
removeFeatures(deletedFeatures, external);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getJSON } from '@utils';
|
import { httpRequest } from '@utils';
|
||||||
import invariant from 'tiny-invariant';
|
import invariant from 'tiny-invariant';
|
||||||
|
|
||||||
type Operation = {
|
type Operation = {
|
||||||
|
@ -45,7 +45,7 @@ export class OperationsQueue {
|
||||||
const url = `${this.baseUrl}${path}`;
|
const url = `${this.baseUrl}${path}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getJSON(url, payload, method);
|
const data = await httpRequest(url, { method, json: payload }).json();
|
||||||
resolve(data);
|
resolve(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(e as OperationError, reject);
|
handleError(e as OperationError, reject);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { QueryClient, QueryFunction } from 'react-query';
|
import { QueryClient, QueryFunction } from 'react-query';
|
||||||
import { getJSON, isNumeric } from '@utils';
|
import { httpRequest, isNumeric } from '@utils';
|
||||||
import { matchSorter } from 'match-sorter';
|
import { matchSorter } from 'match-sorter';
|
||||||
|
|
||||||
type Gon = {
|
type Gon = {
|
||||||
|
@ -57,39 +57,27 @@ function buildURL(scope: string, term: string, extra?: string) {
|
||||||
return `${api_geo_url}/${scope}?nom=${term}&limit=${API_GEO_QUERY_LIMIT}`;
|
return `${api_geo_url}/${scope}?nom=${term}&limit=${API_GEO_QUERY_LIMIT}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOptions(): [RequestInit, AbortController | null] {
|
|
||||||
if (window.AbortController) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const signal = controller.signal;
|
|
||||||
return [{ signal }, controller];
|
|
||||||
}
|
|
||||||
return [{}, null];
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({
|
const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({
|
||||||
queryKey: [scope, term, extra]
|
queryKey: [scope, term, extra],
|
||||||
|
signal
|
||||||
}) => {
|
}) => {
|
||||||
if (scope == 'pays') {
|
if (scope == 'pays') {
|
||||||
return matchSorter(await getPays(), term, { keys: ['label'] });
|
return matchSorter(await getPays(signal), term, { keys: ['label'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = buildURL(scope, term, extra);
|
const url = buildURL(scope, term, extra);
|
||||||
const [options, controller] = buildOptions();
|
return httpRequest(url, { csrf: false, signal }).json();
|
||||||
const promise = fetch(url, options).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw new Error(`Error fetching from "${scope}" API`);
|
|
||||||
});
|
|
||||||
return Object.assign(promise, {
|
|
||||||
cancel: () => controller && controller.abort()
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let paysCache: { label: string }[];
|
let paysCache: { label: string }[];
|
||||||
async function getPays(): Promise<{ label: string }[]> {
|
async function getPays(signal?: AbortSignal): Promise<{ label: string }[]> {
|
||||||
if (!paysCache) {
|
if (!paysCache) {
|
||||||
paysCache = await getJSON('/api/pays', null);
|
const data = await httpRequest('/api/pays', { signal }).json<
|
||||||
|
typeof paysCache
|
||||||
|
>();
|
||||||
|
if (data) {
|
||||||
|
paysCache = data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return paysCache;
|
return paysCache;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,27 @@ import 'yet-another-abortcontroller-polyfill';
|
||||||
import './polyfills/insertAdjacentElement';
|
import './polyfills/insertAdjacentElement';
|
||||||
import './polyfills/dataset';
|
import './polyfills/dataset';
|
||||||
|
|
||||||
// IE 11 has no baseURI
|
// IE 11 has no baseURI (required by turbo)
|
||||||
if (document.baseURI == undefined) {
|
if (document.baseURI == undefined) {
|
||||||
document.baseURI = document.URL;
|
document.baseURI = document.URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IE 11 has no children on DocumentFragment (required by turbo)
|
||||||
|
function polyfillChildren(proto) {
|
||||||
|
Object.defineProperty(proto, 'children', {
|
||||||
|
get: function () {
|
||||||
|
const children = [];
|
||||||
|
for (const node of this.childNodes) {
|
||||||
|
if (node.nodeType == 1) {
|
||||||
|
children.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
if (fragment.children == undefined) {
|
||||||
|
polyfillChildren(DocumentFragment.prototype);
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ajax, delegate } from '@utils';
|
import { httpRequest, delegate } from '@utils';
|
||||||
|
|
||||||
addEventListener('DOMContentLoaded', () => {
|
addEventListener('DOMContentLoaded', () => {
|
||||||
attachementPoller.deactivate();
|
attachementPoller.deactivate();
|
||||||
|
@ -69,7 +69,9 @@ class RemotePoller {
|
||||||
for (let url of urls) {
|
for (let url of urls) {
|
||||||
// Start the request. The JS payload in the response will update the page.
|
// Start the request. The JS payload in the response will update the page.
|
||||||
// (Errors are ignored, because background tasks shouldn't report errors to the user.)
|
// (Errors are ignored, because background tasks shouldn't report errors to the user.)
|
||||||
ajax({ url, type: 'get' }).catch(() => {});
|
httpRequest(url)
|
||||||
|
.js()
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import Rails from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
import debounce from 'debounce';
|
import debounce from 'debounce';
|
||||||
|
import { session } from '@hotwired/turbo';
|
||||||
|
|
||||||
export { debounce };
|
export { debounce };
|
||||||
export const { fire, csrfToken } = Rails;
|
export const { fire, csrfToken, cspNonce } = Rails;
|
||||||
|
|
||||||
export function show(el: HTMLElement) {
|
export function show(el: HTMLElement) {
|
||||||
el && el.classList.remove('hidden');
|
el && el.classList.remove('hidden');
|
||||||
|
@ -93,7 +94,7 @@ export function ajax(options: Rails.AjaxOptions) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ResponseError extends Error {
|
export class ResponseError extends Error {
|
||||||
response: Response;
|
response: Response;
|
||||||
|
|
||||||
constructor(response: Response) {
|
constructor(response: Response) {
|
||||||
|
@ -102,18 +103,126 @@ class ResponseError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getJSON(url: string, data: unknown, method = 'GET') {
|
const FETCH_TIMEOUT = 30 * 1000; // 30 sec
|
||||||
const { query, ...options } = fetchOptions(data, method);
|
|
||||||
|
|
||||||
return fetch(`${url}${query}`, options).then((response) => {
|
// Perform an HTTP request using `fetch` API,
|
||||||
if (response.ok) {
|
// and handle the result depending on the MIME type.
|
||||||
if (response.status === 204) {
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// Execute a GET request, and return the response as parsed JSON
|
||||||
|
// const parsedJson = await httpRequest(url).json();
|
||||||
|
//
|
||||||
|
// Execute a POST request with some JSON payload
|
||||||
|
// const parsedJson = await httpRequest(url, { method: 'POST', json: '{ "foo": 1 }').json();
|
||||||
|
//
|
||||||
|
// Execute a GET request, and apply the Turbo stream in the Response
|
||||||
|
// await httpRequest(url).turbo();
|
||||||
|
//
|
||||||
|
// Execute a GET request, and interpret the JavaScript code in the Response
|
||||||
|
// DEPRECATED: Don't use this in new code; instead let the server respond with a turbo stream
|
||||||
|
// await httpRequest(url).js();
|
||||||
|
//
|
||||||
|
export function httpRequest(
|
||||||
|
url: string,
|
||||||
|
{
|
||||||
|
csrf = true,
|
||||||
|
timeout = FETCH_TIMEOUT,
|
||||||
|
json,
|
||||||
|
controller,
|
||||||
|
...init
|
||||||
|
}: RequestInit & {
|
||||||
|
csrf?: boolean;
|
||||||
|
json?: unknown;
|
||||||
|
timeout?: number | false;
|
||||||
|
controller?: AbortController;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const headers = new Headers(init.headers);
|
||||||
|
if (csrf) {
|
||||||
|
headers.set('x-csrf-token', csrfToken() ?? '');
|
||||||
|
headers.set('x-requested-with', 'XMLHttpRequest');
|
||||||
|
init.credentials = 'same-origin';
|
||||||
|
}
|
||||||
|
init.headers = headers;
|
||||||
|
init.method = init.method?.toUpperCase() ?? 'GET';
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
headers.set('content-type', 'application/json');
|
||||||
|
init.body = JSON.stringify(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
let timer: number;
|
||||||
|
if (!init.signal) {
|
||||||
|
controller = createAbortController(controller);
|
||||||
|
if (controller) {
|
||||||
|
init.signal = controller.signal;
|
||||||
|
if (timeout != false) {
|
||||||
|
timer = setTimeout(() => controller?.abort(), timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = (init: RequestInit, accept?: string): Promise<Response> => {
|
||||||
|
if (accept && init.headers instanceof Headers) {
|
||||||
|
init.headers.set('accept', accept);
|
||||||
|
}
|
||||||
|
return fetch(url, init)
|
||||||
|
.then((response) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return response;
|
||||||
|
} else if (response.status == 401) {
|
||||||
|
location.reload(); // reload whole page so Devise will redirect to sign-in
|
||||||
|
}
|
||||||
|
throw new ResponseError(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
async json<T>(): Promise<T | null> {
|
||||||
|
const response = await request(init, 'application/json');
|
||||||
|
if (response.status == 204) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
|
},
|
||||||
|
async turbo(): Promise<void> {
|
||||||
|
const response = await request(init, 'text/vnd.turbo-stream.html');
|
||||||
|
if (response.status != 204) {
|
||||||
|
const stream = await response.text();
|
||||||
|
session.renderStreamMessage(stream);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async js(): Promise<void> {
|
||||||
|
const response = await request(init, 'text/javascript');
|
||||||
|
if (response.status != 204) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
const nonce = cspNonce();
|
||||||
|
if (nonce) {
|
||||||
|
script.setAttribute('nonce', nonce);
|
||||||
|
}
|
||||||
|
script.text = await response.text();
|
||||||
|
document.head.appendChild(script);
|
||||||
|
document.head.removeChild(script);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new ResponseError(response);
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function createAbortController(controller?: AbortController) {
|
||||||
|
if (controller) {
|
||||||
|
return controller;
|
||||||
|
} else if (window.AbortController) {
|
||||||
|
return new AbortController();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scrollTo(container: HTMLElement, scrollTo: HTMLElement) {
|
export function scrollTo(container: HTMLElement, scrollTo: HTMLElement) {
|
||||||
|
@ -162,53 +271,3 @@ export function timeoutable<T>(
|
||||||
});
|
});
|
||||||
return Promise.race([promise, timeoutPromise]);
|
return Promise.race([promise, timeoutPromise]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const FETCH_TIMEOUT = 30 * 1000; // 30 sec
|
|
||||||
|
|
||||||
function fetchOptions(data: unknown, method = 'GET') {
|
|
||||||
const options: RequestInit & {
|
|
||||||
query: string;
|
|
||||||
headers: Record<string, string>;
|
|
||||||
} = {
|
|
||||||
query: '',
|
|
||||||
method: method.toUpperCase(),
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json',
|
|
||||||
'x-csrf-token': csrfToken() ?? '',
|
|
||||||
'x-requested-with': 'XMLHttpRequest'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
if (options.method === 'GET') {
|
|
||||||
options.query = objectToQuerystring(data as Record<string, string>);
|
|
||||||
} else {
|
|
||||||
options.headers['content-type'] = 'application/json';
|
|
||||||
options.body = JSON.stringify(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.AbortController) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
options.signal = controller.signal;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
controller.abort();
|
|
||||||
}, FETCH_TIMEOUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
function objectToQuerystring(obj: Record<string, string>): string {
|
|
||||||
return Object.keys(obj).reduce(function (query, key, i) {
|
|
||||||
return [
|
|
||||||
query,
|
|
||||||
i === 0 ? '?' : '&',
|
|
||||||
encodeURIComponent(key),
|
|
||||||
'=',
|
|
||||||
encodeURIComponent(obj[key])
|
|
||||||
].join('');
|
|
||||||
}, '');
|
|
||||||
}
|
|
||||||
|
|
7
app/jobs/cron/purge_unused_admin_job.rb
Normal file
7
app/jobs/cron/purge_unused_admin_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class Cron::PurgeUnusedAdminJob < Cron::CronJob
|
||||||
|
self.schedule_expression = "every monday at 5 am"
|
||||||
|
|
||||||
|
def perform(*args)
|
||||||
|
Administrateur.unused.destroy_all
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,6 +12,8 @@
|
||||||
class Administrateur < ApplicationRecord
|
class Administrateur < ApplicationRecord
|
||||||
include ActiveRecord::SecureToken
|
include ActiveRecord::SecureToken
|
||||||
|
|
||||||
|
UNUSED_ADMIN_THRESHOLD = 6.months
|
||||||
|
|
||||||
has_and_belongs_to_many :instructeurs
|
has_and_belongs_to_many :instructeurs
|
||||||
has_and_belongs_to_many :procedures
|
has_and_belongs_to_many :procedures
|
||||||
has_many :services
|
has_many :services
|
||||||
|
@ -23,6 +25,14 @@ class Administrateur < ApplicationRecord
|
||||||
scope :inactive, -> { joins(:user).where(users: { last_sign_in_at: nil }) }
|
scope :inactive, -> { joins(:user).where(users: { last_sign_in_at: nil }) }
|
||||||
scope :with_publiees_ou_closes, -> { joins(:procedures).where(procedures: { aasm_state: [:publiee, :close, :depubliee] }) }
|
scope :with_publiees_ou_closes, -> { joins(:procedures).where(procedures: { aasm_state: [:publiee, :close, :depubliee] }) }
|
||||||
|
|
||||||
|
scope :unused, -> do
|
||||||
|
joins(:user)
|
||||||
|
.where.missing(:services)
|
||||||
|
.left_outer_joins(:administrateurs_procedures) # needed to bypass procedure hidden default scope
|
||||||
|
.where(administrateurs_procedures: { procedure_id: nil })
|
||||||
|
.where("users.last_sign_in_at < ? ", UNUSED_ADMIN_THRESHOLD.ago)
|
||||||
|
end
|
||||||
|
|
||||||
def self.by_email(email)
|
def self.by_email(email)
|
||||||
Administrateur.find_by(users: { email: email })
|
Administrateur.find_by(users: { email: email })
|
||||||
end
|
end
|
||||||
|
|
|
@ -137,6 +137,23 @@ class Champ < ApplicationRecord
|
||||||
"#{html_id}-input"
|
"#{html_id}-input"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# A predictable string to use when generating an input name for this champ.
|
||||||
|
#
|
||||||
|
# Rail's FormBuilder can auto-generate input names, using the form "dossier[champs_attributes][5]",
|
||||||
|
# where [5] is the index of the field in the form.
|
||||||
|
# However the field index makes it difficult to render a single field, independent from the ordering of the others.
|
||||||
|
#
|
||||||
|
# Luckily, this is only used to make the name unique, but the actual value is ignored when Rails parses nested
|
||||||
|
# attributes. So instead of the field index, this method uses the champ id; which gives us an independent and
|
||||||
|
# predictable input name.
|
||||||
|
def input_name
|
||||||
|
if parent_id
|
||||||
|
"#{parent.input_name}[#{champs_attributes_accessor}][#{id}]"
|
||||||
|
else
|
||||||
|
"dossier[#{champs_attributes_accessor}][#{id}]"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def labelledby_id
|
def labelledby_id
|
||||||
"#{html_id}-label"
|
"#{html_id}-label"
|
||||||
end
|
end
|
||||||
|
@ -169,6 +186,14 @@ class Champ < ApplicationRecord
|
||||||
"#{stable_id}-#{id}"
|
"#{stable_id}-#{id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def champs_attributes_accessor
|
||||||
|
if private?
|
||||||
|
"champs_private_attributes"
|
||||||
|
else
|
||||||
|
"champs_attributes"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def needs_dossier_id?
|
def needs_dossier_id?
|
||||||
!dossier_id && parent_id
|
!dossier_id && parent_id
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,12 +28,15 @@ class Champs::RepetitionChamp < Champ
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_row
|
def add_row
|
||||||
|
added_champs = []
|
||||||
transaction do
|
transaction do
|
||||||
row = (blank? ? -1 : champs.last.row) + 1
|
row = (blank? ? -1 : champs.last.row) + 1
|
||||||
type_de_champ.types_de_champ.each do |type_de_champ|
|
type_de_champ.types_de_champ.each do |type_de_champ|
|
||||||
self.champs << type_de_champ.champ.build(row: row)
|
added_champs << type_de_champ.champ.build(row: row)
|
||||||
end
|
end
|
||||||
|
self.champs << added_champs
|
||||||
end
|
end
|
||||||
|
added_champs
|
||||||
end
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<%= render_flash(timeout: 5000, fixed: true) %>
|
<%= render_flash(timeout: 5000, fixed: true) %>
|
||||||
|
|
||||||
<%= render_to_element("#{@selector} + .geo-areas",
|
<%= render_to_element("##{@champ.input_group_id} .geo-areas",
|
||||||
partial: 'shared/champs/carte/geo_areas',
|
partial: 'shared/champs/carte/geo_areas',
|
||||||
locals: { champ: @champ, editing: true }) %>
|
locals: { champ: @champ, editing: true }) %>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<%= render_to_element(".dossier-link-#{@position} .help-block",
|
<%= render_to_element("##{@champ.input_group_id} .help-block",
|
||||||
partial: 'shared/champs/dossier_link/help_block',
|
partial: 'shared/champs/dossier_link/help_block',
|
||||||
locals: { id: @dossier_id }) %>
|
locals: { id: @linked_dossier_id }) %>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<%= render_champ(@champ) %>
|
<%= fields_for @champ.input_name, @champ do |form| %>
|
||||||
|
<%= render_to_element("##{@champ.input_group_id}", partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: @champ, form: form }, outer: true) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% attachment = @champ.piece_justificative_file.attachment %>
|
<% attachment = @champ.piece_justificative_file.attachment %>
|
||||||
<% if attachment.virus_scanner.pending? %>
|
<% if attachment.virus_scanner.pending? %>
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
- champs = champ.rows.last
|
|
||||||
- if champs.present?
|
|
||||||
- index = (champ.rows.size - 1) * champs.size
|
|
||||||
- row_dom_id = "row-#{SecureRandom.hex(4)}"
|
|
||||||
%div{ class: "row row-#{champs.first.row}", id: row_dom_id }
|
|
||||||
-# Tell the controller which DOM element should be removed once the row deletion is successful
|
|
||||||
= hidden_field_tag 'deleted_row_dom_ids[]', row_dom_id, disabled: true
|
|
||||||
|
|
||||||
- champs.each.with_index(index) do |champ, index|
|
|
||||||
= fields_for "#{attribute}[#{index}]", champ do |form|
|
|
||||||
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form }
|
|
||||||
= form.hidden_field :id
|
|
||||||
= form.hidden_field :_destroy, disabled: true
|
|
||||||
.flex.row-reverse
|
|
||||||
%button.button.danger.remove-row
|
|
||||||
Supprimer l’élément
|
|
|
@ -1,3 +1,3 @@
|
||||||
<%= append_to_element(".repetition-#{@position}",
|
<%= fields_for @champ.input_name, @champ do |form| %>
|
||||||
partial: 'champs/repetition/show',
|
<%= append_to_element("##{@champ.input_group_id} .repetition", partial: 'shared/dossiers/editable_champs/repetition_row', locals: { form: form, champ: @champ, row: @champs }) %>
|
||||||
locals: { champ: @champ, attribute: @attribute }) %>
|
<% end %>
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
<%= render_to_element(".siret-info-#{@position}",
|
<%= render_to_element("##{@champ.input_group_id} .siret-info",
|
||||||
partial: 'shared/champs/siret/etablissement',
|
partial: 'shared/champs/siret/etablissement',
|
||||||
locals: {
|
locals: { siret: @siret, etablissement: @etablissement }) %>
|
||||||
siret: @siret,
|
|
||||||
attribute: @attribute,
|
|
||||||
etablissement: @etablissement }) %>
|
|
||||||
|
|
|
@ -6,10 +6,9 @@
|
||||||
- if @dossier.champs_private.present?
|
- if @dossier.champs_private.present?
|
||||||
%section
|
%section
|
||||||
= form_for @dossier, url: annotations_instructeur_dossier_path(@dossier.procedure, @dossier), html: { class: 'form' } do |f|
|
= form_for @dossier, url: annotations_instructeur_dossier_path(@dossier.procedure, @dossier), html: { class: 'form' } do |f|
|
||||||
= f.fields_for :champs_private, f.object.champs_private do |champ_form|
|
- @dossier.champs_private.each do |champ|
|
||||||
- champ = champ_form.object
|
= fields_for champ.input_name, champ do |form|
|
||||||
= render partial: "shared/dossiers/editable_champs/editable_champ",
|
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { form: form, champ: champ, seen_at: @annotations_privees_seen_at }
|
||||||
locals: { champ: champ, form: champ_form, seen_at: @annotations_privees_seen_at }
|
|
||||||
|
|
||||||
.send-wrapper
|
.send-wrapper
|
||||||
= f.submit 'Sauvegarder', class: 'button primary send', data: { disable: true }
|
= f.submit 'Sauvegarder', class: 'button primary send', data: { disable: true }
|
||||||
|
|
|
@ -25,7 +25,7 @@ as defined by the routes in the `admin/` namespace
|
||||||
|
|
||||||
<%= link_to "Delayed Jobs", manager_delayed_job_path, class: "navigation__link" %>
|
<%= link_to "Delayed Jobs", manager_delayed_job_path, class: "navigation__link" %>
|
||||||
<%= link_to "Features", manager_flipper_path, class: "navigation__link" %>
|
<%= link_to "Features", manager_flipper_path, class: "navigation__link" %>
|
||||||
<% if ENV["SENDINBLUE_ENABLED"] == "enabled" && ENV["SAML_IDP_ENABLED"] == "enabled" %>
|
<% if Rails.application.secrets.sendinblue[:enabled] && ENV["SAML_IDP_ENABLED"] == "enabled" %>
|
||||||
<%= link_to "Sendinblue", ENV.fetch("SENDINBLUE_LOGIN_URL"), class: "navigation__link", target: '_blank' %>
|
<%= link_to "Sendinblue", ENV.fetch("SENDINBLUE_LOGIN_URL"), class: "navigation__link", target: '_blank' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -35,10 +35,9 @@
|
||||||
dossier.procedure.groupe_instructeurs.order(:label).map { |gi| [gi.label, gi.id] },
|
dossier.procedure.groupe_instructeurs.order(:label).map { |gi| [gi.label, gi.id] },
|
||||||
{ include_blank: dossier.brouillon? }
|
{ include_blank: dossier.brouillon? }
|
||||||
|
|
||||||
= f.fields_for :champs, dossier.champs do |champ_form|
|
- dossier.champs.each do |champ|
|
||||||
- champ = champ_form.object
|
= fields_for champ.input_name, champ do |form|
|
||||||
= render partial: "shared/dossiers/editable_champs/editable_champ",
|
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { form: form, champ: champ }
|
||||||
locals: { champ: champ, form: champ_form }
|
|
||||||
|
|
||||||
- if !dossier.for_procedure_preview?
|
- if !dossier.for_procedure_preview?
|
||||||
.dossier-edit-sticky-footer
|
.dossier-edit-sticky-footer
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
= react_component("MapEditor", { featureCollection: champ.to_feature_collection, url: champs_carte_features_path(champ), options: champ.render_options }, class: "carte-#{champ.id}")
|
= react_component("MapEditor", featureCollection: champ.to_feature_collection, url: champs_carte_features_path(champ), options: champ.render_options)
|
||||||
|
|
||||||
.geo-areas
|
.geo-areas
|
||||||
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, editing: true }
|
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, editing: true }
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
.dossier-link{ class: "dossier-link-#{form.index}" }
|
.dossier-link
|
||||||
= form.number_field :value,
|
= form.number_field :value,
|
||||||
id: champ.input_id,
|
id: champ.input_id,
|
||||||
aria: { describedby: champ.describedby_id },
|
aria: { describedby: champ.describedby_id },
|
||||||
placeholder: "Numéro de dossier",
|
placeholder: "Numéro de dossier",
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
data: { remote: true, url: champs_dossier_link_path(form.index) }
|
data: { remote: true, url: champs_dossier_link_path(champ.id) }
|
||||||
|
|
||||||
.help-block
|
.help-block
|
||||||
= render partial: 'shared/champs/dossier_link/help_block', locals: { id: champ.value }
|
= render partial: 'shared/champs/dossier_link/help_block', locals: { id: champ.value }
|
||||||
|
|
|
@ -19,4 +19,4 @@
|
||||||
= form.select :value, champ.options, { selected: champ.selected}, required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id }
|
= form.select :value, champ.options, { selected: champ.selected}, required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id }
|
||||||
|
|
||||||
- if champ.drop_down_other?
|
- if champ.drop_down_other?
|
||||||
= render partial: "shared/dossiers/drop_down_other_input", locals: { form: form, champ: champ }
|
= render partial: "shared/dossiers/editable_champs/drop_down_other_input", locals: { form: form, champ: champ }
|
||||||
|
|
|
@ -8,5 +8,6 @@
|
||||||
= render partial: 'shared/dossiers/editable_champs/champ_label', locals: { form: form, champ: champ, seen_at: defined?(seen_at) ? seen_at : nil }
|
= render partial: 'shared/dossiers/editable_champs/champ_label', locals: { form: form, champ: champ, seen_at: defined?(seen_at) ? seen_at : nil }
|
||||||
- if champ.type_champ == "titre_identite"
|
- if champ.type_champ == "titre_identite"
|
||||||
%p.notice Carte nationale d’identité (uniquement le recto), passeport, titre de séjour ou autre justificatif d’identité. Formats acceptés : jpg/png
|
%p.notice Carte nationale d’identité (uniquement le recto), passeport, titre de séjour ou autre justificatif d’identité. Formats acceptés : jpg/png
|
||||||
= render partial: "shared/dossiers/editable_champs/#{champ.type_champ}",
|
|
||||||
locals: { champ: champ, form: form }
|
= form.hidden_field :id, value: champ.id
|
||||||
|
= render partial: "shared/dossiers/editable_champs/#{champ.type_champ}", locals: { form: form, champ: champ }
|
||||||
|
|
|
@ -1,24 +1,9 @@
|
||||||
%div{ class: "repetition-#{form.index}" }
|
.repetition
|
||||||
- champ.rows.each do |champs|
|
- champ.rows.each do |champs|
|
||||||
- row_dom_id = "row-#{SecureRandom.hex(4)}"
|
= render partial: 'shared/dossiers/editable_champs/repetition_row', locals: { form: form, champ: champ, row: champs }
|
||||||
%div{ class: "row row-#{champs.first.row}", id: row_dom_id }
|
|
||||||
-# Tell the controller which DOM element should be removed once the row deletion is successful
|
|
||||||
= hidden_field_tag 'deleted_row_dom_ids[]', row_dom_id, disabled: true
|
|
||||||
|
|
||||||
- champs.each do |champ|
|
|
||||||
= form.fields_for :champs, champ do |form|
|
|
||||||
= render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form }
|
|
||||||
= form.hidden_field :_destroy, disabled: true
|
|
||||||
.flex.row-reverse
|
|
||||||
- if champ.persisted?
|
|
||||||
%button.button.danger.remove-row{ type: :button }
|
|
||||||
Supprimer l’élément
|
|
||||||
- else
|
|
||||||
%button.button.danger{ type: :button }
|
|
||||||
Supprimer l’élément
|
|
||||||
|
|
||||||
- if champ.persisted?
|
- if champ.persisted?
|
||||||
= link_to champs_repetition_path(form.index), class: 'button add-row', data: { remote: true, disable: true, method: 'POST', params: { champ_id: champ&.id }.to_query } do
|
= link_to champs_repetition_path(champ.id), class: 'button add-row', data: { remote: true, disable: true, method: 'POST' } do
|
||||||
%span.icon.add
|
%span.icon.add
|
||||||
Ajouter un élément pour « #{champ.libelle} »
|
Ajouter un élément pour « #{champ.libelle} »
|
||||||
- else
|
- else
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
- row_dom_id = "row-#{SecureRandom.hex(4)}"
|
||||||
|
.row{ id: row_dom_id }
|
||||||
|
-# Tell the controller which DOM element should be removed once the row deletion is successful
|
||||||
|
= hidden_field_tag 'deleted_row_dom_ids[]', row_dom_id, disabled: true
|
||||||
|
|
||||||
|
- row.each do |champ|
|
||||||
|
= fields_for champ.input_name, champ do |form|
|
||||||
|
= render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { form: form, champ: champ }
|
||||||
|
= form.hidden_field :_destroy, disabled: true
|
||||||
|
|
||||||
|
.flex.row-reverse
|
||||||
|
%button.button.danger.remove-row{ type: :button }
|
||||||
|
Supprimer l’élément
|
|
@ -2,11 +2,11 @@
|
||||||
id: champ.input_id,
|
id: champ.input_id,
|
||||||
aria: { describedby: champ.describedby_id },
|
aria: { describedby: champ.describedby_id },
|
||||||
placeholder: champ.libelle,
|
placeholder: champ.libelle,
|
||||||
data: { remote: true, debounce: true, url: champs_siret_path(form.index), params: { champ_id: champ&.id }.to_query, spinner: true },
|
data: { remote: true, debounce: true, url: champs_siret_path(champ.id), spinner: true },
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
pattern: "[0-9]{14}",
|
pattern: "[0-9]{14}",
|
||||||
title: "Le numéro de SIRET doit comporter exactement 14 chiffres"
|
title: "Le numéro de SIRET doit comporter exactement 14 chiffres"
|
||||||
.spinner.right.hidden
|
.spinner.right.hidden
|
||||||
.siret-info{ class: "siret-info-#{form.index}" }
|
.siret-info
|
||||||
- if champ.etablissement.present?
|
- if champ.etablissement.present?
|
||||||
= render partial: 'shared/dossiers/editable_champs/etablissement_titre', locals: { etablissement: champ.etablissement }
|
= render partial: 'shared/dossiers/editable_champs/etablissement_titre', locals: { etablissement: champ.etablissement }
|
||||||
|
|
|
@ -84,19 +84,12 @@ MATOMO_HOST="matomo.example.org"
|
||||||
MAILJET_API_KEY=""
|
MAILJET_API_KEY=""
|
||||||
MAILJET_SECRET_KEY=""
|
MAILJET_SECRET_KEY=""
|
||||||
|
|
||||||
# Alternate SMTP Provider: SendInBlue
|
# Alternate SMTP Provider: SendInBlue/DoList
|
||||||
SENDINBLUE_ENABLED="disabled"
|
|
||||||
SENDINBLUE_CLIENT_KEY=""
|
SENDINBLUE_CLIENT_KEY=""
|
||||||
SENDINBLUE_SMTP_KEY=""
|
SENDINBLUE_SMTP_KEY=""
|
||||||
SENDINBLUE_USER_NAME=""
|
SENDINBLUE_USER_NAME=""
|
||||||
# SENDINBLUE_LOGIN_URL="https://app.sendinblue.com/account/saml/login/truc"
|
# SENDINBLUE_LOGIN_URL="https://app.sendinblue.com/account/saml/login/truc"
|
||||||
|
|
||||||
# Ratio of emails sent using SendInBlue
|
|
||||||
# When enabled, N % of emails will be sent using SendInBlue
|
|
||||||
# (and the others using the default SMTP provider)
|
|
||||||
SENDINBLUE_BALANCING="disabled"
|
|
||||||
SENDINBLUE_BALANCING_VALUE="50"
|
|
||||||
|
|
||||||
# Alternate SMTP Provider: Mailtrap (mail catcher for staging environments)
|
# Alternate SMTP Provider: Mailtrap (mail catcher for staging environments)
|
||||||
# When enabled, all emails will be sent using this provider
|
# When enabled, all emails will be sent using this provider
|
||||||
MAILTRAP_ENABLED="disabled"
|
MAILTRAP_ENABLED="disabled"
|
||||||
|
|
|
@ -119,3 +119,12 @@ MATOMO_IFRAME_URL="https://matomo.example.org/index.php?module=CoreAdminHome&act
|
||||||
# DOLIST_ACCOUNT_ID=""
|
# DOLIST_ACCOUNT_ID=""
|
||||||
# DOLIST_NO_REPLY_EMAIL=""
|
# DOLIST_NO_REPLY_EMAIL=""
|
||||||
# DOLIST_API_KEY=""
|
# DOLIST_API_KEY=""
|
||||||
|
|
||||||
|
# Ratio of emails sent using SendInBlue
|
||||||
|
# When present, N % of emails will be sent using SendInBlue
|
||||||
|
# (and the others using the default SMTP provider)
|
||||||
|
SENDINBLUE_BALANCING_VALUE="50"
|
||||||
|
# Ratio of emails sent using DoList
|
||||||
|
# When present, N % of emails will be sent using DoList
|
||||||
|
# (and the others using the default SMTP provider)
|
||||||
|
DOLIST_BALANCING_VALUE="50"
|
||||||
|
|
|
@ -81,24 +81,8 @@ Rails.application.configure do
|
||||||
elsif ENV['MAILCATCHER_ENABLED'] == 'enabled'
|
elsif ENV['MAILCATCHER_ENABLED'] == 'enabled'
|
||||||
config.action_mailer.delivery_method = :mailcatcher
|
config.action_mailer.delivery_method = :mailcatcher
|
||||||
else
|
else
|
||||||
|
sendinblue_weigth = ENV.fetch('SENDINBLUE_BALANCING_VALUE') { 0 }.to_i
|
||||||
sendinblue_weigth = case [ENV['SENDINBLUE_ENABLED'] == 'enabled', ENV['SENDINBLUE_BALANCING'] == 'enabled']
|
dolist_weigth = ENV.fetch('DOLIST_BALANCING_VALUE') { 0 }.to_i
|
||||||
in [false, _]
|
|
||||||
0
|
|
||||||
in [true, false]
|
|
||||||
100
|
|
||||||
else
|
|
||||||
ENV.fetch('SENDINBLUE_BALANCING_VALUE').to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
dolist_weigth = case [ENV['DOLIST_ENABLED'] == 'enabled', ENV['DOLIST_BALANCING'] == 'enabled']
|
|
||||||
in [false, _]
|
|
||||||
0
|
|
||||||
in [true, false]
|
|
||||||
100
|
|
||||||
else
|
|
||||||
ENV.fetch('DOLIST_BALANCING_VALUE').to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
ActionMailer::Base.add_delivery_method :balancer, BalancerDeliveryMethod
|
ActionMailer::Base.add_delivery_method :balancer, BalancerDeliveryMethod
|
||||||
config.action_mailer.balancer_settings = {
|
config.action_mailer.balancer_settings = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
if ENV.fetch('SENDINBLUE_ENABLED') == 'enabled'
|
if ENV.key?('SENDINBLUE_BALANCING_VALUE')
|
||||||
require 'sib-api-v3-sdk'
|
require 'sib-api-v3-sdk'
|
||||||
|
|
||||||
ActiveSupport.on_load(:action_mailer) do
|
ActiveSupport.on_load(:action_mailer) do
|
||||||
|
|
|
@ -141,18 +141,17 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :champs do
|
namespace :champs do
|
||||||
get ':position/siret', to: 'siret#show', as: :siret
|
get ':champ_id/siret', to: 'siret#show', as: :siret
|
||||||
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
|
get ':champ_id/dossier_link', to: 'dossier_link#show', as: :dossier_link
|
||||||
post ':position/carte', to: 'carte#show', as: :carte
|
post ':champ_id/carte', to: 'carte#show', as: :carte
|
||||||
|
post ':champ_id/repetition', to: 'repetition#show', as: :repetition
|
||||||
|
|
||||||
get ':champ_id/carte/features', to: 'carte#index', as: :carte_features
|
get ':champ_id/carte/features', to: 'carte#index', as: :carte_features
|
||||||
post ':champ_id/carte/features', to: 'carte#create'
|
post ':champ_id/carte/features', to: 'carte#create'
|
||||||
post ':champ_id/carte/features/import', to: 'carte#import'
|
|
||||||
patch ':champ_id/carte/features/:id', to: 'carte#update'
|
patch ':champ_id/carte/features/:id', to: 'carte#update'
|
||||||
delete ':champ_id/carte/features/:id', to: 'carte#destroy'
|
delete ':champ_id/carte/features/:id', to: 'carte#destroy'
|
||||||
|
|
||||||
post ':position/repetition', to: 'repetition#show', as: :repetition
|
put ':champ_id/piece_justificative', to: 'piece_justificative#update', as: :piece_justificative
|
||||||
put 'piece_justificative/:champ_id', to: 'piece_justificative#update', as: :piece_justificative
|
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :attachments, only: [:show, :destroy]
|
resources :attachments, only: [:show, :destroy]
|
||||||
|
|
|
@ -53,7 +53,7 @@ defaults: &defaults
|
||||||
client_secret: <%= ENV['HELPSCOUT_CLIENT_SECRET'] %>
|
client_secret: <%= ENV['HELPSCOUT_CLIENT_SECRET'] %>
|
||||||
webhook_secret: <%= ENV['HELPSCOUT_WEBHOOK_SECRET'] %>
|
webhook_secret: <%= ENV['HELPSCOUT_WEBHOOK_SECRET'] %>
|
||||||
sendinblue:
|
sendinblue:
|
||||||
enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %>
|
enabled: <%= ENV.key?('SENDINBLUE_BALANCING_VALUE') %>
|
||||||
username: <%= ENV['SENDINBLUE_USER_NAME'] %>
|
username: <%= ENV['SENDINBLUE_USER_NAME'] %>
|
||||||
client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %>
|
client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %>
|
||||||
smtp_key: <%= ENV['SENDINBLUE_SMTP_KEY'] %>
|
smtp_key: <%= ENV['SENDINBLUE_SMTP_KEY'] %>
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
diff --git a/node_modules/@hotwired/turbo/dist/turbo.es2017-esm.js b/node_modules/@hotwired/turbo/dist/turbo.es2017-esm.js
|
|
||||||
index 963422f..7820263 100644
|
|
||||||
--- a/node_modules/@hotwired/turbo/dist/turbo.es2017-esm.js
|
|
||||||
+++ b/node_modules/@hotwired/turbo/dist/turbo.es2017-esm.js
|
|
||||||
@@ -551,7 +551,8 @@ class StreamMessage {
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
get templateChildren() {
|
|
||||||
- return Array.from(this.templateElement.content.children);
|
|
||||||
+ const content = this.templateElement.content;
|
|
||||||
+ return content.children ? Array.from(content.children) : Array.from(content.childNodes).filter((tag) => tag.tagName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StreamMessage.contentType = "text/vnd.turbo-stream.html";
|
|
||||||
diff --git a/node_modules/@hotwired/turbo/dist/turbo.es2017-umd.js b/node_modules/@hotwired/turbo/dist/turbo.es2017-umd.js
|
|
||||||
index 101db1f..a63cfbe 100644
|
|
||||||
--- a/node_modules/@hotwired/turbo/dist/turbo.es2017-umd.js
|
|
||||||
+++ b/node_modules/@hotwired/turbo/dist/turbo.es2017-umd.js
|
|
||||||
@@ -557,7 +557,8 @@ Copyright © 2021 Basecamp, LLC
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
get templateChildren() {
|
|
||||||
- return Array.from(this.templateElement.content.children);
|
|
||||||
+ const content = this.templateElement.content;
|
|
||||||
+ return content.children ? Array.from(content.children) : Array.from(content.childNodes).filter((tag) => tag.tagName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StreamMessage.contentType = "text/vnd.turbo-stream.html";
|
|
|
@ -1,22 +1,26 @@
|
||||||
describe Champs::DossierLinkController, type: :controller do
|
describe Champs::DossierLinkController, type: :controller do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:procedure) { create(:procedure, :published) }
|
let(:procedure) { create(:procedure, :published, :with_dossier_link) }
|
||||||
|
|
||||||
describe '#show' do
|
describe '#show' do
|
||||||
let(:dossier) { create(:dossier, user: user, procedure: procedure) }
|
let(:dossier) { create(:dossier, user: user, procedure: procedure) }
|
||||||
|
let(:champ) { dossier.champs.first }
|
||||||
|
|
||||||
context 'when user is connected' do
|
context 'when user is connected' do
|
||||||
render_views
|
render_views
|
||||||
before { sign_in user }
|
before { sign_in user }
|
||||||
|
|
||||||
|
let(:champs_attributes) do
|
||||||
|
champ_attributes = []
|
||||||
|
champ_attributes[champ.id] = { value: dossier_id }
|
||||||
|
champ_attributes
|
||||||
|
end
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
|
champ_id: champ.id,
|
||||||
dossier: {
|
dossier: {
|
||||||
champs_attributes: {
|
champs_attributes: champs_attributes
|
||||||
'1' => { value: dossier_id.to_s }
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
position: '1'
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
let(:dossier_id) { dossier.id }
|
let(:dossier_id) { dossier.id }
|
||||||
|
@ -30,7 +34,7 @@ describe Champs::DossierLinkController, type: :controller do
|
||||||
expect(response.body).to include('Dossier en brouillon')
|
expect(response.body).to include('Dossier en brouillon')
|
||||||
expect(response.body).to include(procedure.libelle)
|
expect(response.body).to include(procedure.libelle)
|
||||||
expect(response.body).to include(procedure.organisation)
|
expect(response.body).to include(procedure.organisation)
|
||||||
expect(response.body).to include('.dossier-link-1 .help-block')
|
expect(response.body).to include("##{champ.input_group_id} .help-block")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,14 +46,14 @@ describe Champs::DossierLinkController, type: :controller do
|
||||||
|
|
||||||
it 'renders error message' do
|
it 'renders error message' do
|
||||||
expect(response.body).to include('Ce dossier est inconnu')
|
expect(response.body).to include('Ce dossier est inconnu')
|
||||||
expect(response.body).to include('.dossier-link-1 .help-block')
|
expect(response.body).to include("##{champ.input_group_id} .help-block")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is not connected' do
|
context 'when user is not connected' do
|
||||||
before do
|
before do
|
||||||
get :show, params: { position: '1' }, format: :js, xhr: true
|
get :show, params: { champ_id: champ.id }, format: :js, xhr: true
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(response.code).to eq('401') }
|
it { expect(response.code).to eq('401') }
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
describe Champs::SiretController, type: :controller do
|
describe Champs::SiretController, type: :controller do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:procedure) do
|
let(:procedure) { create(:procedure, :published, :with_siret) }
|
||||||
tdc_siret = build(:type_de_champ_siret, procedure: nil)
|
|
||||||
create(:procedure, :published, types_de_champ: [tdc_siret])
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#show' do
|
describe '#show' do
|
||||||
let(:dossier) { create(:dossier, user: user, procedure: procedure) }
|
let(:dossier) { create(:dossier, user: user, procedure: procedure) }
|
||||||
let(:champ) { dossier.champs.first }
|
let(:champ) { dossier.champs.first }
|
||||||
|
|
||||||
|
let(:champs_attributes) do
|
||||||
|
champ_attributes = []
|
||||||
|
champ_attributes[champ.id] = { value: siret }
|
||||||
|
champ_attributes
|
||||||
|
end
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
champ_id: champ.id,
|
champ_id: champ.id,
|
||||||
dossier: {
|
dossier: {
|
||||||
champs_attributes: {
|
champs_attributes: champs_attributes
|
||||||
'1' => { value: siret.to_s }
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
position: '1'
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
let(:siret) { '' }
|
let(:siret) { '' }
|
||||||
|
@ -47,7 +46,7 @@ describe Champs::SiretController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'clears any information or error message' do
|
it 'clears any information or error message' do
|
||||||
expect(response.body).to include('.siret-info-1')
|
expect(response.body).to include("##{champ.input_group_id} .siret-info")
|
||||||
expect(response.body).to include('innerHTML = ""')
|
expect(response.body).to include('innerHTML = ""')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -120,7 +119,7 @@ describe Champs::SiretController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is not signed in' do
|
context 'when user is not signed in' do
|
||||||
subject! { get :show, params: { position: '1' }, format: :js, xhr: true }
|
subject! { get :show, params: { champ_id: champ.id }, format: :js, xhr: true }
|
||||||
|
|
||||||
it { expect(response.code).to eq('401') }
|
it { expect(response.code).to eq('401') }
|
||||||
end
|
end
|
||||||
|
|
|
@ -154,6 +154,12 @@ FactoryBot.define do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :with_siret do
|
||||||
|
after(:build) do |procedure, _evaluator|
|
||||||
|
build(:type_de_champ_siret, procedure: procedure)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
trait :with_yes_no do
|
trait :with_yes_no do
|
||||||
after(:build) do |procedure, _evaluator|
|
after(:build) do |procedure, _evaluator|
|
||||||
build(:type_de_champ_yes_no, procedure: procedure)
|
build(:type_de_champ_yes_no, procedure: procedure)
|
||||||
|
|
|
@ -163,4 +163,34 @@ describe Administrateur, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'unused' do
|
||||||
|
subject { Administrateur.unused }
|
||||||
|
|
||||||
|
let(:new_admin) { create(:administrateur) }
|
||||||
|
let(:unused_admin) { create(:administrateur) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
new_admin.user.update(last_sign_in_at: (6.months - 1.day).ago)
|
||||||
|
unused_admin.user.update(last_sign_in_at: (6.months + 1.day).ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to match([unused_admin]) }
|
||||||
|
|
||||||
|
context 'with a hidden procedure' do
|
||||||
|
let(:procedure) { create(:procedure, hidden_at: 1.month.ago) }
|
||||||
|
|
||||||
|
before { unused_admin.procedures << procedure }
|
||||||
|
|
||||||
|
it { is_expected.to be_empty }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a service' do
|
||||||
|
let(:service) { create(:service) }
|
||||||
|
|
||||||
|
before { unused_admin.services << service }
|
||||||
|
|
||||||
|
it { is_expected.to be_empty }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -113,7 +113,7 @@ describe 'The user' do
|
||||||
|
|
||||||
click_on 'Ajouter un élément pour'
|
click_on 'Ajouter un élément pour'
|
||||||
|
|
||||||
within '.row-1' do
|
within '.repetition .row:first-child' do
|
||||||
fill_in('sub type de champ', with: 'un autre texte')
|
fill_in('sub type de champ', with: 'un autre texte')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ describe 'The user' do
|
||||||
|
|
||||||
expect(page).to have_content('Supprimer', count: 2)
|
expect(page).to have_content('Supprimer', count: 2)
|
||||||
|
|
||||||
within '.row-1' do
|
within '.repetition .row:first-child' do
|
||||||
click_on 'Supprimer l’élément'
|
click_on 'Supprimer l’élément'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue