commit
308e48b611
45 changed files with 365 additions and 286 deletions
|
@ -364,4 +364,20 @@ class ApplicationController < ActionController::Base
|
|||
def set_customizable_view_path
|
||||
prepend_view_path "app/custom_views"
|
||||
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
|
||||
|
|
|
@ -2,7 +2,6 @@ class Champs::CarteController < ApplicationController
|
|||
before_action :authenticate_logged_user!
|
||||
|
||||
def index
|
||||
@selector = ".carte-#{params[:champ_id]}"
|
||||
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||
@focus = params[:focus].present?
|
||||
end
|
||||
|
|
|
@ -2,12 +2,7 @@ class Champs::DossierLinkController < ApplicationController
|
|||
before_action :authenticate_logged_user!
|
||||
|
||||
def show
|
||||
@position = params[:position]
|
||||
|
||||
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
|
||||
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||
@linked_dossier_id = read_param_value(@champ.input_name, 'value')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,13 +3,6 @@ class Champs::RepetitionController < ApplicationController
|
|||
|
||||
def show
|
||||
@champ = policy_scope(Champ).includes(:champs).find(params[:champ_id])
|
||||
@position = params[:position]
|
||||
@champ.add_row
|
||||
|
||||
if @champ.private?
|
||||
@attribute = "dossier[champs_private_attributes][#{@position}][champs_attributes]"
|
||||
else
|
||||
@attribute = "dossier[champs_attributes][#{@position}][champs_attributes]"
|
||||
end
|
||||
@champs = @champ.add_row
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,9 +2,9 @@ class Champs::SiretController < ApplicationController
|
|||
before_action :authenticate_logged_user!
|
||||
|
||||
def show
|
||||
@position = params[:position]
|
||||
extract_siret
|
||||
find_etablisement
|
||||
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||
@siret = read_param_value(@champ.input_name, 'value')
|
||||
@etablissement = @champ.etablissement
|
||||
|
||||
if @siret.empty?
|
||||
return clear_siret_and_etablissement
|
||||
|
@ -34,22 +34,6 @@ class Champs::SiretController < ApplicationController
|
|||
|
||||
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
|
||||
APIEntrepriseService.create_etablissement(@champ, @siret, current_user.id)
|
||||
end
|
||||
|
|
|
@ -60,15 +60,6 @@ module ApplicationHelper
|
|||
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)
|
||||
script = "(function() {";
|
||||
script << "var el = document.querySelector('#{selector}');"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { getJSON, ajax, fire } from '@utils';
|
||||
import { httpRequest, fire } from '@utils';
|
||||
import type { Feature, FeatureCollection, Geometry } from 'geojson';
|
||||
|
||||
export const SOURCE_SELECTION_UTILISATEUR = 'selection_utilisateur';
|
||||
|
@ -37,7 +37,9 @@ export function useFeatureCollection(
|
|||
type: 'FeatureCollection',
|
||||
features: callback(features)
|
||||
}));
|
||||
ajax({ url, type: 'GET' }).catch(() => null);
|
||||
httpRequest(url)
|
||||
.js()
|
||||
.catch(() => null);
|
||||
},
|
||||
[url, setFeatureCollection]
|
||||
);
|
||||
|
@ -99,7 +101,10 @@ export function useFeatureCollection(
|
|||
try {
|
||||
const newFeatures: Feature[] = [];
|
||||
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 (source == SOURCE_SELECTION_UTILISATEUR) {
|
||||
data.feature.lid = feature.id;
|
||||
|
@ -128,9 +133,15 @@ export function useFeatureCollection(
|
|||
for (const feature of features) {
|
||||
const id = feature.properties?.id;
|
||||
if (id) {
|
||||
await getJSON(`${url}/${id}`, { feature }, 'patch');
|
||||
await httpRequest(`${url}/${id}`, {
|
||||
method: 'patch',
|
||||
json: { feature }
|
||||
}).json();
|
||||
} 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 (source == SOURCE_SELECTION_UTILISATEUR) {
|
||||
data.feature.lid = feature.id;
|
||||
|
@ -157,7 +168,7 @@ export function useFeatureCollection(
|
|||
const deletedFeatures = [];
|
||||
for (const feature of features) {
|
||||
const id = feature.properties?.id;
|
||||
await getJSON(`${url}/${id}`, null, 'delete');
|
||||
await httpRequest(`${url}/${id}`, { method: 'delete' }).json();
|
||||
deletedFeatures.push(feature);
|
||||
}
|
||||
removeFeatures(deletedFeatures, external);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getJSON } from '@utils';
|
||||
import { httpRequest } from '@utils';
|
||||
import invariant from 'tiny-invariant';
|
||||
|
||||
type Operation = {
|
||||
|
@ -45,7 +45,7 @@ export class OperationsQueue {
|
|||
const url = `${this.baseUrl}${path}`;
|
||||
|
||||
try {
|
||||
const data = await getJSON(url, payload, method);
|
||||
const data = await httpRequest(url, { method, json: payload }).json();
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
handleError(e as OperationError, reject);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { QueryClient, QueryFunction } from 'react-query';
|
||||
import { getJSON, isNumeric } from '@utils';
|
||||
import { httpRequest, isNumeric } from '@utils';
|
||||
import { matchSorter } from 'match-sorter';
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
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 ({
|
||||
queryKey: [scope, term, extra]
|
||||
queryKey: [scope, term, extra],
|
||||
signal
|
||||
}) => {
|
||||
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 [options, controller] = buildOptions();
|
||||
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()
|
||||
});
|
||||
return httpRequest(url, { csrf: false, signal }).json();
|
||||
};
|
||||
|
||||
let paysCache: { label: string }[];
|
||||
async function getPays(): Promise<{ label: string }[]> {
|
||||
async function getPays(signal?: AbortSignal): Promise<{ label: string }[]> {
|
||||
if (!paysCache) {
|
||||
paysCache = await getJSON('/api/pays', null);
|
||||
const data = await httpRequest('/api/pays', { signal }).json<
|
||||
typeof paysCache
|
||||
>();
|
||||
if (data) {
|
||||
paysCache = data;
|
||||
}
|
||||
}
|
||||
return paysCache;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,27 @@ import 'yet-another-abortcontroller-polyfill';
|
|||
import './polyfills/insertAdjacentElement';
|
||||
import './polyfills/dataset';
|
||||
|
||||
// IE 11 has no baseURI
|
||||
// IE 11 has no baseURI (required by turbo)
|
||||
if (document.baseURI == undefined) {
|
||||
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', () => {
|
||||
attachementPoller.deactivate();
|
||||
|
@ -69,7 +69,9 @@ class RemotePoller {
|
|||
for (let url of urls) {
|
||||
// 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.)
|
||||
ajax({ url, type: 'get' }).catch(() => {});
|
||||
httpRequest(url)
|
||||
.js()
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import Rails from '@rails/ujs';
|
||||
import debounce from 'debounce';
|
||||
import { session } from '@hotwired/turbo';
|
||||
|
||||
export { debounce };
|
||||
export const { fire, csrfToken } = Rails;
|
||||
export const { fire, csrfToken, cspNonce } = Rails;
|
||||
|
||||
export function show(el: HTMLElement) {
|
||||
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;
|
||||
|
||||
constructor(response: Response) {
|
||||
|
@ -102,18 +103,126 @@ class ResponseError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export function getJSON(url: string, data: unknown, method = 'GET') {
|
||||
const { query, ...options } = fetchOptions(data, method);
|
||||
const FETCH_TIMEOUT = 30 * 1000; // 30 sec
|
||||
|
||||
// Perform an HTTP request using `fetch` API,
|
||||
// and handle the result depending on the MIME type.
|
||||
//
|
||||
// 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);
|
||||
|
||||
return fetch(`${url}${query}`, options).then((response) => {
|
||||
if (response.ok) {
|
||||
if (response.status === 204) {
|
||||
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 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);
|
||||
}
|
||||
throw new ResponseError(response);
|
||||
});
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createAbortController(controller?: AbortController) {
|
||||
if (controller) {
|
||||
return controller;
|
||||
} else if (window.AbortController) {
|
||||
return new AbortController();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export function scrollTo(container: HTMLElement, scrollTo: HTMLElement) {
|
||||
|
@ -162,53 +271,3 @@ export function timeoutable<T>(
|
|||
});
|
||||
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
|
||||
include ActiveRecord::SecureToken
|
||||
|
||||
UNUSED_ADMIN_THRESHOLD = 6.months
|
||||
|
||||
has_and_belongs_to_many :instructeurs
|
||||
has_and_belongs_to_many :procedures
|
||||
has_many :services
|
||||
|
@ -23,6 +25,14 @@ class Administrateur < ApplicationRecord
|
|||
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 :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)
|
||||
Administrateur.find_by(users: { email: email })
|
||||
end
|
||||
|
|
|
@ -137,6 +137,23 @@ class Champ < ApplicationRecord
|
|||
"#{html_id}-input"
|
||||
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
|
||||
"#{html_id}-label"
|
||||
end
|
||||
|
@ -169,6 +186,14 @@ class Champ < ApplicationRecord
|
|||
"#{stable_id}-#{id}"
|
||||
end
|
||||
|
||||
def champs_attributes_accessor
|
||||
if private?
|
||||
"champs_private_attributes"
|
||||
else
|
||||
"champs_attributes"
|
||||
end
|
||||
end
|
||||
|
||||
def needs_dossier_id?
|
||||
!dossier_id && parent_id
|
||||
end
|
||||
|
|
|
@ -28,12 +28,15 @@ class Champs::RepetitionChamp < Champ
|
|||
end
|
||||
|
||||
def add_row
|
||||
added_champs = []
|
||||
transaction do
|
||||
row = (blank? ? -1 : champs.last.row) + 1
|
||||
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
|
||||
self.champs << added_champs
|
||||
end
|
||||
added_champs
|
||||
end
|
||||
|
||||
def blank?
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%= 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',
|
||||
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',
|
||||
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 %>
|
||||
<% 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}",
|
||||
partial: 'champs/repetition/show',
|
||||
locals: { champ: @champ, attribute: @attribute }) %>
|
||||
<%= fields_for @champ.input_name, @champ do |form| %>
|
||||
<%= append_to_element("##{@champ.input_group_id} .repetition", partial: 'shared/dossiers/editable_champs/repetition_row', locals: { form: form, champ: @champ, row: @champs }) %>
|
||||
<% 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',
|
||||
locals: {
|
||||
siret: @siret,
|
||||
attribute: @attribute,
|
||||
etablissement: @etablissement }) %>
|
||||
locals: { siret: @siret, etablissement: @etablissement }) %>
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
- if @dossier.champs_private.present?
|
||||
%section
|
||||
= 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|
|
||||
- champ = champ_form.object
|
||||
= render partial: "shared/dossiers/editable_champs/editable_champ",
|
||||
locals: { champ: champ, form: champ_form, seen_at: @annotations_privees_seen_at }
|
||||
- @dossier.champs_private.each do |champ|
|
||||
= fields_for champ.input_name, champ do |form|
|
||||
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { form: form, champ: champ, seen_at: @annotations_privees_seen_at }
|
||||
|
||||
.send-wrapper
|
||||
= 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 "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' %>
|
||||
<% end %>
|
||||
</nav>
|
||||
|
|
|
@ -35,10 +35,9 @@
|
|||
dossier.procedure.groupe_instructeurs.order(:label).map { |gi| [gi.label, gi.id] },
|
||||
{ include_blank: dossier.brouillon? }
|
||||
|
||||
= f.fields_for :champs, dossier.champs do |champ_form|
|
||||
- champ = champ_form.object
|
||||
= render partial: "shared/dossiers/editable_champs/editable_champ",
|
||||
locals: { champ: champ, form: champ_form }
|
||||
- dossier.champs.each do |champ|
|
||||
= fields_for champ.input_name, champ do |form|
|
||||
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { form: form, champ: champ }
|
||||
|
||||
- if !dossier.for_procedure_preview?
|
||||
.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
|
||||
= 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,
|
||||
id: champ.input_id,
|
||||
aria: { describedby: champ.describedby_id },
|
||||
placeholder: "Numéro de dossier",
|
||||
autocomplete: 'off',
|
||||
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
|
||||
= 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 }
|
||||
|
||||
- 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 }
|
||||
- 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
|
||||
= 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|
|
||||
- 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 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
|
||||
= render partial: 'shared/dossiers/editable_champs/repetition_row', locals: { form: form, champ: champ, row: champs }
|
||||
|
||||
- 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
|
||||
Ajouter un élément pour « #{champ.libelle} »
|
||||
- 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,
|
||||
aria: { describedby: champ.describedby_id },
|
||||
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?,
|
||||
pattern: "[0-9]{14}",
|
||||
title: "Le numéro de SIRET doit comporter exactement 14 chiffres"
|
||||
.spinner.right.hidden
|
||||
.siret-info{ class: "siret-info-#{form.index}" }
|
||||
.siret-info
|
||||
- if champ.etablissement.present?
|
||||
= 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_SECRET_KEY=""
|
||||
|
||||
# Alternate SMTP Provider: SendInBlue
|
||||
SENDINBLUE_ENABLED="disabled"
|
||||
# Alternate SMTP Provider: SendInBlue/DoList
|
||||
SENDINBLUE_CLIENT_KEY=""
|
||||
SENDINBLUE_SMTP_KEY=""
|
||||
SENDINBLUE_USER_NAME=""
|
||||
# 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)
|
||||
# When enabled, all emails will be sent using this provider
|
||||
MAILTRAP_ENABLED="disabled"
|
||||
|
|
|
@ -119,3 +119,12 @@ MATOMO_IFRAME_URL="https://matomo.example.org/index.php?module=CoreAdminHome&act
|
|||
# DOLIST_ACCOUNT_ID=""
|
||||
# DOLIST_NO_REPLY_EMAIL=""
|
||||
# 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'
|
||||
config.action_mailer.delivery_method = :mailcatcher
|
||||
else
|
||||
|
||||
sendinblue_weigth = case [ENV['SENDINBLUE_ENABLED'] == 'enabled', ENV['SENDINBLUE_BALANCING'] == 'enabled']
|
||||
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
|
||||
sendinblue_weigth = ENV.fetch('SENDINBLUE_BALANCING_VALUE') { 0 }.to_i
|
||||
dolist_weigth = ENV.fetch('DOLIST_BALANCING_VALUE') { 0 }.to_i
|
||||
|
||||
ActionMailer::Base.add_delivery_method :balancer, BalancerDeliveryMethod
|
||||
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'
|
||||
|
||||
ActiveSupport.on_load(:action_mailer) do
|
||||
|
|
|
@ -141,18 +141,17 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
namespace :champs do
|
||||
get ':position/siret', to: 'siret#show', as: :siret
|
||||
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
|
||||
post ':position/carte', to: 'carte#show', as: :carte
|
||||
get ':champ_id/siret', to: 'siret#show', as: :siret
|
||||
get ':champ_id/dossier_link', to: 'dossier_link#show', as: :dossier_link
|
||||
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
|
||||
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'
|
||||
delete ':champ_id/carte/features/:id', to: 'carte#destroy'
|
||||
|
||||
post ':position/repetition', to: 'repetition#show', as: :repetition
|
||||
put 'piece_justificative/:champ_id', to: 'piece_justificative#update', as: :piece_justificative
|
||||
put ':champ_id/piece_justificative', to: 'piece_justificative#update', as: :piece_justificative
|
||||
end
|
||||
|
||||
resources :attachments, only: [:show, :destroy]
|
||||
|
|
|
@ -53,7 +53,7 @@ defaults: &defaults
|
|||
client_secret: <%= ENV['HELPSCOUT_CLIENT_SECRET'] %>
|
||||
webhook_secret: <%= ENV['HELPSCOUT_WEBHOOK_SECRET'] %>
|
||||
sendinblue:
|
||||
enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %>
|
||||
enabled: <%= ENV.key?('SENDINBLUE_BALANCING_VALUE') %>
|
||||
username: <%= ENV['SENDINBLUE_USER_NAME'] %>
|
||||
client_key: <%= ENV['SENDINBLUE_CLIENT_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
|
||||
let(:user) { create(:user) }
|
||||
let(:procedure) { create(:procedure, :published) }
|
||||
let(:procedure) { create(:procedure, :published, :with_dossier_link) }
|
||||
|
||||
describe '#show' do
|
||||
let(:dossier) { create(:dossier, user: user, procedure: procedure) }
|
||||
let(:champ) { dossier.champs.first }
|
||||
|
||||
context 'when user is connected' do
|
||||
render_views
|
||||
before { sign_in user }
|
||||
|
||||
let(:champs_attributes) do
|
||||
champ_attributes = []
|
||||
champ_attributes[champ.id] = { value: dossier_id }
|
||||
champ_attributes
|
||||
end
|
||||
let(:params) do
|
||||
{
|
||||
champ_id: champ.id,
|
||||
dossier: {
|
||||
champs_attributes: {
|
||||
'1' => { value: dossier_id.to_s }
|
||||
champs_attributes: champs_attributes
|
||||
}
|
||||
},
|
||||
position: '1'
|
||||
}
|
||||
end
|
||||
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(procedure.libelle)
|
||||
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
|
||||
|
||||
|
@ -42,14 +46,14 @@ describe Champs::DossierLinkController, type: :controller do
|
|||
|
||||
it 'renders error message' do
|
||||
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
|
||||
|
||||
context 'when user is not connected' do
|
||||
before do
|
||||
get :show, params: { position: '1' }, format: :js, xhr: true
|
||||
get :show, params: { champ_id: champ.id }, format: :js, xhr: true
|
||||
end
|
||||
|
||||
it { expect(response.code).to eq('401') }
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
describe Champs::SiretController, type: :controller do
|
||||
let(:user) { create(:user) }
|
||||
let(:procedure) do
|
||||
tdc_siret = build(:type_de_champ_siret, procedure: nil)
|
||||
create(:procedure, :published, types_de_champ: [tdc_siret])
|
||||
end
|
||||
let(:procedure) { create(:procedure, :published, :with_siret) }
|
||||
|
||||
describe '#show' do
|
||||
let(:dossier) { create(:dossier, user: user, procedure: procedure) }
|
||||
let(:champ) { dossier.champs.first }
|
||||
|
||||
let(:champs_attributes) do
|
||||
champ_attributes = []
|
||||
champ_attributes[champ.id] = { value: siret }
|
||||
champ_attributes
|
||||
end
|
||||
let(:params) do
|
||||
{
|
||||
champ_id: champ.id,
|
||||
dossier: {
|
||||
champs_attributes: {
|
||||
'1' => { value: siret.to_s }
|
||||
champs_attributes: champs_attributes
|
||||
}
|
||||
},
|
||||
position: '1'
|
||||
}
|
||||
end
|
||||
let(:siret) { '' }
|
||||
|
@ -47,7 +46,7 @@ describe Champs::SiretController, type: :controller do
|
|||
end
|
||||
|
||||
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 = ""')
|
||||
end
|
||||
end
|
||||
|
@ -120,7 +119,7 @@ describe Champs::SiretController, type: :controller do
|
|||
end
|
||||
|
||||
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') }
|
||||
end
|
||||
|
|
|
@ -154,6 +154,12 @@ FactoryBot.define do
|
|||
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
|
||||
after(:build) do |procedure, _evaluator|
|
||||
build(:type_de_champ_yes_no, procedure: procedure)
|
||||
|
|
|
@ -163,4 +163,34 @@ describe Administrateur, type: :model do
|
|||
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
|
||||
|
|
|
@ -113,7 +113,7 @@ describe 'The user' do
|
|||
|
||||
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')
|
||||
end
|
||||
|
||||
|
@ -124,7 +124,7 @@ describe 'The user' do
|
|||
|
||||
expect(page).to have_content('Supprimer', count: 2)
|
||||
|
||||
within '.row-1' do
|
||||
within '.repetition .row:first-child' do
|
||||
click_on 'Supprimer l’élément'
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue