diff --git a/app/controllers/new_gestionnaire/dossiers_controller.rb b/app/controllers/new_gestionnaire/dossiers_controller.rb index 9fe474620..5adafcdf3 100644 --- a/app/controllers/new_gestionnaire/dossiers_controller.rb +++ b/app/controllers/new_gestionnaire/dossiers_controller.rb @@ -7,21 +7,25 @@ module NewGestionnaire def show @dossier = dossier dossier.notifications.demande.mark_as_read + current_gestionnaire.mark_tab_as_seen(dossier, :demande) end def messagerie @dossier = dossier dossier.notifications.messagerie.mark_as_read + current_gestionnaire.mark_tab_as_seen(dossier, :messagerie) end def annotations_privees @dossier = dossier dossier.notifications.annotations_privees.mark_as_read + current_gestionnaire.mark_tab_as_seen(dossier, :annotations_privees) end def avis @dossier = dossier dossier.notifications.avis.mark_as_read + current_gestionnaire.mark_tab_as_seen(dossier, :avis) end def follow diff --git a/app/models/avis.rb b/app/models/avis.rb index 39ef291ae..2f9aac2c9 100644 --- a/app/models/avis.rb +++ b/app/models/avis.rb @@ -12,6 +12,7 @@ class Avis < ApplicationRecord scope :without_answer, -> { where(answer: nil) } scope :for_dossier, ->(dossier_id) { where(dossier_id: dossier_id) } scope :by_latest, -> { order(updated_at: :desc) } + scope :updated_since?, -> (date) { where('avis.updated_at > ?', date) } def email_to_display gestionnaire.try(:email) || email diff --git a/app/models/champ.rb b/app/models/champ.rb index 084b294e8..ee0ee446f 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -11,6 +11,8 @@ class Champ < ActiveRecord::Base after_save :internal_notification, if: Proc.new { !dossier.nil? } + scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) } + def mandatory? mandatory end diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb index 05c3a5f08..5fd425029 100644 --- a/app/models/commentaire.rb +++ b/app/models/commentaire.rb @@ -5,6 +5,7 @@ class Commentaire < ActiveRecord::Base belongs_to :piece_justificative default_scope { order(created_at: :asc) } + scope :updated_since?, -> (date) { where('commentaires.updated_at > ?', date) } after_create :notify diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 1da325eb3..e18d241a5 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -62,6 +62,7 @@ class Dossier < ActiveRecord::Base scope :en_cours, -> { not_archived.state_en_construction_ou_instruction } scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) } scope :with_unread_notifications, -> { where(notifications: { already_read: false }) } + scope :followed_by , -> (gestionnaire) { joins(:follows).where(follows: { gestionnaire: gestionnaire }) } accepts_nested_attributes_for :individual diff --git a/app/models/follow.rb b/app/models/follow.rb index 53e234f7e..2df639902 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -3,4 +3,15 @@ class Follow < ActiveRecord::Base belongs_to :dossier validates_uniqueness_of :gestionnaire_id, :scope => :dossier_id + + before_create :set_default_date + + private + + def set_default_date + self.demande_seen_at = DateTime.now + self.annotations_privees_seen_at = DateTime.now + self.avis_seen_at = DateTime.now + self.messagerie_seen_at = DateTime.now + end end diff --git a/app/models/gestionnaire.rb b/app/models/gestionnaire.rb index 5b1344e83..eed1f4598 100644 --- a/app/models/gestionnaire.rb +++ b/app/models/gestionnaire.rb @@ -131,6 +131,56 @@ class Gestionnaire < ActiveRecord::Base assign_to.find_by(procedure_id: procedure_id).procedure_presentation_or_default end + def notifications_for_dossier(dossier) + follow = Follow + .includes(dossier: [:champs, :avis, :commentaires]) + .find_by(gestionnaire: self, dossier: dossier) + + if follow.present? + #retirer le seen_at.present? une fois la contrainte de presence en base (et les migrations ad hoc) + champs_publiques = follow.demande_seen_at.present? && + follow.dossier.champs.updated_since?(follow.demande_seen_at).any? + + pieces_justificatives = follow.demande_seen_at.present? && + follow.dossier.pieces_justificatives.updated_since?(follow.demande_seen_at).any? + + demande = champs_publiques || pieces_justificatives + + annotations_privees = follow.annotations_privees_seen_at.present? && + follow.dossier.champs_private.updated_since?(follow.annotations_privees_seen_at).any? + + avis_notif = follow.avis_seen_at.present? && + follow.dossier.avis.updated_since?(follow.avis_seen_at).any? + + messagerie = follow.messagerie_seen_at.present? && + dossier.commentaires + .where.not(email: 'contact@tps.apientreprise.fr') + .updated_since?(follow.messagerie_seen_at).any? + + annotations_hash(demande, annotations_privees, avis_notif, messagerie) + else + annotations_hash(false, false, false, false) + end + end + + def notifications_for_procedure(procedure) + dossiers = procedure.dossiers.en_cours.followed_by(self) + + dossiers_id_with_notifications(dossiers) + end + + def notifications_per_procedure + dossiers = Dossier.en_cours.followed_by(self) + + Dossier.where(id: dossiers_id_with_notifications(dossiers)).group(:procedure_id).count + end + + def mark_tab_as_seen(dossier, tab) + attributes = {} + attributes["#{tab}_seen_at"] = DateTime.now + Follow.where(gestionnaire: self, dossier: dossier).update_all(attributes) + end + private def valid_couple_table_attr? table, column @@ -153,4 +203,42 @@ class Gestionnaire < ActiveRecord::Base couples.include?({table: table, column: column}) end + + def annotations_hash(demande, annotations_privees, avis, messagerie) + { + demande: demande, + annotations_privees: annotations_privees, + avis: avis, + messagerie: messagerie + } + end + + def dossiers_id_with_notifications(dossiers) + updated_demandes = dossiers + .joins(:champs) + .where('champs.updated_at > follows.demande_seen_at') + + updated_pieces_justificatives = dossiers + .joins(:pieces_justificatives) + .where('pieces_justificatives.updated_at > follows.demande_seen_at') + + updated_annotations = dossiers + .joins(:champs_private) + .where('champs.updated_at > follows.annotations_privees_seen_at') + + updated_avis = dossiers + .joins(:avis) + .where('avis.updated_at > follows.avis_seen_at') + + updated_messagerie = dossiers + .joins(:commentaires) + .where('commentaires.updated_at > follows.messagerie_seen_at') + .where.not(commentaires: { email: 'contact@tps.apientreprise.fr' }) + + [updated_demandes, + updated_pieces_justificatives, + updated_annotations, + updated_avis, + updated_messagerie].map { |query| query.distinct.ids }.flatten.uniq + end end diff --git a/app/models/piece_justificative.rb b/app/models/piece_justificative.rb index 70f780aed..393d892cc 100644 --- a/app/models/piece_justificative.rb +++ b/app/models/piece_justificative.rb @@ -15,6 +15,8 @@ class PieceJustificative < ActiveRecord::Base after_save :internal_notification, if: Proc.new { !dossier.nil? } + scope :updated_since?, -> (date) { where('pieces_justificatives.updated_at > ?', date) } + def empty? content.blank? end diff --git a/app/views/new_gestionnaire/dossiers/_header.html.haml b/app/views/new_gestionnaire/dossiers/_header.html.haml index 5d6f36316..acdd37e64 100644 --- a/app/views/new_gestionnaire/dossiers/_header.html.haml +++ b/app/views/new_gestionnaire/dossiers/_header.html.haml @@ -12,7 +12,7 @@ = render partial: "new_gestionnaire/procedures/dossier_actions", locals: { procedure: dossier.procedure, dossier: dossier, dossier_is_followed: current_gestionnaire&.follow?(dossier) } = render partial: "state_button", locals: { dossier: dossier } %ul.tabs - - notifications_summary = dossier.notifications_summary + - notifications_summary = current_gestionnaire.notifications_for_dossier(dossier) %li{ class: current_page?(dossier_path(dossier.procedure, dossier)) ? 'active' : nil } - if notifications_summary[:demande] %span.notifications{ 'aria-label': 'notifications' } diff --git a/app/views/new_gestionnaire/procedures/index.html.haml b/app/views/new_gestionnaire/procedures/index.html.haml index d9f95ad54..224ceb5a3 100644 --- a/app/views/new_gestionnaire/procedures/index.html.haml +++ b/app/views/new_gestionnaire/procedures/index.html.haml @@ -25,7 +25,7 @@ %li %object = link_to(procedure_path(p, statut: 'suivis')) do - - if @notifications_count_per_procedure[p.id].present? + - if current_gestionnaire.notifications_per_procedure[p.id].present? %span.notifications{ 'aria-label': "notifications" } - followed_count = @followed_dossiers_count_per_procedure[p.id] || 0 .stats-number diff --git a/app/views/new_gestionnaire/procedures/show.html.haml b/app/views/new_gestionnaire/procedures/show.html.haml index d8ec7f23c..c1e9dfc49 100644 --- a/app/views/new_gestionnaire/procedures/show.html.haml +++ b/app/views/new_gestionnaire/procedures/show.html.haml @@ -15,7 +15,7 @@ %span.badge= @a_suivre_dossiers.count %li{ class: (@statut == 'suivis') ? 'active' : nil }> - - if @followed_dossiers.with_unread_notifications.present? + - if current_gestionnaire.notifications_for_procedure(@procedure).present? %span.notifications{ 'aria-label': 'notifications' } = link_to(procedure_path(@procedure, statut: 'suivis')) do = t('pluralize.followed', count: @followed_dossiers.count) @@ -27,7 +27,7 @@ %span.badge= @termines_dossiers.count %li{ class: (@statut == 'tous') ? 'active' : nil }> - - if @followed_dossiers.with_unread_notifications.present? + - if current_gestionnaire.notifications_for_procedure(@procedure).present? %span.notifications{ 'aria-label': 'notifications' } = link_to(procedure_path(@procedure, statut: 'tous')) do tous les dossiers @@ -99,7 +99,7 @@ %td.number-col = link_to(dossier_path(@procedure, dossier), class: 'cell-link') do .icon.folder - - if @followed_dossiers.with_unread_notifications.include?(dossier) + - if current_gestionnaire.notifications_for_procedure(@procedure).include?(dossier.id) %span.notifications{ 'aria-label': 'notifications' } = dossier.id diff --git a/db/migrate/20171024100606_add_time_stamp_to_champs.rb b/db/migrate/20171024100606_add_time_stamp_to_champs.rb new file mode 100644 index 000000000..c144ed4a2 --- /dev/null +++ b/db/migrate/20171024100606_add_time_stamp_to_champs.rb @@ -0,0 +1,6 @@ +class AddTimeStampToChamps < ActiveRecord::Migration[5.0] + def change + add_column :champs, :created_at, :datetime + add_column :champs, :updated_at, :datetime + end +end diff --git a/db/migrate/20171024101439_add_last_views_at_to_follow.rb b/db/migrate/20171024101439_add_last_views_at_to_follow.rb new file mode 100644 index 000000000..f9f831db8 --- /dev/null +++ b/db/migrate/20171024101439_add_last_views_at_to_follow.rb @@ -0,0 +1,8 @@ +class AddLastViewsAtToFollow < ActiveRecord::Migration[5.0] + def change + add_column :follows, :demande_seen_at, :datetime + add_column :follows, :annotations_privees_seen_at, :datetime + add_column :follows, :avis_seen_at, :datetime + add_column :follows, :messagerie_seen_at, :datetime + end +end diff --git a/db/migrate/20171024135653_add_column_updated_at_to_piece_justificative.rb b/db/migrate/20171024135653_add_column_updated_at_to_piece_justificative.rb new file mode 100644 index 000000000..820b41703 --- /dev/null +++ b/db/migrate/20171024135653_add_column_updated_at_to_piece_justificative.rb @@ -0,0 +1,5 @@ +class AddColumnUpdatedAtToPieceJustificative < ActiveRecord::Migration[5.0] + def change + add_column :pieces_justificatives, :updated_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 48a55012d..77dee82db 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: 20171019113610) do +ActiveRecord::Schema.define(version: 20171024135653) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -136,10 +136,12 @@ ActiveRecord::Schema.define(version: 20171019113610) do end create_table "champs", force: :cascade do |t| - t.string "value" - t.integer "type_de_champ_id" - t.integer "dossier_id" - t.string "type" + t.string "value" + t.integer "type_de_champ_id" + t.integer "dossier_id" + t.string "type" + t.datetime "created_at" + t.datetime "updated_at" t.index ["dossier_id"], name: "index_champs_on_dossier_id", using: :btree t.index ["type_de_champ_id"], name: "index_champs_on_type_de_champ_id", using: :btree end @@ -250,8 +252,12 @@ ActiveRecord::Schema.define(version: 20171019113610) do end create_table "follows", force: :cascade do |t| - t.integer "gestionnaire_id", null: false - t.integer "dossier_id", null: false + t.integer "gestionnaire_id", null: false + t.integer "dossier_id", null: false + t.datetime "demande_seen_at" + t.datetime "annotations_privees_seen_at" + t.datetime "avis_seen_at" + t.datetime "messagerie_seen_at" t.index ["dossier_id"], name: "index_follows_on_dossier_id", using: :btree t.index ["gestionnaire_id", "dossier_id"], name: "index_follows_on_gestionnaire_id_and_dossier_id", unique: true, using: :btree t.index ["gestionnaire_id"], name: "index_follows_on_gestionnaire_id", using: :btree @@ -348,6 +354,7 @@ ActiveRecord::Schema.define(version: 20171019113610) do t.integer "user_id" t.string "original_filename" t.string "content_secure_token" + t.datetime "updated_at" t.index ["dossier_id"], name: "index_pieces_justificatives_on_dossier_id", using: :btree t.index ["type_de_piece_justificative_id"], name: "index_pieces_justificatives_on_type_de_piece_justificative_id", using: :btree end diff --git a/lib/tasks/2017_10_06_set_follow_date.rake b/lib/tasks/2017_10_06_set_follow_date.rake new file mode 100644 index 000000000..c40f3f2f4 --- /dev/null +++ b/lib/tasks/2017_10_06_set_follow_date.rake @@ -0,0 +1,49 @@ +namespace :'2017_10_06_set_follow_date' do + task set: :environment do + set_default_date_to_champs_and_pieces_justificatives + set_all_dossiers_as_read + apply_legacy_notification_to_new_system + end + + def set_default_date_to_champs_and_pieces_justificatives + ActiveRecord::Base.connection + .execute('UPDATE champs SET created_at = dossiers.created_at, updated_at = dossiers.updated_at FROM dossiers where champs.dossier_id = dossiers.id') + + PieceJustificative.includes(:dossier).where(created_at: nil).each do |piece_justificative| + piece_justificative.update_attribute('created_at', piece_justificative.dossier.created_at) + end + + ActiveRecord::Base.connection + .execute('UPDATE pieces_justificatives SET updated_at = created_at') + end + + def set_all_dossiers_as_read + Gestionnaire.includes(:follows).all.each do |gestionnaire| + gestionnaire.follows.update_all( + demande_seen_at: gestionnaire.current_sign_in_at, + annotations_privees_seen_at: gestionnaire.current_sign_in_at, + avis_seen_at: gestionnaire.current_sign_in_at, + messagerie_seen_at: gestionnaire.current_sign_in_at) + end + end + + def apply_legacy_notification_to_new_system + Notification.joins(dossier: :follows).unread.each do |notification| + if notification.demande? + notification.dossier.follows.update_all(demande_seen_at: notification.created_at) + end + + if notification.annotations_privees? + notification.dossier.follows.update_all(annotations_privees_seen_at: notification.created_at) + end + + if notification.avis? + notification.dossier.follows.update_all(avis_seen_at: notification.created_at) + end + + if notification.messagerie? + notification.dossier.follows.update_all(messagerie_seen_at: notification.created_at) + end + end + end +end diff --git a/spec/models/gestionnaire_spec.rb b/spec/models/gestionnaire_spec.rb index 99639c25f..6ed814082 100644 --- a/spec/models/gestionnaire_spec.rb +++ b/spec/models/gestionnaire_spec.rb @@ -318,6 +318,8 @@ describe Gestionnaire, type: :model do Timecop.freeze(friday) end + after { Timecop.return } + context 'when no procedure published was active last week' do let!(:procedure) { create(:procedure, gestionnaires: [gestionnaire2], libelle: 'procedure', published_at: Time.now) } context 'when the gestionnaire has no notifications' do @@ -404,4 +406,167 @@ describe Gestionnaire, type: :model do it { expect(gestionnaire.procedure_presentation_for_procedure_id(procedure.id)).to eq(pp)} it { expect(gestionnaire.procedure_presentation_for_procedure_id(procedure_2.id).persisted?).to be_falsey} end + + describe '#notifications_for_dossier' do + let!(:dossier) { create(:dossier, :followed, state: 'initiated') } + let(:gestionnaire) { dossier.follows.first.gestionnaire } + + subject { gestionnaire.notifications_for_dossier(dossier) } + + context 'when the gestionnaire has just followed the dossier' do + it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: false }) } + end + + context 'when there is a modification on public champs' do + before { dossier.champs.first.update_attribute('value', 'toto') } + + it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false }) } + end + + context 'when there is a modification on a piece jusitificative' do + before { dossier.pieces_justificatives << create(:piece_justificative, :contrat) } + + it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false }) } + end + + context 'when there is a modification on private champs' do + before { dossier.champs_private.first.update_attribute('value', 'toto') } + + it { is_expected.to match({ demande: false, annotations_privees: true, avis: false, messagerie: false }) } + end + + context 'when there is a modification on avis' do + before { create(:avis, dossier: dossier) } + + it { is_expected.to match({ demande: false, annotations_privees: false, avis: true, messagerie: false }) } + end + + context 'messagerie' do + context 'when there is a new commentaire' do + before { create(:commentaire, dossier: dossier, email: 'a@b.com') } + + it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: true }) } + end + + context 'when there is a new commentaire issued by tps' do + before { create(:commentaire, dossier: dossier, email: 'contact@tps.apientreprise.fr') } + + it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: false }) } + end + end + end + + describe '#notification_for_procedure' do + let!(:dossier) { create(:dossier, :followed, state: 'initiated') } + let(:gestionnaire) { dossier.follows.first.gestionnaire } + let(:procedure) { dossier.procedure } + let!(:gestionnaire_2) { create(:gestionnaire, procedures: [procedure]) } + + let!(:dossier_on_procedure_2) { create(:dossier, :followed, state: 'initiated') } + let!(:gestionnaire_on_procedure_2) { dossier_on_procedure_2.follows.first.gestionnaire } + + before do + gestionnaire_2.followed_dossiers << dossier + end + + subject { gestionnaire.notifications_for_procedure(procedure) } + + context 'when the gestionnaire has just followed the dossier' do + it { is_expected.to match([]) } + end + + context 'when there is a modification on public champs' do + before { dossier.champs.first.update_attribute('value', 'toto') } + + it { is_expected.to match([dossier.id]) } + it { expect(gestionnaire_2.notifications_for_procedure(procedure)).to match([dossier.id]) } + it { expect(gestionnaire_on_procedure_2.notifications_for_procedure(procedure)).to match([]) } + + context 'and there is a modification on private champs' do + before { dossier.champs_private.first.update_attribute('value', 'toto') } + + it { is_expected.to match([dossier.id]) } + end + + context 'when gestionnaire update it s public champs last seen' do + let(:follow) { gestionnaire.follows.find_by(dossier: dossier) } + + before { follow.update_attribute('demande_seen_at', DateTime.now) } + + it { is_expected.to match([]) } + it { expect(gestionnaire_2.notifications_for_procedure(procedure)).to match([dossier.id]) } + end + end + + context 'when there is a modification on a piece justificative' do + before { dossier.pieces_justificatives << create(:piece_justificative, :contrat) } + + it { is_expected.to match([dossier.id]) } + end + + context 'when there is a modification on public champs on a followed dossier from another procedure' do + before { dossier_on_procedure_2.champs.first.update_attribute('value', 'toto') } + + it { is_expected.to match([]) } + end + + context 'when there is a modification on private champs' do + before { dossier.champs_private.first.update_attribute('value', 'toto') } + + it { is_expected.to match([dossier.id]) } + end + + context 'when there is a modification on avis' do + before { create(:avis, dossier: dossier) } + + it { is_expected.to match([dossier.id]) } + end + + context 'the messagerie' do + context 'when there is a new commentaire' do + before { create(:commentaire, dossier: dossier, email: 'a@b.com') } + + it { is_expected.to match([dossier.id]) } + end + + context 'when there is a new commentaire issued by tps' do + before { create(:commentaire, dossier: dossier, email: 'contact@tps.apientreprise.fr') } + + it { is_expected.to match([]) } + end + end + end + + describe '#notifications_per_procedure' do + let!(:dossier) { create(:dossier, :followed, state: 'initiated') } + let(:gestionnaire) { dossier.follows.first.gestionnaire } + let(:procedure) { dossier.procedure } + + subject { gestionnaire.notifications_per_procedure } + + context 'when there is a modification on public champs' do + before { dossier.champs.first.update_attribute('value', 'toto') } + + it { is_expected.to match({ procedure.id => 1 }) } + end + end + + describe '#mark_tab_as_seen' do + let!(:dossier) { create(:dossier, :followed, state: 'initiated') } + let(:gestionnaire) { dossier.follows.first.gestionnaire } + let(:freeze_date) { DateTime.parse('12/12/2012') } + + context 'when demande is acknowledged' do + let(:follow) { gestionnaire.follows.find_by(dossier: dossier) } + + before do + Timecop.freeze(freeze_date) + gestionnaire.mark_tab_as_seen(dossier, :demande) + end + + it { expect(follow.demande_seen_at).to eq(freeze_date) } + + after { Timecop.return } + end + end end diff --git a/spec/views/new_gestionnaire/dossiers/show.html.haml_spec.rb b/spec/views/new_gestionnaire/dossiers/show.html.haml_spec.rb index cd5d6730e..68ad70c78 100644 --- a/spec/views/new_gestionnaire/dossiers/show.html.haml_spec.rb +++ b/spec/views/new_gestionnaire/dossiers/show.html.haml_spec.rb @@ -1,10 +1,12 @@ describe 'new_gestionnaire/dossiers/show.html.haml', type: :view do + let(:current_gestionnaire) { create(:gestionnaire) } let(:individual) { nil } let(:entreprise) { nil } let(:dossier) { create(:dossier, :initiated, entreprise: entreprise, individual: individual) } before do assign(:dossier, dossier) + view.stub(:current_gestionnaire).and_return(current_gestionnaire) render end