diff --git a/app/models/metadata_document_date.rb b/app/models/metadata_document_date.rb new file mode 100644 index 00000000..425e8935 --- /dev/null +++ b/app/models/metadata_document_date.rb @@ -0,0 +1,11 @@ +# Maneja la fecha del document +class MetadataDocumentDate < MetadataTemplate + # La fecha por defecto es ahora! + def default_value + Date.today.to_time + end + + def value + self[:value] || document.date || default_value + end +end diff --git a/app/models/metadata_slug.rb b/app/models/metadata_slug.rb new file mode 100644 index 00000000..905ba0c4 --- /dev/null +++ b/app/models/metadata_slug.rb @@ -0,0 +1,42 @@ +require 'jekyll/utils' + +# El slug es el nombre del archivo sin la fecha ni la extensión y se +# deriva del título. +# +# Si el slug del document es distinto al que le asignaría el título +# slugificado, quiere decir que lo indicamos manualmente y no hay que +# cambiarlo. +# +# Pero si cambiamos el slug manualmente, tenemos que darle prioridad a +# ese cambio. +# +# El slug por defecto es el asignado por el documento. +# +# Si no hay slug, slugificamos el título. +# +# Si cambiamos el título y el slug coincide con el slug del título +# anterior, también lo cambiamos para mantener las URLs consistentes. +# Luego podemos usar el plugin que guarda los slugs anteriores para +# generar links. +# +# TODO: Transliterar tildes? +class MetadataSlug < MetadataTemplate + # Trae el slug desde el título si existe o una string al azar + def default_value + if title + Jekyll::Utils.slugify(title) + else + SecureRandom.hex + end + end + + def value + self[:value] || document.data.fetch('slug', default_value) + end + + private + + def title + layout.metadata[:title].try(:value) + end +end diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index 9802ec70..96bc7b5c 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -5,7 +5,7 @@ # TODO: Validar el tipo de valor pasado a value= según el :type MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, :value, :help, :required, :errors, - keyword_init: true) do + :layout, keyword_init: true) do # El valor por defecto def default_value raise NotImplementedError diff --git a/app/models/post.rb b/app/models/post.rb index 722fdafb..59580d29 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -36,9 +36,7 @@ class Post < OpenStruct # 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'] + self.errors = {} # Genera un atributo por cada uno de los campos de la plantilla, # MetadataFactory devuelve un tipo de campo por cada campo. A @@ -49,11 +47,18 @@ class Post < OpenStruct MetadataFactory.build(document: document, site: site, name: name, + layout: layout, type: template['type'], label: template['label'], help: template['help'], required: template['required']) end + + load_slug! + load_date! + + # Leer el documento + read end # rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/MethodLength @@ -81,7 +86,15 @@ class Post < OpenStruct 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 @@ -137,11 +150,23 @@ class Post < OpenStruct end alias save! save + # Lee el documento + def read + Dir.chdir(site.path) do + document.read + end + end + # Devuelve la ruta del post, si se cambió alguno de los datos, # generamos una ruta nueva para tener siempre la ruta actualizada. def path document.path end + alias relative_path path + + def absolute_path + File.join site.path, path + end # Detecta si el artículo es válido para guardar def valid? @@ -161,22 +186,6 @@ class Post < OpenStruct end alias validate! validate - def basename_changed? - @post.try(:basename) != basename_from_front_matter - end - - def slug_changed? - new? || @post.data.dig('slug') != slug - end - - # devuelve las plantillas como strong params, primero los valores - # simples, luego los arrays y al final los hashes - def template_params - @template_params ||= template_fields.map(&:to_param).sort_by do |s| - s.is_a?(Symbol) ? 0 : 1 - end - end - # Solo agregar el post al sitio una vez que lo guardamos # # TODO no sería la forma correcta de hacerlo en Rails @@ -211,17 +220,6 @@ class Post < OpenStruct # new_post # end - def cleanup! - default_date_is_today! - clean_content! - slugify_title! - end - - # Aplica limpiezas básicas del contenido - def clean_content! - content.try(:delete!, "\r") - end - # Guarda los cambios en el archivo destino def write return true if persisted? @@ -243,22 +241,57 @@ class Post < OpenStruct # Verifica si hace falta escribir cambios def persisted? - File.exist?(path) && full_content == File.read(path) + File.exist?(absolute_path) && full_content == File.read(absolute_path) end private - def default_date_is_today! - self.date ||= Time.now + 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) + name == :content ? document.content : document.data[name.to_s] + end + + attr_was end - def slugify_title! - self.slug = Jekyll::Utils.slugify(title) if slug.blank? - end + # Pregunta si el atributo cambió + # rubocop:disable Metrics/AbcSize + def new_attribute_changed(method) + attr_changed = (attribute_name(method).to_s + '_changed?').to_sym - # Obtiene el nombre del atributo sin + 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) - attr.to_s.split('=').first.to_sym + # 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, + required: true) + end + + def load_date! + self.date = MetadataDocumentDate.new(document: document, site: site, + layout: layout, name: :date, + required: true) end end # rubocop:enable Metrics/ClassLength diff --git a/doc/posts.md b/doc/posts.md index 2a82380e..2461a904 100644 --- a/doc/posts.md +++ b/doc/posts.md @@ -100,4 +100,3 @@ Al instanciar un `Post`, se pasan el sitio y la plantilla por defecto. * 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 479e2476..4d0cb651 100644 --- a/test/models/post_test.rb +++ b/test/models/post_test.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'test_helper' + class PostTest < ActiveSupport::TestCase setup do # Trabajamos con el sitio de sutty porque tiene artículos @@ -41,36 +43,24 @@ class PostTest < ActiveSupport::TestCase FileUtils.mv tmp, @post.path end - test 'se puede ver el contenido completo' do - tmp = Tempfile.new + test 'se puede ver el contenido completo después de guardar' do + assert @post.save + @post.document.read - begin - tmp.write(@post.full_content) - tmp.close + # Queremos saber si todos los atributos del post terminaron en el + # archivo + @post.attributes.each do |attr| + metadata = @post.send(attr) + # ignorar atributos que no son datos + next unless metadata.is_a? MetadataTemplate - collection = Jekyll::Collection.new(@site.jekyll, I18n.locale.to_s) - document = Jekyll::Document.new(tmp.path, site: @site.jekyll, - collection: collection) - document.read - document.data['categories'].try(:delete_if) do |x| - x == 'tmp' + if metadata.empty? + assert_not @post.document.data[attr.to_s].present? + elsif attr == :date + assert_equal metadata.value, @post.document.date + else + assert_equal metadata.value, @post.document.data[attr.to_s] end - - # Queremos saber si todos los atributos del post terminaron en el - # archivo - @post.attributes.each do |attr| - template = @post.send(attr) - # ignorar atributos que no son datos - next unless template.is_a? MetadataTemplate - - if template.empty? - assert_not document.data[attr.to_s].present? - else - assert_equal template.value, document.data[attr.to_s] - end - end - ensure - tmp.unlink end end @@ -103,4 +93,39 @@ class PostTest < ActiveSupport::TestCase assert_equal 'post', document.data['layout'] end end + + test 'attribute_name' do + assert_equal :hola, @post.send(:attribute_name, :hola) + assert_equal :hola, @post.send(:attribute_name, :hola?) + assert_equal :hola, @post.send(:attribute_name, :hola_was) + assert_equal :hola, @post.send(:attribute_name, :hola_changed?) + end + + test 'se puede cambiar el slug' do + assert_equal @post.slug_was, @post.slug.value + assert_not @post.slug_changed? + assert @post.slug.valid? + + ex_slug = @post.slug.value + @post.slug.value = SecureRandom.hex + + assert_not_equal ex_slug, @post.slug.value + assert_equal ex_slug, @post.slug_was + assert @post.slug_changed? + assert @post.slug.valid? + end + + test 'se puede cambiar la fecha' do + assert_equal @post.date_was, @post.date.value + assert_not @post.date_changed? + assert @post.date.valid? + + ex_date = @post.date.value + @post.date.value = 2.days.ago + + assert_not_equal ex_date, @post.date.value + assert_equal ex_date, @post.date_was + assert @post.date_changed? + assert @post.date.valid? + end end