sutty/app/models/post.rb

300 lines
8.5 KiB
Ruby
Raw Normal View History

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
# para modificarlos y crear nuevos.
#
# rubocop:disable Metrics/ClassLength
2019-08-07 15:10:14 +00:00
# rubocop:disable Style/MethodMissingSuper
# rubocop:disable Style/MissingRespondToMissing
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
2019-08-07 15:10:14 +00:00
# TODO: Mover a su propia clase para poder hacer limpiezas
# independientemente
self.content = document.content
2019-08-07 21:35:37 +00:00
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.
layout.metadata.each_pair do |name, template|
send "#{name}=".to_sym,
MetadataFactory.build(document: document,
site: site,
name: name,
2019-08-07 21:35:37 +00:00
layout: layout,
type: template['type'],
label: template['label'],
help: template['help'],
required: template['required'])
end
2019-08-07 21:35:37 +00:00
load_slug!
load_date!
# Leer el documento
read
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
# 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
raise ArgumentError, i18n unless args[attr].present?
end
2018-01-29 22:19:10 +00:00
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)
unless attribute? mid
raise NoMethodError, I18n.t('exceptions.post.no_method',
method: mid)
end
2019-08-07 21:35:37 +00:00
# Definir los attribute_*
new_attribute_was(mid)
new_attribute_changed(mid)
# OpenStruct
super(mid, *args)
2019-08-07 21:35:37 +00:00
# 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)
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
# Genera el post con metadatos en YAML
2019-08-07 15:10:14 +00:00
# rubocop:disable Metrics/AbcSize
def full_content
yaml = layout.metadata.keys.map(&:to_sym).map do |metadata|
template = send(metadata)
2018-04-27 18:48:26 +00:00
{ metadata.to_s => template.value } unless template.empty?
end.compact.inject(:merge)
2018-02-22 19:01:11 +00:00
2019-08-07 15:10:14 +00:00
# Asegurarse que haya un layout
yaml['layout'] = layout.name.to_s
"#{yaml.to_yaml}---\n\n#{content}"
end
# Eliminar el artículo del repositorio y de la lista de artículos del
# sitio
#
# XXX Commit
def destroy
FileUtils.rm_f path
site.posts(lang: lang).delete_if do |post|
post.path == path
end
2019-03-26 15:32:20 +00:00
!File.exist?(path) && !site.posts(lang: lang).include?(self)
2018-02-22 19:01:11 +00:00
end
alias destroy! destroy
2019-08-07 15:10:14 +00:00
# rubocop:enable Metrics/AbcSize
2018-02-22 19:01:11 +00:00
2019-08-07 15:10:14 +00:00
# Guarda los cambios
2018-01-29 22:19:10 +00:00
def save
return false unless valid?
2019-08-07 15:10:14 +00:00
return false unless write
2018-01-29 22:19:10 +00:00
# Vuelve a leer el post para tomar los cambios
document.read
2019-08-07 15:10:14 +00:00
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
2019-08-07 21:35:37 +00:00
# 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.
2018-01-29 22:19:10 +00:00
def path
document.path
2018-01-29 22:19:10 +00:00
end
2019-08-07 21:35:37 +00:00
alias relative_path path
def absolute_path
File.join site.path, path
end
2018-01-29 22:19:10 +00:00
# Detecta si el artículo es válido para guardar
def valid?
validate
errors.blank?
end
# Requisitos para que el post sea válido
def validate
self.errors = {}
2018-02-02 22:20:31 +00:00
layout.metadata.keys.map(&:to_sym).each do |metadata|
template = send(metadata)
2018-07-23 19:48:34 +00:00
errors[metadata] = template.errors unless template.valid?
end
end
alias validate! validate
# Solo agregar el post al sitio una vez que lo guardamos
#
# TODO no sería la forma correcta de hacerlo en Rails
2019-08-07 15:10:14 +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-07 15:10:14 +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?
# 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
Rails.logger.info I18n.t('posts.logger.rm', path: path)
FileUtils.rm @post.path
2019-08-07 15:10:14 +00:00
# replace_post!
2018-01-29 22:19:10 +00:00
end
# Reemplaza el post en el sitio por uno nuevo
2019-08-07 15:10:14 +00:00
# TODO: refactorizar
# def replace_post!
# @old_post = @site.jekyll.collections[@lang].docs.delete @post
2018-01-29 22:19:10 +00:00
2019-08-07 15:10:14 +00:00
# new_post
# end
2018-01-29 22:19:10 +00:00
# Guarda los cambios en el archivo destino
def write
2019-08-07 15:10:14 +00:00
return true if persisted?
2018-01-29 22:19:10 +00:00
2019-08-07 15:10:14 +00:00
Site::Writer.new(site: site, file: path,
content: full_content, usuarie: usuarie,
message: title.value).save
2018-01-29 22:19:10 +00:00
end
2019-08-07 15:10:14 +00:00
# Devuelve le autore de un artículo
# XXX: Si se cambia le autore en el editor también cambia quién hace
# un cambio y se le puede asignar a cualquier otre.
# TODO: Mover la escritura a un servicio que combine escritura, commit
# y current_usuarie
def usuarie
OpenStruct.new(email: author.value.first,
name: author.value.first.split('@', 2).first)
end
2018-02-02 22:20:31 +00:00
2019-08-07 15:10:14 +00:00
# Verifica si hace falta escribir cambios
def persisted?
2019-08-07 21:35:37 +00:00
File.exist?(absolute_path) && full_content == File.read(absolute_path)
2018-02-02 22:20:31 +00:00
end
2019-08-07 15:10:14 +00:00
private
2019-08-07 21:35:37 +00:00
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
2019-08-07 21:35:37 +00:00
# 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
2019-08-07 21:35:37 +00:00
# rubocop:enable Metrics/AbcSize
2019-08-07 21:35:37 +00:00
# Obtiene el nombre del atributo a partir del nombre del método
def attribute_name(attr)
2019-08-07 21:35:37 +00:00
# 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
2018-01-29 22:19:10 +00:00
end
# rubocop:enable Metrics/ClassLength
2019-08-07 15:10:14 +00:00
# rubocop:enable Style/MethodMissingSuper
# rubocop:enable Style/MissingRespondToMissing