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
|
||||
|
||||
# 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,
|
||||
:value, :help, :required,
|
||||
:value, :help, :required, :errors,
|
||||
keyword_init: true) do
|
||||
# El valor por defecto
|
||||
def default_value
|
||||
|
@ -12,11 +14,36 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
|||
# Valores posibles, busca todos los valores actuales en otros
|
||||
# artículos del mismo sitio
|
||||
def values
|
||||
site.everything_of(name.to_s)
|
||||
site.everything_of(name)
|
||||
end
|
||||
|
||||
# Valor actual o por defecto
|
||||
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
|
||||
|
|
|
@ -3,125 +3,120 @@
|
|||
require 'jekyll/utils'
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
class Post
|
||||
attr_accessor :content, :front_matter
|
||||
attr_reader :post, :site, :errors, :old_post, :lang, :template,
|
||||
:template_fields, :collection
|
||||
# rubocop:disable Metrics/ClassLength
|
||||
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
|
||||
|
||||
REJECT_FROM_DATA = %w[excerpt].freeze
|
||||
# datos que no tienen que terminar en el front matter
|
||||
REJECT_FROM_FRONT_MATTER = %w[date slug ext].freeze
|
||||
# datos que no traemos del template
|
||||
REJECT_FROM_TEMPLATE = %w[draft categories layout ext tags date slug post pre].freeze
|
||||
DEFAULT_PARAMS = [:title, :date, :content, :slug, :cover,
|
||||
:layout, :permalink, :dir,
|
||||
{ lang: {} }, { tags: [] }, { categories: [] }].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)
|
||||
|
||||
def inspect
|
||||
"#<Post @id=#{id} @site=#{site.name}>"
|
||||
# Genera un método con todos los atributos disponibles
|
||||
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
|
||||
|
||||
# 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
|
||||
def initialize(site:, post: nil, front_matter: {}, lang: nil, template: nil)
|
||||
unless site.is_a?(Site)
|
||||
raise ArgumentError,
|
||||
I18n.t('errors.argument_error', argument: :site, class: Site)
|
||||
# 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...
|
||||
#
|
||||
# rubocop:disable Style/MethodMissingSuper
|
||||
def method_missing(mid, *args)
|
||||
unless attribute? mid
|
||||
raise NoMethodError, I18n.t('exceptions.post.no_method',
|
||||
method: mid)
|
||||
end
|
||||
|
||||
unless post.nil? || post.is_a?(Jekyll::Document)
|
||||
raise ArgumentError,
|
||||
I18n.t('errors.argument_error', argument: :post,
|
||||
class: Jekyll::Document)
|
||||
end
|
||||
super(mid, *args)
|
||||
end
|
||||
# rubocop:enable Style/MethodMissingSuper
|
||||
|
||||
@site = site
|
||||
@post = post
|
||||
@template = template
|
||||
# los errores tienen que ser un hash para que
|
||||
# ActiveModel pueda traer los errores normalmente
|
||||
@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
|
||||
'posts'
|
||||
end
|
||||
|
||||
# sincronizar los datos del document
|
||||
if new?
|
||||
@front_matter = front_matter_from_template
|
||||
update_attributes front_matter
|
||||
# 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
|
||||
load_front_matter!
|
||||
merge_with_front_matter! front_matter.stringify_keys
|
||||
(DEFAULT_ATTRIBUTES + ATTRIBUTES).include? attribute_name(mid)
|
||||
end
|
||||
end
|
||||
|
||||
# Limpiar los errores
|
||||
def reset_errors!
|
||||
@errors = {}
|
||||
# Genera el post con metadatos en YAML
|
||||
def full_content
|
||||
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
|
||||
|
||||
# El post es nuevo si no hay un documento asociado
|
||||
def new?
|
||||
@post.nil? || !File.exist?(@post.try(:path))
|
||||
end
|
||||
|
||||
def draft?
|
||||
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
|
||||
# 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
|
||||
|
||||
!File.exist?(path) && !site.posts(lang: lang).include?(self)
|
||||
end
|
||||
alias destroy! destroy
|
||||
|
||||
# Guarda los cambios.
|
||||
#
|
||||
|
@ -133,60 +128,49 @@ class Post
|
|||
|
||||
return false unless valid?
|
||||
|
||||
new_post if new?
|
||||
|
||||
return unless write
|
||||
return unless detect_file_rename!
|
||||
|
||||
# Vuelve a leer el post para tomar los cambios
|
||||
@post.read
|
||||
add_post_to_site!
|
||||
document.read
|
||||
# add_post_to_site!
|
||||
true
|
||||
end
|
||||
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,
|
||||
# generamos una ruta nueva para tener siempre la ruta actualizada.
|
||||
def path
|
||||
if basename_changed?
|
||||
File.join(@site.path, "_#{@collection}", basename_from_front_matter)
|
||||
else
|
||||
@post.try(:path)
|
||||
end
|
||||
document.path
|
||||
end
|
||||
|
||||
# TODO: los slugs se pueden repetir, el identificador real sería
|
||||
# fecha+slug, pero se ve feo en las urls?
|
||||
def id
|
||||
get_front_matter 'slug'
|
||||
# Detecta si el artículo es válido para guardar
|
||||
def valid?
|
||||
validate
|
||||
errors.blank?
|
||||
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?
|
||||
@post.try(:basename) != basename_from_front_matter
|
||||
|
@ -196,108 +180,6 @@ class Post
|
|||
new? || @post.data.dig('slug') != slug
|
||||
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
|
||||
def url?(name)
|
||||
path = get_front_matter(name)
|
||||
|
@ -309,87 +191,14 @@ class Post
|
|||
end.all?
|
||||
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
|
||||
# simples, luego los arrays y al final los hashes
|
||||
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
|
||||
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
|
||||
|
||||
# Completa el front_matter a partir de las variables de otro post que
|
||||
|
@ -406,30 +215,18 @@ class Post
|
|||
ft
|
||||
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
|
||||
#
|
||||
# TODO no sería la forma correcta de hacerlo en Rails
|
||||
def add_post_to_site!
|
||||
@site.jekyll.collections[@collection].docs << @post
|
||||
@site.jekyll.collections[@collection].docs.sort!
|
||||
# def add_post_to_site!
|
||||
# @site.jekyll.collections[@collection].docs << @post
|
||||
# @site.jekyll.collections[@collection].docs.sort!
|
||||
|
||||
unless @site.collections[@collection].include? self
|
||||
@site.collections[@collection] << self
|
||||
@site.collections[@collection].sort!
|
||||
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
|
||||
# unless @site.collections[@collection].include? self
|
||||
# @site.collections[@collection] << self
|
||||
# @site.collections[@collection].sort!
|
||||
# end
|
||||
# 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
|
||||
|
@ -441,7 +238,7 @@ class Post
|
|||
|
||||
Rails.logger.info I18n.t('posts.logger.rm', path: path)
|
||||
FileUtils.rm @post.path
|
||||
replace_post!
|
||||
# replace_post!
|
||||
end
|
||||
|
||||
# Reemplaza el post en el sitio por uno nuevo
|
||||
|
@ -478,25 +275,14 @@ class Post
|
|||
end
|
||||
|
||||
def cleanup!
|
||||
things_to_arrays!
|
||||
default_date_is_today!
|
||||
date_to_time!
|
||||
clean_content!
|
||||
slugify_title!
|
||||
remove_empty_front_matter!
|
||||
update_lang_front_matter!
|
||||
update_translations!
|
||||
put_in_order!
|
||||
create_glossary!
|
||||
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
|
||||
def update_translations!
|
||||
return unless translated?
|
||||
|
@ -508,15 +294,9 @@ class Post
|
|||
end
|
||||
end
|
||||
|
||||
def remove_empty_front_matter!
|
||||
@front_matter.delete_if do |_k, v|
|
||||
v.blank?
|
||||
end
|
||||
end
|
||||
|
||||
# Aplica limpiezas básicas del contenido
|
||||
def clean_content!
|
||||
@content.try(:delete!, "\r")
|
||||
content.try(:delete!, "\r")
|
||||
end
|
||||
|
||||
# Guarda los cambios en el archivo destino
|
||||
|
@ -543,20 +323,6 @@ class Post
|
|||
false
|
||||
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)
|
||||
hash.each_pair do |k, i|
|
||||
@errors[k] = if @errors.key?(k)
|
||||
|
@ -570,28 +336,11 @@ class Post
|
|||
end
|
||||
|
||||
def default_date_is_today!
|
||||
set_front_matter('date', Time.now) unless date
|
||||
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
|
||||
date ||= Time.now
|
||||
end
|
||||
|
||||
def slugify_title!
|
||||
@front_matter['slug'] = Jekyll::Utils.slugify(title) if slug.blank?
|
||||
self.slug = Jekyll::Utils.slugify(title) if slug.blank?
|
||||
end
|
||||
|
||||
# Agregar al final de la cola si no especificamos un orden
|
||||
|
@ -627,4 +376,12 @@ class Post
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Obtiene el nombre del atributo sin
|
||||
def attribute_name(attr)
|
||||
attr.to_s.split('=').first.to_sym
|
||||
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!
|
||||
# Carga el sitio Jekyll una vez que se inicializa el modelo o después
|
||||
# de crearlo
|
||||
after_initialize :load_jekyll!
|
||||
after_create :load_jekyll!
|
||||
after_initialize :load_jekyll
|
||||
after_create :load_jekyll
|
||||
# Cambiar el nombre del directorio
|
||||
before_update :update_name!
|
||||
# Guardar la configuración si hubo cambios
|
||||
after_save :sync_attributes_with_config!
|
||||
|
||||
attr_accessor :jekyll, :collections
|
||||
|
||||
accepts_nested_attributes_for :deploys, allow_destroy: true
|
||||
|
||||
# El sitio en Jekyll
|
||||
attr_accessor :jekyll
|
||||
|
||||
# No permitir HTML en estos atributos
|
||||
def title=(title)
|
||||
super(title.strip_tags)
|
||||
|
@ -56,11 +57,6 @@ class Site < ApplicationRecord
|
|||
@repository ||= Site::Repository.new path
|
||||
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
|
||||
# de cosas a la memoria
|
||||
def invitade?(usuarie)
|
||||
|
@ -71,179 +67,138 @@ class Site < ApplicationRecord
|
|||
usuaries.pluck(:id).include? usuarie.id
|
||||
end
|
||||
|
||||
# Este sitio acepta invitades?
|
||||
def invitades?
|
||||
config.fetch('invitades', false)
|
||||
end
|
||||
|
||||
# Traer la ruta del sitio
|
||||
#
|
||||
# Equivale a _sites + nombre
|
||||
def path
|
||||
File.join(Site.site_path, name)
|
||||
end
|
||||
|
||||
def old_path
|
||||
# La ruta anterior
|
||||
def path_was
|
||||
File.join(Site.site_path, name_was)
|
||||
end
|
||||
|
||||
# Este sitio acepta invitadxs?
|
||||
def invitadxs?
|
||||
config.fetch('invitadxs', false)
|
||||
end
|
||||
|
||||
def cover
|
||||
"/covers/#{name}.png"
|
||||
end
|
||||
|
||||
# Determina si el sitio está en varios idiomas
|
||||
def i18n?
|
||||
!translations.empty?
|
||||
end
|
||||
|
||||
# Define si el sitio tiene un glosario
|
||||
def glossary?
|
||||
config.fetch('glossary', false)
|
||||
end
|
||||
|
||||
# Obtiene la lista de traducciones actuales
|
||||
def translations
|
||||
config.fetch('i18n', [])
|
||||
end
|
||||
|
||||
# Devuelve el idioma por defecto del sitio
|
||||
#
|
||||
# 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
|
||||
end
|
||||
else
|
||||
# Si el sitio no está traducido, estamos trabajando con posts en
|
||||
# cualquier idioma
|
||||
#
|
||||
# XXX: no será un dirty hack?
|
||||
'posts'
|
||||
end
|
||||
# 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
|
||||
config.fetch('translations', I18n.available_locales.map(&:to_s))
|
||||
end
|
||||
|
||||
def layouts
|
||||
@layouts ||= @jekyll.layouts.keys.sort
|
||||
end
|
||||
|
||||
def name_with_i18n(lang)
|
||||
[name, lang].join('/')
|
||||
# Devuelve el idioma por defecto del sitio, el primero de la lista.
|
||||
def default_language
|
||||
translations.first
|
||||
end
|
||||
alias default_lang default_language
|
||||
|
||||
# Lee el sitio y todos los artículos
|
||||
def read
|
||||
@jekyll.read
|
||||
end
|
||||
|
||||
# Fuerza relectura del sitio, eliminando el sitio actual en favor de
|
||||
# un sitio nuevo, leído desde cero
|
||||
def read!
|
||||
@jekyll = Site.load_jekyll(@jekyll.path)
|
||||
|
||||
@jekyll.read
|
||||
end
|
||||
|
||||
# Trae los datos del directorio _data dentro del sitio
|
||||
#
|
||||
# XXX: Leer directamente sin pasar por Jekyll
|
||||
def data
|
||||
if @jekyll.data.empty?
|
||||
read
|
||||
Rails.logger.info 'Leyendo data'
|
||||
end
|
||||
read if @jekyll.data.empty?
|
||||
|
||||
@jekyll.data
|
||||
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
|
||||
@config ||= Site::Config.new(self)
|
||||
end
|
||||
|
||||
# TODO: Cambiar a Site::Config apenas empecemos a testear esto
|
||||
def collections_names
|
||||
@jekyll.config['collections'].keys
|
||||
end
|
||||
# Los posts en el idioma actual o en uno en particular
|
||||
#
|
||||
# @param lang: [String|Symbol] traer los artículos de este idioma
|
||||
def posts(lang: nil)
|
||||
@posts ||= {}
|
||||
lang ||= I18n.locale
|
||||
|
||||
# Los posts de este sitio, si el sitio está traducido, trae los posts
|
||||
# del idioma actual, porque _posts va a estar vacío
|
||||
def posts
|
||||
@posts ||= posts_for('posts')
|
||||
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
|
||||
|
||||
@posts[lang]
|
||||
end
|
||||
|
||||
# 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
|
||||
def posts_for(collection)
|
||||
return unless collections_names.include? collection
|
||||
|
||||
# Si pedimos 'posts' pero estamos en un sitio traducido, traemos el
|
||||
# idioma actual
|
||||
collection = default_lang if collection == 'posts' && i18n?
|
||||
|
||||
@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
|
||||
# @return { post: Layout }
|
||||
def layouts
|
||||
@layouts ||= data.fetch('layouts', {}).map do |name, metadata|
|
||||
{ name.to_sym => Layout.new(site: self,
|
||||
name: name.to_sym,
|
||||
metadata: metadata) }
|
||||
end.inject(:merge)
|
||||
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)
|
||||
collection = lang || 'posts'
|
||||
|
||||
return [] unless collections_names.include? collection
|
||||
|
||||
posts_for(collection).map do |p|
|
||||
p.get_front_matter attr
|
||||
posts(lang: lang).map do |p|
|
||||
# XXX: Tener cuidado con los métodos que no existan
|
||||
p.send(attr).try :value
|
||||
end.flatten.uniq.compact
|
||||
end
|
||||
|
||||
# Poner en la cola de compilación
|
||||
def enqueue!
|
||||
!enqueued? && update_attribute(:status, 'enqueued')
|
||||
end
|
||||
|
||||
# Está en la cola de compilación?
|
||||
def enqueued?
|
||||
status == 'enqueued'
|
||||
end
|
||||
|
||||
# Verifica si los posts están ordenados
|
||||
def ordered?(collection = 'posts')
|
||||
posts_for(collection).map(&:order).all?
|
||||
def ordered?(lang: nil)
|
||||
posts(lang: lang).map(&:order).all?
|
||||
end
|
||||
|
||||
# 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
|
||||
# valor la posición nueva
|
||||
#
|
||||
# TODO: Refactorizar y testear
|
||||
def reorder_collection(collection, new_order)
|
||||
# Tenemos que pasar el mismo orden
|
||||
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!
|
||||
|
||||
# Obtener una ruta disponible para Sutty
|
||||
#
|
||||
# TODO: Refactorizar y testear
|
||||
def get_url_for_sutty(path)
|
||||
# Remover los puntos para que no nos envíen a ../../
|
||||
File.join('/', 'sites', id, path.gsub('..', ''))
|
||||
end
|
||||
|
||||
# El directorio donde se almacenan los sitios
|
||||
def self.site_path
|
||||
File.join(Rails.root, '_sites')
|
||||
end
|
||||
|
||||
# Cargar el sitio Jekyll
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
# actual y se mezclan :/
|
||||
# actual
|
||||
#
|
||||
# Especificamos `safe` para no cargar los _plugins, que interfieren
|
||||
# entre sitios incluso
|
||||
config = ::Jekyll.configuration('source' => path,
|
||||
'destination' => File.join(path, '_site'),
|
||||
'safe' => true,
|
||||
'watch' => false,
|
||||
'quiet' => true)
|
||||
# entre sitios incluso.
|
||||
#
|
||||
# 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'),
|
||||
'safe' => true, 'watch' => false,
|
||||
'quiet' => true, 'excerpt_separator' => '')
|
||||
|
||||
# No necesitamos cargar plugins en este momento
|
||||
%w[plugins gems theme].each do |unneeded|
|
||||
config[unneeded] = [] if config.key? unneeded
|
||||
configuration[unneeded] = [] if configuration.key? unneeded
|
||||
end
|
||||
|
||||
# Si estamos usando nuestro propio plugin de i18n, los posts están
|
||||
# en "colecciones"
|
||||
i18n = config.dig('i18n')
|
||||
i18n&.each do |i|
|
||||
config['collections'][i] = {}
|
||||
translations.each do |i|
|
||||
configuration['collections'][i] = {}
|
||||
end
|
||||
|
||||
Jekyll::Site.new(config)
|
||||
configuration
|
||||
end
|
||||
|
||||
# Devuelve el dominio actual
|
||||
def self.domain
|
||||
ENV.fetch('SUTTY', 'sutty.nl')
|
||||
end
|
||||
|
||||
# El directorio donde se almacenan los sitios
|
||||
def self.site_path
|
||||
File.join(Rails.root, '_sites')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# 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
|
||||
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
|
||||
def remove_directories!
|
||||
FileUtils.rm_rf path
|
||||
|
@ -346,11 +307,14 @@ class Site < ApplicationRecord
|
|||
def update_name!
|
||||
return unless name_changed?
|
||||
|
||||
FileUtils.mv old_path, path
|
||||
FileUtils.mv path_was, path
|
||||
end
|
||||
|
||||
# Sincroniza algunos atributos del sitio con su configuración y
|
||||
# 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!
|
||||
config.theme = design.gem unless design_id_changed?
|
||||
config.description = description unless description_changed?
|
||||
|
|
|
@ -79,6 +79,11 @@ class Site
|
|||
walker.each.to_a
|
||||
end
|
||||
|
||||
# Hay commits sin aplicar?
|
||||
def needs_pull?
|
||||
commits.empty?
|
||||
end
|
||||
|
||||
# Guarda los cambios en git, de a un archivo por vez
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def commit(file:, usuarie:, message:)
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
# Política de aceptación de colaboradorxs
|
||||
CollaborationPolicy = Struct.new(:usuarie, :collaboration) do
|
||||
def collaborate?
|
||||
collaboration.site.invitadxs?
|
||||
collaboration.site.invitades?
|
||||
end
|
||||
|
||||
def accept_collaboration?
|
||||
collaboration.site.invitadxs?
|
||||
collaboration.site.invitades?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
= fa_icon 'building'
|
||||
= t('sites.enqueue')
|
||||
|
||||
- if policy(site).pull? && site.needs_pull?
|
||||
- if policy(site).pull? && site.repository.needs_pull?
|
||||
= render 'layouts/btn_with_tooltip',
|
||||
tooltip: t('help.sites.pull'),
|
||||
text: t('.pull'),
|
||||
|
|
|
@ -1,4 +1,17 @@
|
|||
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
|
||||
en: English
|
||||
seconds: '%{seconds} seconds'
|
||||
|
|
|
@ -1,4 +1,17 @@
|
|||
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
|
||||
en: Inglés
|
||||
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
|
||||
site = create :site, name: 'sutty.nl'
|
||||
site.read
|
||||
|
||||
assert site.valid?
|
||||
assert !site.posts.empty?
|
||||
|
@ -85,4 +86,13 @@ class SiteTest < ActiveSupport::TestCase
|
|||
assert_equal 'hola', site.description
|
||||
assert_equal 'hola', site.title
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue