Méthodologie
Une pull request synthétise la migration d’un objet direct : https://github.com/osunyorg/admin/pull/2123
La méthode est la suivante :
- Modifier la base de données
- Modifier les modèles
- Modifier le controleur
- Modifier les vues
- Modifier les locales
Les routes ne bougent pas !
1. Base de donnnées
Migrer l’objet principal
Toutes les informations localisées sont supprimées de l’organisation elle-même, et déplacées dans la localisation. Pour gérer la migration, cela se fait en 2 passes, d’abord le déplacement, puis la suppression. La partie documentée ici est la première passe.
class CreateCommunicationAgendaEventLocalizations < ActiveRecord::Migration[7.1]
def up
change_column_null :communication_website_agenda_events, :language_id, true
create_table :communication_website_agenda_event_localizations, id: :uuid do |t|
t.jsonb :add_to_calendar_urls
t.string :featured_image_alt
t.text :featured_image_credit
t.string :meta_description
t.string :migration_identifier
t.boolean :published, default: false
t.datetime :published_at
t.string :slug
t.string :subtitle
t.text :summary
t.text :text # No text yet
t.string :title
t.references :about, foreign_key: { to_table: :communication_website_agenda_events }, type: :uuid
t.references :language, foreign_key: true, type: :uuid
t.references :communication_website, foreign_key: true, type: :uuid
t.references :university, foreign_key: true, type: :uuid
t.timestamps
end
end
def down
drop_table :communication_website_agenda_event_localizations
end
end
Migrer les catégories liées
class CreateCommunicationWebsiteAgendaCategoryLocalizations < ActiveRecord::Migration[7.1]
def up
change_column_null :communication_website_agenda_categories, :language_id, true
create_table :communication_website_agenda_category_localizations, id: :uuid do |t|
t.string :featured_image_alt
t.text :featured_image_credit
t.text :meta_description
t.string :name
t.string :path
t.string :slug, index: true
t.text :summary
t.references :about, foreign_key: { to_table: :communication_website_agenda_categories }, type: :uuid
t.references :language, foreign_key: true, type: :uuid
t.references :university, foreign_key: true, type: :uuid
t.references :communication_website, null: false, foreign_key: true, type: :uuid
t.timestamps
end
end
def down
drop_table :communication_website_agenda_category_localizations
end
end
2. Modèles
Créer le modèle de localisation
D’abord on crée le fichier du modèle :
class Communication::Website::Agenda::Event::Localization < ApplicationRecord
...
end
Puis on ajoute les traits nécessaires :
class Communication::Website::Agenda::Event::Localization < ApplicationRecord
include AsLocalization # comportements communs des localisations
include Contentful # blocks
include Initials # initiales pour l'affichage dans les listes si pas de visuel
include Permalinkable # permaliens et alias
include Sanitizable # sécurité par contrôle des entrées
include Shareable # visuel de partage
include WithAccessibility # contrôle d'accessibilité
include WithBlobs # gestion des blobs attachés
include WithCal # trait spécifique aux événements pour la création des liens d'ajout au calendrier
include WithFeaturedImage # visuel principal
include WithGitFiles # fichiers Git
include WithPublication # système de publication
include WithUniversity # multitenant
...
end
Ensuite on ajoute les relations, les délégations et les validations :
class Communication::Website::Agenda::Event::Localization < ApplicationRecord
...
# lien au site Web
belongs_to :website,
class_name: 'Communication::Website',
foreign_key: :communication_website_id
# alias pour clarifier
alias :event :about
# délégations liées aux méthodes laissées dans le modèle `Event`
delegate :archive?,
:from_day, :from_hour,
:to_day, :to_hour,
:time_zone,
to: :event
# validation du titre dans la l10n
validates :title, presence: true
# récupération de l'id du site Web
before_validation :set_communication_website_id
end
On définit les méthodes publiques :
class Communication::Website::Agenda::Event::Localization < ApplicationRecord
...
# le chemin absolu du fichier git dans un site Web
def git_path(website)
return unless website.id == communication_website_id && published && published_at
git_path_content_prefix(website) + git_path_relative
end
# le chemin relatif du fichier git (pas toujours nécessaire)
def git_path_relative
path = "events/"
path += "archives/#{from_day.year}/" if archive?
path += "#{from_day.strftime "%Y-%m-%d"}-#{slug}.html"
path
end
# le chemin du template static
def template_static
"admin/communication/websites/agenda/events/static"
end
# les dépendances
def dependencies
active_storage_blobs +
contents_dependencies
end
# les références (là il n'y en a pas, parce qu'elles sont gérées dans l'événement)
# une méthode d'utilité pour récupérer les catégories localisées (si elles existent)
def categories
about.categories.ordered.map { |category| category.localization_for(language) }.compact
end
def to_s
"#{title}"
end
...
end
Enfin, les méthodes protégées :
class Communication::Website::Agenda::Event::Localization < ApplicationRecord
...
protected
# la vérification d'accessibilité
def check_accessibility
accessibility_merge_array blocks
end
# la restriction du slug
def slug_unavailable?(slug)
self.class.unscoped
.where(communication_website_id: self.communication_website_id, language_id: language_id, slug: slug)
.where.not(id: self.id)
.exists?
end
# la définition du website_id
def set_communication_website_id
self.communication_website_id ||= about.communication_website_id
end
# la liste des blobs
def explicit_blob_ids
super.concat [
featured_image&.blob_id,
shared_image&.blob_id
]
end
# la localisation d'éléments supplémentaires (mais on peut se demander pourquoi l'image de partage n'est pas standard)
def localize_other_attachments(localization)
localize_attachment(localization, :shared_image) if shared_image.attached?
end
end
Modifier l’objet principal
class Communication::Website::Agenda::Event < ApplicationRecord
...
include Contentful # TODO L10N : To remove
...
include Shareable # TODO L10N : To remove
...
include WithBlobs # TODO L10N : To remove
...
include WithFeaturedImage # TODO L10N : To remove
...
# TODO L10N : remove after migrations
has_many :permalinks,
class_name: "Communication::Website::Permalink",
as: :about,
dependent: :destroy
...
end
On supprime les traits inutiles :
Permalinkable
(et on ajoute un has_many permalinks pour permettre la migration)WithCal
parce que les liens d’ajouts calendrier contiennent le titre, et sont donc localisés
On marque les traits à supprimer en phase 2, après migration (TODO L10N : To remove
).
On supprime les méthodes qui passent dans la localisation :
def git_path(website)
def git_path_relative
def template_static
def to_s
def check_accessibility
def explicit_blob_ids
On enlève les active_storage_blobs
des dependencies
Mapper le permalink
Il faut ajouter la localisation au tableau MAPPING pour faire le lien avec la classe en charge des permaliens.
...
"Communication::Website::Agenda::Event::Localization" => Communication::Website::Permalink::Agenda::Event,
...
Gérer les catégories
La logique est identique, cf PR.
Transférer les données
module Migrations
class L10nLocalizations
def self.execute
...
migrate_communication_website_agenda_event_localizations
migrate_communication_website_agenda_category_localizations
migrate_categories Communication::Website::Agenda::Event
end
def self.migrate_communication_website_agenda_event_localizations
Communication::Website::Agenda::Event.find_each do |event|
puts "Migration event #{event.id}"
about_id = event.original_id || event.id
l10n = Communication::Website::Agenda::Event::Localization.create(
add_to_calendar_urls: event.add_to_calendar_urls,
featured_image_alt: event.featured_image_alt,
featured_image_credit: event.featured_image_credit,
meta_description: event.meta_description,
migration_identifier: event.migration_identifier,
published: event.published,
published_at: event.updated_at, # No published_at yet
slug: event.slug,
subtitle: event.subtitle,
summary: event.summary,
title: event.title,
about_id: about_id,
language_id: event.language_id,
communication_website_id: event.communication_website_id,
university_id: event.university_id,
created_at: event.created_at
)
event.translate_contents!(l10n)
event.translate_attachment(l10n, :featured_image)
event.translate_other_attachments(l10n)
event.permalinks.each do |permalink|
new_permalink = permalink.dup
new_permalink.about = l10n
new_permalink.save
end
l10n.save
end
end
def self.migrate_communication_website_agenda_category_localizations
Communication::Website::Agenda::Category.find_each do |category|
about_id = category.original_id || category.id
l10n = Communication::Website::Agenda::Category::Localization.create(
featured_image_alt: category.featured_image_alt,
featured_image_credit: category.featured_image_credit,
meta_description: category.meta_description,
name: category.name,
slug: category.slug,
path: category.path,
about_id: about_id,
language_id: category.language_id,
communication_website_id: category.communication_website_id,
university_id: category.university_id,
created_at: category.created_at
)
category.translate_contents!(l10n)
category.translate_attachment(l10n, :featured_image)
category.permalinks.each do |permalink|
new_permalink = permalink.dup
new_permalink.about = l10n
new_permalink.save
end
l10n.save
end
end
end
end
3. Contrôleur
Il n’y a pas de contrôleur dédié aux localisations, on utilise celui des événements.
Il faut ajouter le trait Admin::HasStaticAction
et supprimer la méthode static.
Dans l’action index, il faut remplacer for_language(current_language)
par tmp_original # TODO L10N : To remove
.
Dans les notices, il faut remplacer les to_s
par des to_s_in(current_language)
.
Enfin, il faut autoriser les attributs des localisations. Cela veut dire :
- couper les propriétés localisées de l’événement
- créer une propriété
localizations_attributes
- dedans, intégrer
id
etlanguage_id
- coller les propriétés localisées
def event_params
params.require(:communication_website_agenda_event)
.permit(
:from_day, :from_hour, :to_day, :to_hour, :time_zone,
category_ids: [],
localizations_attributes: [
:id, :title, :subtitle, :meta_description, :summary, :text,
:published, :published_at, :slug,
:featured_image, :featured_image_delete, :featured_image_infos, :featured_image_alt, :featured_image_credit,
:shared_image, :shared_image_delete, :shared_image_infos,
:language_id
]
)
.merge(
university_id: current_university.id,
language_id: current_language.id
)
end
4. Vues
Liste
...
<%
events.each do |event|
event_l10n = event.best_localization_for(current_language)
%>
<div>
<div class="card card--horizontal">
<%= osuny_thumbnail_localized event %>
<div class="card-body">
<%= osuny_published_localized event unless small %>
<%= osuny_link_localized event,
admin_communication_website_agenda_event_path(
website_id: event.website.id,
id: event.id
),
classes: "stretched-link" %>
<% if event_l10n.subtitle.present? %>
<br>
<span class="text-muted">
<%= event_l10n.subtitle %>
</span>
<% end %>
</div>
<div class="card-footer text-end text-muted">
<%= render 'admin/communication/websites/agenda/events/dates', event: event %>
</div>
</div>
</div>
</div>
...
Lecture
<% content_for :title, @l10n %>
<div class="row">
<div class="col-lg-7">
<%= osuny_panel Communication::Website::Agenda::Event::Localization.human_attribute_name(:title), small: true do %>
<p class="lead"><%= @l10n.title %></p>
<% if @l10n.subtitle.present? %>
<p class="mt-n3 text-muted"><%= @l10n.subtitle %></p>
<% end %>
<p><%= render 'admin/communication/websites/agenda/events/dates', event: @event, detailed: true %></p>
<% end %>
</div>
...
Même logique que dans le formulaire, il faut aller chercher les valeurs localisées dans @l10n
et les valeurs non localisées dans @event
.
Cela concerne aussi le partiel metadata.
Création
<% content_for :title, Communication::Website::Agenda::Event.model_name.human %>
<%= render 'form', event: @event, l10n: @l10n %>
Édition
<% content_for :title, @l10n %>
<%= render 'form', event: @event, l10n: @l10n %>
On prend le titre localisé, et on passe les 2 objets au formulaire.
Formulaire
<%= simple_form_for [:admin, event] do |f| %>
<%= f.simple_fields_for :localizations, l10n do |lf| %>
<%= f.error_notification %>
<%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
<%= lf.hidden_field :language_id, value: current_language.id %>
<div class="row">
<div class="col-md-8">
<%= osuny_panel t('content') do %>
<%= lf.input :title, input_html: { data: { translatable: true } } %>
<%= lf.input :subtitle, input_html: { data: { translatable: true } } %>
<%= render 'admin/application/summary/form', f: lf, about: l10n %>
<% end %>
<%= osuny_panel Communication::Website::Agenda::Event.human_attribute_name('dates') do %>
<div class="row pure__row--small">
<div class="col-md-6">
<%= f.input :from_day, html5: true %>
</div>
<div class="col-md-6">
<%= f.input :from_hour, html5: true %>
</div>
...
On ajoute un formulaire niché pour la localisation (lf, pour localisation_form), et on fait pointer les propriétés vers f
ou lf
.
On ajoute également un champ caché pour la langue.
Dans cet exemple, le titre est localisé, la date de début ne l’est pas.
Statique
<%
event = @l10n.about
language = @l10n.language
%>---
<%= render 'admin/application/static/title', about: @l10n %>
subtitle: >-
<%= prepare_text_for_static @l10n.subtitle %>
<%
# https://github.com/osunyorg/admin/issues/1880
if event.archive? %>
date: "<%= event.from_day&.iso8601 %>"
<% elsif event.current? %>
weight: -1
date: "<%= event.from_day&.iso8601 %>"
<% else %>
weight: <%= event.distance_in_days %>
<% end %>
<%= render 'admin/application/static/permalink', about: @l10n %>
<%= render 'admin/application/static/breadcrumbs', about: @l10n %>
<%= render 'admin/application/static/design',
about: event,
full_width: false,
toc_offcanvas: false %>
<%= render 'admin/communication/websites/agenda/events/static/dates',
event: event,
l10n: @l10n,
locale: language.iso_code %>
<%= render 'admin/application/l10n/static', about: @l10n %>
<%= render 'admin/application/featured_image/static', about: @l10n %>
<%= render 'admin/application/shared_image/static', about: @l10n %>
<%= render 'admin/application/meta_description/static', about: @l10n %>
<%= render 'admin/application/summary/static', about: @l10n %>
<% if @l10n.categories.any? %>
events_categories:
<% @l10n.categories.ordered.each do |category| %>
- "<%= category.slug %>"
<% end %>
<% end %>
<%= render 'admin/communication/blocks/content/static', about: @l10n %>
---
Là encore, même logique, on prend au bon endroit.
Petite particularité sur @l10n.categories
, qui renvoie des catégories localisées directement.
5. Locales
communication/website/agenda/event:
categories: Catégories
dates: Dates
from_day: Jour de début
from_hour: Heure de début
status: Statut
status_archive: Archive
status_current: En cours
status_future: À venir
time_zone: Fuseau horaire
to_day: Jour de fin
to_hour: Heure de fin
communication/website/agenda/event/localization:
featured_image: Image à la une
published: Publié
subtitle: Sous-titre
title: Titre
On déplace comme précédemment, en français et en anglais.