Merge branch 'notifications' into develop

This commit is contained in:
Xavier J 2016-12-27 16:03:33 +01:00
commit 12edb06a02
63 changed files with 719 additions and 72 deletions

View file

@ -3,6 +3,9 @@ source 'https://rubygems.org'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '5.0.0.1'
gem 'actioncable', '5.0.0.1'
gem 'redis'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
@ -116,6 +119,8 @@ group :development do
# Access an IRB console on exception pages or by using <%= console %> in views
gem 'web-console'
gem 'rack-handlers'
end
group :development, :test do

View file

@ -417,6 +417,8 @@ GEM
pry (~> 0.10)
public_suffix (2.0.4)
rack (2.0.1)
rack-handlers (0.7.3)
rack
rack-oauth2 (1.4.0)
activesupport (>= 2.3)
attr_required (>= 0.0.5)
@ -465,6 +467,7 @@ GEM
nokogiri (~> 1.5)
trollop (~> 2.1)
rdoc (4.3.0)
redis (3.3.0)
ref (2.0.0)
request_store (1.3.1)
responders (2.3.0)
@ -624,6 +627,7 @@ PLATFORMS
ruby
DEPENDENCIES
actioncable (= 5.0.0.1)
active_model_serializers
apipie-rails
as_csv
@ -665,9 +669,11 @@ DEPENDENCIES
pg
poltergeist
pry-byebug
rack-handlers
railroady
rails (= 5.0.0.1)
rails-controller-testing
redis
rest-client
rgeo-geojson
rspec-rails (~> 3.0)

View file

@ -0,0 +1,13 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the rails generate channel command.
//
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);

View file

@ -0,0 +1,23 @@
App.messages = App.cable.subscriptions.create('NotificationsChannel', {
received: function (data) {
if (window.location.href.indexOf('backoffice') !== -1) {
$("#notification_alert").html(data['message']);
slideIn_notification_alert();
}
}
});
function slideIn_notification_alert (){
$("#notification_alert").animate({
right: '20px'
}, 250);
setTimeout(slideOut_notification_alert, 3500);
}
function slideOut_notification_alert (){
$("#notification_alert").animate({
right: '-250px'
}, 200);
}

View file

@ -1,5 +1,22 @@
$(document).on('page:load', the_terms);
$(document).ready(the_terms);
$(document).on('page:load', pannel_switch);
$(document).ready(pannel_switch);
function pannel_switch() {
$('#switch-notifications').click(function () {
$('#procedure_list').addClass('hidden');
$('#notifications_list').removeClass('hidden');
$(this).addClass('active');
$('#switch-procedures').removeClass('active');
})
$('#switch-procedures').click(function () {
$('#notifications_list').addClass('hidden');
$('#procedure_list').removeClass('hidden');
$(this).addClass('active');
$('#switch-notifications').removeClass('active');
})
}
function the_terms() {
var the_terms = $("#dossier_autorisation_donnees");

View file

@ -1,9 +1,8 @@
$(document).on('page:load', link_init);
$(document).ready(link_init);
function link_init() {
$('#dossiers_list tr').on('click', function () {
$(location).attr('href', $(this).data('dossier_url'))
});
}
$('#dossiers_list tr').on('click', function () {
$(location).attr('href', $(this).data('dossier_url'))
});
}

View file

@ -42,24 +42,22 @@ h5 span {
cursor: pointer;
}
#procedure_list {
#procedure_list, #notifications_list {
margin-left: -10px;
margin-top: 20px;
a, a:hover {
color: #FFFFFF;
text-decoration: none;
}
.procedure_list_element.active{
.procedure_list_element.active, .notification.active {
background-color: #668ABD;
}
.procedure_list_element {
.procedure_list_element, .notification {
padding: 15px 40px 15px 20px;
cursor: pointer;
line-height: 1.8em;
}
.procedure_list_element:hover{
.procedure_list_element:hover, .notification:hover {
background-color: #668ABD;
cursor: pointer;
}
@ -67,5 +65,5 @@ h5 span {
.split-hr-left {
border-bottom: 1px solid #FFFFFF;
margin: 20px 10px 0px 0;
margin: 20px 10px 0px 10px;
}

View file

@ -1,6 +1,6 @@
#left-pannel {
margin-top: 60px;
padding: 0 0 0 10px;
padding: 0;
background-color: #003189;
height: calc(100% - 60px);
position: fixed;
@ -51,6 +51,53 @@
}
}
#menu-block {
#switch-buttons {
height: 30px;
line-height: 30px;
font-size: 16px;
margin-top: 20px;
margin-left: auto;
margin-right: auto;
width: 205px;
border: 1px solid;
padding: 0 0 0 10px;
border-radius: 25px;
cursor: pointer;
.active {
background-color: #668ABD !important;
cursor: default;
}
.separator {
height: 26px;
width: 1px;
display: inline-block;
background-color: #FFFFFF;
}
#switch-procedures:hover, #switch-notifications:hover {
background-color: #668AEA;
}
#switch-procedures {
height: 28px;
margin: 0 0 0 -10px;
padding-left: 10px;
width: 100px;
display: inline-block;
border-radius: 25px 0 0 25px;
}
#switch-notifications {
width: 103px;
display: inline-block;
border-radius: 0 25px 25px 0;
height: 28px;
margin: 0 0 0 -5px;
padding: 0 0 0 5px;
}
}
.split-hr {
border-bottom: 1px solid #FFFFFF;
width: 200px;
margin: 20px 0 20px 0;
}
}
#infos-block {
.split-hr {
@ -63,18 +110,42 @@
font-size: 25px;
width: 200px;
margin-top: 20px;
width: 200px;
margin-left: auto;
margin-right: auto;
}
.tips {
margin: 0 10px 0 5px;
.fa {
color: #FFFFFF;
font-size: 40px;
width: inherit;
padding: 5px;
#notifications_list {
.notification {
padding: 10px 10px 10px 20px;
.dossier, .updated-at {
display: inline-block;
color: #CCCCCC;
font-size: 12px;
text-align: left;
}
}
.notice {
font-size: 18px;
display: initial;
}
.notifications {
margin: 20px 10px 0 5px;
.fa {
font-size: 25px;
width: 100%;
margin: 0 0 15px 0;
}
.notification {
margin: 10px 0 10px 10px;
.type {
margin-bottom: 20px;
}
.updated-at {
color: #CCCCCC;
font-size: 12px;
text-align: left;
}
.split-hr {
width: 40px;
margin: auto;
}
}
}
}

View file

@ -0,0 +1,12 @@
#notification_alert {
position: fixed;
top: 20px;
right: -250px;
z-index: 1000;
width: 250px;
height: 80px;
border: solid black 1px;
}

View file

@ -1,5 +1,5 @@
#search-block{
margin: 15px 10px 0 0;
margin: 15px 10px 0 10px;
height: 30px;
}

View file

@ -0,0 +1,5 @@
# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View file

@ -0,0 +1,5 @@
# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View file

@ -0,0 +1,5 @@
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from 'notifications'
end
end

View file

@ -19,6 +19,7 @@ class Backoffice::DossiersController < Backoffice::DossiersListController
def show
create_dossier_facade params[:id]
unless @facade.nil?
@champs_private = @facade.champs_private
@ -27,6 +28,8 @@ class Backoffice::DossiersController < Backoffice::DossiersListController
acc
end
end
Notification.where(dossier_id: params[:id].to_i).update_all already_read: true
end
def filter

View file

@ -1,9 +1,9 @@
class CommentairesController < ApplicationController
def index
@facade = DossierFacades.new(
params[:dossier_id],
(current_gestionnaire || current_user).email,
params[:champs_id]
params[:dossier_id],
(current_gestionnaire || current_user).email,
params[:champs_id]
)
render layout: false
rescue ActiveRecord::RecordNotFound
@ -48,12 +48,15 @@ class CommentairesController < ApplicationController
end
NotificationMailer.new_answer(@commentaire.dossier).deliver_now! if saved
redirect_to url_for(controller: 'backoffice/dossiers', action: :show, id: params['dossier_id'])
elsif current_user.email != @commentaire.dossier.user.email
invite = Invite.where(dossier: @commentaire.dossier, user: current_user).first
redirect_to url_for(controller: 'users/dossiers/invites', action: :show, id: invite.id)
else
redirect_to url_for(controller: :recapitulatif, action: :show, dossier_id: params['dossier_id'])
if current_user.email != @commentaire.dossier.user.email
invite = Invite.where(dossier: @commentaire.dossier, user: current_user).first
redirect_to url_for(controller: 'users/dossiers/invites', action: :show, id: invite.id)
else
redirect_to url_for(controller: :recapitulatif, action: :show, dossier_id: params['dossier_id'])
end
end
end

View file

@ -0,0 +1,8 @@
class NotificationDecorator < Draper::Decorator
delegate_all
def index_display
['champs', 'piece_justificative'].include?(type_notif) ? type = liste.join(" ") : type = liste.last
{ dossier: "Dossier n°#{dossier.id}", date: updated_at.strftime('%d/%m %H:%M'), type: type }
end
end

View file

@ -10,6 +10,10 @@ class DossierFacades
@dossier.decorate
end
def last_notifications
@dossier.notifications.order("updated_at DESC").limit(5)
end
def champs
@dossier.ordered_champs
end

View file

@ -26,8 +26,16 @@ class DossiersListFacades
current_devise_profil.dossiers.where(state: :initiated, archived: false).count
end
def new_dossier_number procedure_id
current_devise_profil.dossiers.where(state: :initiated, archived: false, procedure_id: procedure_id).count
end
def gestionnaire_procedures_name_and_id_list
@current_devise_profil.procedures.order('libelle ASC').inject([]) { |acc, procedure| acc.push({id: procedure.id, libelle: procedure.libelle}) }
@current_devise_profil.procedures.order('libelle ASC').inject([]) { |acc, procedure| acc.push({id: procedure.id, libelle: procedure.libelle, unread_notifications: @current_devise_profil.notifications_for(procedure)}) }
end
def unread_notifications
current_devise_profil.notifications
end
def procedure_id
@ -130,4 +138,4 @@ class DossiersListFacades
@procedure.nil? ? backoffice_dossiers_path(liste: liste) : backoffice_dossiers_procedure_path(id: @procedure.id, liste: liste)
end
end
end

View file

@ -2,6 +2,6 @@ class InviteDossierFacades < DossierFacades
#TODO rechercher en fonction de la personne/email
def initialize dossier_id, email
@dossier = (Invite.where(email: email).find(dossier_id)).dossier
@dossier = Invite.where(email: email, dossier_id: dossier_id).first!.dossier
end
end

View file

@ -0,0 +1,2 @@
class ApplicationJob < ActiveJob::Base
end

View file

@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

View file

@ -5,6 +5,8 @@ class Cerfa < ActiveRecord::Base
mount_uploader :content, CerfaUploader
validates :content, :file_size => {:maximum => 20.megabytes}
after_save :internal_notification, if: Proc.new { !dossier.nil? }
def empty?
content.blank?
end
@ -18,4 +20,12 @@ class Cerfa < ActiveRecord::Base
end
end
end
private
def internal_notification
unless dossier.state == 'draft'
NotificationService.new('cerfa', self.dossier.id).notify
end
end
end

View file

@ -5,6 +5,8 @@ class Champ < ActiveRecord::Base
delegate :libelle, :type_champ, :order_place, :mandatory, :description, :drop_down_list, to: :type_de_champ
after_save :internal_notification, if: Proc.new { !dossier.nil? }
def mandatory?
mandatory
end
@ -36,14 +38,22 @@ class Champ < ActiveRecord::Base
end
def self.regions
JSON.parse(Carto::GeoAPI::Driver.regions).sort_by{|e| e['nom']}.inject([]){|acc, liste| acc.push(liste['nom']) }
JSON.parse(Carto::GeoAPI::Driver.regions).sort_by { |e| e['nom'] }.inject([]) { |acc, liste| acc.push(liste['nom']) }
end
def self.departements
JSON.parse(Carto::GeoAPI::Driver.departements).inject([]){|acc, liste| acc.push(liste['code'] + ' - ' + liste['nom']) }.push('99 - Étranger')
JSON.parse(Carto::GeoAPI::Driver.departements).inject([]) { |acc, liste| acc.push(liste['code'] + ' - ' + liste['nom']) }.push('99 - Étranger')
end
def self.pays
JSON.parse(Carto::GeoAPI::Driver.pays).inject([]){|acc, liste| acc.push(liste['nom']) }
JSON.parse(Carto::GeoAPI::Driver.pays).inject([]) { |acc, liste| acc.push(liste['nom']) }
end
private
def internal_notification
unless dossier.state == 'draft'
NotificationService.new('champs', self.dossier.id, self.libelle).notify
end
end
end

View file

@ -4,7 +4,17 @@ class Commentaire < ActiveRecord::Base
belongs_to :piece_justificative
after_save :internal_notification
def header
"#{email}, " + created_at.localtime.strftime('%d %b %Y %H:%M')
end
private
def internal_notification
if email == dossier.user.email || dossier.invites_user.pluck(:email).to_a.include?(email)
NotificationService.new('commentaire', self.dossier.id).notify
end
end
end

View file

@ -27,6 +27,7 @@ class Dossier < ActiveRecord::Base
has_many :invites, dependent: :destroy
has_many :invites_user, class_name: 'InviteUser', dependent: :destroy
has_many :follows
has_many :notifications, dependent: :destroy
belongs_to :procedure
belongs_to :user
@ -41,6 +42,7 @@ class Dossier < ActiveRecord::Base
after_save :build_default_champs, if: Proc.new { procedure_id_changed? }
after_save :build_default_individual, if: Proc.new { procedure.for_individual? }
after_save :internal_notification
validates :user, presence: true
@ -326,4 +328,12 @@ class Dossier < ActiveRecord::Base
def invite_by_user? email
(invites_user.pluck :email).include? email
end
private
def internal_notification
if state_changed? && state == 'submitted'
NotificationService.new('submitted', self.id).notify
end
end
end

View file

@ -63,6 +63,22 @@ class Gestionnaire < ActiveRecord::Base
PreferenceSmartListingPage.create(page: 1, procedure: nil, gestionnaire: self, liste: 'a_traiter')
end
def notifications
Notification.where(already_read: false, dossier_id: follows.pluck(:dossier_id) ).order("updated_at DESC")
end
def notifications_for procedure
procedure_ids = dossiers_follow.pluck(:procedure_id)
if procedure_ids.include?(procedure.id)
return dossiers_follow.where(procedure_id: procedure.id)
.inject(0) do |acc, dossier|
acc += dossier.notifications.where(already_read: false).count
end
end
0
end
private
def valid_couple_table_attr? table, column

View file

@ -0,0 +1,19 @@
class Notification < ActiveRecord::Base
belongs_to :dossier
# after_save :broadcast_notification
enum type_notif: {
commentaire: 'commentaire',
cerfa: 'cerfa',
piece_justificative: 'piece_justificative',
champs: 'champs',
submitted: 'submitted'
}
# def broadcast_notification
# ActionCable.server.broadcast 'notifications',
# message: "Dossier n°#{self.dossier.id} : #{self.liste.last}",
# dossier: {id: self.dossier.id}
# end
end

View file

@ -13,6 +13,8 @@ class PieceJustificative < ActiveRecord::Base
validates :content, :file_size => {:maximum => 20.megabytes}
validates :content, presence: true, allow_blank: false, allow_nil: false
after_save :internal_notification, if: Proc.new { !dossier.nil? }
def empty?
content.blank?
end
@ -43,4 +45,12 @@ class PieceJustificative < ActiveRecord::Base
image/jpeg
"
end
private
def internal_notification
unless self.type_de_piece_justificative.nil? && dossier.state == 'draft'
NotificationService.new('piece_justificative', self.dossier.id, self.libelle).notify
end
end
end

View file

@ -19,9 +19,9 @@ class ChampsService
end
end
champ.save
champ.save if champ.changed?
end
errors
end
end
end

View file

@ -0,0 +1,42 @@
class NotificationService
def initialize type_notif, dossier_id, attribut_change=''
@type_notif = type_notif
@dossier_id = dossier_id
notification.liste.push text_for_notif attribut_change
notification.liste = notification.liste.uniq
self
end
def notify
notification.save
end
def notification
@notification ||=
begin
Notification.find_by! dossier_id: @dossier_id, already_read: false, type_notif: @type_notif
rescue ActiveRecord::RecordNotFound
Notification.new dossier_id: @dossier_id, type_notif: @type_notif, liste: []
end
end
def text_for_notif attribut=''
case @type_notif
when 'commentaire'
"#{notification.liste.size + 1} nouveau(x) commentaire(s) déposé(s)."
when 'cerfa'
"Un nouveau formulaire a été déposé."
when 'piece_justificative'
attribut
when 'champs'
attribut
when 'submitted'
"Le dossier n°#{@dossier_id} a été déposé."
else
'Notification par défaut'
end
end
end

View file

@ -35,4 +35,4 @@ class SyncCredentialsService
end
end
end
end
end

View file

@ -1,5 +1,8 @@
%table#dossiers_list.table
%thead
- if smart_listing.name.to_s == 'follow_dossiers'
%th
%i.fa.fa-bell
- @facade_data_view.preference_list_dossiers_filter.each do |preference|
%th{class: "col-md-#{preference.bootstrap_lg} col-lg-#{preference.bootstrap_lg}"}
- if preference.table.to_s.include? 'champs'
@ -16,6 +19,15 @@
- unless smart_listing.empty?
- smart_listing.collection.each do |dossier|
%tr.dossier-row{id: "tr_dossier_#{dossier.id}", 'data-dossier_url' => backoffice_dossier_url(id: dossier.id)}
- if smart_listing.name.to_s == 'follow_dossiers'
%td.center
- total_notif = dossier.notifications.where(already_read: false).count
- if total_notif == 0
.badge.progress-bar-default
= total_notif
- else
.badge.progress-bar-warning
= total_notif
- @facade_data_view.preference_list_dossiers_filter.each_with_index do |preference, index|
%td
- if preference.table.nil? || preference.table.empty?

View file

@ -0,0 +1 @@
#notification_alert.alert.alert-success

View file

@ -9,6 +9,7 @@
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
= action_cable_meta_tag
%body
= render partial: 'layouts/support_navigator_banner'
#beta{class:(Rails.env == 'production' ? '' : 'beta_staging')}
@ -49,6 +50,7 @@
%i.fa.fa-times{style:'position: fixed; top: 10; right: 30; color: white;'}
= render partial: 'layouts/switch_devise_profile_module'
= render partial: 'layouts/notifications_alert'
= render partial: 'layouts/footer', locals: {main_container_size: main_container_size}
= render partial: 'layouts/google_analytics'

View file

@ -9,6 +9,10 @@
%div#action-block
%div#menu-block
%div.split-hr-left
#switch-buttons
#switch-procedures.active Procédures
#switch-notifications Notifications
%div#infos-block
%div.split-hr-left
@ -17,3 +21,17 @@
= link_to backoffice_dossiers_procedure_path(procedure[:id]), {title: procedure[:libelle]} do
%div.procedure_list_element{ class: ('active' if procedure[:id] == @facade_data_view.procedure.id rescue '') }
= truncate(procedure[:libelle], length: 50)
- total_new = @facade_data_view.new_dossier_number procedure[:id]
- if total_new > 0
.badge.progress-bar-success{title:'Nouveaux dossiers'}
= total_new
-if procedure[:unread_notifications] > 0
.badge.progress-bar-warning{title: 'Notifications'}
= procedure[:unread_notifications]
#notifications_list.hidden
- @facade_data_view.unread_notifications.each do |notification|
= link_to backoffice_dossier_path(notification.dossier.id) do
.notification
.dossier= notification.decorate.index_display[:dossier]
.updated-at= notification.decorate.index_display[:date]
.type= notification.decorate.index_display[:type]

View file

@ -1,6 +1,5 @@
%div#first-block
%div.infos
%div.projet-name #{@facade.dossier.nom_projet.capitalize rescue nil}
#dossier_id= t('dynamics.dossiers.numéro') + @facade.dossier.id.to_s
%div#action-block
@ -30,6 +29,24 @@
%div.split-hr-left
%div.dossier-state= @facade.dossier.display_state
%div.split-hr-left
%div.tips.hidden
%i.fa.fa-lightbulb-o
%div.notice= "Ceci est un bloc destiné à contenir des informations sur ce que vous êtes censé pouvoir faire à ce stade de traitement du dossier."
%div.notifications
- if @facade.dossier.notifications.empty?
= "Aucune notification pour le moment."
- else
%i.fa.fa-bell-o
- @facade.last_notifications.each do |notification|
.notification
.updated-at= notification.updated_at.strftime('%d/%m/%Y %H:%M')
- if ['champs'].include?(notification.type_notif)
- if notification.liste.size > 1
.type= "Plusieurs attributs ont été changés, dont: #{notification.liste.join(" ")}"
- else
.type= "Un attribut à été changé: #{notification.liste.last}"
- elsif ['piece_justificative'].include?(notification.type_notif)
- if notification.liste.size > 1
.type= "Plusieurs pièces justificatives ont été changés, dont: #{notification.liste.join(" ")}"
- else
.type= "Une pièce justificative à été changée: #{notification.liste.last}"
- else
.type= notification.liste.last
.split-hr

View file

@ -2,7 +2,6 @@
%div.en-cours
%h2 Récapitulatif
%div.infos
%div #{@facade.dossier.nom_projet}
%div= t('dynamics.dossiers.numéro') + @facade.dossier.id.to_s
%div#action-block

View file

@ -1,4 +1,8 @@
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
# Action Cable requires that all classes are loaded in advance
Rails.application.eager_load!
run Rails.application

10
config/cable.yml Normal file
View file

@ -0,0 +1,10 @@
production:
adapter: redis
url: redis://localhost:6379
development:
adapter: redis
url: redis://localhost:6379
test:
adapter: async

View file

@ -45,5 +45,5 @@ Rails.application.configure do
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
config.action_cable.url = "ws://localhost:3000/cable"
end

View file

@ -26,10 +26,6 @@ fr:
mail_contact:
blank: 'doit être rempli'
invalid: 'est incorrect'
nom_projet:
blank: 'doit être rempli'
description:
blank: 'doit être remplie'
montant_projet:
blank: 'doit être rempli'
montant_aide_demande:

View file

@ -202,4 +202,6 @@ Rails.application.routes.draw do
end
apipie
mount ActionCable.server => '/cable'
end

109
config/unicorn.rb Normal file
View file

@ -0,0 +1,109 @@
# Sample verbose configuration file for Unicorn (not Rack)
#
# This configuration file documents many features of Unicorn
# that may not be needed for some applications. See
# http://unicorn.bogomips.org/examples/unicorn.conf.minimal.rb
# for a much simpler configuration file.
#
# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.
# Use at least one worker per core if you're on a dedicated server,
# more will usually help for _short_ waits on databases/caches.
worker_processes 2
# Since Unicorn is never exposed to outside clients, it does not need to
# run on the standard HTTP port (80), there is no reason to start Unicorn
# as root unless it's from system init scripts.
# If running the master process as root and the workers as an unprivileged
# user, do this to switch euid/egid in the workers (also chowns logs):
# user "unprivileged_user", "unprivileged_group"
# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
listen "127.0.0.1:3000", :tcp_nopush => true
# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 30
# By default, the Unicorn logger will write to stderr.
# Additionally, ome applications/frameworks log to stderr or stdout,
# so prevent them from going to /dev/null when daemonized here:
# combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
GC.copy_on_write_friendly = true
# Enable this flag to have unicorn test client connections by writing the
# beginning of the HTTP headers before calling the application. This
# prevents calling the application for connections that have disconnected
# while queued. This is only guaranteed to detect clients on the same
# host unicorn runs on, and unlikely to detect disconnects even on a
# fast LAN.
check_client_connection false
# local variable to guard against running a hook multiple times
run_once = true
before_fork do |server, worker|
# the following is highly recomended for Rails + "preload_app true"
# as there's no need for the master process to hold a connection
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
# Occasionally, it may be necessary to run non-idempotent code in the
# master before forking. Keep in mind the above disconnect! example
# is idempotent and does not need a guard.
if run_once
# do_something_once_here ...
run_once = false # prevent from firing again
end
# The following is only recommended for memory/DB-constrained
# installations. It is not needed if your system can house
# twice as many worker_processes as you have configured.
#
# # This allows a new master process to incrementally
# # phase out the old master process with SIGTTOU to avoid a
# # thundering herd (especially in the "preload_app false" case)
# # when doing a transparent upgrade. The last worker spawned
# # will then kill off the old master process with a SIGQUIT.
old_pid = "#{server.config[:pid]}.oldbin"
if old_pid != server.pid
begin
sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
Process.kill(sig, File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
#
# Throttle the master from forking too quickly by sleeping. Due
# to the implementation of standard Unix signal handlers, this
# helps (but does not completely) prevent identical, repeated signals
# from being lost when the receiving process is busy.
sleep 1
end
after_fork do |server, worker|
# per-process listener ports for debugging/admin/migrations
# addr = "127.0.0.1:#{9293 + worker.nr}"
# server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
# the following is *required* for Rails + "preload_app true",
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
# if preload_app is true, then you may also want to check and
# restart any other shared sockets/descriptors such as Memcached,
# and Redis. TokyoCabinet file handles are safe to reuse
# between any number of forked children (assuming your kernel
# correctly implements pread()/pwrite() system calls)
end

View file

@ -0,0 +1,16 @@
class CreateNotification < ActiveRecord::Migration[5.0]
def change
create_table :notifications do |t|
t.boolean :already_read, default: false
t.string :liste, array: true
t.boolean :multiple, default: false
t.string :type_notif
t.datetime :created_at
t.datetime :updated_at
end
add_belongs_to :notifications, :dossier
end
end

View file

@ -0,0 +1,7 @@
class DeleteOldAttrInDataBase < ActiveRecord::Migration[5.0]
def change
remove_column :dossiers, :nom_projet
remove_column :procedures, :test
remove_column :notifications, :multiple
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: 20161205110427) do
ActiveRecord::Schema.define(version: 20161227103823) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -118,7 +118,6 @@ ActiveRecord::Schema.define(version: 20161205110427) do
create_table "dossiers", force: :cascade do |t|
t.boolean "autorisation_donnees"
t.string "nom_projet"
t.integer "procedure_id"
t.datetime "created_at"
t.datetime "updated_at"
@ -249,6 +248,16 @@ ActiveRecord::Schema.define(version: 20161205110427) do
t.index ["procedure_id"], name: "index_module_api_cartos_on_procedure_id", unique: true, using: :btree
end
create_table "notifications", force: :cascade do |t|
t.boolean "already_read", default: false
t.string "liste", array: true
t.string "type_notif"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "dossier_id"
t.index ["dossier_id"], name: "index_notifications_on_dossier_id", using: :btree
end
create_table "pieces_justificatives", force: :cascade do |t|
t.string "content"
t.integer "dossier_id"
@ -302,7 +311,6 @@ ActiveRecord::Schema.define(version: 20161205110427) do
t.string "lien_demarche"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "test"
t.integer "administrateur_id"
t.boolean "archived", default: false
t.boolean "euro_flag", default: false

View file

@ -36,6 +36,10 @@ describe Backoffice::CommentairesController, type: :controller do
expect { subject }.to change(Follow, :count).by(0)
end
end
it 'Internal notification is not create' do
expect { subject }.to change(Notification, :count).by (0)
end
end
context 'when document is upload whith a commentaire', vcr: {cassette_name: 'controllers_backoffice_commentaires_controller_doc_upload_with_comment'} do
@ -54,6 +58,10 @@ describe Backoffice::CommentairesController, type: :controller do
subject
end
it 'Internal notification is not create' do
expect { subject }.to change(Notification, :count).by (0)
end
describe 'piece justificative created' do
let(:pj) { PieceJustificative.last }

View file

@ -38,34 +38,40 @@ describe Backoffice::DossiersController, type: :controller do
end
describe 'GET #show' do
subject { get :show, params: {id: dossier_id} }
context 'gestionnaire is connected' do
before do
sign_in gestionnaire
end
it 'returns http success' do
get :show, params: {id: dossier_id}
expect(response).to have_http_status(200)
expect(subject).to have_http_status(200)
end
describe 'all notifications unread are changed' do
it do
expect(Notification).to receive(:where).with(dossier_id: dossier_id).and_return(Notification::ActiveRecord_Relation)
expect(Notification::ActiveRecord_Relation).to receive(:update_all).with(already_read: true).and_return(true)
subject
end
end
context ' when dossier is archived' do
before do
get :show, params: {id: dossier_archived.id}
end
it { expect(response).to redirect_to('/backoffice') }
let(:dossier_id) { dossier_archived }
it { expect(subject).to redirect_to('/backoffice') }
end
context 'when dossier id does not exist' do
before do
get :show, params: {id: bad_dossier_id}
end
it { expect(response).to redirect_to('/backoffice') }
let(:dossier_id) { bad_dossier_id }
it { expect(subject).to redirect_to('/backoffice') }
end
end
context 'gestionnaire does not connected but dossier id is correct' do
subject { get :show, params: {id: dossier_id} }
it { is_expected.to redirect_to('/gestionnaires/sign_in') }
end
end

View file

@ -28,6 +28,10 @@ describe Users::CommentairesController, type: :controller do
subject
end
it 'Notification interne is create' do
expect { subject }.to change(Notification, :count).by (1)
end
end
context 'when document is upload whith a commentaire', vcr: {cassette_name: 'controllers_sers_commentaires_controller_upload_doc'} do

View file

@ -145,6 +145,13 @@ shared_examples 'description_controller_spec' do
end
context 'Quand la procédure accepte les CERFA' do
subject { post :create, params: {dossier_id: dossier_id,
cerfa_pdf: cerfa_pdf} }
it 'Notification interne is create' do
expect { subject }.to change(Notification, :count).by (1)
end
context 'Sauvegarde du CERFA PDF', vcr: {cassette_name: 'controllers_users_description_controller_save_cerfa'} do
before do
post :create, params: {dossier_id: dossier_id,
@ -292,6 +299,10 @@ shared_examples 'description_controller_spec' do
sign_in guest
end
it 'Notification interne is create' do
expect { subject }.to change(Notification, :count).by (1)
end
context 'when PJ have no documents' do
it { expect(dossier.pieces_justificatives.size).to eq 0 }

View file

@ -7,7 +7,7 @@ describe Users::DescriptionController, type: :controller, vcr: {cassette_name: '
let(:invite_by_user) { create :user, email: 'invite@plop.com' }
let(:procedure) { create(:procedure, :with_two_type_de_piece_justificative, :with_type_de_champ, :with_datetime, cerfa_flag: true) }
let(:dossier) { create(:dossier, procedure: procedure, user: owner_user) }
let(:dossier) { create(:dossier, procedure: procedure, user: owner_user, state: 'initiated') }
let(:dossier_id) { dossier.id }
let(:bad_dossier_id) { Dossier.count + 10000 }

View file

@ -1,4 +1,4 @@
RSpec.describe Users::Dossiers::InvitesController, type: :controller do
describe Users::Dossiers::InvitesController, type: :controller do
describe '#authenticate_user!' do
let(:user) { create :user }
let(:invite) { create :invite }

View file

@ -82,6 +82,10 @@ describe Users::RecapitulatifController, type: :controller do
dossier.validated!
post :submit, params: {dossier_id: dossier.id}
end
it 'Internal notification is created' do
expect(Notification.where(dossier_id: dossier.id, type_notif: 'submitted').first).not_to be_nil
end
end
end
end

View file

@ -50,10 +50,12 @@ describe DossiersListFacades do
it { expect(subject.first[:id]).to eq procedure.id }
it { expect(subject.first[:libelle]).to eq procedure.libelle }
it { expect(subject.first[:unread_notifications]).to eq 0 }
it { expect(subject.last[:id]).to eq procedure_2.id }
it { expect(subject.last[:libelle]).to eq procedure_2.libelle }
it { expect(subject.last[:unread_notifications]).to eq 0 }
end
describe '#active_filter?' do

View file

@ -1,5 +1,11 @@
FactoryGirl.define do
factory :commentaire do
body 'plop'
before(:create) do |commentaire, _evaluator|
unless commentaire.dossier
commentaire.dossier = create :dossier
end
end
end
end

View file

@ -1,5 +1,5 @@
FactoryGirl.define do
sequence(:gestionnaire_email) { |n| "plop#{n}@plop.com" }
sequence(:gestionnaire_email) { |n| "gest#{n}@plop.com" }
factory :gestionnaire do
email { generate(:gestionnaire_email) }
password 'password'

View file

@ -0,0 +1,12 @@
FactoryGirl.define do
factory :notification do
type_notif 'commentaire'
liste []
before(:create) do |notification, _evaluator|
unless notification.dossier
notification.dossier = create :dossier
end
end
end
end

View file

@ -5,7 +5,6 @@ describe Dossier do
describe 'database columns' do
it { is_expected.to have_db_column(:autorisation_donnees) }
it { is_expected.to have_db_column(:nom_projet) }
it { is_expected.to have_db_column(:created_at) }
it { is_expected.to have_db_column(:updated_at) }
it { is_expected.to have_db_column(:state) }
@ -27,6 +26,7 @@ describe Dossier do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:invites) }
it { is_expected.to have_many(:follows) }
it { is_expected.to have_many(:notifications) }
end
describe 'delegation' do

View file

@ -208,4 +208,40 @@ describe Gestionnaire, type: :model do
expect(admin.valid_password?('super secret')).to be(true)
end
end
describe '#notifications_for' do
subject { gestionnaire.notifications_for procedure }
context 'when gestionnaire follow any dossier' do
it { is_expected.to eq 0 }
it { expect(gestionnaire.follows.count).to eq 0 }
it { expect_any_instance_of(Dossier::ActiveRecord_AssociationRelation).not_to receive(:inject)
subject }
end
context 'when gestionnaire follow any dossier into the procedure past in params' do
before do
create :follow, gestionnaire: gestionnaire, dossier: create(:dossier, procedure: procedure_2)
end
it { is_expected.to eq 0 }
it { expect(gestionnaire.follows.count).to eq 1 }
it { expect_any_instance_of(Dossier::ActiveRecord_AssociationRelation).not_to receive(:inject)
subject }
end
context 'when gestionnaire follow a dossier with a notification into the procedure past in params' do
let(:dossier) { create(:dossier, procedure: procedure, state: 'initiated') }
before do
create :follow, gestionnaire: gestionnaire, dossier: dossier
create :notification, dossier: dossier
end
it { is_expected.to eq 1 }
it { expect(gestionnaire.follows.count).to eq 1 }
it { expect_any_instance_of(Dossier::ActiveRecord_AssociationRelation).to receive(:inject)
subject }
end
end
end

View file

@ -0,0 +1,11 @@
require 'spec_helper'
describe Notification do
it { is_expected.to have_db_column(:already_read) }
it { is_expected.to have_db_column(:liste) }
it { is_expected.to have_db_column(:type_notif) }
it { is_expected.to have_db_column(:created_at) }
it { is_expected.to have_db_column(:updated_at) }
it { is_expected.to belong_to(:dossier) }
end

View file

@ -0,0 +1,29 @@
require 'spec_helper'
describe NotificationService do
describe '.notify' do
let(:dossier) { create :dossier }
let(:service) { described_class.new type_notif, dossier.id }
subject { service.notify }
context 'when is the first notification for dossier_id and type_notif and alread_read is false' do
let(:type_notif) { 'commentaire' }
it { expect { subject }.to change(Notification, :count).by (1) }
context 'when is not the first notification' do
before do
create :notification, dossier: dossier, type_notif: type_notif
end
it { expect { subject }.to change(Notification, :count).by (0) }
end
end
end
describe 'text_for_notif' do
pending
end
end

View file

@ -41,7 +41,7 @@ Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, js_errors: true, port: 44_678 + ENV['TEST_ENV_NUMBER'].to_i, phantomjs_options: ['--proxy-type=none'], timeout: 180)
end
#ActiveSupport::Deprecation.silenced = true
ActiveSupport::Deprecation.silenced = true
Capybara.default_max_wait_time = 1

View file

@ -29,6 +29,6 @@ describe 'admin/gestionnaires/index.html.haml', type: :view do
array: true))
render
end
it { expect(rendered).to match(/plop\d+@plop.com/) }
it { expect(rendered).to match(/gest\d+@plop.com/) }
end
end