diff --git a/app/assets/stylesheets/gallery.scss b/app/assets/stylesheets/gallery.scss index eb2bec9ee..3c8331912 100644 --- a/app/assets/stylesheets/gallery.scss +++ b/app/assets/stylesheets/gallery.scss @@ -52,7 +52,15 @@ flex-wrap: wrap; .gallery-item { - margin: 0 2rem 1.5rem 0; + margin: 0 2rem 3rem 0; + + .fr-download { + margin-bottom: 0; + } + + .fr-text--sm { + margin-bottom: 0; + } } } diff --git a/app/components/attachment/gallery_item_component.rb b/app/components/attachment/gallery_item_component.rb new file mode 100644 index 000000000..748387cf4 --- /dev/null +++ b/app/components/attachment/gallery_item_component.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +class Attachment::GalleryItemComponent < ApplicationComponent + include GalleryHelper + attr_reader :attachment, :seen_at + + def initialize(attachment:, gallery_demande: false, seen_at: nil) + @attachment = attachment + @gallery_demande = gallery_demande + @seen_at = seen_at + end + + def blob + attachment.blob + end + + def gallery_demande? = @gallery_demande + + def libelle + from_dossier? ? attachment.record.libelle : 'Pièce jointe au message' + end + + def origin + case + when from_dossier? + 'Dossier usager' + when from_messagerie_instructeur? + 'Messagerie (instructeur)' + when from_messagerie_usager? + 'Messagerie (usager)' + end + end + + def title + "#{libelle} -- #{sanitize(blob.filename.to_s)}" + end + + def gallery_link(blob, &block) + if displayable_image?(blob) + link_to image_url(blob_url(attachment)), title: title, data: { src: blob.url }, class: 'gallery-link' do + yield + end + elsif displayable_pdf?(blob) + link_to blob.url, id: blob.id, data: { iframe: true, src: blob.url }, class: 'gallery-link', type: blob.content_type, title: title do + yield + end + end + end + + def created_at + attachment.record.created_at + end + + def updated? + from_dossier? && updated_at > attachment.record.dossier.depose_at + end + + def updated_at + blob.created_at + end + + def badge_updated_class + class_names( + "fr-badge fr-badge--sm" => true, + "highlighted" => seen_at.present? && updated_at&.>(seen_at) + ) + end + + private + + def from_dossier? + attachment.record.class.in?([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp]) + end + + def from_messagerie? + attachment.record.is_a?(Commentaire) + end + + def from_messagerie_instructeur? + from_messagerie? && attachment.record.instructeur.present? + end + + def from_messagerie_usager? + from_messagerie? && attachment.record.instructeur.nil? + end +end diff --git a/app/components/attachment/gallery_item_component/gallery_item_component.en.yml b/app/components/attachment/gallery_item_component/gallery_item_component.en.yml new file mode 100644 index 000000000..7e273db89 --- /dev/null +++ b/app/components/attachment/gallery_item_component/gallery_item_component.en.yml @@ -0,0 +1,3 @@ +en: + created_at: "Added on %{datetime}" + updated_at: "Updated on %{datetime}" diff --git a/app/components/attachment/gallery_item_component/gallery_item_component.fr.yml b/app/components/attachment/gallery_item_component/gallery_item_component.fr.yml new file mode 100644 index 000000000..788df7462 --- /dev/null +++ b/app/components/attachment/gallery_item_component/gallery_item_component.fr.yml @@ -0,0 +1,3 @@ +fr: + created_at: "Ajoutée le %{datetime}" + updated_at: "Modifiée le %{datetime}" diff --git a/app/components/attachment/gallery_item_component/gallery_item_component.html.haml b/app/components/attachment/gallery_item_component/gallery_item_component.html.haml new file mode 100644 index 000000000..2428d5822 --- /dev/null +++ b/app/components/attachment/gallery_item_component/gallery_item_component.html.haml @@ -0,0 +1,26 @@ +.gallery-item + - if !gallery_demande? + %p.fr-tag.fr-tag--sm.fr-mb-3v= origin + - if displayable_pdf?(blob) || displayable_image?(blob) + = gallery_link(blob) do + .thumbnail + = image_tag(representation_url_for(attachment), loading: :lazy) + .fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button } + Visualiser + - if !gallery_demande? + .fr-text--sm.fr-mt-2v.fr-mb-1v + = libelle.truncate(25) + = render Attachment::ShowComponent.new(attachment:, truncate: true, new_tab: gallery_demande?) + - if !gallery_demande? + .fr-mt-2v.fr-mb-2v{ class: badge_updated_class } + = t(updated? ? '.updated_at' : '.created_at', datetime: helpers.try_format_datetime(updated_at, format: :veryshort)) + - else + .thumbnail + = image_tag('apercu-indisponible.png') + - if !gallery_demande? + .fr-text--sm.fr-mt-2v.fr-mb-1v + = libelle.truncate(25) + = render Attachment::ShowComponent.new(attachment:, truncate: true, new_tab: gallery_demande?) + - if !gallery_demande? + .fr-mt-2v.fr-mb-2v{ class: badge_updated_class } + = t(updated? ? '.updated_at' : '.created_at', datetime: helpers.try_format_datetime(updated_at, format: :veryshort)) diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 152b81f7b..0b7eb3a74 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -13,11 +13,13 @@ module Instructeurs before_action :redirect_on_dossier_not_found, only: :show before_action :redirect_on_dossier_in_batch_operation, only: [:archive, :unarchive, :follow, :unfollow, :passer_en_instruction, :repasser_en_construction, :repasser_en_instruction, :terminer, :restore, :destroy, :extend_conservation] + before_action :set_gallery_attachments, only: [:show, :pieces_jointes, :annotations_privees, :avis, :messagerie, :personnes_impliquees, :reaffectation] after_action :mark_demande_as_read, only: :show after_action :mark_messagerie_as_read, only: [:messagerie, :create_commentaire, :pending_correction] after_action :mark_avis_as_read, only: [:avis, :create_avis] after_action :mark_annotations_privees_as_read, only: [:annotations_privees, :update_annotations] + after_action :mark_pieces_jointes_as_read, only: [:pieces_jointes] def extend_conservation dossier.extend_conservation(1.month) @@ -371,25 +373,8 @@ module Instructeurs end def pieces_jointes - @dossier = current_instructeur.dossiers.find(params[:dossier_id]) - - champs_attachments_and_libelles = @dossier - .champs - .filter { _1.class.in?([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp]) } - .flat_map do |c| - c.piece_justificative_file.map do |attachment| - [attachment, c.libelle] - end - end - - commentaires_attachments_and_libelles = @dossier - .commentaires - .map(&:piece_jointe) - .map(&:attachments) - .flatten - .map { [_1, 'Messagerie'] } - - @attachments_and_libelles = champs_attachments_and_libelles + commentaires_attachments_and_libelles + @dossier = dossier + @pieces_jointes_seen_at = current_instructeur.follows.find_by(dossier: dossier)&.pieces_jointes_seen_at end private @@ -471,6 +456,10 @@ module Instructeurs current_instructeur.mark_tab_as_seen(dossier, :annotations_privees) end + def mark_pieces_jointes_as_read + current_instructeur.mark_tab_as_seen(dossier, :pieces_jointes) + end + def aasm_error_message(exception, target_state:) if exception.originating_state == target_state "Le dossier est déjà #{dossier_display_state(target_state, lower: true)}." @@ -498,5 +487,26 @@ module Instructeurs redirect_back(fallback_location: instructeur_dossier_path(procedure, dossier_in_batch)) end end + + def set_gallery_attachments + gallery_attachments_ids = Rails.cache.fetch([dossier, "gallery_attachments"], expires_in: 10.minutes) do + champs_attachments_ids = dossier + .champs + .where(type: [Champs::PieceJustificativeChamp.name, Champs::TitreIdentiteChamp.name]) + .flat_map(&:piece_justificative_file) + .map(&:id) + + commentaires_attachments_ids = dossier + .commentaires + .includes(piece_jointe_attachments: :blob) + .map(&:piece_jointe) + .map(&:attachments) + .flatten + .map(&:id) + + champs_attachments_ids + commentaires_attachments_ids + end + @gallery_attachments = ActiveStorage::Attachment.where(id: gallery_attachments_ids) + end end end diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 824751910..b7166d911 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -341,7 +341,10 @@ module Users @commentaire = CommentaireService.create(current_user, dossier, commentaire_params) if @commentaire.errors.empty? - @commentaire.dossier.update!(last_commentaire_updated_at: Time.zone.now) + timestamps = [:last_commentaire_updated_at, :updated_at] + timestamps << :last_commentaire_piece_jointe_updated_at if @commentaire.piece_jointe.attached? + + @commentaire.dossier.touch(*timestamps) flash.notice = t('.message_send') redirect_to messagerie_dossier_path(dossier) diff --git a/app/helpers/gallery_helper.rb b/app/helpers/gallery_helper.rb index b9f97ca8d..0f97ea9b9 100644 --- a/app/helpers/gallery_helper.rb +++ b/app/helpers/gallery_helper.rb @@ -9,6 +9,12 @@ module GalleryHelper blob.variable? && blob.content_type.in?(AUTHORIZED_IMAGE_TYPES) end + def representation_url_for(attachment) + return variant_url_for(attachment) if displayable_image?(attachment.blob) + + preview_url_for(attachment) if displayable_pdf?(attachment.blob) + end + def preview_url_for(attachment) preview = attachment.preview(resize_to_limit: [400, 400]) preview.image.attached? ? preview.processed.url : 'pdf-placeholder.png' diff --git a/app/models/concerns/dossier_clone_concern.rb b/app/models/concerns/dossier_clone_concern.rb index 691226c0e..6d7b185e4 100644 --- a/app/models/concerns/dossier_clone_concern.rb +++ b/app/models/concerns/dossier_clone_concern.rb @@ -71,6 +71,7 @@ module DossierCloneConcern diff = make_diff(editing_fork) apply_diff(diff) touch(:last_champ_updated_at) + touch(:last_champ_piece_jointe_updated_at) if diff[:updated].any? { |c| c.class.in?([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp]) } end reload index_search_terms_later diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 9a980ae60..f6707f444 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -377,7 +377,9 @@ class Dossier < ApplicationRecord ' OR groupe_instructeur_updated_at > follows.demande_seen_at' \ ' OR last_champ_private_updated_at > follows.annotations_privees_seen_at' \ ' OR last_avis_updated_at > follows.avis_seen_at' \ - ' OR last_commentaire_updated_at > follows.messagerie_seen_at') + ' OR last_commentaire_updated_at > follows.messagerie_seen_at' \ + ' OR last_commentaire_piece_jointe_updated_at > follows.pieces_jointes_seen_at' \ + ' OR last_champ_piece_jointe_updated_at > follows.pieces_jointes_seen_at') .distinct end diff --git a/app/models/follow.rb b/app/models/follow.rb index c494eb47d..7c2dcaed2 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -18,5 +18,6 @@ class Follow < ApplicationRecord self.annotations_privees_seen_at ||= Time.zone.now self.avis_seen_at ||= Time.zone.now self.messagerie_seen_at ||= Time.zone.now + self.pieces_jointes_seen_at ||= Time.zone.now end end diff --git a/app/models/instructeur.rb b/app/models/instructeur.rb index b9e105802..6c9f388af 100644 --- a/app/models/instructeur.rb +++ b/app/models/instructeur.rb @@ -125,10 +125,11 @@ class Instructeur < ApplicationRecord annotations_privees = dossier.last_champ_private_updated_at&.>(follow.annotations_privees_seen_at) || false avis_notif = dossier.last_avis_updated_at&.>(follow.avis_seen_at) || false messagerie = dossier.last_commentaire_updated_at&.>(follow.messagerie_seen_at) || false + pieces_jointes = dossier.last_champ_piece_jointe_updated_at&.>(follow.pieces_jointes_seen_at) || dossier.last_commentaire_piece_jointe_updated_at&.>(follow.pieces_jointes_seen_at) || false - annotations_hash(demande, annotations_privees, avis_notif, messagerie) + annotations_hash(demande, annotations_privees, avis_notif, messagerie, pieces_jointes) else - annotations_hash(false, false, false, false) + annotations_hash(false, false, false, false, false) end end @@ -314,12 +315,13 @@ class Instructeur < ApplicationRecord private - def annotations_hash(demande, annotations_privees, avis, messagerie) + def annotations_hash(demande, annotations_privees, avis, messagerie, pieces_jointes) { demande: demande, annotations_privees: annotations_privees, avis: avis, - messagerie: messagerie + messagerie: messagerie, + pieces_jointes: pieces_jointes } end diff --git a/app/views/instructeurs/dossiers/_header.html.haml b/app/views/instructeurs/dossiers/_header.html.haml index 90e146b58..6432f87aa 100644 --- a/app/views/instructeurs/dossiers/_header.html.haml +++ b/app/views/instructeurs/dossiers/_header.html.haml @@ -7,7 +7,7 @@ .sub-header = render partial: 'instructeurs/dossiers/header_top', locals: { dossier: } - = render partial: 'instructeurs/dossiers/header_bottom', locals: { dossier: } + = render partial: 'instructeurs/dossiers/header_bottom', locals: { dossier:, gallery_attachments: } .fr-container .print-header diff --git a/app/views/instructeurs/dossiers/_header_bottom.html.haml b/app/views/instructeurs/dossiers/_header_bottom.html.haml index 0358c4aa0..b2cba4d8f 100644 --- a/app/views/instructeurs/dossiers/_header_bottom.html.haml +++ b/app/views/instructeurs/dossiers/_header_bottom.html.haml @@ -7,9 +7,10 @@ instructeur_dossier_path(dossier.procedure, dossier), notification: notifications_summary[:demande]) - - if dossier.champs.map(&:piece_justificative_file).flatten.any? + - if gallery_attachments.present? = dynamic_tab_item(t('views.instructeurs.dossiers.tab_steps.attachments'), - pieces_jointes_instructeur_dossier_path(dossier.procedure, dossier)) + pieces_jointes_instructeur_dossier_path(dossier.procedure, dossier), + notification: notifications_summary[:pieces_jointes]) = dynamic_tab_item(t('views.instructeurs.dossiers.tab_steps.private_annotations'), annotations_privees_instructeur_dossier_path(dossier.procedure, dossier), diff --git a/app/views/instructeurs/dossiers/annotations_privees.html.haml b/app/views/instructeurs/dossiers/annotations_privees.html.haml index 9b556be5c..f603d753a 100644 --- a/app/views/instructeurs/dossiers/annotations_privees.html.haml +++ b/app/views/instructeurs/dossiers/annotations_privees.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Annotations privées · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } #dossier-annotations-privees .fr-container diff --git a/app/views/instructeurs/dossiers/avis.html.haml b/app/views/instructeurs/dossiers/avis.html.haml index 46e608f9e..172816591 100644 --- a/app/views/instructeurs/dossiers/avis.html.haml +++ b/app/views/instructeurs/dossiers/avis.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Avis · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } .container .fr-grid-row diff --git a/app/views/instructeurs/dossiers/avis_new.html.haml b/app/views/instructeurs/dossiers/avis_new.html.haml index 143fe618d..6499ad2be 100644 --- a/app/views/instructeurs/dossiers/avis_new.html.haml +++ b/app/views/instructeurs/dossiers/avis_new.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Avis · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } .container .fr-grid-row diff --git a/app/views/instructeurs/dossiers/messagerie.html.haml b/app/views/instructeurs/dossiers/messagerie.html.haml index f98fbde63..212d521c8 100644 --- a/app/views/instructeurs/dossiers/messagerie.html.haml +++ b/app/views/instructeurs/dossiers/messagerie.html.haml @@ -1,5 +1,5 @@ - content_for(:title, "Messagerie · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } = render partial: "shared/dossiers/messagerie", locals: { dossier: @dossier, connected_user: current_instructeur, messagerie_seen_at: @messagerie_seen_at , new_commentaire: @commentaire, form_url: commentaire_instructeur_dossier_path(@dossier.procedure, @dossier) } diff --git a/app/views/instructeurs/dossiers/personnes_impliquees.html.haml b/app/views/instructeurs/dossiers/personnes_impliquees.html.haml index b90952e91..30d3224ec 100644 --- a/app/views/instructeurs/dossiers/personnes_impliquees.html.haml +++ b/app/views/instructeurs/dossiers/personnes_impliquees.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Personnes impliquées · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } .personnes-impliquees.container = render partial: 'instructeurs/dossiers/envoyer_dossier_block', locals: { dossier: @dossier, potential_recipients: @potential_recipients } diff --git a/app/views/instructeurs/dossiers/pieces_jointes.html.haml b/app/views/instructeurs/dossiers/pieces_jointes.html.haml index 95e08d1fc..527b2c65c 100644 --- a/app/views/instructeurs/dossiers/pieces_jointes.html.haml +++ b/app/views/instructeurs/dossiers/pieces_jointes.html.haml @@ -1,35 +1,8 @@ - content_for(:title, "Pièces jointes") -= render partial: "header", locals: { dossier: @dossier } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } .fr-container .gallery.gallery-pieces-jointes{ "data-controller": "lightbox" } - - @attachments_and_libelles.each do |attachment, libelle| - .gallery-item - - blob = attachment.blob - - if displayable_pdf?(blob) - = link_to blob.url, id: blob.id, data: { iframe: true, src: blob.url }, class: 'gallery-link', type: blob.content_type, title: "#{libelle} -- #{sanitize(blob.filename.to_s)}" do - .thumbnail - = image_tag(preview_url_for(attachment), loading: :lazy) - .fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button } - Visualiser - .champ-libelle - = libelle.truncate(25) - = render Attachment::ShowComponent.new(attachment: attachment, truncate: true) - - - elsif displayable_image?(blob) - = link_to image_url(blob_url(attachment)), title: "#{libelle} -- #{sanitize(blob.filename.to_s)}", data: { src: blob.url }, class: 'gallery-link' do - .thumbnail - = image_tag(variant_url_for(attachment), loading: :lazy) - .fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button } - Visualiser - .champ-libelle - = libelle.truncate(25) - = render Attachment::ShowComponent.new(attachment: attachment, truncate: true) - - - else - .thumbnail - = image_tag('apercu-indisponible.png') - .champ-libelle - = libelle.truncate(25) - = render Attachment::ShowComponent.new(attachment: attachment, truncate: true) + - @gallery_attachments.each do |attachment| + = render Attachment::GalleryItemComponent.new(attachment:, seen_at: @pieces_jointes_seen_at) diff --git a/app/views/instructeurs/dossiers/reaffectation.html.haml b/app/views/instructeurs/dossiers/reaffectation.html.haml index 5b5307592..364b5415a 100644 --- a/app/views/instructeurs/dossiers/reaffectation.html.haml +++ b/app/views/instructeurs/dossiers/reaffectation.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Réaffectation · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } .container.groupe-instructeur diff --git a/app/views/instructeurs/dossiers/show.html.haml b/app/views/instructeurs/dossiers/show.html.haml index b93389716..8c9ad55e4 100644 --- a/app/views/instructeurs/dossiers/show.html.haml +++ b/app/views/instructeurs/dossiers/show.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Demande · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } - if @dossier.etablissement&.as_degraded_mode? diff --git a/app/views/shared/champs/piece_justificative/_show.html.haml b/app/views/shared/champs/piece_justificative/_show.html.haml index 70041de21..46c3c0f3d 100644 --- a/app/views/shared/champs/piece_justificative/_show.html.haml +++ b/app/views/shared/champs/piece_justificative/_show.html.haml @@ -2,25 +2,7 @@ - if profile == 'instructeur' .gallery-items-list - champ.piece_justificative_file.attachments.with_all_variant_records.each do |attachment| - .gallery-item - - blob = attachment.blob - - if displayable_pdf?(blob) - = link_to blob.url, id: blob.id, data: { iframe: true, src: blob.url }, class: 'gallery-link', type: blob.content_type, title: "#{champ.libelle} -- #{sanitize(blob.filename.to_s)}" do - .thumbnail - = image_tag(preview_url_for(attachment), loading: :lazy) - .fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button } - = 'Visualiser' - - - elsif displayable_image?(blob) - = link_to image_url(blob_url(attachment)), title: "#{champ.libelle} -- #{sanitize(blob.filename.to_s)}", data: { src: blob.url }, class: 'gallery-link' do - .thumbnail - = image_tag(variant_url_for(attachment), loading: :lazy) - .fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button } - = 'Visualiser' - - else - .thumbnail - = image_tag('apercu-indisponible.png') - = render Attachment::ShowComponent.new(attachment:, new_tab: true, truncate: true) + = render Attachment::GalleryItemComponent.new(attachment:, gallery_demande: true) - else %ul - champ.piece_justificative_file.attachments.each do |attachment| diff --git a/db/migrate/20240910135752_add_pieces_jointes_updates_to_dossiers.rb b/db/migrate/20240910135752_add_pieces_jointes_updates_to_dossiers.rb new file mode 100644 index 000000000..e8374aa23 --- /dev/null +++ b/db/migrate/20240910135752_add_pieces_jointes_updates_to_dossiers.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddPiecesJointesUpdatesToDossiers < ActiveRecord::Migration[7.0] + def change + add_column :dossiers, :last_champ_piece_jointe_updated_at, :datetime + add_column :dossiers, :last_commentaire_piece_jointe_updated_at, :datetime + end +end diff --git a/db/migrate/20240910145644_add_pieces_jointes_seen_at_to_follows.rb b/db/migrate/20240910145644_add_pieces_jointes_seen_at_to_follows.rb new file mode 100644 index 000000000..cce53f945 --- /dev/null +++ b/db/migrate/20240910145644_add_pieces_jointes_seen_at_to_follows.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddPiecesJointesSeenAtToFollows < ActiveRecord::Migration[7.0] + def up + add_column :follows, :pieces_jointes_seen_at, :datetime + change_column_default :follows, :pieces_jointes_seen_at, from: nil, to: 'CURRENT_TIMESTAMP' + end + + def down + remove_column :follows, :pieces_jointes_seen_at + end +end diff --git a/db/migrate/20240911064340_backfill_follows_with_pieces_jointes_seen_at.rb b/db/migrate/20240911064340_backfill_follows_with_pieces_jointes_seen_at.rb new file mode 100644 index 000000000..82a51de34 --- /dev/null +++ b/db/migrate/20240911064340_backfill_follows_with_pieces_jointes_seen_at.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class BackfillFollowsWithPiecesJointesSeenAt < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + def up + Follow.in_batches do |relation| + relation.update_all pieces_jointes_seen_at: Time.zone.now + sleep(0.001) # throttle + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index a628a89ea..e55fcb86b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -488,8 +488,10 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_23_125619) do t.datetime "hidden_by_user_at", precision: nil t.datetime "identity_updated_at", precision: nil t.datetime "last_avis_updated_at", precision: nil + t.datetime "last_champ_piece_jointe_updated_at" t.datetime "last_champ_private_updated_at", precision: nil t.datetime "last_champ_updated_at", precision: nil + t.datetime "last_commentaire_piece_jointe_updated_at" t.datetime "last_commentaire_updated_at", precision: nil t.string "mandataire_first_name" t.string "mandataire_last_name" @@ -689,6 +691,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_23_125619) do t.integer "dossier_id", null: false t.integer "instructeur_id", null: false t.datetime "messagerie_seen_at", precision: nil, null: false + t.datetime "pieces_jointes_seen_at" t.datetime "unfollowed_at", precision: nil t.datetime "updated_at", precision: nil t.index ["dossier_id"], name: "index_follows_on_dossier_id" diff --git a/spec/components/attachment/gallery_item_component_spec.rb b/spec/components/attachment/gallery_item_component_spec.rb new file mode 100644 index 000000000..382c6cf10 --- /dev/null +++ b/spec/components/attachment/gallery_item_component_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Attachment::GalleryItemComponent, type: :component do + let(:instructeur) { create(:instructeur) } + let(:procedure) { create(:procedure, :published, types_de_champ_public:) } + let(:types_de_champ_public) { [{ type: :piece_justificative }] } + let(:dossier) { create(:dossier, :with_populated_champs, :en_construction, procedure:) } + let(:filename) { attachment.blob.filename.to_s } + let(:gallery_demande) { false } + let(:seen_at) { nil } + let(:now) { Time.zone.now } + + let(:component) { described_class.new(attachment: attachment, gallery_demande:, seen_at: seen_at) } + + subject { render_inline(component).to_html } + + context "when attachment is from a piece justificative champ" do + let(:champ) { dossier.champs.first } + let(:libelle) { champ.libelle } + let(:attachment) { champ.piece_justificative_file.attachments.first } + + # Correspond au cas standard où le blob est créé avant le dépôt du dossier + before { dossier.touch(:depose_at) } + + it "displays libelle, link, tag and renders title" do + expect(subject).to have_text(libelle) + expect(subject).not_to have_text('Pièce jointe au message') + expect(subject).to have_link(filename) + expect(subject).to have_text('Dossier usager') + expect(component.title).to eq("#{libelle} -- #{filename}") + end + + it "displays when gallery item has been added" do + expect(subject).to have_text('Ajoutée le') + expect(subject).not_to have_css('.highlighted') + expect(subject).to have_text(component.helpers.try_format_datetime(attachment.record.created_at, format: :veryshort)) + end + + context "when gallery item has been updated" do + # un nouveau blob est créé après modification d'un champ pièce justificative + before { attachment.blob.touch(:created_at) } + + it 'displays the right text' do + expect(subject).to have_text('Modifiée le') + end + end + + context "when gallery item is in page Demande" do + let(:gallery_demande) { true } + + it "does not display libelle" do + expect(subject).not_to have_text(libelle) + end + end + end + + context "when attachment is from a commentaire" do + let(:commentaire) { create(:commentaire, :with_file, dossier: dossier) } + let(:attachment) { commentaire.piece_jointe.first } + + context 'from an usager' do + it "displays a generic libelle, link, tag and renders title" do + expect(subject).to have_text('Pièce jointe au message') + expect(subject).to have_link(filename) + expect(subject).to have_text('Messagerie (usager)') + expect(component.title).to eq("Pièce jointe au message -- #{filename}") + end + + context "when instructeur has not seen it yet" do + let(:seen_at) { now - 1.day } + + before do + attachment.blob.update(created_at: now) + end + + it 'displays datetime in the right style' do + expect(subject).to have_css('.highlighted') + end + end + + context "when instructeur has already seen it" do + let!(:seen_at) { now } + + before do + freeze_time + attachment.blob.touch(:created_at) + end + + it 'displays datetime in the right style' do + expect(subject).not_to have_css('.highlighted') + end + end + end + + context 'from an instructeur' do + before { commentaire.update!(instructeur:) } + it "displays the right tag" do + expect(subject).to have_text('Messagerie (instructeur)') + end + end + end +end diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 510a16ab6..42a4a3bd3 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -1513,7 +1513,9 @@ describe Instructeurs::DossiersController, type: :controller do expect(response.body).to include('Télécharger le fichier logo_test_procedure.png') expect(response.body).to include('Télécharger le fichier RIB.pdf') expect(response.body).to include('Visualiser') - expect(assigns(:attachments_and_libelles).count).to eq 3 + expect(assigns(:gallery_attachments).count).to eq 3 + expect(assigns(:gallery_attachments)).to all(be_a(ActiveStorage::Attachment)) + expect([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp, Commentaire]).to include(*assigns(:gallery_attachments).map { _1.record.class }) end end end diff --git a/spec/helpers/gallery_helper_spec.rb b/spec/helpers/gallery_helper_spec.rb index 6f4d3567c..a6783f13f 100644 --- a/spec/helpers/gallery_helper_spec.rb +++ b/spec/helpers/gallery_helper_spec.rb @@ -74,4 +74,20 @@ RSpec.describe GalleryHelper, type: :helper do it { is_expected.to eq("pdf-placeholder.png") } end end + + describe ".representation_url_for" do + subject { representation_url_for(attachment) } + + context "when attachment is an image with no variant" do + let(:file) { fixture_file_upload('spec/fixtures/files/logo_test_procedure.png', 'image/png') } + + it { is_expected.to eq("apercu-indisponible.png") } + end + + context "when attachment is a pdf with no preview" do + let(:file) { fixture_file_upload('spec/fixtures/files/RIB.pdf', 'application/pdf') } + + it { is_expected.to eq("pdf-placeholder.png") } + end + end end diff --git a/spec/models/concerns/dossier_clone_concern_spec.rb b/spec/models/concerns/dossier_clone_concern_spec.rb index e7e5f4869..dd33470ab 100644 --- a/spec/models/concerns/dossier_clone_concern_spec.rb +++ b/spec/models/concerns/dossier_clone_concern_spec.rb @@ -44,7 +44,9 @@ RSpec.describe DossierCloneConcern do expect(new_dossier.last_avis_updated_at).to be_nil expect(new_dossier.last_champ_private_updated_at).to be_nil expect(new_dossier.last_champ_updated_at).to be_nil + expect(new_dossier.last_champ_piece_jointe_updated_at).to be_nil expect(new_dossier.last_commentaire_updated_at).to be_nil + expect(new_dossier.last_commentaire_piece_jointe_updated_at).to be_nil expect(new_dossier.motivation).to be_nil expect(new_dossier.processed_at).to be_nil end diff --git a/spec/models/instructeur_spec.rb b/spec/models/instructeur_spec.rb index 54f2a21b0..9523e6779 100644 --- a/spec/models/instructeur_spec.rb +++ b/spec/models/instructeur_spec.rb @@ -196,7 +196,7 @@ describe Instructeur, type: :model do subject { instructeur.notifications_for_dossier(dossier) } context 'when the instructeur has just followed the dossier' do - it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: false }) } + it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: false, pieces_jointes: false }) } end context 'when there is a modification on public champs' do @@ -205,20 +205,20 @@ describe Instructeur, type: :model do dossier.update(last_champ_updated_at: Time.zone.now) } - it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false }) } + it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false, pieces_jointes: false }) } end context 'when there is a modification on identity' do before { dossier.update(identity_updated_at: Time.zone.now) } - it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false }) } + it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false, pieces_jointes: false }) } end context 'when there is a modification on groupe instructeur' do let(:groupe_instructeur) { create(:groupe_instructeur, instructeurs: [instructeur], procedure: dossier.procedure) } before { dossier.assign_to_groupe_instructeur(groupe_instructeur, DossierAssignment.modes.fetch(:auto)) } - it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false }) } + it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false, pieces_jointes: false }) } end context 'when there is a modification on private champs' do @@ -227,7 +227,7 @@ describe Instructeur, type: :model do dossier.update(last_champ_private_updated_at: Time.zone.now) } - it { is_expected.to match({ demande: false, annotations_privees: true, avis: false, messagerie: false }) } + it { is_expected.to match({ demande: false, annotations_privees: true, avis: false, messagerie: false, pieces_jointes: false }) } end context 'when there is a modification on avis' do @@ -236,23 +236,34 @@ describe Instructeur, type: :model do dossier.update(last_avis_updated_at: Time.zone.now) } - it { is_expected.to match({ demande: false, annotations_privees: false, avis: true, messagerie: false }) } + it { is_expected.to match({ demande: false, annotations_privees: false, avis: true, messagerie: false, pieces_jointes: false }) } end context 'messagerie' do context 'when there is a new commentaire' do - before { - create(:commentaire, dossier: dossier, email: 'a@b.com') - dossier.update(last_commentaire_updated_at: Time.zone.now) - } + context 'without a file' do + before { + create(:commentaire, dossier: dossier, email: 'a@b.com') + dossier.update(last_commentaire_updated_at: Time.zone.now) + } - it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: true }) } + it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: true, pieces_jointes: false }) } + end + + context 'with a file' do + before { + create(:commentaire, :with_file, dossier: dossier, email: 'a@b.com') + dossier.update(last_commentaire_updated_at: Time.zone.now, last_commentaire_piece_jointe_updated_at: Time.zone.now) + } + + it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: true, pieces_jointes: true }) } + end end context 'when there is a new commentaire issued by tps' do before { create(:commentaire, dossier: dossier, email: CONTACT_EMAIL) } - it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: false }) } + it { is_expected.to match({ demande: false, annotations_privees: false, avis: false, messagerie: false, pieces_jointes: false }) } end end end