diff --git a/Gemfile b/Gemfile index bb142816d..6484c2565 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 3d8969e9e..ebf23e02c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js new file mode 100644 index 000000000..9139c80fd --- /dev/null +++ b/app/assets/javascripts/cable.js @@ -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); diff --git a/app/assets/javascripts/channels/notifications.js b/app/assets/javascripts/channels/notifications.js new file mode 100644 index 000000000..78248d290 --- /dev/null +++ b/app/assets/javascripts/channels/notifications.js @@ -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); +} \ No newline at end of file diff --git a/app/assets/javascripts/dossiers.js b/app/assets/javascripts/dossiers.js index adc57a499..c294b6c16 100644 --- a/app/assets/javascripts/dossiers.js +++ b/app/assets/javascripts/dossiers.js @@ -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"); diff --git a/app/assets/javascripts/dossiers_list_link.js b/app/assets/javascripts/dossiers_list_link.js index 1764dd571..09fb31542 100644 --- a/app/assets/javascripts/dossiers_list_link.js +++ b/app/assets/javascripts/dossiers_list_link.js @@ -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')) - }); -} \ No newline at end of file + $('#dossiers_list tr').on('click', function () { + $(location).attr('href', $(this).data('dossier_url')) + }); +} diff --git a/app/assets/stylesheets/dossiers.scss b/app/assets/stylesheets/dossiers.scss index 61dcf0a52..31fe3e57b 100644 --- a/app/assets/stylesheets/dossiers.scss +++ b/app/assets/stylesheets/dossiers.scss @@ -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; } diff --git a/app/assets/stylesheets/left_pannel.scss b/app/assets/stylesheets/left_pannel.scss index 7debdf58c..08aadf3a1 100644 --- a/app/assets/stylesheets/left_pannel.scss +++ b/app/assets/stylesheets/left_pannel.scss @@ -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; + } } } } diff --git a/app/assets/stylesheets/notification_alert.scss b/app/assets/stylesheets/notification_alert.scss new file mode 100644 index 000000000..caf95bdf9 --- /dev/null +++ b/app/assets/stylesheets/notification_alert.scss @@ -0,0 +1,12 @@ +#notification_alert { + position: fixed; + top: 20px; + right: -250px; + + z-index: 1000; + + width: 250px; + height: 80px; + + border: solid black 1px; +} \ No newline at end of file diff --git a/app/assets/stylesheets/search.scss b/app/assets/stylesheets/search.scss index 969d5b7a6..ce3219ac5 100644 --- a/app/assets/stylesheets/search.scss +++ b/app/assets/stylesheets/search.scss @@ -1,5 +1,5 @@ #search-block{ - margin: 15px 10px 0 0; + margin: 15px 10px 0 10px; height: 30px; } diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 000000000..d56fa30f4 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -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 diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 000000000..b4f41389a --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -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 diff --git a/app/channels/notifications_channel.rb b/app/channels/notifications_channel.rb new file mode 100644 index 000000000..a57c0f242 --- /dev/null +++ b/app/channels/notifications_channel.rb @@ -0,0 +1,5 @@ +class NotificationsChannel < ApplicationCable::Channel + def subscribed + stream_from 'notifications' + end +end \ No newline at end of file diff --git a/app/controllers/backoffice/dossiers_controller.rb b/app/controllers/backoffice/dossiers_controller.rb index 3537d6c5e..73fd4ec13 100644 --- a/app/controllers/backoffice/dossiers_controller.rb +++ b/app/controllers/backoffice/dossiers_controller.rb @@ -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 diff --git a/app/controllers/commentaires_controller.rb b/app/controllers/commentaires_controller.rb index 06c7edc19..cdf3cb747 100644 --- a/app/controllers/commentaires_controller.rb +++ b/app/controllers/commentaires_controller.rb @@ -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 diff --git a/app/decorators/notification_decorator.rb b/app/decorators/notification_decorator.rb new file mode 100644 index 000000000..911471e1a --- /dev/null +++ b/app/decorators/notification_decorator.rb @@ -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 diff --git a/app/facades/dossier_facades.rb b/app/facades/dossier_facades.rb index b0b622ae6..6893f4bcb 100644 --- a/app/facades/dossier_facades.rb +++ b/app/facades/dossier_facades.rb @@ -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 diff --git a/app/facades/dossiers_list_facades.rb b/app/facades/dossiers_list_facades.rb index bed18c852..d32cb0eb4 100644 --- a/app/facades/dossiers_list_facades.rb +++ b/app/facades/dossiers_list_facades.rb @@ -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 \ No newline at end of file +end diff --git a/app/facades/invite_dossier_facades.rb b/app/facades/invite_dossier_facades.rb index 63e9b5a61..cda91257e 100644 --- a/app/facades/invite_dossier_facades.rb +++ b/app/facades/invite_dossier_facades.rb @@ -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 \ No newline at end of file diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 000000000..a009ace51 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 000000000..10a4cba84 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/cerfa.rb b/app/models/cerfa.rb index 0022ad244..6d6d0c702 100644 --- a/app/models/cerfa.rb +++ b/app/models/cerfa.rb @@ -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 \ No newline at end of file diff --git a/app/models/champ.rb b/app/models/champ.rb index f6ba23268..e4a6e820e 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -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 diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb index 463285482..09c147dd4 100644 --- a/app/models/commentaire.rb +++ b/app/models/commentaire.rb @@ -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 diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 344a583e7..8680dc64c 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -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 diff --git a/app/models/gestionnaire.rb b/app/models/gestionnaire.rb index d98b2e742..580dc1b96 100644 --- a/app/models/gestionnaire.rb +++ b/app/models/gestionnaire.rb @@ -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 diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 000000000..4ff810f37 --- /dev/null +++ b/app/models/notification.rb @@ -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 diff --git a/app/models/piece_justificative.rb b/app/models/piece_justificative.rb index e1a0df9c8..0c2056b6a 100644 --- a/app/models/piece_justificative.rb +++ b/app/models/piece_justificative.rb @@ -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 diff --git a/app/services/champs_service.rb b/app/services/champs_service.rb index faf25cf7c..04fdf9602 100644 --- a/app/services/champs_service.rb +++ b/app/services/champs_service.rb @@ -19,9 +19,9 @@ class ChampsService end end - champ.save + champ.save if champ.changed? end errors end -end \ No newline at end of file +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb new file mode 100644 index 000000000..01bfe97e3 --- /dev/null +++ b/app/services/notification_service.rb @@ -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 diff --git a/app/services/sync_credentials_service.rb b/app/services/sync_credentials_service.rb index 690323759..491ec1d55 100644 --- a/app/services/sync_credentials_service.rb +++ b/app/services/sync_credentials_service.rb @@ -35,4 +35,4 @@ class SyncCredentialsService end end end -end \ No newline at end of file +end diff --git a/app/views/admin/procedures/_list.html.haml b/app/views/admin/procedures/_list.html.haml index d9d054d03..31879e2f6 100644 --- a/app/views/admin/procedures/_list.html.haml +++ b/app/views/admin/procedures/_list.html.haml @@ -19,7 +19,7 @@ %td = procedure.created_at_fr %td - = link_to('Cloner', admin_procedure_clone_path(procedure.id), 'data-method' => :put, class: 'btn-sm btn-primary') + = link_to('Cloner', admin_procedure_clone_path(procedure.id), 'data-method' => :put, class: 'btn-sm btn-primary clone-btn') - unless procedure.published? || procedure.archived? = link_to('X', url_for(controller: 'admin/procedures', action: :destroy, id: procedure.id), 'data-method' => :delete, class: 'btn-sm btn-danger') diff --git a/app/views/backoffice/dossiers/_list.html.haml b/app/views/backoffice/dossiers/_list.html.haml index 9e3249b3c..43e50eff7 100644 --- a/app/views/backoffice/dossiers/_list.html.haml +++ b/app/views/backoffice/dossiers/_list.html.haml @@ -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? diff --git a/app/views/dossiers/_invites.html.haml b/app/views/dossiers/_invites.html.haml index 2474ad7f5..72307147e 100644 --- a/app/views/dossiers/_invites.html.haml +++ b/app/views/dossiers/_invites.html.haml @@ -12,7 +12,7 @@ Aucune personne invitée .col-md-3.col-sm-3.col-xs-3.col-lg-3 - =form_tag invites_dossier_path(dossier_id: @facade.dossier.id), method: :post, class: 'form-inline' do - =text_field_tag :email, '', class: 'form-control', placeholder: 'Envoyer une invitation' - =submit_tag 'Ajouter', class: 'btn btn-success' + = form_tag invites_dossier_path(dossier_id: @facade.dossier.id), method: :post, class: 'form-inline' do + = text_field_tag :email, '', class: 'form-control', placeholder: 'Envoyer une invitation' + = submit_tag 'Ajouter', class: 'btn btn-success', id: 'send-invitation' diff --git a/app/views/layouts/_notifications_alert.html.haml b/app/views/layouts/_notifications_alert.html.haml new file mode 100644 index 000000000..372fe7497 --- /dev/null +++ b/app/views/layouts/_notifications_alert.html.haml @@ -0,0 +1 @@ +#notification_alert.alert.alert-success diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 32f224676..1730f61a7 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -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' diff --git a/app/views/layouts/left_panels/_left_panel_backoffice_dossierscontroller_index.html.haml b/app/views/layouts/left_panels/_left_panel_backoffice_dossierscontroller_index.html.haml index 037f35a33..e715a6ae6 100644 --- a/app/views/layouts/left_panels/_left_panel_backoffice_dossierscontroller_index.html.haml +++ b/app/views/layouts/left_panels/_left_panel_backoffice_dossierscontroller_index.html.haml @@ -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] diff --git a/app/views/layouts/left_panels/_left_panel_backoffice_dossierscontroller_show.html.haml b/app/views/layouts/left_panels/_left_panel_backoffice_dossierscontroller_show.html.haml index a47e64175..6c2fd2e66 100644 --- a/app/views/layouts/left_panels/_left_panel_backoffice_dossierscontroller_show.html.haml +++ b/app/views/layouts/left_panels/_left_panel_backoffice_dossierscontroller_show.html.haml @@ -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 diff --git a/app/views/layouts/left_panels/_left_panel_users_recapitulatifcontroller_show.html.haml b/app/views/layouts/left_panels/_left_panel_users_recapitulatifcontroller_show.html.haml index bc0ebdb0f..23397d47d 100644 --- a/app/views/layouts/left_panels/_left_panel_users_recapitulatifcontroller_show.html.haml +++ b/app/views/layouts/left_panels/_left_panel_users_recapitulatifcontroller_show.html.haml @@ -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 diff --git a/app/views/layouts/navbars/_navbar_backoffice_dossierscontroller_show.html.haml b/app/views/layouts/navbars/_navbar_backoffice_dossierscontroller_show.html.haml index 37d5c5a22..744d4e583 100644 --- a/app/views/layouts/navbars/_navbar_backoffice_dossierscontroller_show.html.haml +++ b/app/views/layouts/navbars/_navbar_backoffice_dossierscontroller_show.html.haml @@ -14,7 +14,7 @@ Suivre le dossier %div.row %div.col-lg-12.col-md-12.col-sm-12.col-xs-12 - %div.dropdown-toggle{ 'data-toggle' => 'dropdown', 'aria-haspopup' => true, 'aria-expanded' => false } + %div#invitations.dropdown-toggle{ 'data-toggle' => 'dropdown', 'aria-haspopup' => true, 'aria-expanded' => false } %i.fa.fa-user = t('utils.involved') %div.dropdown-menu.dropdown-menu-right.dropdown-pannel @@ -34,7 +34,6 @@ = t('dynamics.dossiers.invites.empty') %li - =form_tag invites_dossier_path(dossier_id: @facade.dossier.id), method: :post, class: 'form-inline' do - =text_field_tag :email, '', class: 'form-control', placeholder: 'Envoyer une invitation' - =submit_tag 'Ajouter', class: 'btn btn-success' - + = form_tag invites_dossier_path(dossier_id: @facade.dossier.id), method: :post, class: 'form-inline', id: 'send-invitation' do + = text_field_tag :email, '', class: 'form-control', placeholder: 'Envoyer une invitation', id: 'invitation-email' + = submit_tag 'Ajouter', class: 'btn btn-success' diff --git a/app/views/layouts/navbars/_navbar_users_recapitulatifcontroller_show.html.haml b/app/views/layouts/navbars/_navbar_users_recapitulatifcontroller_show.html.haml index 18dfdf885..ba834c09f 100644 --- a/app/views/layouts/navbars/_navbar_users_recapitulatifcontroller_show.html.haml +++ b/app/views/layouts/navbars/_navbar_users_recapitulatifcontroller_show.html.haml @@ -4,7 +4,7 @@ %div.col-lg-3.col-md-3.col-sm-3.col-xs-3.options %div.row.centered-option %div.col-lg-12.col-md-12.col-sm-12.col-xs-12 - %div.dropdown-toggle{ 'data-toggle' => 'dropdown', 'aria-haspopup' => true, 'aria-expanded' => false } + %div#invitations.dropdown-toggle{ 'data-toggle' => 'dropdown', 'aria-haspopup' => true, 'aria-expanded' => false } %i.fa.fa-user = t('utils.involved') %div.dropdown-menu.dropdown-menu-right.dropdown-pannel @@ -24,6 +24,6 @@ = t('dynamics.dossiers.invites.empty') %li - =form_tag invites_dossier_path(dossier_id: @facade.dossier.id), method: :post, class: 'form-inline' do - =text_field_tag :email, '', class: 'form-control', placeholder: 'Envoyer une invitation' - =submit_tag 'Ajouter', class: 'btn btn-success' + = form_tag invites_dossier_path(dossier_id: @facade.dossier.id), method: :post, class: 'form-inline', id: 'send-invitation' do + = text_field_tag :email, '', class: 'form-control', placeholder: 'Envoyer une invitation', id: 'invitation-email' + = submit_tag 'Ajouter', class: 'btn btn-success' diff --git a/config.ru b/config.ru index bd83b2541..584d0390c 100644 --- a/config.ru +++ b/config.ru @@ -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 diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 000000000..1aeb76f7c --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +production: + adapter: redis + url: redis://localhost:6379 + +development: + adapter: redis + url: redis://localhost:6379 + +test: + adapter: async diff --git a/config/environments/development.rb b/config/environments/development.rb index f7174bfa2..4b96a6c33 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -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 diff --git a/config/locales/models/dossier/fr.yml b/config/locales/models/dossier/fr.yml index 9663b2795..576c47885 100644 --- a/config/locales/models/dossier/fr.yml +++ b/config/locales/models/dossier/fr.yml @@ -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: diff --git a/config/routes.rb b/config/routes.rb index 58d2811dc..aaef99490 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -202,4 +202,6 @@ Rails.application.routes.draw do end apipie + + mount ActionCable.server => '/cable' end diff --git a/config/unicorn.rb b/config/unicorn.rb new file mode 100644 index 000000000..88a5d746a --- /dev/null +++ b/config/unicorn.rb @@ -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 diff --git a/db/migrate/20161221153929_create_notification.rb b/db/migrate/20161221153929_create_notification.rb new file mode 100644 index 000000000..745086412 --- /dev/null +++ b/db/migrate/20161221153929_create_notification.rb @@ -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 diff --git a/db/migrate/20161227103823_delete_old_attr_in_data_base.rb b/db/migrate/20161227103823_delete_old_attr_in_data_base.rb new file mode 100644 index 000000000..92fa6a3e9 --- /dev/null +++ b/db/migrate/20161227103823_delete_old_attr_in_data_base.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index dbbb43529..e9491dca0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/controllers/backoffice/commentaires_controller_spec.rb b/spec/controllers/backoffice/commentaires_controller_spec.rb index 63ebb3261..2fccafe5e 100644 --- a/spec/controllers/backoffice/commentaires_controller_spec.rb +++ b/spec/controllers/backoffice/commentaires_controller_spec.rb @@ -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 } diff --git a/spec/controllers/backoffice/dossiers_controller_spec.rb b/spec/controllers/backoffice/dossiers_controller_spec.rb index 34f2384b2..96e838d19 100644 --- a/spec/controllers/backoffice/dossiers_controller_spec.rb +++ b/spec/controllers/backoffice/dossiers_controller_spec.rb @@ -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 diff --git a/spec/controllers/users/commentaires_controller_spec.rb b/spec/controllers/users/commentaires_controller_spec.rb index 54edd2c4d..b03cc9225 100644 --- a/spec/controllers/users/commentaires_controller_spec.rb +++ b/spec/controllers/users/commentaires_controller_spec.rb @@ -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 diff --git a/spec/controllers/users/description_controller_shared_example.rb b/spec/controllers/users/description_controller_shared_example.rb index a751f08e2..c08c8bbd2 100644 --- a/spec/controllers/users/description_controller_shared_example.rb +++ b/spec/controllers/users/description_controller_shared_example.rb @@ -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 } diff --git a/spec/controllers/users/description_controller_spec.rb b/spec/controllers/users/description_controller_spec.rb index b459d6341..ace2b9f1b 100644 --- a/spec/controllers/users/description_controller_spec.rb +++ b/spec/controllers/users/description_controller_spec.rb @@ -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 } diff --git a/spec/controllers/users/dossiers/invites_controller_spec.rb b/spec/controllers/users/dossiers/invites_controller_spec.rb index 70bfcbc70..e2d71450a 100644 --- a/spec/controllers/users/dossiers/invites_controller_spec.rb +++ b/spec/controllers/users/dossiers/invites_controller_spec.rb @@ -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 } diff --git a/spec/controllers/users/recapitulatif_controller_spec.rb b/spec/controllers/users/recapitulatif_controller_spec.rb index 31e12dad0..3cbbe1696 100644 --- a/spec/controllers/users/recapitulatif_controller_spec.rb +++ b/spec/controllers/users/recapitulatif_controller_spec.rb @@ -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 diff --git a/spec/facades/dossiers_list_facades_spec.rb b/spec/facades/dossiers_list_facades_spec.rb index 7dcd9a6e0..f10826990 100644 --- a/spec/facades/dossiers_list_facades_spec.rb +++ b/spec/facades/dossiers_list_facades_spec.rb @@ -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 diff --git a/spec/factories/commentaire.rb b/spec/factories/commentaire.rb index 791e295db..bb0a148ca 100644 --- a/spec/factories/commentaire.rb +++ b/spec/factories/commentaire.rb @@ -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 diff --git a/spec/factories/gestionnaire.rb b/spec/factories/gestionnaire.rb index dcdb76c62..63004f8e9 100644 --- a/spec/factories/gestionnaire.rb +++ b/spec/factories/gestionnaire.rb @@ -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' diff --git a/spec/factories/notification.rb b/spec/factories/notification.rb new file mode 100644 index 000000000..fe4d8ecd8 --- /dev/null +++ b/spec/factories/notification.rb @@ -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 diff --git a/spec/features/admin/procedure_cloning_spec.rb b/spec/features/admin/procedure_cloning_spec.rb new file mode 100644 index 000000000..78e13045a --- /dev/null +++ b/spec/features/admin/procedure_cloning_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'As an administrateur I wanna clone a procedure', js: true do + + let(:administrateur) { create(:administrateur) } + + before do + login_as administrateur, scope: :administrateur + visit root_path + end + + context 'Cloning procedure' do + + before 'Create procedure' do + page.find_by_id('new-procedure').click + fill_in 'procedure_libelle', with: 'libelle de la procedure' + page.execute_script("$('#procedure_description').data('wysihtml5').editor.setValue('description de la procedure')") + page.find_by_id('save-procedure').click + end + + scenario 'Cloning' do + visit admin_procedures_draft_path + expect(page.find_by_id('procedures')['data-item-count']).to eq('1') + page.all('.clone-btn').first.click + visit admin_procedures_draft_path + expect(page.find_by_id('procedures')['data-item-count']).to eq('2') + end + end +end diff --git a/spec/features/admin/procedure_creation_spec.rb b/spec/features/admin/procedure_creation_spec.rb index 49f033bc0..c123ad1fc 100644 --- a/spec/features/admin/procedure_creation_spec.rb +++ b/spec/features/admin/procedure_creation_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'as an administrateur I wanna create a new procedure', js: true do +feature 'As an administrateur I wanna create a new procedure', js: true do let(:administrateur) { create(:administrateur) } diff --git a/spec/features/backoffice/invitation_spec.rb b/spec/features/backoffice/invitation_spec.rb new file mode 100644 index 000000000..ffeb61158 --- /dev/null +++ b/spec/features/backoffice/invitation_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +feature 'As an Accompagnateur I can send invitations from dossiers', js: true do + + let(:user) { create(:user) } + let(:gestionnaire) { create(:gestionnaire) } + let(:procedure_1) { create(:procedure, :with_type_de_champ, libelle: 'procedure 1') } + + before 'Assign procedures to Accompagnateur and generating dossiers for each' do + create :assign_to, gestionnaire: gestionnaire, procedure: procedure_1 + Dossier.create(procedure_id: procedure_1.id.to_s, user: user, state: 'initiated') + login_as gestionnaire, scope: :gestionnaire + visit backoffice_dossier_path(1) + end + + context 'On dossier show' do + + scenario 'Sending invitation' do + page.find('#invitations').click + page.find('#invitation-email').set('toto@email.com') + page.find('#send-invitation .btn-success').trigger('click') + end + + end +end diff --git a/spec/features/users/invitation_spec.rb b/spec/features/users/invitation_spec.rb new file mode 100644 index 000000000..7611e6b07 --- /dev/null +++ b/spec/features/users/invitation_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +feature 'As a User I can send invitations from dossiers', js: true do + + let(:user) { create(:user) } + let(:procedure_1) { create(:procedure, :with_type_de_champ, libelle: 'procedure 1') } + + before 'Assign procedures to Accompagnateur and generating dossiers for each' do + Dossier.create(procedure_id: procedure_1.id.to_s, user: user, state: 'initiated') + login_as user, scope: :user + visit users_dossier_recapitulatif_path(1) + end + + context 'On dossier show' do + + scenario 'Sending invitation' do + page.find('#invitations').click + fill_in 'invitation-email', with: 'toto@email.com' + page.find('#send-invitation .btn-success').trigger('click') + save_and_open_page + end + + end +end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 5fe22ff6e..ddbf350ce 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -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 diff --git a/spec/models/gestionnaire_spec.rb b/spec/models/gestionnaire_spec.rb index 18fc65d6d..e61990c7b 100644 --- a/spec/models/gestionnaire_spec.rb +++ b/spec/models/gestionnaire_spec.rb @@ -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 diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb new file mode 100644 index 000000000..0a945ff9f --- /dev/null +++ b/spec/models/notification_spec.rb @@ -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 diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb new file mode 100644 index 000000000..d091c69b2 --- /dev/null +++ b/spec/services/notification_service_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d999bf456..4e7319f2a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/spec/views/admin/gestionnaires/index.html.haml_spec.rb b/spec/views/admin/gestionnaires/index.html.haml_spec.rb index 222b43b2c..9c57ee063 100644 --- a/spec/views/admin/gestionnaires/index.html.haml_spec.rb +++ b/spec/views/admin/gestionnaires/index.html.haml_spec.rb @@ -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 \ No newline at end of file