diff --git a/Gemfile.lock b/Gemfile.lock index 871b07bc..6ab0e0f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -115,7 +115,7 @@ GEM commonmarker (0.20.1) ruby-enum (~> 0.5) concurrent-ruby (1.1.5) - crass (1.0.4) + crass (1.0.5) database_cleaner (1.7.0) devise (4.7.1) bcrypt (~> 3.0) @@ -220,7 +220,7 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) - loofah (2.2.3) + loofah (2.3.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -234,7 +234,7 @@ GEM mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.11.3) - mobility (0.8.8) + mobility (0.8.9) i18n (>= 0.6.10, < 2) request_store (~> 1.0) net-scp (2.0.0) @@ -242,7 +242,7 @@ GEM net-ssh (5.2.0) netaddr (2.0.3) nio4r (2.5.1) - nokogiri (1.10.4) + nokogiri (1.10.5) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) parallel (1.17.0) @@ -282,8 +282,8 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.2.0) - loofah (~> 2.2, >= 2.2.2) + rails-html-sanitizer (1.3.0) + loofah (~> 2.3) rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index cbe806e1..635cfe23 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -31,8 +31,7 @@ class PostsController < ApplicationController def new authorize Post @site = find_site - # TODO: Implementar layout - @post = @site.posts.build(lang: I18n.locale) + @post = @site.posts.build(lang: I18n.locale, layout: params[:layout]) end def create @@ -90,4 +89,17 @@ class PostsController < ApplicationController service.destroy redirect_to site_posts_path(@site) end + + # Reordenar los artículos + def reorder + @site = find_site + authorize @site + + service = PostService.new(site: @site, + usuarie: current_usuarie, + params: params) + + service.reorder + redirect_to site_posts_path(@site) + end end diff --git a/app/models/metadata_path.rb b/app/models/metadata_path.rb index 05e3ed41..474ddee7 100644 --- a/app/models/metadata_path.rb +++ b/app/models/metadata_path.rb @@ -24,7 +24,7 @@ class MetadataPath < MetadataTemplate private def ext - document.extname.blank? ? '.markdown' : document.extname + document.data['ext'].blank? ? '.markdown' : document.data['ext'] end def lang diff --git a/app/models/post_relation.rb b/app/models/post_relation.rb index 9e0df2e5..40ccaa96 100644 --- a/app/models/post_relation.rb +++ b/app/models/post_relation.rb @@ -57,7 +57,7 @@ class PostRelation < Array def build_layout(layout = nil) return layout if layout.is_a? Layout - site.layouts[layout || :post] + site.layouts[layout.try(:to_sym) || :post] end # Devuelve una colección Jekyll que hace pasar el documento diff --git a/app/models/site.rb b/app/models/site.rb index bb95fdb1..72d9cced 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -173,9 +173,13 @@ class Site < ApplicationRecord @posts[lang] = PostRelation.new site: self + # Jekyll lee los documentos en orden cronológico pero los invierte + # durante la renderización. Usamos el orden cronológico inverso por + # defecto para mostrar los artículos más nuevos primero. + docs = collections[lang.to_s].try(:docs).try(:sort) { |a, b| b <=> a } # No fallar si no existe colección para este idioma # XXX: queremos fallar silenciosamente? - (collections[lang.to_s].try(:docs) || []).each do |doc| + (docs || []).each do |doc| layout = layouts[doc.data['layout'].to_sym] @posts[lang].build(document: doc, layout: layout, lang: lang) @@ -202,8 +206,7 @@ class Site < ApplicationRecord # @return Array def everything_of(attr, lang: nil) posts(lang: lang).map do |p| - # XXX: Tener cuidado con los métodos que no existan - p.send(attr).try :value + p.send(attr).try(:value) if p.attribute? attr end.flatten.uniq.compact end @@ -217,47 +220,6 @@ class Site < ApplicationRecord status == 'enqueued' end - # Verifica si los posts están ordenados - def ordered?(lang: nil) - posts(lang: lang).map(&:order).all? - end - - # Reordena la colección usando la posición informada - # - # new_order es un hash cuya key es la posición actual del post y el - # valor la posición nueva - # - # TODO: Refactorizar y testear - def reorder_collection(collection, new_order) - # Tenemos que pasar el mismo orden - return if new_order.values.map(&:to_i).sort != new_order.keys.map(&:to_i).sort - - # Solo traer los posts que vamos a modificar - posts_to_order = posts_for(collection).values_at(*new_order.keys.map(&:to_i)) - - # Recorre todos los posts y asigna el nuevo orden - posts_to_order.each_with_index do |p, i| - # Usar el index si el artículo no estaba ordenado, para tener una - # ruta de adopción - oo = (p.order || i).to_s - no = new_order[oo].to_i - # No modificar nada si no hace falta - next if p.order == no - - p.update_attributes order: no - p.save - end - - posts_to_order.map(&:ordered?).all? - end - - # Reordena la colección usando la posición actual de los artículos - def reorder_collection!(collection = 'posts') - order = Hash[posts_for(collection).count.times.map { |i| [i.to_s, i.to_s] }] - reorder_collection collection, order - end - alias reorder_posts! reorder_collection! - # Obtener una ruta disponible para Sutty # # TODO: Refactorizar y testear diff --git a/app/models/site/repository.rb b/app/models/site/repository.rb index 36305b2a..1923d06d 100644 --- a/app/models/site/repository.rb +++ b/app/models/site/repository.rb @@ -85,9 +85,13 @@ class Site !commits.empty? end - # Guarda los cambios en git, de a un archivo por vez + # Guarda los cambios en git def commit(file:, usuarie:, message:, remove: false) - remove ? rm(file) : add(file) + file = [file] unless file.respond_to? :each + + file.each do |f| + remove ? rm(f) : add(f) + end # Escribir los cambios para que el repositorio se vea tal cual rugged.index.write diff --git a/app/policies/site_policy.rb b/app/policies/site_policy.rb index bf3ac367..1177596c 100644 --- a/app/policies/site_policy.rb +++ b/app/policies/site_policy.rb @@ -67,6 +67,11 @@ class SitePolicy pull? end + # Solo les usuaries pueden reordenar artículos + def reorder? + site.usuarie? usuarie + end + private def current_role diff --git a/app/services/post_service.rb b/app/services/post_service.rb index fddb0b61..890a77fb 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -39,19 +39,50 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do post end + # Reordena todos los posts que soporten orden de acuerdo a un array + # con las nuevas posiciones. La posición actual la da la posición en + # el array. + # + # [ 1, 0, 2 ] => mover el elemento 2 a la posición 1, mantener 3 + def reorder + posts = site.posts(lang: lang) + reorder = params.require(:post).permit(reorder: []) + .try(:[], :reorder) + .try(:map, &:to_i) || [] + + # Tenemos que pasar un array con la misma cantidad de elementos + return false if reorder.size != posts.size + + files = reorder.map.with_index do |new, cur| + post = posts[cur] + next unless post.attributes.include? :order + + post.usuaries << usuarie + post.order.value = new + post.path.absolute + end.compact + + # TODO: Implementar transacciones! + posts.save_all && commit(action: :reorder, file: files) + end + private - def commit(action:) - site.repository.commit(file: post.path.absolute, + def commit(action:, file: nil) + site.repository.commit(file: file || post.path.absolute, usuarie: usuarie, remove: action == :destroyed, message: I18n.t("post_service.#{action}", - title: post.title.value)) + title: post.try(:title).try(:value))) end # Solo permitir cambiar estos atributos de cada articulo def post_params params.require(:post).permit(post.params) end + + def lang + params[:post][:lang] || I18n.locale + end end # rubocop:enable Metrics/BlockLength diff --git a/app/services/site_service.rb b/app/services/site_service.rb index 8c94669c..ca0a774d 100644 --- a/app/services/site_service.rb +++ b/app/services/site_service.rb @@ -74,6 +74,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do post: { lang: lang, title: site.licencia.name, + description: I18n.t('sites.form.licencia.title'), author: %w[Sutty], permalink: "#{I18n.t('activerecord.models.licencia').downcase}/", content: CommonMarker.render_html(site.licencia.deed) diff --git a/config/locales/en.yml b/config/locales/en.yml index d660955c..238dbc6b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,6 +19,7 @@ en: created: 'Created "%{title}"' updated: 'Updated "%{title}"' destroyed: 'Removed "%{title}"' + reorder: 'Reorder' metadata: array: cant_be_empty: 'This field cannot be empty' @@ -296,7 +297,7 @@ en: url: 'Demo' licencia: 'Read the license' licencia: - title: 'License for the site and everything in it' + title: 'License for the site and everything published on it' url: 'Read the license' privacidad: title: 'Privacy policy and code of conduct' @@ -338,6 +339,8 @@ en: invalid_help: 'Some fields need attention! Please search for the fields marked as invalid.' sending_help: Saving, please wait... attributes: + lang: + 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. diff --git a/config/locales/es.yml b/config/locales/es.yml index 7fdeff7a..4eda8ba1 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -19,6 +19,7 @@ es: created: 'Creado "%{title}"' updated: 'Modificado "%{title}"' destroyed: 'Eliminado "%{title}"' + reorder: 'Reordenados' metadata: array: cant_be_empty: 'El campo no puede estar vacío' @@ -306,7 +307,7 @@ es: url: 'Demostración' license: 'Leer la licencia' licencia: - title: 'Licencia del sitio y todo lo que publiques' + title: 'Licencia del sitio y todo lo publicado' url: 'Leer la licencia' privacidad: title: Políticas de privacidad y código de convivencia @@ -348,6 +349,8 @@ es: invalid_help: '¡Te faltan completar algunos campos! Busca los que estén marcados como inválidos' sending_help: Guardando, por favor espera... attributes: + lang: + 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. diff --git a/config/routes.rb b/config/routes.rb index 1c2f79d1..8ef82cbc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,6 +45,7 @@ Rails.application.routes.draw do post 'collaborate', to: 'collaborations#accept_collaboration' # Gestionar artículos + post :'posts/reorder', to: 'posts#reorder' resources :posts # Gestionar traducciones diff --git a/doc/reordenar.md b/doc/reordenar.md index 719856ff..bf5c0c42 100644 --- a/doc/reordenar.md +++ b/doc/reordenar.md @@ -1,134 +1,15 @@ # Reordenar los articulos -La interfaz reordena los articulos y los envia en ese orden particular -(se puede enviar un numero de orden completado con js para estar mas -segurxs). Entonces el algoritmo... +Todos los posts tienen un campo `order`. -* Chequea que los posts tengan fechas en orden - -* Si alguno(s) no tienen, busca fechas intermedias - -* Cuando todos tienen fechas en orden, guarda los cambios modificando - cada post - ---- - -* Compara el nuevo orden con el viejo para saber las diferencias - ---- - -* Recorre el nuevo orden uno por uno -* Se fija si el anterior tiene una fecha menor -* Se fija si el siguiente tiene una fecha mayor -* Se autoasigna una fecha en el medio -* Se guarda al final - ---- - -* Todos los posts tienen un ID (su id en el array de la colección) -* Toma el post actual -* Mueve el post a una posición arbitraria con un nuevo id -* Toma la fecha del post siguiente (nuevo_id + 1) -* Recorre hacia atrás los anteriores, corriéndolos de a un día hasta una - fecha anterior -* Los posts posteriores no se tocan - -Teniendo estos artículos - -``` -1 2017-01-01 -2 2017-01-02 -3 2017-01-03 -4 2017-01-04 -5 2017-01-05 -``` - -Movemos el artículo 2 a la posición 4 - -``` -1 1 2017-01-01 -2 3 2017-01-03 -3 4 2017-01-04 -4 2 2017-01-02 -5 5 2017-01-05 -``` - -Reordenamos las fechas - -``` -1 1 2017-01-01 2017-01-01 -2 3 2017-01-03 2017-01-02 -3 4 2017-01-04 2017-01-03 -4 2 2017-01-02 2017-01-04 -5 5 2017-01-05 2017-01-05 -``` - -Movemos varios - -``` -1 4 2017-01-04 -2 5 2017-01-05 -3 2 2017-01-02 -4 3 2017-01-03 -5 1 2017-01-01 -``` - -Cual es la fecha desde la que se empieza? Vamos hacia atras o hacia -adelante? - -Hacia atrás - -``` -1 4 2017-01-04 2016-12-28 -2 5 2017-01-05 2016-12-29 -3 2 2017-01-02 2016-12-30 -4 3 2017-01-03 2016-12-31 -5 1 2017-01-01 2017-01-01 -``` - -Hacia adelante - -``` -1 4 2017-01-04 2017-01-04 -2 5 2017-01-05 2017-01-05 -3 2 2017-01-02 2017-01-06 -4 3 2017-01-03 2017-01-07 -5 1 2017-01-01 2017-01-08 -``` - -En si las fechas no importan, porque estamos priorizando el orden, las -fechas son arbitrarias para engañar a jekyll a tener los posts en cierto -orden. - -Por el contrario, para mantener los cambios mínimos, podemos reemplazar -hacia adelante comenzando desde la fecha mas baja del orden original. - -``` -1 4 2017-01-04 2017-01-01 -2 5 2017-01-05 2017-01-02 -3 2 2017-01-02 2017-01-03 -4 3 2017-01-03 2017-01-04 -5 1 2017-01-01 2017-01-05 -``` - -No quiere decir que en ordenes de fechas mas dispersas se mantengan los -cambios mínimos. - -También podemos tomar todo el set original de fechas y asociarselo al -orden nuevo de posts. Las fechas se mantienen igual, pero cambia el -orden de los posts. - -Qué pasa cuando dos o más artículos comparten la misma fecha? -Normalmente se ordenan por nombre, pero en este algoritmo no entra ese -orden, se asume que siempre están una fecha adelante o atrás - ---- - -Todos los posts tienen un campo order. - -El orden se actualiza en base al orden cronologico. +El orden se actualiza en base al orden cronológico. En la plantilla se puede ordenar cronólogicamente o por orden numérico. -Es más simple de implementar y no tiene comportamientos inesperados, -como "por qué cambió de fecha?" +El orden es independiente de otros metadatos (como layout, categoria, +etc), todos los artículos siguen un orden + +Como el orden es un metadato, tenemos que ignorar los tipos de posts que +no lo tienen + +El orden por defecto es orden natural, más bajo es más alto. diff --git a/mobility.patch b/mobility.patch deleted file mode 100644 index 9e7248f2..00000000 --- a/mobility.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- lib/mobility/arel/visitor.rb.orig 2019-08-29 14:20:24.180122614 -0300 -+++ lib/mobility/arel/visitor.rb 2019-08-29 14:18:38.146723362 -0300 -@@ -14,7 +14,7 @@ - - private - -- def visit(object) -+ def visit(object, collection = nil) - super - rescue TypeError - visit_default(object) diff --git a/test/controllers/posts_controller_test.rb b/test/controllers/posts_controller_test.rb index 10a2db19..684818b8 100644 --- a/test/controllers/posts_controller_test.rb +++ b/test/controllers/posts_controller_test.rb @@ -7,7 +7,8 @@ class PostsControllerTest < ActionDispatch::IntegrationTest @rol = create :rol @site = @rol.site @usuarie = @rol.usuarie - @post = @site.posts.build(title: SecureRandom.hex) + @post = @site.posts.build(title: SecureRandom.hex, + description: SecureRandom.hex) @post.save @authorization = { @@ -33,6 +34,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest params: { post: { title: title, + description: title, date: 2.days.ago.strftime('%F') } } @@ -62,8 +64,14 @@ class PostsControllerTest < ActionDispatch::IntegrationTest test 'se pueden actualizar' do title = SecureRandom.hex - patch site_post_url(@site, @post.id), headers: @authorization, - params: { post: { title: title } } + patch site_post_url(@site, @post.id), + headers: @authorization, + params: { + post: { + title: title, + description: title + } + } assert_equal 302, response.status @@ -133,4 +141,21 @@ class PostsControllerTest < ActionDispatch::IntegrationTest assert_equal 200, response.status assert_match I18n.t('metadata.image.not_an_image'), response.body end + + test 'se pueden reordenar' do + lang = I18n.available_locales.sample + posts = @site.posts(lang: lang) + reorder = posts.each_index.to_a.shuffle + + post site_posts_reorder_url(@site), + headers: @authorization, + params: { post: { 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, + @site.posts(lang: lang).map(&:order).map(&:value) + end end diff --git a/test/integration/editor_test.rb b/test/integration/editor_test.rb index 63c8d5af..2b64eb06 100644 --- a/test/integration/editor_test.rb +++ b/test/integration/editor_test.rb @@ -43,11 +43,12 @@ class EditorTest < ActionDispatch::IntegrationTest params: { post: { title: SecureRandom.hex, + description: SecureRandom.hex, content: content } } - @post = @site.posts.last + @post = @site.posts.first assert_equal md_content.strip, @post.content.value end @@ -61,11 +62,12 @@ class EditorTest < ActionDispatch::IntegrationTest params: { post: { title: SecureRandom.hex, + description: SecureRandom.hex, content: trix_orig } } - @post = @site.posts.last + @post = @site.posts.first assert_equal trix_md.strip, @post.content.value end diff --git a/test/models/post_test.rb b/test/models/post_test.rb index 996a0263..7b903b6a 100644 --- a/test/models/post_test.rb +++ b/test/models/post_test.rb @@ -5,7 +5,8 @@ require 'test_helper' class PostTest < ActiveSupport::TestCase setup do @site = create :site - @post = @site.posts.create(title: SecureRandom.hex) + @post = @site.posts.create(title: SecureRandom.hex, + description: SecureRandom.hex) end teardown do @@ -65,6 +66,7 @@ class PostTest < ActiveSupport::TestCase test 'se pueden guardar los cambios' do title = SecureRandom.hex @post.title.value = title + @post.description.value = title @post.categories.value << title assert @post.save @@ -78,6 +80,7 @@ class PostTest < ActiveSupport::TestCase assert document.data['categories'].include?(title) assert_equal title, document.data['title'] + assert_equal title, document.data['description'] assert_equal 'post', document.data['layout'] end end @@ -136,14 +139,16 @@ class PostTest < ActiveSupport::TestCase end test 'new?' do - post = @site.posts.build title: SecureRandom.hex + post = @site.posts.build title: SecureRandom.hex, + description: SecureRandom.hex assert post.new? assert post.save assert_not post.new? end test 'no podemos pisar otros archivos' do - post = @site.posts.create title: SecureRandom.hex + post = @site.posts.create title: SecureRandom.hex, + description: SecureRandom.hex @post.slug.value = post.slug.value @post.date.value = post.date.value @@ -157,6 +162,7 @@ class PostTest < ActiveSupport::TestCase post = @site.posts.build(layout: :post) post.title.value = SecureRandom.hex post.content.value = SecureRandom.hex + post.description.value = SecureRandom.hex assert post.new? assert post.save @@ -165,9 +171,12 @@ class PostTest < ActiveSupport::TestCase test 'se pueden inicializar con valores' do title = SecureRandom.hex - post = @site.posts.build(title: title, content: title) + post = @site.posts.build(title: title, + description: title, + content: title) assert_equal title, post.title.value + assert_equal title, post.description.value assert_equal title, post.content.value assert post.save end