2018-01-29 22:19:10 +00:00
|
|
|
# frozen_string_literal: true
|
2019-03-26 15:32:20 +00:00
|
|
|
|
2018-01-29 22:19:10 +00:00
|
|
|
require 'jekyll/utils'
|
|
|
|
|
|
|
|
# Esta clase representa un post en un sitio jekyll e incluye métodos
|
2019-08-06 23:17:29 +00:00
|
|
|
# para modificarlos y crear nuevos.
|
2018-02-03 22:37:09 +00:00
|
|
|
#
|
2019-08-06 23:17:29 +00:00
|
|
|
# rubocop:disable Metrics/ClassLength
|
|
|
|
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 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
|
|
|
|
# rubocop:disable Metrics/MethodLength
|
|
|
|
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
|
|
|
|
self.content = document.content
|
|
|
|
self.date = document.date
|
|
|
|
self.slug = document.data['slug']
|
|
|
|
|
|
|
|
# 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,
|
|
|
|
site: site,
|
|
|
|
name: name,
|
|
|
|
type: template['type'],
|
|
|
|
label: template['label'],
|
|
|
|
help: template['help'],
|
|
|
|
required: template['required'])
|
2018-02-03 22:37:09 +00:00
|
|
|
end
|
|
|
|
end
|
2019-08-06 23:17:29 +00:00
|
|
|
# rubocop:enable Metrics/AbcSize
|
|
|
|
# rubocop:enable Metrics/MethodLength
|
2018-02-03 22:37:09 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# 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")
|
2018-01-29 22:19:10 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
raise ArgumentError, i18n unless args[attr].present?
|
|
|
|
end
|
2018-01-29 22:19:10 +00:00
|
|
|
end
|
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# 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...
|
|
|
|
#
|
|
|
|
# rubocop:disable Style/MethodMissingSuper
|
|
|
|
def method_missing(mid, *args)
|
|
|
|
unless attribute? mid
|
|
|
|
raise NoMethodError, I18n.t('exceptions.post.no_method',
|
|
|
|
method: mid)
|
|
|
|
end
|
2018-12-14 15:27:50 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
super(mid, *args)
|
2018-12-14 15:27:50 +00:00
|
|
|
end
|
2019-08-06 23:17:29 +00:00
|
|
|
# rubocop:enable Style/MethodMissingSuper
|
2018-12-14 15:27:50 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# 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
|
2018-04-27 18:48:26 +00:00
|
|
|
end
|
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# Genera el post con metadatos en YAML
|
|
|
|
def full_content
|
|
|
|
yaml = layout.metadata.keys.map(&:to_sym).map do |metadata|
|
|
|
|
template = send(metadata)
|
2018-04-27 18:48:26 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
{ metadata.to_s => template.value } unless template.empty?
|
|
|
|
end.compact.inject(:merge)
|
2018-02-22 19:01:11 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
"#{yaml.to_yaml}---\n\n#{content}"
|
2018-02-24 21:24:11 +00:00
|
|
|
end
|
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# Eliminar el artículo del repositorio y de la lista de artículos del
|
|
|
|
# sitio
|
|
|
|
#
|
|
|
|
# XXX Commit
|
|
|
|
def destroy
|
|
|
|
FileUtils.rm_f path
|
2018-02-26 18:58:56 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
site.posts(lang: lang).delete_if do |post|
|
|
|
|
post.path == path
|
|
|
|
end
|
2019-03-26 15:32:20 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
!File.exist?(path) && !site.posts(lang: lang).include?(self)
|
2018-02-22 19:01:11 +00:00
|
|
|
end
|
2019-08-06 23:17:29 +00:00
|
|
|
alias destroy! destroy
|
2018-02-22 19:01:11 +00:00
|
|
|
|
2018-02-03 22:37:09 +00:00
|
|
|
# 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
|
2018-01-29 22:19:10 +00:00
|
|
|
def save
|
2018-02-03 22:37:09 +00:00
|
|
|
cleanup!
|
|
|
|
|
|
|
|
return false unless valid?
|
|
|
|
|
2018-01-29 22:19:10 +00:00
|
|
|
return unless write
|
|
|
|
return unless detect_file_rename!
|
|
|
|
|
|
|
|
# Vuelve a leer el post para tomar los cambios
|
2019-08-06 23:17:29 +00:00
|
|
|
document.read
|
|
|
|
# add_post_to_site!
|
2018-01-29 22:19:10 +00:00
|
|
|
true
|
|
|
|
end
|
2019-03-26 15:32:20 +00:00
|
|
|
alias save! save
|
2018-01-29 22:19:10 +00:00
|
|
|
|
2018-02-03 22:37:09 +00:00
|
|
|
# Devuelve la ruta del post, si se cambió alguno de los datos,
|
|
|
|
# generamos una ruta nueva para tener siempre la ruta actualizada.
|
2018-01-29 22:19:10 +00:00
|
|
|
def path
|
2019-08-06 23:17:29 +00:00
|
|
|
document.path
|
2018-01-29 22:19:10 +00:00
|
|
|
end
|
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# Detecta si el artículo es válido para guardar
|
|
|
|
def valid?
|
|
|
|
validate
|
|
|
|
errors.blank?
|
2018-02-24 21:24:11 +00:00
|
|
|
end
|
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# Requisitos para que el post sea válido
|
|
|
|
def validate
|
|
|
|
self.errors = {}
|
2018-02-02 22:20:31 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
layout.metadata.keys.map(&:to_sym).each do |metadata|
|
|
|
|
template = send(metadata)
|
2018-07-23 19:48:34 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
errors[metadata] = template.errors unless template.valid?
|
2018-07-27 13:45:32 +00:00
|
|
|
end
|
2018-06-25 20:44:47 +00:00
|
|
|
end
|
2019-08-06 23:17:29 +00:00
|
|
|
alias validate! validate
|
2018-06-25 20:44:47 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# Permite ordenar los posts
|
|
|
|
def <=>(other)
|
|
|
|
@post <=> other.post
|
2018-02-03 22:37:09 +00:00
|
|
|
end
|
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# ****************
|
|
|
|
# A PARTIR DE ACA ESTAMOS CONSIDERANDO CUALES METODOS QUEDAN Y CUALES
|
|
|
|
# NO
|
|
|
|
# ****************
|
2018-02-03 22:37:09 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
def basename_changed?
|
|
|
|
@post.try(:basename) != basename_from_front_matter
|
2018-02-03 22:37:09 +00:00
|
|
|
end
|
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
def slug_changed?
|
|
|
|
new? || @post.data.dig('slug') != slug
|
2018-01-30 15:20:19 +00:00
|
|
|
end
|
|
|
|
|
2018-07-02 22:07:06 +00:00
|
|
|
# Detecta si un valor es un archivo
|
|
|
|
def url?(name)
|
|
|
|
path = get_front_matter(name)
|
2018-07-20 18:17:34 +00:00
|
|
|
return false unless path.is_a?(String) || path.is_a?(Array)
|
2019-03-26 15:32:20 +00:00
|
|
|
|
2018-07-02 22:07:06 +00:00
|
|
|
# El primer valor es '' porque la URL empieza con /
|
2018-07-20 18:17:34 +00:00
|
|
|
[path].flatten.map do |p|
|
|
|
|
p.split('/').second == 'public'
|
|
|
|
end.all?
|
2018-07-02 22:07:06 +00:00
|
|
|
end
|
|
|
|
|
2018-06-21 18:15:03 +00:00
|
|
|
# devuelve las plantillas como strong params, primero los valores
|
|
|
|
# simples, luego los arrays y al final los hashes
|
2018-05-14 21:45:52 +00:00
|
|
|
def template_params
|
2019-08-06 23:17:29 +00:00
|
|
|
@template_params ||= template_fields.map(&:to_param).sort_by do |s|
|
2018-06-21 18:15:03 +00:00
|
|
|
s.is_a?(Symbol) ? 0 : 1
|
2018-05-11 20:00:45 +00:00
|
|
|
end
|
2018-05-14 19:27:29 +00:00
|
|
|
end
|
|
|
|
|
2018-01-29 22:19:10 +00:00
|
|
|
private
|
|
|
|
|
2018-06-21 17:33:45 +00:00
|
|
|
# Completa el front_matter a partir de las variables de otro post que
|
|
|
|
# le sirve de plantilla
|
2018-05-08 21:57:11 +00:00
|
|
|
def front_matter_from_template
|
2018-05-17 19:06:08 +00:00
|
|
|
# XXX: Llamamos a @template en lugar de template porque sino
|
|
|
|
# entramos en una race condition
|
|
|
|
return {} unless @template
|
2018-05-08 21:57:11 +00:00
|
|
|
|
2019-03-26 15:32:20 +00:00
|
|
|
ft = template_fields.map(&:to_front_matter).reduce({}, :merge)
|
2018-05-11 15:12:23 +00:00
|
|
|
# Convertimos el slug en layout
|
2018-06-21 17:33:45 +00:00
|
|
|
ft['layout'] = template.slug
|
2018-05-11 15:12:23 +00:00
|
|
|
|
|
|
|
ft
|
2018-05-08 21:57:11 +00:00
|
|
|
end
|
|
|
|
|
2018-02-03 22:37:09 +00:00
|
|
|
# Solo agregar el post al sitio una vez que lo guardamos
|
|
|
|
#
|
|
|
|
# TODO no sería la forma correcta de hacerlo en Rails
|
2019-08-06 23:17:29 +00:00
|
|
|
# def add_post_to_site!
|
|
|
|
# @site.jekyll.collections[@collection].docs << @post
|
|
|
|
# @site.jekyll.collections[@collection].docs.sort!
|
2018-01-29 22:19:10 +00:00
|
|
|
|
2019-08-06 23:17:29 +00:00
|
|
|
# unless @site.collections[@collection].include? self
|
|
|
|
# @site.collections[@collection] << self
|
|
|
|
# @site.collections[@collection].sort!
|
|
|
|
# end
|
|
|
|
# end
|
2018-01-29 22:19:10 +00:00
|
|
|
|
|
|
|
# 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?
|
2018-02-03 22:37:09 +00:00
|
|
|
# No eliminamos el archivo a menos que ya exista el reemplazo!
|
|
|
|
return false unless File.exist? path
|
2018-01-29 22:19:10 +00:00
|
|
|
|
2018-02-03 22:37:09 +00:00
|
|
|
Rails.logger.info I18n.t('posts.logger.rm', path: path)
|
|
|
|
FileUtils.rm @post.path
|
2019-08-06 23:17:29 +00:00
|
|
|
# replace_post!
|
2018-01-29 22:19:10 +00:00
|
|
|
end
|
|
|
|
|
2018-02-03 22:37:09 +00:00
|
|
|
# Reemplaza el post en el sitio por uno nuevo
|
|
|
|
def replace_post!
|
2018-02-23 19:20:51 +00:00
|
|
|
@old_post = @site.jekyll.collections[@lang].docs.delete @post
|
2018-01-29 22:19:10 +00:00
|
|
|
|
|
|
|
new_post
|
|
|
|
end
|
|
|
|
|
|
|
|
# Obtiene el nombre del archivo a partir de los datos que le
|
|
|
|
# pasemos
|
|
|
|
def basename_from_front_matter
|
2019-03-26 15:32:20 +00:00
|
|
|
ext = get_front_matter('ext') || '.markdown'
|
2018-01-29 22:19:10 +00:00
|
|
|
|
2018-02-24 21:24:11 +00:00
|
|
|
"#{date_as_string}-#{slug}#{ext}"
|
2018-01-29 22:19:10 +00:00
|
|
|
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,
|
2018-02-02 22:20:31 +00:00
|
|
|
# porque Jekyll trabaja con cadenas. Se excluyen otros datos que no
|
|
|
|
# van en el frontmatter
|
2018-02-03 22:37:09 +00:00
|
|
|
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
|
2018-02-02 22:20:31 +00:00
|
|
|
end.compact]
|
2018-01-29 22:19:10 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Carga una copia de los datos del post original excluyendo datos
|
|
|
|
# que no nos interesan
|
2018-02-03 22:37:09 +00:00
|
|
|
def load_front_matter!
|
|
|
|
@front_matter = @post.data.reject do |key, _|
|
2018-01-29 22:19:10 +00:00
|
|
|
REJECT_FROM_DATA.include? key
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-02-03 22:37:09 +00:00
|
|
|
def cleanup!
|
2018-07-23 19:48:51 +00:00
|
|
|
default_date_is_today!
|
2018-02-03 22:37:09 +00:00
|
|
|
clean_content!
|
|
|
|
slugify_title!
|
2018-02-24 21:24:11 +00:00
|
|
|
update_translations!
|
2018-05-02 18:38:46 +00:00
|
|
|
put_in_order!
|
2018-09-05 20:25:47 +00:00
|
|
|
create_glossary!
|
2018-02-24 21:24:11 +00:00
|
|
|
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
|
2018-02-19 20:17:52 +00:00
|
|
|
end
|
|
|
|
|
2018-01-29 22:19:10 +00:00
|
|
|
# Aplica limpiezas básicas del contenido
|
|
|
|
def clean_content!
|
2019-08-06 23:17:29 +00:00
|
|
|
content.try(:delete!, "\r")
|
2018-01-29 22:19:10 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Guarda los cambios en el archivo destino
|
|
|
|
def write
|
2018-02-03 22:37:09 +00:00
|
|
|
r = File.open(path, File::RDWR | File::CREAT, 0o640) do |f|
|
2018-01-29 22:19:10 +00:00
|
|
|
# Bloquear el archivo para que no sea accedido por otro
|
|
|
|
# proceso u otra editora
|
|
|
|
f.flock(File::LOCK_EX)
|
|
|
|
|
|
|
|
# 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?
|
|
|
|
|
2018-02-02 22:20:31 +00:00
|
|
|
add_error file: I18n.t('posts.errors.file')
|
2018-01-29 22:19:10 +00:00
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2018-02-02 22:20:31 +00:00
|
|
|
def add_error(hash)
|
2019-03-26 15:32:20 +00:00
|
|
|
hash.each_pair do |k, i|
|
|
|
|
@errors[k] = if @errors.key?(k)
|
|
|
|
[@errors[k], i]
|
|
|
|
else
|
|
|
|
i
|
|
|
|
end
|
2018-02-02 22:20:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
@errors
|
|
|
|
end
|
2018-02-03 22:37:09 +00:00
|
|
|
|
2018-07-23 19:48:51 +00:00
|
|
|
def default_date_is_today!
|
2019-08-06 23:17:29 +00:00
|
|
|
date ||= Time.now
|
2018-02-03 22:37:09 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def slugify_title!
|
2019-08-06 23:17:29 +00:00
|
|
|
self.slug = Jekyll::Utils.slugify(title) if slug.blank?
|
2018-02-03 22:37:09 +00:00
|
|
|
end
|
2018-05-02 18:38:46 +00:00
|
|
|
|
|
|
|
# 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
|
2018-09-05 20:25:47 +00:00
|
|
|
|
|
|
|
# Crea el artículo de glosario para cada categoría o tag
|
|
|
|
def create_glossary!
|
|
|
|
return unless site.glossary?
|
|
|
|
|
2019-03-26 15:32:20 +00:00
|
|
|
%i[tags categories].each do |i|
|
|
|
|
send(i).each do |c|
|
|
|
|
# TODO: no hardcodear, hacer _configurable
|
2018-09-05 20:25:47 +00:00
|
|
|
next if c == 'Glossary'
|
|
|
|
next if site.posts.find do |p|
|
|
|
|
p.title == c
|
|
|
|
end
|
|
|
|
|
|
|
|
glossary = Post.new(site: site, lang: lang)
|
2019-03-26 15:32:20 +00:00
|
|
|
glossary.update_attributes(
|
2018-09-05 20:25:47 +00:00
|
|
|
title: c,
|
|
|
|
layout: 'glossary',
|
|
|
|
categories: 'Glossary'
|
2019-03-26 15:32:20 +00:00
|
|
|
)
|
2018-09-05 20:25:47 +00:00
|
|
|
|
|
|
|
glossary.save!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2019-08-06 23:17:29 +00:00
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
# Obtiene el nombre del atributo sin
|
|
|
|
def attribute_name(attr)
|
|
|
|
attr.to_s.split('=').first.to_sym
|
|
|
|
end
|
2018-01-29 22:19:10 +00:00
|
|
|
end
|
2019-08-06 23:17:29 +00:00
|
|
|
# rubocop:enable Metrics/ClassLength
|