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 @@ + + + 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 %> +
@@ -68,10 +71,15 @@
-<%= 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"