mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-14 22:51:41 +00:00
2a70d6a8db
antes permitiamos la modificación en memoria pero no al salvar, lo que producía un bug porque el valor se seteaba pero no se aplicaban conversiones al guardarlo. los numeros, por ejemplo se guardaban como strings. XXX: no aplicar conversiones al guardar sino al setear.
387 lines
11 KiB
Ruby
387 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Esta clase representa un post en un sitio jekyll e incluye métodos
|
|
# para modificarlos y crear nuevos.
|
|
#
|
|
# * Los metadatos se tienen que cargar dinámicamente, solo usamos los
|
|
# que necesitamos
|
|
#
|
|
#
|
|
class Post
|
|
# 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
|
|
ATTR_SUFFIXES = %w[? =].freeze
|
|
|
|
attr_reader :attributes, :errors, :layout, :site, :document
|
|
|
|
class << self
|
|
# Obtiene el layout sin leer el Document
|
|
#
|
|
# TODO: Reemplazar cuando leamos el contenido del Document
|
|
# a demanda?
|
|
def find_layout(path)
|
|
IO.foreach(path).lazy.grep(/^layout: /).take(1).first&.split(' ')&.last&.tr('\'', '')&.tr('"', '')&.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)
|
|
|
|
# Genera un método con todos los atributos disponibles
|
|
@layout = args[:layout]
|
|
@site = args[:site]
|
|
@document = args[:document]
|
|
@attributes = layout.attributes + PUBLIC_ATTRIBUTES
|
|
@errors = {}
|
|
@metadata = {}
|
|
|
|
# Inicializar valores
|
|
attributes.each do |attr|
|
|
public_send(attr)&.value = args[attr] if args.key?(attr)
|
|
end
|
|
|
|
# XXX: No usamos Post#read porque a esta altura todavía no sabemos
|
|
# nada del Document
|
|
document.read! if File.exist? document.path
|
|
end
|
|
|
|
def inspect
|
|
"#<Post id=\"#{id}\">"
|
|
end
|
|
|
|
# Renderiza el artículo para poder previsualizarlo. Leemos solo la
|
|
# información básica, con lo que no van a funcionar artículos
|
|
# relacionados y otras cuestiones.
|
|
#
|
|
# @see app/lib/jekyll/tags/base.rb
|
|
def render
|
|
Dir.chdir site.path do
|
|
# Compatibilidad con jekyll-locales, necesario para el filtro
|
|
# date_local
|
|
#
|
|
# TODO: Cambiar el locale en otro lado
|
|
l = lang.value.to_s
|
|
site.jekyll.config['locale'] = site.jekyll.config['lang'] = l
|
|
|
|
# Payload básico con traducciones.
|
|
document.renderer.payload = {
|
|
'site' => {
|
|
'data' => site.data,
|
|
'i18n' => site.data[l],
|
|
'lang' => l,
|
|
'locale' => l
|
|
},
|
|
'page' => document.to_liquid
|
|
}
|
|
|
|
# Renderizar lo estrictamente necesario y convertir a HTML para
|
|
# poder reemplazar valores.
|
|
html = Nokogiri::HTML document.renderer.render_document
|
|
# Las imágenes se cargan directamente desde el repositorio, porque
|
|
# no son públicas hasta que se publica el artículo.
|
|
html.css('img').each do |img|
|
|
next if %r{\Ahttps?://} =~ img.attributes['src']
|
|
|
|
img.attributes['src'].value = Rails.application.routes.url_helpers.site_static_file_url(site,
|
|
file: img.attributes['src'].value)
|
|
end
|
|
|
|
# Notificar a les usuaries que están viendo una previsualización
|
|
# XXX: Asume que estamos usando Bootstrap :B
|
|
html.at_css('body')&.first_element_child&.before("<div class=\"alert alert-warning text-center\">#{I18n.t('posts.preview.message')}</div>")
|
|
|
|
# Cacofonía
|
|
html.to_html.html_safe
|
|
end
|
|
end
|
|
|
|
# Devuelve una llave para poder guardar el post en una cache
|
|
def cache_key
|
|
'posts/' + uuid.value
|
|
end
|
|
|
|
def cache_version
|
|
updated_at.utc.to_s(:usec)
|
|
end
|
|
|
|
# Agregar el timestamp para saber si cambió, siguiendo el módulo
|
|
# ActiveRecord::Integration
|
|
def cache_key_with_version
|
|
cache_key + '-' + cache_version
|
|
end
|
|
|
|
# TODO: Convertir a UUID?
|
|
def id
|
|
path.basename
|
|
end
|
|
alias to_param id
|
|
|
|
# Fecha de última modificación del archivo
|
|
def updated_at
|
|
File.mtime(path.absolute)
|
|
end
|
|
|
|
# Obtiene la fecha actual de modificación y la guarda hasta la próxima
|
|
# vez.
|
|
def modified_at
|
|
@modified_at ||= Time.now
|
|
end
|
|
|
|
def [](attr)
|
|
public_send attr
|
|
end
|
|
|
|
# Define metadatos a demanda
|
|
def method_missing(name, *_args)
|
|
# Limpiar el nombre del atributo, para que todos los ayudantes
|
|
# reciban el método en limpio
|
|
unless attribute? name
|
|
raise NoMethodError, I18n.t('exceptions.post.no_method',
|
|
method: name)
|
|
end
|
|
|
|
define_singleton_method(name) do
|
|
template = layout.metadata[name.to_s]
|
|
|
|
@metadata[name] ||=
|
|
MetadataFactory.build(document: document,
|
|
post: self,
|
|
site: site,
|
|
name: name,
|
|
layout: layout,
|
|
type: template['type'],
|
|
label: template['label'],
|
|
help: template['help'],
|
|
required: template['required'])
|
|
end
|
|
|
|
public_send name
|
|
end
|
|
|
|
# TODO: Mover a method_missing
|
|
def slug
|
|
@metadata[:slug] ||= MetadataSlug.new(document: document, site: site, layout: layout, name: :slug, type: :slug,
|
|
post: self, required: true)
|
|
end
|
|
|
|
# TODO: Mover a method_missing
|
|
def date
|
|
@metadata[:date] ||= MetadataDocumentDate.new(document: document, site: site, layout: layout, name: :date,
|
|
type: :document_date, post: self, required: true)
|
|
end
|
|
|
|
# TODO: Mover a method_missing
|
|
def path
|
|
@metadata[:path] ||= MetadataPath.new(document: document, site: site, layout: layout, name: :path, type: :path,
|
|
post: self, required: true)
|
|
end
|
|
|
|
# TODO: Mover a method_missing
|
|
def lang
|
|
@metadata[:lang] ||= MetadataLang.new(document: document, site: site, layout: layout, name: :lang, type: :lang,
|
|
post: self, required: true)
|
|
end
|
|
|
|
# TODO: Mover a method_missing
|
|
def uuid
|
|
@metadata[:uuid] ||= MetadataUuid.new(document: document, site: site, layout: layout, name: :uuid, type: :uuid,
|
|
post: self, required: true)
|
|
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)
|
|
|
|
included = attributes.include? mid if !included && singleton_class.method_defined?(:attributes)
|
|
|
|
included
|
|
end
|
|
|
|
# Devuelve los strong params para el layout.
|
|
#
|
|
# XXX: Nos gustaría no tener que instanciar Metadata acá, pero depende
|
|
# del valor por defecto que a su vez depende de Layout.
|
|
def params
|
|
attributes.map do |attr|
|
|
public_send(attr)&.to_param
|
|
end.compact
|
|
end
|
|
|
|
# Genera el post con metadatos en YAML
|
|
#
|
|
# TODO: Cachear por un minuto
|
|
def full_content
|
|
body = ''
|
|
yaml = layout.attributes.map do |attr|
|
|
template = public_send attr
|
|
|
|
unless template.front_matter?
|
|
body += "\n\n"
|
|
body += template.value
|
|
next
|
|
end
|
|
|
|
next if template.empty?
|
|
|
|
[attr.to_s, template.value]
|
|
end.compact.to_h
|
|
|
|
# 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['last_modified_at'] = modified_at
|
|
|
|
"#{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
|
|
# TODO: Agregar una forma de congelar todos los valores y solo guardar
|
|
# uno, para no incorporar modificaciones
|
|
# 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!
|
|
|
|
# Si el archivo va a estar duplicado, agregar un número al slug
|
|
if new? && written?
|
|
original_slug = slug.value
|
|
count = 1
|
|
|
|
while written?
|
|
count += 1
|
|
slug.value = "#{original_slug}-#{count}"
|
|
end
|
|
end
|
|
|
|
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.value_was, path.absolute) &&
|
|
document.path = path.absolute
|
|
end
|
|
|
|
# Detecta si el artículo es válido para guardar
|
|
def valid?
|
|
@errors = {}
|
|
|
|
attributes.each do |attr|
|
|
errors[attr] = self[attr].errors unless self[attr].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
|
|
#
|
|
# TODO: Cachear el resultado o usar otro método, por ejemplo guardando
|
|
# la fecha de modificación al leer y compararla al hacer cambios sin
|
|
# escribirlos.
|
|
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 |attr, value|
|
|
next unless self[attr].writable?
|
|
|
|
self[attr].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 ||= document_usuaries.empty? ? [] : Usuarie.where(id: document_usuaries).to_a
|
|
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|
|
|
raise ArgumentError, I18n.t("exceptions.post.#{attr}_missing") unless args[attr].present?
|
|
end
|
|
end
|
|
|
|
def document_usuaries
|
|
@document_usuaries ||= document.data.fetch('usuaries', [])
|
|
end
|
|
|
|
# Ejecuta la acción de guardado en cada atributo.
|
|
def save_attributes!
|
|
attributes.map do |attr|
|
|
self[attr].save
|
|
end.all?
|
|
end
|
|
end
|