diff --git a/Dockerfile b/Dockerfile index d0a66719..143d4043 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,6 +60,10 @@ ENV RAILS_ENV production # Instalar las dependencias, separamos la librería de base de datos para # poder reutilizar este primer paso desde otros contenedores RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake + +# Chequear que la versión de ruby sea la correcta +RUN test "2.5.7" = `ruby -e 'puts RUBY_VERSION'` + RUN apk add --no-cache postgresql-libs libssh2 # Necesitamos yarn para que Jekyll pueda generar los sitios # XXX: Eliminarlo cuando extraigamos la generación de sitios del proceso diff --git a/Gemfile b/Gemfile index ce16e8a9..e1f61e0a 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,7 @@ git_source(:github) do |repo_name| "https://github.com/#{repo_name}.git" end +# Cambiar en Dockerfile también ruby '2.5.7' gem 'dotenv-rails', require: 'dotenv/rails-now' @@ -21,7 +22,7 @@ gem 'dotenv-rails', require: 'dotenv/rails-now' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 6' # Use Puma as the app server -gem 'puma', '~> 3.7' +gem 'puma' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0' # Use Uglifier as compressor for JavaScript assets diff --git a/Gemfile.lock b/Gemfile.lock index 6ab0e0f3..9f5db802 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,63 +14,63 @@ GIT rake (>= 0.8.7) GEM - remote: https://gems.sutty.nl/ + remote: https://rubygems.org/ specs: - actioncable (6.0.0) - actionpack (= 6.0.0) + actioncable (6.0.1) + actionpack (= 6.0.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.0) - actionpack (= 6.0.0) - activejob (= 6.0.0) - activerecord (= 6.0.0) - activestorage (= 6.0.0) - activesupport (= 6.0.0) + actionmailbox (6.0.1) + actionpack (= 6.0.1) + activejob (= 6.0.1) + activerecord (= 6.0.1) + activestorage (= 6.0.1) + activesupport (= 6.0.1) mail (>= 2.7.1) - actionmailer (6.0.0) - actionpack (= 6.0.0) - actionview (= 6.0.0) - activejob (= 6.0.0) + actionmailer (6.0.1) + actionpack (= 6.0.1) + actionview (= 6.0.1) + activejob (= 6.0.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.0) - actionview (= 6.0.0) - activesupport (= 6.0.0) + actionpack (6.0.1) + actionview (= 6.0.1) + activesupport (= 6.0.1) rack (~> 2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.0) - actionpack (= 6.0.0) - activerecord (= 6.0.0) - activestorage (= 6.0.0) - activesupport (= 6.0.0) + actiontext (6.0.1) + actionpack (= 6.0.1) + activerecord (= 6.0.1) + activestorage (= 6.0.1) + activesupport (= 6.0.1) nokogiri (>= 1.8.5) - actionview (6.0.0) - activesupport (= 6.0.0) + actionview (6.0.1) + activesupport (= 6.0.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.0) - activesupport (= 6.0.0) + activejob (6.0.1) + activesupport (= 6.0.1) globalid (>= 0.3.6) - activemodel (6.0.0) - activesupport (= 6.0.0) - activerecord (6.0.0) - activemodel (= 6.0.0) - activesupport (= 6.0.0) - activestorage (6.0.0) - actionpack (= 6.0.0) - activejob (= 6.0.0) - activerecord (= 6.0.0) + activemodel (6.0.1) + activesupport (= 6.0.1) + activerecord (6.0.1) + activemodel (= 6.0.1) + activesupport (= 6.0.1) + activestorage (6.0.1) + actionpack (= 6.0.1) + activejob (= 6.0.1) + activerecord (= 6.0.1) marcel (~> 0.3.1) - activesupport (6.0.0) + activesupport (6.0.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - zeitwerk (~> 2.1, >= 2.1.8) + zeitwerk (~> 2.2) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) airbrussh (1.3.4) @@ -139,7 +139,7 @@ GEM email_address (0.1.11) netaddr (~> 2.0) simpleidn - erubi (1.8.0) + erubi (1.9.0) eventmachine (1.2.7) exception_notification (4.4.0) actionmailer (>= 4.0, < 7) @@ -178,7 +178,7 @@ GEM railties (>= 4.0.1) hiredis (0.6.3) http_parser.rb (0.6.0) - i18n (1.6.0) + i18n (1.7.0) concurrent-ruby (~> 1.0) image_processing (1.9.3) mini_magick (>= 4.9.5, < 5) @@ -233,7 +233,7 @@ GEM mini_magick (4.9.5) mini_mime (1.0.2) mini_portile2 (2.4.0) - minitest (5.11.3) + minitest (5.13.0) mobility (0.8.9) i18n (>= 0.6.10, < 2) request_store (~> 1.0) @@ -241,7 +241,7 @@ GEM net-ssh (>= 2.6.5, < 6.0.0) net-ssh (5.2.0) netaddr (2.0.3) - nio4r (2.5.1) + nio4r (2.5.2) nokogiri (1.10.5) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) @@ -256,7 +256,8 @@ GEM coderay (~> 1.1.0) method_source (~> 0.9.0) public_suffix (4.0.1) - puma (3.12.1) + puma (4.3.0) + nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) rack (2.0.7) @@ -264,20 +265,20 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.0) - actioncable (= 6.0.0) - actionmailbox (= 6.0.0) - actionmailer (= 6.0.0) - actionpack (= 6.0.0) - actiontext (= 6.0.0) - actionview (= 6.0.0) - activejob (= 6.0.0) - activemodel (= 6.0.0) - activerecord (= 6.0.0) - activestorage (= 6.0.0) - activesupport (= 6.0.0) + rails (6.0.1) + actioncable (= 6.0.1) + actionmailbox (= 6.0.1) + actionmailer (= 6.0.1) + actionpack (= 6.0.1) + actiontext (= 6.0.1) + actionview (= 6.0.1) + activejob (= 6.0.1) + activemodel (= 6.0.1) + activerecord (= 6.0.1) + activestorage (= 6.0.1) + activesupport (= 6.0.1) bundler (>= 1.3.0) - railties (= 6.0.0) + railties (= 6.0.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -289,9 +290,9 @@ GEM railties (>= 6.0.0, < 7) rails_warden (0.6.0) warden (>= 1.2.0) - railties (6.0.0) - actionpack (= 6.0.0) - activesupport (= 6.0.0) + railties (6.0.1) + actionpack (= 6.0.1) + activesupport (= 6.0.1) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -422,7 +423,7 @@ GEM websocket-extensions (0.1.4) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.1.10) + zeitwerk (2.2.1) PLATFORMS ruby @@ -461,7 +462,7 @@ DEPENDENCIES mobility pg pry - puma (~> 3.7) + puma pundit rails (~> 6) rails-i18n diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index fc9a926c..5585413d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,6 +1,5 @@ //= require rails-ujs //= require turbolinks -//= require zepto/dist/zepto.min.js //= require input-tag/input-tag.js -//= require table-dragger/dist/table-dragger +//= require zepto/dist/zepto.min.js //= require_tree . diff --git a/app/assets/javascripts/order.js b/app/assets/javascripts/order.js deleted file mode 100644 index 9e93a302..00000000 --- a/app/assets/javascripts/order.js +++ /dev/null @@ -1,14 +0,0 @@ -$(document).on('turbolinks:load', function() { - var table = document.querySelector('.table-draggable'); - - if (table == null) return; - - tableDragger(table, { - mode: 'row', - onlyBody: true, - dragHandler: '.handle' - }).on('drop', function(from, to, el, mode) { - $('.reorder').val(function(i,v) { return i; }); - $('.submit-reorder').removeClass('d-none'); - }); -}); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9f78638c..30f19368 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -15,6 +15,8 @@ // const images = require.context('../images', true) // const imagePath = (name) => images(name, true) +import tableDragger from "table-dragger" + import {EditorState} from "prosemirror-state" import {EditorView} from "prosemirror-view" import {Schema, DOMParser} from "prosemirror-model" @@ -162,4 +164,17 @@ document.addEventListener('turbolinks:load', e => { textareaEl.value = editor.markdown }, true) } + + const table = document.querySelector('.table-draggable') + + if (table == null) return + + tableDragger(table, { + mode: 'row', + onlyBody: true, + dragHandler: '.handle', + }).on('drop', (from, to, el, mode) => { + $('.reorder').val((i, v) => i) + $('.submit-reorder').removeClass('d-none') + }) }) diff --git a/app/models/metadata_file.rb b/app/models/metadata_file.rb index 06e69be0..efbd2e07 100644 --- a/app/models/metadata_file.rb +++ b/app/models/metadata_file.rb @@ -56,6 +56,8 @@ class MetadataFile < MetadataTemplate # # @return ActiveStorage::Attachment def static_file + return @static_file if @static_file + ActiveRecord::Base.connection_pool.with_connection do if uploaded? blob = ActiveStorage::Blob.find_by(key: key_from_path) @@ -68,10 +70,12 @@ class MetadataFile < MetadataTemplate private + def path + @path ||= Pathname.new value['path'] + end + def key_from_path - # XXX: No podemos usar self#extension porque en este punto todavía - # no sabemos el static_file - File.basename(value['path'], '.*') + path.dirname.basename.to_s end # Hacemos un link duro para colocar el archivo dentro del repositorio @@ -83,7 +87,7 @@ class MetadataFile < MetadataTemplate end def extension - static_file.blob.content_type.split('/').last + @extension ||= static_file.filename.to_s.split('.').last end # Obtener la ruta al archivo @@ -97,7 +101,7 @@ class MetadataFile < MetadataTemplate end def relative_destination_path - File.join('public', [static_file.key, extension].join('.')) + File.join('public', static_file.key, static_file.filename.to_s) end def destination_path diff --git a/app/models/post.rb b/app/models/post.rb index 14f7ce43..0dd9299f 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -156,8 +156,8 @@ class Post < OpenStruct # Guarda los cambios # rubocop:disable Metrics/CyclomaticComplexity - def save - return false unless valid? + def save(validation = true) + return false if validation && !valid? # Salir si tenemos que cambiar el nombre del archivo y no pudimos return false if !new? && path_changed? && !update_path! return false unless save_attributes! diff --git a/app/models/site.rb b/app/models/site.rb index 72d9cced..d81b95f9 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -39,7 +39,7 @@ class Site < ApplicationRecord # Carga el sitio Jekyll una vez que se inicializa el modelo o después # de crearlo after_initialize :load_jekyll - after_create :load_jekyll + after_create :load_jekyll, :static_file_migration! # Cambiar el nombre del directorio before_update :update_name! # Guardar la configuración si hubo cambios @@ -323,6 +323,11 @@ class Site < ApplicationRecord config.url = url end + # Migra los archivos a Sutty + def static_file_migration! + Site::StaticFileMigration.new(site: self).migrate! + end + # Valida si el sitio tiene al menos una forma de alojamiento asociada # y es la local # diff --git a/app/models/site/static_file_migration.rb b/app/models/site/static_file_migration.rb new file mode 100644 index 00000000..4d08fc8c --- /dev/null +++ b/app/models/site/static_file_migration.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +class Site + MigrationAuthor = Struct.new :email, :name, keyword_init: true + + # Obtiene todos los archivos relacionados en artículos del sitio y los + # sube a Sutty de forma que los podamos seguir utilizando normalmente + # sin casos especiales (ej. soportar archivos locales al repositorio y + # remotos, alojados en Sutty) + # + # TODO: Convertir en reutilizable, por ejemplo correr en cada pull, no + # asumir que la migración se hizo una sola vez... + class StaticFileMigration + # Tipos de metadatos que contienen archivos + STATIC_TYPES = %w[file image].freeze + + attr_reader :site + + def initialize(site:) + @site = site + end + + # Recorre todos los artículos cuyos layouts contengan campos con + # archivos estáticos + def migrate! + log = File.open(File.join(site.path, 'migration.log'), 'w') + modified = [] + + Dir.chdir site.path do + site.locales.each do |locale| + # Recorrer todos los documentos de todas las colecciones + site.posts(lang: locale).each do |doc| + # Ignoramos los documentos cuyo layout no contiene archivos + next unless layouts.include? doc.layout.name + + remove = true + + # Buscamos todos los campos con archivos + fields.each do |field| + next unless doc.attribute? field + next unless doc.document.data.key? field.to_s + + # Traemos los metadatos, en este punto, Sutty cree que el + # archivo está subido, porque es una string apuntando a un + # archivo. + metadata = doc.public_send(field) + + next if metadata.value['path'].blank? + + path = Pathname.new(metadata.value['path']) + + # Si no existe, agregamos una imagen faltante para no + # romper el sitio en Sutty + unless path.exist? + log.write "#{path} no existe\n" + path = Pathname.new(Rails.root.join('app/assets/images/logo.png')) + remove = false + end + + # Agregamos el archivo al sitio y se lo asignamos al campo + metadata.value['path'] = { + io: path.open, + filename: path.basename + } + + # Copiar y analizar el archivo sincrónicamente + metadata.static_file.blob.upload path.open + metadata.static_file.blob.analyze + + next unless remove + + dest = Pathname.new(metadata.send(:relative_destination_path)) + + # Eliminamos el archivo original y lo vinculamos al subido + # para mantener la ruta y no romper el sitio + FileUtils.rm_f path + # XXX: Link simbólico o duro? + FileUtils.ln_s dest.relative_path_from(path.dirname), path + end + + # Guardamos los cambios + unless doc.save(false) + log.write "#{doc.path.relative} no se pudo guardar\n" + end + + modified << doc.path.absolute + end + end + end + + log.close + + # TODO: Hacer la migración desde el servicio de creación de sitios? + site.repository.commit(file: modified, + message: I18n.t('sites.static_file_migration'), + usuarie: author) + end + + private + + def author + @author = MigrationAuthor.new email: "sutty@#{Site.domain}", + name: 'Sutty' + end + + # Encuentra todos los layouts con campos estáticos + def layouts + @layouts ||= site.layouts.reject do |_, layout| + layout.metadata.select do |_, desc| + STATIC_TYPES.include? desc['type'] + end.empty? + end.keys + end + + # Encuentra todos los campos con archivos estáticos + def fields + @fields ||= layouts.map do |layout| + site.layouts[layout].metadata.select do |_, desc| + STATIC_TYPES.include? desc['type'] + end.keys + end.flatten.uniq.map(&:to_sym) + end + end +end diff --git a/app/views/devise/mailer/invitation_instructions.haml b/app/views/devise/mailer/invitation_instructions.haml index dc939ae7..74193878 100644 --- a/app/views/devise/mailer/invitation_instructions.haml +++ b/app/views/devise/mailer/invitation_instructions.haml @@ -1,4 +1,4 @@ -- site = @resources.sites.last +- site = @resource.sites.last %p= t('devise.mailer.invitation_instructions.hello', email: @resource.email) diff --git a/app/views/devise/mailer/invitation_instructions.text.haml b/app/views/devise/mailer/invitation_instructions.text.haml index 9735863a..16a9f0a8 100644 --- a/app/views/devise/mailer/invitation_instructions.text.haml +++ b/app/views/devise/mailer/invitation_instructions.text.haml @@ -1,4 +1,4 @@ -- site = @resources.sites.last +- site = @resource.sites.last = t('devise.mailer.invitation_instructions.hello', email: @resource.email) \ diff --git a/app/views/posts/attribute_ro/_date.haml b/app/views/posts/attribute_ro/_date.haml new file mode 100644 index 00000000..dc94def2 --- /dev/null +++ b/app/views/posts/attribute_ro/_date.haml @@ -0,0 +1,3 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td= l metadata.value.to_date diff --git a/app/views/posts/attributes/_date.haml b/app/views/posts/attributes/_date.haml new file mode 100644 index 00000000..ac1c7b02 --- /dev/null +++ b/app/views/posts/attributes/_date.haml @@ -0,0 +1,6 @@ +.form-group + = label_tag "post_#{attribute}", post_label_t(attribute, post: post) + = date_field 'post', attribute, value: metadata.value.strftime('%F'), + **field_options(attribute, metadata) + = render 'posts/attribute_feedback', + post: post, attribute: attribute, metadata: metadata diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml index 12730e82..4c903485 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -6,13 +6,16 @@ @category] %main.row - %aside.col-md-3 + %aside.menu.col-md-3 %h1 = link_to @site.title, @site.url %p.lead= @site.description - = link_to t('posts.new'), new_site_post_path(@site), - class: 'btn' + %h3= t('posts.new') + %ul + - @site.layouts.keys.each do |layout| + %li= link_to layout.to_s.humanize, + new_site_post_path(@site, layout: layout) - if policy(@site).edit? = link_to t('sites.edit.btn', site: @site.title), diff --git a/config/initializers/analyze_job.rb b/config/initializers/analyze_job.rb new file mode 100644 index 00000000..f268e0dd --- /dev/null +++ b/config/initializers/analyze_job.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# TODO: Estamos procesando el análisis de los archivos en el momento +# porque queremos obtener la ruta del archivo en el momento y no +# después. Necesitaríamos poder generar el vínculo en el +# repositorio a destiempo, modificando el Job de ActiveStorage +ActiveStorage::AnalyzeJob.queue_adapter = :inline diff --git a/config/locales/en.yml b/config/locales/en.yml index 466c4324..84215d03 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -148,6 +148,7 @@ en: anexo: 'Appendix' simple: 'Simple' sites: + static_file_migration: 'File migration' index: 'This is the list of sites you can edit.' edit_translations: "You can edit texts from your site other than posts', and you can also translate them to other languages." @@ -354,8 +355,7 @@ en: date: 'date' order: 'Order' content: 'Text' - new: 'New post' - new_with_template: 'New %{template}' + new: 'New post as' dropdown: 'Toggle dropdown' categories: 'Everything' index: 'Posts' diff --git a/config/locales/es.yml b/config/locales/es.yml index 4c104fcb..2c71813e 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -149,6 +149,7 @@ es: anexo: 'Anexo' simple: 'Simple' sites: + static_file_migration: 'Migración de archivos' index: 'Este es el listado de sitios que puedes editar.' edit_translations: 'Puedes editar los textos que salen en tu sitio que no corresponden a artículos aquí, además de traducirlos a @@ -366,8 +367,7 @@ es: content: 'Cuerpo del artículo' categories: 'Todos' dropdown: 'Desplegar el menú' - new: 'Crear artículo' - new_with_template: 'Comenzar %{template}' + new: 'Crear artículo en:' index: 'Artículos' edit: 'Editar' open: 'Nota: Puedes agregar más opciones a medida que las escribes y presionas Entrar' diff --git a/doc/static_files.md b/doc/static_files.md new file mode 100644 index 00000000..8a7c91a3 --- /dev/null +++ b/doc/static_files.md @@ -0,0 +1,30 @@ +# Archivos estáticos + +El objetivo es encontrar una forma de importar los archivos estáticos de +un sitio que se ha migrado a la estructura de Sutty, es decir, subirlos +como archivos de Rails y luego vincularlos a los artículos. + +Actualmente, los archivos primero se suben a la interfaz via +ActiveStorage y luego se vinculan internamente al repositorio[^git]. + +Lo que deberíamos lograr es reconocer los archivos vinculados desde los +artículos y hacer el proceso de cargarlos internamente, modificando los +artículos para que apunten a las nuevas versiones. + +Esto podría hacerse por cada artículo individual o durante el proceso de +creación del sitio, después de clonarlo. + +El algoritmo sería: + +* Clonar el sitio +* Obtener todos los artículos de todas las colecciones +* Encontrar todos los layouts con archivos adjuntos +* Buscar todos los artículos que tengan esos layouts +* Tomar el archivo físico desde los metadatos y asociarlo al sitio via Active Storage +* Asociar el archivo al MetadataFile/Image correspondiente +* ??? +* Migración cumplida! + + +[^git]: esto puede hacer que los repositorios git sean gigantes, + podríamos usar algo como git-annex quizás? diff --git a/test/models/post_test.rb b/test/models/post_test.rb index 7b903b6a..827aeb5c 100644 --- a/test/models/post_test.rb +++ b/test/models/post_test.rb @@ -63,6 +63,12 @@ class PostTest < ActiveSupport::TestCase assert_not @post.valid? end + test 'se pueden guardar sin validar' do + assert @post.valid? + @post.title.value = '' + assert @post.save(false) + end + test 'se pueden guardar los cambios' do title = SecureRandom.hex @post.title.value = title