5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-15 04:51:43 +00:00
panel/app/models/post.rb

332 lines
8.4 KiB
Ruby
Raw Normal View History

2018-01-29 22:19:10 +00:00
# frozen_string_literal: true
require 'jekyll/utils'
# Esta clase representa un post en un sitio jekyll e incluye métodos
# para modificarlos y crear nuevos
#
# Cuando estamos editando un post, instanciamos este modelo y le
# asociamos el Jekyll::Document correspondiente.
#
# Cuando estamos creando un post, no creamos su Jekyll::Document
# hasta que se guardan los datos, porque para poder guardarlo
# necesitamos su front_matter completo.
#
# El front matter está duplicado. El Post mantiene una copia de los
# datos y los sincroniza al momento de leer y de escribir el Document.
2018-01-29 22:19:10 +00:00
class Post
attr_accessor :content, :front_matter
2018-02-22 19:01:11 +00:00
attr_reader :post, :site, :errors, :old_post, :lang
2018-01-29 22:19:10 +00:00
REJECT_FROM_DATA = %w[excerpt].freeze
# datos que no tienen que terminar en el front matter
REJECT_FROM_FRONT_MATTER = %w[date slug draft ext].freeze
2018-01-29 22:19:10 +00:00
# Trabajar con posts. Si estamos creando uno nuevo, el **site** y
# el **front_matter** son necesarios, sino, **site** y **post**.
# XXX chequear que se den las condiciones
2018-02-22 19:01:11 +00:00
def initialize(site:, post: nil, front_matter: {}, lang: nil)
unless site.is_a?(Site)
raise ArgumentError,
I18n.t('errors.argument_error', argument: :site, class: Site)
end
unless post.nil? || post.is_a?(Jekyll::Document)
raise ArgumentError,
I18n.t('errors.argument_error', argument: :post,
class: Jekyll::Document)
end
2018-02-02 22:20:31 +00:00
2018-02-22 19:01:11 +00:00
@lang = lang || I18n.locale.to_s
2018-01-29 22:19:10 +00:00
@site = site
@post = post
2018-01-31 20:29:27 +00:00
# los errores tienen que ser un hash para que
# ActiveModel pueda traer los errores normalmente
@errors = {}
2018-01-29 22:19:10 +00:00
# sincronizar los datos del document
if new?
@front_matter = {}
update_attributes front_matter
else
load_front_matter!
merge_with_front_matter! front_matter.stringify_keys
end
end
# Limpiar los errores
def reset_errors!
@errors = {}
2018-01-29 22:19:10 +00:00
end
# El post es nuevo si no hay un documento asociado
2018-01-29 22:19:10 +00:00
def new?
@post.nil? || !File.exist?(@post.try(:path))
2018-01-29 22:19:10 +00:00
end
2018-02-22 19:01:11 +00:00
# Determina si fue traducido, buscando los slugs de su front_matter
# lang en otras colecciones
def translated?
get_front_matter('lang').present?
end
def translations
@translations ||= get_front_matter('lang').map do |lang, id|
next if lang == @lang
@site.posts_for(lang).find do |p|
p.id == id
end
end.compact
end
# 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
cleanup!
return false unless valid?
new_post if new?
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
@post.read
add_post_to_site!
2018-01-29 22:19:10 +00:00
true
end
alias :save! :save
def title
get_front_matter 'title'
2018-01-29 22:19:10 +00:00
end
def date
get_front_matter 'date'
2018-01-29 22:19:10 +00:00
end
def tags
get_front_matter 'tags'
2018-01-29 22:19:10 +00:00
end
def categories
get_front_matter 'categories'
2018-01-29 22:19:10 +00:00
end
alias :category :categories
# 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
2018-02-02 22:20:31 +00:00
basename_changed? ? File.join(@site.path, '_posts', basename_from_front_matter) : @post.try(:path)
2018-01-29 22:19:10 +00:00
end
# TODO los slugs se pueden repetir, el identificador real sería
# fecha+slug, pero se ve feo en las urls?
2018-01-29 22:19:10 +00:00
def id
get_front_matter 'slug'
2018-01-29 22:19:10 +00:00
end
alias :slug :id
alias :to_s :id
def basename_changed?
@post.try(:basename) != basename_from_front_matter
2018-01-30 15:20:19 +00:00
end
def content
@content ||= @post.try :content
2018-02-02 22:20:31 +00:00
end
# imita Model.update_attributes de ActiveRecord
def update_attributes(attrs)
# el cuerpo se maneja por separado
@content = attrs.delete('content') if attrs.key? 'content'
merge_with_front_matter! attrs.stringify_keys
end
# Requisitos para que el post sea válido
def validate
add_error validate: I18n.t('posts.errors.date') unless get_front_matter('date').is_a? Time
add_error validate: I18n.t('posts.errors.title') if get_front_matter('title').blank?
# TODO verificar que el id sea único
end
def valid?
reset_errors!
validate
@errors.empty?
end
# Permite ordenar los posts
def <=>(other)
@post <=> other.post
2018-01-30 15:20:19 +00:00
end
2018-02-08 14:05:05 +00:00
# Obtiene metadatos asegurándose que siempre trabajamos con strings
def get_front_matter(name)
@front_matter.dig(name.to_s)
end
2018-01-29 22:19:10 +00:00
private
# Genera un post nuevo y lo agrega a la colección del sitio.
2018-01-29 22:19:10 +00:00
def new_post
opts = { site: @site.jekyll, collection: @site.jekyll.posts }
2018-02-02 22:20:31 +00:00
@post = Jekyll::Document.new(path, opts)
end
# Solo agregar el post al sitio una vez que lo guardamos
#
# TODO no sería la forma correcta de hacerlo en Rails
def add_post_to_site!
2018-02-02 22:20:31 +00:00
@site.jekyll.posts.docs << @post
@site.jekyll.posts.docs.sort!
2018-01-29 22:19:10 +00:00
@site.posts << self unless @site.posts.include? self
@site.posts.sort!
2018-01-29 22:19:10 +00:00
end
# Los define, asegurandose que las llaves siempre son strings, para no
# tener incompatibilidades con jekyll
def set_front_matter(name, value)
@front_matter[name.to_s] = value
2018-01-29 22:19:10 +00:00
end
# 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
replace_post!
2018-01-29 22:19:10 +00:00
end
# Reemplaza el post en el sitio por uno nuevo
def replace_post!
@old_post = @site.jekyll.posts.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
date = get_front_matter('date').strftime('%F')
slug = get_front_matter('slug')
ext = get_front_matter('ext') || '.markdown'
2018-01-29 22:19:10 +00:00
"#{date}-#{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
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
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
def cleanup!
things_to_arrays!
date_to_time!
clean_content!
slugify_title!
remove_empty_front_matter!
end
def remove_empty_front_matter!
@front_matter.delete_if do |k,v|
v.blank?
end
end
2018-01-29 22:19:10 +00:00
# Aplica limpiezas básicas del contenido
def clean_content!
@content.try(:delete!, "\r")
2018-01-29 22:19:10 +00:00
end
# Guarda los cambios en el archivo destino
def write
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
# Genera el post con front matter, menos los campos que no necesitamos
# que estén en el front matter
2018-01-29 22:19:10 +00:00
def full_content
yaml = @front_matter.reject do |k,_|
REJECT_FROM_FRONT_MATTER.include? k
end
"#{yaml.to_yaml}---\n\n#{@content}"
2018-01-29 22:19:10 +00:00
end
2018-02-02 22:20:31 +00:00
def add_error(hash)
hash.each_pair do |k,i|
if @errors.key?(k)
@errors[k] = [@errors[k], i]
else
@errors[k] = i
end
end
@errors
end
def date_to_time!
unless @front_matter.dig(:date).is_a? Time
@front_matter['date'] = @front_matter.dig('date').try(:to_time) || Time.now
end
end
def things_to_arrays!
[:tags,:categories].each do |c|
thing = @front_matter.dig(c.to_s)
next if thing.is_a? Array
@front_matter[c.to_s] = thing.split(',').map(&:strip)
end
end
def slugify_title!
if get_front_matter('slug').blank?
title = get_front_matter('title')
@front_matter['slug'] = Jekyll::Utils.slugify(title)
end
end
2018-01-29 22:19:10 +00:00
end