# frozen_string_literal: true # 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 y modificar las asociaciones # # @return Post def create self.post = Post.build(site: site, locale: locale, layout: layout) post.usuaries << usuarie params[:post][:draft] = true if site.invitade? usuarie params.require(:post).permit(:slug).tap do |p| post.slug.value = p[:slug] if p[:slug].present? end if post.update(post_params) added_paths << post.path.value # Recorrer todas las asociaciones y agregarse donde corresponda update_associations(post) commit(action: :created, add: added_paths) end # Devolver el post aunque no se haya salvado para poder rescatar los # errores post 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 self.post = Post.build(site: site, locale: locale, layout: layouts) # Los artículos anónimos siempre son borradores params[:draft] = true commit(action: :created, add: [post.path.absolute]) if post.update(anon_post_params) 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 if post.update(post_params) # Eliminar ("mover") el archivo si cambió de ubicación. rm = [] rm << post.path.value_was if post.path.changed? added_paths << post.path.value # Recorrer todas las asociaciones y agregarse donde corresponda update_associations(post) commit(action: :updated, add: added_paths, rm: rm) end # Devolver el post aunque no se haya salvado para poder rescatar los # errores post end # @todo Eliminar relaciones def destroy post.destroy! commit(action: :destroyed, rm: [post.path.absolute]) if post.destroyed? post end # Reordena todos los posts que soporten orden de acuerdo a un hash de # uuids y nuevas posiciones. La posición actual la da la posición en # el array. # # { uuid => 2, uuid => 1, uuid => 0 } def reorder reorder = params.require(:post).permit(reorder: {})&.dig(:reorder)&.transform_values(&:to_i) posts = site.indexed_posts.where(locale: locale, post_id: reorder.keys).map(&:post) files = posts.map do |post| next unless post.attribute? :order order = reorder[post.uuid.value] next if post.order.value == order post.order.value = order post.path.absolute end.compact return if files.empty? # TODO: Implementar transacciones! posts.map do |post| post.save(validate: false) end commit(action: :reorder, add: files) end private def commit(action:, add: [], rm: []) site.repository.commit(add: add, rm: rm, usuarie: usuarie, message: I18n.t("post_service.#{action}", title: post&.title&.value)) GitPushJob.perform_later(site) end # Solo permitir cambiar estos atributos de cada articulo def post_params params.require(:post).permit(post.params) end # Eliminar metadatos internos def anon_post_params params.permit(post.params).delete_if do |k, _| %w[date slug order uuid].include? k.to_s end end # @return [Symbol] def locale params.dig(:post, :lang)&.to_sym || I18n.locale end # @return [Layout] def layout site.layouts[ (params.dig(:post, :layout) || params[:layout]).to_sym ] end # Si les usuaries modifican o crean una licencia, considerarla # personalizada en el panel. def update_site_license! if site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom? site.update licencia: Licencia.find_by_icons('custom') end end # @return [Set] def associated_posts_to_save @associated_posts_to_save ||= Set.new end # @return [Set] def added_paths @added_paths ||= 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 en cascada, manteniendo reciprocidad # y guardando los archivos correspondientes. # # HABTM, Locales: si se rompe de un lado se elimina en el otro y lo # mismo si se agrega. # # HasMany: la relación es de uno a muchos. Al quitar uno, se elimina # la relación inversa. Al agregar uno, se elimina su relación # anterior en el tercer Post y se actualiza con la nueva. # # BelongsTo: la inversa de HasMany. Al cambiarla, se quita de la # relación anterior y se agrega en la nueva. # # @param :post [Post] # @return [nil] def update_associations(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', 'locales' associated_posts(value_was - value).each do |remove_post| remove_relation_from(remove_post[inverse_attribute], post.uuid.value) end associated_posts(value - value_was).each do |add_post| add_relation_to(add_post[inverse_attribute], post.uuid.value) end when 'has_many' associated_posts(value_was - value).each do |remove_post| remove_relation_from(remove_post[inverse_attribute], '') end associated_posts(value - value_was).each do |add_post| associated_posts(add_post[inverse_attribute].value_was).each do |remove_post| remove_relation_from(remove_post[attribute], add_post.uuid.value) end add_relation_to(add_post[inverse_attribute], post.uuid.value) end when 'belongs_to', 'has_one' if value_was.present? associated_posts(value_was).each do |remove_post| remove_relation_from(remove_post[inverse_attribute], post.uuid.value) end end associated_posts(value).each do |add_post| add_relation_to(add_post[inverse_attribute], post.uuid.value) end end end associated_posts_to_save.each do |associated_post| next unless associated_post.save(validate: false) added_paths << associated_post.path.value end nil end # @todo por qué no podemos usar nil para deshabilitar un valor? # @param :metadata [MetadataTemplate] # @param :value [String] # @return [nil] def remove_relation_from(metadata, value) case metadata.value when Array then metadata.value.delete(value) when String then metadata.value = '' end associated_posts_to_save << metadata.post nil end # @todo El validador ya debería eliminar valores duplicados # @param :metadata [MetadataTemplate] # @param :value [String] # @return [nil] def add_relation_to(metadata, value) case metadata.value when Array metadata.value << value metadata.value.uniq! when String then metadata.value = value end associated_posts_to_save << metadata.post nil end end