diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f33954ca61..9e2c14e703 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,7 +134,14 @@ jobs: path: node_modules - name: Install JS dependencies run: yarn install + - name: Cache precompiled assets + id: cache-precompiled-assets + uses: actions/cache@v4 + with: + key: assets-${{ hashFiles('yarn.lock', 'app/javascript/**') }} + path: app/assets/builds - name: Precompile assets + if: steps.cache-precompiled-assets.outputs.cache-hit != 'true' # Skip if cache was restored run: yarn run build - name: Prepare runtime log cache key run: ls spec/**/*.rb > tmp/spec_files.txt @@ -202,7 +209,14 @@ jobs: path: node_modules - name: Install JS dependencies run: yarn install + - name: Cache precompiled assets + id: cache-precompiled-assets + uses: actions/cache@v4 + with: + key: assets-${{ hashFiles('yarn.lock', 'app/javascript/**') }} + path: app/assets/builds - name: Precompile assets + if: steps.cache-precompiled-assets.outputs.cache-hit != 'true' # Skip if cache was restored run: yarn run build - name: Prepare runtime log cache key run: "ls spec/features/${{ matrix.dirname }}/**/*.rb > tmp/feature_spec_${{ matrix.dirname }}_files.txt" diff --git a/Gemfile.lock b/Gemfile.lock index 01cbdf8d33..a5f376ada5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -711,7 +711,7 @@ GEM unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uniform_notifier (1.16.0) - uri (0.13.0) + uri (1.0.3) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) diff --git a/app/controllers/admin/organisations_controller.rb b/app/controllers/admin/organisations_controller.rb index 9cbdf53fd4..d3813c4d40 100644 --- a/app/controllers/admin/organisations_controller.rb +++ b/app/controllers/admin/organisations_controller.rb @@ -2,7 +2,6 @@ class Admin::OrganisationsController < AgentAuthController respond_to :html, :json before_action :set_organisation, except: :index - before_action :follow_unique, only: :index def index @organisations_by_territory = policy_scope(current_agent.organisations, policy_scope_class: Agent::OrganisationPolicy::Scope) @@ -73,11 +72,4 @@ def organisation_params def new_organisation_params params.require(:organisation).permit(:name, :territory_id) end - - def follow_unique - accessible_organisations = policy_scope(Organisation, policy_scope_class: Agent::OrganisationPolicy::Scope) - return if params[:follow_unique].blank? || accessible_organisations.count != 1 - - redirect_to admin_organisation_agent_agenda_path(accessible_organisations.first, current_agent) - end end diff --git a/app/controllers/agent_connect_controller.rb b/app/controllers/agent_connect_controller.rb index 638e959f20..c5d6222ca0 100644 --- a/app/controllers/agent_connect_controller.rb +++ b/app/controllers/agent_connect_controller.rb @@ -14,7 +14,7 @@ def auth redirect_to auth_client.redirect_url(agent_connect_callback_url), allow_other_host: true end - def callback + def callback # rubocop:disable Metrics/PerceivedComplexity callback_client = AgentConnectOpenIdClient::Callback.new( session_state: session.delete(:agent_connect_state), params_state: params[:state], @@ -33,6 +33,10 @@ def callback # voir https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/donnees_fournies.md#le-champ-sub agent = Agent.active.find_by(email: callback_client.user_email) + if current_domain.allow_agent_creation_with_agent_connect + agent ||= Agent.new(email: callback_client.user_email, password: SecureRandom.base64(32)) + end + if agent agent.update!( connected_with_agent_connect: true, diff --git a/app/controllers/agents/pages_controller.rb b/app/controllers/agents/pages_controller.rb new file mode 100644 index 0000000000..dfaa24fc15 --- /dev/null +++ b/app/controllers/agents/pages_controller.rb @@ -0,0 +1,23 @@ +class Agents::PagesController < AgentAuthController + layout "application" + + CONTACT_TEAM_URL = "https://cal.com/forms/937585aa-48a4-4efd-a642-961fad79c9c5".freeze + + def home + skip_authorization + + accessible_organisations = policy_scope(Organisation, policy_scope_class: Agent::OrganisationPolicy::Scope) + + if accessible_organisations.count == 1 + redirect_to admin_organisation_agent_agenda_path(accessible_organisations.first, current_agent) + elsif accessible_organisations.count > 1 + redirect_to admin_organisations_path + end + end + + private + + def pundit_user + AgentContext.new(current_agent) + end +end diff --git a/app/controllers/super_admins/comptes_controller.rb b/app/controllers/super_admins/comptes_controller.rb index 9114d98f45..ed92fcb9ca 100644 --- a/app/controllers/super_admins/comptes_controller.rb +++ b/app/controllers/super_admins/comptes_controller.rb @@ -1,5 +1,14 @@ module SuperAdmins class ComptesController < SuperAdmins::ApplicationController + def new + @agent = Agent.find_by(id: params[:agent_id]) + if @agent + authorize_resource(@agent) + end + + super + end + def create compte_params[:agent][:invited_by] = current_super_admin compte = Compte.new(compte_params, current_domain) @@ -24,7 +33,7 @@ def compte_params territory: %i[name departement_number], organisation: %i[name ants_connectable], lieu: %i[address latitude longitude], - agent: %i[first_name last_name email service_ids] + agent: %i[id first_name last_name email service_ids] ) end end diff --git a/app/form_models/compte.rb b/app/form_models/compte.rb index 9eb3c0f027..2e091f9b10 100644 --- a/app/form_models/compte.rb +++ b/app/form_models/compte.rb @@ -26,10 +26,7 @@ def save! organisation.save! lieu.save! - self.agent = Agent.invite!(@attributes[:agent].merge( - password: SecureRandom.base64(32), - roles_attributes: [{ organisation: organisation, access_level: AgentRole::ACCESS_LEVEL_ADMIN }] - )) + self.agent = find_or_invite_agent(organisation) agent.services.each do |service| TerritoryService.create!(service: service, territory: territory) @@ -65,6 +62,23 @@ def to_s private + def find_or_invite_agent(organisation) + if @attributes.dig(:agent, :id) + Agent.find(@attributes.dig(:agent, :id)).tap do |agent| + agent.update( + @attributes[:agent].merge( + roles_attributes: [{ organisation: organisation, access_level: AgentRole::ACCESS_LEVEL_ADMIN }] + ) + ) + end + else + Agent.invite!(@attributes[:agent].merge( + roles_attributes: [{ organisation: organisation, access_level: AgentRole::ACCESS_LEVEL_ADMIN }], + password: SecureRandom.base64(32) + )) + end + end + def create_mairie_motifs! service = Service.find_by(name: Service::MAIRIE) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 39965623f8..7fb377bfdb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -113,9 +113,12 @@ def boolean_attribute_tag(object, attribute_name) boolean_tag(value) { object.class.human_attribute_value(attribute_name, value) } end - def object_attribute_tag(object, attribute_name, value = nil) + def object_attribute_tag(object, attribute_name, value = :delegate_to_object) name = object.class.human_attribute_name(attribute_name) - value ||= object.human_attribute_value(attribute_name) + + if value == :delegate_to_object + value = object.human_attribute_value(attribute_name) + end tag.strong(tag.span(name) + tag.span(" : ")) + tag.span(value.presence || "Non renseigné", class: class_names("text-muted": value.blank?)) diff --git a/app/javascript/stylesheets/administrate/base/_forms.scss b/app/javascript/stylesheets/administrate/base/_forms.scss index 695b440f86..e2e637fc2d 100644 --- a/app/javascript/stylesheets/administrate/base/_forms.scss +++ b/app/javascript/stylesheets/administrate/base/_forms.scss @@ -107,3 +107,7 @@ select { outline-offset: $focus-outline-offset; } } + +.form-group { + margin: 8px 0; +} diff --git a/app/jobs/create_crisp_ticket_job.rb b/app/jobs/create_crisp_ticket_job.rb index 98f929730b..6f4ee8768f 100644 --- a/app/jobs/create_crisp_ticket_job.rb +++ b/app/jobs/create_crisp_ticket_job.rb @@ -32,6 +32,9 @@ def perform(nickname:, email:, phone:, subject:, message:, role:, domain:) domain, ], subject:, + device: { + locales: ["fr"], + }, } ) end diff --git a/app/models/agent.rb b/app/models/agent.rb index 216dc9b00e..af0f0e0e06 100644 --- a/app/models/agent.rb +++ b/app/models/agent.rb @@ -100,7 +100,7 @@ def timeout_in = 14.days # Used by Devise's :timeoutable # * it validates :email (the invite_key) specifically with Devise.email_regexp. validates :first_name, presence: true, unless: -> { allow_blank_name || is_an_intervenant? } validates :last_name, presence: true, unless: -> { allow_blank_name } - validates :agent_services, presence: true + validates :agent_services, presence: true, unless: -> { roles.none? } # Hooks before_destroy :prevent_destroy_if_rdvs diff --git a/app/models/domain.rb b/app/models/domain.rb index 62ec2640c7..4823b80c88 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -16,6 +16,7 @@ :support_email, :secretariat_email, :verticale, + :allow_agent_creation_with_agent_connect, keyword_init: true ) @@ -36,6 +37,7 @@ class Domain france_connect_enabled: true, support_email: "support@rdv-solidarites.fr", verticale: :rdv_solidarites, + allow_agent_creation_with_agent_connect: false, secretariat_email: "secretariat-auto@rdv-solidarites.fr" # secretariat_email est utilisé comme adresse de "Reply-To" pour les e-mails # qui contiennent des ICS. Lorsque l'événement ICS est acceptée par le @@ -58,6 +60,7 @@ class Domain france_connect_enabled: false, support_email: "support@rdv-aide-numerique.fr", verticale: :rdv_aide_numerique, + allow_agent_creation_with_agent_connect: false, secretariat_email: "secretariat-auto@rdv-solidarites.fr" ), @@ -76,6 +79,7 @@ class Domain france_connect_enabled: true, support_email: "support@rdv-service-public.fr", verticale: :rdv_mairie, + allow_agent_creation_with_agent_connect: true, secretariat_email: "secretariat-auto@rdv-service-public.fr" ), ].freeze diff --git a/app/services/admin_creates_agent.rb b/app/services/admin_creates_agent.rb index b077d23c8f..23993464e9 100644 --- a/app/services/admin_creates_agent.rb +++ b/app/services/admin_creates_agent.rb @@ -12,6 +12,9 @@ def call @agent = find_agent if @agent + if @agent.services.none? + @agent.update(service_ids: @agent_params[:service_ids]) + end add_agent_to_organisations @warning_message = self.class.check_agent_service(@agent, @agent_params[:service_ids]) elsif @access_level == "intervenant" diff --git a/app/services/concerns/users/creneaux_wizard_concern.rb b/app/services/concerns/users/creneaux_wizard_concern.rb index e00586c644..d3b12364cc 100644 --- a/app/services/concerns/users/creneaux_wizard_concern.rb +++ b/app/services/concerns/users/creneaux_wizard_concern.rb @@ -1,8 +1,7 @@ module Users::CreneauxWizardConcern extend ActiveSupport::Concern - # *** Method that outputs the next step for the user to complete its rdv journey *** - # *** It is used in #to_partial_path to render the matching partial view *** + # *** Method that outputs the current step for the user to complete its rdv journey *** def current_step if departement.blank? :address_selection @@ -23,10 +22,6 @@ def start_date query_params[:date]&.to_date || super end - def to_partial_path - "search/#{current_step}" - end - def wizard_after_creneau_selection_path(params) url_helpers = Rails.application.routes.url_helpers if @prescripteur @@ -48,7 +43,7 @@ def unique_motifs_by_name_and_location_type # Retourne une liste d'organisations et leur prochaine dispo, ordonnées par date de prochaine dispo def next_availability_by_motifs_organisations @next_availability_by_motifs_organisations ||= matching_motifs.to_h do |motif| - [motif.organisation, creneaux_search_for(nil, date_range, motif).next_availability] + [motif.organisation, creneaux_search_for(nil, motif).next_availability] end.compact.sort_by(&:last).to_h end @@ -72,7 +67,7 @@ def next_availability_by_lieux return @next_availability_by_lieux if @next_availability_by_lieux next_availability_by_lieux = Lieu.with_open_slots_for_motifs(matching_motifs).includes(:organisation).to_h do |lieu| - next_availability = creneaux_search_for(lieu, date_range, matching_motifs.where(organisation: lieu.organisation).first).next_availability + next_availability = creneaux_search_for(lieu, matching_motifs.where(organisation: lieu.organisation).first).next_availability [lieu, next_availability] end.compact diff --git a/app/services/invitation_search_context.rb b/app/services/invitation_search_context.rb index 4761d01b63..411919c971 100644 --- a/app/services/invitation_search_context.rb +++ b/app/services/invitation_search_context.rb @@ -35,20 +35,4 @@ def matching_motifs Motif.available_for_booking.where(organisation_id: @organisation_ids).joins(:organisation) ) end - - def contactable_organisations - @contactable_organisations ||= Organisation.where(id: @organisation_ids).contactable - end - - def organisations_emails - contactable_organisations.where.not(email: [nil, ""]).pluck(:email).join(",") - end - - def motif_category_name - @motif_category_short_name.present? ? MotifCategory.find_by(short_name: @motif_category_short_name)&.name : nil - end - - private - - attr_reader :referent_ids, :lieu_id end diff --git a/app/services/merge_users_service.rb b/app/services/merge_users_service.rb index 71206fa72d..cc80a12fa0 100644 --- a/app/services/merge_users_service.rb +++ b/app/services/merge_users_service.rb @@ -25,8 +25,8 @@ def merge_annotations current_territory = @organisation.territory annotation_to_merge = @user_to_merge.annotations.find_by(territory: current_territory) - @user_target.annotate!(annotation_to_merge.content, territory: current_territory) - annotation_to_merge.destroy! + @user_target.annotate!(annotation_to_merge&.content, territory: current_territory) + annotation_to_merge&.destroy! end def merge_user_attributes diff --git a/app/services/search_context.rb b/app/services/search_context.rb index 4bcc242727..0335a6ca0b 100644 --- a/app/services/search_context.rb +++ b/app/services/search_context.rb @@ -25,7 +25,7 @@ def available_collective_rdvs end def creneaux_search - creneaux_search_for(lieu, date_range, first_matching_motif) + creneaux_search_for(lieu, first_matching_motif) end def first_matching_motif @@ -54,9 +54,7 @@ def filter_motifs(available_motifs) private - def referent_ids - raise NoMethodError - end + attr_reader :referent_ids, :lieu_id def matching_motifs raise NoMethodError @@ -74,11 +72,7 @@ def street_ban_id raise NoMethodError end - def lieu_id - raise NoMethodError - end - - def creneaux_search_for(lieu, date_range, motif) + def creneaux_search_for(lieu, motif) CreneauxSearch::ForUser.new( user: @user, motif: motif, diff --git a/app/services/users/contactable_organisations.rb b/app/services/users/contactable_organisations.rb new file mode 100644 index 0000000000..8534ffc3cb --- /dev/null +++ b/app/services/users/contactable_organisations.rb @@ -0,0 +1,18 @@ +class Users::ContactableOrganisations + def initialize(organisation_ids, motif_category_short_name) + @organisation_ids = organisation_ids + @motif_category_short_name = motif_category_short_name + end + + def organisations + @organisations ||= Organisation.where(id: @organisation_ids).contactable + end + + def organisations_emails + organisations.where.not(email: [nil, ""]).pluck(:email).join(",") + end + + def motif_category_name + @motif_category_short_name.present? ? MotifCategory.find_by(short_name: @motif_category_short_name)&.name : nil + end +end diff --git a/app/services/web_invitation_search_context.rb b/app/services/web_invitation_search_context.rb index fb5820bab2..9eeb4ec47a 100644 --- a/app/services/web_invitation_search_context.rb +++ b/app/services/web_invitation_search_context.rb @@ -1,6 +1,6 @@ class WebInvitationSearchContext < InvitationSearchContext include Users::CreneauxWizardConcern - attr_reader :errors, :query_params, :address, :latitude, :longitude + attr_reader :errors, :query_params, :address, :latitude, :longitude, :organisation_ids, :motif_category_short_name def initialize(user:, query_params: {}) super diff --git a/app/services/web_search_context.rb b/app/services/web_search_context.rb index b8f450c7ca..77fa1d965e 100644 --- a/app/services/web_search_context.rb +++ b/app/services/web_search_context.rb @@ -13,7 +13,6 @@ def initialize(user:, query_params: {}) @user_selected_organisation_id = query_params[:user_selected_organisation_id] @external_organisation_ids = query_params[:external_organisation_ids] @motif_id = query_params[:motif_id] - @motif_category_short_name = query_params[:motif_category_short_name] @motif_name_with_location_type = query_params[:motif_name_with_location_type] @service_id = query_params[:service_id] @lieu_id = query_params[:lieu_id] @@ -68,8 +67,6 @@ def motif_param_present? private - attr_reader :referent_ids, :lieu_id - def matching_motifs @matching_motifs ||= filter_motifs(geo_search.available_motifs) end diff --git a/app/views/admin/participations/_user_details.html.slim b/app/views/admin/participations/_user_details.html.slim index 32df7ec260..9a3edab811 100644 --- a/app/views/admin/participations/_user_details.html.slim +++ b/app/views/admin/participations/_user_details.html.slim @@ -11,7 +11,7 @@ div li= object_attribute_tag(user, :address) li= object_attribute_tag(user, :email, clickable_user_email(user)) - if current_territory.enable_notes_field? - li= object_attribute_tag(user, :annotation_content, formatted_user_annotation(user, current_territory).to_s) + li= object_attribute_tag(user, :annotation_content, formatted_user_annotation(user, current_territory)) - if current_territory.enable_logement_field li= object_attribute_tag(user, :logement) - Territory::SOCIAL_FIELD_TOGGLES.each do |toggle, field_name| diff --git a/app/views/admin/prescription/search_creneau.html.slim b/app/views/admin/prescription/search_creneau.html.slim index f858cbf2d3..6272bc1566 100644 --- a/app/views/admin/prescription/search_creneau.html.slim +++ b/app/views/admin/prescription/search_creneau.html.slim @@ -9,4 +9,4 @@ h1 Nouveau RDV par prescription |> Pas d'adresse = link_to("ajouter une addresse", edit_admin_organisation_user_path(current_organisation, @context.user)) section.container - = render partial: @context.to_partial_path, locals: { context: @context } + = render partial: "search/#{@context.current_step}", locals: { context: @context } diff --git a/app/views/admin/users/_responsible_information.html.slim b/app/views/admin/users/_responsible_information.html.slim index d078d854ec..63d51b3a17 100644 --- a/app/views/admin/users/_responsible_information.html.slim +++ b/app/views/admin/users/_responsible_information.html.slim @@ -21,7 +21,7 @@ ul.list-unstyled.mb-5 | En attente de confirmation pour #{user.unconfirmed_email} - if current_territory.enable_notes_field? - li.mb-2= object_attribute_tag(user, :annotation_content, formatted_user_annotation(user, current_territory).to_s) + li.mb-2= object_attribute_tag(user, :annotation_content, formatted_user_annotation(user, current_territory)) - if current_territory.any_social_field_enabled? h4.card-title.mb-3 Informations sociales diff --git a/app/views/agents/pages/home.html.slim b/app/views/agents/pages/home.html.slim new file mode 100644 index 0000000000..a52c2d285f --- /dev/null +++ b/app/views/agents/pages/home.html.slim @@ -0,0 +1,17 @@ +.fr-container.fr-pb-8w + h1.fr-h3.fr-pb-2w Bienvenue ! + p.fr-py-1w Pour commencer, aidez-nous à en savoir plus : + + p.fr-pt-1w.fr-pb-2w + | Votre structure utilise déjà #{current_domain.name} et vous souhaitez disposer d’un accès ? + br + | Vos collègues peuvent vous inviter depuis le menu “Paramètres > Agents > Ajouter un agent”. + + p.fr-py-1w + | Votre structure n’utilise pas #{current_domain.name} et vous souhaitez créer un compte ? + br + | Nous vous invitons à contacter notre équipe. Nous organiserons un temps d’échanges pour vous présenter la solution et créer le compte de votre structure. + + ul.fr-btns-group.fr-btns-group--center.fr-btns-group--inline-md.fr-mb-8w + li + = link_to "Contacter notre équipe", "https://cal.com/team/rdv-service-public/temps-d-echanges", class: "fr-btn" diff --git a/app/views/dsfr/rdv_mairie/homepage.html.slim b/app/views/dsfr/rdv_mairie/homepage.html.slim index 067b1ee4f0..df014efbba 100644 --- a/app/views/dsfr/rdv_mairie/homepage.html.slim +++ b/app/views/dsfr/rdv_mairie/homepage.html.slim @@ -1,4 +1,4 @@ -- contact_team_url = "https://cal.com/forms/937585aa-48a4-4efd-a642-961fad79c9c5" +- contact_team_url = Agents::PagesController::CONTACT_TEAM_URL .fr-py-8w.rdv-background-color-alt-blue-ecume .fr-container @@ -14,7 +14,7 @@ | Éviter les rendez-vous non honorés, gagner du temps au quotidien et améliorer la relation entre les agents et les usagers ul.fr-btns-group.fr-btns-group--center.fr-btns-group--inline-md.fr-mb-0 li - = link_to "Contacter notre équipe", contact_team_url, class: "fr-btn fr-mb-0" + = link_to "Demander une démo", contact_team_url, class: "fr-btn fr-mb-0" .fr-container.fr-py-8w .fr-grid-row.fr-grid-row--gutters.fr-grid-row--center @@ -417,5 +417,5 @@ hr h2 Plus de RDV, moins de lapins p | Votre agenda vous remerciera. - .fr-my-4w= link_to "Contacter notre équipe", contact_team_url, class: "fr-btn" + .fr-my-4w= link_to "Demander une démo", contact_team_url, class: "fr-btn" = link_to "Revenir en haut de la page", "#header", class: "fr-link fr-icon-arrow-up-line fr-link--icon-left" diff --git a/app/views/search/_nothing_to_show_invitation.html.slim b/app/views/search/_nothing_to_show_invitation.html.slim index 021ee918c2..6571f80ee7 100644 --- a/app/views/search/_nothing_to_show_invitation.html.slim +++ b/app/views/search/_nothing_to_show_invitation.html.slim @@ -1,17 +1,19 @@ -- if context.contactable_organisations.present? - - if context.contactable_organisations.one? +- contactable_organisations = Users::ContactableOrganisations.new(context.organisation_ids, context.motif_category_short_name) + +- if contactable_organisations.organisations.present? + - if contactable_organisations.organisations.one? p Vous pouvez contacter l'organisation pour demander l'ouverture de créneaux. - else p Vous pouvez contacter les organisations suivantes pour leur demander d'ouvrir des créneaux. .row - - context.contactable_organisations.each do |organisation| + - contactable_organisations.organisations.each do |organisation| = render "nothing_to_show_contactable_organisation", organisation: organisation p Ou leur envoyer un email en cliquant sur le bouton ci-dessous. -- email = context.organisations_emails.presence || "support@rdv-insertion.fr" +- email = contactable_organisations.organisations_emails.presence || "support@rdv-insertion.fr" = mail_to email, - subject: "[Problème Invitation. Créneaux Indisponibles, motif : #{context.motif_category_name}]", - cc: "#{'support@rdv-insertion.fr' unless context.organisations_emails.empty?}", + subject: "[Problème Invitation. Créneaux Indisponibles, motif : #{contactable_organisations.motif_category_name}]", + cc: "#{'support@rdv-insertion.fr' unless contactable_organisations.organisations_emails.empty?}", class: "fr-btn fr-btn--icon-left fr-icon-mail-line" do | Envoyer une demande d'ouverture de créneaux diff --git a/app/views/search/search_rdv.html.slim b/app/views/search/search_rdv.html.slim index 138b602eb2..65745866cc 100644 --- a/app/views/search/search_rdv.html.slim +++ b/app/views/search/search_rdv.html.slim @@ -9,4 +9,4 @@ - else .fr-container section.py-4.fr-col-12.fr-col-lg-10.fr-col-offset-lg-1 - = render @context, context: @context + = render partial: "search/#{@context.current_step}", locals: { context: @context } diff --git a/app/views/static_pages/politique_de_confidentialite.html.slim b/app/views/static_pages/politique_de_confidentialite.html.slim index e9b9832619..a9de423bd8 100644 --- a/app/views/static_pages/politique_de_confidentialite.html.slim +++ b/app/views/static_pages/politique_de_confidentialite.html.slim @@ -162,20 +162,25 @@ th Garanties tbody tr - td Outscale SASU + td Scalingo td Hébergement td France - td = link_to "CGV Outscale", "https://fr.outscale.com/wp-content/uploads/2020/10/Outscale-CGV-2020-09.pdf", target: "_blank" + td = link_to " Contrat de Gestion des Traitements de Données Personnelles Scalingo", "https://scalingo.com/fr/contrat-gestion-traitements-donnees-personnelles", target: "_blank" tr - td Sendinblue + td Brevo td Envoi de mails td France - td = link_to "RGPD Brevo", "https://fr.sendinblue.com/rgpd/", target: "_blank" + td = link_to "Politique de Confidentialité Brevo", "https://www.brevo.com/fr/legal/privacypolicy/", target: "_blank" tr td LinkMobility td Envoi de SMS td France td = link_to "Politique de confidentialité Link Mobility", "https://www.linkmobility.com/fr/legal/politique-de-confidentialite", target: "_blank" + tr + td Crisp + td Service support (email et formulaire de contact) + td Pays-Bas + td = link_to "Politique de confidentialité Crisp", "https://storage.crisp.chat/public/documents/Crisp%20Privacy%20Policy%20FR.pdf", target: "_blank" h3 Sécurité et confidentialité des données diff --git a/app/views/super_admins/agents/show.html.slim b/app/views/super_admins/agents/show.html.slim index c0becefa68..67960bc7f6 100644 --- a/app/views/super_admins/agents/show.html.slim +++ b/app/views/super_admins/agents/show.html.slim @@ -7,8 +7,11 @@ header.main-content__header role="banner" => link_to "Inviter", invite_super_admins_agent_path(page.resource), method: :post, class: "button", data: { disable_with: "Veuillez patienter…"} if accessible_action?(page.resource, :invite) - if sign_in_as_allowed? => link_to "Se logger en tant que", sign_in_as_super_admins_agent_path(page.resource), class: "button" if accessible_action?(page.resource, :sign_in_as) - => link_to(t("administrate.actions.edit_resource", name: page.page_title), [:edit, namespace, page.resource], class: "button") if accessible_action?(page.resource, :edit) + => link_to("Modifier", [:edit, namespace, page.resource], class: "button") if accessible_action?(page.resource, :edit) => link_to("Migrer", new_super_admins_agent_migration_path(agent_id: page.resource.id), class: "button") + - if page.resource.roles.none? + = link_to("Ouvrir un compte", new_super_admins_compte_path(agent_id: page.resource.id), class: "button") + section.main-content__body dl - page.attributes.each do |attribute| diff --git a/app/views/super_admins/comptes/new.html.slim b/app/views/super_admins/comptes/new.html.slim index 1d74443e7e..1f541d8868 100644 --- a/app/views/super_admins/comptes/new.html.slim +++ b/app/views/super_admins/comptes/new.html.slim @@ -12,13 +12,14 @@ section.main-content__body p | Ce formulaire vous permet de créer un territoire, une organisation, et un lieu pour un agent qui en sera admin (généralement la personne référente du projet). p - | Un motif d'exemple "Mon premier motif" sera créé, sauf s'il s'agit d'une ouverture de compte pour une mairie. + | Des motifs "Suivi de dossier" seront créés par défaut = simple_form_for([namespace, page.resource], html: { class: "form" }) do |f| = f.simple_fields_for :territory do |ff| = ff.input :name, label: "Nom du territoire" = f.simple_fields_for :organisation do |ff| - = ff.input :name, label: "Nom de la première organisation et du premier lieu" + div[style="margin: 8px 0"] + = ff.input :name, label: "Nom de la première organisation et du premier lieu" div[style="margin: 16px 0 8px 0"] = ff.input :ants_connectable, as: :boolean, label: "Autoriser le branchement au moteur de recherche de l'ANTS", hint: "En cochant cette case, vous permettrez à cette organisation (probablement une mairie) d'apparaitre sur https://rendezvouspasseport.ants.gouv.fr/" @@ -32,11 +33,14 @@ section.main-content__body h2[style="margin: 32px 0 16px 0"] Admin de territoire = f.simple_fields_for :agent do |ff| - = ff.input :first_name, label: "Prénom" - = ff.input :last_name, label: "Nom" - = ff.input :email, label: "Adresse mail", hint: "Une invitation sera envoyée automatiquement" - - # rubocop:disable Rails/OutputSafety - = ff.input :service_ids, label: "Service", collection: Service.all, hint: "Si nécessaire, vous pouvez #{link_to('créer un nouveau service', new_super_admins_service_path, target: :blank)}".html_safe - - # rubocop:enable Rails/OutputSafety + - if @agent + p = @agent.full_name + p = @agent.email + = ff.hidden_field :id, value: @agent.id + - else + = ff.input :first_name, label: "Prénom" + = ff.input :last_name, label: "Nom" + = ff.input :email, label: "Adresse mail", hint: "Une invitation sera envoyée automatiquement" + = ff.input :service_ids, label: "Service", collection: Service.all, hint: sanitize("Si nécessaire, vous pouvez #{link_to('créer un nouveau service', new_super_admins_service_path, target: :blank)}") = f.submit diff --git a/app/views/users/relatives/_form_fields.html.slim b/app/views/users/relatives/_form_fields.html.slim index ef0cb41ac8..fdf2ef2f1f 100644 --- a/app/views/users/relatives/_form_fields.html.slim +++ b/app/views/users/relatives/_form_fields.html.slim @@ -1,9 +1,9 @@ -.form-row - .col-md-6= f.input :first_name - .col-md-6= f.input :last_name +.fr-grid-row.fr-grid-row--gutters.fr-mb-5v + .fr-col-md-6= f.dsfr_text_field :first_name + .fr-col-md-6= f.dsfr_text_field :last_name - if f.object.ants_pre_demande_number_required - = f.input :ants_pre_demande_number, required: true, hint: t("simple_form.hints.user.ants_pre_demande_number_html"), input_html: {style: "text-transform: uppercase;"} + = f.dsfr_text_field :ants_pre_demande_number, required: true, hint: t("simple_form.hints.user.ants_pre_demande_number_html"), input_html: {style: "text-transform: uppercase;"} = f.hidden_field :ants_pre_demande_number_required, value: "true" - = f.input :ants_meeting_point_id, as: :hidden + = f.hidden_field :ants_meeting_point_id - if current_domain != Domain::RDV_MAIRIE - = f.input :birth_date, as: :string, input_html: { type: "date" } + = f.dsfr_text_field :birth_date, type: :date, input_html: { type: "date" } diff --git a/app/views/users/relatives/edit.html.slim b/app/views/users/relatives/edit.html.slim index d769e20acc..b9c786a334 100644 --- a/app/views/users/relatives/edit.html.slim +++ b/app/views/users/relatives/edit.html.slim @@ -2,12 +2,12 @@ .card .card-body - = simple_form_for @form, url: relative_path(@form.user) do |f| + = form_for @form, url: relative_path(@form.user), builder: Dsfr::FormBuilder do |f| = render "model_errors", model: @form, f: f = render "form_fields", f: f .d-flex.justify-content-between - = link_to "Supprimer", relative_path(@form.user), method: :delete, class: "btn btn-outline-danger", data: { confirm: "Confirmez-vous la suppression de ce proche ?"} + = link_to "Supprimer", relative_path(@form.user), method: :delete, class: "fr-btn fr-btn--icon-left fr-btn--secondary fr-icon-delete-line", data: { confirm: "Confirmez-vous la suppression de ce proche ?"} .d-flex.justify-content-end - = link_to "Annuler", users_informations_path, class: "btn btn-link" - = f.button :submit, class: "btn" + = link_to "Annuler", users_informations_path, class: "fr-btn fr-btn--tertiary fr-mr-2v" + = f.button :submit, class: "fr-btn" diff --git a/app/views/users/relatives/new.html.slim b/app/views/users/relatives/new.html.slim index 845f70e181..8cab262e93 100644 --- a/app/views/users/relatives/new.html.slim +++ b/app/views/users/relatives/new.html.slim @@ -1,10 +1,11 @@ - content_for(:title) do h1.rdv-color-white Ajouter un proche -= simple_form_for @form, url: relatives_path, remote: true, data: { modal: true } do |f| += form_for @form, url: relatives_path, remote: true, data: { modal: true }, builder: Dsfr::FormBuilder do |f| = render "model_errors", model: @form, f: f = render "form_fields", f: f + // Note : on laisse le row et le col pour le moment car on va passer la modale au DSFR dans une PR suivante .row .col.rdv-text-align-right - button.btn.btn-link data-dismiss="modal" type="button" Annuler - = f.button :submit + button.fr-btn.fr-btn--secondary.fr-mr-2v data-dismiss="modal" type="button" Annuler + = f.button :submit, class: "fr-btn fr-btn--primary" diff --git a/app/views/welcome/super_admin.html.slim b/app/views/welcome/super_admin.html.slim index f1e71d4474..d4fc909b26 100644 --- a/app/views/welcome/super_admin.html.slim +++ b/app/views/welcome/super_admin.html.slim @@ -1,15 +1,13 @@ .fr-mb-3w - .row.justify-content-center.pt-6 - .col-3 - = link_to "Accéder à superadmin", "/omniauth/github", class: "btn btn-primary", method: :post + .rdv-text-align-center + = link_to "Accéder à superadmin", "/omniauth/github", class: "fr-btn", method: :post - if Rails.env.development? - .row.justify-content-center.mt-3 - .col-3 - h5 Liens utiles - ul - li = link_to "Letter Opener", letter_opener_web_path - li = link_to "Mailer Previews", "/rails/mailers" - li = link_to "SMS Previews", "/lapin/sms_preview" - li = link_to "Routes", "/rails/info/routes" - li = link_to "Rails properties", "/rails/info/properties" + .fr-col-lg-3.fr-col-offset-lg-5.fr-mt-3w + h5 Liens utiles + ul + li = link_to "Letter Opener", letter_opener_web_path + li = link_to "Mailer Previews", "/rails/mailers" + li = link_to "SMS Previews", "/lapin/sms_preview" + li = link_to "Routes", "/rails/info/routes" + li = link_to "Rails properties", "/rails/info/properties" diff --git a/config/routes.rb b/config/routes.rb index 9862271b80..fc61ea1de1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -290,7 +290,7 @@ end end authenticated :agent do - root to: "admin/organisations#index", as: :authenticated_agent_root, defaults: { follow_unique: "1" } + root to: "agents/pages#home", as: :authenticated_agent_root end scope path: "prescripteur", as: "prescripteur", controller: "prescripteur_rdv_wizard" do diff --git a/db/seeds/ccas.rb b/db/seeds/ccas.rb index c4e193abdb..e4bb8eaf4b 100644 --- a/db/seeds/ccas.rb +++ b/db/seeds/ccas.rb @@ -49,3 +49,18 @@ user.skip_confirmation! user.save! + +# Un agent pour tester l'absence d'orga et de services +agent = Agent.new( + email: "bob-sans-orga@demo.rdv-solidarites.fr", + uid: "bob-sans-orga@demo.rdv-solidarites.fr", + first_name: "Bob", + last_name: "Sans Organisation", + password: "Rdvservicepublictest1!", + services: [], + invitation_accepted_at: 1.day.ago, + roles_attributes: [], + agent_territorial_access_rights_attributes: [] +) +agent.skip_confirmation! +agent.save! diff --git a/docs/architecture-technique.md b/docs/architecture-technique.md index 1f59890930..08b96f5333 100644 --- a/docs/architecture-technique.md +++ b/docs/architecture-technique.md @@ -85,6 +85,7 @@ Ces choix techniques sont aussi influencés par la culture de la communauté Rub | Navigateur redirigé par App Rails | API Microsoft | HTTPS (OAuth) | 443 | Amsterdam, Pays-Bas | login.microsoftonline.com | | App Rails | API dédoublonnage ANTS | HTTPS | 443 | Paris, France | api-coordination.rendezvouspasseport.ants.gouv.fr/api | | Moteur de recherche ANTS | App Rails | HTTPS | 443 | Paris, France | rdv.anct.gouv.fr/api/ants/availableTimeSlots | +| App Rails | Crisp | HTTPS | 443 | Amsterdam, Pays-Bas | api.crisp.chat | ##### Webhooks @@ -111,11 +112,11 @@ plusieurs tables dans la base de données de RDV Insertion. ### Inventaire des dépendances -| Nom de l’applicatif | Service | Version | Commentaires | -|---------------------|------------------|-----------|-----------------------------------------------------------------| -| Serveur web | Rails @ Scalingo | Rails 7 | Voir ci-dessous pour le détail des librairies | -| BDD métier | PostgreSQL | `14.10.0` | Stockage des données métier, voir [db/schema.rb](/db/schema.rb) | -| BDD technique | Redis | `7.2.3` | Stockage du cache | +| Nom de l’applicatif | Service | Version | Commentaires | +|---------------------|------------------|----------|-----------------------------------------------------------------| +| Serveur web | Rails @ Scalingo | Rails 7 | Voir ci-dessous pour le détail des librairies | +| BDD métier | PostgreSQL | `16.6.0` | Stockage des données métier, voir [db/schema.rb](/db/schema.rb) | +| BDD technique | Redis | `7.2.5` | Stockage du cache | La liste des librairies Ruby est disponible dans : - [Gemfile](/Gemfile) pour la liste des dépendances directes et la description de la fonctionnalité de chacune des gems diff --git a/public/robots.txt b/public/robots.txt index fd4868ccc6..ee34469857 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,4 +1,11 @@ # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file User-agent: * -Disallow: /prendre_rdv_prescripteur/ -Disallow: /prescripteur/ +Disallow: / +Allow: /$ +Allow: /mds +Allow: /accessibility +Allow: /mentions_legales +Allow: /cgu +Allow: /politique_de_confidentialite +Allow: /domaines +Allow: /aide diff --git a/spec/factories/agent.rb b/spec/factories/agent.rb index c9917a1967..6ccb6d5a8d 100644 --- a/spec/factories/agent.rb +++ b/spec/factories/agent.rb @@ -12,8 +12,17 @@ transient do service { build(:service) } + no_services { false } + + trait :no_services do + no_services { true } + end end after(:build) do |agent, evaluator| + next if evaluator.no_services + next if agent.agent_services.any? + next if agent.services.any? + if agent.agent_services.empty? && agent.services.empty? agent.services = if evaluator.service [evaluator.service] diff --git a/spec/features/agents/account/agent_can_reset_his_password_spec.rb b/spec/features/agents/account/agent_can_reset_his_password_spec.rb index 25d0b76e6d..db782ac20a 100644 --- a/spec/features/agents/account/agent_can_reset_his_password_spec.rb +++ b/spec/features/agents/account/agent_can_reset_his_password_spec.rb @@ -22,7 +22,6 @@ fill_in "Mot de passe", with: "correct H0rse battery! staple" expect { click_on "Enregistrer" }.to change { agent.reload.encrypted_password } expect(page).to have_content("Votre mot de passe a été édité avec succès") - expect(page).to have_link("Vos organisations") end it "works when using the user's password reset form" do @@ -39,6 +38,5 @@ fill_in "Mot de passe", with: "correct H0rse battery! staple" expect { click_on "Enregistrer" }.to change { agent.reload.encrypted_password } expect(page).to have_content("Votre mot de passe a été édité avec succès") - expect(page).to have_link("Vos organisations") end end diff --git a/spec/features/agents/agent_can_see_users_rdv_spec.rb b/spec/features/agents/agent_can_see_users_rdv_spec.rb index 90991ad2b0..ae0f41afe6 100644 --- a/spec/features/agents/agent_can_see_users_rdv_spec.rb +++ b/spec/features/agents/agent_can_see_users_rdv_spec.rb @@ -1,27 +1,26 @@ RSpec.describe "can see users' RDV" do - let!(:organisation) { create(:organisation) } - let!(:service) { create(:service) } - let!(:agent) { create(:agent, basic_role_in_organisations: [organisation], service: service) } - let!(:user) { create(:user, first_name: "Tanguy", last_name: "Laverdure", organisations: [organisation]) } - let!(:motif) { create(:motif, organisation: organisation, service: service) } - - before do - login_as(agent, scope: :agent) - visit authenticated_agent_root_path - end - context "with no RDV" do - before do - visit admin_organisation_user_path(organisation, user) - end + let!(:organisation) { create(:organisation) } + let!(:service) { create(:service) } + let!(:agent) { create(:agent, basic_role_in_organisations: [organisation], service: service) } + let!(:user) { create(:user, first_name: "Tanguy", last_name: "Laverdure", organisations: [organisation]) } + let!(:motif) { create(:motif, organisation: organisation, service: service) } it do + login_as(agent, scope: :agent) + visit admin_organisation_user_path(organisation, user) expect(page).to have_content("À venir\n0 RDV") expect(page).to have_content("aucun RDV") end end context "with one RDV" do + let!(:organisation) { create(:organisation) } + let!(:service) { create(:service) } + let!(:agent) { create(:agent, basic_role_in_organisations: [organisation], service: service) } + let!(:user) { create(:user, first_name: "Tanguy", last_name: "Laverdure", organisations: [organisation]) } + let!(:motif) { create(:motif, organisation: organisation, service: service) } + let!(:rdv) { create :rdv, :future, users: [user], organisation: organisation, motif: motif, agents: [agent] } before do @@ -29,11 +28,12 @@ create(:rdv, :past, status: :excused, users: [user], organisation: organisation, motif: motif, agents: [agent]) create(:rdv, :past, status: :revoked, users: [user], organisation: organisation, motif: motif, agents: [agent]) create(:rdv, :past, status: :noshow, users: [user], organisation: organisation, motif: motif, agents: [agent]) - - visit admin_organisation_user_path(organisation, user) end it do + login_as(agent, scope: :agent) + visit admin_organisation_user_path(organisation, user) + expect(page).to have_content("Excusé\n1 RDV") expect(page).to have_content("Vu\n1 RDV") expect(page).to have_content("Annulé par un agent\n1 ") @@ -45,4 +45,33 @@ expect(page).to have_content("Le #{I18n.l(rdv.starts_at, format: :human)} (durée : #{rdv.duration_in_min} minutes)") end end + + describe "displaying annotations" do + let!(:territory) { create(:territory, enable_notes_field: true) } + let!(:organisation) { create(:organisation, territory:) } + let!(:responsible) { create(:user, organisations: [organisation]) } + let!(:relative) { create(:user, organisations: [organisation], responsible:) } + + let!(:agent) { create(:agent, basic_role_in_organisations: [organisation]) } + + it "works" do + login_as(agent, scope: :agent) + visit admin_organisation_user_path(organisation, relative) + # On visite pour vérifier que la page ne crash pas en l'absence d'annotation, voir #5133. + expect(page).to have_content("Informations de votre proche") + + responsible.annotate!("Ce responsable est très responsable", territory:) + relative.annotate!("Ce proche est très proche", territory:) + + # Sur la page du proche, on affiche les remarques du responsable ET du proche + visit admin_organisation_user_path(organisation, relative) + expect(page).to have_content("Ce responsable est très responsable") + expect(page).to have_content("Ce proche est très proche") + + # Sur la page du responsable, on affiche uniquement la remarque du responsable + visit admin_organisation_user_path(organisation, responsible) + expect(page).to have_content("Ce responsable est très responsable") + expect(page).not_to have_content("Ce proche est très proche") + end + end end diff --git a/spec/services/admin_creates_agent_spec.rb b/spec/services/admin_creates_agent_spec.rb new file mode 100644 index 0000000000..4437927ff6 --- /dev/null +++ b/spec/services/admin_creates_agent_spec.rb @@ -0,0 +1,27 @@ +RSpec.describe AdminCreatesAgent do + context "when inviting an agent that doesn't have any services" do + let(:agent) do + create(:agent, :no_services, organisations: []) + end + let(:service1) { create(:service) } + let(:service2) { create(:service) } + let(:organisation) do + create(:organisation) + end + let(:admin) do + create(:agent, admin_role_in_organisations: [organisation]) + end + + it "adds the services to the agent" do + described_class.new( + agent_params: { email: agent.email, service_ids: [service1.id, service2.id] }, + current_agent: admin, + organisations: [organisation], + access_level: :basic + ).call + + expect(agent.reload.services).to contain_exactly(service1, service2) + expect(agent.organisations).to eq [organisation] + end + end +end diff --git a/spec/services/merge_users_service_spec.rb b/spec/services/merge_users_service_spec.rb index 444fed6f72..3d3f4947ae 100644 --- a/spec/services/merge_users_service_spec.rb +++ b/spec/services/merge_users_service_spec.rb @@ -4,8 +4,8 @@ # defaults let!(:organisation) { create(:organisation) } let(:attributes_to_merge) { [] } - let(:user_target) { create(:user, organisations: [organisation]) } - let(:user_to_merge) { create(:user, organisations: [organisation]) } + let!(:user_target) { create(:user, organisations: [organisation]) } + let!(:user_to_merge) { create(:user, organisations: [organisation]) } context "simply merge first_name" do let(:user_target) { create(:user, first_name: "Jean", last_name: "PAUL", email: "jean@paul.fr", organisations: [organisation]) } @@ -179,8 +179,8 @@ let!(:organisation2) { create(:organisation) } let!(:agent1) { create(:agent, basic_role_in_organisations: [organisation]) } let!(:agent2) { create(:agent, basic_role_in_organisations: [organisation2]) } - let(:user_target) { create(:user, referent_agents: [agent1], organisations: [organisation]) } - let(:user_to_merge) { create(:user, referent_agents: [agent2], organisations: [organisation, organisation2]) } + let!(:user_target) { create(:user, referent_agents: [agent1], organisations: [organisation]) } + let!(:user_to_merge) { create(:user, referent_agents: [agent2], organisations: [organisation, organisation2]) } it "does not move the agent from the other orga anything" do perform @@ -249,6 +249,46 @@ end end + context "only user to merge has an annotation" do + before do + user_to_merge.annotations.create!(territory: organisation.territory, content: "user to merge") + end + + it "deletes the annotation along with the merged user by default" do + expect { perform }.to change(Annotation, :count).by(-1).and(change(User, :count).by(-1)) + expect(user_target.annotations.find_by(territory: organisation.territory)).to be_nil + end + + context "when merging annotations" do + let(:attributes_to_merge) { [:annotation_content] } + + it "moves the annotation to the target user" do + perform + expect(user_target.annotation_for(organisation.territory)).to eq("user to merge") + end + end + end + + context "only target user has an annotation" do + before do + user_target.annotations.create!(territory: organisation.territory, content: "target user") + end + + it "keeps the annotation by default" do + expect { perform }.not_to change(Annotation, :count) + expect(user_target.annotation_for(organisation.territory)).to eq("target user") + end + + context "when merging annotations" do + let(:attributes_to_merge) { [:annotation_content] } + + it "deletes the annotation of the target user, to replace it with with the absence of annotation of the user to merge" do + expect { perform }.to change(Annotation, :count).by(-1) + expect(user_target.annotations.find_by(territory: organisation.territory)).to be_nil + end + end + end + context "when one user is connected by FranceConnect" do it "keep FranceConnect attributes when merged user logged once with franceconnect" do user_to_merge = create(:user, logged_once_with_franceconnect: true, franceconnect_openid_sub: "unechainedecharacteres", organisations: [organisation])