Merge branch 'rails' into void/editor

This commit is contained in:
f 2021-02-19 14:07:11 -03:00
commit 0b86702bdb
38 changed files with 378 additions and 357 deletions

View file

@ -5,8 +5,11 @@
# tiempo buscando soporte para musl # tiempo buscando soporte para musl
if ENV['RAILS_ENV'] == 'production' if ENV['RAILS_ENV'] == 'production'
source 'https://gems.sutty.nl' source 'https://gems.sutty.nl'
ruby '2.7.2'
else else
source 'https://rubygems.org' source 'https://rubygems.org'
# Cambiar en Dockerfile también
ruby '2.7.1'
end end
git_source(:github) do |repo_name| git_source(:github) do |repo_name|
@ -14,8 +17,6 @@ git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git" "https://github.com/#{repo_name}.git"
end end
# Cambiar en Dockerfile también
ruby '2.7.2'
gem 'dotenv-rails', require: 'dotenv/rails-now' gem 'dotenv-rails', require: 'dotenv/rails-now'
@ -71,6 +72,7 @@ gem 'redis', require: %w[redis redis/connection/hiredis]
gem 'redis-rails' gem 'redis-rails'
gem 'rubyzip' gem 'rubyzip'
gem 'rugged' gem 'rugged'
gem 'concurrent-ruby-ext'
gem 'sucker_punch' gem 'sucker_punch'
gem 'symbol-fstring', require: 'fstring/all' gem 'symbol-fstring', require: 'fstring/all'
gem 'terminal-table' gem 'terminal-table'

View file

@ -10,60 +10,60 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
remote: https://gems.sutty.nl/ remote: https://gems.sutty.nl/
specs: specs:
actioncable (6.1.1) actioncable (6.1.2.1)
actionpack (= 6.1.1) actionpack (= 6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.1) actionmailbox (6.1.2.1)
actionpack (= 6.1.1) actionpack (= 6.1.2.1)
activejob (= 6.1.1) activejob (= 6.1.2.1)
activerecord (= 6.1.1) activerecord (= 6.1.2.1)
activestorage (= 6.1.1) activestorage (= 6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.1) actionmailer (6.1.2.1)
actionpack (= 6.1.1) actionpack (= 6.1.2.1)
actionview (= 6.1.1) actionview (= 6.1.2.1)
activejob (= 6.1.1) activejob (= 6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.1) actionpack (6.1.2.1)
actionview (= 6.1.1) actionview (= 6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.1) actiontext (6.1.2.1)
actionpack (= 6.1.1) actionpack (= 6.1.2.1)
activerecord (= 6.1.1) activerecord (= 6.1.2.1)
activestorage (= 6.1.1) activestorage (= 6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.1) actionview (6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.1) activejob (6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.1) activemodel (6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
activerecord (6.1.1) activerecord (6.1.2.1)
activemodel (= 6.1.1) activemodel (= 6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
activestorage (6.1.1) activestorage (6.1.2.1)
actionpack (= 6.1.1) actionpack (= 6.1.2.1)
activejob (= 6.1.1) activejob (= 6.1.2.1)
activerecord (= 6.1.1) activerecord (= 6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
marcel (~> 0.3.1) marcel (~> 0.3.1)
mimemagic (~> 0.3.2) mimemagic (~> 0.3.2)
activesupport (6.1.1) activesupport (6.1.2.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -86,7 +86,7 @@ GEM
bcrypt (3.1.16) bcrypt (3.1.16)
benchmark-ips (2.8.4) benchmark-ips (2.8.4)
bindex (0.8.1) bindex (0.8.1)
blazer (2.4.1) blazer (2.4.2)
activerecord (>= 5) activerecord (>= 5)
chartkick (>= 3.2) chartkick (>= 3.2)
railties (>= 5) railties (>= 5)
@ -108,18 +108,20 @@ GEM
childprocess (3.0.0) childprocess (3.0.0)
coderay (1.1.3) coderay (1.1.3)
colorator (1.1.0) colorator (1.1.0)
commonmarker (0.21.1) commonmarker (0.21.2)
ruby-enum (~> 0.5) ruby-enum (~> 0.5)
concurrent-ruby (1.1.8) concurrent-ruby (1.1.8)
concurrent-ruby-ext (1.1.8)
concurrent-ruby (= 1.1.8)
crass (1.0.6) crass (1.0.6)
database_cleaner (2.0.0) database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0) database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.0.0) database_cleaner-active_record (2.0.0)
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.0) database_cleaner-core (2.0.1)
dead_end (1.1.4) dead_end (1.1.4)
derailed_benchmarks (2.0.0) derailed_benchmarks (2.0.1)
benchmark-ips (~> 2) benchmark-ips (~> 2)
dead_end dead_end
get_process_mem (~> 0) get_process_mem (~> 0)
@ -127,6 +129,7 @@ GEM
memory_profiler (>= 0, < 2) memory_profiler (>= 0, < 2)
mini_histogram (>= 0.3.0) mini_histogram (>= 0.3.0)
rack (>= 1) rack (>= 1)
rack-test
rake (> 10, < 14) rake (> 10, < 14)
ruby-statistics (>= 2.1) ruby-statistics (>= 2.1)
thor (>= 0.19, < 2) thor (>= 0.19, < 2)
@ -287,7 +290,7 @@ GEM
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2) ruby_dep (~> 1.2)
lockbox (0.6.1) lockbox (0.6.2)
lograge (0.11.2) lograge (0.11.2)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
@ -313,7 +316,7 @@ GEM
jekyll-feed (~> 0.9) jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1) jekyll-seo-tag (~> 2.1)
minitest (5.14.3) minitest (5.14.3)
mobility (1.0.5) mobility (1.1.0)
i18n (>= 0.6.10, < 2) i18n (>= 0.6.10, < 2)
request_store (~> 1.0) request_store (~> 1.0)
netaddr (2.0.4) netaddr (2.0.4)
@ -331,7 +334,7 @@ GEM
popper_js (1.16.0) popper_js (1.16.0)
prometheus_exporter (0.7.0) prometheus_exporter (0.7.0)
webrick webrick
pry (0.13.1) pry (0.14.0)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
public_suffix (4.0.6) public_suffix (4.0.6)
@ -360,20 +363,20 @@ GEM
jekyll-relative-urls (~> 0.0) jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1) jekyll-seo-tag (~> 2.1)
jekyll-turbolinks (~> 0) jekyll-turbolinks (~> 0)
rails (6.1.1) rails (6.1.2.1)
actioncable (= 6.1.1) actioncable (= 6.1.2.1)
actionmailbox (= 6.1.1) actionmailbox (= 6.1.2.1)
actionmailer (= 6.1.1) actionmailer (= 6.1.2.1)
actionpack (= 6.1.1) actionpack (= 6.1.2.1)
actiontext (= 6.1.1) actiontext (= 6.1.2.1)
actionview (= 6.1.1) actionview (= 6.1.2.1)
activejob (= 6.1.1) activejob (= 6.1.2.1)
activemodel (= 6.1.1) activemodel (= 6.1.2.1)
activerecord (= 6.1.1) activerecord (= 6.1.2.1)
activestorage (= 6.1.1) activestorage (= 6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.1) railties (= 6.1.2.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -385,9 +388,9 @@ GEM
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
rails_warden (0.6.0) rails_warden (0.6.0)
warden (>= 1.2.0) warden (>= 1.2.0)
railties (6.1.1) railties (6.1.2.1)
actionpack (= 6.1.1) actionpack (= 6.1.2.1)
activesupport (= 6.1.1) activesupport (= 6.1.2.1)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (~> 1.0) thor (~> 1.0)
@ -457,7 +460,7 @@ GEM
i18n i18n
ruby-filemagic (0.7.2) ruby-filemagic (0.7.2)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby-statistics (2.1.2) ruby-statistics (2.1.3)
ruby-vips (2.0.17) ruby-vips (2.0.17)
ffi (~> 1.9) ffi (~> 1.9)
ruby_dep (1.5.0) ruby_dep (1.5.0)
@ -537,7 +540,7 @@ GEM
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
thor (1.1.0) thor (1.1.0)
tilt (2.0.10) tilt (2.0.10)
timecop (0.9.2) timecop (0.9.4)
turbolinks (5.2.1) turbolinks (5.2.1)
turbolinks-source (~> 5.2) turbolinks-source (~> 5.2)
turbolinks-source (5.2.0) turbolinks-source (5.2.0)
@ -583,6 +586,7 @@ DEPENDENCIES
brakeman brakeman
capybara (~> 2.13) capybara (~> 2.13)
commonmarker commonmarker
concurrent-ruby-ext
database_cleaner database_cleaner
derailed_benchmarks derailed_benchmarks
devise devise
@ -660,7 +664,7 @@ DEPENDENCIES
yaml_db! yaml_db!
RUBY VERSION RUBY VERSION
ruby 2.7.2p137 ruby 2.7.1p83
BUNDLED WITH BUNDLED WITH
2.1.4 2.1.4

View file

@ -9,10 +9,10 @@ assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type
alpine_version := 3.12 alpine_version := 3.12
public/packs/manifest.json: $(assets) public/packs/manifest.json.br: $(assets)
PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean
assets: public/packs/manifest.json assets: public/packs/manifest.json.br
serve: /etc/hosts serve: /etc/hosts
bundle exec rails s -b "ssl://0.0.0.0:3000?key=../sutty.local/domain/$(SUTTY).key&cert=../sutty.local/domain/$(SUTTY).crt" bundle exec rails s -b "ssl://0.0.0.0:3000?key=../sutty.local/domain/$(SUTTY).key&cert=../sutty.local/domain/$(SUTTY).crt"
@ -82,7 +82,7 @@ $(dirs):
/etc/hosts: always /etc/hosts: always
@echo "Chequeando si es necesario agregar el dominio local $(SUTTY)" @echo "Chequeando si es necesario agregar el dominio local $(SUTTY)"
@grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@ @grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@
@grep -q " api.$(SUTTY)$$" $@ || echo -e "127.0.0.1 api.$(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@ @grep -q " api.$(SUTTY)$$" $@ || echo -e "127.0.0.1 api.$(SUTTY)\n::1 api.$(SUTTY)" | sudo tee -a $@
@grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@ @grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 panel.$(SUTTY)" | sudo tee -a $@
.PHONY: always .PHONY: always

View file

@ -4,7 +4,7 @@
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include ExceptionHandler include ExceptionHandler
protect_from_forgery with: :exception protect_from_forgery with: :exception, prepend: true
before_action :prepare_exception_notifier before_action :prepare_exception_notifier
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?

View file

@ -73,7 +73,7 @@ module ApplicationHelper
# Opciones por defecto para el campo de un formulario # Opciones por defecto para el campo de un formulario
def field_options(attribute, metadata, **extra) def field_options(attribute, metadata, **extra)
required = metadata.required || extra[:required] required = extra.key?(:required) ? extra[:required] : metadata.required
{ {
class: "form-control #{invalid(metadata.post, attribute)} #{extra[:class]}", class: "form-control #{invalid(metadata.post, attribute)} #{extra[:class]}",

View file

@ -107,4 +107,13 @@ document.addEventListener('turbolinks:load', () => {
// Ocultar el area // Ocultar el area
textArea.style.display = 'none' textArea.style.display = 'none'
}) })
// Validar fechas en navegadores que no soportan date
document.querySelectorAll('input[type="date"]').forEach(date => {
if (date.type === 'date') return
date.addEventListener('change', event => {
date.setCustomValidity(date.validity.patternMismatch ? date.dataset.patternMismatch : '')
})
})
}) })

View file

@ -22,15 +22,13 @@ class MaintenanceJob < ApplicationJob
# XXX: Parece que [0] es más rápido que []#first # XXX: Parece que [0] es más rápido que []#first
Usuarie.all.pluck(:email, :lang).each do |u| Usuarie.all.pluck(:email, :lang).each do |u|
begin MaintenanceMailer.with(maintenance: maintenance,
MaintenanceMailer.with(maintenance: maintenance, email: u[0],
email: u[0], lang: u[1]).public_send(mailer).deliver_now
lang: u[1]).public_send(mailer).deliver_now rescue Net::SMTPServerBusy => e
rescue Net::SMTPServerBusy => e # Algunas direcciones no son válidas, no queremos detener el
# Algunas direcciones no son válidas, no queremos detener el # envío pero sí enterarnos cuáles son
# envío pero sí enterarnos cuáles son ExceptionNotifier.notify_exception(e, data: { maintenance_id: maintenance_id, email: u[0] })
ExceptionNotifier.notify_exception e
end
end end
end end
end end

View file

@ -9,6 +9,10 @@ Layout = Struct.new(:site, :name, :meta, :metadata, keyword_init: true) do
name.to_s name.to_s
end end
def attributes
@attributes ||= metadata.keys.map(&:to_sym)
end
# Busca la traducción del Layout en el sitio o intenta humanizarlo # Busca la traducción del Layout en el sitio o intenta humanizarlo
# según Rails. # según Rails.
# #

View file

@ -7,15 +7,22 @@ class MetadataArray < MetadataTemplate
super || [] super || []
end end
# Los Arrays no se pueden cifrar todavía
# TODO: Cifrar y decifrar arrays
def private?
false
end
private private
# TODO: Sanitizar otros valores
# XXX: Por qué eliminamos el punto del final?
def sanitize(values) def sanitize(values)
values.map do |v| values.map do |v|
if v.is_a? String case v
super(v).sub(/\.\z/, '') when String then super(v).sub(/\.\z/, '')
else else v
v
end end
end end.select(&:present?)
end end
end end

View file

@ -3,6 +3,13 @@
# 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
# #
@ -23,36 +30,36 @@ class MetadataBelongsTo < MetadataRelatedPosts
# Guardar y guardar la relación inversa también, eliminando la # Guardar y guardar la relación inversa también, eliminando la
# relación anterior si existía. # relación anterior si existía.
#
# XXX: Esto es un poco enclenque, porque habría que guardar tres
# archivos en lugar de uno solo e indicarle al artículo que tiene uno
# o muchos que busque los datos actualizados filtrando. Pero también
# nos ahorra recursos en la búsqueda al cachear la información. En
# una relación HABTM también vamos a hacer lo mismo.
def save def save
return super unless inverse? && !included? super
# Evitar que se cambie el orden de la relación # Si no hay relación inversa, no hacer nada más
belonged_to&.dig(inverse)&.value&.delete post.uuid.value if belonged_to != belongs_to return true unless changed?
return true unless inverse?
belongs_to[inverse].value << post.uuid.value unless belongs_to[inverse].value.include? post.uuid.value # Si estamos cambiando la relación, tenemos que eliminar la relación
# anterior
belonged_to[inverse].value.delete post.uuid.value if changed? && belonged_to.present?
# No duplicar las relaciones
belongs_to[inverse].value << post.uuid.value unless belongs_to.blank? || included?
true true
end end
# El Post actual está incluido en la relación inversa? # El Post actual está incluido en la relación inversa?
def included? def included?
belongs_to[inverse].value.include? post.uuid.value belongs_to[inverse].value.include?(post.uuid.value)
end end
# Hay una relación inversa y el artículo existe? # Hay una relación inversa y el artículo existe?
def inverse? def inverse?
inverse.present? && belongs_to.present? inverse.present?
end end
# El campo que es la relación inversa de este # El campo que es la relación inversa de este
def inverse def inverse
layout.metadata.dig name, 'inverse' @inverse ||= layout.metadata.dig(name, 'inverse')&.to_sym
end end
# El Post relacionado con este artículo # El Post relacionado con este artículo
@ -62,15 +69,14 @@ class MetadataBelongsTo < MetadataRelatedPosts
def belongs_to def belongs_to
return if value.blank? return if value.blank?
@belongs_to ||= {} @belongs_to ||= posts.find(value, uuid: true)
@belongs_to[value] ||= posts.find(value, uuid: true)
end end
# El anterior artículo relacionado # El artículo relacionado anterior
def belonged_to def belonged_to
return if document.data[name.to_s].blank? return if value_was.blank?
@belonged_to ||= posts.find(document.data[name.to_s], uuid: true) @belonged_to ||= posts.find(value_was, uuid: true)
end end
def related_posts? def related_posts?
@ -83,11 +89,13 @@ class MetadataBelongsTo < MetadataRelatedPosts
private private
def sanitize(uuid) def post_exists?
uuid.gsub(/[^a-f0-9\-]/, '') return true if sanitize(value).blank?
sanitize(value).present? && belongs_to.present?
end end
def post_exists? def sanitize(uuid)
!value.blank? && posts.find(sanitize(value), uuid: true) uuid.to_s.gsub(/[^a-f0-9\-]/i, '')
end end
end end

View file

@ -7,10 +7,14 @@ class MetadataDocumentDate < MetadataTemplate
Date.today.to_time Date.today.to_time
end end
def value_from_document
document.date
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"
def value def value
return (self[:value] = document.date || default_value) if self[:value].nil? return (self[:value] = value_from_document || default_value) if self[:value].nil?
self[:value] = Date.iso8601(self[:value]).to_time if self[:value].is_a? String self[:value] = Date.iso8601(self[:value]).to_time if self[:value].is_a? String

View file

@ -2,9 +2,14 @@
# Devuelve metadatos de cierto tipo # Devuelve metadatos de cierto tipo
class MetadataFactory class MetadataFactory
def self.build(**args) class << self
@@factory_cache ||= {} def build(**args)
@@factory_cache[args[:type]] ||= ('Metadata' + args[:type].to_s.camelcase).constantize classify(args[:type]).new(**args)
@@factory_cache[args[:type]].new(args) end
def classify(type)
@factory_cache ||= {}
@factory_cache[type] ||= ('Metadata' + type.to_s.camelcase).constantize
end
end end
end end

View file

@ -73,10 +73,14 @@ class MetadataFile < MetadataTemplate
end end
end end
def key_from_path
path.dirname.basename.to_s
end
private private
def path? def path?
!value['path'].blank? value['path'].present?
end end
def filemagic def filemagic
@ -100,10 +104,6 @@ class MetadataFile < MetadataTemplate
end end
end end
def key_from_path
path.dirname.basename.to_s
end
# Hacemos un link duro para colocar el archivo dentro del repositorio # Hacemos un link duro para colocar el archivo dentro del repositorio
# y no duplicar el espacio que ocupan. Esto requiere que ambos # y no duplicar el espacio que ocupan. Esto requiere que ambos
# directorios estén dentro del mismo punto de montaje. # directorios estén dentro del mismo punto de montaje.

View file

@ -10,40 +10,7 @@
# el libro actual. La relación belongs_to tiene que traer todes les # el libro actual. La relación belongs_to tiene que traer todes les
# autores que tienen este libro. La relación es bidireccional, no hay # autores que tienen este libro. La relación es bidireccional, no hay
# diferencia entre has_many y belongs_to. # diferencia entre has_many y belongs_to.
class MetadataHasAndBelongsToMany < MetadataBelongsTo class MetadataHasAndBelongsToMany < MetadataHasMany
def default_value
[]
end
# Posts a los que pertenece. Memoizamos por value para obtener
# siempre la última relación.
#
# Buscamos todos los Post contenidos en el valor actual. No
# garantizamos el orden.
#
# @return [PostRelation] Posts
def belongs_to
@belongs_to ||= {}
@belongs_to[value.hash.to_s] ||= posts.where(uuid: value)
end
# Devuelve la lista de Posts relacionados con este buscándolos en la
# relación inversa. #save debería mantenerlos sincronizados.
#
# @return [PostRelation]
def has_many
@has_many ||= {}
@has_many[value.hash.to_s] ||= posts.where(inverse.to_sym => post.uuid.value)
end
alias had_many has_many
# Posts a los que pertenecía
#
# @return [PostRelation] Posts
def belonged_to
@belonged_to ||= posts.where(uuid: document.data.fetch(name.to_s, []))
end
# Mantiene la relación inversa si existe. # Mantiene la relación inversa si existe.
# #
# La relación belongs_to se mantiene actualizada en la modificación # La relación belongs_to se mantiene actualizada en la modificación
@ -52,39 +19,33 @@ class MetadataHasAndBelongsToMany < MetadataBelongsTo
# 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
return true unless changed? # XXX: No usamos super
self[:value] = sanitize value self[:value] = sanitize value
return true unless inverse? && !included? return true unless changed?
return true unless inverse?
(belonged_to - belongs_to).each do |p| (had_many - has_many).each do |remove|
p[inverse].value.delete post.uuid.value remove[inverse]&.value&.delete post.uuid.value
end end
(belongs_to - belonged_to).each do |p| (has_many - had_many).each do |add|
p[inverse].value << post.uuid.value next unless add[inverse]
next if add[inverse].value.include? post.uuid.value
add[inverse].value << post.uuid.value
end end
true true
end end
def sanitize(sanitizable) private
sanitizable.map do |v|
super v
end
end
def post_exists? # Igual que en MetadataRelatedPosts
return true if empty? && can_be_empty? # TODO: Mover a un módulo
def sanitize(uuid)
!belongs_to.empty? super(uuid.map do |u|
end u.to_s.gsub(/[^a-f0-9\-]/i, '')
end)
# Todos los artículos relacionados incluyen a este?
def included?
belongs_to.map do |p|
p[inverse].value.include? post.uuid.value
end.all?
end end
end end

View file

@ -6,22 +6,32 @@
# 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
# Todos los Post relacionados según la relación remota # Invalidar la relación anterior
def has_many_remote def value_was=(new_value)
@has_many_remote ||= posts.where(inverse => post.uuid.value) @had_many = nil
@has_many = nil
super(new_value)
end
def validate
super
errors << I18n.t('metadata.has_many.missing_posts') unless posts_exist?
errors.empty?
end end
# Todos los Post relacionados # Todos los Post relacionados
def has_many def has_many
@has_many ||= {} @has_many ||= posts.where(uuid: value)
@has_many[value.hash.to_s] ||= posts.where(uuid: value)
end end
# La relación anterior # La relación anterior
def had_many def had_many
return [] if document.data[name.to_s].blank? return [] if value_was.blank?
@had_many ||= posts.where(uuid: document.data[name.to_s]) @had_many ||= posts.where(uuid: value_was)
end end
def inverse? def inverse?
@ -38,18 +48,17 @@ class MetadataHasMany < MetadataRelatedPosts
# Actualizar las relaciones inversas. Hay que buscar la diferencia # Actualizar las relaciones inversas. Hay que buscar la diferencia
# entre had y has_many. # entre had y has_many.
def save def save
super
return true unless changed? return true unless changed?
self[:value] = sanitize value
return true unless inverse? return true unless inverse?
(had_many - has_many).each do |remove| (had_many - has_many).each do |remove|
remove[inverse].value = remove[inverse].default_value remove[inverse]&.value = remove[inverse].default_value
end end
(has_many - had_many).each do |add| (has_many - had_many).each do |add|
add[inverse].value = post.uuid.value add[inverse]&.value = post.uuid.value
end end
true true
@ -63,11 +72,7 @@ class MetadataHasMany < MetadataRelatedPosts
@related_methods ||= %i[has_many had_many].freeze @related_methods ||= %i[has_many had_many].freeze
end end
private def posts_exist?
has_many.size == sanitize(value).size
def sanitize(sanitizable)
sanitizable.map do |uuid|
uuid.gsub(/[^a-f0-9\-]/, '')
end
end end
end end

View file

@ -6,8 +6,12 @@ class MetadataLang < MetadataTemplate
super || I18n.locale super || I18n.locale
end end
def value_from_document
document.collection.label
end
def value def value
self[:value] ||= document.collection.label || default_value self[:value] ||= value_from_document || default_value
end end
def values def values

View file

@ -7,8 +7,12 @@ class MetadataPath < MetadataTemplate
File.join(site.path, "_#{lang}", "#{date}-#{slug}#{ext}") File.join(site.path, "_#{lang}", "#{date}-#{slug}#{ext}")
end end
# El valor no vuelve desde el documento
def value_from_document
document.path
end
def value def value
@value_was ||= default_value
self[:value] = default_value self[:value] = default_value
end end
alias absolute value alias absolute value

View file

@ -31,4 +31,10 @@ class MetadataRelatedPosts < MetadataArray
def filter def filter
layout.metadata.dig(name, 'filter')&.to_h&.symbolize_keys || {} layout.metadata.dig(name, 'filter')&.to_h&.symbolize_keys || {}
end end
def sanitize(uuid)
super(uuid.map do |u|
u.to_s.gsub(/[^a-f0-9\-]/i, '')
end)
end
end end

View file

@ -25,11 +25,7 @@ require 'jekyll/utils'
class MetadataSlug < MetadataTemplate class MetadataSlug < MetadataTemplate
# Trae el slug desde el título si existe o una string al azar # Trae el slug desde el título si existe o una string al azar
def default_value def default_value
if title title ? Jekyll::Utils.slugify(title) : SecureRandom.uuid
Jekyll::Utils.slugify(title)
else
SecureRandom.hex
end
end end
def value def value
@ -40,6 +36,9 @@ class MetadataSlug < MetadataTemplate
# Devuelve el título a menos que sea privado y no esté vacío # Devuelve el título a menos que sea privado y no esté vacío
def title def title
post.title&.value&.to_s unless post.title.private? && !post.title&.value&.blank? return if post.title&.private?
return if post.title&.value&.blank?
post.title&.value&.to_s
end end
end end

View file

@ -7,8 +7,6 @@
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
:value, :help, :required, :errors, :post, :value, :help, :required, :errors, :post,
:layout, keyword_init: true) do :layout, keyword_init: true) do
attr_reader :value_was
# Queremos que los artículos nuevos siempre cacheen, si usamos el UUID # Queremos que los artículos nuevos siempre cacheen, si usamos el UUID
# siempre vamos a obtener un item nuevo. # siempre vamos a obtener un item nuevo.
def cache_key def cache_key
@ -25,13 +23,26 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
cache_key + '-' + cache_version cache_key + '-' + cache_version
end end
# XXX: Deberíamos sanitizar durante la asignación?
def value=(new_value) def value=(new_value)
@value_was = value @value_was = value
self[:value] = new_value self[:value] = new_value
end end
# Siempre obtener el valor actual y solo obtenerlo del documento una
# vez.
def value_was
return @value_was if instance_variable_defined? '@value_was'
@value_was = value_from_document
end
def value_from_document
@value_from_document ||= document.data[name.to_s]
end
def changed? def changed?
!value_was.nil? && value_was != value value_was != value
end end
# Obtiene el valor del JekyllDocument # Obtiene el valor del JekyllDocument
@ -59,7 +70,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
# Valor actual o por defecto. Al memoizarlo podemos modificarlo # Valor actual o por defecto. Al memoizarlo podemos modificarlo
# usando otros métodos que el de asignación. # usando otros métodos que el de asignación.
def value def value
self[:value] ||= if (data = document.data[name.to_s]).present? self[:value] ||= if (data = value_from_document).present?
private? ? decrypt(data) : data private? ? decrypt(data) : data
else else
default_value default_value
@ -112,6 +123,8 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
# En caso de que algún campo necesite realizar acciones antes de ser # En caso de que algún campo necesite realizar acciones antes de ser
# guardado # guardado
def save def save
return true unless changed?
self[:value] = sanitize value self[:value] = sanitize value
self[:value] = encrypt(value) if private? self[:value] = encrypt(value) if private?
@ -123,12 +136,12 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
end end
def related_methods def related_methods
raise NotImplementedError @related_methods ||= [].freeze
end end
# Determina si el campo es privado y debería ser cifrado # Determina si el campo es privado y debería ser cifrado
def private? def private?
!!layout.metadata.dig(name, 'private') layout.metadata.dig(name, 'private').present?
end end
private private
@ -172,7 +185,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
box.decrypt_str value.to_s box.decrypt_str value.to_s
rescue Lockbox::DecryptionError => e rescue Lockbox::DecryptionError => e
ExceptionNotifier.notify_exception(e) ExceptionNotifier.notify_exception(e, data: { site: site.name, post: post.path.absolute, name: name })
I18n.t('lockbox.help.decryption_error') I18n.t('lockbox.help.decryption_error')
end end

View file

@ -3,9 +3,11 @@
# 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.
# #
# rubocop:disable Metrics/ClassLength # * Los metadatos se tienen que cargar dinámicamente, solo usamos los
# rubocop:disable Style/MissingRespondToMissing # que necesitamos
class Post < OpenStruct #
#
class Post
# Atributos por defecto # Atributos por defecto
DEFAULT_ATTRIBUTES = %i[site document layout].freeze DEFAULT_ATTRIBUTES = %i[site document layout].freeze
# Otros atributos que no vienen en los metadatos # Otros atributos que no vienen en los metadatos
@ -13,6 +15,8 @@ class Post < OpenStruct
PUBLIC_ATTRIBUTES = %i[lang date uuid].freeze PUBLIC_ATTRIBUTES = %i[lang date uuid].freeze
ATTR_SUFFIXES = %w[? =].freeze ATTR_SUFFIXES = %w[? =].freeze
attr_reader :attributes, :errors, :layout, :site, :document
class << self class << self
# Obtiene el layout sin leer el Document # Obtiene el layout sin leer el Document
# #
@ -31,41 +35,20 @@ class Post < OpenStruct
# #
def initialize(**args) def initialize(**args)
default_attributes_missing(**args) default_attributes_missing(**args)
super(**args)
# Genera un método con todos los atributos disponibles # Genera un método con todos los atributos disponibles
self.attributes = layout.metadata.keys.map(&:to_sym) + PUBLIC_ATTRIBUTES @layout = args[:layout]
self.errors = {} @site = args[:site]
@document = args[:document]
@attributes = layout.attributes + PUBLIC_ATTRIBUTES
@errors = {}
@metadata = {}
# Genera un atributo por cada uno de los campos de la plantilla, # Inicializar valores
# MetadataFactory devuelve un tipo de campo por cada campo. A attributes.each do |attr|
# partir de ahí se pueden obtener los valores actuales y una lista public_send(attr)&.value = args[attr] if args.key?(attr)
# de valores por defecto.
#
# XXX: En el primer intento de hacerlo más óptimo, movimos esta
# lógica a instanciación bajo demanda, pero no solo no logramos
# optimizar sino que aumentamos el tiempo de carga :/
layout.metadata.each_pair do |name, template|
send "#{name}=".to_sym,
MetadataFactory.build(document: document,
post: self,
site: site,
name: name.to_sym,
value: args[name.to_sym],
layout: layout,
type: template['type'],
label: template['label'],
help: template['help'],
required: template['required'])
end end
# TODO: Llamar dinámicamente
load_lang!
load_slug!
load_date!
load_path!
load_uuid!
# XXX: No usamos Post#read porque a esta altura todavía no sabemos # XXX: No usamos Post#read porque a esta altura todavía no sabemos
# nada del Document # nada del Document
document.read! if File.exist? document.path document.read! if File.exist? document.path
@ -108,7 +91,8 @@ class Post < OpenStruct
html.css('img').each do |img| html.css('img').each do |img|
next if %r{\Ahttps?://} =~ img.attributes['src'] next if %r{\Ahttps?://} =~ img.attributes['src']
img.attributes['src'].value = Rails.application.routes.url_helpers.site_static_file_url(site, file: img.attributes['src'].value) img.attributes['src'].value = Rails.application.routes.url_helpers.site_static_file_url(site,
file: img.attributes['src'].value)
end end
# Notificar a les usuaries que están viendo una previsualización # Notificar a les usuaries que están viendo una previsualización
@ -152,29 +136,65 @@ class Post < OpenStruct
@modified_at ||= Time.now @modified_at ||= Time.now
end end
# Solo ejecuta la magia de OpenStruct si el campo existe en la def [](attr)
# plantilla public_send attr
# end
# XXX: Reemplazarlo por nuestro propio método, mantener todo lo demás
# compatible con OpenStruct # Define metadatos a demanda
# def method_missing(name, *_args)
# XXX: rubocop dice que tenemos que usar super cuando ya lo estamos
# usando...
def method_missing(mid, *args)
# Limpiar el nombre del atributo, para que todos los ayudantes # Limpiar el nombre del atributo, para que todos los ayudantes
# reciban el método en limpio # reciban el método en limpio
name = attribute_name mid
unless attribute? name unless attribute? name
raise NoMethodError, I18n.t('exceptions.post.no_method', raise NoMethodError, I18n.t('exceptions.post.no_method',
method: mid) method: name)
end end
# OpenStruct define_singleton_method(name) do
super(mid, *args) template = layout.metadata[name.to_s]
# Devolver lo mismo que devuelve el método después de definirlo @metadata[name] ||=
send(mid, *args) MetadataFactory.build(document: document,
post: self,
site: site,
name: name,
layout: layout,
type: template['type'],
label: template['label'],
help: template['help'],
required: template['required'])
end
public_send name
end
# TODO: Mover a method_missing
def slug
@metadata[:slug] ||= MetadataSlug.new(document: document, site: site, layout: layout, name: :slug, type: :slug,
post: self, required: true)
end
# TODO: Mover a method_missing
def date
@metadata[:date] ||= MetadataDocumentDate.new(document: document, site: site, layout: layout, name: :date,
type: :document_date, post: self, required: true)
end
# TODO: Mover a method_missing
def path
@metadata[:path] ||= MetadataPath.new(document: document, site: site, layout: layout, name: :path, type: :path,
post: self, required: true)
end
# TODO: Mover a method_missing
def lang
@metadata[:lang] ||= MetadataLang.new(document: document, site: site, layout: layout, name: :lang, type: :lang,
post: self, required: true)
end
# TODO: Mover a method_missing
def uuid
@metadata[:uuid] ||= MetadataUuid.new(document: document, site: site, layout: layout, name: :uuid, type: :uuid,
post: self, required: true)
end end
# Detecta si es un atributo válido o no, a partir de la tabla de la # Detecta si es un atributo válido o no, a partir de la tabla de la
@ -189,11 +209,14 @@ class Post < OpenStruct
included included
end end
# Devuelve los strong params para el layout # Devuelve los strong params para el layout.
#
# XXX: Nos gustaría no tener que instanciar Metadata acá, pero depende
# del valor por defecto que a su vez depende de Layout.
def params def params
attributes.map do |attr| attributes.map do |attr|
send(attr).to_param public_send(attr)&.to_param
end end.compact
end end
# Genera el post con metadatos en YAML # Genera el post con metadatos en YAML
@ -201,8 +224,8 @@ class Post < OpenStruct
# TODO: Cachear por un minuto # TODO: Cachear por un minuto
def full_content def full_content
body = '' body = ''
yaml = layout.metadata.keys.map(&:to_sym).map do |metadata| yaml = layout.attributes.map do |attr|
template = send(metadata) template = public_send attr
unless template.front_matter? unless template.front_matter?
body += "\n\n" body += "\n\n"
@ -212,7 +235,7 @@ class Post < OpenStruct
next if template.empty? next if template.empty?
[metadata.to_s, template.value] [attr.to_s, template.value]
end.compact.to_h end.compact.to_h
# TODO: Convertir a Metadata? # TODO: Convertir a Metadata?
@ -281,7 +304,7 @@ class Post < OpenStruct
# Detecta si el artículo es válido para guardar # Detecta si el artículo es válido para guardar
def valid? def valid?
self.errors = {} @errors = {}
layout.metadata.keys.map(&:to_sym).each do |metadata| layout.metadata.keys.map(&:to_sym).each do |metadata|
template = send(metadata) template = send(metadata)
@ -339,9 +362,7 @@ class Post < OpenStruct
# Levanta un error si al construir el artículo no pasamos un atributo. # Levanta un error si al construir el artículo no pasamos un atributo.
def default_attributes_missing(**args) def default_attributes_missing(**args)
DEFAULT_ATTRIBUTES.each do |attr| DEFAULT_ATTRIBUTES.each do |attr|
i18n = I18n.t("exceptions.post.#{attr}_missing") raise ArgumentError, I18n.t("exceptions.post.#{attr}_missing") unless args[attr].present?
raise ArgumentError, i18n unless args[attr].present?
end end
end end
@ -349,57 +370,11 @@ class Post < OpenStruct
document.data.fetch('usuaries', []) document.data.fetch('usuaries', [])
end end
# Obtiene el nombre del atributo a partir del nombre del método
def attribute_name(attr)
# XXX: Los simbolos van al final
@attribute_name_cache ||= {}
@attribute_name_cache[attr] ||= ATTR_SUFFIXES.reduce(attr.to_s) do |a, suffix|
a.chomp suffix
end.to_sym
end
def load_slug!
self.slug = MetadataSlug.new(document: document, site: site,
layout: layout, name: :slug, type: :slug,
post: self,
required: true)
end
def load_date!
self.date = MetadataDocumentDate.new(document: document, site: site,
layout: layout, name: :date,
type: :document_date,
post: self,
required: true)
end
def load_path!
self.path = MetadataPath.new(document: document, site: site,
layout: layout, name: :path,
type: :path, post: self,
required: true)
end
def load_lang!
self.lang = MetadataLang.new(document: document, site: site,
layout: layout, name: :lang,
type: :lang, post: self,
required: true)
end
def load_uuid!
self.uuid = MetadataUuid.new(document: document, site: site,
layout: layout, name: :uuid,
type: :uuid, post: self,
required: true)
end
# Ejecuta la acción de guardado en cada atributo # Ejecuta la acción de guardado en cada atributo
# TODO: Solo guardar los que se modificaron
def save_attributes! def save_attributes!
attributes.map do |attr| attributes.map do |attr|
send(attr).save public_send(attr).save
end.all? end.all?
end end
end end
# rubocop:enable Metrics/ClassLength
# rubocop:enable Style/MissingRespondToMissing

View file

@ -4,10 +4,11 @@
# artículos como si estuviésemos usando ActiveRecord. # artículos como si estuviésemos usando ActiveRecord.
class PostRelation < Array class PostRelation < Array
# No necesitamos cambiar el sitio # No necesitamos cambiar el sitio
attr_reader :site attr_reader :site, :lang
def initialize(site:) def initialize(site:, lang:)
@site = site @site = site
@lang = lang
# Proseguimos la inicialización sin valores por defecto # Proseguimos la inicialización sin valores por defecto
super() super()
end end
@ -15,7 +16,7 @@ class PostRelation < Array
# Genera un artículo nuevo con los parámetros que le pasemos y lo suma # Genera un artículo nuevo con los parámetros que le pasemos y lo suma
# al array # al array
def build(**args) def build(**args)
args[:lang] ||= I18n.locale args[:lang] = lang
args[:document] ||= build_document(collection: args[:lang]) args[:document] ||= build_document(collection: args[:lang])
args[:layout] = build_layout(args[:layout]) args[:layout] = build_layout(args[:layout])
@ -94,7 +95,7 @@ class PostRelation < Array
@where ||= {} @where ||= {}
@where[args.hash.to_s] ||= begin @where[args.hash.to_s] ||= begin
PostRelation.new(site: site).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)
@ -123,7 +124,7 @@ class PostRelation < Array
# @return [PostRelation] # @return [PostRelation]
alias array_select select alias array_select select
def select(&block) def select(&block)
PostRelation.new(site: site).concat array_select(&block) PostRelation.new(site: site, lang: lang).concat array_select(&block)
end end
# Intenta guardar todos y devuelve true si pudo # Intenta guardar todos y devuelve true si pudo

View file

@ -225,6 +225,7 @@ class Site < ApplicationRecord
# Traemos los posts del idioma actual por defecto # Traemos los posts del idioma actual por defecto
lang ||= I18n.locale lang ||= I18n.locale
lang = lang.to_sym
# Crea un Struct dinámico con los valores de los locales, si # Crea un Struct dinámico con los valores de los locales, si
# llegamos a pasar un idioma que no existe vamos a tener una # llegamos a pasar un idioma que no existe vamos a tener una
@ -233,7 +234,7 @@ class Site < ApplicationRecord
return @posts[lang] unless @posts[lang].blank? return @posts[lang] unless @posts[lang].blank?
@posts[lang] = PostRelation.new site: self @posts[lang] = PostRelation.new site: self, lang: lang
# No fallar si no existe colección para este idioma # No fallar si no existe colección para este idioma
# XXX: queremos fallar silenciosamente? # XXX: queremos fallar silenciosamente?
@ -250,7 +251,7 @@ class Site < ApplicationRecord
# #
# @return PostRelation # @return PostRelation
def docs def docs
@docs ||= PostRelation.new(site: self).push(locales.flat_map do |locale| @docs ||= PostRelation.new(site: self, lang: :docs).push(locales.flat_map do |locale|
posts(lang: locale) posts(lang: locale)
end).flatten! end).flatten!
end end

View file

@ -42,6 +42,7 @@ class Site
metadata = doc.public_send(field) metadata = doc.public_send(field)
next if metadata.value['path'].blank? next if metadata.value['path'].blank?
next if ActiveStorage::Blob.find_by(key: metadata.key_from_path)
path = Pathname.new(metadata.value['path']) path = Pathname.new(metadata.value['path'])

View file

@ -36,6 +36,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
post.usuaries << usuarie post.usuaries << usuarie
params[:post][:draft] = true if site.invitade? usuarie params[:post][:draft] = true if site.invitade? usuarie
# Es importante que el artículo se guarde primero y luego los
# relacionados.
commit(action: :updated, file: update_related_posts) if post.update(post_params) commit(action: :updated, file: update_related_posts) if post.update(post_params)
# Devolver el post aunque no se haya salvado para poder rescatar los # Devolver el post aunque no se haya salvado para poder rescatar los
@ -111,23 +113,24 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
# Actualiza los artículos relacionados según los métodos que los # Actualiza los artículos relacionados según los métodos que los
# metadatos declaren. # metadatos declaren.
# #
# Este método se asegura que todos los artículos se guardan una sola
# vez.
#
# @return [Array] Lista de archivos modificados # @return [Array] Lista de archivos modificados
def update_related_posts def update_related_posts
files = [post.path.absolute] posts = Set.new
post.attributes.each do |a| post.attributes.each do |a|
next unless post[a].related_posts?
post[a].related_methods.each do |m| post[a].related_methods.each do |m|
next unless post[a].respond_to? m next unless post[a].respond_to? m
# La respuesta puede ser una PostRelation también # La respuesta puede ser una PostRelation también
[post[a].public_send(m)].flatten.compact.uniq.each do |p| posts.merge [post[a].public_send(m)].flatten.compact
files << p.path.absolute if p.save(validate: false)
end
end end
end end
files posts.map do |p|
p.path.absolute if p.save(validate: false)
end.compact << post.path.absolute
end end
end end

View file

@ -2,5 +2,5 @@
%th= post_label_t(attribute, post: post) %th= post_label_t(attribute, post: post)
%td %td
%ul{ dir: dir, lang: locale } %ul{ dir: dir, lang: locale }
- metadata.belongs_to.each do |p| - metadata.has_many.each do |p|
%li= link_to p.title.value, site_post_path(site, p.id) %li= link_to p.title.value, site_post_path(site, p.id)

View file

@ -1,5 +1,6 @@
.form-group .form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post) = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= hidden_field_tag "#{base}[#{attribute}][]", ''
.taggable{ dir: dir, lang: locale, data: { values: metadata.value.to_json, .taggable{ dir: dir, lang: locale, data: { values: metadata.value.to_json,
name: "#{base}[#{attribute}][]", list: id_for_datalist(attribute), name: "#{base}[#{attribute}][]", list: id_for_datalist(attribute),

View file

@ -1,13 +1,7 @@
.form-group .form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post) = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= text_field base, attribute, value: metadata.value, = select_tag(plain_field_name_for(base, attribute),
dir: dir, lang: locale, list: id_for_datalist(attribute), options_for_select(metadata.values, metadata.value),
pattern: metadata.values.values.join('|'), autocomplete: 'off', **field_options(attribute, metadata), include_blank: true)
**field_options(attribute, metadata)
= render 'posts/attribute_feedback', = render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata post: post, attribute: attribute, metadata: metadata
-# TODO: Ocultar el UUID
%datalist{ id: id_for_datalist(attribute) }
- metadata.values.each_pair do |key, value|
%option{ value: value }= key

View file

@ -1,6 +1,7 @@
.form-group .form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post) = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= date_field base, attribute, value: metadata.value.to_date.strftime('%F'), = date_field base, attribute, value: metadata.value.to_date.strftime('%F'),
**field_options(attribute, metadata) **field_options(attribute, metadata), pattern: '\d{4}-\d{2}-\d{2}',
data: { 'pattern-mismatch': t('metadata.date.invalid_format') }
= render 'posts/attribute_feedback', = render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata post: post, attribute: attribute, metadata: metadata

View file

@ -1,6 +1,7 @@
.form-group .form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post) = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= date_field base, attribute, value: metadata.value.strftime('%F'), = date_field base, attribute, value: metadata.value.strftime('%F'),
**field_options(attribute, metadata) **field_options(attribute, metadata), pattern: '\d{4}-\d{2}-\d{2}',
data: { 'pattern-mismatch': t('metadata.date.invalid_format') }
= render 'posts/attribute_feedback', = render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata post: post, attribute: attribute, metadata: metadata

View file

@ -34,6 +34,6 @@
= text_field(*field_name_for(base, attribute, :description), = text_field(*field_name_for(base, attribute, :description),
value: metadata.value['description'], value: metadata.value['description'],
dir: dir, lang: locale, dir: dir, lang: locale,
**field_options(attribute, metadata)) **field_options(attribute, metadata, required: false))
= render 'posts/attribute_feedback', = render 'posts/attribute_feedback',
post: post, attribute: [attribute, :description], metadata: metadata post: post, attribute: [attribute, :description], metadata: metadata

View file

@ -1,5 +1,6 @@
.form-group .form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post) = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= hidden_field_tag "#{base}[#{attribute}][]", ''
.mapable{ dir: dir, lang: locale, .mapable{ dir: dir, lang: locale,
data: { values: metadata.value.to_json, data: { values: metadata.value.to_json,

View file

@ -1,5 +1,6 @@
.form-group .form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post) = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= hidden_field_tag "#{base}[#{attribute}][]", ''
.mapable{ dir: dir, lang: locale, .mapable{ dir: dir, lang: locale,
data: { values: metadata.value.to_json, data: { values: metadata.value.to_json,

View file

@ -32,7 +32,7 @@
= text_field(*field_name_for(base, attribute, :description), = text_field(*field_name_for(base, attribute, :description),
value: metadata.value['description'], value: metadata.value['description'],
dir: dir, lang: locale, dir: dir, lang: locale,
**field_options(attribute, metadata)) **field_options(attribute, metadata, required: false))
= render 'posts/attribute_feedback', = render 'posts/attribute_feedback',
post: post, attribute: [attribute, :description], metadata: metadata post: post, attribute: [attribute, :description], metadata: metadata

View file

@ -48,6 +48,10 @@ en:
end_in_the_past: "Event end can't happen before the start" end_in_the_past: "Event end can't happen before the start"
belongs_to: belongs_to:
missing_post: "Couldn't find the related post" missing_post: "Couldn't find the related post"
has_many:
missing_posts: "Couldn't find some related posts"
date:
invalid_format: "It seems that your browser doesn't support dates and the date is on the incorrect format, please use yyyy-mm-dd, ie. 2021-01-31"
exceptions: exceptions:
post: post:
site_missing: 'Needs an instance of Site' site_missing: 'Needs an instance of Site'
@ -460,7 +464,7 @@ en:
label: Language label: Language
date: date:
label: Date label: Date
help: Publication date for this post. If you use a date in the future the post won't be published until then. help: Date for this post. If you use a date in the future the post won't be published until you publish changes on that day.
required: required:
label: ' (required)' label: ' (required)'
feedback: 'This field cannot be empty!' feedback: 'This field cannot be empty!'

View file

@ -48,6 +48,10 @@ es:
end_in_the_past: 'El fin del evento no puede ser anterior al comienzo' end_in_the_past: 'El fin del evento no puede ser anterior al comienzo'
belongs_to: belongs_to:
missing_post: 'No se pudo encontrar el artículo relacionado' missing_post: 'No se pudo encontrar el artículo relacionado'
has_many:
missing_posts: 'No se pudieron encontrar algunos artículos relacionados'
date:
invalid_format: 'Parece que tu navegador no soporta fechas y la fecha no está en el formato correcto, por favor usa aaaa-mm-dd, por ejemplo: 2021-01-31'
exceptions: exceptions:
post: post:
site_missing: 'Necesita una instancia de Site' site_missing: 'Necesita una instancia de Site'
@ -469,7 +473,7 @@ es:
label: Idioma label: Idioma
date: date:
label: Fecha label: Fecha
help: La fecha de publicación del artículo. Si colocas una fecha en el futuro no se publicará hasta ese día. help: La fecha del artículo. Si colocas una fecha en el futuro no se publicará hasta que publiques cambios ese día.
required: required:
label: ' (requerido)' label: ' (requerido)'
feedback: '¡Este campo no puede estar vacío!' feedback: '¡Este campo no puede estar vacío!'

View file

@ -143,21 +143,26 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
end end
test 'se pueden reordenar' do test 'se pueden reordenar' do
lang = I18n.available_locales.sample lang = { lang: @site.locales.sample }
posts = @site.posts(lang: lang)
(rand * 10).round.times do
@site.posts(**lang).create title: SecureRandom.hex, description: SecureRandom.hex
end
posts = @site.posts(**lang)
reorder = Hash[posts.map { |p| p.uuid.value }.shuffle.each_with_index.to_a] reorder = Hash[posts.map { |p| p.uuid.value }.shuffle.each_with_index.to_a]
post site_posts_reorder_url(@site), post site_posts_reorder_url(@site),
headers: @authorization, headers: @authorization,
params: { post: { lang: lang, reorder: reorder } } params: { post: { lang: lang[:lang], reorder: reorder } }
@site = Site.find @site.id @site = Site.find @site.id
assert_equal I18n.t('post_service.reorder'),
@site.repository.rugged.head.target.message
assert_equal reorder, assert_equal reorder,
Hash[@site.posts(lang: lang).map do |p| Hash[@site.posts(**lang).map do |p|
[p.uuid.value, p.order.value] [p.uuid.value, p.order.value]
end] end]
assert_equal I18n.t('post_service.reorder'),
@site.repository.rugged.head.target.message
end end
end end

View file

@ -95,11 +95,6 @@ class PostTest < ActiveSupport::TestCase
end end
end end
test 'attribute_name' do
assert_equal :hola, @post.send(:attribute_name, :hola)
assert_equal :hola, @post.send(:attribute_name, :hola?)
end
test 'se puede cambiar el slug' do test 'se puede cambiar el slug' do
@post.title.value = SecureRandom.hex @post.title.value = SecureRandom.hex
assert_not @post.slug.changed? assert_not @post.slug.changed?