5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-26 10:56:22 +00:00

refactor: guardar las asociaciones en PostService

(cherry picked from commit 4cf57a441a)
This commit is contained in:
f 2023-10-25 15:29:22 -03:00
parent 26965ea120
commit 232cda1379
No known key found for this signature in database
8 changed files with 124 additions and 210 deletions

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Metadata
module InverseConcern
extend ActiveSupport::Concern
included do
# Hay una relación inversa?
#
# @return [Boolean]
def inverse?
inverse.present?
end
# La relación inversa
#
# @return [Nil,Symbol]
def inverse
@inverse ||= layout.metadata.dig(name, 'inverse')&.to_sym
end
end
end
end

View file

@ -3,6 +3,8 @@
# Almacena el UUID de otro Post y actualiza el valor en el Post # Almacena el UUID de otro Post y actualiza el valor en el Post
# relacionado. # relacionado.
class MetadataBelongsTo < MetadataRelatedPosts class MetadataBelongsTo < MetadataRelatedPosts
include Metadata::InverseConcern
# TODO: Convertir algunos tipos de valores en módulos para poder # TODO: Convertir algunos tipos de valores en módulos para poder
# implementar varios tipos de campo sin repetir código # implementar varios tipos de campo sin repetir código
# #
@ -20,86 +22,12 @@ class MetadataBelongsTo < MetadataRelatedPosts
document.data[name.to_s] document.data[name.to_s]
end end
def validate
super
errors << I18n.t('metadata.belongs_to.missing_post') unless post_exists?
errors.empty?
end
# Guardar y guardar la relación inversa también, eliminando la
# relación anterior si existía.
def save
super
# Si no hay relación inversa, no hacer nada más
return true unless changed?
return true unless inverse?
# Si estamos cambiando la relación, tenemos que eliminar la relación
# anterior
if belonged_to.present?
belonged_to[inverse].value = belonged_to[inverse].value.reject do |rej|
rej == post.uuid.value
end
end
# No duplicar las relaciones
belongs_to[inverse].value = (belongs_to[inverse].value.dup << post.uuid.value) unless belongs_to.blank? || included?
true
end
# El Post actual está incluido en la relación inversa?
def included?
belongs_to[inverse].value.include?(post.uuid.value)
end
# Hay una relación inversa y el artículo existe?
def inverse?
inverse.present?
end
# El campo que es la relación inversa de este
def inverse
@inverse ||= layout.metadata.dig(name, 'inverse')&.to_sym
end
# El Post relacionado con este artículo
#
# @return [Post,nil]
def belongs_to
posts.find_by(post_id: value)&.post if value.present?
end
# El artículo relacionado anterior
#
# @return [Post,nil]
def belonged_to
posts.find_by(post_id: value_was)&.post if value_was.present?
end
def related_posts?
true
end
def related_methods
@related_methods ||= %i[belongs_to belonged_to].freeze
end
def indexable_values def indexable_values
belongs_to&.title&.value posts.find_by_post_uuid(value).try(:title)
end end
private private
def post_exists?
return true if sanitize(value).blank?
sanitize(value).present? && belongs_to.present?
end
def sanitize(uuid) def sanitize(uuid)
uuid.to_s.gsub(/[^a-f0-9\-]/i, '') uuid.to_s.gsub(/[^a-f0-9\-]/i, '')
end end

View file

@ -1,46 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
# Establece una relación de muchos a muchos artículos. Cada campo es un # Establece una relación de muchos a muchos artículos
# Array de UUID que se mantienen sincronizados.
#
# Por ejemplo:
#
# Un libro puede tener muches autores y une autore muchos libros. La
# relación has_many tiene que traer todes les autores relacionades con
# el libro actual. La relación belongs_to tiene que traer todes les
# autores que tienen este libro. La relación es bidireccional, no hay
# diferencia entre has_many y belongs_to.
class MetadataHasAndBelongsToMany < MetadataHasMany class MetadataHasAndBelongsToMany < MetadataHasMany
# Mantiene la relación inversa si existe.
#
# La relación belongs_to se mantiene actualizada en la modificación
# actual. Lo que buscamos es mantener sincronizada esa relación.
#
# Buscamos en belongs_to la relación local, si se eliminó hay que
# quitarla de la relación remota, sino hay que agregarla.
#
def save
# XXX: No usamos super
self[:value] = sanitize value
return true unless changed?
return true unless inverse?
# XXX: Usamos asignación para aprovechar value= que setea el valor
# anterior en @value_was
(had_many - has_many).each do |remove|
remove[inverse].value = remove[inverse].value.reject do |rej|
rej == post.uuid.value
end
end
(has_many - had_many).each do |add|
next unless add[inverse]
next if add[inverse].value.include? post.uuid.value
add[inverse].value = (add[inverse].value.dup << post.uuid.value)
end
true
end
end end

View file

@ -6,59 +6,5 @@
# Localmente tenemos un Array de UUIDs. Remotamente tenemos una String # Localmente tenemos un Array de UUIDs. Remotamente tenemos una String
# apuntando a un Post, que se mantiene actualizado como el actual. # apuntando a un Post, que se mantiene actualizado como el actual.
class MetadataHasMany < MetadataRelatedPosts class MetadataHasMany < MetadataRelatedPosts
# Todos los Post relacionados include Metadata::InverseConcern
#
# @return [Array<Post>]
def has_many
return default_value if value.blank?
posts.where(post_id: value).map(&:post)
end
# La relación anterior
#
# @return [Array<Post>]
def had_many
return default_value if value_was.blank?
posts.where(post_id: value_was).map(&:post)
end
def inverse?
inverse.present?
end
# La relación inversa
#
# @return [Nil,Symbol]
def inverse
@inverse ||= layout.metadata.dig(name, 'inverse')&.to_sym
end
# Actualizar las relaciones inversas. Hay que buscar la diferencia
# entre had y has_many.
def save
super
return true unless changed?
return true unless inverse?
(had_many - has_many).each do |remove|
remove[inverse]&.value = remove[inverse].default_value
end
(has_many - had_many).each do |add|
add[inverse]&.value = post.uuid.value
end
true
end
def related_posts?
true
end
def related_methods
@related_methods ||= %i[has_many had_many].freeze
end
end end

View file

@ -3,7 +3,7 @@
# Este servicio se encarga de crear artículos y guardarlos en git, # Este servicio se encarga de crear artículos y guardarlos en git,
# asignándoselos a une usuarie # asignándoselos a une usuarie
PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
# Crea un artículo nuevo # Crea un artículo nuevo y modificar las asociaciones
# #
# @return Post # @return Post
def create def create
@ -15,9 +15,21 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
post.slug.value = p[:slug] if p[:slug].present? post.slug.value = p[:slug] if p[:slug].present?
end end
commit(action: :created, add: update_related_posts) if post.update(post_params) if post.update(post_params)
added_paths = []
added_paths << post.path.value
update_site_license! # Recorrer todas las asociaciones y agregarse donde corresponda
update_associations_forward(post)
associated_posts_to_save.each do |associated_post|
next unless associated_post.save(validate: false)
added_paths << associated_post.path.value
end
commit(action: :created, add: added_paths)
end
# Devolver el post aunque no se haya salvado para poder rescatar los # Devolver el post aunque no se haya salvado para poder rescatar los
# errores # errores
@ -25,6 +37,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
end end
# Crear un post anónimo, con opciones más limitadas. No usamos post. # Crear un post anónimo, con opciones más limitadas. No usamos post.
#
# @todo Permitir asociaciones?
def create_anonymous def create_anonymous
# XXX: Confiamos en el parámetro de idioma porque estamos # XXX: Confiamos en el parámetro de idioma porque estamos
# verificándolos en Site#posts # verificándolos en Site#posts
@ -36,20 +50,30 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
post post
end end
# Al actualizar, modificamos un post pre-existente, todas las
# relaciones anteriores y las relaciones actuales.
def update def update
post.usuaries << usuarie post.usuaries << usuarie
params[:post][:draft] = true if site.invitade? usuarie params[:post][:draft] = true if site.invitade? usuarie
# Eliminar ("mover") el archivo si cambió de ubicación.
if post.update(post_params) if post.update(post_params)
# Eliminar ("mover") el archivo si cambió de ubicación.
rm = [] rm = []
rm << post.path.value_was if post.path.changed? rm << post.path.value_was if post.path.changed?
# Es importante que el artículo se guarde primero y luego los added_paths = []
# relacionados. added_paths << post.path.value
commit(action: :updated, add: update_related_posts, rm: rm)
update_site_license! # Recorrer todas las asociaciones y agregarse donde corresponda
update_associations_forward(post)
associated_posts_to_save.each do |associated_post|
next unless associated_post.save(validate: false)
added_paths << associated_post.path.value
end
commit(action: :updated, add: added_paths, rm: rm)
end end
# Devolver el post aunque no se haya salvado para poder rescatar los # Devolver el post aunque no se haya salvado para poder rescatar los
@ -128,30 +152,6 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
] ]
end end
# Actualiza los artículos relacionados según los métodos que los
# metadatos declaren.
#
# Este método se asegura que todos los artículos se guardan una sola
# vez.
#
# @return [Array] Lista de archivos modificados
def update_related_posts
posts = Set.new
post.attributes.each do |a|
post[a].related_methods.each do |m|
next unless post[a].respond_to? m
# La respuesta puede ser una PostRelation también
posts.merge [post[a].public_send(m)].flatten.compact
end
end
posts.map do |p|
p.path.absolute if p.save(validate: false)
end.compact << post.path.absolute
end
# Si les usuaries modifican o crean una licencia, considerarla # Si les usuaries modifican o crean una licencia, considerarla
# personalizada en el panel. # personalizada en el panel.
def update_site_license! def update_site_license!
@ -159,4 +159,62 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
site.update licencia: Licencia.find_by_icons('custom') site.update licencia: Licencia.find_by_icons('custom')
end end
end end
# @return [Array<String>]
def associated_posts_to_save
@associated_posts_to_save ||= Set.new
end
# Recolectar campos asociados que no estén vacíos
#
# @param [Post]
# @return [Array<Symbol>]
def association_attributes(post)
post.attributes.select do |attribute|
post[attribute].try(:inverse?)
end
end
# @param :post_ids [Array<String>]
# @return [Association]
def associated_posts(post_ids)
site.indexed_posts.where(post_id: post_ids).map(&:post)
end
# Modificar las asociaciones.
#
# Si el valor actual es una String, es un BelongsTo
#
#
def update_associations_forward(post)
association_attributes(post).each do |attribute|
metadata = post[attribute]
next unless metadata.changed?
inverse_attribute = post[attribute].inverse
value_was = metadata.value_was.dup
value = metadata.value.dup
case metadata.type
when 'has_and_belongs_to_many'
binding.pry
associated_posts(value_was - value).each do |remove_post|
remove_post[inverse_attribute].value.delete(post.uuid.value)
associated_posts_to_save << remove_post
end
associated_posts(value - value_was).each do |add_post|
add_post[inverse_attribute].value << post.uuid.value
add_post[inverse_attribute].value.uniq!
associated_posts_to_save << add_post
end
when 'has_many'
when 'belongs_to'
when 'locales'
end
end
end
end end

View file

@ -1,6 +1,6 @@
%tr{ id: attribute } %tr{ id: attribute }
%th= post_label_t(attribute, post: post) %th= post_label_t(attribute, post: post)
%td{ dir: dir, lang: locale } %td{ dir: dir, lang: locale }
- p = metadata.belongs_to - p = site.indexed_posts.find_by_post_id(metadata.value)
- if p - if p
= link_to p.title.value, site_post_path(site, p.id) = link_to p.title, site_post_path(site, p.path)

View file

@ -2,5 +2,5 @@
%th= post_label_t(attribute, post: post) %th= post_label_t(attribute, post: post)
%td %td
%ul{ dir: dir, lang: locale } %ul{ dir: dir, lang: locale }
- metadata.has_many.each do |p| - site.indexed_posts.where(post_id: metadata.value).find_each do |p|
%li= link_to p.title.value, site_post_path(site, p.id) %li= link_to p.title, site_post_path(site, p.path)

View file

@ -2,5 +2,5 @@
%th= post_label_t(attribute, post: post) %th= post_label_t(attribute, post: post)
%td %td
%ul{ dir: dir, lang: locale } %ul{ dir: dir, lang: locale }
- metadata.has_many.each do |p| - site.indexed_posts.where(post_id: metadata.value).find_each do |p|
%li= link_to p.title.value, site_post_path(site, p.id) %li= link_to p.title, site_post_path(site, p.path)