mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-15 04:31:41 +00:00
el slug y la fecha son campos tambien
This commit is contained in:
parent
ad1ae26e4d
commit
ada14b2294
6 changed files with 177 additions and 67 deletions
11
app/models/metadata_document_date.rb
Normal file
11
app/models/metadata_document_date.rb
Normal file
|
@ -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
|
42
app/models/metadata_slug.rb
Normal file
42
app/models/metadata_slug.rb
Normal file
|
@ -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
|
|
@ -5,7 +5,7 @@
|
||||||
# TODO: Validar el tipo de valor pasado a value= según el :type
|
# TODO: Validar el tipo de valor pasado a value= según el :type
|
||||||
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
:value, :help, :required, :errors,
|
:value, :help, :required, :errors,
|
||||||
keyword_init: true) do
|
:layout, keyword_init: true) do
|
||||||
# El valor por defecto
|
# El valor por defecto
|
||||||
def default_value
|
def default_value
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
@ -36,9 +36,7 @@ class Post < OpenStruct
|
||||||
# TODO: Mover a su propia clase para poder hacer limpiezas
|
# TODO: Mover a su propia clase para poder hacer limpiezas
|
||||||
# independientemente
|
# independientemente
|
||||||
self.content = document.content
|
self.content = document.content
|
||||||
self.date = document.date
|
self.errors = {}
|
||||||
# TODO: idem content
|
|
||||||
self.slug = document.data['slug']
|
|
||||||
|
|
||||||
# Genera un atributo por cada uno de los campos de la plantilla,
|
# Genera un atributo por cada uno de los campos de la plantilla,
|
||||||
# MetadataFactory devuelve un tipo de campo por cada campo. A
|
# MetadataFactory devuelve un tipo de campo por cada campo. A
|
||||||
|
@ -49,11 +47,18 @@ class Post < OpenStruct
|
||||||
MetadataFactory.build(document: document,
|
MetadataFactory.build(document: document,
|
||||||
site: site,
|
site: site,
|
||||||
name: name,
|
name: name,
|
||||||
|
layout: layout,
|
||||||
type: template['type'],
|
type: template['type'],
|
||||||
label: template['label'],
|
label: template['label'],
|
||||||
help: template['help'],
|
help: template['help'],
|
||||||
required: template['required'])
|
required: template['required'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
load_slug!
|
||||||
|
load_date!
|
||||||
|
|
||||||
|
# Leer el documento
|
||||||
|
read
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/AbcSize
|
# rubocop:enable Metrics/AbcSize
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
@ -81,7 +86,15 @@ class Post < OpenStruct
|
||||||
method: mid)
|
method: mid)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Definir los attribute_*
|
||||||
|
new_attribute_was(mid)
|
||||||
|
new_attribute_changed(mid)
|
||||||
|
|
||||||
|
# OpenStruct
|
||||||
super(mid, *args)
|
super(mid, *args)
|
||||||
|
|
||||||
|
# Devolver lo mismo que devuelve el método después de definirlo
|
||||||
|
send(mid, *args)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Detecta si es un atributo válido o no, a partir de la tabla de la
|
# Detecta si es un atributo válido o no, a partir de la tabla de la
|
||||||
|
@ -137,11 +150,23 @@ class Post < OpenStruct
|
||||||
end
|
end
|
||||||
alias save! save
|
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,
|
# Devuelve la ruta del post, si se cambió alguno de los datos,
|
||||||
# generamos una ruta nueva para tener siempre la ruta actualizada.
|
# generamos una ruta nueva para tener siempre la ruta actualizada.
|
||||||
def path
|
def path
|
||||||
document.path
|
document.path
|
||||||
end
|
end
|
||||||
|
alias relative_path path
|
||||||
|
|
||||||
|
def absolute_path
|
||||||
|
File.join site.path, path
|
||||||
|
end
|
||||||
|
|
||||||
# Detecta si el artículo es válido para guardar
|
# Detecta si el artículo es válido para guardar
|
||||||
def valid?
|
def valid?
|
||||||
|
@ -161,22 +186,6 @@ class Post < OpenStruct
|
||||||
end
|
end
|
||||||
alias validate! validate
|
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
|
# Solo agregar el post al sitio una vez que lo guardamos
|
||||||
#
|
#
|
||||||
# TODO no sería la forma correcta de hacerlo en Rails
|
# TODO no sería la forma correcta de hacerlo en Rails
|
||||||
|
@ -211,17 +220,6 @@ class Post < OpenStruct
|
||||||
# new_post
|
# new_post
|
||||||
# end
|
# 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
|
# Guarda los cambios en el archivo destino
|
||||||
def write
|
def write
|
||||||
return true if persisted?
|
return true if persisted?
|
||||||
|
@ -243,22 +241,57 @@ class Post < OpenStruct
|
||||||
|
|
||||||
# Verifica si hace falta escribir cambios
|
# Verifica si hace falta escribir cambios
|
||||||
def persisted?
|
def persisted?
|
||||||
File.exist?(path) && full_content == File.read(path)
|
File.exist?(absolute_path) && full_content == File.read(absolute_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def default_date_is_today!
|
def new_attribute_was(method)
|
||||||
self.date ||= Time.now
|
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
|
end
|
||||||
|
|
||||||
def slugify_title!
|
attr_was
|
||||||
self.slug = Jekyll::Utils.slugify(title) if slug.blank?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtiene el nombre del atributo sin
|
# Pregunta si el atributo cambió
|
||||||
|
# rubocop:disable Metrics/AbcSize
|
||||||
|
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)
|
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
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/ClassLength
|
# rubocop:enable Metrics/ClassLength
|
||||||
|
|
|
@ -100,4 +100,3 @@ Al instanciar un `Post`, se pasan el sitio y la plantilla por defecto.
|
||||||
* Detectar cambio de nombre (fecha y slug)
|
* Detectar cambio de nombre (fecha y slug)
|
||||||
* Crear artículos nuevos
|
* Crear artículos nuevos
|
||||||
* Convertir layout a params
|
* Convertir layout a params
|
||||||
* Guardar cambios
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
class PostTest < ActiveSupport::TestCase
|
class PostTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
# Trabajamos con el sitio de sutty porque tiene artículos
|
# Trabajamos con el sitio de sutty porque tiene artículos
|
||||||
|
@ -41,37 +43,25 @@ class PostTest < ActiveSupport::TestCase
|
||||||
FileUtils.mv tmp, @post.path
|
FileUtils.mv tmp, @post.path
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'se puede ver el contenido completo' do
|
test 'se puede ver el contenido completo después de guardar' do
|
||||||
tmp = Tempfile.new
|
assert @post.save
|
||||||
|
@post.document.read
|
||||||
begin
|
|
||||||
tmp.write(@post.full_content)
|
|
||||||
tmp.close
|
|
||||||
|
|
||||||
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'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Queremos saber si todos los atributos del post terminaron en el
|
# Queremos saber si todos los atributos del post terminaron en el
|
||||||
# archivo
|
# archivo
|
||||||
@post.attributes.each do |attr|
|
@post.attributes.each do |attr|
|
||||||
template = @post.send(attr)
|
metadata = @post.send(attr)
|
||||||
# ignorar atributos que no son datos
|
# ignorar atributos que no son datos
|
||||||
next unless template.is_a? MetadataTemplate
|
next unless metadata.is_a? MetadataTemplate
|
||||||
|
|
||||||
if template.empty?
|
if metadata.empty?
|
||||||
assert_not document.data[attr.to_s].present?
|
assert_not @post.document.data[attr.to_s].present?
|
||||||
|
elsif attr == :date
|
||||||
|
assert_equal metadata.value, @post.document.date
|
||||||
else
|
else
|
||||||
assert_equal template.value, document.data[attr.to_s]
|
assert_equal metadata.value, @post.document.data[attr.to_s]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
ensure
|
|
||||||
tmp.unlink
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'se pueden validar' do
|
test 'se pueden validar' do
|
||||||
|
@ -103,4 +93,39 @@ class PostTest < ActiveSupport::TestCase
|
||||||
assert_equal 'post', document.data['layout']
|
assert_equal 'post', document.data['layout']
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
Loading…
Reference in a new issue