From ff6e06ae86039b4db159731ef3cca957f8015b01 Mon Sep 17 00:00:00 2001 From: pabois Date: Mon, 6 Jul 2026 18:51:04 +0200 Subject: [PATCH 01/18] wip invitations --- .../alumni/persons/invitations_controller.rb | 56 +++++++++++++++++ .../extranet/alumni/persons_controller.rb | 2 + app/models/communication/extranet.rb | 1 + .../communication/extranet/invitation.rb | 62 +++++++++++++++++++ app/models/university/person/with_alumnus.rb | 2 + .../extranets/alumni/_list.html.erb | 2 +- .../extranet/alumni/persons/_person.html.erb | 13 ++++ .../alumni/persons/invitations/new.html.erb | 22 +++++++ .../extranet/alumni/persons/show.html.erb | 14 +++++ config/locales/communication/en.yml | 10 +++ config/locales/communication/fr.yml | 10 +++ config/locales/extranet/en.yml | 8 +++ config/locales/extranet/fr.yml | 8 +++ config/routes/extranet.rb | 2 + ...eate_communication_extranet_invitations.rb | 16 +++++ db/schema.rb | 24 ++++++- 16 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 app/controllers/extranet/alumni/persons/invitations_controller.rb create mode 100644 app/models/communication/extranet/invitation.rb create mode 100644 app/views/extranet/alumni/persons/invitations/new.html.erb create mode 100644 db/migrate/20260706155316_create_communication_extranet_invitations.rb diff --git a/app/controllers/extranet/alumni/persons/invitations_controller.rb b/app/controllers/extranet/alumni/persons/invitations_controller.rb new file mode 100644 index 0000000000..d70d9488db --- /dev/null +++ b/app/controllers/extranet/alumni/persons/invitations_controller.rb @@ -0,0 +1,56 @@ +class Extranet::Alumni::Persons::InvitationsController < Extranet::Alumni::ApplicationController + + before_action :find_person, :ensure_person_is_invitable + + def new + @invitation = current_extranet.invitations.build( + from_name: current_user.to_s, + from_email: current_user.email, + to_name: @l10n.to_s, + to_email: @person.email, + message: "TODO: Add a default message for the invitation" + ) + breadcrumb + end + + def create + @invitation = current_extranet.invitations.build(invitation_params) + if @invitation.save + redirect_to [:alumni, @person], notice: t('extranet.alumni.invitations.created') + else + breadcrumb + render :new, status: :unprocessable_content + end + end + + protected + + def invitation_params + params.require(:communication_extranet_invitation) + .permit(:from_name, :from_email, :to_name, :to_email, :message) + .merge( + user_id: current_user.id, + person_id: @person.id, + university_id: current_university.id + ) + end + + def breadcrumb + super + add_breadcrumb University::Person.model_name.human(count: 2), alumni_university_persons_path + add_breadcrumb @l10n, alumni_university_person_path(@person) + add_breadcrumb t('extranet.alumni.invitations.title') + end + + def find_person + @person = current_extranet.about.university_person_alumni.find(params[:id]) + @l10n = @person.best_localization_for(current_language) + end + + def ensure_person_is_invitable + unless Communication::Extranet::Invitation.sendable_to?(@person) + redirect_to [:alumni, @person], alert: t('extranet.alumni.invitations.send.too_soon') + end + end + +end diff --git a/app/controllers/extranet/alumni/persons_controller.rb b/app/controllers/extranet/alumni/persons_controller.rb index 0b51734907..6f36c09fe8 100644 --- a/app/controllers/extranet/alumni/persons_controller.rb +++ b/app/controllers/extranet/alumni/persons_controller.rb @@ -1,4 +1,5 @@ class Extranet::Alumni::PersonsController < Extranet::Alumni::ApplicationController + def index @facets = University::Person::Alumnus::Facets.new params[:facets], { model: current_extranet.about.university_person_alumni, @@ -36,4 +37,5 @@ def breadcrumb super add_breadcrumb University::Person.model_name.human(count: 2), alumni_university_persons_path end + end diff --git a/app/models/communication/extranet.rb b/app/models/communication/extranet.rb index 60f74a50ab..03fad2799c 100644 --- a/app/models/communication/extranet.rb +++ b/app/models/communication/extranet.rb @@ -67,6 +67,7 @@ class Communication::Extranet < ApplicationRecord has_many :documents has_many :document_categories, class_name: 'Communication::Extranet::Document::Category' has_many :document_kinds, class_name: 'Communication::Extranet::Document::Kind' + has_many :invitations, class_name: 'Communication::Extranet::Invitation', dependent: :destroy validates :host, presence: true diff --git a/app/models/communication/extranet/invitation.rb b/app/models/communication/extranet/invitation.rb new file mode 100644 index 0000000000..9b6eb40044 --- /dev/null +++ b/app/models/communication/extranet/invitation.rb @@ -0,0 +1,62 @@ +# == Schema Information +# +# Table name: communication_extranet_invitations +# +# id :uuid not null, primary key +# from_email :string +# from_name :string +# message :text +# to_email :string +# to_name :string +# created_at :datetime not null +# updated_at :datetime not null +# extranet_id :uuid indexed +# person_id :uuid indexed +# university_id :uuid indexed +# user_id :uuid indexed +# +# Indexes +# +# index_communication_extranet_invitations_on_extranet_id (extranet_id) +# index_communication_extranet_invitations_on_person_id (person_id) +# index_communication_extranet_invitations_on_university_id (university_id) +# index_communication_extranet_invitations_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_2a0ba0dd0d (university_id => universities.id) +# fk_rails_85c1d16bbf (user_id => users.id) +# fk_rails_989e9a94ca (person_id => university_people.id) +# fk_rails_e064970cc5 (extranet_id => communication_extranets.id) +# +class Communication::Extranet::Invitation < ApplicationRecord + include Sanitizable + include WithUniversity + + belongs_to :extranet, class_name: 'Communication::Extranet' + belongs_to :user + belongs_to :person, class_name: 'University::Person' + validates :from_name, :from_email, :to_name, :to_email, :message, presence: true + validate :can_send_to_person, on: :create + validates :to_email, :from_email, format: { with: Devise.email_regexp } + + after_create_commit :send_invitation_email + + def self.sendable_to?(person) + self.where(person: person).where('created_at >= ?', University::Person::WithAlumnus::DELAY_FOR_INVITATION.ago).none? + end + + private + + def can_send_to_person + unless self.class.sendable_to?(person) + errors.add(:to_email, :too_soon) + end + end + + def send_invitation_email + # TODO + # Communication::Extranet::InvitationMailer.with(invitation: self).invitation_email.deliver_later + end + +end diff --git a/app/models/university/person/with_alumnus.rb b/app/models/university/person/with_alumnus.rb index 58a4e0fcd8..fb1e4449d5 100644 --- a/app/models/university/person/with_alumnus.rb +++ b/app/models/university/person/with_alumnus.rb @@ -1,6 +1,8 @@ module University::Person::WithAlumnus extend ActiveSupport::Concern + DELAY_FOR_INVITATION = 5.minutes.freeze + included do has_and_belongs_to_many :cohorts, class_name: '::Administration::Cohort', diff --git a/app/views/admin/communication/extranets/alumni/_list.html.erb b/app/views/admin/communication/extranets/alumni/_list.html.erb index 06563f5752..ae211844b2 100644 --- a/app/views/admin/communication/extranets/alumni/_list.html.erb +++ b/app/views/admin/communication/extranets/alumni/_list.html.erb @@ -15,7 +15,7 @@

<%= t('admin.communication.extranet.alumni.account_active') %>

- <% elsif person.invitation_sent_at.nil? || person.invitation_sent_at < 5.minutes.ago %> + <% elsif person.invitation_sent_at.nil? || person.invitation_sent_at < University::Person::WithAlumnus::DELAY_FOR_INVITATION.ago %> <%= link_to t('admin.communication.extranet.alumni.send_invitation.cta'), send_invitation_admin_communication_extranet_alumnus_path(extranet_id: @extranet.id, id: person.id), method: :post, diff --git a/app/views/extranet/alumni/persons/_person.html.erb b/app/views/extranet/alumni/persons/_person.html.erb index 1fc23889a0..dbadf00351 100644 --- a/app/views/extranet/alumni/persons/_person.html.erb +++ b/app/views/extranet/alumni/persons/_person.html.erb @@ -16,5 +16,18 @@ l10n = person.best_localization_for(person)
<%= link_to l10n, [:alumni, person], class: 'stretched-link' %> + <% if person.user_id.blank? && person.email.blank? %> +

+ <%= t('extranet.alumni.invitations.send.label') %> +
+ <% if Communication::Extranet::Invitation.sendable_to?(person) %> + <%= link_to t('extranet.alumni.invitations.send.cta', name: l10n), + alumni_new_invitation_path(person), + class: 'btn btn-light btn-sm position-relative z-2' %> + <% else %> + <%= t('extranet.alumni.invitations.send.too_soon') %> + <% end %> +

+ <% end %>
diff --git a/app/views/extranet/alumni/persons/invitations/new.html.erb b/app/views/extranet/alumni/persons/invitations/new.html.erb new file mode 100644 index 0000000000..1b4f0c5ad5 --- /dev/null +++ b/app/views/extranet/alumni/persons/invitations/new.html.erb @@ -0,0 +1,22 @@ +<% content_for :title, t('extranet.alumni.invitations.title') %> + +<%= simple_form_for @invitation, url: alumni_new_invitation_path(@person) do |f| %> + <%= f.error_notification %> + <%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %> + +
+
+ <%= f.input :from_name %> + <%= f.input :from_email %> + <%= f.input :to_name %> + <%= f.input :to_email %> +
+
+ <%= f.input :message %> +
+
+ + <%= submit f %> + +<% end %> + diff --git a/app/views/extranet/alumni/persons/show.html.erb b/app/views/extranet/alumni/persons/show.html.erb index 7a86b52dbe..95e1c59d62 100644 --- a/app/views/extranet/alumni/persons/show.html.erb +++ b/app/views/extranet/alumni/persons/show.html.erb @@ -1,5 +1,19 @@ <% content_for :title, @l10n %> +<% if @person.user_id.blank? && @person.email.blank? %> +

+ <%= t('extranet.alumni.invitations.send.label') %> +
+ <% if Communication::Extranet::Invitation.sendable_to?(@person) %> + <%= link_to t('extranet.alumni.invitations.send.cta', name: @l10n), + alumni_new_invitation_path(@person), + class: 'btn btn-light btn-sm position-relative z-2' %> + <% else %> + <%= t('extranet.alumni.invitations.send.too_soon') %> + <% end %> +

+ <% end %> +
diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml index d92992ea5d..3105c5445f 100644 --- a/config/locales/communication/en.yml +++ b/config/locales/communication/en.yml @@ -56,6 +56,12 @@ en: name: Name published: Published? published_at: Publication date + communication/extranet/invitation: + from_email: Your email + from_name: From + message: Message + to_email: Alumnus' email + to_name: To communication/extranet/localization: cookies_policy: Cookies policy favicon: Browser icon (favicon) @@ -330,6 +336,10 @@ en: unavailable: is not available for this kind of extranet sso_mapping: missing_email: doesn't handle the email + communication/extranet/invitation: + attributes: + to_email: + too_soon: has already received an invitation recently. You can try again later. communication/extranet/localization: attributes: published: diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml index 69810083ee..1bf1a59253 100644 --- a/config/locales/communication/fr.yml +++ b/config/locales/communication/fr.yml @@ -56,6 +56,12 @@ fr: name: Nom published: Publié ? published_at: Date de publication + communication/extranet/invitation: + from_email: Votre email + from_name: De la part de + message: Message + to_email: Email de la personne invitée + to_name: Pour communication/extranet/localization: cookies_policy: Politique de cookies favicon: Icône de navigateur (favicon) @@ -330,6 +336,10 @@ fr: unavailable: n'est pas disponible pour ce type d'extranet sso_mapping: missing_email: ne gère pas l'adresse email + communication/extranet/invitation: + attributes: + to_email: + too_soon: a déjà reçu une invitation récemment. Vous pouvez réessayer plus tard. communication/extranet/localization: attributes: published: diff --git a/config/locales/extranet/en.yml b/config/locales/extranet/en.yml index ac38c94a72..0ca99960f6 100644 --- a/config/locales/extranet/en.yml +++ b/config/locales/extranet/en.yml @@ -8,6 +8,14 @@ en: logout: Log out my: My account updated: Updated + alumni: + invitations: + created: Invitation sent successfully + send: + cta: Invite %{name} to join the alumni + label: Do you know his/her email? You can invite him/her. + too_soon: An invitation has already been sent to this person recently. You can try again later. + title: Invite contacts: organizations: people: Members of this organization diff --git a/config/locales/extranet/fr.yml b/config/locales/extranet/fr.yml index 2ac47e60dd..d39cf876e3 100644 --- a/config/locales/extranet/fr.yml +++ b/config/locales/extranet/fr.yml @@ -8,6 +8,14 @@ fr: logout: Déconnexion my: Mon compte updated: Mise à jour effectuée + alumni: + invitations: + created: Invitation envoyée avec succès + send: + cta: Inviter %{name} à rejoindre les alumni + label: Vous connaissez son email ? Vous pouvez l’inviter. + too_soon: Une invitation a déjà été envoyée à cette personne récemment. Vous pouvez réessayer plus tard. + title: Inviter contacts: organizations: people: Membres de cette organisation diff --git a/config/routes/extranet.rb b/config/routes/extranet.rb index fdc2e568d3..0b00f56986 100644 --- a/config/routes/extranet.rb +++ b/config/routes/extranet.rb @@ -18,6 +18,8 @@ get 'organization/:id' => 'organizations#show', as: :university_organization get 'persons' => 'persons#index', as: :university_persons get 'persons/:id' => 'persons#show', as: :university_person + get 'persons/:id/invite' => 'persons/invitations#new', as: :new_invitation + post 'persons/:id/invite' => 'persons/invitations#create' get 'years' => 'academic_years#index', as: :administration_academic_years get 'years/:id' => 'academic_years#show', as: :administration_academic_year root to: 'persons#index' diff --git a/db/migrate/20260706155316_create_communication_extranet_invitations.rb b/db/migrate/20260706155316_create_communication_extranet_invitations.rb new file mode 100644 index 0000000000..4d606e4269 --- /dev/null +++ b/db/migrate/20260706155316_create_communication_extranet_invitations.rb @@ -0,0 +1,16 @@ +class CreateCommunicationExtranetInvitations < ActiveRecord::Migration[8.1] + def change + create_table :communication_extranet_invitations, id: :uuid do |t| + t.references :extranet, foreign_key: { to_table: :communication_extranets }, type: :uuid + t.references :user, foreign_key: true, type: :uuid + t.references :person, foreign_key: { to_table: :university_people }, type: :uuid + t.references :university, foreign_key: true, type: :uuid + t.string :from_name + t.string :from_email + t.string :to_name + t.string :to_email + t.text :message + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d5c3a601fb..6be281fe1c 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[8.1].define(version: 2026_07_02_133651) do +ActiveRecord::Schema[8.1].define(version: 2026_07_06_155316) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pg_stat_statements" @@ -328,6 +328,24 @@ t.index ["university_id"], name: "index_communication_extranet_documents_on_university_id" end + create_table "communication_extranet_invitations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.uuid "extranet_id" + t.string "from_email" + t.string "from_name" + t.text "message" + t.uuid "person_id" + t.string "to_email" + t.string "to_name" + t.uuid "university_id" + t.datetime "updated_at", null: false + t.uuid "user_id" + t.index ["extranet_id"], name: "index_communication_extranet_invitations_on_extranet_id" + t.index ["person_id"], name: "index_communication_extranet_invitations_on_person_id" + t.index ["university_id"], name: "index_communication_extranet_invitations_on_university_id" + t.index ["user_id"], name: "index_communication_extranet_invitations_on_user_id" + end + create_table "communication_extranet_localizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id" t.text "cookies_policy" @@ -2609,6 +2627,10 @@ add_foreign_key "communication_extranet_documents", "communication_extranet_document_kinds", column: "kind_id" add_foreign_key "communication_extranet_documents", "communication_extranets", column: "extranet_id" add_foreign_key "communication_extranet_documents", "universities" + add_foreign_key "communication_extranet_invitations", "communication_extranets", column: "extranet_id" + add_foreign_key "communication_extranet_invitations", "universities" + add_foreign_key "communication_extranet_invitations", "university_people", column: "person_id" + add_foreign_key "communication_extranet_invitations", "users" add_foreign_key "communication_extranet_localizations", "communication_extranets", column: "about_id" add_foreign_key "communication_extranet_localizations", "languages" add_foreign_key "communication_extranet_localizations", "universities" From daddab00312c68c85c98b823678c6ff424f42c2d Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 16 Jul 2026 16:23:18 +0200 Subject: [PATCH 02/18] custom message for extranet invitations --- .../communication/extranets_controller.rb | 7 ++- app/mailers/extranet_mailer.rb | 2 +- .../communication/extranet/localization.rb | 48 +++++++++++-------- .../communication/extranets/_form.html.erb | 34 +++++++++++-- .../extranet/invitation_message.html.erb | 6 +-- config/locales/communication/en.yml | 14 ++++-- config/locales/communication/fr.yml | 14 ++++-- config/locales/en.yml | 15 ++++-- config/locales/fr.yml | 15 ++++-- ...l_invitation_message_infos_to_extranets.rb | 9 ++++ db/schema.rb | 9 ++-- 11 files changed, 124 insertions(+), 49 deletions(-) create mode 100644 db/migrate/20260716132522_add_manual_invitation_message_infos_to_extranets.rb diff --git a/app/controllers/admin/communication/extranets_controller.rb b/app/controllers/admin/communication/extranets_controller.rb index 97067e6829..0b40028ade 100644 --- a/app/controllers/admin/communication/extranets_controller.rb +++ b/app/controllers/admin/communication/extranets_controller.rb @@ -101,8 +101,11 @@ def base_localization_params :favicon_delete, :home_sentence, :id, - :invitation_message_subject, - :invitation_message_text, + :invitation_message_automatic_subject, + :invitation_message_automatic_text, + :invitation_message_manual_subject, + :invitation_message_manual_text, + :invitation_message_manual_signature, :language_id, :logo, :logo_delete, diff --git a/app/mailers/extranet_mailer.rb b/app/mailers/extranet_mailer.rb index 6fbab626cc..a99a8dd5c7 100644 --- a/app/mailers/extranet_mailer.rb +++ b/app/mailers/extranet_mailer.rb @@ -17,7 +17,7 @@ def invitation_message(extranet, person) I18n.with_locale(@language.iso_code.to_sym) do mail from: @university.mail_from[:full], to: @email, - subject: @l10n.invitation_message_subject if should_send?(@email) + subject: @l10n.invitation_message_automatic_subject if should_send?(@email) end end diff --git a/app/models/communication/extranet/localization.rb b/app/models/communication/extranet/localization.rb index cd067fa5e3..4ab3a0161c 100644 --- a/app/models/communication/extranet/localization.rb +++ b/app/models/communication/extranet/localization.rb @@ -2,23 +2,26 @@ # # Table name: communication_extranet_localizations # -# id :uuid not null, primary key -# cookies_policy :text -# home_sentence :text -# invitation_message_subject :string default("") -# invitation_message_text :text default("") -# name :string -# privacy_policy :text -# published :boolean default(FALSE) -# published_at :datetime -# registration_contact :string -# sso_button_label :string -# terms :text -# created_at :datetime not null -# updated_at :datetime not null -# about_id :uuid uniquely indexed => [language_id], indexed -# language_id :uuid uniquely indexed => [about_id], indexed -# university_id :uuid indexed +# id :uuid not null, primary key +# cookies_policy :text +# home_sentence :text +# invitation_message_automatic_subject :string default("") +# invitation_message_automatic_text :text default("") +# invitation_message_manual_signature :text +# invitation_message_manual_subject :string +# invitation_message_manual_text :text +# name :string +# privacy_policy :text +# published :boolean default(FALSE) +# published_at :datetime +# registration_contact :string +# sso_button_label :string +# terms :text +# created_at :datetime not null +# updated_at :datetime not null +# about_id :uuid uniquely indexed => [language_id], indexed +# language_id :uuid uniquely indexed => [about_id], indexed +# university_id :uuid indexed # # Indexes # @@ -49,7 +52,7 @@ class Communication::Extranet::Localization < ApplicationRecord attachable.variant :thumb, resize_to_limit: [228, 228] end - before_validation :set_default_invitation_message + before_validation :set_default_invitation_messages validates :name, presence: true validates :logo, size: { less_than: 1.megabytes } @@ -68,9 +71,12 @@ def prevent_unpublishing_default_language errors.add(:published, :cannot_unpublished_default) end - def set_default_invitation_message - self.invitation_message_subject = I18n.t('mailers.extranet.invitation_message.subject') if self.invitation_message_subject.blank? - self.invitation_message_text = I18n.t('mailers.extranet.invitation_message.text') if self.invitation_message_text.blank? + def set_default_invitation_messages + self.invitation_message_automatic_subject = I18n.t('mailers.extranet.invitation_messages.automatic.subject') if self.invitation_message_automatic_subject.blank? + self.invitation_message_automatic_text = I18n.t('mailers.extranet.invitation_messages.automatic.text') if self.invitation_message_automatic_text.blank? + self.invitation_message_manual_subject = I18n.t('mailers.extranet.invitation_messages.manual.subject') if self.invitation_message_manual_subject.blank? + self.invitation_message_manual_text = I18n.t('mailers.extranet.invitation_messages.manual.text') if self.invitation_message_manual_text.blank? + self.invitation_message_manual_signature = I18n.t('mailers.extranet.invitation_messages.manual.signature') if self.invitation_message_manual_signature.blank? end end diff --git a/app/views/admin/communication/extranets/_form.html.erb b/app/views/admin/communication/extranets/_form.html.erb index a5fc6dac30..104ca69f07 100644 --- a/app/views/admin/communication/extranets/_form.html.erb +++ b/app/views/admin/communication/extranets/_form.html.erb @@ -72,9 +72,37 @@ <%= f.input :feature_jobs %> <% end %> <% end %> - <%= osuny_panel Communication::Extranet::Localization.human_attribute_name(:invitation_message) do %> - <%= lf.input :invitation_message_subject %> - <%= lf.input :invitation_message_text, input_html: { rows: 5 } %> + <%= osuny_panel t('admin.communication.extranet.invitation_messages.title') do %> +

<%= t('admin.communication.extranet.invitation_messages.automatic') %>

+

<%= t('admin.communication.extranet.invitation_messages.automatic_hint') %>

+ <%= lf.input :invitation_message_automatic_subject, + input_html: { + value: l10n.invitation_message_automatic_subject.presence || t('mailers.extranet.invitation_messages.automatic.subject') + } %> + <%= lf.input :invitation_message_automatic_text, + as: :text, + input_html: { + rows: 5, + value: l10n.invitation_message_automatic_text.presence || t('mailers.extranet.invitation_messages.automatic.text') + } %> +

<%= t('admin.communication.extranet.invitation_messages.manual') %>

+

<%= t('admin.communication.extranet.invitation_messages.manual_hint_html') %>

+ <%= lf.input :invitation_message_manual_subject, + input_html: { + value: l10n.invitation_message_manual_subject.presence || t('mailers.extranet.invitation_messages.manual.subject') + } %> + <%= lf.input :invitation_message_manual_text, + as: :text, + input_html: { + rows: 5, + value: l10n.invitation_message_manual_text.presence || t('mailers.extranet.invitation_messages.manual.text') + } %> + <%= lf.input :invitation_message_manual_signature, + as: :text, + input_html: { + rows: 2, + value: l10n.invitation_message_manual_signature.presence || t('mailers.extranet.invitation_messages.manual.signature') + } %> <% end %> <%= osuny_panel t('legal') do %> <%= lf.input :terms, as: :summernote %> diff --git a/app/views/mailers/extranet/invitation_message.html.erb b/app/views/mailers/extranet/invitation_message.html.erb index 2aa5b94f5d..49517198a3 100644 --- a/app/views/mailers/extranet/invitation_message.html.erb +++ b/app/views/mailers/extranet/invitation_message.html.erb @@ -1,14 +1,14 @@ -<%= simple_format @l10n.invitation_message_text %> +<%= simple_format @l10n.invitation_message_automatic_text %> <% if @user.present? %>

- <%= t('mailers.extranet.invitation_message.with_user_html', + <%= t('mailers.extranet.invitation_messages.automatic.with_user_html', email: @email, sign_in_url: new_user_session_url(email: @email, host: @extranet.host)) %>

<% else %>

- <%= t('mailers.extranet.invitation_message.without_user_html', + <%= t('mailers.extranet.invitation_messages.automatic.without_user_html', email: @email, sign_up_url: new_user_registration_url(email: @email, host: @extranet.host)) %>

diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml index 3105c5445f..527f8bd5cc 100644 --- a/config/locales/communication/en.yml +++ b/config/locales/communication/en.yml @@ -66,9 +66,11 @@ en: cookies_policy: Cookies policy favicon: Browser icon (favicon) home_sentence: Sentence displayed on homepage - invitation_message: Invitation message (automatically sended) - invitation_message_subject: Subject - invitation_message_text: Texte + invitation_message_automatic_subject: Subject + invitation_message_automatic_text: Text + invitation_message_manual_signature: Signature + invitation_message_manual_subject: Subject + invitation_message_manual_text: Text logo: Logo name: Name privacy_policy: Privacy policy @@ -509,6 +511,12 @@ en: confirm_localization: text_html: "The creation of %{about} in %{language} requires the localization of the extranet %{extranet} in %{language}." title: Localization confirmation + invitation_messages: + automatic: Automatic message + automatic_hint: This message will be sent after an alumni import or when clicking on the "send invitation" button on admin, if the alumnus has an email. + manual: Manual message + manual_hint_html: "This message will be sent from the extranet, when a user clicks on the \"invite alumnus\" button, if the user has no email.
In these fields you can use the following variables:
  • {{name}}: the name of the alumnus
  • {{email}}: the email of the alumnus
  • {{sender_name}}: the name of the user sending the invitation
  • {{sender_promotion}}: the promotion of the user sending the invitation
" + title: Invitation messages languages: Languages website: agenda: diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml index 1bf1a59253..ab7444d981 100644 --- a/config/locales/communication/fr.yml +++ b/config/locales/communication/fr.yml @@ -66,9 +66,11 @@ fr: cookies_policy: Politique de cookies favicon: Icône de navigateur (favicon) home_sentence: Phrase affichée sur la home - invitation_message: Message d'invitation automatique - invitation_message_subject: Sujet du mail - invitation_message_text: Texte du mail + invitation_message_automatic_subject: Sujet du mail + invitation_message_automatic_text: Texte du mail + invitation_message_manual_signature: Signature + invitation_message_manual_subject: Sujet du mail + invitation_message_manual_text: Texte du mail logo: Logo name: Nom privacy_policy: Politique de confidentialité @@ -509,6 +511,12 @@ fr: confirm_localization: text_html: "La création de %{about} en %{language} nécessite la traduction de l'extranet %{extranet} en %{language}." title: Confirmation de traduction + invitation_messages: + automatic: Message automatique + automatic_hint: Ce message sera envoyé après un import d'alumni ou lors du clic sur le bouton "envoyer l'invitation" dans l'admin, si l'alumnus a un email. + manual: Message manuel + manual_hint_html: "Ce message sera envoyé depuis l'extranet, lorsqu'un utilisateur clique sur le bouton \"inviter un alumnus\", si l'utilisateur n'a pas d'email.
Dans ces champs vous pouvez utiliser les variables suivantes :
  • {{name}} : le nom de l'alumnus
  • {{email}} : l'email indiqué pour l'alumnus
  • {{sender_name}} : le nom de l'utilisateur qui envoie l'invitation
  • {{sender_promotion}} : la promotion de l'utilisateur qui envoie l'invitation
" + title: Messages d'invitation languages: Langues website: agenda: diff --git a/config/locales/en.yml b/config/locales/en.yml index 9b89adae69..29f76a9b06 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -497,11 +497,16 @@ en: look_feel: Look & feel mailers: extranet: - invitation_message: - subject: Welcome to your extranet - text: You now have access to the extranet. - with_user_html: You can sign in with your email address %{email} by clicking here. - without_user_html: You can sign up with your email address %{email} by clicking here. + invitation_messages: + automatic: + subject: Welcome to your extranet + text: You now have access to the extranet. + with_user_html: You can sign in with your email address %{email} by clicking here. + without_user_html: You can sign up with your email address %{email} by clicking here. + manual: + signature: Yours + subject: Invitation from {{sender_name}} (promotion {{sender_promotion}}) to join the extranet + text: "Hello {{name}},\n\n{{sender_name}} invites you to join the Alumni extranet.\n\nYou will find, in particular, the directory of all promotions.\nSee you soon!\n\n{{sender_name}} (promotion {{sender_promotion}})" notifications: gdpr_deletion_incoming: subject: "osuny - your account will soon be deleted" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5d6d02f4aa..4104b1ba29 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -498,11 +498,16 @@ fr: look_feel: Look & feel mailers: extranet: - invitation_message: - subject: Bienvenue sur votre extranet - text: Vous avez désormais accès à l'extranet. - with_user_html: Vous pouvez vous y connecter avec votre adresse email %{email} en cliquant ici. - without_user_html: Vous pouvez vous y inscrire avec votre adresse email %{email} en cliquant ici. + invitation_messages: + automatic: + subject: Bienvenue sur votre extranet + text: Vous avez désormais accès à l'extranet. + with_user_html: Vous pouvez vous y connecter avec votre adresse email %{email} en cliquant ici. + without_user_html: Vous pouvez vous y inscrire avec votre adresse email %{email} en cliquant ici. + manual: + signature: Cordialement + subject: Invitation de {{sender_name}} (promotion {{sender_promotion}}) à rejoindre l'extranet + text: "Bonjour {{name}},\n\n{{sender_name}} vous invite à rejoindre l'extranet des Alumni.\n\nVous y retrouverez notamment l'annuaire de toutes les promotions.\nÀ très bientôt !\n\n{{sender_name}} (promotion {{sender_promotion}})" notifications: gdpr_deletion_incoming: subject: "osuny - votre compte va bientôt être supprimé" diff --git a/db/migrate/20260716132522_add_manual_invitation_message_infos_to_extranets.rb b/db/migrate/20260716132522_add_manual_invitation_message_infos_to_extranets.rb new file mode 100644 index 0000000000..eb1e3c1497 --- /dev/null +++ b/db/migrate/20260716132522_add_manual_invitation_message_infos_to_extranets.rb @@ -0,0 +1,9 @@ +class AddManualInvitationMessageInfosToExtranets < ActiveRecord::Migration[8.1] + def change + rename_column :communication_extranet_localizations, :invitation_message_subject, :invitation_message_automatic_subject + rename_column :communication_extranet_localizations, :invitation_message_text, :invitation_message_automatic_text + add_column :communication_extranet_localizations, :invitation_message_manual_subject, :string + add_column :communication_extranet_localizations, :invitation_message_manual_text, :text + add_column :communication_extranet_localizations, :invitation_message_manual_signature, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 6be281fe1c..cb5f149814 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[8.1].define(version: 2026_07_06_155316) do +ActiveRecord::Schema[8.1].define(version: 2026_07_16_132522) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pg_stat_statements" @@ -351,8 +351,11 @@ t.text "cookies_policy" t.datetime "created_at", null: false t.text "home_sentence" - t.string "invitation_message_subject", default: "" - t.text "invitation_message_text", default: "" + t.string "invitation_message_automatic_subject", default: "" + t.text "invitation_message_automatic_text", default: "" + t.text "invitation_message_manual_signature" + t.string "invitation_message_manual_subject" + t.text "invitation_message_manual_text" t.uuid "language_id" t.string "name" t.text "privacy_policy" From a31f5de892bb76559589280bcb0f2106f5a7d45a Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 10:43:46 +0200 Subject: [PATCH 03/18] factorize invitation cta --- .../communication/extranet/invitation.rb | 2 +- .../extranet/alumni/persons/_person.html.erb | 16 +-- .../extranet/alumni/persons/show.html.erb | 14 +-- .../extranet/home/features/_alumni.html.erb | 6 +- db/schema.rb | 108 +++++++++--------- 5 files changed, 62 insertions(+), 84 deletions(-) diff --git a/app/models/communication/extranet/invitation.rb b/app/models/communication/extranet/invitation.rb index 9b6eb40044..f231acf24a 100644 --- a/app/models/communication/extranet/invitation.rb +++ b/app/models/communication/extranet/invitation.rb @@ -30,8 +30,8 @@ # fk_rails_e064970cc5 (extranet_id => communication_extranets.id) # class Communication::Extranet::Invitation < ApplicationRecord + include HasUniversity include Sanitizable - include WithUniversity belongs_to :extranet, class_name: 'Communication::Extranet' belongs_to :user diff --git a/app/views/extranet/alumni/persons/_person.html.erb b/app/views/extranet/alumni/persons/_person.html.erb index dbadf00351..e6cf8632de 100644 --- a/app/views/extranet/alumni/persons/_person.html.erb +++ b/app/views/extranet/alumni/persons/_person.html.erb @@ -1,5 +1,5 @@ <% -l10n = person.best_localization_for(person) +l10n = person.best_localization_for(current_language) %>
@@ -16,18 +16,6 @@ l10n = person.best_localization_for(person)
<%= link_to l10n, [:alumni, person], class: 'stretched-link' %> - <% if person.user_id.blank? && person.email.blank? %> -

- <%= t('extranet.alumni.invitations.send.label') %> -
- <% if Communication::Extranet::Invitation.sendable_to?(person) %> - <%= link_to t('extranet.alumni.invitations.send.cta', name: l10n), - alumni_new_invitation_path(person), - class: 'btn btn-light btn-sm position-relative z-2' %> - <% else %> - <%= t('extranet.alumni.invitations.send.too_soon') %> - <% end %> -

- <% end %> + <%= render 'extranet/alumni/persons/invitations/cta', person: person, l10n: l10n %>
diff --git a/app/views/extranet/alumni/persons/show.html.erb b/app/views/extranet/alumni/persons/show.html.erb index 95e1c59d62..458c8746f8 100644 --- a/app/views/extranet/alumni/persons/show.html.erb +++ b/app/views/extranet/alumni/persons/show.html.erb @@ -1,18 +1,6 @@ <% content_for :title, @l10n %> -<% if @person.user_id.blank? && @person.email.blank? %> -

- <%= t('extranet.alumni.invitations.send.label') %> -
- <% if Communication::Extranet::Invitation.sendable_to?(@person) %> - <%= link_to t('extranet.alumni.invitations.send.cta', name: @l10n), - alumni_new_invitation_path(@person), - class: 'btn btn-light btn-sm position-relative z-2' %> - <% else %> - <%= t('extranet.alumni.invitations.send.too_soon') %> - <% end %> -

- <% end %> +<%= render 'extranet/alumni/persons/invitations/cta', person: @person, l10n: @l10n %>
diff --git a/app/views/extranet/home/features/_alumni.html.erb b/app/views/extranet/home/features/_alumni.html.erb index c409a16f84..ece21e87e5 100644 --- a/app/views/extranet/home/features/_alumni.html.erb +++ b/app/views/extranet/home/features/_alumni.html.erb @@ -6,6 +6,7 @@
    <% @experiences.ordered.each do |experience| %> <% l10n = experience.localization_for(current_language) %> + <% person_l10n = experience.person.best_localization_for(current_language) %> <% organization_l10n = experience.organization.best_localization_for(current_language) if experience.organization.present? %>
  • @@ -23,14 +24,15 @@

    - <%= experience.person.best_localization_for(current_language) %> + <%= person_l10n %>
    <%= [l10n&.description, organization_l10n&.to_s].compact_blank.join('-') %>

    + <%= render 'extranet/alumni/persons/invitations/cta', person: experience.person, l10n: person_l10n %> <%= l experience.created_at, format: :date_with_explicit_month %>
    <% if organization_l10n && organization_l10n.logo.attached? %> - <%= link_to [:alumni, experience.organization] do %> + <%= link_to [:alumni, experience.organization], class: 'position-relative z-2' do %> <%= kamifusen_tag organization_l10n.logo, height: 80, class: 'img-fluid' %> <% end %> <% end %> diff --git a/db/schema.rb b/db/schema.rb index 1e0206a6de..c88fa6f2c7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -18,7 +18,7 @@ enable_extension "pgcrypto" enable_extension "unaccent" - create_table "action_text_rich_texts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "action_text_rich_texts", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.text "body" t.datetime "created_at", null: false t.string "name", null: false @@ -45,7 +45,7 @@ t.index ["ip_address", "created_at"], name: "index_active_hashcash_stamps_on_ip_address_and_created_at", where: "(ip_address IS NOT NULL)" end - create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "active_storage_attachments", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "blob_id", null: false t.datetime "created_at", precision: nil, null: false t.datetime "deleted_at" @@ -56,7 +56,7 @@ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end - create_table "active_storage_blobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "active_storage_blobs", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.bigint "byte_size", null: false t.string "checksum" t.string "content_type" @@ -70,7 +70,7 @@ t.index ["university_id"], name: "index_active_storage_blobs_on_university_id" end - create_table "active_storage_variant_records", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "active_storage_variant_records", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "blob_id", null: false t.string "variation_digest", null: false t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true @@ -90,7 +90,7 @@ t.index ["university_id"], name: "idx_on_university_id_31eabbc7a7" end - create_table "administration_academic_years", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "administration_academic_years", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deleted_at" t.uuid "university_id", null: false @@ -120,7 +120,7 @@ t.index ["university_id"], name: "index_administration_cohort_localizations_on_university_id" end - create_table "administration_cohorts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "administration_cohorts", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "academic_year_id", null: false t.datetime "created_at", null: false t.datetime "deleted_at" @@ -193,7 +193,7 @@ t.index ["education_school_id", "administration_location_id"], name: "index_location_school" end - create_table "administration_qualiopi_criterions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "administration_qualiopi_criterions", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.text "description" t.text "name" @@ -201,7 +201,7 @@ t.datetime "updated_at", null: false end - create_table "administration_qualiopi_indicators", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "administration_qualiopi_indicators", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.uuid "criterion_id", null: false t.text "glossary" @@ -215,7 +215,7 @@ t.index ["criterion_id"], name: "index_administration_qualiopi_indicators_on_criterion_id" end - create_table "communication_blocks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_blocks", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id" t.string "about_type" t.uuid "communication_website_id" @@ -236,7 +236,7 @@ t.index ["university_id", "template_kind"], name: "index_communication_blocks_on_university_id_and_template_kind" end - create_table "communication_extranet_connections", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_extranet_connections", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id" t.string "about_type" t.datetime "created_at", null: false @@ -248,7 +248,7 @@ t.index ["university_id"], name: "index_communication_extranet_connections_on_university_id" end - create_table "communication_extranet_document_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_extranet_document_categories", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.uuid "extranet_id", null: false t.uuid "university_id", null: false @@ -289,7 +289,7 @@ t.index ["university_id"], name: "idx_on_university_id_0dc1259072" end - create_table "communication_extranet_document_kinds", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_extranet_document_kinds", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.uuid "extranet_id", null: false t.uuid "university_id", null: false @@ -315,7 +315,7 @@ t.index ["university_id"], name: "idx_on_university_id_95419f1df4" end - create_table "communication_extranet_documents", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_extranet_documents", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "category_id" t.datetime "created_at", null: false t.uuid "extranet_id", null: false @@ -372,7 +372,7 @@ t.index ["university_id"], name: "index_communication_extranet_localizations_on_university_id" end - create_table "communication_extranet_post_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_extranet_post_categories", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.uuid "extranet_id", null: false t.uuid "university_id", null: false @@ -419,7 +419,7 @@ t.index ["university_id"], name: "idx_on_university_id_28188e2217" end - create_table "communication_extranet_posts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_extranet_posts", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "author_id" t.uuid "category_id" t.datetime "created_at", null: false @@ -432,7 +432,7 @@ t.index ["university_id"], name: "index_communication_extranet_posts_on_university_id" end - create_table "communication_extranets", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_extranets", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id" t.string "about_type" t.string "color" @@ -787,7 +787,7 @@ t.index ["university_id"], name: "idx_on_university_id_bca328e63c" end - create_table "communication_website_agenda_events", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_website_agenda_events", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "bodyclass" t.uuid "communication_website_id", null: false t.datetime "created_at", null: false @@ -978,7 +978,7 @@ t.index ["university_id"], name: "index_communication_website_alerts_on_university_id" end - create_table "communication_website_connections", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_website_connections", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.uuid "direct_source_id" t.string "direct_source_type" @@ -1034,7 +1034,7 @@ t.index ["university_id"], name: "index_communication_website_git_file_orphans_on_university_id" end - create_table "communication_website_git_files", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_website_git_files", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id" t.string "about_type" t.datetime "created_at", null: false @@ -1180,7 +1180,7 @@ t.index ["university_id"], name: "index_communication_website_localizations_on_university_id" end - create_table "communication_website_menu_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_website_menu_items", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id" t.string "about_type" t.datetime "created_at", null: false @@ -1203,7 +1203,7 @@ t.index ["website_id"], name: "index_communication_website_menu_items_on_website_id" end - create_table "communication_website_menus", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_website_menus", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.boolean "automatic", default: true t.uuid "communication_website_id", null: false t.datetime "created_at", null: false @@ -1302,7 +1302,7 @@ t.index ["university_id"], name: "idx_on_university_id_e62b2aba53" end - create_table "communication_website_pages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_website_pages", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "bodyclass" t.uuid "communication_website_id", null: false t.datetime "created_at", null: false @@ -1323,7 +1323,7 @@ t.index ["university_id"], name: "index_communication_website_pages_on_university_id" end - create_table "communication_website_permalinks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_website_permalinks", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id" t.string "about_type" t.datetime "created_at", null: false @@ -1436,7 +1436,7 @@ t.index ["university_id"], name: "idx_on_university_id_ac2f4a0bfc" end - create_table "communication_website_post_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_website_post_categories", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "bodyclass" t.uuid "communication_website_id", null: false t.datetime "created_at", null: false @@ -1524,7 +1524,7 @@ t.index ["unpublication_job_id"], name: "idx_on_unpublication_job_id" end - create_table "communication_website_posts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_website_posts", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "bodyclass" t.uuid "communication_website_id", null: false t.datetime "created_at", null: false @@ -1559,7 +1559,7 @@ t.index ["communication_website_showcase_tag_id", "communication_website_id"], name: "index_showcase_tag_website" end - create_table "communication_websites", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "communication_websites", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id" t.string "about_type" t.string "access_token" @@ -1650,7 +1650,7 @@ t.index ["university_id"], name: "index_education_diploma_localizations_on_university_id" end - create_table "education_diplomas", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "education_diplomas", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "certification" t.datetime "created_at", null: false t.datetime "deleted_at" @@ -1749,7 +1749,7 @@ t.index ["university_id"], name: "index_education_program_localizations_on_university_id" end - create_table "education_programs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "education_programs", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.boolean "apprenticeship" t.string "bodyclass" t.integer "capacity" @@ -1803,7 +1803,7 @@ t.index ["university_id"], name: "index_education_school_localizations_on_university_id" end - create_table "education_schools", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "education_schools", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "address" t.string "city" t.string "country" @@ -1818,7 +1818,7 @@ t.index ["university_id"], name: "index_education_schools_on_university_id" end - create_table "emergency_messages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "emergency_messages", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.text "content_en" t.text "content_fr" t.datetime "created_at", null: false @@ -1924,7 +1924,7 @@ t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)" end - create_table "imports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "imports", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.integer "kind" t.uuid "language_id", null: false @@ -1939,7 +1939,7 @@ t.index ["user_id"], name: "index_imports_on_user_id" end - create_table "languages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "languages", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.string "iso_code" t.string "name" @@ -1953,7 +1953,7 @@ t.index ["university_id", "language_id"], name: "index_languages_universities_on_university_id_and_language_id" end - create_table "research_hal_authors", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "research_hal_authors", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.string "docid" t.string "first_name" @@ -2012,7 +2012,7 @@ t.index ["university_id"], name: "idx_on_university_id_dc9f1267b7" end - create_table "research_journal_paper_kinds", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "research_journal_paper_kinds", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deleted_at" t.uuid "journal_id", null: false @@ -2044,7 +2044,7 @@ t.index ["university_id"], name: "index_research_journal_paper_localizations_on_university_id" end - create_table "research_journal_papers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "research_journal_papers", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.date "accepted_at" t.text "bibliography" t.datetime "created_at", null: false @@ -2096,7 +2096,7 @@ t.index ["university_id"], name: "index_research_journal_volume_localizations_on_university_id" end - create_table "research_journal_volumes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "research_journal_volumes", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deleted_at" t.integer "number" @@ -2107,7 +2107,7 @@ t.index ["university_id"], name: "index_research_journal_volumes_on_university_id" end - create_table "research_journals", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "research_journals", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deleted_at" t.uuid "university_id", null: false @@ -2115,7 +2115,7 @@ t.index ["university_id"], name: "index_research_journals_on_university_id" end - create_table "research_laboratories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "research_laboratories", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "address" t.string "city" t.string "country" @@ -2134,7 +2134,7 @@ t.index ["university_person_id", "research_laboratory_id"], name: "laboratory_person" end - create_table "research_laboratory_axes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "research_laboratory_axes", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deleted_at" t.integer "position", null: false @@ -2179,7 +2179,7 @@ t.index ["university_id"], name: "index_research_laboratory_localizations_on_university_id" end - create_table "research_publications", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "research_publications", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.text "abstract" t.text "anr_project_references", default: [], array: true t.json "authors_citeproc" @@ -2213,7 +2213,7 @@ t.index ["university_person_id", "research_publication_id"], name: "index_publication_person" end - create_table "research_theses", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "research_theses", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "author_id", null: false t.boolean "completed", default: false t.date "completed_at" @@ -2272,7 +2272,7 @@ t.datetime "updated_at", null: false end - create_table "universities", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "universities", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "address" t.boolean "admin_already_auto_promoted", default: false t.string "city" @@ -2303,7 +2303,7 @@ t.index ["name"], name: "index_universities_on_name", opclass: :gin_trgm_ops, using: :gin end - create_table "university_apps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_apps", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.string "name" t.string "token" @@ -2314,7 +2314,7 @@ t.index ["university_id"], name: "index_university_apps_on_university_id" end - create_table "university_organization_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_organization_categories", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "bodyclass" t.datetime "created_at", null: false t.boolean "is_taxonomy", default: false @@ -2328,7 +2328,7 @@ t.index ["university_id"], name: "index_university_organization_categories_on_university_id" end - create_table "university_organization_categories_organizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_organization_categories_organizations", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "category_id", null: false t.uuid "organization_id", null: false t.index ["category_id"], name: "idx_on_category_id_7494b991ff" @@ -2391,7 +2391,7 @@ t.index ["university_id"], name: "index_university_organization_localizations_on_university_id" end - create_table "university_organizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_organizations", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "address" t.string "bodyclass" t.string "city" @@ -2414,7 +2414,7 @@ t.index ["university_id"], name: "index_university_organizations_on_university_id" end - create_table "university_people", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_people", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "address" t.integer "address_visibility", default: 0 t.date "birthdate" @@ -2453,14 +2453,14 @@ t.index ["user_id"], name: "index_university_people_on_user_id" end - create_table "university_people_person_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_people_person_categories", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "category_id", null: false t.uuid "person_id", null: false t.index ["category_id"], name: "index_university_people_person_categories_on_category_id" t.index ["person_id"], name: "index_university_people_person_categories_on_person_id" end - create_table "university_person_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_person_categories", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.string "bodyclass" t.datetime "created_at", null: false t.boolean "is_taxonomy", default: false @@ -2512,7 +2512,7 @@ t.index ["university_id"], name: "idx_on_university_id_1be9c668d5" end - create_table "university_person_experiences", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_person_experiences", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deleted_at" t.integer "from_year" @@ -2540,7 +2540,7 @@ t.index ["university_id"], name: "idx_on_university_id_0b815cf13a" end - create_table "university_person_involvements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_person_involvements", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deleted_at" t.integer "kind" @@ -2599,7 +2599,7 @@ t.index ["university_id"], name: "index_university_role_localizations_on_university_id" end - create_table "university_roles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "university_roles", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deleted_at" t.integer "position", null: false @@ -2611,7 +2611,7 @@ t.index ["university_id"], name: "index_university_roles_on_university_id" end - create_table "user_favorites", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "user_favorites", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id", null: false t.string "about_type", null: false t.datetime "created_at", null: false @@ -2621,7 +2621,7 @@ t.index ["user_id"], name: "index_user_favorites_on_user_id" end - create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "users", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t| t.integer "brevo_contact_id" t.datetime "confirmation_sent_at", precision: nil t.string "confirmation_token" From e6cda95c5afc760fc2a702f06fb4ed32aa1ddc01 Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 10:43:52 +0200 Subject: [PATCH 04/18] factorize invitation cta --- .../alumni/persons/invitations/_cta.html.erb | 13 +++++++++++++ mise.toml | 2 ++ 2 files changed, 15 insertions(+) create mode 100644 app/views/extranet/alumni/persons/invitations/_cta.html.erb create mode 100644 mise.toml diff --git a/app/views/extranet/alumni/persons/invitations/_cta.html.erb b/app/views/extranet/alumni/persons/invitations/_cta.html.erb new file mode 100644 index 0000000000..23d0297500 --- /dev/null +++ b/app/views/extranet/alumni/persons/invitations/_cta.html.erb @@ -0,0 +1,13 @@ +<% if person.user_id.blank? && person.email.blank? %> +

    + <%= t('extranet.alumni.invitations.send.label') %> +
    + <% if Communication::Extranet::Invitation.sendable_to?(person) %> + <%= link_to t('extranet.alumni.invitations.send.cta', name: l10n), + alumni_new_invitation_path(person), + class: 'btn btn-light btn-sm position-relative z-2' %> + <% else %> + <%= t('extranet.alumni.invitations.send.too_soon') %> + <% end %> +

    + <% end %> \ No newline at end of file diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..8478956175 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +ruby = "4.0.6" From 0dec6a82fb601e7c6ba25c5dee6b2798f341494d Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 11:07:09 +0200 Subject: [PATCH 05/18] add token creation for invitations --- app/models/communication/block.rb | 2 +- app/models/communication/extranet.rb | 2 +- app/models/communication/extranet/invitation.rb | 4 ++++ app/models/communication/media.rb | 2 +- app/models/communication/website.rb | 4 ++-- app/models/communication/website/alert.rb | 2 +- app/models/communication/website/menu/item.rb | 2 +- app/models/education/diploma.rb | 2 +- app/models/import.rb | 2 +- app/models/research/publication.rb | 2 +- app/models/university.rb | 2 +- app/models/university/organization.rb | 2 +- app/models/university/person.rb | 16 ++++++++-------- app/models/university/person/alumnus.rb | 16 ++++++++-------- app/models/user.rb | 2 +- ...oken_to_communication_extranet_invitations.rb | 6 ++++++ db/schema.rb | 4 +++- 17 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 db/migrate/20260806085855_add_token_to_communication_extranet_invitations.rb diff --git a/app/models/communication/block.rb b/app/models/communication/block.rb index 816a08f2dd..8127307087 100644 --- a/app/models/communication/block.rb +++ b/app/models/communication/block.rb @@ -11,7 +11,7 @@ # migration_identifier :string # position :integer not null # published :boolean default(TRUE) -# template_kind :integer default(NULL), not null, indexed => [university_id] +# template_kind :integer default(0), not null, indexed => [university_id] # title :string # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/communication/extranet.rb b/app/models/communication/extranet.rb index b290da2bd3..fa3400a78b 100644 --- a/app/models/communication/extranet.rb +++ b/app/models/communication/extranet.rb @@ -17,7 +17,7 @@ # sso_cert :text # sso_mapping :jsonb # sso_name_identifier_format :string -# sso_provider :integer default("saml") +# sso_provider :integer default(0) # sso_target_url :string # upper_menu :text default("") # created_at :datetime not null diff --git a/app/models/communication/extranet/invitation.rb b/app/models/communication/extranet/invitation.rb index f231acf24a..5e78696545 100644 --- a/app/models/communication/extranet/invitation.rb +++ b/app/models/communication/extranet/invitation.rb @@ -8,6 +8,7 @@ # message :text # to_email :string # to_name :string +# token :string uniquely indexed # created_at :datetime not null # updated_at :datetime not null # extranet_id :uuid indexed @@ -19,6 +20,7 @@ # # index_communication_extranet_invitations_on_extranet_id (extranet_id) # index_communication_extranet_invitations_on_person_id (person_id) +# index_communication_extranet_invitations_on_token (token) UNIQUE # index_communication_extranet_invitations_on_university_id (university_id) # index_communication_extranet_invitations_on_user_id (user_id) # @@ -33,6 +35,8 @@ class Communication::Extranet::Invitation < ApplicationRecord include HasUniversity include Sanitizable + has_secure_token :token + belongs_to :extranet, class_name: 'Communication::Extranet' belongs_to :user belongs_to :person, class_name: 'University::Person' diff --git a/app/models/communication/media.rb b/app/models/communication/media.rb index 25850fd7df..0944549a1f 100644 --- a/app/models/communication/media.rb +++ b/app/models/communication/media.rb @@ -3,7 +3,7 @@ # Table name: communication_medias # # id :uuid not null, primary key -# origin :integer default("upload"), not null +# origin :integer default(1), not null # original_byte_size :bigint # original_checksum :string # original_content_type :string diff --git a/app/models/communication/website.rb b/app/models/communication/website.rb index 6189e31370..436e64114f 100644 --- a/app/models/communication/website.rb +++ b/app/models/communication/website.rb @@ -24,9 +24,9 @@ # git_branch :string # git_endpoint :string # git_files_analysed_at :datetime -# git_provider :integer default("github") +# git_provider :integer default(0) # highlighted_in_showcase :boolean default(FALSE) -# hosting :integer default("deuxfleurs"), not null +# hosting :integer default(1), not null # in_production :boolean default(FALSE) # in_production_at :datetime # in_showcase :boolean default(TRUE) diff --git a/app/models/communication/website/alert.rb b/app/models/communication/website/alert.rb index d484925d25..ce01fd4aa8 100644 --- a/app/models/communication/website/alert.rb +++ b/app/models/communication/website/alert.rb @@ -4,7 +4,7 @@ # # id :uuid not null, primary key # deleted_at :datetime -# kind :integer default("info"), not null +# kind :integer default(0), not null # created_at :datetime not null # updated_at :datetime not null # communication_website_id :uuid not null, indexed diff --git a/app/models/communication/website/menu/item.rb b/app/models/communication/website/menu/item.rb index 92b426c86f..f2dd1b7a62 100644 --- a/app/models/communication/website/menu/item.rb +++ b/app/models/communication/website/menu/item.rb @@ -5,7 +5,7 @@ # id :uuid not null, primary key # about_type :string indexed => [about_id] # html_class :string -# kind :integer default("blank") +# kind :integer default(0) # position :integer not null # position_in_tree :integer # should_open_new_tab :boolean default(FALSE) diff --git a/app/models/education/diploma.rb b/app/models/education/diploma.rb index 40eda6a697..3ccbccff34 100644 --- a/app/models/education/diploma.rb +++ b/app/models/education/diploma.rb @@ -6,7 +6,7 @@ # certification :string # deleted_at :datetime # ects :integer -# level :integer default("not_applicable") +# level :integer default(0) # position :integer not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/import.rb b/app/models/import.rb index c4f08546c2..5e8c040f9f 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -6,7 +6,7 @@ # kind :integer # number_of_lines :integer # processing_errors :jsonb -# status :integer default("pending") +# status :integer default(0) # created_at :datetime not null # updated_at :datetime not null # language_id :uuid not null, indexed diff --git a/app/models/research/publication.rb b/app/models/research/publication.rb index d5f4aca37f..11efe7a644 100644 --- a/app/models/research/publication.rb +++ b/app/models/research/publication.rb @@ -20,7 +20,7 @@ # publication_date :date # ref :string # slug :string indexed -# source :integer default("osuny") +# source :integer default(0) # title :string # url :string # created_at :datetime not null diff --git a/app/models/university.rb b/app/models/university.rb index ea77d0a0ad..3502b0d004 100644 --- a/app/models/university.rb +++ b/app/models/university.rb @@ -23,7 +23,7 @@ # sso_cert :text # sso_mapping :jsonb # sso_name_identifier_format :string -# sso_provider :integer default("saml") +# sso_provider :integer default(0) # sso_target_url :string # zipcode :string # created_at :datetime not null diff --git a/app/models/university/organization.rb b/app/models/university/organization.rb index 400d48e52c..0e83b35498 100644 --- a/app/models/university/organization.rb +++ b/app/models/university/organization.rb @@ -9,7 +9,7 @@ # country :string # deleted_at :datetime # email :string -# kind :integer default("company") +# kind :integer default(10) # latitude :float # longitude :float # migration_identifier :string diff --git a/app/models/university/person.rb b/app/models/university/person.rb index 844118e191..897514e283 100644 --- a/app/models/university/person.rb +++ b/app/models/university/person.rb @@ -4,14 +4,14 @@ # # id :uuid not null, primary key # address :string -# address_visibility :integer default("private") +# address_visibility :integer default(0) # birthdate :date # bodyclass :string # city :string # country :string # deleted_at :datetime # email :string -# email_visibility :integer default("private") +# email_visibility :integer default(0) # gender :integer # habilitation :boolean default(FALSE) # invitation_sent_at :datetime @@ -20,16 +20,16 @@ # is_author :boolean # is_researcher :boolean # is_teacher :boolean -# linkedin_visibility :integer default("private") -# mastodon_visibility :integer default("private") +# linkedin_visibility :integer default(0) +# mastodon_visibility :integer default(0) # phone_mobile :string -# phone_mobile_visibility :integer default("private") +# phone_mobile_visibility :integer default(0) # phone_personal :string -# phone_personal_visibility :integer default("private") +# phone_personal_visibility :integer default(0) # phone_professional :string -# phone_professional_visibility :integer default("private") +# phone_professional_visibility :integer default(0) # tenure :boolean default(FALSE) -# twitter_visibility :integer default("private") +# twitter_visibility :integer default(0) # zipcode :string # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/university/person/alumnus.rb b/app/models/university/person/alumnus.rb index 59a83846dd..ecd01470c1 100644 --- a/app/models/university/person/alumnus.rb +++ b/app/models/university/person/alumnus.rb @@ -4,14 +4,14 @@ # # id :uuid not null, primary key # address :string -# address_visibility :integer default("private") +# address_visibility :integer default(0) # birthdate :date # bodyclass :string # city :string # country :string # deleted_at :datetime # email :string -# email_visibility :integer default("private") +# email_visibility :integer default(0) # gender :integer # habilitation :boolean default(FALSE) # invitation_sent_at :datetime @@ -20,16 +20,16 @@ # is_author :boolean # is_researcher :boolean # is_teacher :boolean -# linkedin_visibility :integer default("private") -# mastodon_visibility :integer default("private") +# linkedin_visibility :integer default(0) +# mastodon_visibility :integer default(0) # phone_mobile :string -# phone_mobile_visibility :integer default("private") +# phone_mobile_visibility :integer default(0) # phone_personal :string -# phone_personal_visibility :integer default("private") +# phone_personal_visibility :integer default(0) # phone_professional :string -# phone_professional_visibility :integer default("private") +# phone_professional_visibility :integer default(0) # tenure :boolean default(FALSE) -# twitter_visibility :integer default("private") +# twitter_visibility :integer default(0) # zipcode :string # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/user.rb b/app/models/user.rb index 2bd11ecec5..f0afb71c9f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,7 +28,7 @@ # remember_created_at :datetime # reset_password_sent_at :datetime # reset_password_token :string uniquely indexed -# role :integer default("visitor") +# role :integer default(0) # second_factor_attempts_count :integer default(0) # session_token :string # sign_in_count :integer default(0), not null diff --git a/db/migrate/20260806085855_add_token_to_communication_extranet_invitations.rb b/db/migrate/20260806085855_add_token_to_communication_extranet_invitations.rb new file mode 100644 index 0000000000..9fe69d81b0 --- /dev/null +++ b/db/migrate/20260806085855_add_token_to_communication_extranet_invitations.rb @@ -0,0 +1,6 @@ +class AddTokenToCommunicationExtranetInvitations < ActiveRecord::Migration[8.1] + def change + add_column :communication_extranet_invitations, :token, :string + add_index :communication_extranet_invitations, :token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index c88fa6f2c7..54dcfc2258 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[8.1].define(version: 2026_07_16_132522) do +ActiveRecord::Schema[8.1].define(version: 2026_08_06_085855) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pg_stat_statements" @@ -337,11 +337,13 @@ t.uuid "person_id" t.string "to_email" t.string "to_name" + t.string "token" t.uuid "university_id" t.datetime "updated_at", null: false t.uuid "user_id" t.index ["extranet_id"], name: "index_communication_extranet_invitations_on_extranet_id" t.index ["person_id"], name: "index_communication_extranet_invitations_on_person_id" + t.index ["token"], name: "index_communication_extranet_invitations_on_token", unique: true t.index ["university_id"], name: "index_communication_extranet_invitations_on_university_id" t.index ["user_id"], name: "index_communication_extranet_invitations_on_user_id" end From 4ed8aaec9bb124894818605b83d9ea02d08d97c2 Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 12:27:15 +0200 Subject: [PATCH 06/18] fix cohort denorms and set correct default message for invitations --- .../alumni/persons/invitations_controller.rb | 10 +++++++++- .../communication/extranet/localization.rb | 18 ++++++++++++++++++ app/models/university/person/with_alumnus.rb | 19 ++++++++++++++++++- app/services/importers/hash_to_cohort.rb | 8 +------- .../alumni/persons/invitations/new.html.erb | 2 +- config/locales/communication/en.yml | 2 +- config/locales/communication/fr.yml | 2 +- 7 files changed, 49 insertions(+), 12 deletions(-) diff --git a/app/controllers/extranet/alumni/persons/invitations_controller.rb b/app/controllers/extranet/alumni/persons/invitations_controller.rb index d70d9488db..0b6cb96ca6 100644 --- a/app/controllers/extranet/alumni/persons/invitations_controller.rb +++ b/app/controllers/extranet/alumni/persons/invitations_controller.rb @@ -8,7 +8,7 @@ def new from_email: current_user.email, to_name: @l10n.to_s, to_email: @person.email, - message: "TODO: Add a default message for the invitation" + message: default_message ) breadcrumb end @@ -52,5 +52,13 @@ def ensure_person_is_invitable redirect_to [:alumni, @person], alert: t('extranet.alumni.invitations.send.too_soon') end end + + def default_message + current_extranet_l10n.invitation_manual_text( + from_name: current_user.to_s, + from_promotion: current_user.person&.promotion, + to_name: @l10n.to_s + ) + end end diff --git a/app/models/communication/extranet/localization.rb b/app/models/communication/extranet/localization.rb index a3c0ca212d..e7a762484c 100644 --- a/app/models/communication/extranet/localization.rb +++ b/app/models/communication/extranet/localization.rb @@ -63,8 +63,26 @@ def to_s "#{name}" end + def invitation_manual_subject(from_name:, from_promotion:, to_name:) + substitute_invitation_placeholders(invitation_message_manual_subject, from_name: from_name, from_promotion: from_promotion, to_name: to_name) + end + + def invitation_manual_text(from_name:, from_promotion:, to_name:) + substitute_invitation_placeholders(invitation_message_manual_text, from_name: from_name, from_promotion: from_promotion, to_name: to_name) + end + protected + def substitute_invitation_placeholders(text, from_name:, from_promotion:, to_name:) + substitutions = { + 'name' => to_name, + 'sender_name' => from_name, + 'sender_promotion' => from_promotion + } + substitutions.each { |key, value| text = text.gsub("{{#{key}}}", value.to_s) } + text + end + def prevent_unpublishing_default_language return unless about.default_language_id == language_id return if published? diff --git a/app/models/university/person/with_alumnus.rb b/app/models/university/person/with_alumnus.rb index fb1e4449d5..676486e104 100644 --- a/app/models/university/person/with_alumnus.rb +++ b/app/models/university/person/with_alumnus.rb @@ -11,7 +11,7 @@ module University::Person::WithAlumnus accepts_nested_attributes_for :cohorts, reject_if: :all_blank, allow_destroy: true - before_validation :find_cohorts + before_validation :find_cohorts, :sync_diploma_denormalizations validates_associated :cohorts # Dénormalisation des liens via cohorts, pour la recherche par facettes @@ -71,6 +71,10 @@ def for_alumni_account(with_account, extranet) end end + def promotion + diploma_years.map(&:year).compact.uniq.sort.join(', ') + end + def find_cohorts # based on https://stackoverflow.com/questions/3579924/accepts-nested-attributes-for-with-find-or-create cohorts_to_set = [] @@ -85,8 +89,21 @@ def find_cohorts self.cohorts = cohorts_to_set end + # Ajoute une cohorte en gardant les dénormalisations à jour + def add_cohort(cohort) + return if cohort.in?(cohorts) + cohorts << cohort + sync_diploma_denormalizations + end + private + # Dénormalisation des liens via cohorts, pour la recherche par facettes + def sync_diploma_denormalizations + self.diploma_years = cohorts.map(&:academic_year).compact.uniq + self.diploma_programs = cohorts.map(&:program).compact.uniq + end + def find_cohort_for_nested(object) academic_year = Administration::AcademicYear.where(university_id: university_id, year: object.year).first_or_create cohort = Administration::Cohort.where(university_id: university_id, school_id: object.school_id, program_id: object.program_id, academic_year_id: academic_year.id).first_or_initialize diff --git a/app/services/importers/hash_to_cohort.rb b/app/services/importers/hash_to_cohort.rb index 7e428bee53..633f82fa43 100644 --- a/app/services/importers/hash_to_cohort.rb +++ b/app/services/importers/hash_to_cohort.rb @@ -36,13 +36,7 @@ def extranet_ids protected def add_to_cohort(person, cohort) - add_object_if_necessary cohort, person.cohorts - add_object_if_necessary cohort.academic_year, person.diploma_years - add_object_if_necessary cohort.program, person.diploma_programs - end - - def add_object_if_necessary(object, list) - list << object unless object.in?(list) + person.add_cohort(cohort) end def extract_variables diff --git a/app/views/extranet/alumni/persons/invitations/new.html.erb b/app/views/extranet/alumni/persons/invitations/new.html.erb index 1b4f0c5ad5..abd4f9e4c7 100644 --- a/app/views/extranet/alumni/persons/invitations/new.html.erb +++ b/app/views/extranet/alumni/persons/invitations/new.html.erb @@ -12,7 +12,7 @@ <%= f.input :to_email %>
    - <%= f.input :message %> + <%= f.input :message, input_html: { rows: 10 } %>
    diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml index 8edb0a0e55..cb23cb7ad4 100644 --- a/config/locales/communication/en.yml +++ b/config/locales/communication/en.yml @@ -536,7 +536,7 @@ en: automatic: Automatic message automatic_hint: This message will be sent after an alumni import or when clicking on the "send invitation" button on admin, if the alumnus has an email. manual: Manual message - manual_hint_html: "This message will be sent from the extranet, when a user clicks on the \"invite alumnus\" button, if the user has no email.
    In these fields you can use the following variables:
    • {{name}}: the name of the alumnus
    • {{email}}: the email of the alumnus
    • {{sender_name}}: the name of the user sending the invitation
    • {{sender_promotion}}: the promotion of the user sending the invitation
    " + manual_hint_html: "This message will be sent from the extranet, when a user clicks on the \"invite alumnus\" button, if the user has no email.
    In these fields you can use the following variables:
    • {{name}}: the name of the alumnus
    • {{sender_name}}: the name of the user sending the invitation
    • {{sender_promotion}}: the promotion of the user sending the invitation
    " title: Invitation messages languages: Languages website: diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml index a57441f289..4487f3fee6 100644 --- a/config/locales/communication/fr.yml +++ b/config/locales/communication/fr.yml @@ -536,7 +536,7 @@ fr: automatic: Message automatique automatic_hint: Ce message sera envoyé après un import d'alumni ou lors du clic sur le bouton "envoyer l'invitation" dans l'admin, si l'alumnus a un email. manual: Message manuel - manual_hint_html: "Ce message sera envoyé depuis l'extranet, lorsqu'un utilisateur clique sur le bouton \"inviter un alumnus\", si l'utilisateur n'a pas d'email.
    Dans ces champs vous pouvez utiliser les variables suivantes :
    • {{name}} : le nom de l'alumnus
    • {{email}} : l'email indiqué pour l'alumnus
    • {{sender_name}} : le nom de l'utilisateur qui envoie l'invitation
    • {{sender_promotion}} : la promotion de l'utilisateur qui envoie l'invitation
    " + manual_hint_html: "Ce message sera envoyé depuis l'extranet, lorsqu'un utilisateur clique sur le bouton \"inviter un alumnus\", si l'utilisateur n'a pas d'email.
    Dans ces champs vous pouvez utiliser les variables suivantes :
    • {{name}} : le nom de l'alumnus
    • {{sender_name}} : le nom de l'utilisateur qui envoie l'invitation
    • {{sender_promotion}} : la promotion de l'utilisateur qui envoie l'invitation
    " title: Messages d'invitation languages: Langues website: From ddfe7fa0c83ad62ed144cb47d85db37291f6dd81 Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 15:13:09 +0200 Subject: [PATCH 07/18] add mailer --- .../extranets/alumni_controller.rb | 2 +- app/mailers/extranet_mailer.rb | 34 ++++++++++++++++++- .../communication/extranet/connection.rb | 2 +- .../communication/extranet/invitation.rb | 3 +- app/services/importers/alumni_cohorts.rb | 2 +- ... => invitation_message_automatic.html.erb} | 0 .../invitation_message_manual.html.erb | 7 ++++ config/locales/en.yml | 1 + config/locales/fr.yml | 1 + .../previews/extranet_mailer_preview.rb | 31 +++++++++++++++-- 10 files changed, 74 insertions(+), 9 deletions(-) rename app/views/mailers/extranet/{invitation_message.html.erb => invitation_message_automatic.html.erb} (100%) create mode 100644 app/views/mailers/extranet/invitation_message_manual.html.erb diff --git a/app/controllers/admin/communication/extranets/alumni_controller.rb b/app/controllers/admin/communication/extranets/alumni_controller.rb index 5dad9657e8..02e32ad4c2 100644 --- a/app/controllers/admin/communication/extranets/alumni_controller.rb +++ b/app/controllers/admin/communication/extranets/alumni_controller.rb @@ -27,7 +27,7 @@ def index def send_invitation person = @extranet.alumni.find(params[:id]) unless person.user_id - ExtranetMailer.invitation_message(@extranet, person).deliver_later + ExtranetMailer.invitation_message_automatic(@extranet, person).deliver_later person.update_column(:invitation_sent_at, Time.current) redirect_to admin_communication_extranet_alumni_path(@extranet), notice: t('admin.communication.extranet.alumni.send_invitation.just_sent', name: person.to_s_in(current_language)) diff --git a/app/mailers/extranet_mailer.rb b/app/mailers/extranet_mailer.rb index a99a8dd5c7..21c0dfaac0 100644 --- a/app/mailers/extranet_mailer.rb +++ b/app/mailers/extranet_mailer.rb @@ -2,7 +2,7 @@ class ExtranetMailer < ApplicationMailer helper :application # Gives access to all helpers defined within `application_helper` default template_path: 'mailers/extranet' - def invitation_message(extranet, person) + def invitation_message_automatic(extranet, person) @extranet = extranet @university = @extranet.university @person = person @@ -21,4 +21,36 @@ def invitation_message(extranet, person) end end + def invitation_message_manual(invitation) + @invitation = invitation + extranet = @invitation.extranet + @university = extranet.university + language = @invitation.person&.user&.language || @university.default_language + extranet_l10n = extranet.best_localization_for(language) + @signature = extranet_l10n.invitation_message_manual_signature + @registration_url = new_user_registration_url( + host: extranet.host, + invitation_token: @invitation.token + ) + + merge_with_university_infos(@university, {}) + + I18n.with_locale(language.iso_code.to_sym) do + mail from: @university.mail_from[:full], + to: @invitation.to_email, + subject: invitation_manual_subject(extranet_l10n) if should_send?(@invitation.to_email) + end + end + + private + + def invitation_manual_subject(extranet_l10n) + extranet_l10n.invitation_manual_subject( + from_name: @invitation.from_name, + from_promotion: @invitation.user.person&.promotion, + to_name: @invitation.to_name + ) + end + + end diff --git a/app/models/communication/extranet/connection.rb b/app/models/communication/extranet/connection.rb index d35791c052..d7e492f413 100644 --- a/app/models/communication/extranet/connection.rb +++ b/app/models/communication/extranet/connection.rb @@ -37,7 +37,7 @@ def self.permitted_about_classes def send_invitation_to_person # Do not send invitation if there is no email on the person's user or the person itself return unless about.user.try(:email).present? || about.email.present? - ExtranetMailer.invitation_message(extranet, about).deliver_later + ExtranetMailer.invitation_message_automatic(extranet, about).deliver_later about.update_column(:invitation_sent_at, Time.current) end end diff --git a/app/models/communication/extranet/invitation.rb b/app/models/communication/extranet/invitation.rb index 5e78696545..c6188322ef 100644 --- a/app/models/communication/extranet/invitation.rb +++ b/app/models/communication/extranet/invitation.rb @@ -59,8 +59,7 @@ def can_send_to_person end def send_invitation_email - # TODO - # Communication::Extranet::InvitationMailer.with(invitation: self).invitation_email.deliver_later + ExtranetMailer.invitation_message_manual(self).deliver_later end end diff --git a/app/services/importers/alumni_cohorts.rb b/app/services/importers/alumni_cohorts.rb index 1b5b8556dc..adbb23b325 100644 --- a/app/services/importers/alumni_cohorts.rb +++ b/app/services/importers/alumni_cohorts.rb @@ -36,7 +36,7 @@ def send_extranet_invitation_emails @connections.uniq.each do |connection| extranet = @university.communication_extranets.find(connection[:communication_extranet_id]) person = @university.people.find(connection[:person_id]) - ExtranetMailer.invitation_message(extranet, person).deliver_later + ExtranetMailer.invitation_message_automatic(extranet, person).deliver_later person.update_column(:invitation_sent_at, Time.current) end end diff --git a/app/views/mailers/extranet/invitation_message.html.erb b/app/views/mailers/extranet/invitation_message_automatic.html.erb similarity index 100% rename from app/views/mailers/extranet/invitation_message.html.erb rename to app/views/mailers/extranet/invitation_message_automatic.html.erb diff --git a/app/views/mailers/extranet/invitation_message_manual.html.erb b/app/views/mailers/extranet/invitation_message_manual.html.erb new file mode 100644 index 0000000000..67b761d221 --- /dev/null +++ b/app/views/mailers/extranet/invitation_message_manual.html.erb @@ -0,0 +1,7 @@ +<%= simple_format @invitation.message %> + +

    + <%= link_to t('mailers.extranet.invitation_messages.manual.cta'), @registration_url, target: '_blank' %> +

    + +<%= simple_format @signature %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 29f76a9b06..afe4937aa3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -504,6 +504,7 @@ en: with_user_html: You can sign in with your email address %{email} by clicking here. without_user_html: You can sign up with your email address %{email} by clicking here. manual: + cta: Join the extranet signature: Yours subject: Invitation from {{sender_name}} (promotion {{sender_promotion}}) to join the extranet text: "Hello {{name}},\n\n{{sender_name}} invites you to join the Alumni extranet.\n\nYou will find, in particular, the directory of all promotions.\nSee you soon!\n\n{{sender_name}} (promotion {{sender_promotion}})" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 4104b1ba29..0af31c15e9 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -505,6 +505,7 @@ fr: with_user_html: Vous pouvez vous y connecter avec votre adresse email %{email} en cliquant ici. without_user_html: Vous pouvez vous y inscrire avec votre adresse email %{email} en cliquant ici. manual: + cta: Rejoindre l'extranet signature: Cordialement subject: Invitation de {{sender_name}} (promotion {{sender_promotion}}) à rejoindre l'extranet text: "Bonjour {{name}},\n\n{{sender_name}} vous invite à rejoindre l'extranet des Alumni.\n\nVous y retrouverez notamment l'annuaire de toutes les promotions.\nÀ très bientôt !\n\n{{sender_name}} (promotion {{sender_promotion}})" diff --git a/test/mailers/previews/extranet_mailer_preview.rb b/test/mailers/previews/extranet_mailer_preview.rb index 9fc5b800b2..e0b0550363 100644 --- a/test/mailers/previews/extranet_mailer_preview.rb +++ b/test/mailers/previews/extranet_mailer_preview.rb @@ -2,9 +2,34 @@ class ExtranetMailerPreview < BaseMailerPreview - # Preview this email at http://localhost:3000/rails/mailers/extranet_mailer/invitation_message - def invitation_message - ExtranetMailer.invitation_message(extranet, person) + # Preview this email at http://localhost:3000/rails/mailers/extranet_mailer/invitation_message_automatic + def invitation_message_automatic + ExtranetMailer.invitation_message_automatic(extranet, person) + end + + # Preview this email at http://localhost:3000/rails/mailers/extranet_mailer/invitation_message_manual + def invitation_message_manual + ExtranetMailer.invitation_message_manual(invitation) + end + + protected + + def invitation + l10n = extranet.best_localization_for(extranet.default_language) + extranet.invitations.build( + user: user, + person: person, + token: 'sample-invitation-token', + from_name: user.to_s, + from_email: user.email, + to_name: "Invité de test", + to_email: "guest@noesya.coop", + message: l10n.invitation_manual_text( + from_name: user.to_s, + from_promotion: '2020', + to_name: person.to_s + ) + ) end end From c1eed6f590202bb58e88f072bbc50c0a3ecad1bd Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 15:57:08 +0200 Subject: [PATCH 08/18] registration workflow --- .../users/registrations_controller.rb | 26 +++++++++++++++++++ app/models/user/with_person.rb | 11 +++++++- app/views/devise/registrations/new.html.erb | 2 ++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 6e6d6452a4..6e1fa44b60 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -10,6 +10,12 @@ class Users::RegistrationsController < Devise::RegistrationsController before_action :configure_account_update_params, only: :update before_action :confirm_two_factor_authenticated, except: [:new, :create, :cancel] + def new + super do |resource| + prefill_from_invitation(resource) + end + end + def edit # this action is not used anymore, replaced for both universities and extranets. # so we redirect to the appropriate profile edition @@ -23,6 +29,26 @@ def edit protected + def build_resource(hash = {}) + super + resource.invitation = invitation + end + + def prefill_from_invitation(resource) + return if invitation.blank? + resource.email = invitation.to_email + resource.mobile_phone = invitation.person&.phone_mobile + person_l10n = invitation.person&.best_localization_for(current_language) + resource.first_name = person_l10n&.first_name + resource.last_name = person_l10n&.last_name + end + + def invitation + return @invitation if defined?(@invitation) + token = params[:invitation_token] + @invitation = token.present? ? current_extranet&.invitations&.find_by(token: token) : nil + end + def sign_up(resource_name, resource) sign_in(resource, event: :authentication) end diff --git a/app/models/user/with_person.rb b/app/models/user/with_person.rb index 7b3bd544f8..4796bed3fc 100644 --- a/app/models/user/with_person.rb +++ b/app/models/user/with_person.rb @@ -5,8 +5,12 @@ module User::WithPerson # Original person has_one :person, class_name: 'University::Person', dependent: :nullify + # Set during registration through an extranet invitation + attr_accessor :invitation + delegate :experiences, to: :person + before_validation :assign_email_to_invited_person, if: :invitation after_save_commit :sync_person, if: :person after_create :find_or_create_person, unless: :server_admin? end @@ -28,7 +32,7 @@ def sync_person_safely protected def find_or_create_person - person = university.people.where(email: email).first_or_initialize + person = invitation&.person || university.people.where(email: email).first_or_initialize person_l10n = person.localizations.find_by(language_id: university.default_language_id) person.user = self person.localizations_attributes = [ @@ -43,4 +47,9 @@ def find_or_create_person def sync_person User::SyncPersonJob.perform_later(self) end + + def assign_email_to_invited_person + # As this is done on before_validation, it will be rollbacked if the save fails + invitation.person.update_column(:email, email) + end end diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 575d78d982..4529950b17 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -6,6 +6,8 @@ <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <%= f.error_notification %> + <%= hidden_field_tag :invitation_token, params[:invitation_token] if params[:invitation_token].present? %> +
    <%= f.invisible_captcha :osuny_verification %> From adf9c54936a68faa0d6b4cbce89654411ba441a7 Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 16:16:20 +0200 Subject: [PATCH 09/18] adjust preset loca --- app/models/communication/extranet/localization.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/communication/extranet/localization.rb b/app/models/communication/extranet/localization.rb index e7a762484c..f408f392ff 100644 --- a/app/models/communication/extranet/localization.rb +++ b/app/models/communication/extranet/localization.rb @@ -90,11 +90,11 @@ def prevent_unpublishing_default_language end def set_default_invitation_messages - self.invitation_message_automatic_subject = I18n.t('mailers.extranet.invitation_messages.automatic.subject') if self.invitation_message_automatic_subject.blank? - self.invitation_message_automatic_text = I18n.t('mailers.extranet.invitation_messages.automatic.text') if self.invitation_message_automatic_text.blank? - self.invitation_message_manual_subject = I18n.t('mailers.extranet.invitation_messages.manual.subject') if self.invitation_message_manual_subject.blank? - self.invitation_message_manual_text = I18n.t('mailers.extranet.invitation_messages.manual.text') if self.invitation_message_manual_text.blank? - self.invitation_message_manual_signature = I18n.t('mailers.extranet.invitation_messages.manual.signature') if self.invitation_message_manual_signature.blank? + self.invitation_message_automatic_subject = I18n.t('mailers.extranet.invitation_messages.automatic.subject', locale: language.iso_code) if self.invitation_message_automatic_subject.blank? + self.invitation_message_automatic_text = I18n.t('mailers.extranet.invitation_messages.automatic.text', locale: language.iso_code) if self.invitation_message_automatic_text.blank? + self.invitation_message_manual_subject = I18n.t('mailers.extranet.invitation_messages.manual.subject', locale: language.iso_code) if self.invitation_message_manual_subject.blank? + self.invitation_message_manual_text = I18n.t('mailers.extranet.invitation_messages.manual.text', locale: language.iso_code) if self.invitation_message_manual_text.blank? + self.invitation_message_manual_signature = I18n.t('mailers.extranet.invitation_messages.manual.signature', locale: language.iso_code) if self.invitation_message_manual_signature.blank? end end From 7cef6c5896d7007387dd0b5ac4965918c1f4b4e2 Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 17:13:41 +0200 Subject: [PATCH 10/18] adjust wording --- .../alumni/persons/invitations_controller.rb | 4 ++-- .../users/registrations_controller.rb | 10 ++++---- app/mailers/extranet_mailer.rb | 2 +- .../communication/extranet/invitation.rb | 4 +--- app/models/university/person/with_alumnus.rb | 2 +- .../alumni/persons/invitations/_cta.html.erb | 24 +++++++++---------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/controllers/extranet/alumni/persons/invitations_controller.rb b/app/controllers/extranet/alumni/persons/invitations_controller.rb index 0b6cb96ca6..f199db23fb 100644 --- a/app/controllers/extranet/alumni/persons/invitations_controller.rb +++ b/app/controllers/extranet/alumni/persons/invitations_controller.rb @@ -43,7 +43,7 @@ def breadcrumb end def find_person - @person = current_extranet.about.university_person_alumni.find(params[:id]) + @person = current_extranet.alumni.find(params[:id]) @l10n = @person.best_localization_for(current_language) end @@ -56,7 +56,7 @@ def ensure_person_is_invitable def default_message current_extranet_l10n.invitation_manual_text( from_name: current_user.to_s, - from_promotion: current_user.person&.promotion, + from_promotion: current_user.person&.diploma_years_sentence, to_name: @l10n.to_s ) end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 6e1fa44b60..f34e035227 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -36,11 +36,13 @@ def build_resource(hash = {}) def prefill_from_invitation(resource) return if invitation.blank? - resource.email = invitation.to_email - resource.mobile_phone = invitation.person&.phone_mobile person_l10n = invitation.person&.best_localization_for(current_language) - resource.first_name = person_l10n&.first_name - resource.last_name = person_l10n&.last_name + resource.assign_attributes( + first_name: person_l10n&.first_name, + last_name: person_l10n&.last_name, + email: invitation.to_email, + mobile_phone: invitation.person&.phone_mobile + ) end def invitation diff --git a/app/mailers/extranet_mailer.rb b/app/mailers/extranet_mailer.rb index 21c0dfaac0..c5e4004514 100644 --- a/app/mailers/extranet_mailer.rb +++ b/app/mailers/extranet_mailer.rb @@ -47,7 +47,7 @@ def invitation_message_manual(invitation) def invitation_manual_subject(extranet_l10n) extranet_l10n.invitation_manual_subject( from_name: @invitation.from_name, - from_promotion: @invitation.user.person&.promotion, + from_promotion: @invitation.user.person&.diploma_years_sentence, to_name: @invitation.to_name ) end diff --git a/app/models/communication/extranet/invitation.rb b/app/models/communication/extranet/invitation.rb index c6188322ef..c2a8115448 100644 --- a/app/models/communication/extranet/invitation.rb +++ b/app/models/communication/extranet/invitation.rb @@ -53,9 +53,7 @@ def self.sendable_to?(person) private def can_send_to_person - unless self.class.sendable_to?(person) - errors.add(:to_email, :too_soon) - end + errors.add(:to_email, :too_soon) unless self.class.sendable_to?(person) end def send_invitation_email diff --git a/app/models/university/person/with_alumnus.rb b/app/models/university/person/with_alumnus.rb index 676486e104..35fb5a6a36 100644 --- a/app/models/university/person/with_alumnus.rb +++ b/app/models/university/person/with_alumnus.rb @@ -71,7 +71,7 @@ def for_alumni_account(with_account, extranet) end end - def promotion + def diploma_years_sentence diploma_years.map(&:year).compact.uniq.sort.join(', ') end diff --git a/app/views/extranet/alumni/persons/invitations/_cta.html.erb b/app/views/extranet/alumni/persons/invitations/_cta.html.erb index 23d0297500..235aab7b98 100644 --- a/app/views/extranet/alumni/persons/invitations/_cta.html.erb +++ b/app/views/extranet/alumni/persons/invitations/_cta.html.erb @@ -1,13 +1,13 @@ <% if person.user_id.blank? && person.email.blank? %> -

    - <%= t('extranet.alumni.invitations.send.label') %> -
    - <% if Communication::Extranet::Invitation.sendable_to?(person) %> - <%= link_to t('extranet.alumni.invitations.send.cta', name: l10n), - alumni_new_invitation_path(person), - class: 'btn btn-light btn-sm position-relative z-2' %> - <% else %> - <%= t('extranet.alumni.invitations.send.too_soon') %> - <% end %> -

    - <% end %> \ No newline at end of file +

    + <%= t('extranet.alumni.invitations.send.label') %> +
    + <% if Communication::Extranet::Invitation.sendable_to?(person) %> + <%= link_to t('extranet.alumni.invitations.send.cta', name: l10n), + alumni_new_invitation_path(person), + class: 'btn btn-light btn-sm position-relative z-2' %> + <% else %> + <%= t('extranet.alumni.invitations.send.too_soon') %> + <% end %> +

    +<% end %> \ No newline at end of file From b1728fd5180f9f0158f903c0febf2d397ccecd85 Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 17:17:59 +0200 Subject: [PATCH 11/18] adjust --- app/models/user/with_person.rb | 7 +------ app/models/user/with_registration_context.rb | 6 +++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/models/user/with_person.rb b/app/models/user/with_person.rb index 4796bed3fc..e4188141a3 100644 --- a/app/models/user/with_person.rb +++ b/app/models/user/with_person.rb @@ -10,7 +10,6 @@ module User::WithPerson delegate :experiences, to: :person - before_validation :assign_email_to_invited_person, if: :invitation after_save_commit :sync_person, if: :person after_create :find_or_create_person, unless: :server_admin? end @@ -35,6 +34,7 @@ def find_or_create_person person = invitation&.person || university.people.where(email: email).first_or_initialize person_l10n = person.localizations.find_by(language_id: university.default_language_id) person.user = self + person.email = email person.localizations_attributes = [ { id: person_l10n&.id, language_id: university.default_language_id, @@ -47,9 +47,4 @@ def find_or_create_person def sync_person User::SyncPersonJob.perform_later(self) end - - def assign_email_to_invited_person - # As this is done on before_validation, it will be rollbacked if the save fails - invitation.person.update_column(:email, email) - end end diff --git a/app/models/user/with_registration_context.rb b/app/models/user/with_registration_context.rb index eaa45ad7ac..73a53b5ca3 100644 --- a/app/models/user/with_registration_context.rb +++ b/app/models/user/with_registration_context.rb @@ -32,7 +32,7 @@ def extranet_access end def user_can_access_registration_context? - user_is_alumni? || user_is_contact? + user_is_alumni? || user_is_contact? || user_is_invited? end def user_is_alumni? @@ -43,6 +43,10 @@ def user_is_contact? registration_context.has_feature?(:contacts) && registration_context.connected_people.where(email: email).any? end + def user_is_invited? + invitation.present? && invitation.extranet_id == registration_context.id + end + def send_notification_to_admins return if server_admin? # ignore server admins to prevent spam during account replication wetween universities GroupNotificationMailer.new_registration(university, self).deliver_later From 525466cd1108ae48ea9ad29e04de9da524073eb9 Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 17:21:19 +0200 Subject: [PATCH 12/18] adjust --- .../alumni/persons/invitations_controller.rb | 2 +- app/mailers/extranet_mailer.rb | 2 +- app/models/communication/extranet/localization.rb | 12 ++++++------ config/locales/communication/en.yml | 2 +- config/locales/communication/fr.yml | 2 +- config/locales/en.yml | 4 ++-- config/locales/fr.yml | 4 ++-- test/mailers/previews/extranet_mailer_preview.rb | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/controllers/extranet/alumni/persons/invitations_controller.rb b/app/controllers/extranet/alumni/persons/invitations_controller.rb index f199db23fb..78222473d2 100644 --- a/app/controllers/extranet/alumni/persons/invitations_controller.rb +++ b/app/controllers/extranet/alumni/persons/invitations_controller.rb @@ -56,7 +56,7 @@ def ensure_person_is_invitable def default_message current_extranet_l10n.invitation_manual_text( from_name: current_user.to_s, - from_promotion: current_user.person&.diploma_years_sentence, + from_years: current_user.person&.diploma_years_sentence, to_name: @l10n.to_s ) end diff --git a/app/mailers/extranet_mailer.rb b/app/mailers/extranet_mailer.rb index c5e4004514..cdeb290f82 100644 --- a/app/mailers/extranet_mailer.rb +++ b/app/mailers/extranet_mailer.rb @@ -47,7 +47,7 @@ def invitation_message_manual(invitation) def invitation_manual_subject(extranet_l10n) extranet_l10n.invitation_manual_subject( from_name: @invitation.from_name, - from_promotion: @invitation.user.person&.diploma_years_sentence, + from_years: @invitation.user.person&.diploma_years_sentence, to_name: @invitation.to_name ) end diff --git a/app/models/communication/extranet/localization.rb b/app/models/communication/extranet/localization.rb index f408f392ff..dbb4291da6 100644 --- a/app/models/communication/extranet/localization.rb +++ b/app/models/communication/extranet/localization.rb @@ -63,21 +63,21 @@ def to_s "#{name}" end - def invitation_manual_subject(from_name:, from_promotion:, to_name:) - substitute_invitation_placeholders(invitation_message_manual_subject, from_name: from_name, from_promotion: from_promotion, to_name: to_name) + def invitation_manual_subject(from_name:, from_years:, to_name:) + substitute_invitation_placeholders(invitation_message_manual_subject, from_name: from_name, from_years: from_years, to_name: to_name) end - def invitation_manual_text(from_name:, from_promotion:, to_name:) - substitute_invitation_placeholders(invitation_message_manual_text, from_name: from_name, from_promotion: from_promotion, to_name: to_name) + def invitation_manual_text(from_name:, from_years:, to_name:) + substitute_invitation_placeholders(invitation_message_manual_text, from_name: from_name, from_years: from_years, to_name: to_name) end protected - def substitute_invitation_placeholders(text, from_name:, from_promotion:, to_name:) + def substitute_invitation_placeholders(text, from_name:, from_years:, to_name:) substitutions = { 'name' => to_name, 'sender_name' => from_name, - 'sender_promotion' => from_promotion + 'sender_years' => from_years } substitutions.each { |key, value| text = text.gsub("{{#{key}}}", value.to_s) } text diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml index cb23cb7ad4..2bd89a4c3e 100644 --- a/config/locales/communication/en.yml +++ b/config/locales/communication/en.yml @@ -536,7 +536,7 @@ en: automatic: Automatic message automatic_hint: This message will be sent after an alumni import or when clicking on the "send invitation" button on admin, if the alumnus has an email. manual: Manual message - manual_hint_html: "This message will be sent from the extranet, when a user clicks on the \"invite alumnus\" button, if the user has no email.
    In these fields you can use the following variables:
    • {{name}}: the name of the alumnus
    • {{sender_name}}: the name of the user sending the invitation
    • {{sender_promotion}}: the promotion of the user sending the invitation
    " + manual_hint_html: "This message will be sent from the extranet, when a user clicks on the \"invite alumnus\" button, if the user has no email.
    In these fields you can use the following variables:
    • {{name}}: the name of the alumnus
    • {{sender_name}}: the name of the user sending the invitation
    • {{sender_years}}: the promotion(s) of the user sending the invitation
    " title: Invitation messages languages: Languages website: diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml index 4487f3fee6..f4a78ca6e4 100644 --- a/config/locales/communication/fr.yml +++ b/config/locales/communication/fr.yml @@ -536,7 +536,7 @@ fr: automatic: Message automatique automatic_hint: Ce message sera envoyé après un import d'alumni ou lors du clic sur le bouton "envoyer l'invitation" dans l'admin, si l'alumnus a un email. manual: Message manuel - manual_hint_html: "Ce message sera envoyé depuis l'extranet, lorsqu'un utilisateur clique sur le bouton \"inviter un alumnus\", si l'utilisateur n'a pas d'email.
    Dans ces champs vous pouvez utiliser les variables suivantes :
    • {{name}} : le nom de l'alumnus
    • {{sender_name}} : le nom de l'utilisateur qui envoie l'invitation
    • {{sender_promotion}} : la promotion de l'utilisateur qui envoie l'invitation
    " + manual_hint_html: "Ce message sera envoyé depuis l'extranet, lorsqu'un utilisateur clique sur le bouton \"inviter un alumnus\", si l'utilisateur n'a pas d'email.
    Dans ces champs vous pouvez utiliser les variables suivantes :
    • {{name}} : le nom de l'alumnus
    • {{sender_name}} : le nom de l'utilisateur qui envoie l'invitation
    • {{sender_years}} : la/les promotion(s) de l'utilisateur qui envoie l'invitation
    " title: Messages d'invitation languages: Langues website: diff --git a/config/locales/en.yml b/config/locales/en.yml index afe4937aa3..e7cc3ab98a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -506,8 +506,8 @@ en: manual: cta: Join the extranet signature: Yours - subject: Invitation from {{sender_name}} (promotion {{sender_promotion}}) to join the extranet - text: "Hello {{name}},\n\n{{sender_name}} invites you to join the Alumni extranet.\n\nYou will find, in particular, the directory of all promotions.\nSee you soon!\n\n{{sender_name}} (promotion {{sender_promotion}})" + subject: Invitation from {{sender_name}} (promotion {{sender_years}}) to join the extranet + text: "Hello {{name}},\n\n{{sender_name}} invites you to join the Alumni extranet.\n\nYou will find, in particular, the directory of all promotions.\nSee you soon!\n\n{{sender_name}} (promotion {{sender_years}})" notifications: gdpr_deletion_incoming: subject: "osuny - your account will soon be deleted" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0af31c15e9..0e0e45df8f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -507,8 +507,8 @@ fr: manual: cta: Rejoindre l'extranet signature: Cordialement - subject: Invitation de {{sender_name}} (promotion {{sender_promotion}}) à rejoindre l'extranet - text: "Bonjour {{name}},\n\n{{sender_name}} vous invite à rejoindre l'extranet des Alumni.\n\nVous y retrouverez notamment l'annuaire de toutes les promotions.\nÀ très bientôt !\n\n{{sender_name}} (promotion {{sender_promotion}})" + subject: Invitation de {{sender_name}} (promotion {{sender_years}}) à rejoindre l'extranet + text: "Bonjour {{name}},\n\n{{sender_name}} vous invite à rejoindre l'extranet des Alumni.\n\nVous y retrouverez notamment l'annuaire de toutes les promotions.\nÀ très bientôt !\n\n{{sender_name}} (promotion {{sender_years}})" notifications: gdpr_deletion_incoming: subject: "osuny - votre compte va bientôt être supprimé" diff --git a/test/mailers/previews/extranet_mailer_preview.rb b/test/mailers/previews/extranet_mailer_preview.rb index e0b0550363..afc0294d3d 100644 --- a/test/mailers/previews/extranet_mailer_preview.rb +++ b/test/mailers/previews/extranet_mailer_preview.rb @@ -26,7 +26,7 @@ def invitation to_email: "guest@noesya.coop", message: l10n.invitation_manual_text( from_name: user.to_s, - from_promotion: '2020', + from_years: '2020', to_name: person.to_s ) ) From 88e937cc272b4e8a2f4aefc6be59b4d78d71d103 Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 17:24:57 +0200 Subject: [PATCH 13/18] signature now do substitutions too --- app/mailers/extranet_mailer.rb | 14 +++++++++++--- app/models/communication/extranet/localization.rb | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/mailers/extranet_mailer.rb b/app/mailers/extranet_mailer.rb index cdeb290f82..06a176d042 100644 --- a/app/mailers/extranet_mailer.rb +++ b/app/mailers/extranet_mailer.rb @@ -27,7 +27,7 @@ def invitation_message_manual(invitation) @university = extranet.university language = @invitation.person&.user&.language || @university.default_language extranet_l10n = extranet.best_localization_for(language) - @signature = extranet_l10n.invitation_message_manual_signature + @signature = invitation_manual_signature(extranet_l10n) @registration_url = new_user_registration_url( host: extranet.host, invitation_token: @invitation.token @@ -45,11 +45,19 @@ def invitation_message_manual(invitation) private def invitation_manual_subject(extranet_l10n) - extranet_l10n.invitation_manual_subject( + extranet_l10n.invitation_manual_subject(**invitation_substitutions) + end + + def invitation_manual_signature(extranet_l10n) + extranet_l10n.invitation_manual_signature(**invitation_substitutions) + end + + def invitation_substitutions + { from_name: @invitation.from_name, from_years: @invitation.user.person&.diploma_years_sentence, to_name: @invitation.to_name - ) + } end diff --git a/app/models/communication/extranet/localization.rb b/app/models/communication/extranet/localization.rb index dbb4291da6..aad97c1501 100644 --- a/app/models/communication/extranet/localization.rb +++ b/app/models/communication/extranet/localization.rb @@ -71,6 +71,10 @@ def invitation_manual_text(from_name:, from_years:, to_name:) substitute_invitation_placeholders(invitation_message_manual_text, from_name: from_name, from_years: from_years, to_name: to_name) end + def invitation_manual_signature(from_name:, from_years:, to_name:) + substitute_invitation_placeholders(invitation_message_manual_signature, from_name: from_name, from_years: from_years, to_name: to_name) + end + protected def substitute_invitation_placeholders(text, from_name:, from_years:, to_name:) From ac02f97db50b967480c6d1aad6f1669920fc77ed Mon Sep 17 00:00:00 2001 From: pabois Date: Thu, 6 Aug 2026 17:36:17 +0200 Subject: [PATCH 14/18] restreint invitations to person without user --- app/controllers/users/registrations_controller.rb | 2 +- app/models/communication/extranet/invitation.rb | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index f34e035227..cb22cb3712 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -48,7 +48,7 @@ def prefill_from_invitation(resource) def invitation return @invitation if defined?(@invitation) token = params[:invitation_token] - @invitation = token.present? ? current_extranet&.invitations&.find_by(token: token) : nil + @invitation = token.present? ? current_extranet&.invitations&.pending&.find_by(token: token) : nil end def sign_up(resource_name, resource) diff --git a/app/models/communication/extranet/invitation.rb b/app/models/communication/extranet/invitation.rb index c2a8115448..0843230ba3 100644 --- a/app/models/communication/extranet/invitation.rb +++ b/app/models/communication/extranet/invitation.rb @@ -46,7 +46,10 @@ class Communication::Extranet::Invitation < ApplicationRecord after_create_commit :send_invitation_email + scope :pending, -> { joins(:person).where(university_people: { user_id: nil }) } + def self.sendable_to?(person) + return false if person.user.present? self.where(person: person).where('created_at >= ?', University::Person::WithAlumnus::DELAY_FOR_INVITATION.ago).none? end From 3a7b6b32440c4c97f8f3fbbf6da8a28e293d68ee Mon Sep 17 00:00:00 2001 From: pabois Date: Fri, 7 Aug 2026 12:53:40 +0200 Subject: [PATCH 15/18] delete file --- mise.toml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 mise.toml diff --git a/mise.toml b/mise.toml deleted file mode 100644 index 8478956175..0000000000 --- a/mise.toml +++ /dev/null @@ -1,2 +0,0 @@ -[tools] -ruby = "4.0.6" From 818ab070703dbee586fd8fb3ec9a1cb7d16eeb2c Mon Sep 17 00:00:00 2001 From: pabois Date: Fri, 7 Aug 2026 12:54:37 +0200 Subject: [PATCH 16/18] change gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7a0e072c2c..9bb47e7663 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ coverage # Ignore editors .vscode +mise.toml # Instruments IAc AGENTS.md From 730953adcc24620cecfa9e7183b8897796b4ea15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Andr=C3=A9=20Boissinot?= Date: Fri, 7 Aug 2026 12:55:10 +0200 Subject: [PATCH 17/18] Update config/locales/fr.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Gaya --- config/locales/fr.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0e0e45df8f..fe1c97c2e1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -508,7 +508,15 @@ fr: cta: Rejoindre l'extranet signature: Cordialement subject: Invitation de {{sender_name}} (promotion {{sender_years}}) à rejoindre l'extranet - text: "Bonjour {{name}},\n\n{{sender_name}} vous invite à rejoindre l'extranet des Alumni.\n\nVous y retrouverez notamment l'annuaire de toutes les promotions.\nÀ très bientôt !\n\n{{sender_name}} (promotion {{sender_years}})" + text: | + Bonjour {{name}}, + + {{sender_name}} vous invite à rejoindre l'extranet des Alumni. + + Vous y retrouverez notamment l'annuaire de toutes les promotions. + À très bientôt ! + + {{sender_name}} (promotion {{sender_years}}) notifications: gdpr_deletion_incoming: subject: "osuny - votre compte va bientôt être supprimé" From b1d020ac10281db68a5493ec47bb03d168c3fab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Andr=C3=A9=20Boissinot?= Date: Fri, 7 Aug 2026 12:55:17 +0200 Subject: [PATCH 18/18] Update config/locales/en.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Gaya --- config/locales/en.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index e7cc3ab98a..acbc25788e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -507,7 +507,15 @@ en: cta: Join the extranet signature: Yours subject: Invitation from {{sender_name}} (promotion {{sender_years}}) to join the extranet - text: "Hello {{name}},\n\n{{sender_name}} invites you to join the Alumni extranet.\n\nYou will find, in particular, the directory of all promotions.\nSee you soon!\n\n{{sender_name}} (promotion {{sender_years}})" + text: | + Hello {{name}}, + + {{sender_name}} invites you to join the Alumni extranet. + + You will find, in particular, the directory of all promotions. + See you soon! + + {{sender_name}} (promotion {{sender_years}})" notifications: gdpr_deletion_incoming: subject: "osuny - your account will soon be deleted"