From 9baf585e78c16be3efa1d1f6f7c32ac8c7f10a93 Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Sun, 6 Sep 2026 08:45:42 +0200 Subject: [PATCH 01/10] Serveur de fichiers --- .../server/universities_controller.rb | 3 +- app/models/university.rb | 1 + app/models/university/file_server.rb | 33 +++++ app/models/university/with_file_server.rb | 13 ++ app/views/server/universities/_form.html.erb | 3 + .../universities/form/_file_server.html.erb | 22 ++++ config/locales/university/en.yml | 10 ++ config/locales/university/fr.yml | 10 ++ ...06061411_create_university_file_servers.rb | 15 +++ db/schema.rb | 124 ++++++++++-------- 10 files changed, 177 insertions(+), 57 deletions(-) create mode 100644 app/models/university/file_server.rb create mode 100644 app/models/university/with_file_server.rb create mode 100644 app/views/server/universities/form/_file_server.html.erb create mode 100644 db/migrate/20260906061411_create_university_file_servers.rb diff --git a/app/controllers/server/universities_controller.rb b/app/controllers/server/universities_controller.rb index 96b4867aa5..3d5bf62ada 100644 --- a/app/controllers/server/universities_controller.rb +++ b/app/controllers/server/universities_controller.rb @@ -83,7 +83,8 @@ 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? params.require(:university).permit(attribute_names) diff --git a/app/models/university.rb b/app/models/university.rb index 3502b0d004..0e4d002e72 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 0000000000..a66561c376 --- /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_file_server.rb b/app/models/university/with_file_server.rb new file mode 100644 index 0000000000..1a2c6ec78b --- /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/views/server/universities/_form.html.erb b/app/views/server/universities/_form.html.erb index 357a5f90d2..3d1ed3bb93 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 0000000000..b046bec4f4 --- /dev/null +++ b/app/views/server/universities/form/_file_server.html.erb @@ -0,0 +1,22 @@ +<% +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 %> +
+
+ <%= lf.input :ftp_path %> +<% end %> \ No newline at end of file diff --git a/config/locales/university/en.yml b/config/locales/university/en.yml index de226e1e2a..7f50e4d649 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 diff --git a/config/locales/university/fr.yml b/config/locales/university/fr.yml index 5d23a1279f..1215b41384 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 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 0000000000..ce932afe79 --- /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/schema.rb b/db/schema.rb index a22bb880b2..7be4925b1b 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_06_061411) 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" @@ -831,7 +831,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 +1024,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 +1080,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 +1230,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 +1253,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 +1356,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 +1377,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 +1495,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 +1587,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 +1622,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 +1713,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 +1816,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 +1872,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" @@ -1886,7 +1886,6 @@ t.string "zipcode" 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" @@ -1901,7 +1900,6 @@ 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 +1991,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 +2006,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 +2020,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 +2079,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 +2111,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 +2165,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 +2176,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 +2184,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 +2203,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 +2248,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 +2282,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 +2341,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 +2372,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 +2383,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 +2410,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 +2477,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 +2500,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 +2539,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 +2600,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 +2628,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 +2689,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 +2701,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 +2711,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" @@ -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" From 8b6b8f593f8e8accf2b13df5c9800a9270818508 Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Sun, 6 Sep 2026 11:05:46 +0200 Subject: [PATCH 02/10] Simple envoi --- Gemfile | 1 + Gemfile.lock | 4 + Procfile | 1 + .../server/universities_controller.rb | 3 + .../file/synchronize_with_file_server_job.rb | 7 ++ app/models/communication/file/localization.rb | 1 + .../file/localization/with_file_server.rb | 73 +++++++++++++++++++ .../communication/library/files/show.html.erb | 4 + .../universities/form/_file_server.html.erb | 5 +- config/locales/university/en.yml | 3 + config/locales/university/fr.yml | 3 + 11 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/jobs/communication/file/synchronize_with_file_server_job.rb create mode 100644 app/models/communication/file/localization/with_file_server.rb diff --git a/Gemfile b/Gemfile index 2cc529457c..9d34b0ea0f 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 8ba4fc7702..2c527f4fa6 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 6faf2d0c9c..65cf016467 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/server/universities_controller.rb b/app/controllers/server/universities_controller.rb index 3d5bf62ada..6bbb707926 100644 --- a/app/controllers/server/universities_controller.rb +++ b/app/controllers/server/universities_controller.rb @@ -87,6 +87,9 @@ def university_params 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/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 0000000000..43b566be7e --- /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 51f862a4b6..f0c6dd6e08 100644 --- a/app/models/communication/file/localization.rb +++ b/app/models/communication/file/localization.rb @@ -59,6 +59,7 @@ class Communication::File::Localization < ApplicationRecord include Permalinkable include Publishable include Sanitizable + 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 0000000000..10ef4b49bc --- /dev/null +++ b/app/models/communication/file/localization/with_file_server.rb @@ -0,0 +1,73 @@ +module Communication::File::Localization::WithFileServer + extend ActiveSupport::Concern + + included do + 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 file_server_url + "#{file_server.url}/#{file_server_path}" + end + + def file_server_directory + "#{created_at.year}" + end + + def file_server_filename + "#{slug}#{original_extension}" + end + + def file_server_path + "#{file_server_directory}/#{file_server_filename}" + end + + def sync_to_file_server_safely + create_file_server_directory_if_necessary(file_server_remote_directory) + original_blob.open do |file| + ftp.putbinaryfile(file.path, file_server_filename) + end + rescue => e + Rails.logger.error("[Communication::File::Localization##{id}] Échec de la synchronisation FTP vers #{file_server&.ftp_host.inspect} : #{e.class} #{e.message}") + raise + ensure + @ftp&.close + end + + protected + + def file_server + @file_server ||= university.file_server + end + + def ftp + unless @ftp + @ftp = Net::FTP.new + @ftp.open_timeout = 10 + @ftp.read_timeout = 10 + @ftp.connect(file_server.ftp_host, file_server.ftp_port) + @ftp.login(file_server.ftp_username, file_server.ftp_password) + end + @ftp + end + + def file_server_remote_directory + File.join(*[file_server.ftp_path.presence, file_server_directory].compact) + end + + def create_file_server_directory_if_necessary(path) + ftp.chdir('/') + path.split('/').reject(&:blank?).each do |part| + begin + ftp.chdir(part) + rescue Net::FTPPermError + ftp.mkdir(part) + ftp.chdir(part) + end + end + end +end diff --git a/app/views/admin/communication/library/files/show.html.erb b/app/views/admin/communication/library/files/show.html.erb index 41f29a8516..52e32dae49 100644 --- a/app/views/admin/communication/library/files/show.html.erb +++ b/app/views/admin/communication/library/files/show.html.erb @@ -4,6 +4,10 @@ <%= button_advanced do %> <%= destroy_link @file if @file.contexts.none? %> <% end %> + <%= link_to t('open'), + @l10n.file_server_url, + target: :_blank, + class: button_classes if current_university.file_server? %> <%= edit_link @file %> <% end %> diff --git a/app/views/server/universities/form/_file_server.html.erb b/app/views/server/universities/form/_file_server.html.erb index b046bec4f4..8246aa7a60 100644 --- a/app/views/server/universities/form/_file_server.html.erb +++ b/app/views/server/universities/form/_file_server.html.erb @@ -15,7 +15,10 @@ file_server = university.file_server || university.build_file_server <%= lf.input :ftp_username %>
- <%= lf.input :ftp_password %> + <%= 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 %> diff --git a/config/locales/university/en.yml b/config/locales/university/en.yml index 7f50e4d649..5f1bc76d29 100644 --- a/config/locales/university/en.yml +++ b/config/locales/university/en.yml @@ -201,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 1215b41384..4a29feee3e 100644 --- a/config/locales/university/fr.yml +++ b/config/locales/university/fr.yml @@ -201,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) From 3e4b1a31a486e2d8273a16be92392788cb763286 Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Sun, 6 Sep 2026 14:02:51 +0200 Subject: [PATCH 03/10] Static --- .../file/localization/with_file_server.rb | 19 ++++++++++--------- .../library/files/static.html.erb | 5 +++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/models/communication/file/localization/with_file_server.rb b/app/models/communication/file/localization/with_file_server.rb index 10ef4b49bc..b4f39f5836 100644 --- a/app/models/communication/file/localization/with_file_server.rb +++ b/app/models/communication/file/localization/with_file_server.rb @@ -11,21 +11,14 @@ def sync_to_file_server end def file_server_url + return unless university.file_server? "#{file_server.url}/#{file_server_path}" end - def file_server_directory - "#{created_at.year}" - end - def file_server_filename "#{slug}#{original_extension}" end - def file_server_path - "#{file_server_directory}/#{file_server_filename}" - end - def sync_to_file_server_safely create_file_server_directory_if_necessary(file_server_remote_directory) original_blob.open do |file| @@ -44,6 +37,14 @@ def file_server @file_server ||= university.file_server end + def file_server_directory + "#{created_at.year}" + end + + def file_server_path + "#{file_server_directory}/#{file_server_filename}" + end + def ftp unless @ftp @ftp = Net::FTP.new @@ -56,7 +57,7 @@ def ftp end def file_server_remote_directory - File.join(*[file_server.ftp_path.presence, file_server_directory].compact) + "#{file_server.ftp_path}/#{file_server_directory}" end def create_file_server_directory_if_necessary(path) diff --git a/app/views/admin/communication/library/files/static.html.erb b/app/views/admin/communication/library/files/static.html.erb index f7c1ba7e48..5c806a3e55 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 %> --- From 4555f8f708e484c64ca434e8473a2372ac05c31c Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Mon, 7 Sep 2026 13:08:17 +0200 Subject: [PATCH 04/10] wip --- app/models/communication/file/localization.rb | 1 + .../file/localization/permalink.rb | 42 ++++++++++++++ .../file/localization/with_file_server.rb | 56 +++++++----------- .../file/localization/with_permalinks.rb | 17 ++++++ app/services/ftp.rb | 57 +++++++++++++++++++ .../library/files/_permalinks.html.erb | 33 +++++++++++ .../communication/library/files/show.html.erb | 11 +++- config/routes/admin/communication.rb | 1 + ...munication_file_localization_permalinks.rb | 11 ++++ db/schema.rb | 27 ++++----- 10 files changed, 205 insertions(+), 51 deletions(-) create mode 100644 app/models/communication/file/localization/permalink.rb create mode 100644 app/models/communication/file/localization/with_permalinks.rb create mode 100644 app/services/ftp.rb create mode 100644 app/views/admin/communication/library/files/_permalinks.html.erb create mode 100644 db/migrate/20260907101353_create_communication_file_localization_permalinks.rb diff --git a/app/models/communication/file/localization.rb b/app/models/communication/file/localization.rb index f0c6dd6e08..7f6557a801 100644 --- a/app/models/communication/file/localization.rb +++ b/app/models/communication/file/localization.rb @@ -61,6 +61,7 @@ class Communication::File::Localization < ApplicationRecord include Sanitizable include WithFileServer include WithOpenApi + include WithPermalinks belongs_to :updated_by, class_name: 'User', diff --git a/app/models/communication/file/localization/permalink.rb b/app/models/communication/file/localization/permalink.rb new file mode 100644 index 0000000000..e2912a8812 --- /dev/null +++ b/app/models/communication/file/localization/permalink.rb @@ -0,0 +1,42 @@ +# == Schema Information +# +# Table name: communication_file_localization_permalinks +# +# id :uuid not null, primary key +# is_current :boolean +# path :string +# created_at :datetime not null +# updated_at :datetime not null +# communication_file_localization_id :uuid not null, indexed +# +# Indexes +# +# idx_on_communication_file_localization_id_d9f5f5a2f8 (communication_file_localization_id) +# +# Foreign Keys +# +# fk_rails_e23549fca9 (communication_file_localization_id => communication_file_localizations.id) +# +class Communication::File::Localization::Permalink < ApplicationRecord + belongs_to :communication_file_localization + + scope :ordered, -> { order(created_at: :desc) } + scope :current, -> { where(is_current: true) } + scope :not_current, -> { where(is_current: false) } + + after_commit :manage_previous_permalinks + + protected + + def manage_previous_permalinks + siblings.update_all :is_current, false + end + + def siblings + Communication::File::Localization::Permalink.where( + communication_file_localization_id: id + ).where.not( + id: id + ) + end +end diff --git a/app/models/communication/file/localization/with_file_server.rb b/app/models/communication/file/localization/with_file_server.rb index b4f39f5836..a22dc386a5 100644 --- a/app/models/communication/file/localization/with_file_server.rb +++ b/app/models/communication/file/localization/with_file_server.rb @@ -10,65 +10,53 @@ def sync_to_file_server Communication::File::SynchronizeWithFileServerJob.perform_later(self) end + def sync_to_file_server_safely + ftp.send_blob( + original_blob, + file_server_remote_directory, + file_server_filename + ) + 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 + # rapport-annuel.pdf def file_server_filename "#{slug}#{original_extension}" end - def sync_to_file_server_safely - create_file_server_directory_if_necessary(file_server_remote_directory) - original_blob.open do |file| - ftp.putbinaryfile(file.path, file_server_filename) - end - rescue => e - Rails.logger.error("[Communication::File::Localization##{id}] Échec de la synchronisation FTP vers #{file_server&.ftp_host.inspect} : #{e.class} #{e.message}") - raise - ensure - @ftp&.close - end - protected def file_server @file_server ||= university.file_server end + # fr/2026 def file_server_directory - "#{created_at.year}" + "#{language.iso_code}/#{created_at.year}" end + # fr/2026/rapport-annuel.pdf def file_server_path "#{file_server_directory}/#{file_server_filename}" end - def ftp - unless @ftp - @ftp = Net::FTP.new - @ftp.open_timeout = 10 - @ftp.read_timeout = 10 - @ftp.connect(file_server.ftp_host, file_server.ftp_port) - @ftp.login(file_server.ftp_username, file_server.ftp_password) - end - @ftp - end - + # /path-on-ftp-server/fr/2026/rapport-annuel.pdf def file_server_remote_directory "#{file_server.ftp_path}/#{file_server_directory}" end - def create_file_server_directory_if_necessary(path) - ftp.chdir('/') - path.split('/').reject(&:blank?).each do |part| - begin - ftp.chdir(part) - rescue Net::FTPPermError - ftp.mkdir(part) - ftp.chdir(part) - end - end + def ftp + @ftp ||= ::Ftp.new( + file_server.ftp_host, + file_server.ftp_port, + file_server.ftp_username, + file_server.ftp_password + ) end + end diff --git a/app/models/communication/file/localization/with_permalinks.rb b/app/models/communication/file/localization/with_permalinks.rb new file mode 100644 index 0000000000..1144e9969d --- /dev/null +++ b/app/models/communication/file/localization/with_permalinks.rb @@ -0,0 +1,17 @@ +module Communication::File::Localization::WithPermalinks + extend ActiveSupport::Concern + + included do + has_many :permalinks, + foreign_key: :communication_file_localization_id, + dependent: :destroy + end + + def current_permalink + permalinks.current.first + end + + def previous_permalinks + permalinks.not_current.ordered + end +end \ No newline at end of file diff --git a/app/services/ftp.rb b/app/services/ftp.rb new file mode 100644 index 0000000000..90253b2d66 --- /dev/null +++ b/app/services/ftp.rb @@ -0,0 +1,57 @@ +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(blob, filename) + rescue => e + Rails.logger.error("Échec de la synchronisation FTP vers #{host.inspect} : #{e.class} #{e.message}") + raise + ensure + server.close + 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(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/_permalinks.html.erb b/app/views/admin/communication/library/files/_permalinks.html.erb new file mode 100644 index 0000000000..c7dce01d3c --- /dev/null +++ b/app/views/admin/communication/library/files/_permalinks.html.erb @@ -0,0 +1,33 @@ +

+ + <%= t('admin.communication.website.redirects.button') %> + +

+
+
+

<%= t('admin.communication.website.redirects.title') %>

+ +
+
+

+ <%= about.current_permalink %> +

+ <%= osuny_small_panel t('admin.communication.website.redirects.previous_paths') do %> + + + <% about.previous_permalinks.each do |permalink| %> + <%= render 'admin/communication/websites/permalinks/permalink', permalink: permalink %> + <% end %> + + + <%= simple_form_for Communication::File::Localization::Permalink.new, + url: admin_communication_file_permalink_path( + file_id: about.id + ), + remote: true do |f| %> + <%= f.input :path, label: t('admin.communication.website.redirects.add_path') %> + <%= f.button :submit, t('add'), class: button_classes %> + <% end %> + <% 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 52e32dae49..5f76f13f72 100644 --- a/app/views/admin/communication/library/files/show.html.erb +++ b/app/views/admin/communication/library/files/show.html.erb @@ -72,7 +72,16 @@ -<%= render 'admin/application/l10n/widget', about: @file, l10n: @l10n %> +
+
+ <%= render 'admin/communication/library/files/permalinks', about: @l10n %> +
+
+ <%= render 'admin/application/l10n/widget', about: @file, l10n: @l10n %> +
+
+ + <%= render 'admin/communication/library/contexts/list', contexts: @contexts %> diff --git a/config/routes/admin/communication.rb b/config/routes/admin/communication.rb index 910550c2b8..ca5a6513ab 100644 --- a/config/routes/admin/communication.rb +++ b/config/routes/admin/communication.rb @@ -322,6 +322,7 @@ member do get :static end + resources :permalinks end end root to: 'dashboard#index' diff --git a/db/migrate/20260907101353_create_communication_file_localization_permalinks.rb b/db/migrate/20260907101353_create_communication_file_localization_permalinks.rb new file mode 100644 index 0000000000..cda2cc5344 --- /dev/null +++ b/db/migrate/20260907101353_create_communication_file_localization_permalinks.rb @@ -0,0 +1,11 @@ +class CreateCommunicationFileLocalizationPermalinks < ActiveRecord::Migration[8.1] + def change + create_table :communication_file_localization_permalinks, id: :uuid do |t| + t.string :path + t.references :communication_file_localization, null: false, foreign_key: true, type: :uuid + t.boolean :is_current + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 7be4925b1b..40a860f25f 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_09_06_061411) 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" @@ -521,6 +521,15 @@ t.index ["university_id"], name: "index_communication_file_contexts_on_university_id" end + create_table "communication_file_localization_permalinks", 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.boolean "is_current" + t.string "path" + t.datetime "updated_at", null: false + t.index ["communication_file_localization_id"], name: "idx_on_communication_file_localization_id_d9f5f5a2f8" + end + create_table "communication_file_localizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id", null: false t.datetime "created_at", null: false @@ -1886,20 +1895,6 @@ t.string "zipcode" 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" @@ -2836,6 +2831,7 @@ add_foreign_key "communication_file_contexts", "communication_file_localizations" add_foreign_key "communication_file_contexts", "communication_websites" add_foreign_key "communication_file_contexts", "universities" + add_foreign_key "communication_file_localization_permalinks", "communication_file_localizations" add_foreign_key "communication_file_localizations", "active_storage_blobs", column: "original_blob_id" add_foreign_key "communication_file_localizations", "communication_files", column: "about_id" add_foreign_key "communication_file_localizations", "communication_medias", column: "featured_media_id", on_delete: :nullify @@ -3048,7 +3044,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" From 56d3cc78ef556882cb544f3b5983f0992c585f73 Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Tue, 8 Sep 2026 12:28:10 +0200 Subject: [PATCH 05/10] working --- .../library/files/redirections_controller.rb | 39 ++++ .../apps/file-server/FileServerApp.vue | 200 ++++++++++++++++++ app/javascript/apps/index.js | 2 + app/models/communication/file/localization.rb | 50 ++--- .../file/localization/permalink.rb | 42 ---- .../file/localization/with_file_server.rb | 84 ++++++-- .../file/localization/with_permalinks.rb | 17 -- .../file/localization/with_redirections.rb | 10 + app/models/communication/file/redirection.rb | 60 ++++++ app/services/ftp.rb | 10 +- .../library/files/_permalinks.html.erb | 33 --- .../library/files/redirections/_app.html.erb | 5 + .../files/redirections/index.json.jbuilder | 16 ++ .../communication/library/files/show.html.erb | 20 +- config/locales/vue/fr.yml | 14 ++ config/routes/admin/communication.rb | 8 +- ...munication_file_localization_permalinks.rb | 11 - ..._create_communication_file_redirections.rb | 14 ++ db/schema.rb | 28 ++- 19 files changed, 488 insertions(+), 175 deletions(-) create mode 100644 app/controllers/admin/communication/library/files/redirections_controller.rb create mode 100644 app/javascript/apps/file-server/FileServerApp.vue delete mode 100644 app/models/communication/file/localization/permalink.rb delete mode 100644 app/models/communication/file/localization/with_permalinks.rb create mode 100644 app/models/communication/file/localization/with_redirections.rb create mode 100644 app/models/communication/file/redirection.rb delete mode 100644 app/views/admin/communication/library/files/_permalinks.html.erb create mode 100644 app/views/admin/communication/library/files/redirections/_app.html.erb create mode 100644 app/views/admin/communication/library/files/redirections/index.json.jbuilder delete mode 100644 db/migrate/20260907101353_create_communication_file_localization_permalinks.rb create mode 100644 db/migrate/20260907101353_create_communication_file_redirections.rb 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 0000000000..3828630558 --- /dev/null +++ b/app/controllers/admin/communication/library/files/redirections_controller.rb @@ -0,0 +1,39 @@ +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 + @l10n.file_server_slug = params[:file_server_slug] + @l10n.save + render :index + end + + def create + @redirection = @l10n.redirections.create( + path_without_extension: params[:path_without_extension], + university: @l10n.university, + ) + render :index + end + + def destroy + @redirection = @l10n.redirections.find(params[:id]) + @redirection.destroy + render :index + 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 +end diff --git a/app/javascript/apps/file-server/FileServerApp.vue b/app/javascript/apps/file-server/FileServerApp.vue new file mode 100644 index 0000000000..3bc783fd46 --- /dev/null +++ b/app/javascript/apps/file-server/FileServerApp.vue @@ -0,0 +1,200 @@ + + + diff --git a/app/javascript/apps/index.js b/app/javascript/apps/index.js index b149db9453..42ba375698 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/models/communication/file/localization.rb b/app/models/communication/file/localization.rb index 7f6557a801..25ec71578f 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,9 +61,9 @@ class Communication::File::Localization < ApplicationRecord include Permalinkable include Publishable include Sanitizable + include WithRedirections include WithFileServer include WithOpenApi - include WithPermalinks belongs_to :updated_by, class_name: 'User', diff --git a/app/models/communication/file/localization/permalink.rb b/app/models/communication/file/localization/permalink.rb deleted file mode 100644 index e2912a8812..0000000000 --- a/app/models/communication/file/localization/permalink.rb +++ /dev/null @@ -1,42 +0,0 @@ -# == Schema Information -# -# Table name: communication_file_localization_permalinks -# -# id :uuid not null, primary key -# is_current :boolean -# path :string -# created_at :datetime not null -# updated_at :datetime not null -# communication_file_localization_id :uuid not null, indexed -# -# Indexes -# -# idx_on_communication_file_localization_id_d9f5f5a2f8 (communication_file_localization_id) -# -# Foreign Keys -# -# fk_rails_e23549fca9 (communication_file_localization_id => communication_file_localizations.id) -# -class Communication::File::Localization::Permalink < ApplicationRecord - belongs_to :communication_file_localization - - scope :ordered, -> { order(created_at: :desc) } - scope :current, -> { where(is_current: true) } - scope :not_current, -> { where(is_current: false) } - - after_commit :manage_previous_permalinks - - protected - - def manage_previous_permalinks - siblings.update_all :is_current, false - end - - def siblings - Communication::File::Localization::Permalink.where( - communication_file_localization_id: id - ).where.not( - id: id - ) - end -end diff --git a/app/models/communication/file/localization/with_file_server.rb b/app/models/communication/file/localization/with_file_server.rb index a22dc386a5..d50259d020 100644 --- a/app/models/communication/file/localization/with_file_server.rb +++ b/app/models/communication/file/localization/with_file_server.rb @@ -2,6 +2,7 @@ module Communication::File::Localization::WithFileServer extend ActiveSupport::Concern included do + before_validation :set_file_server_slug_if_empty? after_save :sync_to_file_server end @@ -11,43 +12,81 @@ def sync_to_file_server end def sync_to_file_server_safely - ftp.send_blob( - original_blob, - file_server_remote_directory, - file_server_filename - ) + 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 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}" + # rapport-annuel + # file_server_slug + + # .pdf + def file_server_extension + "#{original_extension}" end # rapport-annuel.pdf def file_server_filename - "#{slug}#{original_extension}" + "#{file_server_slug}#{file_server_extension}" end - protected + # /fr/2026/ + def file_server_directory + "/#{language.iso_code}/#{created_at.year}/" + end - def file_server - @file_server ||= university.file_server + # /fr/2026/rapport-annuel.pdf + def file_server_path + "#{file_server_directory}#{file_server_filename}" end - # fr/2026 - def file_server_directory - "#{language.iso_code}/#{created_at.year}" + # /fr/2026/rapport-annuel + def file_server_path_without_extension + "#{file_server_directory}#{file_server_slug}" end - # fr/2026/rapport-annuel.pdf - def file_server_path - "#{file_server_directory}/#{file_server_filename}" + # /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_directory - "#{file_server.ftp_path}/#{file_server_directory}" + def file_server_remote_path + "#{file_server.ftp_path}#{file_server_path}" + 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 set_file_server_slug_if_empty? + return if file_server_slug.present? + self.file_server_slug = slug end def ftp @@ -59,4 +98,7 @@ def ftp ) end + def file_server + @file_server ||= university.file_server + end end diff --git a/app/models/communication/file/localization/with_permalinks.rb b/app/models/communication/file/localization/with_permalinks.rb deleted file mode 100644 index 1144e9969d..0000000000 --- a/app/models/communication/file/localization/with_permalinks.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Communication::File::Localization::WithPermalinks - extend ActiveSupport::Concern - - included do - has_many :permalinks, - foreign_key: :communication_file_localization_id, - dependent: :destroy - end - - def current_permalink - permalinks.current.first - end - - def previous_permalinks - permalinks.not_current.ordered - end -end \ No newline at end of file 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 0000000000..cc8d0f2fa5 --- /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 0000000000..6f85b1faa5 --- /dev/null +++ b/app/models/communication/file/redirection.rb @@ -0,0 +1,60 @@ +# == 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) } + + validates_presence_of :path + + before_validation :set_path + + def path_without_extension + self.path.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 set_path + self.path = "/#{@path_without_extension}#{extension}" + end +end diff --git a/app/services/ftp.rb b/app/services/ftp.rb index 90253b2d66..25279a830c 100644 --- a/app/services/ftp.rb +++ b/app/services/ftp.rb @@ -10,7 +10,7 @@ def initialize(host, port, username, password) def send_blob(blob, directory, filename) manage_directory(directory) - send_blob(blob, filename) + send(blob, filename) rescue => e Rails.logger.error("Échec de la synchronisation FTP vers #{host.inspect} : #{e.class} #{e.message}") raise @@ -18,6 +18,12 @@ def send_blob(blob, directory, filename) server.close 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 @@ -45,7 +51,7 @@ def manage_directory(path) end end - def send_blob(blob, filename) + def send(blob, filename) blob.open do |file| server.putbinaryfile(file.path, filename) end diff --git a/app/views/admin/communication/library/files/_permalinks.html.erb b/app/views/admin/communication/library/files/_permalinks.html.erb deleted file mode 100644 index c7dce01d3c..0000000000 --- a/app/views/admin/communication/library/files/_permalinks.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -

- - <%= t('admin.communication.website.redirects.button') %> - -

-
-
-

<%= t('admin.communication.website.redirects.title') %>

- -
-
-

- <%= about.current_permalink %> -

- <%= osuny_small_panel t('admin.communication.website.redirects.previous_paths') do %> - - - <% about.previous_permalinks.each do |permalink| %> - <%= render 'admin/communication/websites/permalinks/permalink', permalink: permalink %> - <% end %> - - - <%= simple_form_for Communication::File::Localization::Permalink.new, - url: admin_communication_file_permalink_path( - file_id: about.id - ), - remote: true do |f| %> - <%= f.input :path, label: t('admin.communication.website.redirects.add_path') %> - <%= f.button :submit, t('add'), class: button_classes %> - <% 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 0000000000..0ec0b0ebb5 --- /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 0000000000..68f47a3a20 --- /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/show.html.erb b/app/views/admin/communication/library/files/show.html.erb index 5f76f13f72..bf73462f67 100644 --- a/app/views/admin/communication/library/files/show.html.erb +++ b/app/views/admin/communication/library/files/show.html.erb @@ -4,10 +4,6 @@ <%= button_advanced do %> <%= destroy_link @file if @file.contexts.none? %> <% end %> - <%= link_to t('open'), - @l10n.file_server_url, - target: :_blank, - class: button_classes if current_university.file_server? %> <%= edit_link @file %> <% end %> @@ -27,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 %> +
@@ -72,16 +71,9 @@
-
-
- <%= render 'admin/communication/library/files/permalinks', about: @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 %> diff --git a/config/locales/vue/fr.yml b/config/locales/vue/fr.yml index 12c73fc6d5..f07c1b809b 100644 --- a/config/locales/vue/fr.yml +++ b/config/locales/vue/fr.yml @@ -124,6 +124,20 @@ 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: + 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 ca5a6513ab..be968cb643 100644 --- a/config/routes/admin/communication.rb +++ b/config/routes/admin/communication.rb @@ -322,7 +322,13 @@ member do get :static end - resources :permalinks + resources :redirections, + controller: '/admin/communication/library/files/redirections', + defaults: { format: :json } do + collection do + patch :change_server_slug + end + end end end root to: 'dashboard#index' diff --git a/db/migrate/20260907101353_create_communication_file_localization_permalinks.rb b/db/migrate/20260907101353_create_communication_file_localization_permalinks.rb deleted file mode 100644 index cda2cc5344..0000000000 --- a/db/migrate/20260907101353_create_communication_file_localization_permalinks.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CreateCommunicationFileLocalizationPermalinks < ActiveRecord::Migration[8.1] - def change - create_table :communication_file_localization_permalinks, id: :uuid do |t| - t.string :path - t.references :communication_file_localization, null: false, foreign_key: true, type: :uuid - t.boolean :is_current - - 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 0000000000..bcf8744ef3 --- /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 40a860f25f..53b20409dc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -521,15 +521,6 @@ t.index ["university_id"], name: "index_communication_file_contexts_on_university_id" end - create_table "communication_file_localization_permalinks", 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.boolean "is_current" - t.string "path" - t.datetime "updated_at", null: false - t.index ["communication_file_localization_id"], name: "idx_on_communication_file_localization_id_d9f5f5a2f8" - end - create_table "communication_file_localizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "about_id", null: false t.datetime "created_at", null: false @@ -537,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" @@ -561,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" @@ -1895,6 +1898,7 @@ t.string "zipcode" t.index ["university_id"], name: "index_education_schools_on_university_id" 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" @@ -2831,13 +2835,14 @@ add_foreign_key "communication_file_contexts", "communication_file_localizations" add_foreign_key "communication_file_contexts", "communication_websites" add_foreign_key "communication_file_contexts", "universities" - add_foreign_key "communication_file_localization_permalinks", "communication_file_localizations" add_foreign_key "communication_file_localizations", "active_storage_blobs", column: "original_blob_id" add_foreign_key "communication_file_localizations", "communication_files", column: "about_id" add_foreign_key "communication_file_localizations", "communication_medias", column: "featured_media_id", on_delete: :nullify 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" @@ -3091,6 +3096,9 @@ add_foreign_key "search_index", "communication_extranets", column: "extranet_id" add_foreign_key "search_index", "communication_websites", column: "website_id" add_foreign_key "search_index", "universities" + add_foreign_key "server_emergency_messages", "universities" + add_foreign_key "server_evolution_localizations", "languages" + add_foreign_key "server_evolution_localizations", "server_evolutions", column: "evolution_id" add_foreign_key "universities", "languages", column: "default_language_id" add_foreign_key "university_apps", "universities" add_foreign_key "university_file_servers", "universities" From 3432c771f377b9e6b74987ec9de22bce97c3a3d6 Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Tue, 8 Sep 2026 15:25:03 +0200 Subject: [PATCH 06/10] redirection --- .../library/files/redirections_controller.rb | 4 + .../apps/file-server/FileServerApp.vue | 170 +++++++++--------- app/models/communication/file/redirection.rb | 1 + 3 files changed, 92 insertions(+), 83 deletions(-) diff --git a/app/controllers/admin/communication/library/files/redirections_controller.rb b/app/controllers/admin/communication/library/files/redirections_controller.rb index 3828630558..af2dc6b22c 100644 --- a/app/controllers/admin/communication/library/files/redirections_controller.rb +++ b/app/controllers/admin/communication/library/files/redirections_controller.rb @@ -10,6 +10,10 @@ def index # - 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 + @l10n.redirections.create( + path: @l10n.file_server_path, + university: @l10n.university, + ) @l10n.file_server_slug = params[:file_server_slug] @l10n.save render :index diff --git a/app/javascript/apps/file-server/FileServerApp.vue b/app/javascript/apps/file-server/FileServerApp.vue index 3bc783fd46..2c0436bc7a 100644 --- a/app/javascript/apps/file-server/FileServerApp.vue +++ b/app/javascript/apps/file-server/FileServerApp.vue @@ -103,98 +103,102 @@ export default { diff --git a/app/models/communication/file/redirection.rb b/app/models/communication/file/redirection.rb index 6f85b1faa5..5182faf45b 100644 --- a/app/models/communication/file/redirection.rb +++ b/app/models/communication/file/redirection.rb @@ -55,6 +55,7 @@ def to_s protected def set_path + return if path.present? self.path = "/#{@path_without_extension}#{extension}" end end From 03558e228881b72463836bc0d33ebddf8006e5e0 Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Tue, 8 Sep 2026 17:03:15 +0200 Subject: [PATCH 07/10] Good! --- .../library/files/redirections_controller.rb | 37 +++++-- .../apps/file-server/FileServerApp.vue | 102 +++++++++--------- .../file/localization/with_file_server.rb | 25 +++++ app/models/communication/file/redirection.rb | 39 ++++++- config/locales/communication/en.yml | 5 + config/locales/communication/fr.yml | 7 +- config/locales/vue/en.yml | 14 +++ config/locales/vue/fr.yml | 1 + 8 files changed, 163 insertions(+), 67 deletions(-) diff --git a/app/controllers/admin/communication/library/files/redirections_controller.rb b/app/controllers/admin/communication/library/files/redirections_controller.rb index af2dc6b22c..6f7c50f24f 100644 --- a/app/controllers/admin/communication/library/files/redirections_controller.rb +++ b/app/controllers/admin/communication/library/files/redirections_controller.rb @@ -10,13 +10,18 @@ def index # - 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 - @l10n.redirections.create( - path: @l10n.file_server_path, - university: @l10n.university, - ) + previous_path = @l10n.file_server_path @l10n.file_server_slug = params[:file_server_slug] - @l10n.save - render :index + 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 @@ -24,13 +29,20 @@ def create path_without_extension: params[:path_without_extension], university: @l10n.university, ) - render :index + if @redirection.persisted? + render :index + else + render_error(@redirection) + end end def destroy @redirection = @l10n.redirections.find(params[:id]) - @redirection.destroy - render :index + if @redirection.destroy + render :index + else + render_error(@redirection) + end end protected @@ -40,4 +52,11 @@ def load_and_authorize 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/javascript/apps/file-server/FileServerApp.vue b/app/javascript/apps/file-server/FileServerApp.vue index 2c0436bc7a..6042543780 100644 --- a/app/javascript/apps/file-server/FileServerApp.vue +++ b/app/javascript/apps/file-server/FileServerApp.vue @@ -9,82 +9,75 @@ export default { }, methods: { async load() { - try { - const response = await fetch(this.endpoint); - if (!response.ok) throw new Error(response.statusText); - this.data = await response.json(); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); + await this.request(this.endpoint); + }, + blockInvalidPermalinkChars(event) { + // Empêche l'insertion avant qu'elle n'atteigne le DOM, pour éviter + // le clignotement d'un caractère invalide le temps que `parsePermalink` + // (déclenché après coup par @input) le retire. + if (event.data && /[^a-zA-Z0-9-]/.test(event.data)) { + event.preventDefault(); } }, parsePermalink(event) { - // TODO filtre - this.data.file_server.slug = event.target.value; + this.data.file_server.slug = event.target.value + .toLowerCase() + .replace(/[^a-z0-9-]/g, ''); this.permalinkChanged = true; }, async savePermalink() { - const response = await fetch(this.data.file_server.endpoint, { + const json = await this.request(this.data.file_server.endpoint, { method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': this.csrfToken, - }, - body: JSON.stringify({ - file_server_slug: this.data.file_server.slug - }), - }); - if (!response.ok) { - throw new Error(response.statusText); - } else { - this.permalinkChanged = false; - this.data = await response.json(); - this.notify(this.$t('fileServer.permalink.changed')); - } + body: { file_server_slug: this.data.file_server.slug }, + }, this.$t('fileServer.permalink.changed')); + if (json) this.permalinkChanged = false; }, parseRedirection(event) { // TODO filtre this.redirection = event.target.value; }, async addRedirection() { - const response = await fetch(this.data.redirections.endpoint, { + await this.request(this.data.redirections.endpoint, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': this.csrfToken, - }, - body: JSON.stringify({ - path_without_extension: this.redirection - }), - }); - if (!response.ok) { - throw new Error(response.statusText); - } else { - this.data = await response.json(); - this.notify(this.$t('fileServer.redirections.added')); - } + body: { path_without_extension: this.redirection }, + }, this.$t('fileServer.redirections.added')); }, async removeRedirection(redirection) { - const response = await fetch(redirection.endpoint, { + await this.request(redirection.endpoint, { method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': this.csrfToken, - }, - }); - if (!response.ok) { - throw new Error(response.statusText); - } else { - this.data = await response.json(); - this.notify(this.$t('fileServer.redirections.removed')); + }, this.$t('fileServer.redirections.removed')); + }, + async request(url, options = {}, successMessage = null) { + try { + const response = await fetch(url, { + method: options.method || 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': this.csrfToken, + }, + body: options.body ? JSON.stringify(options.body) : undefined, + }); + const json = await response.json(); + if (!response.ok) { + this.notify(json.error, 'error'); + return null; + } + this.data = json; + if (successMessage) this.notify(successMessage, 'success'); + return json; + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + this.notify(this.$t('fileServer.error'), 'error'); + return null; } }, - notify(message) { + notify(message, type) { const notyf = new Notyf(); notyf.open({ - type: 'success', - position: { x: 'left', y: 'bottom' }, message: message, + type: type, + position: { x: 'left', y: 'bottom' }, duration: 9000, ripple: true, dismissible: true, @@ -115,6 +108,7 @@ export default { type="text" class="form-control" :value="data.file_server?.slug" + @beforeinput="blockInvalidPermalinkChars" @input="parsePermalink" /> diff --git a/app/models/communication/file/localization/with_file_server.rb b/app/models/communication/file/localization/with_file_server.rb index d50259d020..02e34dac7f 100644 --- a/app/models/communication/file/localization/with_file_server.rb +++ b/app/models/communication/file/localization/with_file_server.rb @@ -2,6 +2,15 @@ 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 @@ -101,4 +110,20 @@ def ftp 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/redirection.rb b/app/models/communication/file/redirection.rb index 5182faf45b..ccf79f6ca2 100644 --- a/app/models/communication/file/redirection.rb +++ b/app/models/communication/file/redirection.rb @@ -32,12 +32,33 @@ class Communication::File::Redirection < ApplicationRecord scope :current, -> { where(is_current: true) } scope :not_current, -> { where(is_current: false) } - validates_presence_of :path - 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.delete_suffix extension + self.path.to_s.delete_suffix extension end def url @@ -54,6 +75,18 @@ def to_s 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}" diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml index 574ee5bb4b..2d13431b03 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 1afc563c18..46c908d516 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/vue/en.yml b/config/locales/vue/en.yml index 3e7396d3e1..1d59d348bb 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 f07c1b809b..397b5d2ced 100644 --- a/config/locales/vue/fr.yml +++ b/config/locales/vue/fr.yml @@ -125,6 +125,7 @@ fr: 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 From 844fa1257b802276fa6817a1e2a1f6ed6a4f644f Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Tue, 8 Sep 2026 17:06:29 +0200 Subject: [PATCH 08/10] oops --- db/schema.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 53b20409dc..58783bf1f0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -3096,7 +3096,6 @@ add_foreign_key "search_index", "communication_extranets", column: "extranet_id" add_foreign_key "search_index", "communication_websites", column: "website_id" add_foreign_key "search_index", "universities" - add_foreign_key "server_emergency_messages", "universities" add_foreign_key "server_evolution_localizations", "languages" add_foreign_key "server_evolution_localizations", "server_evolutions", column: "evolution_id" add_foreign_key "universities", "languages", column: "default_language_id" From b594ae7412e9f1f7dc5507bbcedd494aecc7b124 Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Tue, 8 Sep 2026 23:43:08 +0200 Subject: [PATCH 09/10] htaccess --- .../library/files/redirections_controller.rb | 6 ++++++ .../file/localization/with_file_server.rb | 19 ++++++++++++++++++- app/models/university/with_communication.rb | 5 +++++ app/services/ftp.rb | 8 ++++++++ .../files/redirections/static.html.erb | 9 +++++++++ .../communication/library/files/show.html.erb | 3 +++ config/routes/admin/communication.rb | 1 + 7 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app/views/admin/communication/library/files/redirections/static.html.erb diff --git a/app/controllers/admin/communication/library/files/redirections_controller.rb b/app/controllers/admin/communication/library/files/redirections_controller.rb index 6f7c50f24f..7232f144b2 100644 --- a/app/controllers/admin/communication/library/files/redirections_controller.rb +++ b/app/controllers/admin/communication/library/files/redirections_controller.rb @@ -45,6 +45,12 @@ def destroy end end + def static + @about = @file + render layout: false, + content_type: "text/plain; charset=utf-8" + end + protected def load_and_authorize diff --git a/app/models/communication/file/localization/with_file_server.rb b/app/models/communication/file/localization/with_file_server.rb index 02e34dac7f..f1638f3ecd 100644 --- a/app/models/communication/file/localization/with_file_server.rb +++ b/app/models/communication/file/localization/with_file_server.rb @@ -36,6 +36,7 @@ def sync_to_file_server_safely end update_column :file_server_current_path, file_server_path + send_htaccess end # rapport-annuel @@ -73,7 +74,7 @@ def file_server_remote_directory # /path-on-ftp-server/fr/2026/rapport-annuel.pdf def file_server_remote_path - "#{file_server.ftp_path}#{file_server_path}" + "#{file_server.ftp_path}#{file_server_path}".gsub('//', '/') end # https://files.osuny.org/ @@ -93,6 +94,22 @@ def file_server_url 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 diff --git a/app/models/university/with_communication.rb b/app/models/university/with_communication.rb index 0e5f6c7a00..95c722ca58 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/services/ftp.rb b/app/services/ftp.rb index 25279a830c..16a689c962 100644 --- a/app/services/ftp.rb +++ b/app/services/ftp.rb @@ -18,6 +18,14 @@ def send_blob(blob, directory, filename) 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}" 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 0000000000..7de122cb3f --- /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 bf73462f67..a41ab969cf 100644 --- a/app/views/admin/communication/library/files/show.html.erb +++ b/app/views/admin/communication/library/files/show.html.erb @@ -79,4 +79,7 @@ <% 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/config/routes/admin/communication.rb b/config/routes/admin/communication.rb index be968cb643..c7d5d5638c 100644 --- a/config/routes/admin/communication.rb +++ b/config/routes/admin/communication.rb @@ -327,6 +327,7 @@ defaults: { format: :json } do collection do patch :change_server_slug + get :static, defaults: { format: :html } end end end From f8b171a90866155e20d153b69f9e177d72e3a5ce Mon Sep 17 00:00:00 2001 From: Arnaud Levy Date: Wed, 9 Sep 2026 00:04:10 +0200 Subject: [PATCH 10/10] fux --- db/schema.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 58783bf1f0..a79901301b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -3096,8 +3096,6 @@ add_foreign_key "search_index", "communication_extranets", column: "extranet_id" add_foreign_key "search_index", "communication_websites", column: "website_id" add_foreign_key "search_index", "universities" - add_foreign_key "server_evolution_localizations", "languages" - add_foreign_key "server_evolution_localizations", "server_evolutions", column: "evolution_id" add_foreign_key "universities", "languages", column: "default_language_id" add_foreign_key "university_apps", "universities" add_foreign_key "university_file_servers", "universities"