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 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/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/controllers/extranet/alumni/persons/invitations_controller.rb b/app/controllers/extranet/alumni/persons/invitations_controller.rb new file mode 100644 index 0000000000..78222473d2 --- /dev/null +++ b/app/controllers/extranet/alumni/persons/invitations_controller.rb @@ -0,0 +1,64 @@ +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: default_message + ) + 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.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 + + def default_message + current_extranet_l10n.invitation_manual_text( + from_name: current_user.to_s, + from_years: current_user.person&.diploma_years_sentence, + to_name: @l10n.to_s + ) + 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/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 6e6d6452a4..cb22cb3712 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,28 @@ def edit protected + def build_resource(hash = {}) + super + resource.invitation = invitation + end + + def prefill_from_invitation(resource) + return if invitation.blank? + person_l10n = invitation.person&.best_localization_for(current_language) + 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 + return @invitation if defined?(@invitation) + token = params[:invitation_token] + @invitation = token.present? ? current_extranet&.invitations&.pending&.find_by(token: token) : nil + end + def sign_up(resource_name, resource) sign_in(resource, event: :authentication) end diff --git a/app/mailers/extranet_mailer.rb b/app/mailers/extranet_mailer.rb index 6fbab626cc..06a176d042 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 @@ -17,8 +17,48 @@ 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 + 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 = invitation_manual_signature(extranet_l10n) + @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(**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 + + end 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 1435b2870a..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 @@ -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/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 new file mode 100644 index 0000000000..0843230ba3 --- /dev/null +++ b/app/models/communication/extranet/invitation.rb @@ -0,0 +1,66 @@ +# == 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 +# token :string uniquely indexed +# 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_token (token) UNIQUE +# 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 HasUniversity + include Sanitizable + + has_secure_token :token + + 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 + + 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 + + private + + def can_send_to_person + errors.add(:to_email, :too_soon) unless self.class.sendable_to?(person) + end + + def send_invitation_email + ExtranetMailer.invitation_message_manual(self).deliver_later + end + +end diff --git a/app/models/communication/extranet/localization.rb b/app/models/communication/extranet/localization.rb index a9f30cea87..aad97c1501 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 } @@ -60,17 +63,42 @@ def to_s "#{name}" end + 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_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:) + substitutions = { + 'name' => to_name, + 'sender_name' => from_name, + 'sender_years' => from_years + } + 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? 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', 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 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/university/person/with_alumnus.rb b/app/models/university/person/with_alumnus.rb index 58a4e0fcd8..35fb5a6a36 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', @@ -9,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 @@ -69,6 +71,10 @@ def for_alumni_account(with_account, extranet) end end + def diploma_years_sentence + 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 = [] @@ -83,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/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/app/models/user/with_person.rb b/app/models/user/with_person.rb index 7b3bd544f8..e4188141a3 100644 --- a/app/models/user/with_person.rb +++ b/app/models/user/with_person.rb @@ -5,6 +5,9 @@ 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 after_save_commit :sync_person, if: :person @@ -28,9 +31,10 @@ 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.email = email person.localizations_attributes = [ { id: person_l10n&.id, language_id: university.default_language_id, 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 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/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/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/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/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? %> +
+ <%= 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 %>
+
- <%= experience.person.best_localization_for(current_language) %>
+ <%= person_l10n %>
<%= [l10n&.description, organization_l10n&.to_s].compact_blank.join('-') %>
- <%= 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/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/communication/en.yml b/config/locales/communication/en.yml index 854a535f40..2bd89a4c3e 100644 --- a/config/locales/communication/en.yml +++ b/config/locales/communication/en.yml @@ -56,13 +56,21 @@ 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) 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 @@ -337,6 +345,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: @@ -520,6 +532,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.