diff --git a/Gemfile b/Gemfile index f0736381..96ffa020 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index ae912f98..8d4da435 100644 --- a/Gemfile.lock +++ b/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 diff --git a/Makefile b/Makefile index 7e5cf70a..66638fbb 100644 --- a/Makefile +++ b/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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2f09ceab..9893daa6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a389327a..23b4c995 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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]}", diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 7295446d..4c232fd6 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -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 : '') + }) + }) }) diff --git a/app/jobs/maintenance_job.rb b/app/jobs/maintenance_job.rb index 114ff77b..4c411d0e 100644 --- a/app/jobs/maintenance_job.rb +++ b/app/jobs/maintenance_job.rb @@ -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 diff --git a/app/models/layout.rb b/app/models/layout.rb index c05a02f6..2d68273a 100644 --- a/app/models/layout.rb +++ b/app/models/layout.rb @@ -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. # diff --git a/app/models/metadata_array.rb b/app/models/metadata_array.rb index a1ff8c72..5c0b16f7 100644 --- a/app/models/metadata_array.rb +++ b/app/models/metadata_array.rb @@ -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 diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb index 21195470..0626ba0c 100644 --- a/app/models/metadata_belongs_to.rb +++ b/app/models/metadata_belongs_to.rb @@ -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 diff --git a/app/models/metadata_document_date.rb b/app/models/metadata_document_date.rb index 036877ae..39e68735 100644 --- a/app/models/metadata_document_date.rb +++ b/app/models/metadata_document_date.rb @@ -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 diff --git a/app/models/metadata_factory.rb b/app/models/metadata_factory.rb index 8cac1c86..c33f1f18 100644 --- a/app/models/metadata_factory.rb +++ b/app/models/metadata_factory.rb @@ -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 diff --git a/app/models/metadata_file.rb b/app/models/metadata_file.rb index fcabf05d..8fecf553 100644 --- a/app/models/metadata_file.rb +++ b/app/models/metadata_file.rb @@ -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. diff --git a/app/models/metadata_has_and_belongs_to_many.rb b/app/models/metadata_has_and_belongs_to_many.rb index e08c9917..f14827ec 100644 --- a/app/models/metadata_has_and_belongs_to_many.rb +++ b/app/models/metadata_has_and_belongs_to_many.rb @@ -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 diff --git a/app/models/metadata_has_many.rb b/app/models/metadata_has_many.rb index cf7a1b0a..61354011 100644 --- a/app/models/metadata_has_many.rb +++ b/app/models/metadata_has_many.rb @@ -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 diff --git a/app/models/metadata_lang.rb b/app/models/metadata_lang.rb index da13d902..93ef87cc 100644 --- a/app/models/metadata_lang.rb +++ b/app/models/metadata_lang.rb @@ -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 diff --git a/app/models/metadata_path.rb b/app/models/metadata_path.rb index 6dede4b5..3c93cca6 100644 --- a/app/models/metadata_path.rb +++ b/app/models/metadata_path.rb @@ -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 diff --git a/app/models/metadata_related_posts.rb b/app/models/metadata_related_posts.rb index 6712ce2b..324ba4ff 100644 --- a/app/models/metadata_related_posts.rb +++ b/app/models/metadata_related_posts.rb @@ -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 diff --git a/app/models/metadata_slug.rb b/app/models/metadata_slug.rb index 1f12475c..09da23f9 100644 --- a/app/models/metadata_slug.rb +++ b/app/models/metadata_slug.rb @@ -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 diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index e7e2edb8..8fa5a971 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -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 diff --git a/app/models/post.rb b/app/models/post.rb index 39de2db8..8fc11b9a 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -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 diff --git a/app/models/post_relation.rb b/app/models/post_relation.rb index 7ea9aef6..850a83dc 100644 --- a/app/models/post_relation.rb +++ b/app/models/post_relation.rb @@ -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 diff --git a/app/models/site.rb b/app/models/site.rb index 6c930836..4ec0b9f9 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -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 diff --git a/app/models/site/static_file_migration.rb b/app/models/site/static_file_migration.rb index 798601a5..614f63dd 100644 --- a/app/models/site/static_file_migration.rb +++ b/app/models/site/static_file_migration.rb @@ -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']) diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 571588cb..bc8def1c 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -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 diff --git a/app/views/posts/attribute_ro/_has_and_belongs_to_many.haml b/app/views/posts/attribute_ro/_has_and_belongs_to_many.haml index d7186718..d6b51a7a 100644 --- a/app/views/posts/attribute_ro/_has_and_belongs_to_many.haml +++ b/app/views/posts/attribute_ro/_has_and_belongs_to_many.haml @@ -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) diff --git a/app/views/posts/attributes/_array.haml b/app/views/posts/attributes/_array.haml index 73e96dea..da9ec654 100644 --- a/app/views/posts/attributes/_array.haml +++ b/app/views/posts/attributes/_array.haml @@ -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), diff --git a/app/views/posts/attributes/_belongs_to.haml b/app/views/posts/attributes/_belongs_to.haml index 06594742..2d17c3fb 100644 --- a/app/views/posts/attributes/_belongs_to.haml +++ b/app/views/posts/attributes/_belongs_to.haml @@ -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 diff --git a/app/views/posts/attributes/_date.haml b/app/views/posts/attributes/_date.haml index 38c4f500..1347c59c 100644 --- a/app/views/posts/attributes/_date.haml +++ b/app/views/posts/attributes/_date.haml @@ -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 diff --git a/app/views/posts/attributes/_document_date.haml b/app/views/posts/attributes/_document_date.haml index 65516718..1be997cc 100644 --- a/app/views/posts/attributes/_document_date.haml +++ b/app/views/posts/attributes/_document_date.haml @@ -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 diff --git a/app/views/posts/attributes/_file.haml b/app/views/posts/attributes/_file.haml index 56adb239..54f87b5b 100644 --- a/app/views/posts/attributes/_file.haml +++ b/app/views/posts/attributes/_file.haml @@ -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 diff --git a/app/views/posts/attributes/_has_and_belongs_to_many.haml b/app/views/posts/attributes/_has_and_belongs_to_many.haml index d39d124d..e36b6552 100644 --- a/app/views/posts/attributes/_has_and_belongs_to_many.haml +++ b/app/views/posts/attributes/_has_and_belongs_to_many.haml @@ -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, diff --git a/app/views/posts/attributes/_has_many.haml b/app/views/posts/attributes/_has_many.haml index d39d124d..e36b6552 100644 --- a/app/views/posts/attributes/_has_many.haml +++ b/app/views/posts/attributes/_has_many.haml @@ -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, diff --git a/app/views/posts/attributes/_image.haml b/app/views/posts/attributes/_image.haml index 06503700..2824b4c9 100644 --- a/app/views/posts/attributes/_image.haml +++ b/app/views/posts/attributes/_image.haml @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 7aa061c3..6e050249 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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!' diff --git a/config/locales/es.yml b/config/locales/es.yml index 7a35ee5b..309b40ea 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -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!' diff --git a/test/controllers/posts_controller_test.rb b/test/controllers/posts_controller_test.rb index 376c740f..b8c9f560 100644 --- a/test/controllers/posts_controller_test.rb +++ b/test/controllers/posts_controller_test.rb @@ -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 diff --git a/test/models/post_test.rb b/test/models/post_test.rb index 67dd1a24..ab7dd510 100644 --- a/test/models/post_test.rb +++ b/test/models/post_test.rb @@ -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?