Merge branch 'dev'

This commit is contained in:
gregoirenovel 2018-10-10 15:29:23 +02:00
commit 90ab10d1d8
35 changed files with 154 additions and 147 deletions

View file

@ -65,7 +65,6 @@
}
.confidentiel-explanation {
display: none;
font-size: 14px;
color: $grey;
margin-top: -$default-padding;

View file

@ -2,7 +2,6 @@
@import "constants";
.motivation {
display: none;
padding: $default-padding;
color: $black;
width: 450px;

View file

@ -5,7 +5,7 @@ class InvitesController < ApplicationController
email = params[:invite_email].downcase
dossier = current_user.dossiers.find(params[:dossier_id])
invite = InviteUser.create(
invite = Invite.create(
dossier: dossier,
user: User.find_by(email: email),
email: email,

View file

@ -1,5 +1,5 @@
import $ from 'jquery';
import { toggle } from '@utils';
export function toggleCondidentielExplanation() {
$('.confidentiel-explanation').toggle();
toggle(document.querySelector('.confidentiel-explanation'));
}

View file

@ -1,5 +1,5 @@
import $ from 'jquery';
import L from 'leaflet';
import { getJSON } from '@utils';
import { getData } from '../shared/data';
import { DEFAULT_POSITION } from '../shared/carto';
@ -11,8 +11,8 @@ import {
} from './carto/draw';
function initialize() {
if ($('#map').length > 0) {
$.getJSON(getData('carto').getPositionUrl).then(
if (document.getElementById('map')) {
getJSON(getData('carto').getPositionUrl).then(
position => initializeWithPosition(position),
() => initializeWithPosition(DEFAULT_POSITION)
);

View file

@ -1,8 +1,6 @@
import Rails from 'rails-ujs';
import { delegate } from '@utils';
const { delegate } = Rails;
delegate(document, 'body', 'click', event => {
delegate('click', 'body', event => {
if (!event.target.closest('.dropdown')) {
[...document.querySelectorAll('.dropdown')].forEach(element =>
element.classList.remove('open', 'fade-in-down')
@ -10,7 +8,7 @@ delegate(document, 'body', 'click', event => {
}
});
delegate(document, '.dropdown-button', 'click', event => {
delegate('click', '.dropdown-button', event => {
event.stopPropagation();
const parent = event.target.closest('.dropdown-button').parentElement;
if (parent.classList.contains('dropdown')) {

View file

@ -1,10 +1,19 @@
import $ from 'jquery';
import { delegate } from '@utils';
$(document).on('blur keydown', 'input, textarea', () => {
$(this).addClass('touched');
delegate('blur keydown', 'input, textarea', ({ target }) => {
touch(target);
});
$(document).on('click', 'input[type="submit"]:not([formnovalidate])', () => {
const $form = $(this).closest('form');
$('input, textarea', $form).addClass('touched');
});
delegate(
'click',
'input[type="submit"]:not([formnovalidate])',
({ target }) => {
let form = target.closest('form');
let inputs = form ? form.querySelectorAll('input, textarea') : [];
[...inputs].forEach(touch);
}
);
function touch({ classList }) {
classList.add('touched');
}

View file

@ -1,27 +1,17 @@
import $ from 'jquery';
import { scrollTo, scrollToBottom } from '@utils';
export function scrollMessagerie() {
const $ul = $('.messagerie ul').first();
const ul = document.querySelector('.messagerie ul');
if ($ul.length) {
const $elementToScroll = $('.date.highlighted').first();
if (ul) {
const elementToScroll = document.querySelector('.date.highlighted');
if ($elementToScroll.length != 0) {
scrollTo($ul, $elementToScroll);
if (elementToScroll) {
scrollTo(ul, elementToScroll);
} else {
scrollToBottom($ul);
scrollToBottom(ul);
}
}
}
function scrollTo($container, $scrollTo) {
$container.scrollTop(
$scrollTo.offset().top - $container.offset().top + $container.scrollTop()
);
}
function scrollToBottom($container) {
$container.scrollTop($container.prop('scrollHeight'));
}
addEventListener('turbolinks:load', scrollMessagerie);

View file

@ -1,7 +1,4 @@
import Rails from 'rails-ujs';
import { show, hide } from '../shared/utils';
const { delegate } = Rails;
import { show, hide, delegate } from '@utils';
function showSpinner() {
[...document.querySelectorAll('.spinner')].forEach(show);
@ -11,6 +8,6 @@ function hideSpinner() {
[...document.querySelectorAll('.spinner')].forEach(hide);
}
delegate(document, '[data-spinner]', 'ajax:complete', hideSpinner);
delegate(document, '[data-spinner]', 'ajax:stopped', hideSpinner);
delegate(document, '[data-spinner]', 'ajax:send', showSpinner);
delegate('ajax:complete', '[data-spinner]', hideSpinner);
delegate('ajax:stopped', '[data-spinner]', hideSpinner);
delegate('ajax:send', '[data-spinner]', showSpinner);

View file

@ -1,12 +1,12 @@
import $ from 'jquery';
import { show, hide } from '@utils';
export function showMotivation(event, state) {
event.preventDefault();
$(`.motivation.${state}`).show();
$('.dropdown-items').hide();
show(document.querySelector(`.motivation.${state}`));
hide(document.querySelector('.dropdown-items'));
}
export function motivationCancel() {
$('.motivation').hide();
$('.dropdown-items').show();
document.querySelectorAll('.motivation').forEach(hide);
show(document.querySelector('.dropdown-items'));
}

View file

@ -1,28 +1,24 @@
import $ from 'jquery';
import Chartkick from 'chartkick';
import { toggle } from '@utils';
export function toggleChart(event, chartClass) {
const nextSelectorItem = $(event.target),
nextChart = $(chartClass),
nextChartId = nextChart
.children()
.first()
.attr('id'),
currentSelectorItem = nextSelectorItem
.parent()
.find('.segmented-control-item-active'),
currentChart = nextSelectorItem
.parent()
.parent()
.find('.chart:not(.hidden)');
const nextSelectorItem = event.target,
nextChart = document.querySelector(chartClass),
nextChartId = nextChart.children[0].id,
currentSelectorItem = nextSelectorItem.parentElement.querySelector(
'.segmented-control-item-active'
),
currentChart = nextSelectorItem.parentElement.parentElement.querySelector(
'.chart:not(.hidden)'
);
// Change the current selector and the next selector states
currentSelectorItem.toggleClass('segmented-control-item-active');
nextSelectorItem.toggleClass('segmented-control-item-active');
currentSelectorItem.classList.toggle('segmented-control-item-active');
nextSelectorItem.classList.toggle('segmented-control-item-active');
// Hide the currently shown chart and show the new one
currentChart.toggleClass('hidden');
nextChart.toggleClass('hidden');
toggle(currentChart);
toggle(nextChart);
// Reflow needed, see https://github.com/highcharts/highcharts/issues/1979
Chartkick.charts[nextChartId].getChartObject().reflow();

View file

@ -4,7 +4,6 @@ import Rails from 'rails-ujs';
import ActiveStorage from '../shared/activestorage/ujs';
import Chartkick from 'chartkick';
import Highcharts from 'highcharts';
import jQuery from 'jquery';
import '../shared/sentry';
import '../shared/rails-ujs-fix';
@ -41,11 +40,6 @@ Rails.start();
Turbolinks.start();
ActiveStorage.start();
// Disable jQuery-driven animations during tests
if (process.env['RAILS_ENV'] === 'test') {
jQuery.fx.off = true;
}
// Expose globals
window.DS = window.DS || DS;
window.Chartkick = Chartkick;

View file

@ -1,5 +1,5 @@
import $ from 'jquery';
import autocomplete from 'autocomplete.js';
import { getJSON, fire } from '@utils';
const sources = [
{
@ -24,7 +24,7 @@ function selector(type) {
function source(url) {
return {
source(query, callback) {
$.getJSON(url, { request: query }).then(callback);
getJSON(url, { request: query }).then(callback);
},
templates: {
suggestion({ label, mine }) {
@ -41,7 +41,7 @@ addEventListener('turbolinks:load', function() {
for (let target of document.querySelectorAll(selector(type))) {
let select = autocomplete(target, options, [source(url)]);
select.on('autocomplete:selected', ({ target }, suggestion) => {
$(target).trigger('autocomplete:select', suggestion);
fire(target, 'autocomplete:select', suggestion);
select.autocomplete.setVal(suggestion.label);
});
}

View file

@ -1,12 +1,13 @@
import Rails from 'rails-ujs';
import jQuery from 'jquery';
import { delegate } from '@utils';
// We use `jQuery.active` in our capybara suit to wait for ajax requests.
// Newer jQuery-less version of rails-ujs is breaking it.
// We have to set `ajax:complete` listener on the same element as the one
// we catch ajax:send on as by the end of the request
// the old element may be removed from DOM.
Rails.delegate(document, '[data-remote]', 'ajax:send', ({ target }) => {
delegate('ajax:send', '[data-remote]', ({ target }) => {
let callback = () => {
jQuery.active--;
target.removeEventListener('ajax:complete', callback);

View file

@ -1,7 +1,4 @@
import Rails from 'rails-ujs';
import debounce from 'debounce';
const { delegate, fire } = Rails;
import { delegate, fire, debounce } from '@utils';
const remote = 'data-remote';
const inputChangeSelector = `input[${remote}], textarea[${remote}]`;
@ -21,4 +18,4 @@ function isRemote(element) {
return value && value !== 'false';
}
delegate(document, inputChangeSelector, 'input', debounce(handleRemote, 200));
delegate('input', inputChangeSelector, debounce(handleRemote, 200));

View file

@ -1,3 +1,10 @@
import Rails from 'rails-ujs';
import $ from 'jquery';
import debounce from 'debounce';
export { debounce };
export const { fire } = Rails;
export function show({ classList }) {
classList.remove('hidden');
}
@ -9,3 +16,45 @@ export function hide({ classList }) {
export function toggle({ classList }) {
classList.toggle('hidden');
}
export function delegate(eventNames, selector, callback) {
eventNames
.split(' ')
.forEach(eventName =>
Rails.delegate(document, selector, eventName, callback)
);
}
export function getJSON(url, data, method = 'get') {
data = method !== 'get' ? JSON.stringify(data) : data;
return $.ajax({
method,
url,
data,
contentType: 'application/json',
dataType: 'json'
});
}
export function scrollTo(container, scrollTo) {
container.scrollTop =
offset(scrollTo).top - offset(container).top + container.scrollTop;
}
export function scrollToBottom(container) {
container.scrollTop = container.scrollHeight;
}
export function on(selector, eventName, fn) {
[...document.querySelectorAll(selector)].forEach(element =>
element.addEventListener(eventName, event => fn(event, event.detail))
);
}
function offset(element) {
const rect = element.getBoundingClientRect();
return {
top: rect.top + document.body.scrollTop,
left: rect.left + document.body.scrollLeft
};
}

View file

@ -35,7 +35,7 @@ class Commentaire < ApplicationRecord
def notify
dossier_user_email = dossier.user.email
invited_users_emails = dossier.invites_user.pluck(:email).to_a
invited_users_emails = dossier.invites.pluck(:email).to_a
# - If the email is the contact email, the commentaire is a copy
# of an automated notification email we sent to a user, so do nothing.

View file

@ -24,8 +24,6 @@ class Dossier < ApplicationRecord
has_many :cadastres, dependent: :destroy
has_many :commentaires, dependent: :destroy
has_many :invites, dependent: :destroy
has_many :invites_user, class_name: 'InviteUser', dependent: :destroy
has_many :invites_gestionnaires, class_name: 'InviteGestionnaire', dependent: :destroy
has_many :follows
has_many :followers_gestionnaires, through: :follows, source: :gestionnaire
has_many :avis, dependent: :destroy
@ -160,7 +158,7 @@ class Dossier < ApplicationRecord
end
def invite_for_user(user)
invites_user.find_by(user_id: user.id)
invites.find_by(user_id: user.id)
end
def can_be_en_construction?

View file

@ -1,2 +0,0 @@
class InviteGestionnaire < Invite
end

View file

@ -1,2 +0,0 @@
class InviteUser < Invite
end

View file

@ -10,8 +10,7 @@ class DossierSerializer < ActiveModel::Serializer
:received_at,
:processed_at,
:motivation,
:instructeurs,
:invites
:instructeurs
has_one :individual
has_one :entreprise
@ -62,10 +61,6 @@ class DossierSerializer < ActiveModel::Serializer
object.followers_gestionnaires.pluck(:email)
end
def invites
object.invites_gestionnaires.pluck(:email)
end
private
def user_geometry(dossier)

View file

@ -1,4 +1,4 @@
.motivation{ class: popup_class }
.motivation.hidden{ class: popup_class }
%h3
%span.icon{ class: popup_class }
#{popup_title}

View file

@ -17,7 +17,7 @@
.confidentiel-wrapper
= f.label :confidentiel, 'Cet avis est'
= f.select :confidentiel, [['partagé avec les autres experts', false], ['confidentiel', true]], {}, onchange: "javascript:DS.toggleCondidentielExplanation(event);"
.confidentiel-explanation
.confidentiel-explanation.hidden
Il ne sera pas affiché aux autres experts consultés mais sera visible par les instructeurs
.send-wrapper
= f.submit 'Demander un avis', class: 'button send'

View file

@ -1,3 +1,4 @@
const path = require('path');
const { environment } = require('@rails/webpacker');
// By default don't transpile JS files in ./node_modules except for some specific modules.
@ -14,4 +15,12 @@ babelLoader.exclude = function(modulePath) {
);
};
const resolve = {
alias: {
'@utils': path.resolve(__dirname, '..', '..', 'app/javascript/shared/utils')
}
};
environment.config.merge({ resolve });
module.exports = environment;

View file

@ -0,0 +1,5 @@
class RemoveTypeFromInvites < ActiveRecord::Migration[5.2]
def change
remove_column :invites, :type
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_10_02_164310) do
ActiveRecord::Schema.define(version: 2018_10_10_070424) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -384,7 +384,6 @@ ActiveRecord::Schema.define(version: 2018_10_02_164310) do
t.string "email_sender"
t.integer "dossier_id"
t.integer "user_id"
t.string "type", default: "InviteGestionnaire"
t.datetime "created_at"
t.datetime "updated_at"
end

View file

@ -139,7 +139,7 @@ describe API::V1::DossiersController do
let!(:dossier) { Timecop.freeze(date_creation) { create(:dossier, :with_entreprise, procedure: procedure, motivation: "Motivation") } }
let(:dossier_id) { dossier.id }
let(:body) { JSON.parse(retour.body, symbolize_names: true) }
let(:field_list) { [:id, :created_at, :updated_at, :archived, :individual, :entreprise, :etablissement, :cerfa, :types_de_piece_justificative, :pieces_justificatives, :champs, :champs_private, :commentaires, :state, :simplified_state, :initiated_at, :processed_at, :received_at, :motivation, :email, :instructeurs, :invites] }
let(:field_list) { [:id, :created_at, :updated_at, :archived, :individual, :entreprise, :etablissement, :cerfa, :types_de_piece_justificative, :pieces_justificatives, :champs, :champs_private, :commentaires, :state, :simplified_state, :initiated_at, :processed_at, :received_at, :motivation, :email, :instructeurs] }
subject { body[:dossier] }
it 'return REST code 200', :show_in_doc do

View file

@ -44,7 +44,7 @@ describe InvitesController, type: :controller do
sign_in(user)
end
it { expect { subject }.to change(InviteUser, :count).by(1) }
it { expect { subject }.to change(Invite, :count).by(1) }
end
end
end
@ -54,7 +54,7 @@ describe InvitesController, type: :controller do
shared_examples_for "he can not create a invite" do
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
it { expect { subject rescue nil }.to change(InviteUser, :count).by(0) }
it { expect { subject rescue nil }.to change(Invite, :count).by(0) }
end
context 'when user has no access to dossier' do
@ -73,7 +73,7 @@ describe InvitesController, type: :controller do
dossier.update(user: signed_in_profile)
end
it { expect { subject }.to change(InviteUser, :count).by(1) }
it { expect { subject }.to change(Invite, :count).by(1) }
it "redirects to the previous URL" do
expect(subject).to redirect_to("/dossiers/#{dossier.id}/brouillon")

View file

@ -50,13 +50,13 @@ describe NewUser::DossiersController, type: :controller do
end
context 'when an invite asks for a dossier where they were invited' do
before { create(:invite, dossier: asked_dossier, user: user, type: 'InviteUser') }
before { create(:invite, dossier: asked_dossier, user: user) }
it_behaves_like 'redirects and flashes'
end
context 'when an invite asks for another dossier' do
before { create(:invite, dossier: create(:dossier), user: user, type: 'InviteUser') }
before { create(:invite, dossier: create(:dossier), user: user) }
it_behaves_like 'redirects and flashes'
end
@ -84,13 +84,13 @@ describe NewUser::DossiersController, type: :controller do
end
context 'when an invite asks for a dossier where they were invited' do
before { create(:invite, dossier: asked_dossier, user: user, type: 'InviteUser') }
before { create(:invite, dossier: asked_dossier, user: user) }
it_behaves_like 'does not redirect nor flash'
end
context 'when an invite asks for another dossier' do
before { create(:invite, dossier: create(:dossier), user: user, type: 'InviteUser') }
before { create(:invite, dossier: create(:dossier), user: user) }
it_behaves_like 'redirects and flashes'
end
@ -123,14 +123,14 @@ describe NewUser::DossiersController, type: :controller do
end
context 'when an invite save the draft for a dossier where they where invited' do
before { create(:invite, dossier: asked_dossier, user: user, type: 'InviteUser') }
before { create(:invite, dossier: asked_dossier, user: user) }
let(:draft) { true }
it_behaves_like 'does not redirect nor flash'
end
context 'when an invite submit a dossier where they where invited' do
before { create(:invite, dossier: asked_dossier, user: user, type: 'InviteUser') }
before { create(:invite, dossier: asked_dossier, user: user) }
let(:draft) { false }
it_behaves_like 'redirects and flashes'
@ -370,7 +370,7 @@ describe NewUser::DossiersController, type: :controller do
context 'when the user has an invitation but is not the owner' do
let(:dossier) { create(:dossier) }
let!(:invite) { create(:invite, dossier: dossier, user: user, type: 'InviteUser') }
let!(:invite) { create(:invite, dossier: dossier, user: user) }
context 'and the invite saves a draft' do
let(:payload) { submit_payload.merge(save_draft: true) }
@ -493,7 +493,7 @@ describe NewUser::DossiersController, type: :controller do
context 'when the user has an invitation but is not the owner' do
let(:dossier) { create(:dossier) }
let!(:invite) { create(:invite, dossier: dossier, user: user, type: 'InviteUser') }
let!(:invite) { create(:invite, dossier: dossier, user: user) }
before do
dossier.en_construction!
@ -525,7 +525,7 @@ describe NewUser::DossiersController, type: :controller do
end
context 'when the user only have some dossiers invites' do
let!(:invite) { create(:invite, dossier: create(:dossier), user: user, type: 'InviteUser') }
let!(:invite) { create(:invite, dossier: create(:dossier), user: user) }
before { get(:index) }
@ -535,7 +535,7 @@ describe NewUser::DossiersController, type: :controller do
context 'when the user has both' do
let!(:own_dossier) { create(:dossier, user: user) }
let!(:invite) { create(:invite, dossier: create(:dossier), user: user, type: 'InviteUser') }
let!(:invite) { create(:invite, dossier: create(:dossier), user: user) }
context 'and there is no current_tab param' do
before { get(:index) }
@ -560,8 +560,8 @@ describe NewUser::DossiersController, type: :controller do
before do
Timecop.freeze(4.days.ago) { create(:dossier, user: user) }
Timecop.freeze(2.days.ago) { create(:dossier, user: user) }
Timecop.freeze(4.days.ago) { create(:invite, dossier: create(:dossier), user: user, type: 'InviteUser') }
Timecop.freeze(2.days.ago) { create(:invite, dossier: create(:dossier), user: user, type: 'InviteUser') }
Timecop.freeze(4.days.ago) { create(:invite, dossier: create(:dossier), user: user) }
Timecop.freeze(2.days.ago) { create(:invite, dossier: create(:dossier), user: user) }
get(:index)
end

View file

@ -17,7 +17,7 @@ RSpec.describe Users::CarteController, type: :controller do
let(:adresse) { etablissement.geo_adresse }
before do
create :invite, dossier: dossier, user: invite_by_user, email: invite_by_user.email, type: 'InviteUser'
create :invite, dossier: dossier, user: invite_by_user, email: invite_by_user.email
sign_in user
end

View file

@ -20,7 +20,7 @@ describe UsersController, type: :controller do
context 'when user is invite by the owner' do
before do
create :invite, email: invite_user.email, dossier: dossier, user: invite_user, type: 'InviteUser'
create :invite, email: invite_user.email, dossier: dossier, user: invite_user
sign_in invite_user
end

View file

@ -1,24 +0,0 @@
FactoryBot.define do
factory :invite_user do
email { 'plop@octo.com' }
after(:build) do |invite, _evaluator|
if invite.dossier.nil?
invite.dossier = create(:dossier)
end
if invite.user.present?
invite.email = invite.user.email
end
end
trait :with_user do
after(:build) do |invite, _evaluator|
if invite.user.nil?
invite.user = create(:user)
invite.email = invite.user.email
end
end
end
end
end

View file

@ -5,7 +5,7 @@ feature 'Invitations' do
let(:owner) { create(:user) }
let(:invited_user) { create(:user, email: 'user_invite@exemple.fr') }
let(:procedure) { create(:simple_procedure) }
let(:invite) { create(:invite_user, user: invited_user, dossier: dossier) }
let(:invite) { create(:invite, user: invited_user, dossier: dossier) }
context 'when the dossier is a brouillon' do
let!(:dossier) { create(:dossier, :for_individual, state: Dossier.states.fetch(:brouillon), user: owner, procedure: procedure) }
@ -26,7 +26,7 @@ feature 'Invitations' do
end
context 'when inviting someone without an existing account' do
let(:invite) { create(:invite_user, dossier: dossier, user: nil) }
let(:invite) { create(:invite, dossier: dossier, user: nil) }
let(:user_password) { 'l33tus3r' }
scenario 'an invited user can register using the registration link sent in the invitation email' do

View file

@ -33,7 +33,7 @@ RSpec.describe CommentaireHelper, type: :helper do
describe '.commentaire_is_from_guest' do
let(:dossier) { create(:dossier) }
let!(:guest) { create(:invite_user, dossier: dossier) }
let!(:guest) { create(:invite, dossier: dossier) }
subject { commentaire_is_from_guest(commentaire) }

View file

@ -66,7 +66,7 @@ describe User, type: :model do
context 'when user was invited by user' do
before do
create(:invite, dossier: dossier, user: invite_user, type: 'InviteUser')
create(:invite, dossier: dossier, user: invite_user)
end
let(:user) { invite_user }
@ -116,7 +116,7 @@ describe User, type: :model do
context 'when user was invited by user' do
before do
create(:invite, dossier: dossier, user: invite_user, type: 'InviteUser')
create(:invite, dossier: dossier, user: invite_user)
end
let(:user) { invite_user }