diff --git a/app/models/concerns/metadata/inverse_concern.rb b/app/models/concerns/metadata/inverse_concern.rb new file mode 100644 index 00000000..aa300fa7 --- /dev/null +++ b/app/models/concerns/metadata/inverse_concern.rb @@ -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 diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb index 204d7bed..93a1f710 100644 --- a/app/models/metadata_belongs_to.rb +++ b/app/models/metadata_belongs_to.rb @@ -3,6 +3,8 @@ # Almacena el UUID de otro Post y actualiza el valor en el Post # relacionado. class MetadataBelongsTo < MetadataRelatedPosts + include Metadata::InverseConcern + # TODO: Convertir algunos tipos de valores en módulos para poder # implementar varios tipos de campo sin repetir código # @@ -20,86 +22,12 @@ class MetadataBelongsTo < MetadataRelatedPosts document.data[name.to_s] 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 - belongs_to&.title&.value + posts.find_by_post_uuid(value).try(:title) end private - def post_exists? - return true if sanitize(value).blank? - - sanitize(value).present? && belongs_to.present? - end - def sanitize(uuid) uuid.to_s.gsub(/[^a-f0-9\-]/i, '') end diff --git a/app/models/metadata_has_and_belongs_to_many.rb b/app/models/metadata_has_and_belongs_to_many.rb index 2c4f3d43..2c1b0f96 100644 --- a/app/models/metadata_has_and_belongs_to_many.rb +++ b/app/models/metadata_has_and_belongs_to_many.rb @@ -1,46 +1,5 @@ # frozen_string_literal: true -# Establece una relación de muchos a muchos artículos. Cada campo es un -# 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. +# Establece una relación de muchos a muchos artículos 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 diff --git a/app/models/metadata_has_many.rb b/app/models/metadata_has_many.rb index 82ec333a..c5f01f7c 100644 --- a/app/models/metadata_has_many.rb +++ b/app/models/metadata_has_many.rb @@ -6,59 +6,5 @@ # Localmente tenemos un Array de UUIDs. Remotamente tenemos una String # apuntando a un Post, que se mantiene actualizado como el actual. class MetadataHasMany < MetadataRelatedPosts - # Todos los Post relacionados - # - # @return [Array] - def has_many - return default_value if value.blank? - - posts.where(post_id: value).map(&:post) - end - - # La relación anterior - # - # @return [Array] - 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 + include Metadata::InverseConcern end diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 0d989871..dc04dda6 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -3,7 +3,7 @@ # Este servicio se encarga de crear artículos y guardarlos en git, # asignándoselos a une usuarie 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 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? 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 # errores @@ -25,6 +37,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do end # Crear un post anónimo, con opciones más limitadas. No usamos post. + # + # @todo Permitir asociaciones? def create_anonymous # XXX: Confiamos en el parámetro de idioma porque estamos # verificándolos en Site#posts @@ -36,20 +50,30 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do post end + # Al actualizar, modificamos un post pre-existente, todas las + # relaciones anteriores y las relaciones actuales. def update post.usuaries << usuarie params[:post][:draft] = true if site.invitade? usuarie - # Eliminar ("mover") el archivo si cambió de ubicación. if post.update(post_params) + # Eliminar ("mover") el archivo si cambió de ubicación. rm = [] rm << post.path.value_was if post.path.changed? - # Es importante que el artículo se guarde primero y luego los - # relacionados. - commit(action: :updated, add: update_related_posts, rm: rm) + 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: :updated, add: added_paths, rm: rm) end # 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 - # 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 # personalizada en el panel. 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') end end + + # @return [Array] + 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] + def association_attributes(post) + post.attributes.select do |attribute| + post[attribute].try(:inverse?) + end + end + + # @param :post_ids [Array] + # @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 diff --git a/app/views/posts/attribute_ro/_belongs_to.haml b/app/views/posts/attribute_ro/_belongs_to.haml index c7e06be8..7410e921 100644 --- a/app/views/posts/attribute_ro/_belongs_to.haml +++ b/app/views/posts/attribute_ro/_belongs_to.haml @@ -1,6 +1,6 @@ %tr{ id: attribute } %th= post_label_t(attribute, post: post) %td{ dir: dir, lang: locale } - - p = metadata.belongs_to + - p = site.indexed_posts.find_by_post_id(metadata.value) - if p - = link_to p.title.value, site_post_path(site, p.id) + = link_to p.title, site_post_path(site, p.path) diff --git a/app/views/posts/attribute_ro/_has_and_belongs_to_many.haml b/app/views/posts/attribute_ro/_has_and_belongs_to_many.haml index d6b51a7a..29c0816f 100644 --- a/app/views/posts/attribute_ro/_has_and_belongs_to_many.haml +++ b/app/views/posts/attribute_ro/_has_and_belongs_to_many.haml @@ -2,5 +2,5 @@ %th= post_label_t(attribute, post: post) %td %ul{ dir: dir, lang: locale } - - metadata.has_many.each do |p| - %li= link_to p.title.value, site_post_path(site, p.id) + - site.indexed_posts.where(post_id: metadata.value).find_each do |p| + %li= link_to p.title, site_post_path(site, p.path) diff --git a/app/views/posts/attribute_ro/_has_many.haml b/app/views/posts/attribute_ro/_has_many.haml index d6b51a7a..29c0816f 100644 --- a/app/views/posts/attribute_ro/_has_many.haml +++ b/app/views/posts/attribute_ro/_has_many.haml @@ -2,5 +2,5 @@ %th= post_label_t(attribute, post: post) %td %ul{ dir: dir, lang: locale } - - metadata.has_many.each do |p| - %li= link_to p.title.value, site_post_path(site, p.id) + - site.indexed_posts.where(post_id: metadata.value).find_each do |p| + %li= link_to p.title, site_post_path(site, p.path)