mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-26 01:16:23 +00:00
357 lines
9.9 KiB
Ruby
357 lines
9.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# 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
|
|
DEFAULT_ATTRIBUTES = %i[site document layout].freeze
|
|
# Otros atributos que no vienen en los metadatos
|
|
PRIVATE_ATTRIBUTES = %i[path slug attributes errors].freeze
|
|
PUBLIC_ATTRIBUTES = %i[lang date uuid].freeze
|
|
|
|
class << self
|
|
# Obtiene el layout sin leer el Document
|
|
def find_layout(doc)
|
|
SafeYAML.load(IO.foreach(doc.path).lazy.grep(/^layout: /).take(1).first)
|
|
.try(:[], 'layout')
|
|
.try(:to_sym)
|
|
end
|
|
end
|
|
|
|
# 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
|
|
#
|
|
def initialize(**args)
|
|
default_attributes_missing(args)
|
|
super(args)
|
|
|
|
# Genera un método con todos los atributos disponibles
|
|
self.attributes = layout.metadata.keys.map(&:to_sym) + PUBLIC_ATTRIBUTES
|
|
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.
|
|
#
|
|
# XXX: En el primer intento de hacerlo más óptimo, movimos esta
|
|
# lógica a instanciación bajo demanda, pero no solo no logramos
|
|
# optimizar sino que aumentamos el tiempo de carga :/
|
|
layout.metadata.each_pair do |name, template|
|
|
send "#{name}=".to_sym,
|
|
MetadataFactory.build(document: document,
|
|
post: self,
|
|
site: site,
|
|
name: name,
|
|
value: args[name.to_sym],
|
|
layout: layout,
|
|
type: template['type'],
|
|
label: template['label'],
|
|
help: template['help'],
|
|
required: template['required'])
|
|
end
|
|
|
|
# TODO: Llamar dinámicamente
|
|
load_lang!
|
|
load_slug!
|
|
load_date!
|
|
load_path!
|
|
load_uuid!
|
|
|
|
# XXX: No usamos Post#read porque a esta altura todavía no sabemos
|
|
# nada del Document
|
|
document.read! if File.exist? document.path
|
|
end
|
|
|
|
# TODO: Convertir a UUID?
|
|
def id
|
|
path.basename
|
|
end
|
|
|
|
def updated_at
|
|
File.mtime(path.absolute)
|
|
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)
|
|
# Limpiar el nombre del atributo, para que todos los ayudantes
|
|
# reciban el método en limpio
|
|
name = attribute_name mid
|
|
|
|
unless attribute? name
|
|
raise NoMethodError, I18n.t('exceptions.post.no_method',
|
|
method: mid)
|
|
end
|
|
|
|
# Definir los attribute_*
|
|
new_attribute_was(name)
|
|
new_attribute_changed(name)
|
|
|
|
# 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)
|
|
included = DEFAULT_ATTRIBUTES.include?(mid) ||
|
|
PRIVATE_ATTRIBUTES.include?(mid) ||
|
|
PUBLIC_ATTRIBUTES.include?(mid)
|
|
|
|
if !included && singleton_class.method_defined?(:attributes)
|
|
included = attributes.include? mid
|
|
end
|
|
|
|
included
|
|
end
|
|
|
|
# Devuelve los strong params para el layout
|
|
def params
|
|
attributes.map do |attr|
|
|
send(attr).to_param
|
|
end
|
|
end
|
|
|
|
# Genera el post con metadatos en YAML
|
|
def full_content
|
|
body = ''
|
|
yaml = layout.metadata.keys.map(&:to_sym).map do |metadata|
|
|
template = send(metadata)
|
|
|
|
unless template.front_matter?
|
|
body += "\n\n"
|
|
body += template.value
|
|
next
|
|
end
|
|
|
|
next if template.empty?
|
|
|
|
{ metadata.to_s => template.value }
|
|
end.compact.inject(:merge)
|
|
|
|
# TODO: Convertir a Metadata?
|
|
# Asegurarse que haya un layout
|
|
yaml['layout'] = layout.name.to_s
|
|
yaml['uuid'] = uuid.value
|
|
# Y que no se procese liquid
|
|
yaml['liquid'] = false
|
|
yaml['usuaries'] = usuaries.map(&:id).uniq
|
|
|
|
"#{yaml.to_yaml}---\n\n#{body}"
|
|
end
|
|
|
|
# Eliminar el artículo del repositorio y de la lista de artículos del
|
|
# sitio
|
|
def destroy
|
|
FileUtils.rm_f path.absolute
|
|
|
|
site.delete_post self
|
|
end
|
|
alias destroy! destroy
|
|
|
|
# Guarda los cambios
|
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
def save(validate: true)
|
|
return false if validate && !valid?
|
|
# Salir si tenemos que cambiar el nombre del archivo y no pudimos
|
|
return false if !new? && path_changed? && !update_path!
|
|
return false unless save_attributes!
|
|
return false unless write
|
|
|
|
# Vuelve a leer el post para tomar los cambios
|
|
read
|
|
|
|
written?
|
|
end
|
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
alias save! save
|
|
|
|
# Actualiza la ruta del documento y lo lee
|
|
def read
|
|
return unless written?
|
|
|
|
document.path = path.absolute
|
|
document.read!
|
|
end
|
|
|
|
def new?
|
|
document.path.blank?
|
|
end
|
|
|
|
def written?
|
|
File.exist? path.absolute
|
|
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?
|
|
self.errors = {}
|
|
|
|
layout.metadata.keys.map(&:to_sym).each do |metadata|
|
|
template = send(metadata)
|
|
|
|
errors[metadata] = template.errors unless template.valid?
|
|
end
|
|
|
|
errors.blank?
|
|
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).save
|
|
end
|
|
|
|
# Verifica si hace falta escribir cambios
|
|
def persisted?
|
|
File.exist?(path.absolute) && full_content == File.read(path.absolute)
|
|
end
|
|
|
|
def destroyed?
|
|
!File.exist?(path.absolute)
|
|
end
|
|
|
|
def update_attributes(hashable)
|
|
hashable.to_hash.each do |name, value|
|
|
self[name].value = value
|
|
end
|
|
|
|
save
|
|
end
|
|
alias update update_attributes
|
|
|
|
# El Document guarda un Array de los ids de Usuarie. Si está vacío,
|
|
# no hacemos una consulta vacía. Si no, traemos todes les Usuaries
|
|
# por su id y convertimos a Array para poder agregar o quitar luego
|
|
# sin pasar por ActiveRecord.
|
|
def usuaries
|
|
@usuaries ||= if (d = document_usuaries).empty?
|
|
[]
|
|
else
|
|
Usuarie.where(id: d).to_a
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# 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
|
|
|
|
def document_usuaries
|
|
document.data.fetch('usuaries', [])
|
|
end
|
|
|
|
def new_attribute_was(method)
|
|
attr_was = "#{method}_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 = "#{method}_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
|
|
|
|
# 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
|
|
|
|
def load_lang!
|
|
self.lang = MetadataLang.new(document: document, site: site,
|
|
layout: layout, name: :lang,
|
|
type: :lang, post: self,
|
|
required: true)
|
|
end
|
|
|
|
def load_uuid!
|
|
self.uuid = MetadataUuid.new(document: document, site: site,
|
|
layout: layout, name: :uuid,
|
|
type: :uuid, post: self,
|
|
required: true)
|
|
end
|
|
|
|
# Ejecuta la acción de guardado en cada atributo
|
|
def save_attributes!
|
|
attributes.map do |attr|
|
|
send(attr).save
|
|
end.all?
|
|
end
|
|
end
|
|
# rubocop:enable Metrics/ClassLength
|
|
# rubocop:enable Style/MethodMissingSuper
|
|
# rubocop:enable Style/MissingRespondToMissing
|