mirror of
https://0xacab.org/sutty/sutty
synced 2025-01-19 13:53:38 +00:00
WIP: refactorización de Post
El objetivo es aprovechar metaprogramación para tener un modelo similar a ActiveRecord pero con otras propiedades, como atributos dinámicos a partir de plantillas. Estamos eliminando un montón de código, con lo que no todo puede funcionar.
This commit is contained in:
parent
3b46600ed5
commit
efe401d740
11 changed files with 449 additions and 546 deletions
|
@ -1,8 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Representa la plantilla de un campo en los metadatos del artículo
|
# Representa la plantilla de un campo en los metadatos del artículo
|
||||||
|
#
|
||||||
|
# TODO: Validar el tipo de valor pasado a value= según el :type
|
||||||
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
:value, :help, :required,
|
:value, :help, :required, :errors,
|
||||||
keyword_init: true) do
|
keyword_init: true) do
|
||||||
# El valor por defecto
|
# El valor por defecto
|
||||||
def default_value
|
def default_value
|
||||||
|
@ -12,11 +14,36 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
# Valores posibles, busca todos los valores actuales en otros
|
# Valores posibles, busca todos los valores actuales en otros
|
||||||
# artículos del mismo sitio
|
# artículos del mismo sitio
|
||||||
def values
|
def values
|
||||||
site.everything_of(name.to_s)
|
site.everything_of(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Valor actual o por defecto
|
# Valor actual o por defecto
|
||||||
def value
|
def value
|
||||||
super || document.data.dig(name.to_s, default_value)
|
self[:value] || document.data.fetch(name.to_s, default_value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el valor está vacío
|
||||||
|
def empty?
|
||||||
|
value.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comprueba si el metadato es válido
|
||||||
|
def valid?
|
||||||
|
validate
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate
|
||||||
|
self.errors = []
|
||||||
|
|
||||||
|
errors << I18n.t("metadata.#{type}.cant_be_empty") unless can_be_empty?
|
||||||
|
|
||||||
|
errors.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Si es obligatorio no puede estar vacío
|
||||||
|
def can_be_empty?
|
||||||
|
true unless required && empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,125 +3,120 @@
|
||||||
require 'jekyll/utils'
|
require 'jekyll/utils'
|
||||||
|
|
||||||
# Esta clase representa un post en un sitio jekyll e incluye métodos
|
# Esta clase representa un post en un sitio jekyll e incluye métodos
|
||||||
# para modificarlos y crear nuevos
|
# para modificarlos y crear nuevos.
|
||||||
#
|
#
|
||||||
# Cuando estamos editando un post, instanciamos este modelo y le
|
# rubocop:disable Metrics/ClassLength
|
||||||
# asociamos el Jekyll::Document correspondiente.
|
class Post < OpenStruct
|
||||||
#
|
# Atributos por defecto
|
||||||
# Cuando estamos creando un post, no creamos su Jekyll::Document
|
# XXX: Volver document opcional cuando estemos creando
|
||||||
# hasta que se guardan los datos, porque para poder guardarlo
|
DEFAULT_ATTRIBUTES = %i[site document layout].freeze
|
||||||
# necesitamos su front_matter completo.
|
# Otros atributos que no vienen en los metadatos
|
||||||
#
|
ATTRIBUTES = %i[content lang date slug attributes errors].freeze
|
||||||
# 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.
|
|
||||||
class Post
|
|
||||||
attr_accessor :content, :front_matter
|
|
||||||
attr_reader :post, :site, :errors, :old_post, :lang, :template,
|
|
||||||
:template_fields, :collection
|
|
||||||
|
|
||||||
REJECT_FROM_DATA = %w[excerpt].freeze
|
# Redefinir el inicializador de OpenStruct
|
||||||
# datos que no tienen que terminar en el front matter
|
#
|
||||||
REJECT_FROM_FRONT_MATTER = %w[date slug ext].freeze
|
# @param site: [Site] el sitio en Sutty
|
||||||
# datos que no traemos del template
|
# @param document: [Jekyll::Document] el documento leído por Jekyll
|
||||||
REJECT_FROM_TEMPLATE = %w[draft categories layout ext tags date slug post pre].freeze
|
# @param layout: [Layout] la plantilla
|
||||||
DEFAULT_PARAMS = [:title, :date, :content, :slug, :cover,
|
#
|
||||||
:layout, :permalink, :dir,
|
# rubocop:disable Metrics/AbcSize
|
||||||
{ lang: {} }, { tags: [] }, { categories: [] }].freeze
|
# rubocop:disable Metrics/MethodLength
|
||||||
|
def initialize(**args)
|
||||||
|
default_attributes_missing(args)
|
||||||
|
super(args)
|
||||||
|
|
||||||
def inspect
|
# Genera un método con todos los atributos disponibles
|
||||||
"#<Post @id=#{id} @site=#{site.name}>"
|
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'])
|
||||||
|
end
|
||||||
|
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")
|
||||||
|
|
||||||
|
raise ArgumentError, i18n unless args[attr].present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Trabajar con posts. Si estamos creando uno nuevo, el **site** y
|
# Solo ejecuta la magia de OpenStruct si el campo existe en la
|
||||||
# el **front_matter** son necesarios, sino, **site** y **post**.
|
# plantilla
|
||||||
# XXX chequear que se den las condiciones
|
#
|
||||||
def initialize(site:, post: nil, front_matter: {}, lang: nil, template: nil)
|
# XXX: Reemplazarlo por nuestro propio método, mantener todo lo demás
|
||||||
unless site.is_a?(Site)
|
# compatible con OpenStruct
|
||||||
raise ArgumentError,
|
#
|
||||||
I18n.t('errors.argument_error', argument: :site, class: Site)
|
# 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
|
end
|
||||||
|
|
||||||
unless post.nil? || post.is_a?(Jekyll::Document)
|
super(mid, *args)
|
||||||
raise ArgumentError,
|
|
||||||
I18n.t('errors.argument_error', argument: :post,
|
|
||||||
class: Jekyll::Document)
|
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Style/MethodMissingSuper
|
||||||
|
|
||||||
@site = site
|
# Detecta si es un atributo válido o no, a partir de la tabla de la
|
||||||
@post = post
|
# plantilla
|
||||||
@template = template
|
def attribute?(mid)
|
||||||
# los errores tienen que ser un hash para que
|
if singleton_class.method_defined? :attributes
|
||||||
# ActiveModel pueda traer los errores normalmente
|
attributes.include? attribute_name(mid)
|
||||||
@errors = {}
|
|
||||||
|
|
||||||
# Si el sitio está traducido, trabajamos con la colección del
|
|
||||||
# idioma, sino, con posts
|
|
||||||
@collection = if @site.i18n?
|
|
||||||
@lang = lang || I18n.locale.to_s
|
|
||||||
else
|
else
|
||||||
'posts'
|
(DEFAULT_ATTRIBUTES + ATTRIBUTES).include? attribute_name(mid)
|
||||||
end
|
|
||||||
|
|
||||||
# sincronizar los datos del document
|
|
||||||
if new?
|
|
||||||
@front_matter = front_matter_from_template
|
|
||||||
update_attributes front_matter
|
|
||||||
else
|
|
||||||
load_front_matter!
|
|
||||||
merge_with_front_matter! front_matter.stringify_keys
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Limpiar los errores
|
# Genera el post con metadatos en YAML
|
||||||
def reset_errors!
|
def full_content
|
||||||
@errors = {}
|
yaml = layout.metadata.keys.map(&:to_sym).map do |metadata|
|
||||||
|
template = send(metadata)
|
||||||
|
|
||||||
|
{ metadata.to_s => template.value } unless template.empty?
|
||||||
|
end.compact.inject(:merge)
|
||||||
|
|
||||||
|
"#{yaml.to_yaml}---\n\n#{content}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# El post es nuevo si no hay un documento asociado
|
# Eliminar el artículo del repositorio y de la lista de artículos del
|
||||||
def new?
|
# sitio
|
||||||
@post.nil? || !File.exist?(@post.try(:path))
|
#
|
||||||
|
# XXX Commit
|
||||||
|
def destroy
|
||||||
|
FileUtils.rm_f path
|
||||||
|
|
||||||
|
site.posts(lang: lang).delete_if do |post|
|
||||||
|
post.path == path
|
||||||
end
|
end
|
||||||
|
|
||||||
def draft?
|
!File.exist?(path) && !site.posts(lang: lang).include?(self)
|
||||||
fetch_front_matter('draft', false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def incomplete?
|
|
||||||
fetch_front_matter('incomplete', false)
|
|
||||||
end
|
|
||||||
|
|
||||||
# El número de orden del artículo, si no tiene uno, se le asigna la
|
|
||||||
# posición en la colección de artículos
|
|
||||||
def order
|
|
||||||
get_front_matter 'order'
|
|
||||||
end
|
|
||||||
|
|
||||||
def ordered?
|
|
||||||
!order.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Determina si fue traducido, buscando los slugs de su front_matter
|
|
||||||
# lang en otras colecciones
|
|
||||||
def translated?
|
|
||||||
@site.i18n? && get_front_matter('lang').present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def translations
|
|
||||||
@translations ||= find_translations
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_translations
|
|
||||||
slugs = get_front_matter('lang')
|
|
||||||
return [] unless slugs.present?
|
|
||||||
|
|
||||||
slugs.map do |lang, id|
|
|
||||||
next if lang == @lang
|
|
||||||
|
|
||||||
@site.posts_for(lang).find do |p|
|
|
||||||
p.id == id
|
|
||||||
end
|
|
||||||
end.compact
|
|
||||||
end
|
end
|
||||||
|
alias destroy! destroy
|
||||||
|
|
||||||
# Guarda los cambios.
|
# Guarda los cambios.
|
||||||
#
|
#
|
||||||
|
@ -133,60 +128,49 @@ class Post
|
||||||
|
|
||||||
return false unless valid?
|
return false unless valid?
|
||||||
|
|
||||||
new_post if new?
|
|
||||||
|
|
||||||
return unless write
|
return unless write
|
||||||
return unless detect_file_rename!
|
return unless detect_file_rename!
|
||||||
|
|
||||||
# Vuelve a leer el post para tomar los cambios
|
# Vuelve a leer el post para tomar los cambios
|
||||||
@post.read
|
document.read
|
||||||
add_post_to_site!
|
# add_post_to_site!
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
alias save! save
|
alias save! save
|
||||||
|
|
||||||
def title
|
|
||||||
get_front_matter 'title'
|
|
||||||
end
|
|
||||||
|
|
||||||
def author
|
|
||||||
get_front_matter 'author'
|
|
||||||
end
|
|
||||||
|
|
||||||
def date
|
|
||||||
get_front_matter 'date'
|
|
||||||
end
|
|
||||||
|
|
||||||
def date_as_string
|
|
||||||
date.strftime('%F')
|
|
||||||
end
|
|
||||||
|
|
||||||
def tags
|
|
||||||
get_front_matter('tags') || []
|
|
||||||
end
|
|
||||||
|
|
||||||
def categories
|
|
||||||
get_front_matter('categories') || []
|
|
||||||
end
|
|
||||||
alias category categories
|
|
||||||
|
|
||||||
# Devuelve la ruta del post, si se cambió alguno de los datos,
|
# Devuelve la ruta del post, si se cambió alguno de los datos,
|
||||||
# generamos una ruta nueva para tener siempre la ruta actualizada.
|
# generamos una ruta nueva para tener siempre la ruta actualizada.
|
||||||
def path
|
def path
|
||||||
if basename_changed?
|
document.path
|
||||||
File.join(@site.path, "_#{@collection}", basename_from_front_matter)
|
|
||||||
else
|
|
||||||
@post.try(:path)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: los slugs se pueden repetir, el identificador real sería
|
# Detecta si el artículo es válido para guardar
|
||||||
# fecha+slug, pero se ve feo en las urls?
|
def valid?
|
||||||
def id
|
validate
|
||||||
get_front_matter 'slug'
|
errors.blank?
|
||||||
end
|
end
|
||||||
alias slug id
|
|
||||||
alias to_s id
|
# Requisitos para que el post sea válido
|
||||||
|
def validate
|
||||||
|
self.errors = {}
|
||||||
|
|
||||||
|
layout.metadata.keys.map(&:to_sym).each do |metadata|
|
||||||
|
template = send(metadata)
|
||||||
|
|
||||||
|
errors[metadata] = template.errors unless template.valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias validate! validate
|
||||||
|
|
||||||
|
# Permite ordenar los posts
|
||||||
|
def <=>(other)
|
||||||
|
@post <=> other.post
|
||||||
|
end
|
||||||
|
|
||||||
|
# ****************
|
||||||
|
# A PARTIR DE ACA ESTAMOS CONSIDERANDO CUALES METODOS QUEDAN Y CUALES
|
||||||
|
# NO
|
||||||
|
# ****************
|
||||||
|
|
||||||
def basename_changed?
|
def basename_changed?
|
||||||
@post.try(:basename) != basename_from_front_matter
|
@post.try(:basename) != basename_from_front_matter
|
||||||
|
@ -196,108 +180,6 @@ class Post
|
||||||
new? || @post.data.dig('slug') != slug
|
new? || @post.data.dig('slug') != slug
|
||||||
end
|
end
|
||||||
|
|
||||||
# Trae el contenido del post, si no lo seteamos ya. O sea que si solo
|
|
||||||
# enviamos actualizaciones al front matter, debería traer el contenido
|
|
||||||
# del post sin cambios
|
|
||||||
def content
|
|
||||||
@content ||= @post.try(:content) || template.try(:content)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Determina si el post lleva contenido o es solo front_matter
|
|
||||||
def content?
|
|
||||||
has_field? :content
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_field?(field)
|
|
||||||
if template
|
|
||||||
template.fetch_front_matter("has_#{field}", true)
|
|
||||||
else
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# imita Model.update_attributes de ActiveRecord
|
|
||||||
# TODO Todo esto es malísimo, necesitamos una forma genérica de
|
|
||||||
# convertir params a objetos ruby (o que lo haga YAML directamente,
|
|
||||||
# ya que estamos). Tal vez separar en varios pasos, uno que arregle
|
|
||||||
# los hashes con indices numericos y los convierta a arrays, otro que
|
|
||||||
# convierta los params en hashes, otro que convierta los archivos
|
|
||||||
# temporales en una subida de archivos, etc.
|
|
||||||
def update_attributes(attrs)
|
|
||||||
# convertir los hashes en arrays si los campos son anidados
|
|
||||||
# usamos to_hash por todos lados porque sino son
|
|
||||||
# HashWithIndifferentAccess
|
|
||||||
_attrs = attrs.to_hash.map do |k, v|
|
|
||||||
t = template_fields.find { |t| t.key == k }
|
|
||||||
if t
|
|
||||||
# Subir la imagen!
|
|
||||||
# TODO pasar a su propio método
|
|
||||||
if t.image?
|
|
||||||
begin
|
|
||||||
i = Post::ImageUploader.new(site)
|
|
||||||
if t.multiple?
|
|
||||||
v = v.map do |tmp|
|
|
||||||
i.store! tmp.tempfile
|
|
||||||
i.url
|
|
||||||
end
|
|
||||||
else
|
|
||||||
i.store! v.tempfile
|
|
||||||
v = i.url
|
|
||||||
end
|
|
||||||
rescue CarrierWave::ProcessingError, CarrierWave::IntegrityError => e
|
|
||||||
v = e.message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if t.nested?
|
|
||||||
v = t.array? ? v.map(&:to_hash) : v.to_hash
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if v.is_a? ActionController::Parameters
|
|
||||||
{ k => v.to_hash }
|
|
||||||
else
|
|
||||||
{ k => v }
|
|
||||||
end
|
|
||||||
end.reduce({}, :merge).stringify_keys
|
|
||||||
|
|
||||||
# el cuerpo se maneja por separado
|
|
||||||
@content = _attrs.delete('content') if _attrs.key? 'content'
|
|
||||||
|
|
||||||
merge_with_front_matter! _attrs
|
|
||||||
end
|
|
||||||
|
|
||||||
# Requisitos para que el post sea válido
|
|
||||||
# TODO verificar que el id sea único
|
|
||||||
# TODO validar los parametros de la plantilla
|
|
||||||
def validate
|
|
||||||
add_error validate: I18n.t('posts.errors.date') unless date.is_a? Time
|
|
||||||
add_error validate: I18n.t('posts.errors.title') if title.blank?
|
|
||||||
add_error validate: I18n.t('posts.errors.slug_with_path') if slug.try(:include?, '/')
|
|
||||||
# XXX este es un principio de validación de plantillas, aunque no es
|
|
||||||
# recursivo
|
|
||||||
return if fetch_front_matter('incomplete', false)
|
|
||||||
|
|
||||||
template_fields.each do |tf|
|
|
||||||
errors = [get_front_matter(tf.key)].flatten.compact
|
|
||||||
if tf.image? && errors.map { |i| File.exist?(File.join(site.path, i)) }.none?
|
|
||||||
add_error Hash[tf.key.to_sym, errors]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid?
|
|
||||||
reset_errors!
|
|
||||||
validate
|
|
||||||
|
|
||||||
@errors.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Permite ordenar los posts
|
|
||||||
def <=>(other)
|
|
||||||
@post <=> other.post
|
|
||||||
end
|
|
||||||
|
|
||||||
# Detecta si un valor es un archivo
|
# Detecta si un valor es un archivo
|
||||||
def url?(name)
|
def url?(name)
|
||||||
path = get_front_matter(name)
|
path = get_front_matter(name)
|
||||||
|
@ -309,87 +191,14 @@ class Post
|
||||||
end.all?
|
end.all?
|
||||||
end
|
end
|
||||||
|
|
||||||
def image?(name)
|
|
||||||
return false unless url? name
|
|
||||||
|
|
||||||
# TODO: no chequear por la extensión
|
|
||||||
%(gif jpg jpeg png).include? get_front_matter(name).gsub(/.*\./, '')
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtiene metadatos de forma recursiva
|
|
||||||
# TODO devolver un valor por defecto en base al template?
|
|
||||||
def get_front_matter(name)
|
|
||||||
name = if name.is_a? Array
|
|
||||||
# Convertir los indices numericos a integers
|
|
||||||
name.map { |i| /[0-9]+/.match?(i) ? i.to_i : i }
|
|
||||||
else
|
|
||||||
# XXX retrocompatibilidad
|
|
||||||
name.to_s
|
|
||||||
end
|
|
||||||
@front_matter.dig(*name)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Como get_front_matter pero con un valor por defecto
|
|
||||||
def fetch_front_matter(name, default)
|
|
||||||
r = get_front_matter(name)
|
|
||||||
|
|
||||||
# Solo cuando es nulo, sino devolvemos el default si el valor es
|
|
||||||
# false
|
|
||||||
r.nil? ? default : r
|
|
||||||
end
|
|
||||||
|
|
||||||
# Trae el template a partir del layout
|
|
||||||
def template_from_layout
|
|
||||||
@site.templates.find do |t|
|
|
||||||
t.get_front_matter('slug') == get_front_matter('layout')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: convertir a hash para que sea más fácil buscar uno
|
|
||||||
def template_fields
|
|
||||||
return [] unless template
|
|
||||||
|
|
||||||
@template_fields ||= template.front_matter.map do |key, contents|
|
|
||||||
next if REJECT_FROM_TEMPLATE.include? key
|
|
||||||
next if key.start_with? 'has_'
|
|
||||||
|
|
||||||
# XXX: Esto está acá hasta que convirtamos todo en plantillas
|
|
||||||
if key == 'title' && content.is_a?(String)
|
|
||||||
contents = {
|
|
||||||
'value' => 'text',
|
|
||||||
'label' => I18n.t('posts.title'),
|
|
||||||
'required' => true
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
Post::TemplateField.new(self, key, contents)
|
|
||||||
end.compact
|
|
||||||
end
|
|
||||||
|
|
||||||
# devuelve las plantillas como strong params, primero los valores
|
# devuelve las plantillas como strong params, primero los valores
|
||||||
# simples, luego los arrays y al final los hashes
|
# simples, luego los arrays y al final los hashes
|
||||||
def template_params
|
def template_params
|
||||||
@template_params ||= (DEFAULT_PARAMS + template_fields.map(&:to_param)).sort_by do |s|
|
@template_params ||= template_fields.map(&:to_param).sort_by do |s|
|
||||||
s.is_a?(Symbol) ? 0 : 1
|
s.is_a?(Symbol) ? 0 : 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def template
|
|
||||||
@template ||= template_from_layout
|
|
||||||
end
|
|
||||||
|
|
||||||
# Eliminar el artículo del repositorio y de la lista de artículos del
|
|
||||||
# sitio
|
|
||||||
def destroy
|
|
||||||
FileUtils.rm_f path
|
|
||||||
|
|
||||||
site.posts_for(collection).delete_if do |post|
|
|
||||||
post.path == path
|
|
||||||
end
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Completa el front_matter a partir de las variables de otro post que
|
# Completa el front_matter a partir de las variables de otro post que
|
||||||
|
@ -406,30 +215,18 @@ class Post
|
||||||
ft
|
ft
|
||||||
end
|
end
|
||||||
|
|
||||||
# Genera un post nuevo y lo agrega a la colección del sitio.
|
|
||||||
def new_post
|
|
||||||
opts = { site: @site.jekyll, collection: @site.jekyll.collections[@collection] }
|
|
||||||
@post = Jekyll::Document.new(path, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Solo agregar el post al sitio una vez que lo guardamos
|
# Solo agregar el post al sitio una vez que lo guardamos
|
||||||
#
|
#
|
||||||
# TODO no sería la forma correcta de hacerlo en Rails
|
# TODO no sería la forma correcta de hacerlo en Rails
|
||||||
def add_post_to_site!
|
# def add_post_to_site!
|
||||||
@site.jekyll.collections[@collection].docs << @post
|
# @site.jekyll.collections[@collection].docs << @post
|
||||||
@site.jekyll.collections[@collection].docs.sort!
|
# @site.jekyll.collections[@collection].docs.sort!
|
||||||
|
|
||||||
unless @site.collections[@collection].include? self
|
# unless @site.collections[@collection].include? self
|
||||||
@site.collections[@collection] << self
|
# @site.collections[@collection] << self
|
||||||
@site.collections[@collection].sort!
|
# @site.collections[@collection].sort!
|
||||||
end
|
# end
|
||||||
end
|
# 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
|
|
||||||
end
|
|
||||||
|
|
||||||
# Cambiar el nombre del archivo si cambió el título o la fecha.
|
# Cambiar el nombre del archivo si cambió el título o la fecha.
|
||||||
# Como Jekyll no tiene métodos para modificar un Document, lo
|
# Como Jekyll no tiene métodos para modificar un Document, lo
|
||||||
|
@ -441,7 +238,7 @@ class Post
|
||||||
|
|
||||||
Rails.logger.info I18n.t('posts.logger.rm', path: path)
|
Rails.logger.info I18n.t('posts.logger.rm', path: path)
|
||||||
FileUtils.rm @post.path
|
FileUtils.rm @post.path
|
||||||
replace_post!
|
# replace_post!
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reemplaza el post en el sitio por uno nuevo
|
# Reemplaza el post en el sitio por uno nuevo
|
||||||
|
@ -478,25 +275,14 @@ class Post
|
||||||
end
|
end
|
||||||
|
|
||||||
def cleanup!
|
def cleanup!
|
||||||
things_to_arrays!
|
|
||||||
default_date_is_today!
|
default_date_is_today!
|
||||||
date_to_time!
|
|
||||||
clean_content!
|
clean_content!
|
||||||
slugify_title!
|
slugify_title!
|
||||||
remove_empty_front_matter!
|
|
||||||
update_lang_front_matter!
|
|
||||||
update_translations!
|
update_translations!
|
||||||
put_in_order!
|
put_in_order!
|
||||||
create_glossary!
|
create_glossary!
|
||||||
end
|
end
|
||||||
|
|
||||||
# Setea el propio idioma en el front_matter de slugs
|
|
||||||
def update_lang_front_matter!
|
|
||||||
return unless translated?
|
|
||||||
|
|
||||||
@front_matter['lang'][@lang] = slug
|
|
||||||
end
|
|
||||||
|
|
||||||
# Busca las traducciones y actualiza el frontmatter si es necesario
|
# Busca las traducciones y actualiza el frontmatter si es necesario
|
||||||
def update_translations!
|
def update_translations!
|
||||||
return unless translated?
|
return unless translated?
|
||||||
|
@ -508,15 +294,9 @@ class Post
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_empty_front_matter!
|
|
||||||
@front_matter.delete_if do |_k, v|
|
|
||||||
v.blank?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Aplica limpiezas básicas del contenido
|
# Aplica limpiezas básicas del contenido
|
||||||
def clean_content!
|
def clean_content!
|
||||||
@content.try(:delete!, "\r")
|
content.try(:delete!, "\r")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Guarda los cambios en el archivo destino
|
# Guarda los cambios en el archivo destino
|
||||||
|
@ -543,20 +323,6 @@ class Post
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
# Genera el post con front matter, menos los campos que no necesitamos
|
|
||||||
# que estén en el front matter.
|
|
||||||
#
|
|
||||||
# El contenido se toma de `content` en lugar de `@content`, para poder
|
|
||||||
# obtener el contenido por defecto si es que no lo enviamos
|
|
||||||
# modificaciones, como en `update_translations!`
|
|
||||||
def full_content
|
|
||||||
yaml = @front_matter.reject do |k, _|
|
|
||||||
REJECT_FROM_FRONT_MATTER.include? k
|
|
||||||
end
|
|
||||||
|
|
||||||
"#{yaml.to_yaml}---\n\n#{content}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_error(hash)
|
def add_error(hash)
|
||||||
hash.each_pair do |k, i|
|
hash.each_pair do |k, i|
|
||||||
@errors[k] = if @errors.key?(k)
|
@errors[k] = if @errors.key?(k)
|
||||||
|
@ -570,28 +336,11 @@ class Post
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_date_is_today!
|
def default_date_is_today!
|
||||||
set_front_matter('date', Time.now) unless date
|
date ||= Time.now
|
||||||
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
|
|
||||||
|
|
||||||
# XXX es necesario ahora que tenemos select2?
|
|
||||||
def things_to_arrays!
|
|
||||||
%i[tags categories].each do |c|
|
|
||||||
thing = @front_matter.dig(c.to_s)
|
|
||||||
next if thing.blank?
|
|
||||||
next if thing.is_a? Array
|
|
||||||
|
|
||||||
@front_matter[c.to_s] = thing.split(',').map(&:strip)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def slugify_title!
|
def slugify_title!
|
||||||
@front_matter['slug'] = Jekyll::Utils.slugify(title) if slug.blank?
|
self.slug = Jekyll::Utils.slugify(title) if slug.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Agregar al final de la cola si no especificamos un orden
|
# Agregar al final de la cola si no especificamos un orden
|
||||||
|
@ -627,4 +376,12 @@ class Post
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Obtiene el nombre del atributo sin
|
||||||
|
def attribute_name(attr)
|
||||||
|
attr.to_s.split('=').first.to_sym
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/ClassLength
|
||||||
|
|
25
app/models/post_relation.rb
Normal file
25
app/models/post_relation.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# La relación de un sitio con sus artículos, esto nos permite generar
|
||||||
|
# artículos como si estuviésemos usando ActiveRecord.
|
||||||
|
class PostRelation < Array
|
||||||
|
# No necesitamos cambiar el sitio
|
||||||
|
attr_reader :site
|
||||||
|
|
||||||
|
def initialize(site:)
|
||||||
|
@site = site
|
||||||
|
# Proseguimos la inicialización sin valores por defecto
|
||||||
|
super()
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera un artículo nuevo con los parámetros que le pasemos y lo suma
|
||||||
|
# al array
|
||||||
|
def build(**args)
|
||||||
|
self << Post.new(site: site, **args)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Intenta guardar todos y devuelve true si pudo
|
||||||
|
def save_all
|
||||||
|
map(&:save).all?
|
||||||
|
end
|
||||||
|
end
|
|
@ -31,17 +31,18 @@ class Site < ApplicationRecord
|
||||||
before_destroy :remove_directories!
|
before_destroy :remove_directories!
|
||||||
# Carga el sitio Jekyll una vez que se inicializa el modelo o después
|
# Carga el sitio Jekyll una vez que se inicializa el modelo o después
|
||||||
# de crearlo
|
# de crearlo
|
||||||
after_initialize :load_jekyll!
|
after_initialize :load_jekyll
|
||||||
after_create :load_jekyll!
|
after_create :load_jekyll
|
||||||
# Cambiar el nombre del directorio
|
# Cambiar el nombre del directorio
|
||||||
before_update :update_name!
|
before_update :update_name!
|
||||||
# Guardar la configuración si hubo cambios
|
# Guardar la configuración si hubo cambios
|
||||||
after_save :sync_attributes_with_config!
|
after_save :sync_attributes_with_config!
|
||||||
|
|
||||||
attr_accessor :jekyll, :collections
|
|
||||||
|
|
||||||
accepts_nested_attributes_for :deploys, allow_destroy: true
|
accepts_nested_attributes_for :deploys, allow_destroy: true
|
||||||
|
|
||||||
|
# El sitio en Jekyll
|
||||||
|
attr_accessor :jekyll
|
||||||
|
|
||||||
# No permitir HTML en estos atributos
|
# No permitir HTML en estos atributos
|
||||||
def title=(title)
|
def title=(title)
|
||||||
super(title.strip_tags)
|
super(title.strip_tags)
|
||||||
|
@ -56,11 +57,6 @@ class Site < ApplicationRecord
|
||||||
@repository ||= Site::Repository.new path
|
@repository ||= Site::Repository.new path
|
||||||
end
|
end
|
||||||
|
|
||||||
# Trae los cambios del skel y verifica que haya cambios
|
|
||||||
def needs_pull?
|
|
||||||
!repository.commits.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: Mover esta consulta a la base de datos para no traer un montón
|
# TODO: Mover esta consulta a la base de datos para no traer un montón
|
||||||
# de cosas a la memoria
|
# de cosas a la memoria
|
||||||
def invitade?(usuarie)
|
def invitade?(usuarie)
|
||||||
|
@ -71,179 +67,138 @@ class Site < ApplicationRecord
|
||||||
usuaries.pluck(:id).include? usuarie.id
|
usuaries.pluck(:id).include? usuarie.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Este sitio acepta invitades?
|
||||||
|
def invitades?
|
||||||
|
config.fetch('invitades', false)
|
||||||
|
end
|
||||||
|
|
||||||
# Traer la ruta del sitio
|
# Traer la ruta del sitio
|
||||||
#
|
|
||||||
# Equivale a _sites + nombre
|
|
||||||
def path
|
def path
|
||||||
File.join(Site.site_path, name)
|
File.join(Site.site_path, name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def old_path
|
# La ruta anterior
|
||||||
|
def path_was
|
||||||
File.join(Site.site_path, name_was)
|
File.join(Site.site_path, name_was)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Este sitio acepta invitadxs?
|
|
||||||
def invitadxs?
|
|
||||||
config.fetch('invitadxs', false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def cover
|
def cover
|
||||||
"/covers/#{name}.png"
|
"/covers/#{name}.png"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Determina si el sitio está en varios idiomas
|
|
||||||
def i18n?
|
|
||||||
!translations.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Define si el sitio tiene un glosario
|
# Define si el sitio tiene un glosario
|
||||||
def glossary?
|
def glossary?
|
||||||
config.fetch('glossary', false)
|
config.fetch('glossary', false)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtiene la lista de traducciones actuales
|
# Obtiene la lista de traducciones actuales
|
||||||
|
#
|
||||||
|
# Siempre tiene que tener algo porque las traducciones están
|
||||||
|
# incorporadas a los sitios de Sutty, aunque les usuaries no traduzcan
|
||||||
|
# sus sitios.
|
||||||
def translations
|
def translations
|
||||||
config.fetch('i18n', [])
|
config.fetch('translations', I18n.available_locales.map(&:to_s))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Devuelve el idioma por defecto del sitio
|
# Devuelve el idioma por defecto del sitio, el primero de la lista.
|
||||||
#
|
def default_language
|
||||||
# TODO: volver elegante
|
|
||||||
def default_lang
|
|
||||||
# Si está traducido, intentamos saber si podemos trabajar en el
|
|
||||||
# idioma actual de la plataforma.
|
|
||||||
if i18n?
|
|
||||||
i18n = I18n.locale.to_s
|
|
||||||
if translations.include? i18n
|
|
||||||
# Podemos trabajar en el idioma actual
|
|
||||||
i18n
|
|
||||||
else
|
|
||||||
# Sino, trabajamos con el primer idioma
|
|
||||||
translations.first
|
translations.first
|
||||||
end
|
end
|
||||||
else
|
alias default_lang default_language
|
||||||
# Si el sitio no está traducido, estamos trabajando con posts en
|
|
||||||
# cualquier idioma
|
|
||||||
#
|
|
||||||
# XXX: no será un dirty hack?
|
|
||||||
'posts'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def layouts
|
|
||||||
@layouts ||= @jekyll.layouts.keys.sort
|
|
||||||
end
|
|
||||||
|
|
||||||
def name_with_i18n(lang)
|
|
||||||
[name, lang].join('/')
|
|
||||||
end
|
|
||||||
|
|
||||||
|
# Lee el sitio y todos los artículos
|
||||||
def read
|
def read
|
||||||
@jekyll.read
|
@jekyll.read
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fuerza relectura del sitio, eliminando el sitio actual en favor de
|
# Trae los datos del directorio _data dentro del sitio
|
||||||
# un sitio nuevo, leído desde cero
|
#
|
||||||
def read!
|
# XXX: Leer directamente sin pasar por Jekyll
|
||||||
@jekyll = Site.load_jekyll(@jekyll.path)
|
|
||||||
|
|
||||||
@jekyll.read
|
|
||||||
end
|
|
||||||
|
|
||||||
def data
|
def data
|
||||||
if @jekyll.data.empty?
|
read if @jekyll.data.empty?
|
||||||
read
|
|
||||||
Rails.logger.info 'Leyendo data'
|
|
||||||
end
|
|
||||||
|
|
||||||
@jekyll.data
|
@jekyll.data
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Traer las colecciones. Todos los artículos van a estar dentro de
|
||||||
|
# colecciones.
|
||||||
|
def collections
|
||||||
|
read if @jekyll.collections.empty?
|
||||||
|
|
||||||
|
@jekyll.collections
|
||||||
|
end
|
||||||
|
|
||||||
|
# Traer la configuración de forma modificable
|
||||||
def config
|
def config
|
||||||
@config ||= Site::Config.new(self)
|
@config ||= Site::Config.new(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Cambiar a Site::Config apenas empecemos a testear esto
|
# Los posts en el idioma actual o en uno en particular
|
||||||
def collections_names
|
#
|
||||||
@jekyll.config['collections'].keys
|
# @param lang: [String|Symbol] traer los artículos de este idioma
|
||||||
|
def posts(lang: nil)
|
||||||
|
@posts ||= {}
|
||||||
|
lang ||= I18n.locale
|
||||||
|
|
||||||
|
return @posts[lang] if @posts[lang].present?
|
||||||
|
|
||||||
|
@posts[lang] = PostRelation.new site: self
|
||||||
|
|
||||||
|
# No fallar si no existe colección para este idioma
|
||||||
|
# XXX: queremos fallar silenciosamente?
|
||||||
|
(collections[lang.to_s].try(:docs) || []).each do |doc|
|
||||||
|
layout = layouts[doc.data['layout'].to_sym]
|
||||||
|
|
||||||
|
@posts[lang].build(document: doc, layout: layout, lang: lang)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Los posts de este sitio, si el sitio está traducido, trae los posts
|
@posts[lang]
|
||||||
# del idioma actual, porque _posts va a estar vacío
|
|
||||||
def posts
|
|
||||||
@posts ||= posts_for('posts')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtiene todas las plantillas de artículos
|
# Obtiene todas las plantillas de artículos
|
||||||
def templates
|
|
||||||
@templates ||= posts_for('templates') || []
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtiene todos los posts de una colección determinada
|
|
||||||
#
|
#
|
||||||
# Devuelve nil si la colección no existe
|
# @return { post: Layout }
|
||||||
def posts_for(collection)
|
def layouts
|
||||||
return unless collections_names.include? collection
|
@layouts ||= data.fetch('layouts', {}).map do |name, metadata|
|
||||||
|
{ name.to_sym => Layout.new(site: self,
|
||||||
# Si pedimos 'posts' pero estamos en un sitio traducido, traemos el
|
name: name.to_sym,
|
||||||
# idioma actual
|
metadata: metadata) }
|
||||||
collection = default_lang if collection == 'posts' && i18n?
|
end.inject(:merge)
|
||||||
|
|
||||||
@collections ||= {}
|
|
||||||
c = @collections[collection]
|
|
||||||
return c if c
|
|
||||||
|
|
||||||
Rails.logger.info "Procesando #{collection}"
|
|
||||||
|
|
||||||
col = @jekyll.collections[collection].docs
|
|
||||||
if col.empty?
|
|
||||||
@jekyll.read
|
|
||||||
# Queremos saber cuantas veces releemos los articulos
|
|
||||||
Rails.logger.info 'Leyendo articulos'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Los convertimos a una clase intermedia capaz de acceder a sus
|
|
||||||
# datos y modificarlos
|
|
||||||
@collections[collection] = col.map do |post|
|
|
||||||
Post.new(site: self, post: post, lang: collection)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def categories(lang: nil)
|
|
||||||
everything_of :categories, lang: lang
|
|
||||||
end
|
|
||||||
|
|
||||||
def tags(lang: nil)
|
|
||||||
everything_of :tags, lang: lang
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Trae todos los valores disponibles para un campo
|
||||||
|
#
|
||||||
|
# TODO: Traer recursivamente, si el campo contiene Hash
|
||||||
|
#
|
||||||
|
# @return Array
|
||||||
def everything_of(attr, lang: nil)
|
def everything_of(attr, lang: nil)
|
||||||
collection = lang || 'posts'
|
posts(lang: lang).map do |p|
|
||||||
|
# XXX: Tener cuidado con los métodos que no existan
|
||||||
return [] unless collections_names.include? collection
|
p.send(attr).try :value
|
||||||
|
|
||||||
posts_for(collection).map do |p|
|
|
||||||
p.get_front_matter attr
|
|
||||||
end.flatten.uniq.compact
|
end.flatten.uniq.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Poner en la cola de compilación
|
||||||
def enqueue!
|
def enqueue!
|
||||||
!enqueued? && update_attribute(:status, 'enqueued')
|
!enqueued? && update_attribute(:status, 'enqueued')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Está en la cola de compilación?
|
||||||
def enqueued?
|
def enqueued?
|
||||||
status == 'enqueued'
|
status == 'enqueued'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Verifica si los posts están ordenados
|
# Verifica si los posts están ordenados
|
||||||
def ordered?(collection = 'posts')
|
def ordered?(lang: nil)
|
||||||
posts_for(collection).map(&:order).all?
|
posts(lang: lang).map(&:order).all?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reordena la colección usando la posición informada
|
# Reordena la colección usando la posición informada
|
||||||
#
|
#
|
||||||
# new_order es un hash cuya key es la posición actual del post y el
|
# new_order es un hash cuya key es la posición actual del post y el
|
||||||
# valor la posición nueva
|
# valor la posición nueva
|
||||||
|
#
|
||||||
|
# TODO: Refactorizar y testear
|
||||||
def reorder_collection(collection, new_order)
|
def reorder_collection(collection, new_order)
|
||||||
# Tenemos que pasar el mismo orden
|
# Tenemos que pasar el mismo orden
|
||||||
return if new_order.values.map(&:to_i).sort != new_order.keys.map(&:to_i).sort
|
return if new_order.values.map(&:to_i).sort != new_order.keys.map(&:to_i).sort
|
||||||
|
@ -275,50 +230,65 @@ class Site < ApplicationRecord
|
||||||
alias reorder_posts! reorder_collection!
|
alias reorder_posts! reorder_collection!
|
||||||
|
|
||||||
# Obtener una ruta disponible para Sutty
|
# Obtener una ruta disponible para Sutty
|
||||||
|
#
|
||||||
|
# TODO: Refactorizar y testear
|
||||||
def get_url_for_sutty(path)
|
def get_url_for_sutty(path)
|
||||||
# Remover los puntos para que no nos envíen a ../../
|
# Remover los puntos para que no nos envíen a ../../
|
||||||
File.join('/', 'sites', id, path.gsub('..', ''))
|
File.join('/', 'sites', id, path.gsub('..', ''))
|
||||||
end
|
end
|
||||||
|
|
||||||
# El directorio donde se almacenan los sitios
|
# Cargar el sitio Jekyll
|
||||||
def self.site_path
|
#
|
||||||
File.join(Rails.root, '_sites')
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: En lugar de leer todo junto de una vez, extraer la carga de
|
# TODO: En lugar de leer todo junto de una vez, extraer la carga de
|
||||||
# documentos de Jekyll hacia Sutty para que podamos leer los datos que
|
# documentos de Jekyll hacia Sutty para que podamos leer los datos que
|
||||||
# necesitamos.
|
# necesitamos.
|
||||||
def self.load_jekyll(path)
|
def load_jekyll
|
||||||
|
return unless name.present? && File.directory?(path)
|
||||||
|
|
||||||
|
Dir.chdir(path) do
|
||||||
|
@jekyll = Jekyll::Site.new(jekyll_config)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def jekyll_config
|
||||||
# Pasamos destination porque configuration() toma el directorio
|
# Pasamos destination porque configuration() toma el directorio
|
||||||
# actual y se mezclan :/
|
# actual
|
||||||
#
|
#
|
||||||
# Especificamos `safe` para no cargar los _plugins, que interfieren
|
# Especificamos `safe` para no cargar los _plugins, que interfieren
|
||||||
# entre sitios incluso
|
# entre sitios incluso.
|
||||||
config = ::Jekyll.configuration('source' => path,
|
#
|
||||||
|
# excerpt_separator está vacío para no incorporar el Excerpt en los
|
||||||
|
# metadatos de Document
|
||||||
|
configuration =
|
||||||
|
::Jekyll.configuration('source' => path,
|
||||||
'destination' => File.join(path, '_site'),
|
'destination' => File.join(path, '_site'),
|
||||||
'safe' => true,
|
'safe' => true, 'watch' => false,
|
||||||
'watch' => false,
|
'quiet' => true, 'excerpt_separator' => '')
|
||||||
'quiet' => true)
|
|
||||||
|
|
||||||
# No necesitamos cargar plugins en este momento
|
# No necesitamos cargar plugins en este momento
|
||||||
%w[plugins gems theme].each do |unneeded|
|
%w[plugins gems theme].each do |unneeded|
|
||||||
config[unneeded] = [] if config.key? unneeded
|
configuration[unneeded] = [] if configuration.key? unneeded
|
||||||
end
|
end
|
||||||
|
|
||||||
# Si estamos usando nuestro propio plugin de i18n, los posts están
|
# Si estamos usando nuestro propio plugin de i18n, los posts están
|
||||||
# en "colecciones"
|
# en "colecciones"
|
||||||
i18n = config.dig('i18n')
|
translations.each do |i|
|
||||||
i18n&.each do |i|
|
configuration['collections'][i] = {}
|
||||||
config['collections'][i] = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Jekyll::Site.new(config)
|
configuration
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Devuelve el dominio actual
|
||||||
def self.domain
|
def self.domain
|
||||||
ENV.fetch('SUTTY', 'sutty.nl')
|
ENV.fetch('SUTTY', 'sutty.nl')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# El directorio donde se almacenan los sitios
|
||||||
|
def self.site_path
|
||||||
|
File.join(Rails.root, '_sites')
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Clona el esqueleto de Sutty para crear el sitio nuevo, no pasa nada
|
# Clona el esqueleto de Sutty para crear el sitio nuevo, no pasa nada
|
||||||
|
@ -329,15 +299,6 @@ class Site < ApplicationRecord
|
||||||
Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path
|
Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path
|
||||||
end
|
end
|
||||||
|
|
||||||
# Carga el sitio Jekyll
|
|
||||||
def load_jekyll!
|
|
||||||
return unless name.present? && File.directory?(path)
|
|
||||||
|
|
||||||
Dir.chdir(path) do
|
|
||||||
@jekyll ||= Site.load_jekyll(Dir.pwd)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Elimina el directorio del sitio
|
# Elimina el directorio del sitio
|
||||||
def remove_directories!
|
def remove_directories!
|
||||||
FileUtils.rm_rf path
|
FileUtils.rm_rf path
|
||||||
|
@ -346,11 +307,14 @@ class Site < ApplicationRecord
|
||||||
def update_name!
|
def update_name!
|
||||||
return unless name_changed?
|
return unless name_changed?
|
||||||
|
|
||||||
FileUtils.mv old_path, path
|
FileUtils.mv path_was, path
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sincroniza algunos atributos del sitio con su configuración y
|
# Sincroniza algunos atributos del sitio con su configuración y
|
||||||
# guarda los cambios
|
# guarda los cambios
|
||||||
|
#
|
||||||
|
# TODO: Guardar la configuración también, quizás aprovechando algún
|
||||||
|
# método de ActiveRecord para que lance un salvado recursivo.
|
||||||
def sync_attributes_with_config!
|
def sync_attributes_with_config!
|
||||||
config.theme = design.gem unless design_id_changed?
|
config.theme = design.gem unless design_id_changed?
|
||||||
config.description = description unless description_changed?
|
config.description = description unless description_changed?
|
||||||
|
|
|
@ -79,6 +79,11 @@ class Site
|
||||||
walker.each.to_a
|
walker.each.to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Hay commits sin aplicar?
|
||||||
|
def needs_pull?
|
||||||
|
commits.empty?
|
||||||
|
end
|
||||||
|
|
||||||
# Guarda los cambios en git, de a un archivo por vez
|
# Guarda los cambios en git, de a un archivo por vez
|
||||||
# rubocop:disable Metrics/AbcSize
|
# rubocop:disable Metrics/AbcSize
|
||||||
def commit(file:, usuarie:, message:)
|
def commit(file:, usuarie:, message:)
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
# Política de aceptación de colaboradorxs
|
# Política de aceptación de colaboradorxs
|
||||||
CollaborationPolicy = Struct.new(:usuarie, :collaboration) do
|
CollaborationPolicy = Struct.new(:usuarie, :collaboration) do
|
||||||
def collaborate?
|
def collaborate?
|
||||||
collaboration.site.invitadxs?
|
collaboration.site.invitades?
|
||||||
end
|
end
|
||||||
|
|
||||||
def accept_collaboration?
|
def accept_collaboration?
|
||||||
collaboration.site.invitadxs?
|
collaboration.site.invitades?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
= fa_icon 'building'
|
= fa_icon 'building'
|
||||||
= t('sites.enqueue')
|
= t('sites.enqueue')
|
||||||
|
|
||||||
- if policy(site).pull? && site.needs_pull?
|
- if policy(site).pull? && site.repository.needs_pull?
|
||||||
= render 'layouts/btn_with_tooltip',
|
= render 'layouts/btn_with_tooltip',
|
||||||
tooltip: t('help.sites.pull'),
|
tooltip: t('help.sites.pull'),
|
||||||
text: t('.pull'),
|
text: t('.pull'),
|
||||||
|
|
|
@ -1,4 +1,17 @@
|
||||||
en:
|
en:
|
||||||
|
metadata:
|
||||||
|
array:
|
||||||
|
cant_be_empty: 'This field cannot be empty'
|
||||||
|
string:
|
||||||
|
cant_be_empty: 'This field cannot be empty'
|
||||||
|
image:
|
||||||
|
cant_be_empty: 'This field cannot be empty'
|
||||||
|
exceptions:
|
||||||
|
post:
|
||||||
|
site_missing: 'Needs an instance of Site'
|
||||||
|
layout_missing: 'Needs an instance of Layout'
|
||||||
|
document_missing: 'Needs an instance of Jekyll::Document'
|
||||||
|
no_method: '%{method} not allowed'
|
||||||
es: Castillian Spanish
|
es: Castillian Spanish
|
||||||
en: English
|
en: English
|
||||||
seconds: '%{seconds} seconds'
|
seconds: '%{seconds} seconds'
|
||||||
|
|
|
@ -1,4 +1,17 @@
|
||||||
es:
|
es:
|
||||||
|
metadata:
|
||||||
|
array:
|
||||||
|
cant_be_empty: 'El campo no puede estar vacío'
|
||||||
|
string:
|
||||||
|
cant_be_empty: 'El campo no puede estar vacío'
|
||||||
|
image:
|
||||||
|
cant_be_empty: 'El campo no puede estar vacío'
|
||||||
|
exceptions:
|
||||||
|
post:
|
||||||
|
site_missing: 'Necesita una instancia de Site'
|
||||||
|
layout_missing: 'Necesita una instancia de Layout'
|
||||||
|
document_missing: 'Necesita una instancia de Jekyll::Document'
|
||||||
|
no_method: '%{method} no está permitido'
|
||||||
es: Castellano
|
es: Castellano
|
||||||
en: Inglés
|
en: Inglés
|
||||||
seconds: '%{seconds} segundos'
|
seconds: '%{seconds} segundos'
|
||||||
|
|
89
test/models/post_test.rb
Normal file
89
test/models/post_test.rb
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PostTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
# Trabajamos con el sitio de sutty porque tiene artículos
|
||||||
|
#
|
||||||
|
# TODO: Cambiar a skel cuando publiquemos los códigos y privacidad
|
||||||
|
@site = create :site, name: 'sutty.nl'
|
||||||
|
@site.read
|
||||||
|
@post = @site.posts.sample
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
# @site.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'se puede acceder a los valores' do
|
||||||
|
assert @site.posts.size.positive?
|
||||||
|
|
||||||
|
assert @post.categories.values.size.positive?
|
||||||
|
assert @post.tags.values.size.positive?
|
||||||
|
assert @post.title.size.positive?
|
||||||
|
assert @post.content.size.positive?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'no se puede setear cualquier atributo' do
|
||||||
|
assert_raise NoMethodError do
|
||||||
|
@post.verdura = 'verdura'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'se pueden eliminar' do
|
||||||
|
# TODO: cuando esté con git, solo aplicar git reset
|
||||||
|
tmp = File.join(Rails.root, 'tmp', 'eliminar.md')
|
||||||
|
FileUtils.cp @post.path, tmp
|
||||||
|
|
||||||
|
assert @post.destroy
|
||||||
|
assert_not File.exist?(@post.path)
|
||||||
|
assert_not @site.posts.include?(@post)
|
||||||
|
|
||||||
|
FileUtils.mv tmp, @post.path
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'se puede ver el contenido completo' do
|
||||||
|
tmp = Tempfile.new
|
||||||
|
|
||||||
|
begin
|
||||||
|
tmp.write(@post.full_content)
|
||||||
|
tmp.close
|
||||||
|
|
||||||
|
collection = Jekyll::Collection.new(@site.jekyll, I18n.locale.to_s)
|
||||||
|
document = Jekyll::Document.new(tmp.path, site: @site.jekyll,
|
||||||
|
collection: collection)
|
||||||
|
document.read
|
||||||
|
document.data['categories'].try(:delete_if) do |x|
|
||||||
|
x == 'tmp'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Queremos saber si todos los atributos del post terminaron en el
|
||||||
|
# archivo
|
||||||
|
@post.attributes.each do |attr|
|
||||||
|
template = @post.send(attr)
|
||||||
|
# ignorar atributos que no son datos
|
||||||
|
next unless template.is_a? MetadataTemplate
|
||||||
|
|
||||||
|
if template.empty?
|
||||||
|
assert_not document.data[attr.to_s].present?
|
||||||
|
else
|
||||||
|
assert_equal template.value, document.data[attr.to_s]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
tmp.unlink
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'se pueden validar' do
|
||||||
|
assert @post.valid?
|
||||||
|
|
||||||
|
# XXX: si usamos nil va a traer el valor original del documento, no
|
||||||
|
# queremos tener esa información sincronizada...
|
||||||
|
@post.title.value = ''
|
||||||
|
|
||||||
|
assert_not @post.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'se pueden guardar los cambios' do
|
||||||
|
end
|
||||||
|
end
|
|
@ -61,6 +61,7 @@ class SiteTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
test 'se puede leer un sitio' do
|
test 'se puede leer un sitio' do
|
||||||
site = create :site, name: 'sutty.nl'
|
site = create :site, name: 'sutty.nl'
|
||||||
|
site.read
|
||||||
|
|
||||||
assert site.valid?
|
assert site.valid?
|
||||||
assert !site.posts.empty?
|
assert !site.posts.empty?
|
||||||
|
@ -85,4 +86,13 @@ class SiteTest < ActiveSupport::TestCase
|
||||||
assert_equal 'hola', site.description
|
assert_equal 'hola', site.description
|
||||||
assert_equal 'hola', site.title
|
assert_equal 'hola', site.title
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test 'el sitio tiene artículos en distintos idiomas' do
|
||||||
|
site = create :site, name: 'sutty.nl'
|
||||||
|
site.read
|
||||||
|
|
||||||
|
I18n.available_locales.each do |locale|
|
||||||
|
assert site.posts(lang: locale).size.positive?
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue