Merge branch 'rails' into search-engine

This commit is contained in:
f 2021-05-17 13:38:59 -03:00
commit 7b15a32de0
23 changed files with 368 additions and 59 deletions

View file

@ -29,10 +29,13 @@ serve: /etc/hosts
# make rails args="db:migrate" # make rails args="db:migrate"
rails: rails:
$(hain) 'cd /Sutty/sutty; bundle exec rails $(args)' $(MAKE) bundle args="exec rails $(args)"
rake: rake:
$(hain) 'cd /Sutty/sutty; bundle exec rake $(args)' $(MAKE) bundle args="exec rake $(args)"
bundle:
$(hain) 'cd /Sutty/sutty; bundle $(args)'
# Servir JS con el dev server. # Servir JS con el dev server.
# Esto acelera la compilación del javascript, tiene que correrse por separado # Esto acelera la compilación del javascript, tiene que correrse por separado

View file

@ -72,7 +72,8 @@ class SitesController < ApplicationController
authorize site authorize site
# XXX: Convertir en una máquina de estados? # XXX: Convertir en una máquina de estados?
DeployJob.perform_async site.id if site.enqueue! site.enqueue!
DeployJob.perform_async site.id
redirect_to site_posts_path(site, locale: site.default_locale) redirect_to site_posts_path(site, locale: site.default_locale)
end end

View file

@ -8,13 +8,20 @@ class DeployJob < ApplicationJob
def perform(site, notify = true) def perform(site, notify = true)
ActiveRecord::Base.connection_pool.with_connection do ActiveRecord::Base.connection_pool.with_connection do
@site = Site.find(site) @site = Site.find(site)
@site.update_attribute :status, 'building'
# Si ya hay una tarea corriendo, aplazar esta
if @site.building?
DeployJob.perform_in(60, site, notify)
return
end
@site.update status: 'building'
# Asegurarse que DeployLocal sea el primero! # Asegurarse que DeployLocal sea el primero!
@deployed = { deploy_local: deploy_locally } @deployed = { deploy_local: deploy_locally }
# No es opcional # No es opcional
unless @deployed[:deploy_local] unless @deployed[:deploy_local]
@site.update_attribute :status, 'waiting' @site.update status: 'waiting'
notify_usuaries if notify notify_usuaries if notify
# Hacer fallar la tarea # Hacer fallar la tarea
@ -23,7 +30,7 @@ class DeployJob < ApplicationJob
deploy_others deploy_others
notify_usuaries if notify notify_usuaries if notify
@site.update_attribute :status, 'waiting' @site.update status: 'waiting'
end end
end end
# rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/MethodLength

View file

@ -3,13 +3,6 @@
# Almacena el UUID de otro Post y actualiza el valor en el Post # Almacena el UUID de otro Post y actualiza el valor en el Post
# relacionado. # relacionado.
class MetadataBelongsTo < MetadataRelatedPosts class MetadataBelongsTo < MetadataRelatedPosts
def value_was=(new_value)
@belongs_to = nil
@belonged_to = nil
super(new_value)
end
# TODO: Convertir algunos tipos de valores en módulos para poder # TODO: Convertir algunos tipos de valores en módulos para poder
# implementar varios tipos de campo sin repetir código # implementar varios tipos de campo sin repetir código
# #
@ -39,10 +32,14 @@ class MetadataBelongsTo < MetadataRelatedPosts
# Si estamos cambiando la relación, tenemos que eliminar la relación # Si estamos cambiando la relación, tenemos que eliminar la relación
# anterior # anterior
belonged_to[inverse].value.delete post.uuid.value if changed? && belonged_to.present? if belonged_to.present?
belonged_to[inverse].value = belonged_to[inverse].value.reject do |rej|
rej == post.uuid.value
end
end
# No duplicar las relaciones # No duplicar las relaciones
belongs_to[inverse].value << post.uuid.value unless belongs_to.blank? || included? belongs_to[inverse].value = (belongs_to[inverse].value.dup << post.uuid.value) unless belongs_to.blank? || included?
true true
end end
@ -63,20 +60,13 @@ class MetadataBelongsTo < MetadataRelatedPosts
end end
# El Post relacionado con este artículo # El Post relacionado con este artículo
#
# XXX: Memoizamos usando el valor para tener el valor siempre
# actualizado.
def belongs_to def belongs_to
return if value.blank? posts.find(value, uuid: true) if value.present?
@belongs_to ||= posts.find(value, uuid: true)
end end
# El artículo relacionado anterior # El artículo relacionado anterior
def belonged_to def belonged_to
return if value_was.blank? posts.find(value_was, uuid: true) if value_was.present?
@belonged_to ||= posts.find(value_was, uuid: true)
end end
def related_posts? def related_posts?

View file

@ -8,6 +8,8 @@ class MetadataDocumentDate < MetadataTemplate
end end
def value_from_document def value_from_document
return nil if post.new?
document.date document.date
end end
@ -15,13 +17,47 @@ class MetadataDocumentDate < MetadataTemplate
true && !private? true && !private?
end end
# Siempre es obligatorio
def required
true
end
def validate
super
errors << I18n.t('metadata.date.invalid_format') unless valid_format?
errors.empty?
end
# El valor puede ser un Date, Time o una String en el formato # El valor puede ser un Date, Time o una String en el formato
# "yyyy-mm-dd" # "yyyy-mm-dd"
#
# XXX: Date.iso8601 acepta fechas en el futuro lejano, como 20000,
# pero Jekyll las limita a cuatro cifras, así que vamos a mantener
# eso.
#
# @see {https://github.com/jekyll/jekyll/blob/master/lib/jekyll/document.rb#L15}
def value def value
return (self[:value] = value_from_document || default_value) if self[:value].nil? self[:value] =
case self[:value]
self[:value] = Date.iso8601(self[:value]).to_time if self[:value].is_a? String when String
begin
self[:value] Date.iso8601(self[:value]).to_time
rescue Date::Error
value_from_document || default_value
end
else
self[:value] || value_from_document || default_value
end
end
private
def valid_format?
return true if self[:value].is_a?(Time)
@valid_format_re ||= /\A\d{2,4}-\d{1,2}-\d{1,2}\z/
@valid_format_re =~ self[:value].to_s
end end
end end

View file

@ -18,6 +18,7 @@ class MetadataHasAndBelongsToMany < MetadataHasMany
# #
# Buscamos en belongs_to la relación local, si se eliminó hay que # Buscamos en belongs_to la relación local, si se eliminó hay que
# quitarla de la relación remota, sino hay que agregarla. # quitarla de la relación remota, sino hay que agregarla.
#
def save def save
# XXX: No usamos super # XXX: No usamos super
self[:value] = sanitize value self[:value] = sanitize value
@ -25,27 +26,21 @@ class MetadataHasAndBelongsToMany < MetadataHasMany
return true unless changed? return true unless changed?
return true unless inverse? return true unless inverse?
# XXX: Usamos asignación para aprovechar value= que setea el valor
# anterior en @value_was
(had_many - has_many).each do |remove| (had_many - has_many).each do |remove|
remove[inverse]&.value&.delete post.uuid.value remove[inverse].value = remove[inverse].value.reject do |rej|
rej == post.uuid.value
end
end end
(has_many - had_many).each do |add| (has_many - had_many).each do |add|
next unless add[inverse] next unless add[inverse]
next if add[inverse].value.include? post.uuid.value next if add[inverse].value.include? post.uuid.value
add[inverse].value << post.uuid.value add[inverse].value = (add[inverse].value.dup << post.uuid.value)
end end
true true
end end
private
# Igual que en MetadataRelatedPosts
# TODO: Mover a un módulo
def sanitize(uuid)
super(uuid.map do |u|
u.to_s.gsub(/[^a-f0-9\-]/i, '')
end)
end
end end

View file

@ -6,14 +6,6 @@
# Localmente tenemos un Array de UUIDs. Remotamente tenemos una String # Localmente tenemos un Array de UUIDs. Remotamente tenemos una String
# apuntando a un Post, que se mantiene actualizado como el actual. # apuntando a un Post, que se mantiene actualizado como el actual.
class MetadataHasMany < MetadataRelatedPosts class MetadataHasMany < MetadataRelatedPosts
# Invalidar la relación anterior
def value_was=(new_value)
@had_many = nil
@has_many = nil
super(new_value)
end
def validate def validate
super super
@ -24,14 +16,16 @@ class MetadataHasMany < MetadataRelatedPosts
# Todos los Post relacionados # Todos los Post relacionados
def has_many def has_many
@has_many ||= posts.where(uuid: value) return default_value if value.blank?
posts.where(uuid: value)
end end
# La relación anterior # La relación anterior
def had_many def had_many
return [] if value_was.blank? return default_value if value_was.blank?
@had_many ||= posts.where(uuid: value_was) posts.where(uuid: value_was)
end end
def inverse? def inverse?

View file

@ -30,7 +30,7 @@ class MetadataRelatedPosts < MetadataArray
# Obtiene todos los posts y opcionalmente los filtra # Obtiene todos los posts y opcionalmente los filtra
def posts def posts
@posts ||= site.posts(lang: lang).where(**filter) site.posts(lang: lang).where(**filter)
end end
def title(post) def title(post)

View file

@ -55,9 +55,7 @@ class Post
public_send(attr)&.value = args[attr] if args.key?(attr) public_send(attr)&.value = args[attr] if args.key?(attr)
end end
# XXX: No usamos Post#read porque a esta altura todavía no sabemos document.read! unless new?
# nada del Document
document.read! if File.exist? document.path
end end
def inspect def inspect
@ -238,12 +236,14 @@ class Post
template = public_send attr template = public_send attr
unless template.front_matter? unless template.front_matter?
body += "\n\n" body += "\n\n" if body.present?
body += template.value body += template.value
next next
end end
next if template.empty? # Queremos mantener los Array en el resultado final para que
# siempre respondan a {% for %} en Liquid.
next if template.empty? && !template.value.is_a?(Array)
[attr.to_s, template.value] [attr.to_s, template.value]
end.compact.to_h end.compact.to_h
@ -301,6 +301,7 @@ class Post
end end
# Vuelve a leer el post para tomar los cambios # Vuelve a leer el post para tomar los cambios
document.reset
read read
written? written?

View file

@ -93,8 +93,7 @@ class PostRelation < Array
def where(**args) def where(**args)
return self if args.empty? return self if args.empty?
@where ||= {} begin
@where[args.hash.to_s] ||= begin
PostRelation.new(site: site, lang: lang).concat(select do |post| PostRelation.new(site: site, lang: lang).concat(select do |post|
result = args.map do |attr, value| result = args.map do |attr, value|
next unless post.attribute?(attr) next unless post.attribute?(attr)

View file

@ -306,14 +306,25 @@ class Site < ApplicationRecord
# Poner en la cola de compilación # Poner en la cola de compilación
def enqueue! def enqueue!
!enqueued? && update_attribute(:status, 'enqueued') update(status: 'enqueued') if waiting?
end end
# Está en la cola de compilación? # Está en la cola de compilación?
#
# TODO: definir todos estos métodos dinámicamente, aunque todavía no
# tenemos una máquina de estados propiamente dicha.
def enqueued? def enqueued?
status == 'enqueued' status == 'enqueued'
end end
def waiting?
status == 'waiting'
end
def building?
status == 'building'
end
# Cargar el sitio Jekyll # Cargar el sitio Jekyll
# #
# 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

View file

@ -73,6 +73,15 @@ module Jekyll
Document.class_eval do Document.class_eval do
alias_method :read!, :read alias_method :read!, :read
def read; end def read; end
# Permitir restablecer el documento sin crear uno nuevo
def reset
@path = @extname = @has_yaml_header = @relative_path = nil
@basename_without_ext = @data = @basename = nil
@renderer = @url_placeholders = @url = nil
@to_liquid = @write_p = @excerpt_separator = @id = nil
@related_posts = @cleaned_relative_path = self.content = nil
end
end end
# Prevenir la instanciación de Time # Prevenir la instanciación de Time

View file

@ -102,6 +102,13 @@ class SitesControllerTest < ActionDispatch::IntegrationTest
'index.html')) 'index.html'))
end end
test 'no se pueden encolar varias veces seguidas' do
assert_enqueued_jobs 2 do
post site_enqueue_url(@site), headers: @authorization
post site_enqueue_url(@site), headers: @authorization
end
end
test 'se pueden actualizar' do test 'se pueden actualizar' do
name = SecureRandom.hex name = SecureRandom.hex
design = Design.all.where.not(id: @site.design_id).sample design = Design.all.where.not(id: @site.design_id).sample

View file

@ -0,0 +1,2 @@
This is site where posts can have many authors and viceversa and posts
can be replies to others.

View file

@ -0,0 +1,2 @@
locales:
- en

View file

@ -0,0 +1,9 @@
---
title:
type: 'string'
required: true
posts:
type: 'has_and_belongs_to_many'
inverse: 'authors'
filter:
layout: 'post'

View file

@ -0,0 +1,23 @@
---
title:
type: 'string'
required: true
authors:
type: 'has_and_belongs_to_many'
inverse: 'posts'
filter:
layout: 'author'
posts:
type: 'has_many'
inverse: 'in_reply_to'
filter:
layout: 'post'
in_reply_to:
type: 'belongs_to'
inverse: 'posts'
filter:
layout: 'post'
recommended_posts:
type: 'related_posts'
filter:
layout: 'post'

View file

View file

@ -0,0 +1 @@
_en

View file

@ -0,0 +1,62 @@
# frozen_string_literal: true
require 'test_helper'
require_relative 'metadata_test'
class MetadataBelongsToTest < ActiveSupport::TestCase
include MetadataTest
test 'se pueden relacionar artículos' do
post = @site.posts.create(layout: :post, title: SecureRandom.hex)
reply = @site.posts.create(layout: :post, title: SecureRandom.hex, in_reply_to: post.uuid.value)
assert_equal post, reply.in_reply_to.belongs_to
assert_includes post.posts.has_many, reply
assert post.save
assert_equal reply.document.data['in_reply_to'], post.document.data['uuid']
assert_includes post.document.data['posts'], reply.document.data['uuid']
end
test 'se puede eliminar la relación' do
post = @site.posts.create(layout: :post, title: SecureRandom.hex)
reply = @site.posts.create(layout: :post, title: SecureRandom.hex, in_reply_to: post.uuid.value)
reply.in_reply_to.value = ''
assert reply.save
assert_not_equal post, reply.in_reply_to.belongs_to
assert_equal post, reply.in_reply_to.belonged_to
assert_nil reply.in_reply_to.belongs_to
assert_not_includes post.posts.has_many, reply
assert post.save
assert_nil reply.document.data['in_reply_to']
assert_not_includes post.document.data['posts'], reply.document.data['uuid']
end
test 'se puede cambiar la relación' do
post1 = @site.posts.create(layout: :post, title: SecureRandom.hex)
post2 = @site.posts.create(layout: :post, title: SecureRandom.hex)
reply = @site.posts.create(layout: :post, title: SecureRandom.hex, in_reply_to: post1.uuid.value)
reply.in_reply_to.value = post2.uuid.value
assert reply.save
assert_not_equal post1, reply.in_reply_to.belongs_to
assert_equal post1, reply.in_reply_to.belonged_to
assert_not_includes post1.posts.has_many, reply
assert_equal post2, reply.in_reply_to.belongs_to
assert_includes post2.posts.has_many, reply
assert post1.save
assert post2.save
assert_equal post2.document.data['uuid'], reply.document.data['in_reply_to']
assert_includes post2.document.data['posts'], reply.document.data['uuid']
assert_not_includes post1.document.data['posts'], reply.document.data['uuid']
end
end

View file

@ -0,0 +1,76 @@
# frozen_string_literal: true
require 'test_helper'
require_relative 'metadata_test'
class MetadataHasAndBelongsManyTest < ActiveSupport::TestCase
include MetadataTest
test 'se pueden relacionar artículos' do
author = @site.posts.create(layout: :author, title: SecureRandom.hex)
post = @site.posts.create(layout: :post, title: SecureRandom.hex)
post.authors.value = [author.uuid.value]
assert post.save
assert_includes author.posts.has_many, post
assert_includes post.authors.has_many, author
assert author.save
assert_includes author.document.data['posts'], post.document.data['uuid']
assert_includes post.document.data['authors'], author.document.data['uuid']
end
test 'se puede eliminar la relación' do
author = @site.posts.create(layout: :author, title: SecureRandom.hex)
post = @site.posts.create(layout: :post, title: SecureRandom.hex, authors: [author.uuid.value])
assert_includes post.authors.value, author.uuid.value
assert_includes author.posts.value, post.uuid.value
post.authors.value = []
assert post.save
assert_not_includes author.posts.has_many, post
assert_not_includes post.authors.has_many, author
assert_includes author.posts.had_many, post
assert_includes post.authors.had_many, author
assert author.save
assert_not_includes author.document.data['posts'], post.document.data['uuid']
assert_not_includes post.document.data['authors'], author.document.data['uuid']
end
test 'se puede cambiar la relación' do
author = @site.posts.create(layout: :author, title: SecureRandom.hex)
post1 = @site.posts.create(layout: :post, title: SecureRandom.hex, authors: [author.uuid.value])
post2 = @site.posts.create(layout: :post, title: SecureRandom.hex)
author.posts.value = [post2.uuid.value]
assert author.save
assert_not_includes author.posts.has_many, post1
assert_not_includes post1.authors.has_many, author
assert_includes author.posts.had_many, post1
assert_includes post1.authors.had_many, author
assert_not_includes author.posts.had_many, post2
assert_not_includes post2.authors.had_many, author
assert_includes author.posts.has_many, post2
assert_includes post2.authors.has_many, author
assert post1.save
assert post2.save
assert_not_includes author.document.data['posts'], post1.document.data['uuid']
assert_not_includes post1.document.data['authors'], author.document.data['uuid']
assert_includes author.document.data['posts'], post2.document.data['uuid']
assert_includes post2.document.data['authors'], author.document.data['uuid']
end
end

View file

@ -0,0 +1,62 @@
# frozen_string_literal: true
require 'test_helper'
require_relative 'metadata_test'
class MetadataHasManyTest < ActiveSupport::TestCase
include MetadataTest
test 'se pueden relacionar artículos' do
reply = @site.posts.create(layout: :post, title: SecureRandom.hex)
post = @site.posts.create(layout: :post, title: SecureRandom.hex, posts: [reply.uuid.value])
assert_equal post, reply.in_reply_to.belongs_to
assert_includes post.posts.has_many, reply
assert reply.save
assert_equal reply.document.data['in_reply_to'], post.document.data['uuid']
assert_includes post.document.data['posts'], reply.document.data['uuid']
end
test 'se puede eliminar la relación' do
reply = @site.posts.create(layout: :post, title: SecureRandom.hex)
post = @site.posts.create(layout: :post, title: SecureRandom.hex, posts: [reply.uuid.value])
post.posts.value = []
assert post.save
assert_not_equal post, reply.in_reply_to.belongs_to
assert_equal post, reply.in_reply_to.belonged_to
assert_nil reply.in_reply_to.belongs_to
assert_not_includes post.posts.has_many, reply
assert reply.save
assert_nil reply.document.data['in_reply_to']
assert_not_includes post.document.data['posts'], reply.document.data['uuid']
end
test 'se puede cambiar la relación' do
reply = @site.posts.create(layout: :post, title: SecureRandom.hex)
post1 = @site.posts.create(layout: :post, title: SecureRandom.hex, posts: [reply.uuid.value])
post2 = @site.posts.create(layout: :post, title: SecureRandom.hex)
reply.in_reply_to.value = post2.uuid.value
assert reply.save
assert_not_equal post1, reply.in_reply_to.belongs_to
assert_equal post1, reply.in_reply_to.belonged_to
assert_not_includes post1.posts.has_many, reply
assert_equal post2, reply.in_reply_to.belongs_to
assert_includes post2.posts.has_many, reply
assert post1.save
assert post2.save
assert_equal post2.document.data['uuid'], reply.document.data['in_reply_to']
assert_includes post2.document.data['posts'], reply.document.data['uuid']
assert_not_includes post1.document.data['posts'], reply.document.data['uuid']
end
end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module MetadataTest
extend ActiveSupport::Concern
included do
setup do
name = SecureRandom.hex
# TODO: Poder cambiar el nombre
FileUtils.cp_r(Rails.root.join('test', 'fixtures', 'site_with_relationships'), Rails.root.join('_sites', name))
@site = create :site, name: name
end
teardown do
@site&.destroy
end
end
end