diff --git a/Gemfile b/Gemfile
index 2cc529457..9d34b0ea0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -57,6 +57,7 @@ gem "leaflet-rails"
gem "libretranslate"#, path: "../libretranslate"
gem "lucide-rails"
gem "metainspector", git: "https://github.com/jaimeiniesta/metainspector.git"
+gem "net-ftp"
gem "octokit"
gem "omniauth-saml", "~> 2"
gem "orthotypo"
diff --git a/Gemfile.lock b/Gemfile.lock
index 8ba4fc770..2c527f4fa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -457,6 +457,9 @@ GEM
namae (1.2.0)
racc (~> 1.7)
nesty (1.0.2)
+ net-ftp (0.3.9)
+ net-protocol
+ time
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.6)
@@ -835,6 +838,7 @@ DEPENDENCIES
lucide-rails
metainspector!
mutex_m (~> 0.3.0)
+ net-ftp
octokit
omniauth-saml (~> 2)
orthotypo
diff --git a/Procfile b/Procfile
index 6faf2d0c9..65cf01646 100644
--- a/Procfile
+++ b/Procfile
@@ -2,6 +2,7 @@ web: bundle exec puma -C config/puma.rb
worker: bundle exec good_job start --queues="default"
miceworker: bundle exec good_job start --queues="mice" --max-threads=2
catsworker: bundle exec good_job start --queues="cats" --max-threads=2
+donkeysworker: bundle exec good_job start --queues="donkeys" --max-threads=1
elephantsworker: bundle exec good_job start --queues="elephants" --max-threads=1
whalesworker: bundle exec good_job start --queues="whales" --max-threads=1
unicornsworker: bundle exec good_job start --queues="unicorns" --max-threads=1
diff --git a/app/controllers/admin/communication/library/files/redirections_controller.rb b/app/controllers/admin/communication/library/files/redirections_controller.rb
new file mode 100644
index 000000000..7232f144b
--- /dev/null
+++ b/app/controllers/admin/communication/library/files/redirections_controller.rb
@@ -0,0 +1,68 @@
+class Admin::Communication::Library::Files::RedirectionsController < Admin::Communication::Library::Files::ApplicationController
+ before_action :load_and_authorize
+
+ def index
+ end
+
+ # Modifie la propriété `file_server_slug` de la localisation de fichier
+ # Conceptuellement, ça appartient plutôt à `FilesController`, mais :
+ # - il est bien encombré déjà
+ # - c'est utilisé par `FileServerApp.vue`
+ # - ça doit render l'index pour renvoyer des données à jour (avec le nouvel alias)
+ def change_server_slug
+ previous_path = @l10n.file_server_path
+ @l10n.file_server_slug = params[:file_server_slug]
+ if @l10n.save(context: :redirection)
+ # Si une redirection existait avec le nouveau chemin, on la supprime.
+ # Les permaliens priment toujours sur les redirections.
+ Communication::File::Redirection.remove(@l10n.university, @l10n.file_server_path)
+ # Le précédent permalien devient une redirection.
+ Communication::File::Redirection.add(@l10n, previous_path)
+ render :index
+ else
+ render_error(@l10n)
+ end
+ end
+
+ def create
+ @redirection = @l10n.redirections.create(
+ path_without_extension: params[:path_without_extension],
+ university: @l10n.university,
+ )
+ if @redirection.persisted?
+ render :index
+ else
+ render_error(@redirection)
+ end
+ end
+
+ def destroy
+ @redirection = @l10n.redirections.find(params[:id])
+ if @redirection.destroy
+ render :index
+ else
+ render_error(@redirection)
+ end
+ end
+
+ def static
+ @about = @file
+ render layout: false,
+ content_type: "text/plain; charset=utf-8"
+ end
+
+ protected
+
+ def load_and_authorize
+ @file = current_university.communication_files.find(params[:file_id])
+ authorize! :create, @file
+ @l10n = @file.localization_for(current_language)
+ end
+
+ def render_error(redirection)
+ render json: {
+ error: redirection.errors.full_messages.to_sentence,
+ status: 400
+ }, status: 400
+ end
+end
diff --git a/app/controllers/server/universities_controller.rb b/app/controllers/server/universities_controller.rb
index 96b4867aa..6bbb70792 100644
--- a/app/controllers/server/universities_controller.rb
+++ b/app/controllers/server/universities_controller.rb
@@ -83,9 +83,13 @@ def university_params
:has_sso, :sso_target_url, :sso_cert, :sso_name_identifier_format, :sso_mapping, :sso_button_label,
:invoice_date, :contribution_amount,
:is_really_a_university,
- :default_language_id, language_ids: []
+ :default_language_id, language_ids: [],
+ file_server_attributes: [:id, :url, :ftp_host, :ftp_port, :ftp_username, :ftp_password, :ftp_path]
]
attribute_names << :default_github_access_token unless params[:university][:default_github_access_token].blank?
+ if params[:university][:file_server_attributes]&.[](:ftp_password).blank?
+ params[:university][:file_server_attributes]&.delete(:ftp_password)
+ end
params.require(:university).permit(attribute_names)
end
diff --git a/app/javascript/apps/file-server/FileServerApp.vue b/app/javascript/apps/file-server/FileServerApp.vue
new file mode 100644
index 000000000..604254378
--- /dev/null
+++ b/app/javascript/apps/file-server/FileServerApp.vue
@@ -0,0 +1,198 @@
+
+
+
+
+ {{ $t('fileServer.permalink.title') }}
+
+
+
+
+ {{ data.file_server?.base_url }}
+
+
+
+ {{ data.file_server?.extension }}
+
+
+
+
+
+
+
+ {{ $t('fileServer.redirections.title') }}
+
+
+
+
+
+ {{ redirection.url }}
+
+
+
+
+
+
+
+
+
+
+ {{ data.redirections?.base_url }}
+
+
+
+ {{ data.file_server?.extension }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/apps/index.js b/app/javascript/apps/index.js
index b149db945..42ba37569 100644
--- a/app/javascript/apps/index.js
+++ b/app/javascript/apps/index.js
@@ -2,6 +2,7 @@ import { createApp } from 'vue';
import { getI18n } from './i18n';
import BlocksEditorApp from './blocks-editor/BlocksEditorApp.vue';
import DownloadableSummaryApp from './downloadable-summary/DownloadableSummaryApp.vue';
+import FileServerApp from './file-server/FileServerApp.vue';
import FeaturedMediaApp from './featured-media/FeaturedMediaApp.vue';
import PickerTestApp from './picker/PickerTestApp.vue';
import SsoMappingApp from './sso-mapping/SsoMappingApp.vue';
@@ -19,6 +20,7 @@ async function boot() {
mount(BlocksEditorApp, '#blocks-editor-app');
mount(DownloadableSummaryApp, '#downloadable-summary-app');
mount(FeaturedMediaApp, '#featured-media-app');
+ mount(FileServerApp, '#file-server-app');
mount(PickerTestApp, '#picker-test-app');
mount(SsoMappingApp, '#sso-mapping-app');
mount(TimeSlotsApp, '#time-slots-app');
diff --git a/app/jobs/communication/file/synchronize_with_file_server_job.rb b/app/jobs/communication/file/synchronize_with_file_server_job.rb
new file mode 100644
index 000000000..43b566be7
--- /dev/null
+++ b/app/jobs/communication/file/synchronize_with_file_server_job.rb
@@ -0,0 +1,7 @@
+class Communication::File::SynchronizeWithFileServerJob < ApplicationJob
+ queue_as :donkeys
+
+ def perform(file_localization)
+ file_localization.sync_to_file_server_safely
+ end
+end
diff --git a/app/models/communication/file/localization.rb b/app/models/communication/file/localization.rb
index 51f862a4b..25ec71578 100644
--- a/app/models/communication/file/localization.rb
+++ b/app/models/communication/file/localization.rb
@@ -2,29 +2,31 @@
#
# Table name: communication_file_localizations
#
-# id :uuid not null, primary key
-# deleted_at :datetime
-# featured_image_credit :text
-# featured_media_alt :string
-# internal_description :text
-# meta_description :text
-# name :string
-# original_byte_size :bigint
-# original_checksum :string
-# original_content_type :string
-# original_extension :string default("")
-# original_filename :string
-# published :boolean default(FALSE)
-# published_at :datetime
-# slug :string
-# created_at :datetime not null
-# updated_at :datetime not null
-# about_id :uuid not null, indexed
-# featured_media_id :uuid indexed
-# language_id :uuid not null, indexed
-# original_blob_id :uuid not null, indexed
-# university_id :uuid not null, indexed
-# updated_by_id :uuid indexed
+# id :uuid not null, primary key
+# deleted_at :datetime
+# featured_image_credit :text
+# featured_media_alt :string
+# file_server_current_path :string
+# file_server_slug :string
+# internal_description :text
+# meta_description :text
+# name :string
+# original_byte_size :bigint
+# original_checksum :string
+# original_content_type :string
+# original_extension :string default("")
+# original_filename :string
+# published :boolean default(FALSE)
+# published_at :datetime
+# slug :string
+# created_at :datetime not null
+# updated_at :datetime not null
+# about_id :uuid not null, indexed
+# featured_media_id :uuid indexed
+# language_id :uuid not null, indexed
+# original_blob_id :uuid not null, indexed
+# university_id :uuid not null, indexed
+# updated_by_id :uuid indexed
#
# Indexes
#
@@ -59,6 +61,8 @@ class Communication::File::Localization < ApplicationRecord
include Permalinkable
include Publishable
include Sanitizable
+ include WithRedirections
+ include WithFileServer
include WithOpenApi
belongs_to :updated_by,
diff --git a/app/models/communication/file/localization/with_file_server.rb b/app/models/communication/file/localization/with_file_server.rb
new file mode 100644
index 000000000..f1638f3ec
--- /dev/null
+++ b/app/models/communication/file/localization/with_file_server.rb
@@ -0,0 +1,146 @@
+module Communication::File::Localization::WithFileServer
+ extend ActiveSupport::Concern
+
+ included do
+ validate :file_server_slug_available,
+ on: :redirection
+ validates :file_server_slug,
+ format: {
+ with: /\A[a-z0-9\-]+\z/,
+ message: I18n.t('slug_error')
+ },
+ on: :redirection
+
+ before_validation :set_file_server_slug_if_empty?
+ after_save :sync_to_file_server
+ end
+
+ def sync_to_file_server
+ return unless university.file_server?
+ Communication::File::SynchronizeWithFileServerJob.perform_later(self)
+ end
+
+ def sync_to_file_server_safely
+ if file_server_current_path.blank?
+ ftp.send_blob(
+ original_blob,
+ file_server_remote_directory,
+ file_server_filename
+ )
+ else
+ ftp.move(
+ file_server.ftp_path,
+ file_server_current_path,
+ file_server_path
+ )
+ end
+ update_column :file_server_current_path,
+ file_server_path
+ send_htaccess
+ end
+
+ # rapport-annuel
+ # file_server_slug
+
+ # .pdf
+ def file_server_extension
+ "#{original_extension}"
+ end
+
+ # rapport-annuel.pdf
+ def file_server_filename
+ "#{file_server_slug}#{file_server_extension}"
+ end
+
+ # /fr/2026/
+ def file_server_directory
+ "/#{language.iso_code}/#{created_at.year}/"
+ end
+
+ # /fr/2026/rapport-annuel.pdf
+ def file_server_path
+ "#{file_server_directory}#{file_server_filename}"
+ end
+
+ # /fr/2026/rapport-annuel
+ def file_server_path_without_extension
+ "#{file_server_directory}#{file_server_slug}"
+ end
+
+ # /path-on-ftp-server/fr/2026/
+ def file_server_remote_directory
+ "#{file_server.ftp_path}#{file_server_directory}".gsub('//', '/')
+ end
+
+ # /path-on-ftp-server/fr/2026/rapport-annuel.pdf
+ def file_server_remote_path
+ "#{file_server.ftp_path}#{file_server_path}".gsub('//', '/')
+ end
+
+ # https://files.osuny.org/
+ def file_server_base_url
+ "#{file_server.url}/"
+ end
+ # https://files.osuny.org/fr/2026/
+ def file_server_base_directory_url
+ "#{file_server.url}#{file_server_directory}"
+ end
+
+ # https://files.osuny.org/fr/2026/rapport-annuel.pdf
+ def file_server_url
+ return unless university.file_server?
+ "#{file_server.url}#{file_server_path}"
+ end
+
+ protected
+
+ def send_htaccess
+ ftp.send_text(htaccess_content, htaccess_path)
+ end
+
+ def htaccess_content
+ Static.render(htaccess_template_static, self, nil)
+ end
+
+ def htaccess_template_static
+ 'admin/communication/library/files/redirections/static'
+ end
+
+ def htaccess_path
+ "#{file_server.ftp_path}.htaccess"
+ end
+
+ def set_file_server_slug_if_empty?
+ return if file_server_slug.present?
+ self.file_server_slug = slug
+ end
+
+ def ftp
+ @ftp ||= ::Ftp.new(
+ file_server.ftp_host,
+ file_server.ftp_port,
+ file_server.ftp_username,
+ file_server.ftp_password
+ )
+ end
+
+ def file_server
+ @file_server ||= university.file_server
+ end
+
+ def file_server_slug_available
+ taken = self.class
+ .unscoped
+ .where(
+ university_id: university_id,
+ file_server_slug: file_server_slug
+ )
+ .where(
+ "date_part('year', created_at) = ?",
+ created_at&.year
+ )
+ .where.not(id: id)
+ .exists?
+ errors.add(:file_server_slug, :taken) if taken
+ end
+end
diff --git a/app/models/communication/file/localization/with_redirections.rb b/app/models/communication/file/localization/with_redirections.rb
new file mode 100644
index 000000000..cc8d0f2fa
--- /dev/null
+++ b/app/models/communication/file/localization/with_redirections.rb
@@ -0,0 +1,10 @@
+module Communication::File::Localization::WithRedirections
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :redirections,
+ class_name: 'Communication::File::Redirection',
+ foreign_key: :communication_file_localization_id,
+ dependent: :destroy
+ end
+end
\ No newline at end of file
diff --git a/app/models/communication/file/redirection.rb b/app/models/communication/file/redirection.rb
new file mode 100644
index 000000000..ccf79f6ca
--- /dev/null
+++ b/app/models/communication/file/redirection.rb
@@ -0,0 +1,94 @@
+# == Schema Information
+#
+# Table name: communication_file_redirections
+#
+# id :uuid not null, primary key
+# path :string
+# created_at :datetime not null
+# updated_at :datetime not null
+# communication_file_localization_id :uuid not null, indexed
+# university_id :uuid not null, indexed
+#
+# Indexes
+#
+# idx_on_communication_file_localization_id_b5293392d9 (communication_file_localization_id)
+# index_communication_file_redirections_on_university_id (university_id)
+#
+# Foreign Keys
+#
+# fk_rails_154fa850da (university_id => universities.id)
+# fk_rails_f6bf714890 (communication_file_localization_id => communication_file_localizations.id)
+#
+class Communication::File::Redirection < ApplicationRecord
+ include HasUniversity
+
+ attr_accessor :path_without_extension
+
+ belongs_to :communication_file_localization,
+ class_name: 'Communication::File::Localization'
+ alias :file_l10n :communication_file_localization
+
+ scope :ordered, -> { order(created_at: :desc) }
+ scope :current, -> { where(is_current: true) }
+ scope :not_current, -> { where(is_current: false) }
+
+ before_validation :set_path
+
+ validate :path_available
+ validates :path, presence: true
+ validates :path_without_extension,
+ format: {
+ with: /\A[a-z0-9\-\/]+\z/,
+ message: I18n.t('slug_error')
+ }
+
+ def self.add(l10n, path)
+ create(
+ communication_file_localization_id: l10n.id,
+ university_id: l10n.university_id,
+ path: path
+ )
+ end
+
+ def self.remove(university, path)
+ where(
+ university_id: university.id,
+ path: path
+ ).destroy_all
+ end
+
+ def path_without_extension
+ self.path.to_s.delete_suffix extension
+ end
+
+ def url
+ "#{university.file_server.url}#{path}"
+ end
+
+ def extension
+ communication_file_localization&.original_extension
+ end
+
+ def to_s
+ "#{path}"
+ end
+
+ protected
+
+ def path_available
+ taken = self.class
+ .unscoped
+ .where(
+ university_id: university_id,
+ path: path
+ )
+ .where.not(id: id)
+ .exists?
+ errors.add(:path, :taken) if taken
+ end
+
+ def set_path
+ return if path.present?
+ self.path = "/#{@path_without_extension}#{extension}"
+ end
+end
diff --git a/app/models/university.rb b/app/models/university.rb
index 3502b0d00..0e4d002e7 100644
--- a/app/models/university.rb
+++ b/app/models/university.rb
@@ -49,6 +49,7 @@ class University < ApplicationRecord
include WithCommunication
include HasCountry
include WithEducation
+ include WithFileServer
include WithGithub
include WithIdentifier
include WithInvoice
diff --git a/app/models/university/file_server.rb b/app/models/university/file_server.rb
new file mode 100644
index 000000000..a66561c37
--- /dev/null
+++ b/app/models/university/file_server.rb
@@ -0,0 +1,33 @@
+# == Schema Information
+#
+# Table name: university_file_servers
+#
+# id :uuid not null, primary key
+# ftp_host :string
+# ftp_password :string
+# ftp_path :string
+# ftp_port :integer
+# ftp_username :string
+# url :string
+# created_at :datetime not null
+# updated_at :datetime not null
+# university_id :uuid uniquely indexed
+#
+# Indexes
+#
+# index_university_file_servers_on_university_id (university_id) UNIQUE
+#
+# Foreign Keys
+#
+# fk_rails_2bd65e6c37 (university_id => universities.id)
+#
+class University::FileServer < ApplicationRecord
+ belongs_to :university
+
+ def correct?
+ ftp_host.present? &&
+ ftp_port.present? &&
+ ftp_username.present? &&
+ ftp_password.present?
+ end
+end
diff --git a/app/models/university/with_communication.rb b/app/models/university/with_communication.rb
index 0e5f6c7a0..95c722ca5 100644
--- a/app/models/university/with_communication.rb
+++ b/app/models/university/with_communication.rb
@@ -22,6 +22,11 @@ module University::WithCommunication
dependent: :destroy
alias_method :file_localizations, :communication_file_localizations
+ has_many :communication_file_redirections,
+ class_name: 'Communication::File::Redirection',
+ dependent: :destroy
+ alias_method :file_redirections, :communication_file_redirections
+
has_many :communication_file_categories,
class_name: 'Communication::File::Category',
dependent: :destroy
diff --git a/app/models/university/with_file_server.rb b/app/models/university/with_file_server.rb
new file mode 100644
index 000000000..1a2c6ec78
--- /dev/null
+++ b/app/models/university/with_file_server.rb
@@ -0,0 +1,13 @@
+module University::WithFileServer
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :file_server
+
+ accepts_nested_attributes_for :file_server, reject_if: :all_blank
+ end
+
+ def file_server?
+ file_server.present? && file_server.correct?
+ end
+end
diff --git a/app/services/ftp.rb b/app/services/ftp.rb
new file mode 100644
index 000000000..16a689c96
--- /dev/null
+++ b/app/services/ftp.rb
@@ -0,0 +1,71 @@
+class Ftp
+ attr_reader :host, :port, :username, :password
+
+ def initialize(host, port, username, password)
+ @host = host
+ @port = port
+ @username = username
+ @password = password
+ end
+
+ def send_blob(blob, directory, filename)
+ manage_directory(directory)
+ send(blob, filename)
+ rescue => e
+ Rails.logger.error("Échec de la synchronisation FTP vers #{host.inspect} : #{e.class} #{e.message}")
+ raise
+ ensure
+ server.close
+ end
+
+ def send_text(text, path)
+ file = ::Tempfile.new
+ file.write(text)
+ file.rewind
+ server.puttextfile(file, path)
+ file.unlink
+ end
+
+ def move(root_path, from_path, to_path)
+ from = "#{root_path}#{from_path}"
+ to = "#{root_path}#{to_path}"
+ server.rename(from, to)
+ end
+
+ protected
+
+ def server
+ unless @server
+ @server = Net::FTP.new
+ @server.open_timeout = 10
+ @server.read_timeout = 10
+ @server.connect(host, port)
+ @server.login(username, password)
+ end
+ @server
+ end
+
+ # Se déplace dans le répertoire indiqué
+ # Crée les répertoires manquants
+ def manage_directory(path)
+ server.chdir('/')
+ path.split('/').reject(&:blank?).each do |part|
+ begin
+ server.chdir(part)
+ rescue Net::FTPPermError
+ server.mkdir(part)
+ server.chdir(part)
+ end
+ end
+ end
+
+ def send(blob, filename)
+ blob.open do |file|
+ server.putbinaryfile(file.path, filename)
+ end
+ end
+
+ def close
+ server.close
+ end
+end
\ No newline at end of file
diff --git a/app/views/admin/communication/library/files/redirections/_app.html.erb b/app/views/admin/communication/library/files/redirections/_app.html.erb
new file mode 100644
index 000000000..0ec0b0ebb
--- /dev/null
+++ b/app/views/admin/communication/library/files/redirections/_app.html.erb
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/app/views/admin/communication/library/files/redirections/index.json.jbuilder b/app/views/admin/communication/library/files/redirections/index.json.jbuilder
new file mode 100644
index 000000000..68f47a3a2
--- /dev/null
+++ b/app/views/admin/communication/library/files/redirections/index.json.jbuilder
@@ -0,0 +1,16 @@
+json.file_server do
+ json.url @l10n.file_server_url
+ json.base_url @l10n.file_server_base_directory_url
+ json.slug @l10n.file_server_slug
+ json.extension @l10n.file_server_extension
+ json.endpoint change_server_slug_admin_communication_file_redirections_path(@file)
+end
+json.redirections do
+ json.base_url @l10n.file_server_base_url
+ json.endpoint admin_communication_file_redirections_path
+ json.list @l10n.redirections.ordered do |redirection|
+ json.id redirection.id
+ json.url redirection.url
+ json.endpoint admin_communication_file_redirection_path(id: redirection)
+ end
+end
\ No newline at end of file
diff --git a/app/views/admin/communication/library/files/redirections/static.html.erb b/app/views/admin/communication/library/files/redirections/static.html.erb
new file mode 100644
index 000000000..7de122cb3
--- /dev/null
+++ b/app/views/admin/communication/library/files/redirections/static.html.erb
@@ -0,0 +1,9 @@
+<%
+# @about en preview, about en render
+university = @about&.university || about&.university
+university.communication_file_redirections.ordered.each do |redirection|
+ from = redirection.path
+ to = redirection.communication_file_localization.file_server_url
+ %>
+RedirectPermanent <%= from %> <%= to %>
+<% end %>
\ No newline at end of file
diff --git a/app/views/admin/communication/library/files/show.html.erb b/app/views/admin/communication/library/files/show.html.erb
index 41f29a851..a41ab969c 100644
--- a/app/views/admin/communication/library/files/show.html.erb
+++ b/app/views/admin/communication/library/files/show.html.erb
@@ -23,6 +23,9 @@
<%= link_to t('download'),
download_medium_path(signed_id: @l10n.original_blob.signed_id),
class: button_classes %>
+
+ <%= render 'admin/application/l10n/widget', about: @file, l10n: @l10n %>
+
-<%= render 'admin/application/l10n/widget', about: @file, l10n: @l10n %>
+<%= render 'admin/communication/library/files/redirections/app',
+ about: @file,
+ l10n: @l10n if current_university.file_server? %>
<%= render 'admin/communication/library/contexts/list', contexts: @contexts %>
<% content_for :server_admin_only do %>
<%= static_link static_admin_communication_file_path(@file) %>
+ <%= link_to '.htaccess',
+ static_admin_communication_file_redirections_path(@file),
+ class: button_classes %>
<% end %>
diff --git a/app/views/admin/communication/library/files/static.html.erb b/app/views/admin/communication/library/files/static.html.erb
index f7c1ba7e4..5c806a3e5 100644
--- a/app/views/admin/communication/library/files/static.html.erb
+++ b/app/views/admin/communication/library/files/static.html.erb
@@ -9,5 +9,10 @@ file = @l10n.about
<% if @l10n.original_blob.present? %>
original_blob:
id: "<%= @l10n.original_blob_id %>"
+<% if @l10n.file_server_url.present? %>
+file_server:
+ url: "<%= @l10n.file_server_url %>"
+ name: "<%= @l10n.file_server_filename %>"
+<% end %>
<% end %>
---
diff --git a/app/views/server/universities/_form.html.erb b/app/views/server/universities/_form.html.erb
index 357a5f90d..3d1ed3bb9 100644
--- a/app/views/server/universities/_form.html.erb
+++ b/app/views/server/universities/_form.html.erb
@@ -105,6 +105,9 @@
<%= f.input :contribution_amount %>
+
+ <%= render 'server/universities/form/file_server', university: university, f: f %>
+
<% content_for :action_bar_right do %>
<%= cancel [:server, university] %>
<%= submit f %>
diff --git a/app/views/server/universities/form/_file_server.html.erb b/app/views/server/universities/form/_file_server.html.erb
new file mode 100644
index 000000000..8246aa7a6
--- /dev/null
+++ b/app/views/server/universities/form/_file_server.html.erb
@@ -0,0 +1,25 @@
+<%
+file_server = university.file_server || university.build_file_server
+%>
+<%= University::FileServer.model_name.human %>
+<%= f.simple_fields_for :file_server, file_server do |lf| %>
+ <%= lf.input :url %>
+
+
+ <%= lf.input :ftp_host %>
+
+
+ <%= lf.input :ftp_port %>
+
+
+ <%= lf.input :ftp_username %>
+
+
+ <%= lf.input :ftp_password,
+ placeholder: masked_string(file_server.ftp_password),
+ hint: t("simple_form.hints.university_file_server.ftp_password_#{file_server.ftp_password.blank? ? 'without' : 'with'}_existing").html_safe,
+ input_html: { value: '' } %>
+
+
+ <%= lf.input :ftp_path %>
+<% end %>
\ No newline at end of file
diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml
index 574ee5bb4..2d13431b0 100644
--- a/config/locales/communication/en.yml
+++ b/config/locales/communication/en.yml
@@ -96,6 +96,7 @@ en:
communication/file/context:
about_type: Type
communication/file/localization:
+ file_server_slug: Path
internal_description: Internal description
name: Name
original_byte_size: Size
@@ -105,6 +106,8 @@ en:
published: Published?
published_at: Publication date
updated_by: Last update
+ communication/file/redirection:
+ path: Path
communication/media:
collection: Collection
created_by: Envoyée par
@@ -353,6 +356,8 @@ en:
cannot_unpublished_default: cannot be unpublished (default language)
communication/file/localization:
attributes:
+ file_server_slug:
+ taken: already taken
original_uploaded_file:
already_imported: already imported in file library
too_big: too heavy!
diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml
index 1afc563c1..46c908d51 100644
--- a/config/locales/communication/fr.yml
+++ b/config/locales/communication/fr.yml
@@ -96,6 +96,7 @@ fr:
communication/file/context:
about_type: Type
communication/file/localization:
+ file_server_slug: Chemin
internal_description: Description interne
name: Nom
original_byte_size: Poids
@@ -105,6 +106,8 @@ fr:
published: Publié ?
published_at: Date de publication
updated_by: Dernière modification
+ communication/file/redirection:
+ path: Chemin
communication/media:
collection: Collection
created_by: Création
@@ -353,9 +356,11 @@ fr:
cannot_unpublished_default: ne peut pas être dépubliée (langue par défaut)
communication/file/localization:
attributes:
+ file_server_slug:
+ taken: déjà utilisé
original_uploaded_file:
already_imported: déjà importé dans la bibliothèque de fichiers
- too_big: trop lourd !
+ too_big: trop lourd !
communication/media:
attributes:
original_uploaded_file:
diff --git a/config/locales/university/en.yml b/config/locales/university/en.yml
index de226e1e2..5f1bc76d2 100644
--- a/config/locales/university/en.yml
+++ b/config/locales/university/en.yml
@@ -29,6 +29,13 @@ en:
university/app:
name: Name
token: Secret token
+ university/file_server:
+ ftp_host: FTP host (without protocole)
+ ftp_path: FTP path
+ ftp_password: FTP password
+ ftp_port: FTP port
+ ftp_username: FTP username
+ url: URL
university/organization:
address: Address
categories: Categories
@@ -146,6 +153,9 @@ en:
university/app:
one: App
other: Apps
+ university/file_server:
+ one: File server
+ other: File servers
university/organization:
one: Organization
other: Organizations
@@ -191,6 +201,9 @@ en:
languages: "Those languages are currently used on websites: %{used_languages}"
sms_sender_name: "11 characters max. Only alphanumeric chars ([A-Z][a-z][0-9])."
sso_button_label: "Default: Sign in via SSO"
+ university_file_server:
+ ftp_password_with_existing: Leave the field blank if you don't want to change the existing password.
+ ftp_password_without_existing: No password is currently saved.
university_organization:
description: If this field is empty the main text's begining will be used.
logo: This logo should contrast properly on a light bacgkground (white or pale grey)
diff --git a/config/locales/university/fr.yml b/config/locales/university/fr.yml
index 5d23a1279..4a29feee3 100644
--- a/config/locales/university/fr.yml
+++ b/config/locales/university/fr.yml
@@ -29,6 +29,13 @@ fr:
university/app:
name: Nom
token: Jeton secret
+ university/file_server:
+ ftp_host: Hôte FTP (sans protocole)
+ ftp_path: Chemin FTP
+ ftp_password: Mot de passe FTP
+ ftp_port: Port FTP
+ ftp_username: Nom d'utilisateur FTP
+ url: URL
university/organization:
address: Adresse
categories: Catégories
@@ -146,6 +153,9 @@ fr:
university/app:
one: App
other: Apps
+ university/file_server:
+ one: Serveur de fichiers
+ other: Serveurs de fichiers
university/organization:
one: Organisation
other: Organisations
@@ -191,6 +201,9 @@ fr:
languages: "Ces langues sont actuellement utilisées sans les sites web : %{used_languages}"
sms_sender_name: "11 caractères maximum. Que des caractères alphadécimaux ([A-Z][a-z][0-9])."
sso_button_label: "Par défaut : Se connecter en SSO"
+ university_file_server:
+ ftp_password_with_existing: Laisser le champ vide pour ne pas modifier le mot de passe existant.
+ ftp_password_without_existing: Aucun mot de passe n'est enregistré pour le moment.
university_organization:
description: Si ce champ est vide le début du texte principal sera utilisé.
logo: Ce logo doit contraster sur un fond clair (blanc ou gris pâle)
diff --git a/config/locales/vue/en.yml b/config/locales/vue/en.yml
index 3e7396d3e..1d59d348b 100644
--- a/config/locales/vue/en.yml
+++ b/config/locales/vue/en.yml
@@ -124,6 +124,20 @@ en:
alt:
hint: This text is important for accessibility. If the image conveys important information, it must be written here. If the information is already written somewhere else in the page, or if the image is purely decorative, the field must be left empty. Otherwise, it would pollute voice navigation.
label: Alternative text
+ fileServer:
+ permalink:
+ changed: The permalink has been changed
+ open: Open
+ save: Save
+ title: Permalink
+ redirections:
+ add: Add
+ added: The redirection has been added
+ error: Could not add this redirection
+ open: Open
+ remove: Remove
+ removed: The redirection has been removed
+ title: Redirections
picker:
pagination:
next: Next page
diff --git a/config/locales/vue/fr.yml b/config/locales/vue/fr.yml
index 12c73fc6d..397b5d2ce 100644
--- a/config/locales/vue/fr.yml
+++ b/config/locales/vue/fr.yml
@@ -124,6 +124,21 @@ fr:
alt:
hint: Ce texte est essentiel pour l'accessibilité. Si l'image donne une information importante, il faut écrire ici cette information. Si l'information est déjà présente ailleurs dans la page, ou si l'image est purement décorative, il faut laisser le texte alternatif vide, pour éviter de polluer la navigation vocale.
label: Texte alternatif
+ fileServer:
+ error: Une erreur est survenue
+ permalink:
+ changed: Le permalien a été modifié
+ open: Ouvrir
+ save: Enregistrer
+ title: Permalien
+ redirections:
+ add: Ajouter
+ added: La redirection a été ajoutée
+ error: Impossible d'ajouter cette redirection
+ open: Ouvrir
+ remove: Supprimer
+ removed: La redirection a été supprimée
+ title: Redirections
picker:
pagination:
next: Page suivante
diff --git a/config/routes/admin/communication.rb b/config/routes/admin/communication.rb
index 910550c2b..c7d5d5638 100644
--- a/config/routes/admin/communication.rb
+++ b/config/routes/admin/communication.rb
@@ -322,6 +322,14 @@
member do
get :static
end
+ resources :redirections,
+ controller: '/admin/communication/library/files/redirections',
+ defaults: { format: :json } do
+ collection do
+ patch :change_server_slug
+ get :static, defaults: { format: :html }
+ end
+ end
end
end
root to: 'dashboard#index'
diff --git a/db/migrate/20260906061411_create_university_file_servers.rb b/db/migrate/20260906061411_create_university_file_servers.rb
new file mode 100644
index 000000000..ce932afe7
--- /dev/null
+++ b/db/migrate/20260906061411_create_university_file_servers.rb
@@ -0,0 +1,15 @@
+class CreateUniversityFileServers < ActiveRecord::Migration[8.1]
+ def change
+ create_table :university_file_servers, id: :uuid do |t|
+ t.string :url
+ t.integer :ftp_port
+ t.string :ftp_username
+ t.string :ftp_password
+ t.string :ftp_path
+ t.string :ftp_host
+ t.belongs_to :university, index: { unique: true }, foreign_key: true, type: :uuid
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20260907101353_create_communication_file_redirections.rb b/db/migrate/20260907101353_create_communication_file_redirections.rb
new file mode 100644
index 000000000..bcf8744ef
--- /dev/null
+++ b/db/migrate/20260907101353_create_communication_file_redirections.rb
@@ -0,0 +1,14 @@
+class CreateCommunicationFileRedirections < ActiveRecord::Migration[8.1]
+ def change
+ create_table :communication_file_redirections, id: :uuid do |t|
+ t.string :path
+ t.references :communication_file_localization, null: false, foreign_key: true, type: :uuid
+ t.references :university, null: false, foreign_key: true, type: :uuid
+
+ t.timestamps
+ end
+
+ add_column :communication_file_localizations, :file_server_slug, :string
+ add_column :communication_file_localizations, :file_server_current_path, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a22bb880b..a79901301 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_08_29_043355) do
+ActiveRecord::Schema[8.1].define(version: 2026_09_07_101353) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "pg_stat_statements"
@@ -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"
@@ -195,7 +195,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"
@@ -203,7 +203,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"
@@ -217,7 +217,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"
@@ -238,7 +238,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
@@ -250,7 +250,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
@@ -291,7 +291,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
@@ -317,7 +317,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
@@ -376,7 +376,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
@@ -425,7 +425,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
@@ -438,7 +438,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"
@@ -528,6 +528,8 @@
t.text "featured_image_credit"
t.string "featured_media_alt"
t.uuid "featured_media_id"
+ t.string "file_server_current_path"
+ t.string "file_server_slug"
t.text "internal_description"
t.uuid "language_id", null: false
t.text "meta_description"
@@ -552,6 +554,16 @@
t.index ["updated_by_id"], name: "index_communication_file_localizations_on_updated_by_id"
end
+ create_table "communication_file_redirections", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.uuid "communication_file_localization_id", null: false
+ t.datetime "created_at", null: false
+ t.string "path"
+ t.uuid "university_id", null: false
+ t.datetime "updated_at", null: false
+ t.index ["communication_file_localization_id"], name: "idx_on_communication_file_localization_id_b5293392d9"
+ t.index ["university_id"], name: "index_communication_file_redirections_on_university_id"
+ end
+
create_table "communication_files", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
t.uuid "created_by_id"
@@ -831,7 +843,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
@@ -1024,7 +1036,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"
@@ -1080,7 +1092,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
@@ -1230,7 +1242,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
@@ -1253,7 +1265,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
@@ -1356,7 +1368,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
@@ -1377,7 +1389,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
@@ -1495,7 +1507,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
@@ -1587,7 +1599,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
@@ -1622,7 +1634,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"
@@ -1713,7 +1725,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"
@@ -1816,7 +1828,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"
@@ -1872,7 +1884,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"
@@ -1887,21 +1899,6 @@
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|
- t.text "content_en"
- t.text "content_fr"
- t.datetime "created_at", null: false
- t.datetime "delivered_at"
- t.integer "delivered_count"
- t.string "name"
- t.string "role"
- t.string "subject_en"
- t.string "subject_fr"
- t.uuid "university_id"
- t.datetime "updated_at", null: false
- t.index ["university_id"], name: "index_emergency_messages_on_university_id", where: "(university_id IS NOT NULL)"
- end
-
create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.integer "callback_priority"
t.text "callback_queue_name"
@@ -1993,7 +1990,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
@@ -2008,7 +2005,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"
@@ -2022,7 +2019,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"
@@ -2081,7 +2078,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
@@ -2113,7 +2110,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
@@ -2167,7 +2164,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"
@@ -2178,7 +2175,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
@@ -2186,7 +2183,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"
@@ -2205,7 +2202,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
@@ -2250,7 +2247,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"
@@ -2284,7 +2281,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"
@@ -2343,7 +2340,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"
@@ -2374,7 +2371,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"
@@ -2385,7 +2382,20 @@
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_file_servers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.string "ftp_host"
+ t.string "ftp_password"
+ t.string "ftp_path"
+ t.integer "ftp_port"
+ t.string "ftp_username"
+ t.uuid "university_id"
+ t.datetime "updated_at", null: false
+ t.string "url"
+ t.index ["university_id"], name: "index_university_file_servers_on_university_id", unique: true
+ end
+
+ 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
@@ -2399,7 +2409,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"
@@ -2466,7 +2476,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"
@@ -2489,7 +2499,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"
@@ -2528,14 +2538,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
@@ -2589,7 +2599,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"
@@ -2617,7 +2627,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"
@@ -2678,7 +2688,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
@@ -2690,7 +2700,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
@@ -2700,7 +2710,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"
@@ -2831,6 +2841,8 @@
add_foreign_key "communication_file_localizations", "languages"
add_foreign_key "communication_file_localizations", "universities"
add_foreign_key "communication_file_localizations", "users", column: "updated_by_id"
+ add_foreign_key "communication_file_redirections", "communication_file_localizations"
+ add_foreign_key "communication_file_redirections", "universities"
add_foreign_key "communication_files", "universities"
add_foreign_key "communication_files", "users", column: "created_by_id"
add_foreign_key "communication_media_categories", "communication_media_categories", column: "parent_id"
@@ -3037,7 +3049,6 @@
add_foreign_key "education_school_localizations", "languages"
add_foreign_key "education_school_localizations", "universities"
add_foreign_key "education_schools", "universities"
- add_foreign_key "emergency_messages", "universities"
add_foreign_key "imports", "languages"
add_foreign_key "imports", "universities"
add_foreign_key "imports", "users"
@@ -3087,6 +3098,7 @@
add_foreign_key "search_index", "universities"
add_foreign_key "universities", "languages", column: "default_language_id"
add_foreign_key "university_apps", "universities"
+ add_foreign_key "university_file_servers", "universities"
add_foreign_key "university_organization_categories", "universities"
add_foreign_key "university_organization_categories", "university_organization_categories", column: "parent_id"
add_foreign_key "university_organization_categories_organizations", "university_organization_categories", column: "category_id"