diff --git a/Gemfile b/Gemfile index cb78d4c3..788637eb 100644 --- a/Gemfile +++ b/Gemfile @@ -69,6 +69,7 @@ gem 'terminal-table' gem 'validates_hostname' gem 'webpacker' gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' +gem 'kaminari' # database gem 'hairtrigger' diff --git a/Gemfile.lock b/Gemfile.lock index 8846d2a5..d8dd4fb4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -309,6 +309,18 @@ GEM jekyll-write-and-commit-changes (0.2.0) jekyll (~> 4) rugged (~> 1) + kaminari (1.2.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.1) + kaminari-activerecord (= 1.2.1) + kaminari-core (= 1.2.1) + kaminari-actionview (1.2.1) + actionview + kaminari-core (= 1.2.1) + kaminari-activerecord (1.2.1) + activerecord + kaminari-core (= 1.2.1) + kaminari-core (1.2.1) kramdown (2.3.1) rexml kramdown-parser-gfm (1.1.0) @@ -669,6 +681,7 @@ DEPENDENCIES jekyll-data! jekyll-images jekyll-include-cache + kaminari letter_opener listen (>= 3.0.5, < 3.2) loaf diff --git a/Makefile b/Makefile index 36139d7a..592592bd 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,6 @@ ota-js: assets ## Actualizar Javascript en el nodo delegado ota: ## Actualizar Rails en el nodo delegado umask 022; git format-patch $(commit) - scp ./0*.patch $(delegate):/tmp/ ssh $(delegate) mkdir -p /tmp/patches-$(commit)/ scp ./0*.patch $(delegate):/tmp/patches-$(commit)/ scp ./ota.sh $(delegate):/tmp/ diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 3ef26720..dbdd4d0a 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -22,21 +22,21 @@ class PostsController < ApplicationController # XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es # más simple saber si hubo cambios. - if stale?([current_usuarie, site, filter_params]) - # Todos los artículos de este sitio para el idioma actual - @posts = site.indexed_posts.where(locale: locale) - # De este tipo - @posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout] - # Que estén dentro de la categoría - @posts = @posts.in_category(filter_params[:category]) if filter_params[:category] - # Aplicar los parámetros de búsqueda - @posts = @posts.search(locale, filter_params[:q]) if filter_params[:q].present? - # A los que este usuarie tiene acceso - @posts = PostPolicy::Scope.new(current_usuarie, @posts).resolve + return unless stale?([current_usuarie, site, filter_params]) - # Filtrar los posts que les invitades no pueden ver - @usuarie = site.usuarie? current_usuarie - end + # Todos los artículos de este sitio para el idioma actual + @posts = site.indexed_posts.where(locale: locale) + # De este tipo + @posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout] + # Que estén dentro de la categoría + @posts = @posts.in_category(filter_params[:category]) if filter_params[:category] + # Aplicar los parámetros de búsqueda + @posts = @posts.search(locale, filter_params[:q]) if filter_params[:q].present? + # A los que este usuarie tiene acceso + @posts = PostPolicy::Scope.new(current_usuarie, @posts).resolve + + # Filtrar los posts que les invitades no pueden ver + @usuarie = site.usuarie? current_usuarie end def show @@ -54,7 +54,7 @@ class PostsController < ApplicationController def new authorize Post - @post = site.posts.build(lang: locale, layout: params[:layout]) + @post = site.posts(lang: locale).build(layout: params[:layout]) breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), '' end @@ -154,7 +154,9 @@ class PostsController < ApplicationController # # @return [Hash] def filter_params - @filter_params ||= params.permit(:q, :category, :layout).to_h.select { |_, v| v.present? } + @filter_params ||= params.permit(:q, :category, :layout).to_hash.select do |_, v| + v.present? + end.transform_keys(&:to_sym) end def site diff --git a/app/models/metadata_boolean.rb b/app/models/metadata_boolean.rb index 5e4b456f..90c002a7 100644 --- a/app/models/metadata_boolean.rb +++ b/app/models/metadata_boolean.rb @@ -25,10 +25,19 @@ class MetadataBoolean < MetadataTemplate # * false # * true def value - return document.data.fetch(name.to_s, default_value) if self[:value].nil? - return self[:value] unless self[:value].is_a? String + case self[:value] + when NilClass + document.data.fetch(name.to_s, default_value) + when String + true_values.include? self[:value] + else + self[:value] + end + end - self[:value] = true_values.include? self[:value] + # Siempre guardar el valor de este campo a menos que sea nulo + def empty? + value.nil? end private diff --git a/app/models/metadata_content.rb b/app/models/metadata_content.rb index 437a0dd9..9d3a1040 100644 --- a/app/models/metadata_content.rb +++ b/app/models/metadata_content.rb @@ -56,7 +56,7 @@ class MetadataContent < MetadataTemplate uri = URI element['src'] # No permitimos recursos externos - element.remove unless uri.hostname.end_with? Site.domain + element.remove unless uri.scheme == 'https' && uri.hostname.end_with?(Site.domain) rescue URI::Error element.remove end diff --git a/app/models/metadata_float.rb b/app/models/metadata_float.rb new file mode 100644 index 00000000..b4288a3b --- /dev/null +++ b/app/models/metadata_float.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Un campo numérico de punto flotante +class MetadataFloat < MetadataTemplate + # Nada + def default_value + super || nil + end + + def save + return true unless changed? + + self[:value] = value.to_f + self[:value] = encrypt(value) if private? + + true + end + + # Indicarle al navegador que acepte números decimales + # + # @return [Float] + def step + 0.05 + end + + private + + def decrypt(value) + super(value).to_f + end +end diff --git a/app/models/metadata_predefined_value.rb b/app/models/metadata_predefined_value.rb new file mode 100644 index 00000000..9cf36382 --- /dev/null +++ b/app/models/metadata_predefined_value.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Un campo de texto seleccionado de una lista de valores posibles +class MetadataPredefinedValue < MetadataString + # Obtiene todos los valores desde el layout, en un formato compatible + # con options_for_select. + # + # @return [Hash] + def values + @values ||= layout.dig(:metadata, name, 'values', I18n.locale.to_s)&.invert || {} + end + + private + + # Solo permite almacenar los valores predefinidos. + # + # @return [String] + def sanitize(string) + v = super string + return '' unless values.values.include? v + + v + end +end diff --git a/app/models/site.rb b/app/models/site.rb index 58f20745..ddfe2bc9 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -65,9 +65,6 @@ class Site < ApplicationRecord accepts_nested_attributes_for :deploys, allow_destroy: true - # El sitio en Jekyll - attr_reader :jekyll - # XXX: Es importante incluir luego de los callbacks de :load_jekyll include Site::Index @@ -180,29 +177,28 @@ class Site < ApplicationRecord # Trae los datos del directorio _data dentro del sitio def data - unless @jekyll.data.present? - @jekyll.reader.read_data - - # Define los valores por defecto según la llave buscada - @jekyll.data.default_proc = proc do |data, key| - data[key] = case key - when 'layout' then {} - end + unless jekyll.data.present? + run_in_path do + jekyll.reader.read_data + jekyll.data['layouts'] ||= {} end end - @jekyll.data + jekyll.data end # Traer las colecciones. Todos los artículos van a estar dentro de # colecciones. def collections unless @read - @jekyll.reader.read_collections + run_in_path do + jekyll.reader.read_collections + end + @read = true end - @jekyll.collections + jekyll.collections end # Traer la configuración de forma modificable @@ -290,7 +286,9 @@ class Site < ApplicationRecord # # @return [Hash] def theme_layouts - @jekyll.reader.read_layouts + run_in_path do + jekyll.reader.read_layouts + end end # Trae todos los valores disponibles para un campo @@ -332,6 +330,12 @@ class Site < ApplicationRecord status == 'building' end + def jekyll + run_in_path do + @jekyll ||= Jekyll::Site.new(configuration) + end + end + # Cargar el sitio Jekyll # # TODO: En lugar de leer todo junto de una vez, extraer la carga de @@ -345,10 +349,7 @@ class Site < ApplicationRecord def reload_jekyll! reset - - Dir.chdir(path) do - @jekyll = Jekyll::Site.new(configuration) - end + jekyll end def reload @@ -526,4 +527,8 @@ class Site < ApplicationRecord errors.add(:design_id, I18n.t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.error')) end + + def run_in_path(&block) + Dir.chdir path, &block + end end diff --git a/app/views/posts/attribute_ro/_float.haml b/app/views/posts/attribute_ro/_float.haml new file mode 100644 index 00000000..67642e2c --- /dev/null +++ b/app/views/posts/attribute_ro/_float.haml @@ -0,0 +1,3 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td{ dir: dir, lang: locale }= metadata.value diff --git a/app/views/posts/attribute_ro/_predefined_value.haml b/app/views/posts/attribute_ro/_predefined_value.haml new file mode 100644 index 00000000..67642e2c --- /dev/null +++ b/app/views/posts/attribute_ro/_predefined_value.haml @@ -0,0 +1,3 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td{ dir: dir, lang: locale }= metadata.value diff --git a/app/views/posts/attributes/_float.haml b/app/views/posts/attributes/_float.haml new file mode 100644 index 00000000..6239c613 --- /dev/null +++ b/app/views/posts/attributes/_float.haml @@ -0,0 +1,6 @@ +.form-group + = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post) + = number_field base, attribute, value: metadata.value, step: metadata.step, + **field_options(attribute, metadata) + = render 'posts/attribute_feedback', + post: post, attribute: attribute, metadata: metadata diff --git a/app/views/posts/attributes/_predefined_value.haml b/app/views/posts/attributes/_predefined_value.haml new file mode 100644 index 00000000..b0d21f35 --- /dev/null +++ b/app/views/posts/attributes/_predefined_value.haml @@ -0,0 +1,7 @@ +.form-group + = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post) + = select_tag(plain_field_name_for(base, attribute), + options_for_select(metadata.values, metadata.value), + **field_options(attribute, metadata), include_blank: t('.empty')) + = 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 8b776590..ad07b9dc 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -40,7 +40,7 @@ %section.col = render 'layouts/flash' .d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2 - %form + %form{ action: site_posts_path } - @filter_params.each do |param, value| - next if param == 'q' %input{ type: 'hidden', name: param, value: value } @@ -51,7 +51,7 @@ - if @site.locales.size > 1 %nav#locales - @site.locales.each do |locale| - = link_to t("locales.#{locale}.name"), site_posts_path(@site, **@filter_params.merge(locale: locale)), + = link_to @site.data.dig(locale.to_s, 'locale') || locale, site_posts_path(@site, **@filter_params.merge(locale: locale)), class: "mr-2 mt-2 mb-2 #{locale == @locale ? 'active font-weight-bold' : ''}" .pl-2-plus - @filter_params.each do |param, value| @@ -72,14 +72,18 @@ %thead %tr %th.border-0.background-white.position-sticky{ style: 'top: 0; z-index: 2', colspan: '4' } - = submit_tag t('posts.reorder.submit'), class: 'btn' - %button.btn{ data: { action: 'reorder#unselect' } } - = t('posts.reorder.unselect') - %span.badge{ data: { target: 'reorder.counter' } } 0 - %button.btn{ data: { action: 'reorder#up' } }= t('posts.reorder.up') - %button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down') - %button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top') - %button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom') + .d-flex.flex-row.justify-content-between + %div + = submit_tag t('posts.reorder.submit'), class: 'btn' + %button.btn{ data: { action: 'reorder#unselect' } } + = t('posts.reorder.unselect') + %span.badge{ data: { target: 'reorder.counter' } } 0 + %button.btn{ data: { action: 'reorder#up' } }= t('posts.reorder.up') + %button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down') + %button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top') + %button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom') + + %div %tbody - dir = t("locales.#{@locale}.dir") - size = @posts.size @@ -104,19 +108,19 @@ %span{ lang: post.locale, dir: dir }= post.title - if post.front_matter['draft'].present? %span.badge.badge-primary= I18n.t('posts.attributes.draft.label') - - if post.front_matter['categories'].present? - %br - %small - - post.front_matter['categories'].each do |category| - = link_to site_posts_path(@site, **@filter_params.merge(category: category)) do - %span{ lang: post.locale, dir: dir }= category - = '/' unless post.front_matter['categories'].last == category + %br + %small + = link_to @site.layouts[post.layout].humanized_name, site_posts_path(@site, **@filter_params.merge(layout: post.layout)) + - post.front_matter['categories']&.each do |category| + = link_to site_posts_path(@site, **@filter_params.merge(category: category)) do + %span{ lang: post.locale, dir: dir }= category + = '/' unless post.front_matter['categories'].last == category - %td + %td.text-nowrap = post.created_at.strftime('%F') %br/ = post.order - %td + %td.text-nowrap - if @usuarie || policy(post).edit? = link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block' - if @usuarie || policy(post).destroy? diff --git a/config/locales/en.yml b/config/locales/en.yml index fc194eab..43ab0d0a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,8 +19,8 @@ en: remember_me: 'Keeps session open for %{remember_for}' actions: sr-help: "After this form you'll find links to recover your account and other actions." - _true: Yes - _false: No + _true: 'Yes' + _false: 'No' svg: sutty: title: Sutty @@ -163,7 +163,7 @@ en: signature: 'With love, Sutty' breadcrumb: title: 'Your location in Sutty' - logout: Exit + logout: Log out mutual_aid: Mutual aid collaborations: collaborate: @@ -376,6 +376,8 @@ en: en: 'English' ar: 'Arabic' posts: + prev: Previous page + next: Next page empty: "There are no results for those search parameters." caption: Post list attribute_ro: @@ -413,6 +415,8 @@ en: destroy: Remove image belongs_to: empty: "(Empty)" + predefined_value: + empty: "(Empty)" draft: label: Draft reorder: diff --git a/config/locales/es.yml b/config/locales/es.yml index e8185391..880b9e7c 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -19,8 +19,8 @@ es: remember_me: 'Mantiene la sesión abierta por %{remember_for}' actions: sr-help: 'Después del formulario encontrarás vínculos para recuperar tu cuenta, entre otras acciones.' - _true: Sí - _false: No + _true: 'Sí' + _false: 'No' svg: sutty: title: Sutty @@ -163,7 +163,7 @@ es: signature: 'Con cariño, Sutty' breadcrumb: title: 'Tu ubicación en Sutty' - logout: Salir + logout: Cerrar sesión mutual_aid: Ayuda mutua collaborations: collaborate: @@ -384,6 +384,8 @@ es: en: 'inglés' ar: 'árabe' posts: + prev: Página anterior + next: Página siguiente empty: No hay artículos con estos parámetros de búsqueda. caption: Lista de artículos attribute_ro: @@ -421,6 +423,8 @@ es: destroy: 'Eliminar imagen' belongs_to: empty: "(Vacío)" + predefined_value: + empty: "(Vacío)" draft: label: Borrador reorder: diff --git a/config/routes.rb b/config/routes.rb index 2c5f1c60..5e172cda 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -57,9 +57,10 @@ Rails.application.routes.draw do # Gestionar artículos según idioma nested do - scope '(:locale)' do + scope '/(:locale)', constraint: /[a-z]{2}/ do post :'posts/reorder', to: 'posts#reorder' resources :posts do + get 'p/:page', action: :index, on: :collection get :preview, to: 'posts#preview' end end diff --git a/db/migrate/20210926205448_add_uniqueness.rb b/db/migrate/20210926205448_add_uniqueness.rb new file mode 100644 index 00000000..7399ba4c --- /dev/null +++ b/db/migrate/20210926205448_add_uniqueness.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Agrega índices únicos que pensábamos que ya existían. +class AddUniqueness < ActiveRecord::Migration[6.1] + def change + add_index :designs, :name, unique: true + add_index :designs, :gem, unique: true + add_index :licencias, :name, unique: true + end +end