From 77e71f6a66c2e73ab33dc10b94e9a52f6d05c5de Mon Sep 17 00:00:00 2001 From: f Date: Wed, 10 Feb 2021 19:32:48 -0300 Subject: [PATCH 01/25] =?UTF-8?q?asegurarse=20que=20la=20protecci=C3=B3n?= =?UTF-8?q?=20csfr=20se=20realiza=20primero=20#239=20#238=20#237=20#232?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2f09cea..9893daa 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? From 3b015e0c22a198284416cee2289fe6b90f2770f2 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 11:26:17 -0300 Subject: [PATCH 02/25] excepciones con contexto closes #255 closes #256 closes #257 closes #258 closes #259 --- app/jobs/maintenance_job.rb | 2 +- app/models/metadata_template.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/maintenance_job.rb b/app/jobs/maintenance_job.rb index 114ff77..9d1eb7d 100644 --- a/app/jobs/maintenance_job.rb +++ b/app/jobs/maintenance_job.rb @@ -29,7 +29,7 @@ class MaintenanceJob < ApplicationJob 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 + ExceptionNotifier.notify_exception(e, data: { maintenance_id: maintenance_id, email: u[0]) }) end end end diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index e7e2edb..0d8669d 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -172,7 +172,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 From 12756e3d6a9bea3319aec6e557950f845eeeae97 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 11:30:59 -0300 Subject: [PATCH 03/25] =?UTF-8?q?actualizaci=C3=B3n=20de=20cuidados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile.lock | 127 ++++++++++++++++++++++++++------------------------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ae912f9..2d355d6 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,18 @@ 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) 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 +127,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 +288,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 +314,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 +332,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 +361,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 +386,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 +458,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 +538,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) From 360b999dcb049fc13bda3ff30096cc691e02d7bc Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 11:49:15 -0300 Subject: [PATCH 04/25] =?UTF-8?q?la=20opci=C3=B3n=20required=20tiene=20m?= =?UTF-8?q?=C3=A1s=20prioridad=20que=20la=20definici=C3=B3n=20closes=20#21?= =?UTF-8?q?7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/application_helper.rb | 2 +- app/views/posts/attributes/_image.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a389327..23b4c99 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/views/posts/attributes/_image.haml b/app/views/posts/attributes/_image.haml index 0650370..2824b4c 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 From 419749b9129fda9cc2198952a3bcdce73fffd33f Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 12:13:18 -0300 Subject: [PATCH 05/25] poder vaciar arrays closes #219 --- app/models/metadata_array.rb | 8 +++++++- app/views/posts/attributes/_array.haml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/models/metadata_array.rb b/app/models/metadata_array.rb index a1ff8c7..b65fd34 100644 --- a/app/models/metadata_array.rb +++ b/app/models/metadata_array.rb @@ -7,6 +7,12 @@ class MetadataArray < MetadataTemplate super || [] end + # Los Arrays no se pueden cifrar todavía + # TODO: Cifrar y decifrar arrays + def private? + false + end + private def sanitize(values) @@ -16,6 +22,6 @@ class MetadataArray < MetadataTemplate else v end - end + end.select(&:present?) end end diff --git a/app/views/posts/attributes/_array.haml b/app/views/posts/attributes/_array.haml index 73e96de..da9ec65 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), From 69159e6749d3554fcecd694cb0f3b81708c40460 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 12:13:53 -0300 Subject: [PATCH 06/25] aunque el archivo sea obligatorio la descripcion no lo es --- app/views/posts/attributes/_file.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_file.haml b/app/views/posts/attributes/_file.haml index 56adb23..54f87b5 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 From 5fdc170db00570efd50f75d64755eb19080d61e8 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 15:27:44 -0300 Subject: [PATCH 07/25] =?UTF-8?q?mostrar=20el=20valor=20de=20la=20relaci?= =?UTF-8?q?=C3=B3n=20y=20actualizarla=20bidireccionalmente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #226 --- app/models/metadata_belongs_to.rb | 25 ++++++++++++--------- app/models/metadata_template.rb | 20 ++++++++++++----- app/services/post_service.rb | 17 ++++++++------ app/views/posts/attributes/_belongs_to.haml | 12 +++------- 4 files changed, 42 insertions(+), 32 deletions(-) diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb index 2119547..2be7b3b 100644 --- a/app/models/metadata_belongs_to.rb +++ b/app/models/metadata_belongs_to.rb @@ -16,7 +16,7 @@ class MetadataBelongsTo < MetadataRelatedPosts def validate super - errors << I18n.t('metadata.belongs_to.missing_post') unless post_exists? + errors << I18n.t('metadata.belongs_to.missing_post') if value.present? && !post_exists? errors.empty? end @@ -30,24 +30,27 @@ class MetadataBelongsTo < MetadataRelatedPosts # 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? + # Si no hay relación inversa, no hacer nada más + return super unless inverse? - # 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 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? - belongs_to[inverse].value << post.uuid.value unless belongs_to[inverse].value.include? post.uuid.value + # 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 @@ -66,11 +69,11 @@ class MetadataBelongsTo < MetadataRelatedPosts @belongs_to[value] ||= 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? @@ -88,6 +91,6 @@ class MetadataBelongsTo < MetadataRelatedPosts end def post_exists? - !value.blank? && posts.find(sanitize(value), uuid: true) + posts.find(sanitize(value), uuid: true).present? end end diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index 0d8669d..7fa9ba7 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 @@ -30,8 +28,20 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, 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 +69,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 @@ -123,7 +133,7 @@ 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 diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 571588c..bc8def1 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/attributes/_belongs_to.haml b/app/views/posts/attributes/_belongs_to.haml index 0659474..2d17c3f 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 From c2f5c22fc2bdde6f74cf3b809d4382e95ce0d2ae Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 16:16:25 -0300 Subject: [PATCH 08/25] =?UTF-8?q?revisi=C3=B3n=20de=20has=20many?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/metadata_array.rb | 9 ++++--- app/models/metadata_belongs_to.rb | 19 ++++++------- app/models/metadata_has_many.rb | 33 +++++++++-------------- app/models/metadata_related_posts.rb | 6 +++++ app/views/posts/attributes/_has_many.haml | 1 + 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/app/models/metadata_array.rb b/app/models/metadata_array.rb index b65fd34..5c0b16f 100644 --- a/app/models/metadata_array.rb +++ b/app/models/metadata_array.rb @@ -15,12 +15,13 @@ class MetadataArray < MetadataTemplate 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.select(&:present?) end diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb index 2be7b3b..112600b 100644 --- a/app/models/metadata_belongs_to.rb +++ b/app/models/metadata_belongs_to.rb @@ -23,15 +23,12 @@ 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 + super + # Si no hay relación inversa, no hacer nada más - return super unless inverse? + return true unless changed? + return true unless inverse? # Si estamos cambiando la relación, tenemos que eliminar la relación # anterior @@ -86,11 +83,11 @@ class MetadataBelongsTo < MetadataRelatedPosts private - def sanitize(uuid) - uuid.gsub(/[^a-f0-9\-]/, '') + def post_exists? + posts.find(value, uuid: true).present? end - def post_exists? - posts.find(sanitize(value), uuid: true).present? + def sanitize(uuid) + uuid.to_s.gsub(/[^a-f0-9\-]/i, '') end end diff --git a/app/models/metadata_has_many.rb b/app/models/metadata_has_many.rb index cf7a1b0..978bc4a 100644 --- a/app/models/metadata_has_many.rb +++ b/app/models/metadata_has_many.rb @@ -6,22 +6,24 @@ # 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 # 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 +40,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 @@ -62,12 +63,4 @@ class MetadataHasMany < MetadataRelatedPosts def related_methods @related_methods ||= %i[has_many had_many].freeze end - - private - - def sanitize(sanitizable) - sanitizable.map do |uuid| - uuid.gsub(/[^a-f0-9\-]/, '') - end - end end diff --git a/app/models/metadata_related_posts.rb b/app/models/metadata_related_posts.rb index 6712ce2..324ba4f 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/views/posts/attributes/_has_many.haml b/app/views/posts/attributes/_has_many.haml index d39d124..e36b655 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, From bbdbfc4ee1f1c91aecdc8354d9c945ea319a129e Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 16:17:45 -0300 Subject: [PATCH 09/25] renovar relaciones --- app/models/metadata_belongs_to.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb index 112600b..0f43fce 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 # @@ -62,8 +69,7 @@ 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 artículo relacionado anterior From c5cd07a82ef83f3532b538eb9ffac3f4fd888b39 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 16:44:36 -0300 Subject: [PATCH 10/25] =?UTF-8?q?revisi=C3=B3n=20de=20habtm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/metadata_belongs_to.rb | 6 +- .../metadata_has_and_belongs_to_many.rb | 75 +++++-------------- app/models/metadata_has_many.rb | 12 +++ .../attributes/_has_and_belongs_to_many.haml | 1 + config/locales/en.yml | 2 + config/locales/es.yml | 2 + 6 files changed, 38 insertions(+), 60 deletions(-) diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb index 0f43fce..27c50f9 100644 --- a/app/models/metadata_belongs_to.rb +++ b/app/models/metadata_belongs_to.rb @@ -23,7 +23,7 @@ class MetadataBelongsTo < MetadataRelatedPosts def validate super - errors << I18n.t('metadata.belongs_to.missing_post') if value.present? && !post_exists? + errors << I18n.t('metadata.belongs_to.missing_post') unless post_exists? errors.empty? end @@ -59,7 +59,7 @@ class MetadataBelongsTo < MetadataRelatedPosts # 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 @@ -90,7 +90,7 @@ class MetadataBelongsTo < MetadataRelatedPosts private def post_exists? - posts.find(value, uuid: true).present? + value.present? && belongs_to.nil? end def sanitize(uuid) diff --git a/app/models/metadata_has_and_belongs_to_many.rb b/app/models/metadata_has_and_belongs_to_many.rb index e08c991..f14827e 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 978bc4a..ec79ddb 100644 --- a/app/models/metadata_has_many.rb +++ b/app/models/metadata_has_many.rb @@ -14,6 +14,14 @@ class MetadataHasMany < MetadataRelatedPosts 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 ||= posts.where(uuid: value) @@ -63,4 +71,8 @@ class MetadataHasMany < MetadataRelatedPosts def related_methods @related_methods ||= %i[has_many had_many].freeze end + + def posts_exist? + has_many.size == value.size + end end 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 d39d124..e36b655 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/config/locales/en.yml b/config/locales/en.yml index 1535a62..9e8cc57 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -48,6 +48,8 @@ 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" exceptions: post: site_missing: 'Needs an instance of Site' diff --git a/config/locales/es.yml b/config/locales/es.yml index 8ec1527..0bf0368 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -48,6 +48,8 @@ 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' exceptions: post: site_missing: 'Necesita una instancia de Site' From 2fb79ffa15fb34620e5b3d6efffe0e3c9b4a42a8 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 16:45:03 -0300 Subject: [PATCH 11/25] arreglos menores --- app/models/metadata_template.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index 7fa9ba7..8fa5a97 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -23,6 +23,7 @@ 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 @@ -122,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? @@ -138,7 +141,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, # 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 From 17d0ab6df3f21be1daa6ef96e99d09d37113050b Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 17:49:49 -0300 Subject: [PATCH 12/25] validar fechas en navegadores que no las soportan closes 168 --- app/javascript/packs/application.js | 9 +++++++++ app/views/posts/attributes/_date.haml | 3 ++- app/views/posts/attributes/_document_date.haml | 3 ++- config/locales/en.yml | 4 +++- config/locales/es.yml | 4 +++- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 7295446..4c232fd 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/views/posts/attributes/_date.haml b/app/views/posts/attributes/_date.haml index 38c4f50..1347c59 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 6551671..1be997c 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/config/locales/en.yml b/config/locales/en.yml index 9e8cc57..e25c4d2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -50,6 +50,8 @@ en: 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' @@ -462,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 0bf0368..b3bc9e5 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -50,6 +50,8 @@ es: 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' @@ -471,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!' From 522494de5ab61e90e18ce1720aee316800cb2276 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 17:57:12 -0300 Subject: [PATCH 13/25] validar relaciones correctamente --- app/models/metadata_belongs_to.rb | 4 +++- app/models/metadata_has_many.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb index 27c50f9..0626ba0 100644 --- a/app/models/metadata_belongs_to.rb +++ b/app/models/metadata_belongs_to.rb @@ -90,7 +90,9 @@ class MetadataBelongsTo < MetadataRelatedPosts private def post_exists? - value.present? && belongs_to.nil? + return true if sanitize(value).blank? + + sanitize(value).present? && belongs_to.present? end def sanitize(uuid) diff --git a/app/models/metadata_has_many.rb b/app/models/metadata_has_many.rb index ec79ddb..6135401 100644 --- a/app/models/metadata_has_many.rb +++ b/app/models/metadata_has_many.rb @@ -73,6 +73,6 @@ class MetadataHasMany < MetadataRelatedPosts end def posts_exist? - has_many.size == value.size + has_many.size == sanitize(value).size end end From 863fd684b464f57c76ed7239086cd3fd1486f79c Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 18:08:55 -0300 Subject: [PATCH 14/25] asegurarse que siempre se compilan los assets --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7e5cf70..d9d82e4 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" From c71bec218271afe43e07f5d9b4d88641c2506ac9 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 18:41:13 -0300 Subject: [PATCH 15/25] typo + rubocop --- app/jobs/maintenance_job.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/jobs/maintenance_job.rb b/app/jobs/maintenance_job.rb index 9d1eb7d..4c411d0 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, data: { maintenance_id: maintenance_id, email: u[0]) }) - 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 From 9c448f1173b7e2a1290e59c87a969129808b4dcf Mon Sep 17 00:00:00 2001 From: f Date: Thu, 11 Feb 2021 19:33:18 -0300 Subject: [PATCH 16/25] crear los subdominios para ipv6 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d9d82e4..66638fb 100644 --- a/Makefile +++ b/Makefile @@ -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 From 16b92e846dcdded8cdf2c68f45c4dfd5e4b51e93 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 12 Feb 2021 11:24:56 -0300 Subject: [PATCH 17/25] belongs_to => has_many closes #266 closes #267 --- app/views/posts/attribute_ro/_has_and_belongs_to_many.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d718671..d6b51a7 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) From d3890a61173b704c9c3f933291c77d0365dd4ece Mon Sep 17 00:00:00 2001 From: f Date: Tue, 16 Feb 2021 18:08:42 -0300 Subject: [PATCH 18/25] saltearse archivos ya migrados --- app/models/metadata_file.rb | 10 +++++----- app/models/site/static_file_migration.rb | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/models/metadata_file.rb b/app/models/metadata_file.rb index fcabf05..8fecf55 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/site/static_file_migration.rb b/app/models/site/static_file_migration.rb index 798601a..614f63d 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']) From 1a2d7931e9269fdfcd9b7e9f0f0674b9e681ed14 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 17 Feb 2021 18:38:02 -0300 Subject: [PATCH 19/25] optimizar posts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eliminar dependencia en OpenStruct * los metadata se instancian a medida que se usan * la lista de atributos se envió a Layout --- app/models/layout.rb | 4 + app/models/post.rb | 191 +++++++++++++++++---------------------- test/models/post_test.rb | 5 - 3 files changed, 87 insertions(+), 113 deletions(-) diff --git a/app/models/layout.rb b/app/models/layout.rb index c05a02f..2d68273 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/post.rb b/app/models/post.rb index 39de2db..8fc11b9 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/test/models/post_test.rb b/test/models/post_test.rb index 67dd1a2..ab7dd51 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? From aa66e2cc9b71e6fa5adbdc23d6147ef78481cc3b Mon Sep 17 00:00:00 2001 From: f Date: Wed, 17 Feb 2021 18:40:07 -0300 Subject: [PATCH 20/25] obtener el valor correo para value_was --- app/models/metadata_document_date.rb | 6 +++++- app/models/metadata_lang.rb | 6 +++++- app/models/metadata_path.rb | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/models/metadata_document_date.rb b/app/models/metadata_document_date.rb index 036877a..39e6873 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_lang.rb b/app/models/metadata_lang.rb index da13d90..93ef87c 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 6dede4b..3c93cca 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 From 0c6b85c030a4e8ed7f4708d74da0ec9add573ee9 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 17 Feb 2021 18:41:42 -0300 Subject: [PATCH 21/25] =?UTF-8?q?preparar=20MetadataFactory=20para=20poder?= =?UTF-8?q?=20llamar=20m=C3=A9todos=20est=C3=A1ticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/metadata_factory.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/models/metadata_factory.rb b/app/models/metadata_factory.rb index 8cac1c8..c33f1f1 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 From 6edd9e67a2797c652aafa033eb09cf3bfdc0e98c Mon Sep 17 00:00:00 2001 From: f Date: Wed, 17 Feb 2021 18:44:28 -0300 Subject: [PATCH 22/25] simplificar MetadataSlug --- app/models/metadata_slug.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/models/metadata_slug.rb b/app/models/metadata_slug.rb index 1f12475..09da23f 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 From 737416488b5ca8b87c90dcf1c6205fad279d71e8 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 17 Feb 2021 18:46:00 -0300 Subject: [PATCH 23/25] =?UTF-8?q?garantizar=20que=20PostRelation=20siempre?= =?UTF-8?q?=20usa=20la=20misma=20colecci=C3=B3n/idioma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/post_relation.rb | 11 ++++++----- app/models/site.rb | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/models/post_relation.rb b/app/models/post_relation.rb index 7ea9aef..850a83d 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 6c93083..4ec0b9f 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 From b38db8f988e9760615dcee2e00c6044991955913 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 17 Feb 2021 18:48:01 -0300 Subject: [PATCH 24/25] tests de reordenamiento --- test/controllers/posts_controller_test.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/controllers/posts_controller_test.rb b/test/controllers/posts_controller_test.rb index 376c740..b8c9f56 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 From f4a130b97e952fffee945331848037538f58cd71 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 19 Feb 2021 14:05:49 -0300 Subject: [PATCH 25/25] =?UTF-8?q?agregar=20extensi=C3=B3n=20para=20concurr?= =?UTF-8?q?encia=20m=C3=A1s=20veloz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit andá a saber cómo vamos a medir esto :P --- Gemfile | 6 ++++-- Gemfile.lock | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index f073638..96ffa02 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 2d355d6..8d4da43 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -111,6 +111,8 @@ GEM 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.1) database_cleaner-active_record (~> 2.0.0) @@ -584,6 +586,7 @@ DEPENDENCIES brakeman capybara (~> 2.13) commonmarker + concurrent-ruby-ext database_cleaner derailed_benchmarks devise @@ -661,7 +664,7 @@ DEPENDENCIES yaml_db! RUBY VERSION - ruby 2.7.2p137 + ruby 2.7.1p83 BUNDLED WITH 2.1.4