Merge branch 'rails' into void/editor
This commit is contained in:
commit
0b86702bdb
38 changed files with 378 additions and 357 deletions
6
Gemfile
6
Gemfile
|
@ -5,8 +5,11 @@
|
|||
# tiempo buscando soporte para musl
|
||||
if ENV['RAILS_ENV'] == 'production'
|
||||
source 'https://gems.sutty.nl'
|
||||
ruby '2.7.2'
|
||||
else
|
||||
source 'https://rubygems.org'
|
||||
# Cambiar en Dockerfile también
|
||||
ruby '2.7.1'
|
||||
end
|
||||
|
||||
git_source(:github) do |repo_name|
|
||||
|
@ -14,8 +17,6 @@ git_source(:github) do |repo_name|
|
|||
"https://github.com/#{repo_name}.git"
|
||||
end
|
||||
|
||||
# Cambiar en Dockerfile también
|
||||
ruby '2.7.2'
|
||||
|
||||
gem 'dotenv-rails', require: 'dotenv/rails-now'
|
||||
|
||||
|
@ -71,6 +72,7 @@ gem 'redis', require: %w[redis redis/connection/hiredis]
|
|||
gem 'redis-rails'
|
||||
gem 'rubyzip'
|
||||
gem 'rugged'
|
||||
gem 'concurrent-ruby-ext'
|
||||
gem 'sucker_punch'
|
||||
gem 'symbol-fstring', require: 'fstring/all'
|
||||
gem 'terminal-table'
|
||||
|
|
132
Gemfile.lock
132
Gemfile.lock
|
@ -10,60 +10,60 @@ GEM
|
|||
remote: https://rubygems.org/
|
||||
remote: https://gems.sutty.nl/
|
||||
specs:
|
||||
actioncable (6.1.1)
|
||||
actionpack (= 6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
actioncable (6.1.2.1)
|
||||
actionpack (= 6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.1)
|
||||
actionpack (= 6.1.1)
|
||||
activejob (= 6.1.1)
|
||||
activerecord (= 6.1.1)
|
||||
activestorage (= 6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
actionmailbox (6.1.2.1)
|
||||
actionpack (= 6.1.2.1)
|
||||
activejob (= 6.1.2.1)
|
||||
activerecord (= 6.1.2.1)
|
||||
activestorage (= 6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.1)
|
||||
actionpack (= 6.1.1)
|
||||
actionview (= 6.1.1)
|
||||
activejob (= 6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
actionmailer (6.1.2.1)
|
||||
actionpack (= 6.1.2.1)
|
||||
actionview (= 6.1.2.1)
|
||||
activejob (= 6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.1)
|
||||
actionview (= 6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
actionpack (6.1.2.1)
|
||||
actionview (= 6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.1)
|
||||
actionpack (= 6.1.1)
|
||||
activerecord (= 6.1.1)
|
||||
activestorage (= 6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
actiontext (6.1.2.1)
|
||||
actionpack (= 6.1.2.1)
|
||||
activerecord (= 6.1.2.1)
|
||||
activestorage (= 6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
actionview (6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
activejob (6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
activerecord (6.1.1)
|
||||
activemodel (= 6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
activestorage (6.1.1)
|
||||
actionpack (= 6.1.1)
|
||||
activejob (= 6.1.1)
|
||||
activerecord (= 6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
activemodel (6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
activerecord (6.1.2.1)
|
||||
activemodel (= 6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
activestorage (6.1.2.1)
|
||||
actionpack (= 6.1.2.1)
|
||||
activejob (= 6.1.2.1)
|
||||
activerecord (= 6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
marcel (~> 0.3.1)
|
||||
mimemagic (~> 0.3.2)
|
||||
activesupport (6.1.1)
|
||||
activesupport (6.1.2.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
|
@ -86,7 +86,7 @@ GEM
|
|||
bcrypt (3.1.16)
|
||||
benchmark-ips (2.8.4)
|
||||
bindex (0.8.1)
|
||||
blazer (2.4.1)
|
||||
blazer (2.4.2)
|
||||
activerecord (>= 5)
|
||||
chartkick (>= 3.2)
|
||||
railties (>= 5)
|
||||
|
@ -108,18 +108,20 @@ GEM
|
|||
childprocess (3.0.0)
|
||||
coderay (1.1.3)
|
||||
colorator (1.1.0)
|
||||
commonmarker (0.21.1)
|
||||
commonmarker (0.21.2)
|
||||
ruby-enum (~> 0.5)
|
||||
concurrent-ruby (1.1.8)
|
||||
concurrent-ruby-ext (1.1.8)
|
||||
concurrent-ruby (= 1.1.8)
|
||||
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)
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
dead_end (1.1.4)
|
||||
derailed_benchmarks (2.0.0)
|
||||
derailed_benchmarks (2.0.1)
|
||||
benchmark-ips (~> 2)
|
||||
dead_end
|
||||
get_process_mem (~> 0)
|
||||
|
@ -127,6 +129,7 @@ GEM
|
|||
memory_profiler (>= 0, < 2)
|
||||
mini_histogram (>= 0.3.0)
|
||||
rack (>= 1)
|
||||
rack-test
|
||||
rake (> 10, < 14)
|
||||
ruby-statistics (>= 2.1)
|
||||
thor (>= 0.19, < 2)
|
||||
|
@ -287,7 +290,7 @@ GEM
|
|||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
ruby_dep (~> 1.2)
|
||||
lockbox (0.6.1)
|
||||
lockbox (0.6.2)
|
||||
lograge (0.11.2)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
|
@ -313,7 +316,7 @@ GEM
|
|||
jekyll-feed (~> 0.9)
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
minitest (5.14.3)
|
||||
mobility (1.0.5)
|
||||
mobility (1.1.0)
|
||||
i18n (>= 0.6.10, < 2)
|
||||
request_store (~> 1.0)
|
||||
netaddr (2.0.4)
|
||||
|
@ -331,7 +334,7 @@ GEM
|
|||
popper_js (1.16.0)
|
||||
prometheus_exporter (0.7.0)
|
||||
webrick
|
||||
pry (0.13.1)
|
||||
pry (0.14.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
public_suffix (4.0.6)
|
||||
|
@ -360,20 +363,20 @@ GEM
|
|||
jekyll-relative-urls (~> 0.0)
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
jekyll-turbolinks (~> 0)
|
||||
rails (6.1.1)
|
||||
actioncable (= 6.1.1)
|
||||
actionmailbox (= 6.1.1)
|
||||
actionmailer (= 6.1.1)
|
||||
actionpack (= 6.1.1)
|
||||
actiontext (= 6.1.1)
|
||||
actionview (= 6.1.1)
|
||||
activejob (= 6.1.1)
|
||||
activemodel (= 6.1.1)
|
||||
activerecord (= 6.1.1)
|
||||
activestorage (= 6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
rails (6.1.2.1)
|
||||
actioncable (= 6.1.2.1)
|
||||
actionmailbox (= 6.1.2.1)
|
||||
actionmailer (= 6.1.2.1)
|
||||
actionpack (= 6.1.2.1)
|
||||
actiontext (= 6.1.2.1)
|
||||
actionview (= 6.1.2.1)
|
||||
activejob (= 6.1.2.1)
|
||||
activemodel (= 6.1.2.1)
|
||||
activerecord (= 6.1.2.1)
|
||||
activestorage (= 6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.1)
|
||||
railties (= 6.1.2.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
|
@ -385,9 +388,9 @@ GEM
|
|||
railties (>= 6.0.0, < 7)
|
||||
rails_warden (0.6.0)
|
||||
warden (>= 1.2.0)
|
||||
railties (6.1.1)
|
||||
actionpack (= 6.1.1)
|
||||
activesupport (= 6.1.1)
|
||||
railties (6.1.2.1)
|
||||
actionpack (= 6.1.2.1)
|
||||
activesupport (= 6.1.2.1)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (~> 1.0)
|
||||
|
@ -457,7 +460,7 @@ GEM
|
|||
i18n
|
||||
ruby-filemagic (0.7.2)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-statistics (2.1.2)
|
||||
ruby-statistics (2.1.3)
|
||||
ruby-vips (2.0.17)
|
||||
ffi (~> 1.9)
|
||||
ruby_dep (1.5.0)
|
||||
|
@ -537,7 +540,7 @@ GEM
|
|||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
thor (1.1.0)
|
||||
tilt (2.0.10)
|
||||
timecop (0.9.2)
|
||||
timecop (0.9.4)
|
||||
turbolinks (5.2.1)
|
||||
turbolinks-source (~> 5.2)
|
||||
turbolinks-source (5.2.0)
|
||||
|
@ -583,6 +586,7 @@ DEPENDENCIES
|
|||
brakeman
|
||||
capybara (~> 2.13)
|
||||
commonmarker
|
||||
concurrent-ruby-ext
|
||||
database_cleaner
|
||||
derailed_benchmarks
|
||||
devise
|
||||
|
@ -660,7 +664,7 @@ DEPENDENCIES
|
|||
yaml_db!
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.2p137
|
||||
ruby 2.7.1p83
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
|
|
8
Makefile
8
Makefile
|
@ -9,10 +9,10 @@ assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type
|
|||
|
||||
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
|
||||
|
||||
assets: public/packs/manifest.json
|
||||
assets: public/packs/manifest.json.br
|
||||
|
||||
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"
|
||||
|
@ -82,7 +82,7 @@ $(dirs):
|
|||
/etc/hosts: always
|
||||
@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 " api.$(SUTTY)$$" $@ || echo -e "127.0.0.1 api.$(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@
|
||||
@grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(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 panel.$(SUTTY)" | sudo tee -a $@
|
||||
|
||||
.PHONY: always
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
class ApplicationController < ActionController::Base
|
||||
include ExceptionHandler
|
||||
|
||||
protect_from_forgery with: :exception
|
||||
protect_from_forgery with: :exception, prepend: true
|
||||
|
||||
before_action :prepare_exception_notifier
|
||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||
|
|
|
@ -73,7 +73,7 @@ module ApplicationHelper
|
|||
|
||||
# Opciones por defecto para el campo de un formulario
|
||||
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]}",
|
||||
|
|
|
@ -107,4 +107,13 @@ document.addEventListener('turbolinks:load', () => {
|
|||
// Ocultar el area
|
||||
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 : '')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -22,15 +22,13 @@ class MaintenanceJob < ApplicationJob
|
|||
|
||||
# XXX: Parece que [0] es más rápido que []#first
|
||||
Usuarie.all.pluck(:email, :lang).each do |u|
|
||||
begin
|
||||
MaintenanceMailer.with(maintenance: maintenance,
|
||||
email: u[0],
|
||||
lang: u[1]).public_send(mailer).deliver_now
|
||||
rescue Net::SMTPServerBusy => e
|
||||
# Algunas direcciones no son válidas, no queremos detener el
|
||||
# envío pero sí enterarnos cuáles son
|
||||
ExceptionNotifier.notify_exception e
|
||||
end
|
||||
MaintenanceMailer.with(maintenance: maintenance,
|
||||
email: u[0],
|
||||
lang: u[1]).public_send(mailer).deliver_now
|
||||
rescue Net::SMTPServerBusy => e
|
||||
# Algunas direcciones no son válidas, no queremos detener el
|
||||
# envío pero sí enterarnos cuáles son
|
||||
ExceptionNotifier.notify_exception(e, data: { maintenance_id: maintenance_id, email: u[0] })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,10 @@ Layout = Struct.new(:site, :name, :meta, :metadata, keyword_init: true) do
|
|||
name.to_s
|
||||
end
|
||||
|
||||
def attributes
|
||||
@attributes ||= metadata.keys.map(&:to_sym)
|
||||
end
|
||||
|
||||
# Busca la traducción del Layout en el sitio o intenta humanizarlo
|
||||
# según Rails.
|
||||
#
|
||||
|
|
|
@ -7,15 +7,22 @@ class MetadataArray < MetadataTemplate
|
|||
super || []
|
||||
end
|
||||
|
||||
# Los Arrays no se pueden cifrar todavía
|
||||
# TODO: Cifrar y decifrar arrays
|
||||
def private?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: Sanitizar otros valores
|
||||
# XXX: Por qué eliminamos el punto del final?
|
||||
def sanitize(values)
|
||||
values.map do |v|
|
||||
if v.is_a? String
|
||||
super(v).sub(/\.\z/, '')
|
||||
else
|
||||
v
|
||||
case v
|
||||
when String then super(v).sub(/\.\z/, '')
|
||||
else v
|
||||
end
|
||||
end
|
||||
end.select(&:present?)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,13 @@
|
|||
# Almacena el UUID de otro Post y actualiza el valor en el Post
|
||||
# relacionado.
|
||||
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
|
||||
# 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
|
||||
# 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
|
||||
return super unless inverse? && !included?
|
||||
super
|
||||
|
||||
# Evitar que se cambie el orden de la relación
|
||||
belonged_to&.dig(inverse)&.value&.delete post.uuid.value if belonged_to != belongs_to
|
||||
# Si no hay relación inversa, no hacer nada más
|
||||
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
|
||||
end
|
||||
|
||||
# El Post actual está incluido en la relación inversa?
|
||||
def included?
|
||||
belongs_to[inverse].value.include? post.uuid.value
|
||||
belongs_to[inverse].value.include?(post.uuid.value)
|
||||
end
|
||||
|
||||
# Hay una relación inversa y el artículo existe?
|
||||
def inverse?
|
||||
inverse.present? && belongs_to.present?
|
||||
inverse.present?
|
||||
end
|
||||
|
||||
# El campo que es la relación inversa de este
|
||||
def inverse
|
||||
layout.metadata.dig name, 'inverse'
|
||||
@inverse ||= layout.metadata.dig(name, 'inverse')&.to_sym
|
||||
end
|
||||
|
||||
# El Post relacionado con este artículo
|
||||
|
@ -62,15 +69,14 @@ class MetadataBelongsTo < MetadataRelatedPosts
|
|||
def belongs_to
|
||||
return if value.blank?
|
||||
|
||||
@belongs_to ||= {}
|
||||
@belongs_to[value] ||= posts.find(value, uuid: true)
|
||||
@belongs_to ||= posts.find(value, uuid: true)
|
||||
end
|
||||
|
||||
# El anterior artículo relacionado
|
||||
# El artículo relacionado anterior
|
||||
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
|
||||
|
||||
def related_posts?
|
||||
|
@ -83,11 +89,13 @@ class MetadataBelongsTo < MetadataRelatedPosts
|
|||
|
||||
private
|
||||
|
||||
def sanitize(uuid)
|
||||
uuid.gsub(/[^a-f0-9\-]/, '')
|
||||
def post_exists?
|
||||
return true if sanitize(value).blank?
|
||||
|
||||
sanitize(value).present? && belongs_to.present?
|
||||
end
|
||||
|
||||
def post_exists?
|
||||
!value.blank? && posts.find(sanitize(value), uuid: true)
|
||||
def sanitize(uuid)
|
||||
uuid.to_s.gsub(/[^a-f0-9\-]/i, '')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,10 +7,14 @@ class MetadataDocumentDate < MetadataTemplate
|
|||
Date.today.to_time
|
||||
end
|
||||
|
||||
def value_from_document
|
||||
document.date
|
||||
end
|
||||
|
||||
# El valor puede ser un Date, Time o una String en el formato
|
||||
# "yyyy-mm-dd"
|
||||
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
|
||||
|
||||
|
|
|
@ -2,9 +2,14 @@
|
|||
|
||||
# Devuelve metadatos de cierto tipo
|
||||
class MetadataFactory
|
||||
def self.build(**args)
|
||||
@@factory_cache ||= {}
|
||||
@@factory_cache[args[:type]] ||= ('Metadata' + args[:type].to_s.camelcase).constantize
|
||||
@@factory_cache[args[:type]].new(args)
|
||||
class << self
|
||||
def build(**args)
|
||||
classify(args[:type]).new(**args)
|
||||
end
|
||||
|
||||
def classify(type)
|
||||
@factory_cache ||= {}
|
||||
@factory_cache[type] ||= ('Metadata' + type.to_s.camelcase).constantize
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -73,10 +73,14 @@ class MetadataFile < MetadataTemplate
|
|||
end
|
||||
end
|
||||
|
||||
def key_from_path
|
||||
path.dirname.basename.to_s
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def path?
|
||||
!value['path'].blank?
|
||||
value['path'].present?
|
||||
end
|
||||
|
||||
def filemagic
|
||||
|
@ -100,10 +104,6 @@ class MetadataFile < MetadataTemplate
|
|||
end
|
||||
end
|
||||
|
||||
def key_from_path
|
||||
path.dirname.basename.to_s
|
||||
end
|
||||
|
||||
# Hacemos un link duro para colocar el archivo dentro del repositorio
|
||||
# y no duplicar el espacio que ocupan. Esto requiere que ambos
|
||||
# directorios estén dentro del mismo punto de montaje.
|
||||
|
|
|
@ -10,40 +10,7 @@
|
|||
# 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
|
||||
# diferencia entre has_many y belongs_to.
|
||||
class MetadataHasAndBelongsToMany < MetadataBelongsTo
|
||||
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
|
||||
|
||||
class MetadataHasAndBelongsToMany < MetadataHasMany
|
||||
# Mantiene la relación inversa si existe.
|
||||
#
|
||||
# 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
|
||||
# quitarla de la relación remota, sino hay que agregarla.
|
||||
def save
|
||||
return true unless changed?
|
||||
|
||||
# XXX: No usamos super
|
||||
self[:value] = sanitize value
|
||||
|
||||
return true unless inverse? && !included?
|
||||
return true unless changed?
|
||||
return true unless inverse?
|
||||
|
||||
(belonged_to - belongs_to).each do |p|
|
||||
p[inverse].value.delete post.uuid.value
|
||||
(had_many - has_many).each do |remove|
|
||||
remove[inverse]&.value&.delete post.uuid.value
|
||||
end
|
||||
|
||||
(belongs_to - belonged_to).each do |p|
|
||||
p[inverse].value << post.uuid.value
|
||||
(has_many - had_many).each do |add|
|
||||
next unless add[inverse]
|
||||
next if add[inverse].value.include? post.uuid.value
|
||||
|
||||
add[inverse].value << post.uuid.value
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def sanitize(sanitizable)
|
||||
sanitizable.map do |v|
|
||||
super v
|
||||
end
|
||||
end
|
||||
private
|
||||
|
||||
def post_exists?
|
||||
return true if empty? && can_be_empty?
|
||||
|
||||
!belongs_to.empty?
|
||||
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?
|
||||
# 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
|
||||
|
|
|
@ -6,22 +6,32 @@
|
|||
# Localmente tenemos un Array de UUIDs. Remotamente tenemos una String
|
||||
# apuntando a un Post, que se mantiene actualizado como el actual.
|
||||
class MetadataHasMany < MetadataRelatedPosts
|
||||
# Todos los Post relacionados según la relación remota
|
||||
def has_many_remote
|
||||
@has_many_remote ||= posts.where(inverse => post.uuid.value)
|
||||
# Invalidar la relación anterior
|
||||
def value_was=(new_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
|
||||
|
||||
# Todos los Post relacionados
|
||||
def has_many
|
||||
@has_many ||= {}
|
||||
@has_many[value.hash.to_s] ||= posts.where(uuid: value)
|
||||
@has_many ||= posts.where(uuid: value)
|
||||
end
|
||||
|
||||
# La relación anterior
|
||||
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
|
||||
|
||||
def inverse?
|
||||
|
@ -38,18 +48,17 @@ class MetadataHasMany < MetadataRelatedPosts
|
|||
# Actualizar las relaciones inversas. Hay que buscar la diferencia
|
||||
# entre had y has_many.
|
||||
def save
|
||||
super
|
||||
|
||||
return true unless changed?
|
||||
|
||||
self[:value] = sanitize value
|
||||
|
||||
return true unless inverse?
|
||||
|
||||
(had_many - has_many).each do |remove|
|
||||
remove[inverse].value = remove[inverse].default_value
|
||||
remove[inverse]&.value = remove[inverse].default_value
|
||||
end
|
||||
|
||||
(has_many - had_many).each do |add|
|
||||
add[inverse].value = post.uuid.value
|
||||
add[inverse]&.value = post.uuid.value
|
||||
end
|
||||
|
||||
true
|
||||
|
@ -63,11 +72,7 @@ class MetadataHasMany < MetadataRelatedPosts
|
|||
@related_methods ||= %i[has_many had_many].freeze
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sanitize(sanitizable)
|
||||
sanitizable.map do |uuid|
|
||||
uuid.gsub(/[^a-f0-9\-]/, '')
|
||||
end
|
||||
def posts_exist?
|
||||
has_many.size == sanitize(value).size
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,8 +6,12 @@ class MetadataLang < MetadataTemplate
|
|||
super || I18n.locale
|
||||
end
|
||||
|
||||
def value_from_document
|
||||
document.collection.label
|
||||
end
|
||||
|
||||
def value
|
||||
self[:value] ||= document.collection.label || default_value
|
||||
self[:value] ||= value_from_document || default_value
|
||||
end
|
||||
|
||||
def values
|
||||
|
|
|
@ -7,8 +7,12 @@ class MetadataPath < MetadataTemplate
|
|||
File.join(site.path, "_#{lang}", "#{date}-#{slug}#{ext}")
|
||||
end
|
||||
|
||||
# El valor no vuelve desde el documento
|
||||
def value_from_document
|
||||
document.path
|
||||
end
|
||||
|
||||
def value
|
||||
@value_was ||= default_value
|
||||
self[:value] = default_value
|
||||
end
|
||||
alias absolute value
|
||||
|
|
|
@ -31,4 +31,10 @@ class MetadataRelatedPosts < MetadataArray
|
|||
def filter
|
||||
layout.metadata.dig(name, 'filter')&.to_h&.symbolize_keys || {}
|
||||
end
|
||||
|
||||
def sanitize(uuid)
|
||||
super(uuid.map do |u|
|
||||
u.to_s.gsub(/[^a-f0-9\-]/i, '')
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,11 +25,7 @@ require 'jekyll/utils'
|
|||
class MetadataSlug < MetadataTemplate
|
||||
# Trae el slug desde el título si existe o una string al azar
|
||||
def default_value
|
||||
if title
|
||||
Jekyll::Utils.slugify(title)
|
||||
else
|
||||
SecureRandom.hex
|
||||
end
|
||||
title ? Jekyll::Utils.slugify(title) : SecureRandom.uuid
|
||||
end
|
||||
|
||||
def value
|
||||
|
@ -40,6 +36,9 @@ class MetadataSlug < MetadataTemplate
|
|||
|
||||
# Devuelve el título a menos que sea privado y no esté vacío
|
||||
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
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||
:value, :help, :required, :errors, :post,
|
||||
:layout, keyword_init: true) do
|
||||
attr_reader :value_was
|
||||
|
||||
# Queremos que los artículos nuevos siempre cacheen, si usamos el UUID
|
||||
# siempre vamos a obtener un item nuevo.
|
||||
def cache_key
|
||||
|
@ -25,13 +23,26 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
|||
cache_key + '-' + cache_version
|
||||
end
|
||||
|
||||
# XXX: Deberíamos sanitizar durante la asignación?
|
||||
def value=(new_value)
|
||||
@value_was = value
|
||||
self[:value] = new_value
|
||||
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?
|
||||
!value_was.nil? && value_was != value
|
||||
value_was != value
|
||||
end
|
||||
|
||||
# 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
|
||||
# usando otros métodos que el de asignación.
|
||||
def value
|
||||
self[:value] ||= if (data = document.data[name.to_s]).present?
|
||||
self[:value] ||= if (data = value_from_document).present?
|
||||
private? ? decrypt(data) : data
|
||||
else
|
||||
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
|
||||
# guardado
|
||||
def save
|
||||
return true unless changed?
|
||||
|
||||
self[:value] = sanitize value
|
||||
self[:value] = encrypt(value) if private?
|
||||
|
||||
|
@ -123,12 +136,12 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
|||
end
|
||||
|
||||
def related_methods
|
||||
raise NotImplementedError
|
||||
@related_methods ||= [].freeze
|
||||
end
|
||||
|
||||
# Determina si el campo es privado y debería ser cifrado
|
||||
def private?
|
||||
!!layout.metadata.dig(name, 'private')
|
||||
layout.metadata.dig(name, 'private').present?
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -172,7 +185,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
|||
|
||||
box.decrypt_str value.to_s
|
||||
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')
|
||||
end
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
# Esta clase representa un post en un sitio jekyll e incluye métodos
|
||||
# para modificarlos y crear nuevos.
|
||||
#
|
||||
# rubocop:disable Metrics/ClassLength
|
||||
# rubocop:disable Style/MissingRespondToMissing
|
||||
class Post < OpenStruct
|
||||
# * Los metadatos se tienen que cargar dinámicamente, solo usamos los
|
||||
# que necesitamos
|
||||
#
|
||||
#
|
||||
class Post
|
||||
# Atributos por defecto
|
||||
DEFAULT_ATTRIBUTES = %i[site document layout].freeze
|
||||
# Otros atributos que no vienen en los metadatos
|
||||
|
@ -13,6 +15,8 @@ class Post < OpenStruct
|
|||
PUBLIC_ATTRIBUTES = %i[lang date uuid].freeze
|
||||
ATTR_SUFFIXES = %w[? =].freeze
|
||||
|
||||
attr_reader :attributes, :errors, :layout, :site, :document
|
||||
|
||||
class << self
|
||||
# Obtiene el layout sin leer el Document
|
||||
#
|
||||
|
@ -31,41 +35,20 @@ class Post < OpenStruct
|
|||
#
|
||||
def initialize(**args)
|
||||
default_attributes_missing(**args)
|
||||
super(**args)
|
||||
|
||||
# Genera un método con todos los atributos disponibles
|
||||
self.attributes = layout.metadata.keys.map(&:to_sym) + PUBLIC_ATTRIBUTES
|
||||
self.errors = {}
|
||||
@layout = args[:layout]
|
||||
@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,
|
||||
# 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.
|
||||
#
|
||||
# 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'])
|
||||
# Inicializar valores
|
||||
attributes.each do |attr|
|
||||
public_send(attr)&.value = args[attr] if args.key?(attr)
|
||||
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
|
||||
# nada del Document
|
||||
document.read! if File.exist? document.path
|
||||
|
@ -108,7 +91,8 @@ class Post < OpenStruct
|
|||
html.css('img').each do |img|
|
||||
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
|
||||
|
||||
# Notificar a les usuaries que están viendo una previsualización
|
||||
|
@ -152,29 +136,65 @@ class Post < OpenStruct
|
|||
@modified_at ||= Time.now
|
||||
end
|
||||
|
||||
# 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...
|
||||
def method_missing(mid, *args)
|
||||
def [](attr)
|
||||
public_send attr
|
||||
end
|
||||
|
||||
# Define metadatos a demanda
|
||||
def method_missing(name, *_args)
|
||||
# Limpiar el nombre del atributo, para que todos los ayudantes
|
||||
# reciban el método en limpio
|
||||
name = attribute_name mid
|
||||
|
||||
unless attribute? name
|
||||
raise NoMethodError, I18n.t('exceptions.post.no_method',
|
||||
method: mid)
|
||||
method: name)
|
||||
end
|
||||
|
||||
# OpenStruct
|
||||
super(mid, *args)
|
||||
define_singleton_method(name) do
|
||||
template = layout.metadata[name.to_s]
|
||||
|
||||
# Devolver lo mismo que devuelve el método después de definirlo
|
||||
send(mid, *args)
|
||||
@metadata[name] ||=
|
||||
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
|
||||
|
||||
# Detecta si es un atributo válido o no, a partir de la tabla de la
|
||||
|
@ -189,11 +209,14 @@ class Post < OpenStruct
|
|||
included
|
||||
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
|
||||
attributes.map do |attr|
|
||||
send(attr).to_param
|
||||
end
|
||||
public_send(attr)&.to_param
|
||||
end.compact
|
||||
end
|
||||
|
||||
# Genera el post con metadatos en YAML
|
||||
|
@ -201,8 +224,8 @@ class Post < OpenStruct
|
|||
# TODO: Cachear por un minuto
|
||||
def full_content
|
||||
body = ''
|
||||
yaml = layout.metadata.keys.map(&:to_sym).map do |metadata|
|
||||
template = send(metadata)
|
||||
yaml = layout.attributes.map do |attr|
|
||||
template = public_send attr
|
||||
|
||||
unless template.front_matter?
|
||||
body += "\n\n"
|
||||
|
@ -212,7 +235,7 @@ class Post < OpenStruct
|
|||
|
||||
next if template.empty?
|
||||
|
||||
[metadata.to_s, template.value]
|
||||
[attr.to_s, template.value]
|
||||
end.compact.to_h
|
||||
|
||||
# TODO: Convertir a Metadata?
|
||||
|
@ -281,7 +304,7 @@ class Post < OpenStruct
|
|||
|
||||
# Detecta si el artículo es válido para guardar
|
||||
def valid?
|
||||
self.errors = {}
|
||||
@errors = {}
|
||||
|
||||
layout.metadata.keys.map(&:to_sym).each do |metadata|
|
||||
template = send(metadata)
|
||||
|
@ -339,9 +362,7 @@ class Post < OpenStruct
|
|||
# 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?
|
||||
raise ArgumentError, I18n.t("exceptions.post.#{attr}_missing") unless args[attr].present?
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -349,57 +370,11 @@ class Post < OpenStruct
|
|||
document.data.fetch('usuaries', [])
|
||||
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
|
||||
# TODO: Solo guardar los que se modificaron
|
||||
def save_attributes!
|
||||
attributes.map do |attr|
|
||||
send(attr).save
|
||||
public_send(attr).save
|
||||
end.all?
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/ClassLength
|
||||
# rubocop:enable Style/MissingRespondToMissing
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
# artículos como si estuviésemos usando ActiveRecord.
|
||||
class PostRelation < Array
|
||||
# No necesitamos cambiar el sitio
|
||||
attr_reader :site
|
||||
attr_reader :site, :lang
|
||||
|
||||
def initialize(site:)
|
||||
def initialize(site:, lang:)
|
||||
@site = site
|
||||
@lang = lang
|
||||
# Proseguimos la inicialización sin valores por defecto
|
||||
super()
|
||||
end
|
||||
|
@ -15,7 +16,7 @@ class PostRelation < Array
|
|||
# Genera un artículo nuevo con los parámetros que le pasemos y lo suma
|
||||
# al array
|
||||
def build(**args)
|
||||
args[:lang] ||= I18n.locale
|
||||
args[:lang] = lang
|
||||
args[:document] ||= build_document(collection: args[:lang])
|
||||
args[:layout] = build_layout(args[:layout])
|
||||
|
||||
|
@ -94,7 +95,7 @@ class PostRelation < Array
|
|||
|
||||
@where ||= {}
|
||||
@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|
|
||||
next unless post.attribute?(attr)
|
||||
|
||||
|
@ -123,7 +124,7 @@ class PostRelation < Array
|
|||
# @return [PostRelation]
|
||||
alias array_select select
|
||||
def select(&block)
|
||||
PostRelation.new(site: site).concat array_select(&block)
|
||||
PostRelation.new(site: site, lang: lang).concat array_select(&block)
|
||||
end
|
||||
|
||||
# Intenta guardar todos y devuelve true si pudo
|
||||
|
|
|
@ -225,6 +225,7 @@ class Site < ApplicationRecord
|
|||
|
||||
# Traemos los posts del idioma actual por defecto
|
||||
lang ||= I18n.locale
|
||||
lang = lang.to_sym
|
||||
|
||||
# Crea un Struct dinámico con los valores de los locales, si
|
||||
# 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?
|
||||
|
||||
@posts[lang] = PostRelation.new site: self
|
||||
@posts[lang] = PostRelation.new site: self, lang: lang
|
||||
|
||||
# No fallar si no existe colección para este idioma
|
||||
# XXX: queremos fallar silenciosamente?
|
||||
|
@ -250,7 +251,7 @@ class Site < ApplicationRecord
|
|||
#
|
||||
# @return PostRelation
|
||||
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)
|
||||
end).flatten!
|
||||
end
|
||||
|
|
|
@ -42,6 +42,7 @@ class Site
|
|||
metadata = doc.public_send(field)
|
||||
|
||||
next if metadata.value['path'].blank?
|
||||
next if ActiveStorage::Blob.find_by(key: metadata.key_from_path)
|
||||
|
||||
path = Pathname.new(metadata.value['path'])
|
||||
|
||||
|
|
|
@ -36,6 +36,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
|||
post.usuaries << 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)
|
||||
|
||||
# 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
|
||||
# metadatos declaren.
|
||||
#
|
||||
# Este método se asegura que todos los artículos se guardan una sola
|
||||
# vez.
|
||||
#
|
||||
# @return [Array] Lista de archivos modificados
|
||||
def update_related_posts
|
||||
files = [post.path.absolute]
|
||||
posts = Set.new
|
||||
|
||||
post.attributes.each do |a|
|
||||
next unless post[a].related_posts?
|
||||
|
||||
post[a].related_methods.each do |m|
|
||||
next unless post[a].respond_to? m
|
||||
|
||||
# La respuesta puede ser una PostRelation también
|
||||
[post[a].public_send(m)].flatten.compact.uniq.each do |p|
|
||||
files << p.path.absolute if p.save(validate: false)
|
||||
end
|
||||
posts.merge [post[a].public_send(m)].flatten.compact
|
||||
end
|
||||
end
|
||||
|
||||
files
|
||||
posts.map do |p|
|
||||
p.path.absolute if p.save(validate: false)
|
||||
end.compact << post.path.absolute
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
%th= post_label_t(attribute, post: post)
|
||||
%td
|
||||
%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)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.form-group
|
||||
= 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,
|
||||
name: "#{base}[#{attribute}][]", list: id_for_datalist(attribute),
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
.form-group
|
||||
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
|
||||
= text_field base, attribute, value: metadata.value,
|
||||
dir: dir, lang: locale, list: id_for_datalist(attribute),
|
||||
pattern: metadata.values.values.join('|'), autocomplete: 'off',
|
||||
**field_options(attribute, metadata)
|
||||
= select_tag(plain_field_name_for(base, attribute),
|
||||
options_for_select(metadata.values, metadata.value),
|
||||
**field_options(attribute, metadata), include_blank: true)
|
||||
= render 'posts/attribute_feedback',
|
||||
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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.form-group
|
||||
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
|
||||
= 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',
|
||||
post: post, attribute: attribute, metadata: metadata
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.form-group
|
||||
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
|
||||
= 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',
|
||||
post: post, attribute: attribute, metadata: metadata
|
||||
|
|
|
@ -34,6 +34,6 @@
|
|||
= text_field(*field_name_for(base, attribute, :description),
|
||||
value: metadata.value['description'],
|
||||
dir: dir, lang: locale,
|
||||
**field_options(attribute, metadata))
|
||||
**field_options(attribute, metadata, required: false))
|
||||
= render 'posts/attribute_feedback',
|
||||
post: post, attribute: [attribute, :description], metadata: metadata
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.form-group
|
||||
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
|
||||
= hidden_field_tag "#{base}[#{attribute}][]", ''
|
||||
|
||||
.mapable{ dir: dir, lang: locale,
|
||||
data: { values: metadata.value.to_json,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.form-group
|
||||
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
|
||||
= hidden_field_tag "#{base}[#{attribute}][]", ''
|
||||
|
||||
.mapable{ dir: dir, lang: locale,
|
||||
data: { values: metadata.value.to_json,
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
= text_field(*field_name_for(base, attribute, :description),
|
||||
value: metadata.value['description'],
|
||||
dir: dir, lang: locale,
|
||||
**field_options(attribute, metadata))
|
||||
**field_options(attribute, metadata, required: false))
|
||||
= render 'posts/attribute_feedback',
|
||||
post: post, attribute: [attribute, :description], metadata: metadata
|
||||
|
||||
|
|
|
@ -48,6 +48,10 @@ en:
|
|||
end_in_the_past: "Event end can't happen before the start"
|
||||
belongs_to:
|
||||
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:
|
||||
post:
|
||||
site_missing: 'Needs an instance of Site'
|
||||
|
@ -460,7 +464,7 @@ en:
|
|||
label: Language
|
||||
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:
|
||||
label: ' (required)'
|
||||
feedback: 'This field cannot be empty!'
|
||||
|
|
|
@ -48,6 +48,10 @@ es:
|
|||
end_in_the_past: 'El fin del evento no puede ser anterior al comienzo'
|
||||
belongs_to:
|
||||
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:
|
||||
post:
|
||||
site_missing: 'Necesita una instancia de Site'
|
||||
|
@ -469,7 +473,7 @@ es:
|
|||
label: Idioma
|
||||
date:
|
||||
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:
|
||||
label: ' (requerido)'
|
||||
feedback: '¡Este campo no puede estar vacío!'
|
||||
|
|
|
@ -143,21 +143,26 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
|
|||
end
|
||||
|
||||
test 'se pueden reordenar' do
|
||||
lang = I18n.available_locales.sample
|
||||
posts = @site.posts(lang: lang)
|
||||
lang = { lang: @site.locales.sample }
|
||||
|
||||
(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]
|
||||
|
||||
post site_posts_reorder_url(@site),
|
||||
headers: @authorization,
|
||||
params: { post: { lang: lang, reorder: reorder } }
|
||||
params: { post: { lang: lang[:lang], reorder: reorder } }
|
||||
|
||||
@site = Site.find @site.id
|
||||
|
||||
assert_equal I18n.t('post_service.reorder'),
|
||||
@site.repository.rugged.head.target.message
|
||||
assert_equal reorder,
|
||||
Hash[@site.posts(lang: lang).map do |p|
|
||||
Hash[@site.posts(**lang).map do |p|
|
||||
[p.uuid.value, p.order.value]
|
||||
end]
|
||||
assert_equal I18n.t('post_service.reorder'),
|
||||
@site.repository.rugged.head.target.message
|
||||
end
|
||||
end
|
||||
|
|
|
@ -95,11 +95,6 @@ class PostTest < ActiveSupport::TestCase
|
|||
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
|
||||
@post.title.value = SecureRandom.hex
|
||||
assert_not @post.slug.changed?
|
||||
|
|
Loading…
Reference in a new issue