From cff18bf8612920b5ba9eb634c86b0329e8b1ad5d Mon Sep 17 00:00:00 2001 From: f Date: Tue, 16 Jul 2019 16:47:44 -0300 Subject: [PATCH] poder actualizar el sitio a partir del skel --- app/controllers/sites_controller.rb | 20 ++++++++ app/models/site.rb | 12 +++++ app/models/site/repository.rb | 78 +++++++++++++++++++++++++++++ app/models/usuarie.rb | 4 ++ app/policies/site_policy.rb | 12 +++++ app/views/layouts/_time.haml | 1 + app/views/sites/fetch.haml | 34 +++++++++++++ app/views/sites/index.haml | 21 +++++--- config/locales/en.yml | 10 ++++ config/locales/es.yml | 10 ++++ config/routes.rb | 6 +++ doc/crear_sitios.md | 53 +++++++++++++++++++- test/models/site/repository_test.rb | 37 ++++++++++++++ 13 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 app/models/site/repository.rb create mode 100644 app/views/layouts/_time.haml create mode 100644 app/views/sites/fetch.haml create mode 100644 test/models/site/repository_test.rb diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index 42fbaba8..ec7ad31d 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -114,6 +114,26 @@ class SitesController < ApplicationController redirect_to site_posts_path @site end + def fetch + @site = find_site + authorize @site + + @commits = @site.repository.commits + end + + def merge + @site = find_site + authorize @site + + if @site.repository.merge(current_usuarie) + flash[:success] = I18n.t('sites.fetch.merge.success') + else + flash[:error] = I18n.t('sites.fetch.merge.error') + end + + redirect_to sites_path + end + private def site_params diff --git a/app/models/site.rb b/app/models/site.rb index ac449f98..24f80b08 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -28,6 +28,18 @@ class Site < ApplicationRecord attr_accessor :jekyll, :collections + # El repositorio git para este sitio + def repository + @repository ||= Site::Repository.new path + end + + # Trae los cambios del skel y verifica que haya cambios + def needs_pull? + !repository.commits.empty? + end + + # TODO: Mover esta consulta a la base de datos para no traer un montón + # de cosas a la memoria def invitade?(usuarie) invitades.pluck(:id).include? usuarie.id end diff --git a/app/models/site/repository.rb b/app/models/site/repository.rb new file mode 100644 index 00000000..e8d49c09 --- /dev/null +++ b/app/models/site/repository.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +class Site + # Acciones para el repositorio Git de un sitio. Por ahora hacemos un + # uso muy básico de Git, con lo que asumimos varias cosas, por ejemplo + # que un sitio tiene un solo origen, que siempre se trabaja con la + # rama master, etc. + class Repository + attr_reader :rugged, :changes + + def initialize(path) + @rugged = Rugged::Repository.new(path) + @changes = 0 + end + + def remote + @remote ||= rugged.remotes.first + end + + # Trae los cambios del repositorio de origen sin aplicarlos y + def fetch + if remote.check_connection :fetch + @changes = rugged.fetch(remote)[:received_objects] + else + 0 + end + end + + # Incorpora los cambios en el repositorio actual + # + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + def merge(author) + master = rugged.branches['master'].target + origin = rugged.branches['origin/master'].target + merge = rugged.merge_commits(master, origin) + + # No hacemos nada si hay conflictos + # + # TODO: Enviar un correo a administración para poder revisar + # manualmente. Idealmente no deberíamos tener conflictos pero + # quién sabe. + return if merge.conflicts? + + author = { name: author.name, email: author.email } + commit = Rugged::Commit + .create(rugged, + parents: [master, origin], + tree: merge.write_tree(rugged), + message: I18n.t('sites.fetch.merge.message'), + author: author, + committer: author, + update_ref: 'HEAD') + + # Forzamos el checkout para mover el HEAD al último commit y + # escribir los cambios + rugged.checkout 'HEAD', strategy: :force + commit + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + + # Compara los commits entre el repositorio remoto y el actual para + # que luego los podamos mostrar. + def commits + walker = Rugged::Walker.new rugged + + # Obtenemos todos los commits que existen en origin/master que no + # están en la rama master local + # + # XXX: monitorear esto por performance + walker.push 'refs/remotes/origin/master' + walker.hide 'refs/heads/master' + + walker.each.to_a + end + end +end diff --git a/app/models/usuarie.rb b/app/models/usuarie.rb index a1a2be76..c2704a1a 100644 --- a/app/models/usuarie.rb +++ b/app/models/usuarie.rb @@ -11,6 +11,10 @@ class Usuarie < ApplicationRecord has_many :roles has_many :sites, through: :roles + def name + email.split('@', 2).first + end + def rol_for_site(site) site.roles.merge(roles).first end diff --git a/app/policies/site_policy.rb b/app/policies/site_policy.rb index 900c8f53..702d41cd 100644 --- a/app/policies/site_policy.rb +++ b/app/policies/site_policy.rb @@ -63,6 +63,18 @@ class SitePolicy build? end + def pull? + build? + end + + def fetch? + pull? + end + + def merge? + pull? + end + private def current_role diff --git a/app/views/layouts/_time.haml b/app/views/layouts/_time.haml new file mode 100644 index 00000000..4fa3151a --- /dev/null +++ b/app/views/layouts/_time.haml @@ -0,0 +1 @@ +%time{ datetime: time, title: time }= time_ago_in_words time diff --git a/app/views/sites/fetch.haml b/app/views/sites/fetch.haml new file mode 100644 index 00000000..ac6c66b1 --- /dev/null +++ b/app/views/sites/fetch.haml @@ -0,0 +1,34 @@ +.row + .col + = render 'layouts/breadcrumb', + crumbs: [link_to(t('sites.index'), sites_path), t('.title')] +.row.justify-content-center + .col-md-8#pull + %h1= t('.title') + %p.lead= sanitize_markdown t('.help.fetch'), tags: %w[em strong a] + + %h2= t('.toc') + %ul.toc + - @commits.each do |commit| + %li= link_to commit.summary, "##{commit.oid}" + +- @commits.each do |commit| + .row.justify-content-center + .col-md-8{ id: commit.oid } + %h1= commit.summary + %p.lead= render 'layouts/time', time: commit.time + + -# + No hay forma de obtener el cuerpo del commit separado del + resumen, cortamos por el primer salto de línea doble y obtenemos + todo lo demás + = sanitize_markdown commit.message.split("\n\n", 2).last, + tags: %w[p a h1 h2 h3 h4 h5 h6 ol ul li strong em] + + %hr + +- unless @commits.empty? + .row.justify-content-center + .col-md-8 + = link_to t('.merge.request'), site_pull_path(@site), + method: 'post', class: 'btn btn-lg btn-success' diff --git a/app/views/sites/index.haml b/app/views/sites/index.haml index b1085caa..d082408c 100644 --- a/app/views/sites/index.haml +++ b/app/views/sites/index.haml @@ -1,7 +1,6 @@ - .row .col - = render 'layouts/breadcrumb', crumbs: [ t('sites.index') ] + = render 'layouts/breadcrumb', crumbs: [t('sites.index')] .row .col %h1 @@ -20,14 +19,15 @@ %h2 - if policy(site).show? = link_to site.name, site_path(site) - - else + - else = site.name - if site.invitade? current_usuarie - %span.badge.badge-warning{data: { toggle: 'tooltip' }, - title: t('help.sites.invitade')} + %span.badge.badge-warning{ data: { toggle: 'tooltip' }, + title: t('help.sites.invitade') } = t('.invitade') %br - .btn-group{role: 'group', 'aria-label': t('sites.actions')} + .btn-group{ role: 'group', + 'aria-label': t('sites.actions') } - if current_usuarie.rol_for_site(site).temporal = button_to t('sites.invitations.accept'), site_usuaries_accept_invitation_path(site), @@ -68,7 +68,8 @@ type: 'secondary', link: nil - else - = form_tag site_enqueue_path(site), method: :post, class: 'form-inline' do + = form_tag site_enqueue_path(site), + method: :post, class: 'form-inline' do = button_tag type: 'submit', class: 'btn btn-success', title: t('help.sites.enqueue'), @@ -85,3 +86,9 @@ text: t('sites.build_log'), type: 'warning', link: site_build_log_path(site) + - if policy(site).pull? && site.needs_pull? + = render 'layouts/btn_with_tooltip', + tooltip: t('help.sites.pull'), + text: t('.pull'), + type: 'info', + link: site_pull_path(site) diff --git a/config/locales/en.yml b/config/locales/en.yml index 64bde6bf..8f64db91 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -158,6 +158,16 @@ en: edit: title: 'Edit %{site}' submit: 'Save changes' + fetch: + title: 'Upgrade the site' + help: + fetch: 'Any changes made to the site are saved into a _git_ repository. Git saves the differences between previous and current versions of files so we can explore them as the history of the project. Also, we can bring and send changes between repositories. In this case, every site managed with Sutty share a common root that we call [skeleton](https://0xacab.org/sutty/skel.sutty.nl). When we upgrade this skeleton, you can explore the changes here and accept them to make your site better.' + toc: 'Table of contents' + merge: + request: 'Upgrade my site with these changes' + success: 'Site upgrade has been completed. Your next build will run this upgrade :)' + error: "There was an error when we were trying to upgrade your site. This could be due to conflicts that couldn't be solved automatically. We've sent a report of the issue to Sutty's admins so they already know about it. Sorry! :(" + message: 'Skeleton upgrade' footer: powered_by: 'is developed by' templates: diff --git a/config/locales/es.yml b/config/locales/es.yml index b0fa8ef5..fc37c162 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -163,6 +163,16 @@ es: edit: title: 'Editar %{site}' submit: 'Guardar cambios' + fetch: + title: 'Actualizar el sitio' + help: + fetch: 'Todos los cambios en el sitio se guardan en un repositorio _git_. En git, se guarda la diferencia entre una versión anterior y la actual de todos los archivos y podemos explorar la historia de un proyecto. Además, podemos traer y enviar cambios con otros repositorios. En este caso, todos los sitios gestionados desde Sutty tienen una raíz común, que llamamos [esqueleto](https://0xacab.org/sutty/skel.sutty.nl). Cuando hacemos cambios en el esqueleto para mejorar los sitios, podés explorar los cambios aquí y aceptarlos.' + toc: 'Tabla de contenidos' + merge: + request: 'Incorporar los cambios en mi sitio' + success: 'Ya se incorporaron los cambios en el sitio, se aplicarán en la próxima compilación que hagas :)' + error: 'Hubo un error al incorporar los cambios en el sitio. Esto puede deberse a conflictos entre cambios que no se pueden resolver automáticamente. Hemos enviado un reporte del problema a les administradores de Sutty para que estén al tanto de la situación. ¡Lo sentimos! :(' + message: 'Actualización del esqueleto' footer: powered_by: 'es desarrollada por' i18n: diff --git a/config/routes.rb b/config/routes.rb index 086fe8c5..3b701878 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Metrics/BlockLength Rails.application.routes.draw do devise_for :usuaries @@ -14,6 +15,10 @@ Rails.application.routes.draw do resources :sites, constraints: { site_id: %r{[^/]+}, id: %r{[^/]+} } do get 'public/:type/:basename', to: 'sites#send_public_file' + # Gestionar actualizaciones del sitio + get 'pull', to: 'sites#fetch' + post 'pull', to: 'sites#merge' + # Gestionar usuaries get 'usuaries/invite', to: 'usuaries#invite' post 'usuaries/invite', to: 'usuaries#send_invitations' @@ -41,3 +46,4 @@ Rails.application.routes.draw do post 'reorder_posts', to: 'sites#reorder_posts' end end +# rubocop:enable Metrics/BlockLength diff --git a/doc/crear_sitios.md b/doc/crear_sitios.md index e7cc3edb..b89e192a 100644 --- a/doc/crear_sitios.md +++ b/doc/crear_sitios.md @@ -96,9 +96,9 @@ El sitio esqueleto es un repositorio Git que se clona al directorio del sitio. Esto permite luego pullear actualizaciones desde el esqueleto a los sitios, esperamos que sin conflictos! -## Plantillas +## Diseño -Las plantillas son plantillas Jekyll adaptadas a Sutty. Vamos a empezar +Los diseños son plantillas Jekyll adaptadas a Sutty. Vamos a empezar adaptando las que estén disponibles en y otras fuentes, agregando features de Sutty y simplificando donde haga falta (algunas plantillas tienen requisitos extraños). @@ -113,3 +113,52 @@ eliminarlo, eliminar el directorio. Lo correcto sería preguntar a todes les usuaries si están de acuerdo en borrar el sitio. Si una no está de acuerdo, el borrado se cancela. + +### Licencias + +Las licencias disponibles se pueden gestionar desde la base de datos. +Los atributos son: + +* Titulo +* URL +* Descripción, por qué la recomendamos, etc. + +El problema que tenemos es que las queremos tener traducidas, entonces +hay varias opciones: + +* Incorporar columna idioma en la base de datos y cada vez que se + muestren las licencias filtrar por idioma actual (o idioma por + defecto). + + Esto nos permitiría ofrecer licencias por jurisdicción también, aunque + empezaríamos con las internacionales... + +* Incorporar la gema de traducción y poner las traducciones en la base + de datos. Esto permite tener una sola licencia con sus distintas + traducciones en un solo registro. + + Pensábamos que necesitábamos cambiar a PostgreSQL, pero la gema + [Mobility](https://github.com/shioyama/mobility) permite usar + distintas estrategias. + +* Incorporar las traducciones a los locales de sutty. Esta es la opción + menos flexible, porque implica agregar licencias a la base de datos y + al mismo tiempo actualizar los archivos y re-deployear Sutty... mejor + no. + +Pero es importante que estén asociadas entre sí por idioma. + +Permitir que les usuaries elijan licencia+privacidad+codigo de +convivencia e informarles que van a ser los primeros artículos dentro de +su sitio y que los pueden modificar después. Que esta es nuestra +propuesta tecnopolítica para que los espacios digitales y analógicos +sean espacios amables siguiente una lógica de cuidados colectivos. + +## Actualizar skel + +Cuando actualizamos el skel, sutty pide a todos los sitios +consentimiento para aplicar las actualizaciones. Antes de aplicar las +actualizaciones muestra el historial para que les usuaries vean cuales +son los cambios que se van a aplicar. Este historial viene del +repositorio git con lo que tenemos que tomarnos la costumbre de escribir +commits completos con explicación. diff --git a/test/models/site/repository_test.rb b/test/models/site/repository_test.rb new file mode 100644 index 00000000..f23925ad --- /dev/null +++ b/test/models/site/repository_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class RepositoryTest < ActiveSupport::TestCase + setup do + @rol = create :rol + @site = @rol.site + @usuarie = @rol.usuarie + + # Volver al principio para poder traer cambios + Dir.chdir(@site.path) do + `git reset --hard e0627e34c6ef6ae2592d7f289b82def20ba56685` + end + end + + teardown do + @site.destroy + end + + test 'se pueden traer cambios' do + assert @site.repository.fetch.is_a?(Integer) + end + + test 'se pueden mergear los cambios' do + assert !@site.repository.commits.empty? + assert @site.repository.merge(@usuarie) + assert @site.repository.commits.empty? + + assert_equal @usuarie.name, + @site.repository.rugged + .branches['master'].target.committer[:name] + + Dir.chdir(@site.path) do + assert_equal 'nothing to commit, working tree clean', + `LC_ALL=C git status`.strip.split("\n").last + end + end +end