# 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 # Si estamos pasando el UUID con los parámetros, el post quizás # existe. # # @return [Post] def create_or_update uuid = params.require(base).permit(:uuid).values.first if uuid.blank? create elsif (indexed_post = site.indexed_posts.find_by(post_id: uuid)).present? self.post = indexed_post.post update else create end end # Crea un artículo nuevo # # @return Post def create self.post ||= site.posts(lang: locale).build(layout: layout) post.usuaries << usuarie post.draft.value = true if site.invitade? usuarie post.assign_attributes(post_params) params.require(base).permit(:slug).tap do |p| post.slug.value = p[:slug] if p[:slug].present? end # Crea los posts anidados create_nested_posts! post, params[base] post.save update_related_posts commit(action: :created, add: files) if post.valid? update_site_license! # 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. def create_anonymous # XXX: Confiamos en el parámetro de idioma porque estamos # verificándolos en Site#posts self.post = site.posts(lang: locale) .build(layout: layout) # Los artículos anónimos siempre son borradores params[:draft] = true commit(action: :created, add: files) if post.update(anon_post_params) post end def update post.usuaries << usuarie params[base][:draft] = true if site.invitade? usuarie # Eliminar ("mover") el archivo si cambió de ubicación. if post.update(post_params) rm = [] rm << post.path.value_was if post.path.changed? create_nested_posts! post, params[base] update_related_posts # Es importante que el artículo se guarde primero y luego los # relacionados. commit(action: :updated, add: files, rm: rm) update_site_license! end # Devolver el post aunque no se haya salvado para poder rescatar los # errores post end 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(base).permit(reorder: {})&.dig(:reorder)&.transform_values(&:to_i) posts = site.posts(lang: locale).where(uuid: reorder.keys) 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.save_all(validate: false) && commit(action: :reorder, add: files) end private # La base donde buscar los parámetros # # @return [Symbol] def base @base ||= params.permit(:base).try(:[], :base).try(:to_sym) || :post end # Una lista de archivos a modificar # # @return [Set] def files @files ||= Set.new.tap do |f| f << post.path.absolute end end 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 @post_params ||= params.require(base).permit(post.params).to_h 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 def locale params.dig(base, :lang)&.to_sym || I18n.locale end def layout params.dig(base, :layout) || params[:layout] 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| next unless p.save(validate: false) files << p.path.absolute end end # Si les usuaries modifican o crean una licencia, considerarla # personalizada en el panel. def update_site_license! return unless site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom? site.update licencia: Licencia.find_by_icons('custom') end # Encuentra todos los posts anidados y los crea o modifica def create_nested_posts!(post, params) post.nested_attributes.each do |nested_attribute| nested_metadata = post[nested_attribute] next unless params[nested_metadata].present? # @todo find_or_initialize nested_post = nested_metadata.has_one || site.posts(lang: post.lang.value).build(layout: nested_metadata.nested) nested_params = params.require(nested_attribute).permit(nested_post.params).to_hash # Completa la relación 1:1 nested_params[nested_metadata.inverse.to_s] = post.uuid.value post[nested_attribute].value = nested_post.uuid.value files << nested_post.path.absolute if nested_post.update(nested_params) end end end