# frozen_string_literal: true require 'jekyll/utils' # Esta clase representa un post en un sitio jekyll e incluye métodos # para modificarlos y crear nuevos. # # rubocop:disable Metrics/ClassLength # rubocop:disable Style/MethodMissingSuper # rubocop:disable Style/MissingRespondToMissing class Post < OpenStruct # Atributos por defecto # XXX: Volver document opcional cuando estemos creando DEFAULT_ATTRIBUTES = %i[site document layout].freeze # Otros atributos que no vienen en los metadatos ATTRIBUTES = %i[content lang path date slug attributes errors].freeze # Redefinir el inicializador de OpenStruct # # @param site: [Site] el sitio en Sutty # @param document: [Jekyll::Document] el documento leído por Jekyll # @param layout: [Layout] la plantilla # # rubocop:disable Metrics/AbcSize def initialize(**args) default_attributes_missing(args) super(args) # Genera un método con todos los atributos disponibles self.attributes = DEFAULT_ATTRIBUTES + ATTRIBUTES + layout.metadata.keys.map(&:to_sym) # El contenido # TODO: Mover a su propia clase para poder hacer limpiezas # independientemente self.content = document.content self.errors = {} # Genera un atributo por cada uno de los campos de la plantilla, # MetadataFactory devuelve un tipo de campo por cada campo. A # partir de ahí se pueden obtener los valores actuales y una lista # de valores por defecto. layout.metadata.each_pair do |name, template| send "#{name}=".to_sym, MetadataFactory.build(document: document, post: self, site: site, name: name, layout: layout, type: template['type'], label: template['label'], help: template['help'], required: template['required']) end load_slug! load_date! load_path! # Leer el documento read end # rubocop:enable Metrics/AbcSize # Levanta un error si al construir el artículo no pasamos un atributo. def default_attributes_missing(**args) DEFAULT_ATTRIBUTES.each do |attr| i18n = I18n.t("exceptions.post.#{attr}_missing") raise ArgumentError, i18n unless args[attr].present? end end # Solo ejecuta la magia de OpenStruct si el campo existe en la # plantilla # # XXX: Reemplazarlo por nuestro propio método, mantener todo lo demás # compatible con OpenStruct # # XXX: rubocop dice que tenemos que usar super cuando ya lo estamos # usando... def method_missing(mid, *args) unless attribute? mid raise NoMethodError, I18n.t('exceptions.post.no_method', method: mid) end # Definir los attribute_* new_attribute_was(mid) new_attribute_changed(mid) # OpenStruct super(mid, *args) # Devolver lo mismo que devuelve el método después de definirlo send(mid, *args) end # Detecta si es un atributo válido o no, a partir de la tabla de la # plantilla def attribute?(mid) if singleton_class.method_defined? :attributes attributes.include? attribute_name(mid) else (DEFAULT_ATTRIBUTES + ATTRIBUTES).include? attribute_name(mid) end end # Genera el post con metadatos en YAML # rubocop:disable Metrics/AbcSize def full_content yaml = layout.metadata.keys.map(&:to_sym).map do |metadata| template = send(metadata) { metadata.to_s => template.value } unless template.empty? end.compact.inject(:merge) # Asegurarse que haya un layout yaml['layout'] = layout.name.to_s "#{yaml.to_yaml}---\n\n#{content}" end # Eliminar el artículo del repositorio y de la lista de artículos del # sitio # # XXX Commit def destroy FileUtils.rm_f path.absolute site.posts(lang: lang).delete_if do |post| post.path.absolute == path.absolute end !File.exist?(path.absolute) && !site.posts(lang: lang).include?(self) end alias destroy! destroy # rubocop:enable Metrics/AbcSize # Guarda los cambios def save return false unless valid? # Salir si tenemos que cambiar el nombre del archivo y no pudimos return false if !new? && path_changed? && !update_path! return false unless write # Vuelve a leer el post para tomar los cambios read true end alias save! save # Lee el documento a menos que estemos trabajando con un documento en # blanco def read document.read unless new? end def new? document.path.blank? end # Actualizar la ubicación del archivo si cambió de lugar y si no # existe el destino def update_path! !File.exist?(path.absolute) && FileUtils.mv(path_was, path.absolute) && document.path = path.absolute end # Detecta si el artículo es válido para guardar def valid? validate errors.blank? end # Requisitos para que el post sea válido def validate self.errors = {} layout.metadata.keys.map(&:to_sym).each do |metadata| template = send(metadata) errors[metadata] = template.errors unless template.valid? end end alias validate! validate # Solo agregar el post al sitio una vez que lo guardamos # # TODO no sería la forma correcta de hacerlo en Rails # def add_post_to_site! # @site.jekyll.collections[@collection].docs << @post # @site.jekyll.collections[@collection].docs.sort! # unless @site.collections[@collection].include? self # @site.collections[@collection] << self # @site.collections[@collection].sort! # end # end # Cambiar el nombre del archivo si cambió el título o la fecha. # Como Jekyll no tiene métodos para modificar un Document, lo # engañamos eliminando la instancia de @post y recargando otra. def detect_file_rename! return true unless basename_changed? # No eliminamos el archivo a menos que ya exista el reemplazo! return false unless File.exist? path Rails.logger.info I18n.t('posts.logger.rm', path: path) FileUtils.rm @post.path # replace_post! end # Reemplaza el post en el sitio por uno nuevo # TODO: refactorizar # def replace_post! # @old_post = @site.jekyll.collections[@lang].docs.delete @post # new_post # end # Guarda los cambios en el archivo destino def write return true if persisted? Site::Writer.new(site: site, file: path.absolute, content: full_content, usuarie: usuarie, message: title.value).save end # Devuelve le autore de un artículo # XXX: Si se cambia le autore en el editor también cambia quién hace # un cambio y se le puede asignar a cualquier otre. # TODO: Mover la escritura a un servicio que combine escritura, commit # y current_usuarie def usuarie OpenStruct.new(email: author.value.first, name: author.value.first.split('@', 2).first) end # Verifica si hace falta escribir cambios def persisted? File.exist?(path.absolute) && full_content == File.read(path.absolute) end private # rubocop:disable Metrics/AbcSize def new_attribute_was(method) attr_was = (attribute_name(method).to_s + '_was').to_sym return attr_was if singleton_class.method_defined? attr_was define_singleton_method(attr_was) do name = attribute_name(attr_was) if document.respond_to?(name) document.send(name) else document.data[name.to_s] end end end # Pregunta si el atributo cambió def new_attribute_changed(method) attr_changed = (attribute_name(method).to_s + '_changed?').to_sym return attr_changed if singleton_class.method_defined? attr_changed define_singleton_method(attr_changed) do name = attribute_name(attr_changed) name_was = (name.to_s + '_was').to_sym (send(name).try(:value) || send(name)) != send(name_was) end end # rubocop:enable Metrics/AbcSize # Obtiene el nombre del atributo a partir del nombre del método def attribute_name(attr) # XXX: Los simbolos van al final %w[_was _changed? ? =].reduce(attr.to_s) do |a, suffix| a.chomp suffix end.to_sym end def load_slug! self.slug = MetadataSlug.new(document: document, site: site, layout: layout, name: :slug, type: :slug, post: self, required: true) end def load_date! self.date = MetadataDocumentDate.new(document: document, site: site, layout: layout, name: :date, type: :document_date, post: self, required: true) end def load_path! self.path = MetadataPath.new(document: document, site: site, layout: layout, name: :path, type: :path, post: self, required: true) end end # rubocop:enable Metrics/ClassLength # rubocop:enable Style/MethodMissingSuper # rubocop:enable Style/MissingRespondToMissing