diff --git a/app/models/post.rb b/app/models/post.rb index 07ee8a5..722fdaf 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -6,6 +6,8 @@ require 'jekyll/utils' # 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 @@ -31,8 +33,11 @@ class Post < OpenStruct layout.metadata.keys.map(&:to_sym) # El contenido + # TODO: Mover a su propia clase para poder hacer limpiezas + # independientemente self.content = document.content self.date = document.date + # TODO: idem content self.slug = document.data['slug'] # Genera un atributo por cada uno de los campos de la plantilla, @@ -70,8 +75,6 @@ class Post < OpenStruct # # XXX: rubocop dice que tenemos que usar super cuando ya lo estamos # usando... - # - # rubocop:disable Style/MethodMissingSuper def method_missing(mid, *args) unless attribute? mid raise NoMethodError, I18n.t('exceptions.post.no_method', @@ -80,7 +83,6 @@ class Post < OpenStruct super(mid, *args) end - # rubocop:enable Style/MethodMissingSuper # Detecta si es un atributo válido o no, a partir de la tabla de la # plantilla @@ -93,6 +95,7 @@ class Post < OpenStruct 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) @@ -100,6 +103,9 @@ class Post < OpenStruct { 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 @@ -117,23 +123,16 @@ class Post < OpenStruct !File.exist?(path) && !site.posts(lang: lang).include?(self) end alias destroy! destroy + # rubocop:enable Metrics/AbcSize - # Guarda los cambios. - # - # Recién cuando vamos a guardar creamos el Post, porque ya tenemos - # todos los datos para escribir el archivo, que es la condición - # necesaria para poder crearlo :P + # Guarda los cambios def save - cleanup! - return false unless valid? - - return unless write - return unless detect_file_rename! + return false unless write # Vuelve a leer el post para tomar los cambios document.read -# add_post_to_site! + true end alias save! save @@ -162,16 +161,6 @@ class Post < OpenStruct end alias validate! validate - # Permite ordenar los posts - def <=>(other) - @post <=> other.post - end - - # **************** - # A PARTIR DE ACA ESTAMOS CONSIDERANDO CUALES METODOS QUEDAN Y CUALES - # NO - # **************** - def basename_changed? @post.try(:basename) != basename_from_front_matter end @@ -180,17 +169,6 @@ class Post < OpenStruct new? || @post.data.dig('slug') != slug end - # Detecta si un valor es un archivo - def url?(name) - path = get_front_matter(name) - return false unless path.is_a?(String) || path.is_a?(Array) - - # El primer valor es '' porque la URL empieza con / - [path].flatten.map do |p| - p.split('/').second == 'public' - end.all? - end - # devuelve las plantillas como strong params, primero los valores # simples, luego los arrays y al final los hashes def template_params @@ -199,34 +177,18 @@ class Post < OpenStruct end end - private - - # Completa el front_matter a partir de las variables de otro post que - # le sirve de plantilla - def front_matter_from_template - # XXX: Llamamos a @template en lugar de template porque sino - # entramos en una race condition - return {} unless @template - - ft = template_fields.map(&:to_front_matter).reduce({}, :merge) - # Convertimos el slug en layout - ft['layout'] = template.slug - - ft - end - # 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! + # 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 + # 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 @@ -238,60 +200,21 @@ class Post < OpenStruct Rails.logger.info I18n.t('posts.logger.rm', path: path) FileUtils.rm @post.path -# replace_post! + # replace_post! end # Reemplaza el post en el sitio por uno nuevo - def replace_post! - @old_post = @site.jekyll.collections[@lang].docs.delete @post + # TODO: refactorizar + # def replace_post! + # @old_post = @site.jekyll.collections[@lang].docs.delete @post - new_post - end - - # Obtiene el nombre del archivo a partir de los datos que le - # pasemos - def basename_from_front_matter - ext = get_front_matter('ext') || '.markdown' - - "#{date_as_string}-#{slug}#{ext}" - end - - # Toma los datos del front matter local y los mueve a los datos - # que van a ir al post. Si hay símbolos se convierten a cadenas, - # porque Jekyll trabaja con cadenas. Se excluyen otros datos que no - # van en el frontmatter - def merge_with_front_matter!(params) - @front_matter.merge! Hash[params.to_hash.map do |k, v| - [k, v] unless REJECT_FROM_DATA.include? k - end.compact] - end - - # Carga una copia de los datos del post original excluyendo datos - # que no nos interesan - def load_front_matter! - @front_matter = @post.data.reject do |key, _| - REJECT_FROM_DATA.include? key - end - end + # new_post + # end def cleanup! default_date_is_today! clean_content! slugify_title! - update_translations! - put_in_order! - create_glossary! - end - - # Busca las traducciones y actualiza el frontmatter si es necesario - def update_translations! - return unless translated? - return unless slug_changed? - - find_translations.each do |post| - post.update_attributes(lang: get_front_matter('lang')) - post.save - end end # Aplica limpiezas básicas del contenido @@ -301,87 +224,43 @@ class Post < OpenStruct # Guarda los cambios en el archivo destino def write - r = File.open(path, File::RDWR | File::CREAT, 0o640) do |f| - # Bloquear el archivo para que no sea accedido por otro - # proceso u otra editora - f.flock(File::LOCK_EX) + return true if persisted? - # Empezar por el principio - f.rewind - - # Escribir - f.write(full_content) - - # Eliminar el resto - f.flush - f.truncate(f.pos) - end - - return true if r.zero? - - add_error file: I18n.t('posts.errors.file') - false + Site::Writer.new(site: site, file: path, + content: full_content, usuarie: usuarie, + message: title.value).save end - def add_error(hash) - hash.each_pair do |k, i| - @errors[k] = if @errors.key?(k) - [@errors[k], i] - else - i - end - end - - @errors + # 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) && full_content == File.read(path) + end + + private + def default_date_is_today! - date ||= Time.now + self.date ||= Time.now end def slugify_title! self.slug = Jekyll::Utils.slugify(title) if slug.blank? end - # Agregar al final de la cola si no especificamos un orden - # - # TODO si el artículo tiene una fecha que lo coloca en medio de - # la colección en lugar de al final, deberíamos reordenar? - def put_in_order! - return unless order.nil? - - @front_matter['order'] = @site.posts_for(@collection).count - end - - # Crea el artículo de glosario para cada categoría o tag - def create_glossary! - return unless site.glossary? - - %i[tags categories].each do |i| - send(i).each do |c| - # TODO: no hardcodear, hacer _configurable - next if c == 'Glossary' - next if site.posts.find do |p| - p.title == c - end - - glossary = Post.new(site: site, lang: lang) - glossary.update_attributes( - title: c, - layout: 'glossary', - categories: 'Glossary' - ) - - glossary.save! - end - end - end - - private - # Obtiene el nombre del atributo sin def attribute_name(attr) attr.to_s.split('=').first.to_sym end end # rubocop:enable Metrics/ClassLength +# rubocop:enable Style/MethodMissingSuper +# rubocop:enable Style/MissingRespondToMissing diff --git a/doc/posts.md b/doc/posts.md index f99e08e..2a82380 100644 --- a/doc/posts.md +++ b/doc/posts.md @@ -94,3 +94,10 @@ Al instanciar un `Post`, se pasan el sitio y la plantilla por defecto. ## TODO * Leer artículos a medida que se los necesita en lugar de todos juntos. +* Reimplementar glosario (se crea un artículo por cada categoría + utilizada) +* Reimplementar orden de artículos (ver doc) +* Detectar cambio de nombre (fecha y slug) +* Crear artículos nuevos +* Convertir layout a params +* Guardar cambios diff --git a/test/models/post_test.rb b/test/models/post_test.rb index 320e493..479e247 100644 --- a/test/models/post_test.rb +++ b/test/models/post_test.rb @@ -85,5 +85,22 @@ class PostTest < ActiveSupport::TestCase end test 'se pueden guardar los cambios' do + title = SecureRandom.hex + @post.title.value = title + @post.categories.value << title + + assert @post.save + + Dir.chdir(@site.path) do + collection = Jekyll::Collection.new(@site.jekyll, I18n.locale.to_s) + document = Jekyll::Document.new(@post.path, + site: @site.jekyll, + collection: collection) + document.read + + assert document.data['categories'].include?(title) + assert_equal title, document.data['title'] + assert_equal 'post', document.data['layout'] + end end end