diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c72a632d..8be521c7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -46,6 +46,7 @@ assets:
- "rails"
- "production.panel.sutty.nl"
- "panel.sutty.nl"
+ - "panel.testing.sutty.nl"
except:
- "schedules"
cache:
diff --git a/Dockerfile b/Dockerfile
index 394a81e5..a73a96cc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,8 +22,6 @@ RUN apk add npm && npm install -g pnpm@~7 && apk del npm
COPY ./monit.conf /etc/monit.d/sutty.conf
-RUN apk add npm && npm install -g pnpm && apk del npm
-
VOLUME "/srv"
EXPOSE 3000
diff --git a/Gemfile b/Gemfile
index 3677d738..59823dd0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -118,9 +118,8 @@ group :development, :test do
gem 'derailed_benchmarks'
gem 'dotenv-rails'
gem 'pry'
- # Adds support for Capybara system testing and selenium driver
- gem 'capybara', '~> 2.13'
- gem 'selenium-webdriver', '~> 4.8.0'
+ gem 'capybara'
+ gem 'selenium-webdriver'
gem 'sqlite3'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 2adf2f1b..5cbecf5c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -118,13 +118,15 @@ GEM
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
- capybara (2.18.0)
+ capybara (3.40.0)
addressable
+ matrix
mini_mime (>= 0.1.3)
- nokogiri (>= 1.3.3)
- rack (>= 1.0.0)
- rack-test (>= 0.5.4)
- xpath (>= 2.0, < 4.0)
+ nokogiri (~> 1.11)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ regexp_parser (>= 1.5, < 3.0)
+ xpath (~> 3.2)
chartkick (5.0.2)
climate_control (1.2.0)
coderay (1.1.3)
@@ -360,13 +362,14 @@ GEM
net-pop
net-smtp
marcel (1.0.2)
+ matrix (0.4.2)
memory_profiler (1.0.1)
mercenary (0.4.0)
method_source (1.0.0)
mini_histogram (0.3.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
- mini_portile2 (2.8.5)
+ mini_portile2 (2.8.6)
minitest (5.21.1)
mobility (1.2.9)
i18n (>= 0.6.10, < 2)
@@ -386,7 +389,7 @@ GEM
net-ssh (7.2.1)
netaddr (2.0.6)
nio4r (2.7.0-x86_64-linux-musl)
- nokogiri (1.16.0-x86_64-linux-musl)
+ nokogiri (1.16.5-x86_64-linux-musl)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
orm_adapter (0.5.0)
@@ -407,7 +410,7 @@ GEM
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
- public_suffix (5.0.4)
+ public_suffix (5.0.5)
puma (6.4.2-x86_64-linux-musl)
nio4r (~> 2.0)
pundit (2.3.1)
@@ -486,7 +489,7 @@ GEM
redis-store (>= 1.2, < 2)
redis-store (1.9.2)
redis (>= 4, < 6)
- regexp_parser (2.9.0)
+ regexp_parser (2.9.2)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.1)
@@ -542,7 +545,7 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
- selenium-webdriver (4.8.6)
+ selenium-webdriver (4.9.1)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@@ -632,7 +635,7 @@ DEPENDENCIES
bootstrap (~> 4)
brakeman
bundler-audit
- capybara (~> 2.13)
+ capybara
chartkick
commonmarker
concurrent-ruby-ext
@@ -707,7 +710,7 @@ DEPENDENCIES
safe_yaml
safely_block (~> 0.3.0)
sassc-rails
- selenium-webdriver (~> 4.8.0)
+ selenium-webdriver
sourcemap
spring
spring-watcher-listen
diff --git a/Procfile b/Procfile
index a74f613b..d3d8207d 100644
--- a/Procfile
+++ b/Procfile
@@ -1,13 +1,6 @@
-migrate: bundle exec rake db:prepare db:seed
-sutty: bundle exec puma config.ru
-blazer_5m: bundle exec rake blazer:run_checks SCHEDULE="5 minutes"
-blazer_1h: bundle exec rake blazer:run_checks SCHEDULE="1 hour"
-blazer_1d: bundle exec rake blazer:run_checks SCHEDULE="1 day"
-blazer: bundle exec rake blazer:send_failing_checks
-prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
-distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
cleanup: bundle exec rake cleanup:everything
+distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
emergency_cleanup: bundle exec rake cleanup:everything BEFORE=7
-stats: bundle exec rake stats:process_all
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que
+stats: bundle exec rake stats:process_all
fediblock: bundle exec rails activity_pub:fediblocks
diff --git a/_sites/_storage/.keep b/_sites/_storage/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 4d1d0848..f90e967a 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -20,6 +20,8 @@ $form-feedback-valid-color: $black;
$form-feedback-invalid-color: $magenta;
$form-feedback-icon-valid-color: $black;
$component-active-bg: $magenta;
+$btn-white-space: nowrap;
+$font-weight-bolder: 700;
$spacers: (
2-plus: 0.75rem
@@ -51,7 +53,7 @@ $sizes: (
.editor {
.editor-content {
figure {
- border: 1px solid transparentize($magenta, 0.3)
+ border: 1px solid transparentize($magenta, 0.3);
}
}
}
@@ -87,6 +89,26 @@ $sizes: (
box-shadow: 0 0 0 0.2rem $cyan;
}
}
+
+ a.black {
+ color: $white;
+
+ &:active {
+ color: var(--color);
+ }
+ }
+
+ form.was-validated {
+ div.border {
+ div.custom-control {
+ div {
+ .custom-control-label {
+ color: $white;
+ }
+ }
+ }
+ }
+ }
}
// TODO: Encontrar la forma de generar esto desde los locales de Rails
@@ -135,6 +157,10 @@ a {
color: var(--color);
}
+ &:focus {
+ outline: 1px solid var(--color);
+ }
+
&[target=_blank] {
/* TODO: Convertir a base64 para no hacer peticiones extra */
&:after {
@@ -143,6 +169,8 @@ a {
}
}
+
+
$footer-height: 60px;
/* Colores */
@@ -256,6 +284,10 @@ svg {
}
}
+.badge {
+ white-space: break-spaces;
+}
+
.btn-sm {
@extend .badge
}
@@ -527,6 +559,7 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
color: var(--#{$color});
}
+
::-moz-selection,
::selection {
background: var(--#{$color});
@@ -551,6 +584,7 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
a {
color: var(--#{$color});
}
+
}
}
@@ -620,4 +654,32 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
}
}
}
+}
+
+
+hr {
+ border-bottom: 1px solid #dee2e6;
}
+
+a.black {
+ &:active {
+ color: var(--color);
+ }
+ &:focus {
+ color: var(--color);
+ }
+}
+
+.break-all {
+ word-break: break-all;
+}
+
+.details-agregar {
+ @extend .d-flex;
+ @extend .border;
+ @extend .border-magenta;
+ @extend .justify-content-between;
+ @extend .align-items-center;
+ @extend .w-100;
+ @extend .mb-3;
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/dark.scss b/app/assets/stylesheets/dark.scss
index f7f3a09d..a04360f9 100644
--- a/app/assets/stylesheets/dark.scss
+++ b/app/assets/stylesheets/dark.scss
@@ -31,4 +31,17 @@ $cyan: #13fefe;
}
}
+a.black {
+ color: $white;
+ &:hover {
+ color: var(--color);
+ }
+ &:active {
+ color: var(--color);
+ }
+ &:focus {
+ color: var(--color);
+ }
+}
+
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 117be995..f9d1a246 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -86,7 +86,7 @@ class ApplicationController < ActionController::Base
end
def site
- @site ||= find_site
+ @site ||= find_site.tap(&:reindex_changes!)
end
protected
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 057c3068..2d5c5a1e 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -55,9 +55,11 @@ class PostsController < ApplicationController
def new
authorize Post
- @post = site.posts(lang: locale).build(layout: params[:layout])
- breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), ''
+ layout = site.layouts[params[:layout].to_sym]
+ @post = Post.build(locale: locale, layout: layout, site: site)
+
+ breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: layout.humanized_name.downcase), ''
end
def create
@@ -155,13 +157,13 @@ class PostsController < ApplicationController
#
# @return [Hash]
def filter_params
- @filter_params ||= params.permit(:q, :category, :layout, :page).to_hash.select do |_, v|
+ @filter_params ||= params.permit(:q, :category, :page, layout: []).to_hash.select do |_, v|
v.present?
end.transform_keys(&:to_sym)
end
def post
- @post ||= site.posts(lang: locale).find(params[:post_id] || params[:id])
+ @post ||= site.indexed_posts.find_by!(locale: locale, path: params[:post_id] || params[:id]).post
end
# Recuerda el nombre del servicio de subida de archivos
diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb
index c2c7bc58..6934b7aa 100644
--- a/app/controllers/stats_controller.rb
+++ b/app/controllers/stats_controller.rb
@@ -175,7 +175,7 @@ class StatsController < ApplicationController
locale: I18n.locale,
empty: I18n.t('stats.index.empty', **please_return_at),
loading: I18n.t('stats.index.loading'),
- html: %(
%{loading}
)
+ html: %(%s
)
}
end
@@ -209,7 +209,7 @@ class StatsController < ApplicationController
#
# @return [Integer]
def nodes
- @nodes ||= ENV.fetch('NODES', 1).to_i
+ @nodes ||= Rails.application.nodes.size + 1
end
def period
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index fcbd4074..7b609c03 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -19,6 +19,24 @@ module ApplicationHelper
[root, name]
end
+ # Devuelve los params sin el valor para una llave, detectando si el
+ # valor es un array.
+ #
+ # @param filtering_params [Hash]
+ # @param key [Symbol,String]
+ # @param value [Any]
+ def filter_params_by(filtering_params, key, value)
+ filtering_params.map do |k, v|
+ if k == key
+ case v
+ when Array then [k, v - [value]]
+ end
+ else
+ [k, v]
+ end
+ end.compact.to_h
+ end
+
def plain_field_name_for(*names)
root, name = field_name_for(*names)
diff --git a/app/jobs/deploy_job.rb b/app/jobs/deploy_job.rb
index 66cccd1b..b91f4d0d 100644
--- a/app/jobs/deploy_job.rb
+++ b/app/jobs/deploy_job.rb
@@ -77,6 +77,8 @@ class DeployJob < ApplicationJob
t << ([type.to_s] + row.values)
end
end)
+ rescue DeployTimedOutException => e
+ notify_exception e
ensure
if site.present?
site.update status: 'waiting'
diff --git a/app/jobs/git_pull_job.rb b/app/jobs/git_pull_job.rb
index 72e20be0..30431495 100644
--- a/app/jobs/git_pull_job.rb
+++ b/app/jobs/git_pull_job.rb
@@ -1,18 +1,29 @@
# frozen_string_literal: true
-# Permite traer los cambios desde webhooks
-
+# Permite traer los cambios desde el repositorio remoto
class GitPullJob < ApplicationJob
# @param :site [Site]
# @param :usuarie [Usuarie]
+ # @param :message [String]
# @return [nil]
- def perform(site, usuarie)
+ def perform(site, usuarie, message)
@site = site
return unless site.repository.origin
- return unless site.repository.fetch.positive?
- site.repository.merge(usuarie)
+ site.repository.fetch
+
+ return if site.repository.up_to_date?
+
+ if site.repository.fast_forward?
+ site.repository.fast_forward!
+ else
+ site.repository.merge(usuarie, message)
+ end
+
+ site.repository.git_lfs_checkout
site.reindex_changes!
+
+ nil
end
end
diff --git a/app/models/concerns/metadata/inverse_concern.rb b/app/models/concerns/metadata/inverse_concern.rb
new file mode 100644
index 00000000..aa300fa7
--- /dev/null
+++ b/app/models/concerns/metadata/inverse_concern.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Metadata
+ module InverseConcern
+ extend ActiveSupport::Concern
+
+ included do
+ # Hay una relación inversa?
+ #
+ # @return [Boolean]
+ def inverse?
+ inverse.present?
+ end
+
+ # La relación inversa
+ #
+ # @return [Nil,Symbol]
+ def inverse
+ @inverse ||= layout.metadata.dig(name, 'inverse')&.to_sym
+ end
+ end
+ end
+end
diff --git a/app/models/deploy_distributed_press.rb b/app/models/deploy_distributed_press.rb
index bbd5a9a0..9fb356dc 100644
--- a/app/models/deploy_distributed_press.rb
+++ b/app/models/deploy_distributed_press.rb
@@ -17,13 +17,13 @@ class DeployDistributedPress < Deploy
before_create :create_remote_site!
before_destroy :delete_remote_site!
- DEPENDENCIES = %i[deploy_local]
+ DEPENDENCIES = %i[deploy_local].freeze
# Actualiza la información y luego envía los cambios
#
# @param :output [Bool]
# @return [Bool]
- def deploy
+ def deploy(output: true)
status = false
log = []
@@ -32,9 +32,7 @@ class DeployDistributedPress < Deploy
create_remote_site! if remote_site_id.blank?
save
- if remote_site_id.blank?
- raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press'
- end
+ raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press' if remote_site_id.blank?
site_client.tap do |c|
stdout = Thread.new(publisher.logger_out) do |io|
@@ -54,7 +52,7 @@ class DeployDistributedPress < Deploy
end
if status
- self.remote_info[:distributed_press] = c.show(publishing_site).to_h
+ remote_info[:distributed_press] = c.show(publishing_site).to_h
save
end
@@ -123,7 +121,10 @@ class DeployDistributedPress < Deploy
#
# @return [DistributedPressPublisher::V1::Schemas::NewSite]
def create_site
- DistributedPress::V1::Schemas::NewSite.new.call(domain: hostname, protocols: { http: true, ipfs: true, hyper: true })
+ DistributedPress::V1::Schemas::NewSite.new.call(domain: hostname,
+ protocols: {
+ http: true, ipfs: true, hyper: true
+ })
end
# Crea el sitio en la instancia con el hostname especificado
@@ -149,7 +150,7 @@ class DeployDistributedPress < Deploy
# @param log [String]
# @return [nil]
def create_stat!(status, log)
- build_stats.create action: publisher.to_s,log: log, seconds: time_spent_in_seconds, bytes: size, status: status
+ build_stats.create action: publisher.to_s, log: log, seconds: time_spent_in_seconds, bytes: size, status: status
nil
end
diff --git a/app/models/deploy_local.rb b/app/models/deploy_local.rb
index 29a31f8c..6a959246 100644
--- a/app/models/deploy_local.rb
+++ b/app/models/deploy_local.rb
@@ -9,8 +9,8 @@ class DeployLocal < Deploy
def bundle(output: false)
run %(bundle config set --local clean 'true'), output: output
- run(%(bundle config set --local deployment 'true'), output: output) if site.gemfile_lock_path?
- run %(bundle config set --local path '#{gems_dir}'), output: output
+ run %(bundle config set --local deployment 'true'), output: output if site.gemfile_lock_path?
+ run %(bundle config set --local path '#{site.bundle_path}'), output: output
run %(bundle config set --local without 'test development'), output: output
run %(bundle config set --local cache_all 'false'), output: output
run %(bundle install), output: output
@@ -86,7 +86,7 @@ class DeployLocal < Deploy
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
'YARN_CACHE_FOLDER' => yarn_cache_dir,
- 'GEMS_SOURCE' => ENV['GEMS_SOURCE']
+ 'GEMS_SOURCE' => ENV.fetch('GEMS_SOURCE', nil)
}
end
diff --git a/app/models/deploy_reindex.rb b/app/models/deploy_reindex.rb
deleted file mode 100644
index f3eb3d23..00000000
--- a/app/models/deploy_reindex.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-# Reindexa los artículos al terminar la compilación
-class DeployReindex < Deploy
- def deploy(**)
- time_start
-
- site.reset
-
- Site.transaction do
- site.indexed_posts.destroy_all
- site.index_posts!
- end
-
- time_stop
-
- build_stats.create action: 'reindex',
- log: 'Reindex',
- seconds: time_spent_in_seconds,
- bytes: size,
- status: true
- site.touch
- end
-
- def size
- 0
- end
-
- def limit
- 1
- end
-
- def hostname; end
-
- def url; end
-
- def destination; end
-end
diff --git a/app/models/deploy_rsync.rb b/app/models/deploy_rsync.rb
index fcc5a65d..4e8e3366 100644
--- a/app/models/deploy_rsync.rb
+++ b/app/models/deploy_rsync.rb
@@ -5,7 +5,7 @@
class DeployRsync < Deploy
store :values, accessors: %i[hostname destination host_keys], coder: JSON
- DEPENDENCIES = %i[deploy_local deploy_zip]
+ DEPENDENCIES = %i[deploy_local deploy_zip].freeze
def deploy(output: false)
ssh? && rsync(output: output)
@@ -39,6 +39,7 @@ class DeployRsync < Deploy
# @return [Boolean]
def ssh?
return true if destination.start_with? 'rsync://'
+
user, host = user_host
ssh_available = false
@@ -66,7 +67,7 @@ class DeployRsync < Deploy
{
'HOME' => home_dir,
'PATH' => '/usr/bin',
- 'LANG' => ENV['LANG']
+ 'LANG' => ENV.fetch('LANG', nil)
}
end
@@ -81,7 +82,7 @@ class DeployRsync < Deploy
#
# @return [Array]
def user_host
- destination.split(':', 2).first.split('@', 2).tap do |d|
+ @user_host ||= destination.split(':', 2).first.split('@', 2).tap do |d|
next unless d.size == 1
d.insert(0, nil)
@@ -92,7 +93,8 @@ class DeployRsync < Deploy
#
# @return [Boolean]
def rsync(output: false)
- run %(rsync -aviH --delete-after --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/), output: output
+ run %(rsync -aviH --delete-after --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/),
+ output: output
end
# El origen es el destino de la compilación
diff --git a/app/models/indexed_post.rb b/app/models/indexed_post.rb
index 184cd05f..2ad1d459 100644
--- a/app/models/indexed_post.rb
+++ b/app/models/indexed_post.rb
@@ -34,15 +34,66 @@ class IndexedPost < ApplicationRecord
scope :in_category, ->(category) { where("front_matter->'categories' ? :category", category: category.to_s) }
scope :by_usuarie, ->(usuarie) { where("front_matter->'usuaries' @> :usuarie::jsonb", usuarie: usuarie.to_s) }
+ # Trae todos los valores únicos para un atributo
+ #
+ # @param :attribute [String,Symbol]
+ # @return [Array]
+ scope :everything_of, lambda { |attribute|
+ where('front_matter ? :attribute', attribute: attribute)
+ .pluck(
+ Arel.sql(
+ ActiveRecord::Base.sanitize_sql(['front_matter -> :attribute', { attribute: attribute }])
+ )
+ )
+ .flatten.uniq
+ }
+
+ validates_presence_of :layout, :path, :locale
+
belongs_to :site
- # Encuentra el post original
+ # La ubicación del Post en el disco
#
- # @return [nil,Post]
- def post
- return if post_id.blank?
+ # @return [String]
+ def full_path
+ @full_path ||= File.join(site.path, "_#{locale}", "#{path}.markdown")
+ end
- @post ||= site.posts(lang: locale).find(post_id, uuid: true)
+ # La colección
+ #
+ # @return [Jekyll::Collection]
+ def collection
+ site.collections[locale.to_s]
+ end
+
+ # Obtiene el documento
+ #
+ # @return [Jekyll::Document]
+ def document
+ @document ||= Jekyll::Document.new(full_path, site: site.jekyll, collection: collection)
+ end
+
+ # El Post
+ #
+ # @todo Decidir qué pasa si el archivo ya no existe
+ # @return [Post]
+ def post
+ @post ||= Post.new(document: document, site: site, layout: schema)
+ end
+
+ # Devuelve el esquema de datos
+ #
+ # @todo Renombrar
+ # @return [Layout]
+ def schema
+ site.layouts[layout.to_sym]
+ end
+
+ # Existe físicamente?
+ #
+ # @return [Boolean]
+ def exist?
+ File.exist?(full_path)
end
# Convertir locale a direccionario de PG
diff --git a/app/models/metadata_belongs_to.rb b/app/models/metadata_belongs_to.rb
index be1fa670..eec3ca5a 100644
--- a/app/models/metadata_belongs_to.rb
+++ b/app/models/metadata_belongs_to.rb
@@ -3,6 +3,8 @@
# Almacena el UUID de otro Post y actualiza el valor en el Post
# relacionado.
class MetadataBelongsTo < MetadataRelatedPosts
+ include Metadata::InverseConcern
+
# TODO: Convertir algunos tipos de valores en módulos para poder
# implementar varios tipos de campo sin repetir código
#
@@ -20,83 +22,13 @@ class MetadataBelongsTo < MetadataRelatedPosts
document.data[name.to_s]
end
- def validate
- super
-
- errors << I18n.t('metadata.belongs_to.missing_post') unless post_exists?
-
- errors.empty?
- end
-
- # Guardar y guardar la relación inversa también, eliminando la
- # relación anterior si existía.
- def save
- super
-
- # Si no hay relación inversa, no hacer nada más
- return true unless changed?
- return true unless inverse?
-
- # Si estamos cambiando la relación, tenemos que eliminar la relación
- # anterior
- if belonged_to.present?
- belonged_to[inverse].value = belonged_to[inverse].value.reject do |rej|
- rej == post.uuid.value
- end
- end
-
- # No duplicar las relaciones
- belongs_to[inverse].value = (belongs_to[inverse].value.dup << 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)
- end
-
- # Hay una relación inversa y el artículo existe?
- def inverse?
- inverse.present?
- end
-
- # El campo que es la relación inversa de este
- def inverse
- @inverse ||= layout.metadata.dig(name, 'inverse')&.to_sym
- end
-
- # El Post relacionado con este artículo
- def belongs_to
- posts.find(value, uuid: true) if value.present?
- end
-
- # El artículo relacionado anterior
- def belonged_to
- posts.find(value_was, uuid: true) if value_was.present?
- end
-
- def related_posts?
- true
- end
-
- def related_methods
- @related_methods ||= %i[belongs_to belonged_to].freeze
- end
-
def indexable_values
- belongs_to&.title&.value
+ posts.find_by_post_uuid(value).try(:title)
end
private
- def post_exists?
- return true if sanitize(value).blank?
-
- sanitize(value).present? && belongs_to.present?
- end
-
def sanitize(uuid)
- uuid.to_s.gsub(/[^a-f0-9\-]/i, '')
+ uuid.to_s.gsub(/[^a-f0-9-]/i, '')
end
end
diff --git a/app/models/metadata_content.rb b/app/models/metadata_content.rb
index 444ee2fe..43f8f5ea 100644
--- a/app/models/metadata_content.rb
+++ b/app/models/metadata_content.rb
@@ -53,20 +53,21 @@ class MetadataContent < MetadataTemplate
# Eliminar elementos sin src y comprobar su origen
html.css(elements).each do |element|
- begin
- raise URI::Error unless element['src'].present?
+ raise URI::Error unless element['src'].present?
- uri = URI element['src']
+ uri = URI element['src']
- # No permitimos recursos externos
- raise URI::Error unless Rails.application.config.hosts.include?(uri.hostname)
-
- element['src'] = convert_src_to_internal_path uri
-
- raise URI::Error if element['src'].blank?
- rescue URI::Error
- element.remove
+ # No permitimos recursos externos, solo si sabemos cuales son
+ # los recursos locales
+ if Rails.application.config.hosts.present? && !Rails.application.config.hosts.include?(uri.hostname)
+ raise URI::Error
end
+
+ element['src'] = convert_src_to_internal_path uri
+
+ raise URI::Error if element['src'].blank?
+ rescue URI::Error
+ element.remove
end
# Eliminar figure sin contenido
@@ -96,11 +97,10 @@ class MetadataContent < MetadataTemplate
# @param style [String]
# @return [String]
def sanitize_style(style)
- style.split(';').reduce({}) do |style_hash, style_string|
+ style.split(';').each_with_object({}) do |style_string, style_hash|
key, value = style_string.split(':', 2)
style_hash[key] ||= value
- style_hash
end.slice(*allowed_styles).map do |style_pair|
style_pair.join(':')
end.join(';')
@@ -116,7 +116,7 @@ class MetadataContent < MetadataTemplate
# Convierte una ubicación local al sitio en una URL de ActiveStorage
#
# XXX: Por qué son tan díficiles de encontrar las rutas de AS
- #
+ #
# @param path [String]
# @return [String]
def convert_internal_path_to_src(path)
diff --git a/app/models/metadata_has_and_belongs_to_many.rb b/app/models/metadata_has_and_belongs_to_many.rb
index 2c4f3d43..2c1b0f96 100644
--- a/app/models/metadata_has_and_belongs_to_many.rb
+++ b/app/models/metadata_has_and_belongs_to_many.rb
@@ -1,46 +1,5 @@
# frozen_string_literal: true
-# Establece una relación de muchos a muchos artículos. Cada campo es un
-# Array de UUID que se mantienen sincronizados.
-#
-# Por ejemplo:
-#
-# Un libro puede tener muches autores y une autore muchos libros. La
-# relación has_many tiene que traer todes les autores relacionades con
-# 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.
+# Establece una relación de muchos a muchos artículos
class MetadataHasAndBelongsToMany < MetadataHasMany
- # Mantiene la relación inversa si existe.
- #
- # La relación belongs_to se mantiene actualizada en la modificación
- # actual. Lo que buscamos es mantener sincronizada esa relación.
- #
- # 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
- # XXX: No usamos super
- self[:value] = sanitize value
-
- return true unless changed?
- return true unless inverse?
-
- # XXX: Usamos asignación para aprovechar value= que setea el valor
- # anterior en @value_was
- (had_many - has_many).each do |remove|
- remove[inverse].value = remove[inverse].value.reject do |rej|
- rej == post.uuid.value
- end
- end
-
- (has_many - had_many).each do |add|
- next unless add[inverse]
- next if add[inverse].value.include? post.uuid.value
-
- add[inverse].value = (add[inverse].value.dup << post.uuid.value)
- end
-
- true
- end
end
diff --git a/app/models/metadata_has_many.rb b/app/models/metadata_has_many.rb
index 13f0dcf5..c5f01f7c 100644
--- a/app/models/metadata_has_many.rb
+++ b/app/models/metadata_has_many.rb
@@ -6,55 +6,5 @@
# 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
- def has_many
- return default_value if value.blank?
-
- posts.where(uuid: value)
- end
-
- # La relación anterior
- def had_many
- return default_value if value_was.blank?
-
- posts.where(uuid: value_was)
- end
-
- def inverse?
- inverse.present?
- end
-
- # La relación inversa
- #
- # @return [Nil,Symbol]
- def inverse
- @inverse ||= layout.metadata.dig(name, 'inverse')&.to_sym
- end
-
- # Actualizar las relaciones inversas. Hay que buscar la diferencia
- # entre had y has_many.
- def save
- super
-
- return true unless changed?
- return true unless inverse?
-
- (had_many - has_many).each do |remove|
- remove[inverse]&.value = remove[inverse].default_value
- end
-
- (has_many - had_many).each do |add|
- add[inverse]&.value = post.uuid.value
- end
-
- true
- end
-
- def related_posts?
- true
- end
-
- def related_methods
- @related_methods ||= %i[has_many had_many].freeze
- end
+ include Metadata::InverseConcern
end
diff --git a/app/models/metadata_has_one.rb b/app/models/metadata_has_one.rb
new file mode 100644
index 00000000..0cadb1d9
--- /dev/null
+++ b/app/models/metadata_has_one.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+class MetadataHasOne < MetadataBelongsTo; end
diff --git a/app/models/metadata_locales.rb b/app/models/metadata_locales.rb
index 37b50286..b4ddf485 100644
--- a/app/models/metadata_locales.rb
+++ b/app/models/metadata_locales.rb
@@ -6,11 +6,16 @@ class MetadataLocales < MetadataHasAndBelongsToMany
#
# @return { lang: { title: uuid } }
def values
- @values ||= site.locales.map do |locale|
- [locale, posts.where(lang: locale).map do |post|
- [title(post), post.uuid.value]
- end.to_h]
- end.to_h
+ @values ||= other_locales.to_h do |other_locale|
+ [
+ other_locale,
+ posts.where(locale: other_locale).pluck(:title, :layout, :post_id).to_h do |row|
+ row.tap do |value|
+ value[0] = "#{value[0]} (#{site.layouts[value.delete_at(1)].humanized_name})"
+ end
+ end
+ ]
+ end
end
# Siempre hay una relación inversa
@@ -33,17 +38,13 @@ class MetadataLocales < MetadataHasAndBelongsToMany
#
# @return [Array]
def other_locales
- site.locales.reject do |locale|
- locale == post.lang.value.to_sym
- end
+ @other_locales ||= site.locales - [locale]
end
# Obtiene todos los posts de los otros locales con el mismo layout
#
- # @return [PostRelation]
+ # @return [IndexedPost::ActiveRecord_AssociationRelation]
def posts
- other_locales.map do |locale|
- site.posts(lang: locale).where(layout: post.layout.value)
- end.reduce(&:concat) || PostRelation.new(site: site, lang: 'any')
+ site.indexed_posts(locale: other_locales).where(layout: post.layout.value).where.not(post_id: post.uuid.value)
end
end
diff --git a/app/models/metadata_order.rb b/app/models/metadata_order.rb
index 1b33a388..f63a7d49 100644
--- a/app/models/metadata_order.rb
+++ b/app/models/metadata_order.rb
@@ -5,7 +5,7 @@ class MetadataOrder < MetadataTemplate
# El valor según la posición del post en la relación ordenada por
# fecha, a fecha más alta, posición más alta
def default_value
- super || site.posts(lang: lang).sort_by(:date).index(post)
+ super || ((site.indexed_posts.where(locale: locale).first&.order || 0) + 1)
end
def save
diff --git a/app/models/metadata_related_posts.rb b/app/models/metadata_related_posts.rb
index 42d1381b..56665fc3 100644
--- a/app/models/metadata_related_posts.rb
+++ b/app/models/metadata_related_posts.rb
@@ -3,14 +3,17 @@
# Devuelve una lista de títulos y UUID de todos los posts del mismo
# idioma que el actual, para usar con input-map.js
class MetadataRelatedPosts < MetadataArray
- # Genera un Hash de { title | slug => uuid } y excluye el Post actual
+ # Genera un Hash de { title (schema) => uuid } para usar en
+ # options_for_select
+ #
# @return [Hash]
def values
- @values ||= posts.map do |p|
- next if p.uuid.value == post.uuid.value
-
- [title(p), p.uuid.value]
- end.compact.to_h
+ @values ||= posts.pluck(:title, :created_at, :layout, :post_id).to_h do |row|
+ row.tap do |value|
+ value[0] =
+ "#{value[0]} #{value.delete_at(1).strftime('%F')} (#{site.layouts[value.delete_at(1)].humanized_name})"
+ end
+ end
end
# Las relaciones nunca son privadas
@@ -23,28 +26,28 @@ class MetadataRelatedPosts < MetadataArray
end
def indexable_values
- posts.where(uuid: value).map(&:title).map(&:value)
+ posts.where(post_id: value).pluck(:title)
end
private
- # Obtiene todos los posts y opcionalmente los filtra
+ # Obtiene todos los posts menos el actual y opcionalmente los filtra
+ #
+ # @return [IndexedPost::ActiveRecord_AssociationRelation]
def posts
- site.posts(lang: lang).where(**filter)
+ site.indexed_posts.where(locale: locale).where.not(post_id: post.uuid.value).where(filter)
end
- def title(post)
- "#{post&.title&.value || post&.slug&.value} #{post&.date&.value.strftime('%F')} (#{post.layout.humanized_name})"
- end
-
- # Encuentra el filtro
+ # Encuentra el filtro desde el esquema del atributo
+ #
+ # @return [Hash,nil]
def filter
- layout.metadata.dig(name, 'filter')&.to_h&.symbolize_keys || {}
+ 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, '')
+ u.to_s.gsub(/[^a-f0-9-]/i, '')
end)
end
end
diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb
index 78989e15..a95f7e12 100644
--- a/app/models/metadata_template.rb
+++ b/app/models/metadata_template.rb
@@ -62,20 +62,28 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
end
# Trae el idioma actual del sitio o del panel
+ #
+ # @deprecated Empezar a usar locale
# @return [String]
def lang
- @lang ||= post&.lang&.value || I18n.locale
+ @lang ||= post&.lang&.value || I18n.locale.to_s
end
- # El valor por defecto
+ alias_method :locale, :lang
+
+ # El valor por defecto desde el esquema de datos
+ #
+ # @return [any]
def default_value
- layout.metadata.dig(name, 'default', lang.to_s)
+ layout.metadata.dig(name, 'default', lang)
end
# Valores posibles, busca todos los valores actuales en otros
# artículos del mismo sitio
+ #
+ # @return [Array]
def values
- site.everything_of(name, lang: lang)
+ site.indexed_posts.everything_of(name)
end
# Valor actual o por defecto. Al memoizarlo podemos modificarlo
diff --git a/app/models/post.rb b/app/models/post.rb
index 8885897f..4118e9c7 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -30,15 +30,44 @@ class Post
# a demanda?
def find_layout(path)
File.foreach(path).lazy.grep(/^layout: /).take(1).first&.split(' ')&.last&.tr('\'', '')&.tr('"', '')&.to_sym
+ rescue Errno::ENOENT => e
+ ExceptionNotifier.notify_exception(e, data: { path: path })
+
+ :post
+ end
+
+ # Genera un Post nuevo
+ #
+ # @todo Mergear en Post#initialize
+ # @params :path [String]
+ # @params :site [Site]
+ # @params :locale [String, Symbol]
+ # @params :document [Jekyll::Document]
+ # @params :layout [String,Symbol]
+ # @return [Post]
+ def build(**args)
+ args[:path] ||= ''
+ args[:document] ||=
+ begin
+ site = args[:site]
+ collection = site.collections[args[:locale].to_s]
+
+ Jekyll::Document.new(args[:path], site: site.jekyll, collection: collection).tap do |doc|
+ doc.data['date'] = Date.today.to_time if args[:path].blank?
+ end
+ end
+
+ args[:layout] = args[:site].layouts[args[:layout]] if args[:layout].is_a? Symbol
+
+ Post.new(**args)
end
end
# Redefinir el inicializador de OpenStruct
#
- # @param site: [Site] el sitio en Sutty
- # @param document: [Jekyll::Document] el documento leído por Jekyll
- # @param layout: [Layout] la plantilla
- #
+ # @param :site [Site] el sitio en Sutty
+ # @param :document [Jekyll::Document] el documento leído por Jekyll
+ # @param :layout [Layout] la plantilla
def initialize(**args)
default_attributes_missing(**args)
@@ -103,6 +132,7 @@ class Post
src = element.attributes['src']
next unless src&.value&.start_with? 'public/'
+
file = MetadataFile.new(site: site, post: self, document: document, layout: layout)
file.value['path'] = src.value
@@ -221,7 +251,8 @@ class Post
# La fecha de creación inmodificable del post
def created_at
- @metadata[:created_at] ||= MetadataCreatedAt.new(document: document, site: site, layout: layout, name: :created_at, type: :created_at, post: self, required: true)
+ @metadata[:created_at] ||= MetadataCreatedAt.new(document: document, site: site, layout: layout, name: :created_at,
+ type: :created_at, post: self, required: true)
end
# Detecta si es un atributo válido o no, a partir de la tabla de la
@@ -289,8 +320,6 @@ class Post
def destroy
run_callbacks :destroy do
FileUtils.rm_f path.absolute
-
- site.delete_post self
end
end
alias destroy! destroy
diff --git a/app/models/post/indexable.rb b/app/models/post/indexable.rb
index 4e46d7b2..51a78f15 100644
--- a/app/models/post/indexable.rb
+++ b/app/models/post/indexable.rb
@@ -6,9 +6,10 @@ class Post
extend ActiveSupport::Concern
included do
- # Indexa o reindexa el Post
- after_save :index!
- after_destroy :remove_from_index!
+ # @return [IndexedPost,nil]
+ def indexed_post
+ site.indexed_posts.find_by_post_id(uuid.value)
+ end
# Devuelve una versión indexable del Post
#
@@ -40,16 +41,19 @@ class Post
private
- # Los metadatos que se almacenan como objetos JSON. Empezamos con
- # las categorías porque se usan para filtrar en el listado de
- # artículos.
+ # Los metadatos que se almacenan como objetos JSON.
#
# @return [Hash]
def indexable_front_matter
{}.tap do |ifm|
ifm[:usuaries] = usuaries.map(&:id)
ifm[:draft] = attribute?(:draft) ? draft.value : false
- ifm[:categories] = categories.indexable_values if attribute? :categories
+
+ indexable_attributes.select do |attr|
+ self[attr].front_matter?
+ end.each do |attr|
+ ifm[attr] = self[attr].try(:indexable_values)
+ end
end
end
diff --git a/app/models/post_relation.rb b/app/models/post_relation.rb
deleted file mode 100644
index 531d3cc4..00000000
--- a/app/models/post_relation.rb
+++ /dev/null
@@ -1,156 +0,0 @@
-# frozen_string_literal: true
-
-# La relación de un sitio con sus artículos, esto nos permite generar
-# artículos como si estuviésemos usando ActiveRecord.
-class PostRelation < Array
- # No necesitamos cambiar el sitio
- attr_reader :site, :lang
-
- def initialize(site:, lang:)
- @site = site
- @lang = lang
- # Proseguimos la inicialización sin valores por defecto
- super()
- end
-
- # Genera un artículo nuevo con los parámetros que le pasemos y lo suma
- # al array
- def build(**args)
- args[:lang] = lang
- args[:document] ||= build_document(collection: args[:lang])
- args[:layout] = build_layout(args[:layout])
-
- post = Post.new(site: site, **args)
-
- self << post
- post
- end
-
- def create(**args)
- post = build(**args)
- post.save
- post
- end
-
- alias sort_by_generic sort_by
- alias sort_by_generic! sort_by!
-
- # Permite ordenar los artículos por sus atributos
- #
- # XXX: Prestar atención cuando estamos mezclando artículos con
- # diferentes tipos de atributos.
- def sort_by(*attrs)
- sort_by_generic do |post|
- attrs.map do |attr|
- # TODO: detectar el tipo de atributo faltante y obtener el valor
- # por defecto para hacer la comparación
- if post.attributes.include? attr
- post.public_send(attr).value
- else
- 0
- end
- end
- end
- end
-
- def sort_by!(*attrs)
- replace sort_by(*attrs)
- end
-
- alias find_generic find
-
- # Encontrar un post por su UUID
- def find(id, uuid: false)
- find_generic do |p|
- if uuid
- p.uuid.value == id
- else
- p.id == id
- end
- end
- end
-
- # Encuentra el primer post por el valor de los atributos
- #
- # @param [Hash]
- # @return [Post]
- def find_by(**args)
- find_generic do |post|
- args.map do |attr, value|
- post.attribute?(attr) &&
- post.public_send(attr).value == value
- end.all?
- end
- end
-
- # Encuentra todos los Post que cumplan las condiciones
- #
- # TODO: Implementar caché
- #
- # @param [Hash] Mapa de atributo => valor. Valor puede ser un Array
- # de valores
- # @return [PostRelation]
- def where(**args)
- return self if args.empty?
-
- begin
- PostRelation.new(site: site, lang: lang).concat(select do |post|
- result = args.map do |attr, value|
- next unless post.attribute?(attr)
-
- attribute = post[attr]
-
- # TODO: Si el valor del atributo también es un Array deberíamos
- # cruzar ambas.
- case value
- when Array then value.include? attribute.value
- else
- case attribute.value
- when Array then attribute.value.include? value
- else attribute.value == value
- end
- end
- end.compact
-
- # Un Array vacío devuelve true para all?
- result.present? && result.all?
- end)
- end
- end
-
- # Como Array#select devolviendo una relación
- #
- # @return [PostRelation]
- alias array_select select
- def select(&block)
- PostRelation.new(site: site, lang: lang).concat array_select(&block)
- end
-
- # Intenta guardar todos y devuelve true si pudo
- def save_all(validate: true)
- map do |post|
- post.save(validate: validate)
- end.all?
- end
-
- private
-
- def build_layout(layout = nil)
- return layout if layout.is_a? Layout
-
- site.layouts[layout&.to_sym || :post]
- end
-
- # Devuelve una colección Jekyll que hace pasar el documento
- def build_collection(label:)
- Jekyll::Collection.new(site.jekyll, label.to_s)
- end
-
- # Un documento borrador con algunas propiedades por defecto
- def build_document(collection:)
- col = build_collection(label: collection)
- doc = Jekyll::Document.new('', site: site.jekyll, collection: col)
- doc.data['date'] = Date.today.to_time
- doc
- end
-end
diff --git a/app/models/site.rb b/app/models/site.rb
index 9c94c31b..0e1f562c 100644
--- a/app/models/site.rb
+++ b/app/models/site.rb
@@ -57,8 +57,7 @@ class Site < ApplicationRecord
before_create :clone_skel!
# Elimina el directorio al destruir un sitio
before_destroy :remove_directories!
- # Cambiar el nombre del directorio
- before_update :update_name!
+
before_save :add_private_key_if_missing!
# Guardar la configuración si hubo cambios
after_save :sync_attributes_with_config!
@@ -219,15 +218,10 @@ class Site < ApplicationRecord
jekyll.data
end
- # Traer las colecciones. Todos los artículos van a estar dentro de
- # colecciones.
+ # Trae las colecciones desde el sitio, sin leer su contenido
+ #
+ # @return [Hash]
def collections
- unless @read
- jekyll.reader.read_collections
-
- @read = true
- end
-
jekyll.collections
end
@@ -236,55 +230,6 @@ class Site < ApplicationRecord
@config ||= Site::Config.new(self)
end
- # Los posts en el idioma actual o en uno en particular
- #
- # @param lang: [String|Symbol] traer los artículos de este idioma
- def posts(lang: nil)
- # Traemos los posts del idioma actual por defecto o el que haya
- lang ||= locales.include?(I18n.locale) ? I18n.locale : default_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
- # excepción NoMethodError
- @posts ||= Struct.new(*locales).new
-
- return @posts[lang] unless @posts[lang].blank?
-
- @posts[lang] = PostRelation.new site: self, lang: lang
-
- # No fallar si no existe colección para este idioma
- # XXX: queremos fallar silenciosamente?
- (collections[lang.to_s]&.docs || []).each do |doc|
- layout = layouts[Post.find_layout(doc.path)]
-
- @posts[lang].build(document: doc, layout: layout, lang: lang)
- rescue TypeError => e
- ExceptionNotifier.notify_exception(e, data: { site: name, site_id: id, path: doc.path })
- end
-
- @posts[lang]
- end
-
- # Todos los Post del sitio para poder buscar en todos.
- #
- # @return PostRelation
- def docs
- @docs ||= PostRelation.new(site: self, lang: :docs).push(locales.flat_map do |locale|
- posts(lang: locale)
- end).flatten!
- end
-
- # Elimina un artículo de la colección
- def delete_post(post)
- lang = post.lang.value
-
- collections[lang.to_s].docs.delete(post.document) &&
- posts(lang: lang).delete(post)
-
- post
- end
-
# Obtiene todas las plantillas de artículos
#
# @return [Hash] { post: Layout }
@@ -323,24 +268,6 @@ class Site < ApplicationRecord
jekyll.reader.read_layouts
end
- # Trae todos los valores disponibles para un campo
- #
- # TODO: Traer recursivamente, si el campo contiene Hash
- #
- # TODO: Mover a PostRelation#pluck
- #
- # @param attr [Symbol|String] El atributo a buscar
- # @return Array
- def everything_of(attr, lang: nil)
- Rails.cache.fetch("#{cache_key_with_version}/everything_of/#{lang}/#{attr}", expires_in: 1.hour) do
- attr = attr.to_sym
-
- posts(lang: lang).flat_map do |p|
- p[attr].value if p.attribute? attr
- end.uniq.compact
- end
- end
-
# Poner en la cola de compilación
def enqueue!
update(status: 'enqueued') if waiting?
@@ -457,7 +384,6 @@ class Site < ApplicationRecord
@incompatible_layouts = nil
@jekyll = nil
@config = nil
- @posts = nil
@docs = nil
end
@@ -493,13 +419,6 @@ class Site < ApplicationRecord
FileUtils.rm_rf path
end
- def update_name!
- return unless name_changed?
-
- FileUtils.mv path_was, path
- reload_jekyll!
- end
-
# Sincroniza algunos atributos del sitio con su configuración y
# guarda los cambios
#
diff --git a/app/models/site/index.rb b/app/models/site/index.rb
index cfa4030a..c663fbdf 100644
--- a/app/models/site/index.rb
+++ b/app/models/site/index.rb
@@ -16,7 +16,12 @@ class Site
def index_posts!
Site.transaction do
- docs.each(&:index!)
+ jekyll.read
+ jekyll.documents.each do |doc|
+ doc.read!
+
+ Post.build(document: doc, site: self, layout: doc['layout'].to_sym).index!
+ end
update(last_indexed_commit: repository.head_commit.oid)
end
@@ -99,9 +104,10 @@ class Site
indexable_posts.select do |delta|
MODIFIED_STATUSES.include? delta.status
end.each do |delta|
- locale, path = locale_and_path_from(delta.new_file[:path])
+ locale, = locale_and_path_from(delta.new_file[:path])
+ full_path = File.join(path, delta.new_file[:path])
- posts(lang: locale).find(path).index!
+ Post.build(path: full_path, site: self, layout: Post.find_layout(full_path), locale: locale).index!
end
end
diff --git a/app/models/site/repository.rb b/app/models/site/repository.rb
index c7056eaa..1376fb82 100644
--- a/app/models/site/repository.rb
+++ b/app/models/site/repository.rb
@@ -75,13 +75,18 @@ class Site
# Forzamos el checkout para mover el HEAD al último commit y
# escribir los cambios
rugged.checkout 'HEAD', strategy: :force
-
- git_sh("git", "lfs", "fetch", "origin", default_branch)
- # reemplaza los pointers por los archivos correspondientes
- git_sh("git", "lfs", "checkout")
+
commit
end
+ # Trae todos los archivos desde LFS
+ #
+ # @return [Boolean]
+ def git_lfs_checkout
+ git_sh('git', 'lfs', 'fetch', 'origin', default_branch)
+ git_sh('git', 'lfs', 'checkout')
+ end
+
# El último commit
#
# @return [Rugged::Commit]
@@ -111,10 +116,30 @@ class Site
walker.each.to_a
end
- # Hay commits sin aplicar?
- def needs_pull?
- fetch
- !commits.empty?
+ # Detecta si hay que hacer un pull o no
+ #
+ # @return [Boolean]
+ def up_to_date?
+ rugged.merge_analysis(remote_head_commit).include?(:up_to_date)
+ end
+
+ # Detecta si es posible adelantar la historia local a la remota o
+ # necesitamos un merge
+ #
+ # @return [Boolean]
+ def fast_forward?
+ rugged.merge_analysis(remote_head_commit).include?(:fastforward)
+ end
+
+ # Mueve la historia local a la remota
+ #
+ # @see {https://stackoverflow.com/a/27077322}
+ # @return [nil]
+ def fast_forward!
+ rugged.checkout_tree(remote_head_commit)
+ rugged.references.update(rugged.head.resolve, remote_head_commit.oid)
+
+ nil
end
# Guarda los cambios en git
@@ -123,7 +148,7 @@ class Site
# @param :rm [Array] Archivos a eliminar
# @param :usuarie [Usuarie] Quién hace el commit
# @param :message [String] Mensaje
- def commit(add: [], rm: [], usuarie:, message:)
+ def commit(usuarie:, message:, add: [], rm: [])
# Cargar el árbol actual
rugged.index.read_tree rugged.head.target.tree
@@ -157,7 +182,7 @@ class Site
#
# @return [Boolean]
def gc
- git_sh("git", "gc")
+ git_sh('git', 'gc')
end
# Pushea cambios al repositorio remoto
@@ -171,8 +196,8 @@ class Site
# Hace limpieza de LFS
def lfs_cleanup
- git_sh("git", "lfs", "prune")
- git_sh("git", "lfs", "dedup")
+ git_sh('git', 'lfs', 'prune')
+ git_sh('git', 'lfs', 'dedup')
end
private
@@ -232,7 +257,7 @@ class Site
# @param :args [Array]
# @return [Boolean]
def git_sh(*args)
- env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path }
+ env = { 'PATH' => '/usr/bin', 'LANG' => ENV.fetch('LANG', nil), 'HOME' => path }
r = nil
Open3.popen2e(env, *args, unsetenv_others: true, chdir: path) do |_, _, t|
diff --git a/app/services/post_service.rb b/app/services/post_service.rb
index 4631a9a4..5eca8466 100644
--- a/app/services/post_service.rb
+++ b/app/services/post_service.rb
@@ -3,12 +3,11 @@
# Este servicio se encarga de crear artículos y guardarlos en git,
# asignándoselos a une usuarie
PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
- # Crea un artículo nuevo
+ # Crea un artículo nuevo y modificar las asociaciones
#
# @return Post
def create
- self.post = site.posts(lang: locale)
- .build(layout: layout)
+ self.post = Post.build(site: site, locale: locale, layout: layout)
post.usuaries << usuarie
params[:post][:draft] = true if site.invitade? usuarie
@@ -16,42 +15,13 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
post.slug.value = p[:slug] if p[:slug].present?
end
- commit(action: :created, add: update_related_posts) if post.update(post_params)
-
- update_site_license!
-
- # Devolver el post aunque no se haya salvado para poder rescatar los
- # errores
- post
- end
-
- # Crear un post anónimo, con opciones más limitadas. No usamos post.
- def create_anonymous
- # XXX: Confiamos en el parámetro de idioma porque estamos
- # verificándolos en Site#posts
- self.post = site.posts(lang: locale)
- .build(layout: layout)
- # Los artículos anónimos siempre son borradores
- params[:draft] = true
-
- commit(action: :created, add: [post.path.absolute]) if post.update(anon_post_params)
- post
- end
-
- def update
- post.usuaries << usuarie
- params[:post][:draft] = true if site.invitade? usuarie
-
- # Eliminar ("mover") el archivo si cambió de ubicación.
if post.update(post_params)
- rm = []
- rm << post.path.value_was if post.path.changed?
+ added_paths << post.path.value
- # Es importante que el artículo se guarde primero y luego los
- # relacionados.
- commit(action: :updated, add: update_related_posts, rm: rm)
+ # Recorrer todas las asociaciones y agregarse donde corresponda
+ update_associations(post)
- update_site_license!
+ commit(action: :created, add: added_paths)
end
# Devolver el post aunque no se haya salvado para poder rescatar los
@@ -59,6 +29,45 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
post
end
+ # Crear un post anónimo, con opciones más limitadas. No usamos post.
+ #
+ # @todo Permitir asociaciones?
+ def create_anonymous
+ # XXX: Confiamos en el parámetro de idioma porque estamos
+ # verificándolos en Site#posts
+ self.post = Post.build(site: site, locale: locale, layout: layouts)
+ # Los artículos anónimos siempre son borradores
+ params[:draft] = true
+
+ commit(action: :created, add: [post.path.absolute]) if post.update(anon_post_params)
+ post
+ end
+
+ # Al actualizar, modificamos un post pre-existente, todas las
+ # relaciones anteriores y las relaciones actuales.
+ def update
+ post.usuaries << usuarie
+ params[:post][:draft] = true if site.invitade? usuarie
+
+ if post.update(post_params)
+ # Eliminar ("mover") el archivo si cambió de ubicación.
+ rm = []
+ rm << post.path.value_was if post.path.changed?
+
+ added_paths << post.path.value
+
+ # Recorrer todas las asociaciones y agregarse donde corresponda
+ update_associations(post)
+
+ commit(action: :updated, add: added_paths, rm: rm)
+ end
+
+ # Devolver el post aunque no se haya salvado para poder rescatar los
+ # errores
+ post
+ end
+
+ # @todo Eliminar relaciones
def destroy
post.destroy!
@@ -74,7 +83,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
# { uuid => 2, uuid => 1, uuid => 0 }
def reorder
reorder = params.require(:post).permit(reorder: {})&.dig(:reorder)&.transform_values(&:to_i)
- posts = site.posts(lang: locale).where(uuid: reorder.keys)
+ posts = site.indexed_posts.where(locale: locale, post_id: reorder.keys).map(&:post)
files = posts.map do |post|
next unless post.attribute? :order
@@ -90,8 +99,11 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
return if files.empty?
# TODO: Implementar transacciones!
- posts.save_all(validate: false) &&
- commit(action: :reorder, add: files)
+ posts.map do |post|
+ post.save(validate: false)
+ end
+
+ commit(action: :reorder, add: files)
end
private
@@ -118,43 +130,147 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
end
end
+ # @return [Symbol]
def locale
params.dig(:post, :lang)&.to_sym || I18n.locale
end
+ # @return [Layout]
def layout
- params.dig(:post, :layout) || params[:layout]
- end
-
- # 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
- posts = Set.new
-
- post.attributes.each do |a|
- post[a].related_methods.each do |m|
- next unless post[a].respond_to? m
-
- # La respuesta puede ser una PostRelation también
- posts.merge [post[a].public_send(m)].flatten.compact
- end
- end
-
- posts.map do |p|
- p.path.absolute if p.save(validate: false)
- end.compact << post.path.absolute
+ site.layouts[
+ (params.dig(:post, :layout) || params[:layout]).to_sym
+ ]
end
# Si les usuaries modifican o crean una licencia, considerarla
# personalizada en el panel.
def update_site_license!
- if site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom?
- site.update licencia: Licencia.find_by_icons('custom')
+ return unless site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom?
+
+ site.update licencia: Licencia.find_by_icons('custom')
+ end
+
+ # @return [Set]
+ def associated_posts_to_save
+ @associated_posts_to_save ||= Set.new
+ end
+
+ # @return [Set]
+ def added_paths
+ @added_paths ||= Set.new
+ end
+
+ # Recolectar campos asociados que no estén vacíos
+ #
+ # @param [Post]
+ # @return [Array]
+ def association_attributes(post)
+ post.attributes.select do |attribute|
+ post[attribute].try(:inverse?)
end
end
+
+ # @param :post_ids [Array]
+ # @return [Association]
+ def associated_posts(post_ids)
+ site.indexed_posts.where(post_id: post_ids).map(&:post)
+ end
+
+ # Modificar las asociaciones en cascada, manteniendo reciprocidad
+ # y guardando los archivos correspondientes.
+ #
+ # HABTM, Locales: si se rompe de un lado se elimina en el otro y lo
+ # mismo si se agrega.
+ #
+ # HasMany: la relación es de uno a muchos. Al quitar uno, se elimina
+ # la relación inversa. Al agregar uno, se elimina su relación
+ # anterior en el tercer Post y se actualiza con la nueva.
+ #
+ # BelongsTo: la inversa de HasMany. Al cambiarla, se quita de la
+ # relación anterior y se agrega en la nueva.
+ #
+ # @param :post [Post]
+ # @return [nil]
+ def update_associations(post)
+ association_attributes(post).each do |attribute|
+ metadata = post[attribute]
+
+ next unless metadata.changed?
+
+ inverse_attribute = post[attribute].inverse
+ value_was = metadata.value_was.dup
+ value = metadata.value.dup
+
+ case metadata.type
+ when 'has_and_belongs_to_many', 'locales'
+ associated_posts(value_was - value).each do |remove_post|
+ remove_relation_from(remove_post[inverse_attribute], post.uuid.value)
+ end
+
+ associated_posts(value - value_was).each do |add_post|
+ add_relation_to(add_post[inverse_attribute], post.uuid.value)
+ end
+ when 'has_many'
+ associated_posts(value_was - value).each do |remove_post|
+ remove_relation_from(remove_post[inverse_attribute], '')
+ end
+
+ associated_posts(value - value_was).each do |add_post|
+ associated_posts(add_post[inverse_attribute].value_was).each do |remove_post|
+ remove_relation_from(remove_post[attribute], add_post.uuid.value)
+ end
+
+ add_relation_to(add_post[inverse_attribute], post.uuid.value)
+ end
+ when 'belongs_to', 'has_one'
+ if value_was.present?
+ associated_posts(value_was).each do |remove_post|
+ remove_relation_from(remove_post[inverse_attribute], post.uuid.value)
+ end
+ end
+
+ associated_posts(value).each do |add_post|
+ add_relation_to(add_post[inverse_attribute], post.uuid.value)
+ end
+ end
+ end
+
+ associated_posts_to_save.each do |associated_post|
+ next unless associated_post.save(validate: false)
+
+ added_paths << associated_post.path.value
+ end
+
+ nil
+ end
+
+ # @todo por qué no podemos usar nil para deshabilitar un valor?
+ # @param :metadata [MetadataTemplate]
+ # @param :value [String]
+ # @return [nil]
+ def remove_relation_from(metadata, value)
+ case metadata.value
+ when Array then metadata.value.delete(value)
+ when String then metadata.value = ''
+ end
+
+ associated_posts_to_save << metadata.post
+ nil
+ end
+
+ # @todo El validador ya debería eliminar valores duplicados
+ # @param :metadata [MetadataTemplate]
+ # @param :value [String]
+ # @return [nil]
+ def add_relation_to(metadata, value)
+ case metadata.value
+ when Array
+ metadata.value << value
+ metadata.value.uniq!
+ when String then metadata.value = value
+ end
+
+ associated_posts_to_save << metadata.post
+ nil
+ end
end
diff --git a/app/services/site_service.rb b/app/services/site_service.rb
index 36868c51..a1007e04 100644
--- a/app/services/site_service.rb
+++ b/app/services/site_service.rb
@@ -24,7 +24,9 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
#
# TODO: hacer que el repositorio se cree cuando es necesario, para
# que no haya estados intermedios.
- site.locales = [usuarie.lang] + I18n.available_locales
+ site.locales = [usuarie.lang]
+
+ add_role_to_deploys! role
add_role_to_deploys! role
@@ -82,16 +84,28 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
commit_config(action: :tor)
end
- # Trae cambios desde la rama remota y reindexa los artículos.
+ # Trae cambios desde la rama remota
#
# @return [Boolean]
def merge
- result = site.repository.merge(usuarie)
+ site.repository.merge(usuarie).present?
+ end
- # TODO: Implementar callbacks
- site.try(:index_posts!) if result
+ def rename(name)
+ return if name == site.name
+ moved = false
+ site.name = name
- result.present?
+ Site.transaction do
+ raise ActiveRecord::Rollback if File.exists?(site.path)
+ FileUtils.mv (site.path_was, site.path)
+ moved = true
+ ActiveStorage::Blob.where(service_name: site.name_was).update_all(service_name: site.name)
+ site.save
+ rescue StandardError
+ FileUtils.mv (site.path, site.path_was) if moved
+ raise
+ end
end
private
@@ -146,7 +160,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
return true if site.licencia.custom?
with_all_locales do |locale|
- post = site.posts(lang: locale).find_by(layout: 'license')
+ post = site.indexed_posts(locale: locale).find_by(layout: 'license')&.post
change_licencia(post: post) if post
end.compact.map(&:valid?).all?
diff --git a/app/views/bootstrap/_custom_checkbox_for_field.haml b/app/views/bootstrap/_custom_checkbox_for_field.haml
new file mode 100644
index 00000000..8c12de57
--- /dev/null
+++ b/app/views/bootstrap/_custom_checkbox_for_field.haml
@@ -0,0 +1,10 @@
+- content = t("activerecord.attributes.#{field.object_name}.#{name}")
+- id = "#{field.object_name}_#{name}"
+- name = "#{field.object_name}[#{name}]"
+
+= render 'bootstrap/custom_checkbox', id: id,
+ name: name,
+ content: content,
+ required: local_assigns[:required],
+ value: '1' do
+ = yield
diff --git a/app/views/build_stats/index.haml b/app/views/build_stats/index.haml
index de04d84d..d505b2d4 100644
--- a/app/views/build_stats/index.haml
+++ b/app/views/build_stats/index.haml
@@ -1,6 +1,6 @@
%main.row
- %aside.menu.col-md-3
- = render 'sites/header', site: @site
+ %aside.menu.col-12.col-lg-3
+ = render 'sites/header', site: @site, filter_params: @filter_params
.col
%h1= t('.title')
@@ -14,7 +14,10 @@
- row[:urls].each do |url|
%tr
%th{ scope: 'row' }= row[:title]
- %td= link_to_if (url.present? && url.scheme.present?), url.to_s, url.to_s, class: 'word-break-all'
+ %td= link_to_if (url.present? && url.scheme.present?),
+ url.to_s,
+ url.to_s,
+ class: 'word-break-all'
%td
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
%td= row[:size]
diff --git a/app/views/collaborations/collaborate.haml b/app/views/collaborations/collaborate.haml
index cc951b0c..f0036c17 100644
--- a/app/views/collaborations/collaborate.haml
+++ b/app/views/collaborations/collaborate.haml
@@ -1,28 +1,28 @@
.row.align-items-center.justify-content-center.full-height
- .col-md-10.align-self-center
+ .col-12.col-lg-10.align-self-center
- welcome = @site.config.dig('welcome', 'message') || t('.welcome',
- site: @site.hostname)
+ site: @site.hostname)
= sanitize_markdown welcome
- .col-md-6.align-self-center
+ .col-12.col-lg-6.align-self-center
-# Copiado y pegado de app/views/devise/registrations/new.haml
- resource = resource_name = @invitade
= form_for(resource, as: resource_name,
- url: site_collaborate_path(@site),
- method: :post) do |f|
+ url: site_collaborate_path(@site),
+ method: :post) do |f|
- unless current_usuarie
.form-group
= f.label :email
= f.email_field :email, autofocus: true, autocomplete: 'email',
- class: 'form-control'
+ class: 'form-control'
.form-group
= f.label :password
- if @minimum_password_length
%em
= t('devise.shared.minimum_password_length',
- count: @minimum_password_length)
+ count: @minimum_password_length)
= f.password_field :password, autocomplete: 'new-password',
- class: 'form-control'
+ class: 'form-control'
.form-group
= f.submit t('.submit'), class: 'btn btn-secondary btn-lg btn-block'
diff --git a/app/views/devise/confirmations/new.haml b/app/views/devise/confirmations/new.haml
index c934edc5..d976e7cc 100644
--- a/app/views/devise/confirmations/new.haml
+++ b/app/views/devise/confirmations/new.haml
@@ -4,31 +4,30 @@
= render 'devise/shared/error_messages', resource: resource
.row.align-items-center.justify-content-center.full-height
- .col-md-4.align-self-center
+ .col-12.col-lg-5.align-self-center
.sr-only
%h2= t('.resend_confirmation_instructions')
= form_for(resource,
- as: resource_name,
- url: confirmation_path(resource_name),
- html: { method: :post }) do |f|
-
+ as: resource_name,
+ url: confirmation_path(resource_name),
+ html: { method: :post }) do |f|
:ruby
value = if resource.pending_reconfirmation?
- resource.unconfirmed_email
- else
- resource.email
- end
+ resource.unconfirmed_email
+ else
+ resource.email
+ end
.form-group
= f.label :email, class: 'sr-only'
= f.email_field :email,
- autofocus: true,
- autocomplete: 'email',
- class: 'form-control',
- value: value,
- placeholder: t('activerecord.attributes.usuarie.email')
+ autofocus: true,
+ autocomplete: 'email',
+ class: 'form-control',
+ value: value,
+ placeholder: t('activerecord.attributes.usuarie.email')
.actions
= f.submit t('.resend_confirmation_instructions'),
- class: 'btn btn-secondary btn-lg btn-block'
+ class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'
diff --git a/app/views/devise/invitations/edit.haml b/app/views/devise/invitations/edit.haml
index 3d2f8d76..536848b6 100644
--- a/app/views/devise/invitations/edit.haml
+++ b/app/views/devise/invitations/edit.haml
@@ -4,32 +4,32 @@
= render 'devise/shared/error_messages', resource: resource
.row.align-items-center.justify-content-center.full-height
- .col-md-5.align-self-center
+ .col-12.col-lg-5.align-self-center
%h2= t 'devise.invitations.edit.header'
= form_for(resource,
- as: resource_name,
- url: invitation_path(resource_name),
- html: { method: :put }) do |f|
+ as: resource_name,
+ url: invitation_path(resource_name),
+ html: { method: :put }) do |f|
= f.hidden_field :invitation_token, readonly: true
- if f.object.class.require_password_on_accepting
.form-group
= f.label :password, class: 'sr-only'
= f.password_field :password, class: 'form-control',
- min: @minimum_password_length,
- aria: { describedby: 'minimum-password-length' },
- placeholder: t('activerecord.attributes.usuarie.password')
+ min: @minimum_password_length,
+ aria: { describedby: 'minimum-password-length' },
+ placeholder: t('activerecord.attributes.usuarie.password')
- if @minimum_password_length
%small.text-muted.form-text#minimum-password-length
= t('devise.shared.minimum_password_length',
- count: @minimum_password_length)
+ count: @minimum_password_length)
.form-group
= f.label :password_confirmation, class: 'sr-only'
= f.password_field :password_confirmation,
- class: 'form-control',
- min: @minimum_password_length,
- aria: { describedby: 'minimum-password-length' },
- placeholder: t('activerecord.attributes.usuarie.password')
+ class: 'form-control',
+ min: @minimum_password_length,
+ aria: { describedby: 'minimum-password-length' },
+ placeholder: t('activerecord.attributes.usuarie.password')
.actions
= f.submit t('devise.invitations.edit.submit_button'),
- class: 'btn btn-secondary btn-lg btn-block'
+ class: 'btn btn-secondary btn-lg btn-block'
diff --git a/app/views/devise/invitations/new.haml b/app/views/devise/invitations/new.haml
index b8b097d0..c0a85c4d 100644
--- a/app/views/devise/invitations/new.haml
+++ b/app/views/devise/invitations/new.haml
@@ -4,16 +4,16 @@
= render 'devise/shared/error_messages', resource: resource
.row.align-items-center.justify-content-center.full-height
- .col-md-5.align-self-center
+ .col-12.col-lg-5.align-self-center
%h2= t 'devise.invitations.new.header'
= form_for(resource,
- as: resource_name,
- url: invitation_path(resource_name),
- html: { method: :post }) do |f|
+ as: resource_name,
+ url: invitation_path(resource_name),
+ html: { method: :post }) do |f|
- resource.class.invite_key_fields.each do |field|
.form-group
= f.label field
= f.text_field field, class: 'form-control'
.actions
= f.submit t('devise.invitations.new.submit_button'),
- class: 'btn btn-secondary btn-lg btn-block'
+ class: 'btn btn-secondary btn-lg btn-block'
diff --git a/app/views/devise/mailer/invitation_instructions.html.haml b/app/views/devise/mailer/invitation_instructions.html.haml
index e87d99d9..779c5021 100644
--- a/app/views/devise/mailer/invitation_instructions.html.haml
+++ b/app/views/devise/mailer/invitation_instructions.html.haml
@@ -10,15 +10,20 @@
- if @resource.needs_invitation_link?
%p= link_to t('devise.mailer.invitation_instructions.accept'),
- accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang)
+ accept_invitation_url(@resource,
+ invitation_token: @token,
+ change_locale_to: @resource.lang)
- if @resource.invitation_due_at
%p= t('devise.mailer.invitation_instructions.accept_until',
- due_date: l(@resource.invitation_due_at,
- format: :'devise.mailer.invitation_instructions.accept_until_format'))
+ due_date: l(@resource.invitation_due_at,
+ format: :'devise.mailer.invitation_instructions.accept_until_format'))
%p= t('devise.mailer.invitation_instructions.ignore')
- elsif !@resource.confirmed? && @resource.confirmation_token
- = confirmation_url(@resource, confirmation_token: @resource.confirmation_token, change_locale_to: @resource.lang)
+ = confirmation_url(@resource,
+ confirmation_token: @resource.confirmation_token,
+ change_locale_to: @resource.lang)
- else
- %p= link_to t('devise.mailer.invitation_instructions.sign_in'), root_url
+ %p= link_to t('devise.mailer.invitation_instructions.sign_in'),
+ root_url(change_locale_to: @resource.lang)
diff --git a/app/views/devise/passwords/edit.haml b/app/views/devise/passwords/edit.haml
index cd8ab8ad..4eb4a054 100644
--- a/app/views/devise/passwords/edit.haml
+++ b/app/views/devise/passwords/edit.haml
@@ -4,41 +4,40 @@
= render 'devise/shared/error_messages', resource: resource
.row.align-items-center.justify-content-center.full-height
- .col-md-5.align-self-center
+ .col-12.col-lg-5.align-self-center
.sr-only
%h2= t('.change_your_password')
%p= t('.help')
= form_for(resource, as: resource_name,
- url: password_path(resource_name),
- html: { method: :put }) do |f|
-
+ url: password_path(resource_name),
+ html: { method: :put }) do |f|
= f.hidden_field :reset_password_token
.form-group
= f.label :password, t('.new_password'), class: 'sr-only'
= f.password_field :password,
- autofocus: true,
- autocomplete: 'new-password',
- class: 'form-control', min: @minimum_password_length,
- aria: { describedby: 'minimum_password_length' },
- placeholder: t('.new_password')
+ autofocus: true,
+ autocomplete: 'new-password',
+ class: 'form-control', min: @minimum_password_length,
+ aria: { describedby: 'minimum_password_length' },
+ placeholder: t('.new_password')
- if @minimum_password_length
%small.form-text.text-muted
= t('devise.shared.minimum_password_length',
- count: @minimum_password_length)
+ count: @minimum_password_length)
.form-group
= f.label :password_confirmation, t('.confirm_new_password'),
- class: 'sr-only'
+ class: 'sr-only'
= f.password_field :password_confirmation, autocomplete: 'off',
- class: 'form-control',
- min: @minimum_password_length,
- aria: { describedby: 'minimum_password_length' },
- placeholder: t('.confirm_new_password')
+ class: 'form-control',
+ min: @minimum_password_length,
+ aria: { describedby: 'minimum_password_length' },
+ placeholder: t('.confirm_new_password')
.actions
= f.submit t('.change_my_password'),
- class: 'btn btn-secondary btn-lg btn-block'
+ class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'
diff --git a/app/views/devise/passwords/new.haml b/app/views/devise/passwords/new.haml
index 4bf7c990..102f49cd 100644
--- a/app/views/devise/passwords/new.haml
+++ b/app/views/devise/passwords/new.haml
@@ -4,21 +4,21 @@
= render 'devise/shared/error_messages', resource: resource
.row.align-items-center.justify-content-center.full-height
- .col-md-5.align-self-center
+ .col-12.col-lg-5.align-self-center
.sr-only
%h2= t('.forgot_your_password')
%p= t('.help')
= form_for(resource,
- as: resource_name,
- url: password_path(resource_name),
- html: { method: :post }) do |f|
+ as: resource_name,
+ url: password_path(resource_name),
+ html: { method: :post }) do |f|
.form-group
= f.label :email, class: 'sr-only'
= f.email_field :email, autofocus: true, autocomplete: 'email',
- class: 'form-control',
- placeholder: t('activerecord.attributes.usuarie.email')
+ class: 'form-control',
+ placeholder: t('activerecord.attributes.usuarie.email')
.actions
= f.submit t('.send_me_reset_password_instructions'),
- class: 'btn btn-secondary btn-lg btn-block'
+ class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'
diff --git a/app/views/devise/registrations/edit.haml b/app/views/devise/registrations/edit.haml
index 8bdc55d9..8574e88e 100644
--- a/app/views/devise/registrations/edit.haml
+++ b/app/views/devise/registrations/edit.haml
@@ -6,61 +6,61 @@
= render 'devise/shared/error_messages', resource: resource
.row.align-items-center.justify-content-center.full-height
- .col-md-6.align-self-center
+ .col-12.col-lg-6.align-self-center
%h2= t('.title')
= form_for(resource,
- as: resource_name,
- url: registration_path(resource_name),
- html: { method: :put }) do |f|
-
+ as: resource_name,
+ url: registration_path(resource_name),
+ html: { method: :put }) do |f|
.form-group
= f.label :email
= f.email_field :email, autofocus: true, autocomplete: 'email',
- class: 'form-control'
+ class: 'form-control'
- if devise_mapping.confirmable? && resource.pending_reconfirmation?
%div
= t('.currently_waiting_confirmation_for_email',
- email: resource.unconfirmed_email)
+ email: resource.unconfirmed_email)
.form-group
= f.label :lang
= f.select :lang,
- I18n.available_locales.map { |lang| [t(lang), lang] }, {},
- class: 'form-control'
+ I18n.available_locales.map { |lang| [t(lang), lang] }, {},
+ class: 'form-control'
.form-group
= f.label :password
= f.password_field :password, autocomplete: 'new-password',
- class: 'form-control', aria: { describedby: 'password-help' }
+ class: 'form-control',
+ aria: { describedby: 'password-help' }
%small.text-muted.form-text#password-help
= t('.leave_blank_if_you_don_t_want_to_change_it')
- if @minimum_password_length
= t('devise.shared.minimum_password_length',
- count: @minimum_password_length)
+ count: @minimum_password_length)
.form-group
= f.label :password_confirmation
= f.password_field :password_confirmation,
- autocomplete: 'new-password',
- class: 'form-control'
+ autocomplete: 'new-password',
+ class: 'form-control'
.form-group
= f.label :current_password
= f.password_field :current_password,
- autocomplete: 'current-password',
- required: true,
- class: 'form-control',
- aria: { describedby: 'current-password-help' }
+ autocomplete: 'current-password',
+ required: true,
+ class: 'form-control',
+ aria: { describedby: 'current-password-help' }
%small.text-muted.form-text#current-password-help
= t('.we_need_your_current_password_to_confirm_your_changes')
.actions
= f.submit t('.update'),
- class: 'btn btn-secondary btn-lg btn-block'
+ class: 'btn btn-secondary btn-lg btn-block'
%hr/
.sr-only
%h3= t('.cancel_my_account')
= button_to t('.cancel_my_account'),
- registration_path(resource_name),
- data: { confirm: t('.are_you_sure') },
- method: :delete, class: 'btn btn-secondary btn-block'
+ registration_path(resource_name),
+ data: { confirm: t('.are_you_sure') },
+ method: :delete, class: 'btn btn-secondary btn-block'
diff --git a/app/views/devise/registrations/new.haml b/app/views/devise/registrations/new.haml
index aabc0487..d19251d2 100644
--- a/app/views/devise/registrations/new.haml
+++ b/app/views/devise/registrations/new.haml
@@ -4,41 +4,41 @@
= render 'devise/shared/error_messages', resource: resource
.row.align-items-center.justify-content-center.full-height
- .col-md-6.align-self-center
+ .col-12.col-lg-6.align-self-center
%h2= t('.sign_up')
%p= t('.help')
= form_for(resource,
- as: resource_name,
- url: registration_path(resource_name, params: { locale: params[:locale] })) do |f|
-
+ as: resource_name,
+ url: registration_path(resource_name, params: { locale: params[:locale] })) do |f|
.form-group
= f.label :email, class: 'sr-only'
= f.email_field :email, autofocus: true, autocomplete: 'email',
- class: 'form-control',
- placeholder: t('activerecord.attributes.usuarie.email')
+ class: 'form-control',
+ placeholder: t('activerecord.attributes.usuarie.email')
- password = 'activerecord.attributes.usuarie.password'
.form-group
= f.label :password, class: 'sr-only'
= f.password_field :password, autocomplete: 'new-password',
- class: 'form-control', min: @minimum_password_length,
- aria: { describedby: 'minimum-password-length' },
- placeholder: t(password)
+ class: 'form-control',
+ min: @minimum_password_length,
+ aria: { describedby: 'minimum-password-length' },
+ placeholder: t(password)
- if @minimum_password_length
%small.text-muted.form-text#minimum-password-length
= t('devise.shared.minimum_password_length',
- count: @minimum_password_length)
+ count: @minimum_password_length)
.form-group
= f.label :password_confirmation, class: 'sr-only'
= f.password_field :password_confirmation,
- autocomplete: 'new-password',
- class: 'form-control',
- min: @minimum_password_length,
- aria: { describedby: 'minimum-password-length' },
- placeholder: t("#{password}_confirmation")
+ autocomplete: 'new-password',
+ class: 'form-control',
+ min: @minimum_password_length,
+ aria: { describedby: 'minimum-password-length' },
+ placeholder: t("#{password}_confirmation")
.form-group
- Usuarie::CONSENT_FIELDS.each do |field|
@@ -48,7 +48,12 @@
- content = t(".#{field}.label")
- href = t(".#{field}.href", default: '')
- help_content = t(".#{field}.help")
- = render 'bootstrap/custom_checkbox', id: id, name: name, content: content, required: required, value: "1" do
+ = render 'bootstrap/custom_checkbox',
+ id: id,
+ name: name,
+ content: content,
+ required: required,
+ value: '1' do
- if href.present?
= link_to help_content, href, target: '_blank', rel: 'noopener'
- else
@@ -56,6 +61,6 @@
.actions
= f.submit t('.sign_up'),
- class: 'btn btn-secondary btn-lg btn-block'
+ class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'
diff --git a/app/views/devise/sessions/new.haml b/app/views/devise/sessions/new.haml
index 03c3974b..e1bc85bb 100644
--- a/app/views/devise/sessions/new.haml
+++ b/app/views/devise/sessions/new.haml
@@ -2,38 +2,37 @@
- 'black-bg'
.row.align-items-center.justify-content-center.full-height
- .col-md-5.align-self-center
+ .col-12.col-lg-5.align-self-center
.sr-only
%h2= t('.sign_in')
%p= t('.help')
= form_for(resource,
- as: resource_name,
- url: session_path(resource_name)) do |f|
+ as: resource_name,
+ url: session_path(resource_name)) do |f|
- if @site
= hidden_field :referer, value: site_path(@site)
.form-group
= f.label :email, class: 'sr-only'
= f.email_field :email,
- autofocus: true,
- autocomplete: 'email',
- class: 'form-control',
- placeholder: t('login.email')
+ autofocus: true,
+ autocomplete: 'email',
+ class: 'form-control',
+ placeholder: t('login.email')
.form-group
= f.label :password, class: 'sr-only'
= f.password_field :password,
- autocomplete: 'current-password',
- class: 'form-control',
- placeholder: t('login.password')
+ autocomplete: 'current-password',
+ class: 'form-control',
+ placeholder: t('login.password')
- if devise_mapping.rememberable?
.form-group
- = f.check_box :remember_me, aria: { describedby: 'remember-for' }
- = f.label :remember_me
- %small.form-text.text-muted#remember-for
+ = render 'bootstrap/custom_checkbox_for_field',
+ field: f, name: :remember_me do
= t('login.remember_me',
- remember_for: distance_of_time_in_words(Usuarie.remember_for))
+ remember_for: distance_of_time_in_words(Usuarie.remember_for))
.actions
= f.submit t('.sign_in'),
- class: 'btn btn-secondary btn-lg btn-block'
+ class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'
diff --git a/app/views/devise/unlocks/new.haml b/app/views/devise/unlocks/new.haml
index 34253f44..c4413d3c 100644
--- a/app/views/devise/unlocks/new.haml
+++ b/app/views/devise/unlocks/new.haml
@@ -4,21 +4,21 @@
= render 'devise/shared/error_messages', resource: resource
.row.align-items-center.justify-content-center.full-height
- .col-md-5.align-self-center
+ .col-12.col-lg-5.align-self-center
.sr-only
%h2= t('.resend_unlock_instructions')
%p= t('.help')
= form_for(resource,
- as: resource_name,
- url: unlock_path(resource_name),
- html: { method: :post }) do |f|
+ as: resource_name,
+ url: unlock_path(resource_name),
+ html: { method: :post }) do |f|
.form-group
= f.label :email, class: 'sr-only'
= f.email_field :email, autofocus: true, autocomplete: 'email',
- class: 'form-control',
- placeholder: t('activerecord.attributes.usuarie.email')
+ class: 'form-control',
+ placeholder: t('activerecord.attributes.usuarie.email')
.actions
= f.submit t('.resend_unlock_instructions'),
- class: 'btn btn-secondary btn-lg btn-block'
+ class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'
diff --git a/app/views/layouts/_breadcrumb.haml b/app/views/layouts/_breadcrumb.haml
index a946243a..639d4a27 100644
--- a/app/views/layouts/_breadcrumb.haml
+++ b/app/views/layouts/_breadcrumb.haml
@@ -1,33 +1,38 @@
-%nav.navbar
- %a.navbar-brand.d-none.d-sm-block{ href: '/' }
+%nav.navbar.flex-md-nowrap.px-0
+ %a.navbar-brand.order-0{ href: '/' }
= inline_svg_tag 'sutty.svg', class: 'black', aria: true,
- title: t('svg.sutty.title'), desc: t('svg.sutty.desc')
+ title: t('svg.sutty.title'),
+ desc: t('svg.sutty.desc')
- %nav{ aria: { label: t('.title') } }
- %ol.breadcrumb.m-0.flex-wrap
- - breadcrumb_trail do |crumb|
- %li.breadcrumb-item{ class: crumb.current? ? 'active' : '' }
- - if crumb.current?
- %span.line-clamp-1{ aria: { current: 'page' } }= crumb.name
- - else
- %span.line-clamp-1= link_to crumb.name, crumb.url
+ - if breadcrumbs?
+ %nav.flex-grow-1.order-2.order-md-1{ aria: { label: t('.title') } }
+ %ol.breadcrumb.m-0.flex-wrap
+ - breadcrumb_trail do |crumb|
+ %li.breadcrumb-item{ class: crumb.current? ? 'active' : '' }
+ - if crumb.current?
+ %span.line-clamp-1{ aria: { current: 'page' } }= crumb.name
+ - else
+ = link_to crumb.name, crumb.url, class: 'line-clamp-1'
- - if @current_usuarie || current_usuarie
- %ul.navbar-nav.flex-row
- - if @site&.tienda?
+ - if usuarie || current_usuarie
+ %ul.navbar-nav.order-1.order-md-2
+ - if site&.tienda?
%li.nav-item
- = link_to t('.tienda'), @site.tienda_url,
- role: 'button', class: 'btn btn-secondary'
+ = link_to t('.tienda'), site.tienda_url,
+ role: 'button', class: 'btn btn-secondary'
%li.nav-item
= link_to t('.contact_us'), t('.contact_us_href'),
- class: 'btn btn-secondary', rel: 'me', target: '_blank'
+ class: 'btn btn-secondary', rel: 'me', target: '_blank'
%li.nav-item
= link_to t('.logout'), main_app.destroy_usuarie_session_path,
- method: :delete, role: 'button', class: 'btn btn-secondary'
+ method: :delete, role: 'button', class: 'btn btn-secondary'
- else
- params.permit!
- I18n.available_locales.each do |locale|
- next if locale == I18n.locale
- = link_to t("switch_locale.#{locale}"), params.to_h.merge(change_locale_to: locale)
+
+ %li.nav-item
+ = link_to t("switch_locale.#{locale}"),
+ params.to_h.merge(change_locale_to: locale)
diff --git a/app/views/layouts/_details.haml b/app/views/layouts/_details.haml
index a21f46c1..5e8d566c 100644
--- a/app/views/layouts/_details.haml
+++ b/app/views/layouts/_details.haml
@@ -7,10 +7,15 @@
@param :summary_class [String] Clases para el summary
- local_assigns[:summary_class] ||= 'h3'
+- local_assigns[:closed] ||= '▶'.html_safe
+- local_assigns[:open] ||= '▼'.html_safe
-%details.details.py-2{ id: local_assigns[:id], data: { controller: 'details', action: 'toggle->details#store' } }
+%details.details.py-2{ id: local_assigns[:id],
+ data: { controller: 'details',
+ action: 'toggle->details#store' },
+ class: local_assigns[:details_class] }
%summary.d-flex.flex-row.align-items-center.justify-content-between{ class: local_assigns[:summary_class] }
%span= summary
- %span.hide-when-open ▶
- %span.show-when-open ▼
+ %span.hide-when-open{ class: local_assigns[:open_class] }= local_assigns[:closed]
+ %span.show-when-open{ class: local_assigns[:closed_class] }= local_assigns[:open]
= yield
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index eaa15eb4..f28c67af 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -13,16 +13,27 @@
%script{ type: 'text/javascript', src: '/env.js' }
= csrf_meta_tags
- = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
- = stylesheet_link_tag 'dark', rel: 'alternate stylesheet', media: 'all', 'data-turbolinks-track': 'reload', title: t('dark')
- = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload', defer: true
- = stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload'
- = favicon_link_tag 'sutty_cuadrada.png', rel: 'apple-touch-icon', type: 'image/png'
+ = stylesheet_link_tag 'application',
+ media: 'all',
+ 'data-turbolinks-track': 'reload'
+ = stylesheet_link_tag 'dark',
+ rel: 'alternate stylesheet',
+ media: 'all',
+ 'data-turbolinks-track': 'reload',
+ title: t('dark')
+ = javascript_pack_tag 'application',
+ 'data-turbolinks-track': 'reload',
+ defer: true
+ = stylesheet_pack_tag 'application',
+ 'data-turbolinks-track': 'reload'
+ = favicon_link_tag 'sutty_cuadrada.png',
+ rel: 'apple-touch-icon',
+ type: 'image/png'
= render 'layouts/link_rel_alternate'
%body{ class: yield(:body) }
.container-fluid#sutty
- = render 'layouts/breadcrumb'
+ = render 'layouts/breadcrumb', usuarie: @current_usuarie, site: @site
= render 'layouts/flash'
= yield
diff --git a/app/views/posts/attribute_ro/_belongs_to.haml b/app/views/posts/attribute_ro/_belongs_to.haml
index c7e06be8..7410e921 100644
--- a/app/views/posts/attribute_ro/_belongs_to.haml
+++ b/app/views/posts/attribute_ro/_belongs_to.haml
@@ -1,6 +1,6 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td{ dir: dir, lang: locale }
- - p = metadata.belongs_to
+ - p = site.indexed_posts.find_by_post_id(metadata.value)
- if p
- = link_to p.title.value, site_post_path(site, p.id)
+ = link_to p.title, site_post_path(site, p.path)
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 d6b51a7a..29c0816f 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.has_many.each do |p|
- %li= link_to p.title.value, site_post_path(site, p.id)
+ - site.indexed_posts.where(post_id: metadata.value).find_each do |p|
+ %li= link_to p.title, site_post_path(site, p.path)
diff --git a/app/views/posts/attribute_ro/_has_many.haml b/app/views/posts/attribute_ro/_has_many.haml
index d6b51a7a..29c0816f 100644
--- a/app/views/posts/attribute_ro/_has_many.haml
+++ b/app/views/posts/attribute_ro/_has_many.haml
@@ -2,5 +2,5 @@
%th= post_label_t(attribute, post: post)
%td
%ul{ dir: dir, lang: locale }
- - metadata.has_many.each do |p|
- %li= link_to p.title.value, site_post_path(site, p.id)
+ - site.indexed_posts.where(post_id: metadata.value).find_each do |p|
+ %li= link_to p.title, site_post_path(site, p.path)
diff --git a/app/views/posts/attribute_ro/_has_one.haml b/app/views/posts/attribute_ro/_has_one.haml
new file mode 100644
index 00000000..fafccd34
--- /dev/null
+++ b/app/views/posts/attribute_ro/_has_one.haml
@@ -0,0 +1,6 @@
+%tr{ id: attribute }
+ %th= post_label_t(attribute, post: post)
+ %td{ dir: dir, lang: locale }
+ - p = site.indexed_posts.find_by_post_id(metadata.value)
+ - if p
+ = link_to p.title, site_post_path(site, p.post_id)
diff --git a/app/views/posts/attribute_ro/_related_posts.haml b/app/views/posts/attribute_ro/_related_posts.haml
index c43b589e..2bf51367 100644
--- a/app/views/posts/attribute_ro/_related_posts.haml
+++ b/app/views/posts/attribute_ro/_related_posts.haml
@@ -2,10 +2,11 @@
%th= post_label_t(attribute, post: post)
%td
%ul{ dir: dir, lang: locale }
- - metadata.value.each do |v|
- - p = site.posts(lang: post.lang.value).find(v, uuid: true)
+ - site.indexed_posts.where(locale: post.lang.value,
+ post_id: metadata.value).find_each do |p|
-#
XXX: Ignorar todos los posts no encontrados (ej: fueron
borrados o el uuid cambió)
- next unless p
- %li= link_to p.title.value, site_post_path(site, p.id)
+
+ %li= link_to p.title, site_post_path(site, p.path)
diff --git a/_deploy/.keep b/app/views/posts/attributes/_draft.haml
similarity index 100%
rename from _deploy/.keep
rename to app/views/posts/attributes/_draft.haml
diff --git a/app/views/posts/attributes/_file.haml b/app/views/posts/attributes/_file.haml
index 20c27399..daa54157 100644
--- a/app/views/posts/attributes/_file.haml
+++ b/app/views/posts/attributes/_file.haml
@@ -1,43 +1,54 @@
.form-group{ data: { controller: 'file-preview' } }
+ = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
- if metadata.static_file
- case metadata.static_file.blob.content_type
- - when %r{\Avideo/}
- = video_tag url_for(metadata.static_file),
- controls: true, class: 'img-fluid',
- data: { target: 'file-preview.preview' }
- - when %r{\Aaudio/}
- = audio_tag url_for(metadata.static_file),
- controls: true, class: 'img-fluid',
- data: { target: 'file-preview.preview' }
- - when 'application/pdf'
- %iframe{ src: url_for(metadata.static_file) }
- - else
- = link_to t('posts.attribute_ro.file.download'),
- url_for(metadata.static_file)
+ - when %r{\Avideo/}
+ = video_tag url_for(metadata.static_file),
+ controls: true, class: 'img-fluid',
+ data: { target: 'file-preview.preview' }
+ - when %r{\Aaudio/}
+ = audio_tag url_for(metadata.static_file),
+ controls: true, class: 'img-fluid',
+ data: { target: 'file-preview.preview' }
+ - when 'application/pdf'
+ %iframe{ src: url_for(metadata.static_file) }
+ - else
+ = link_to t('posts.attribute_ro.file.download'),
+ url_for(metadata.static_file)
-# Mantener el valor si no enviamos ninguna imagen
= hidden_field_tag "#{base}[#{attribute}][path]", metadata.value['path']
-# Los archivos requeridos solo se pueden reemplazar
- unless metadata.required
.custom-control.custom-switch
- = check_box_tag "#{base}[#{attribute}][path]", '', false, id: "#{base}_#{attribute}_destroy", class: 'custom-control-input'
- = label_tag "#{base}_#{attribute}_destroy", t('posts.attributes.file.destroy'), class: 'custom-control-label'
+ = check_box_tag "#{base}[#{attribute}][path]",
+ '',
+ false,
+ id: "#{base}_#{attribute}_destroy",
+ class: 'custom-control-input'
+ = label_tag "#{base}_#{attribute}_destroy",
+ t('posts.attributes.file.destroy'),
+ class: 'custom-control-label'
.custom-file
= file_field(*field_name_for(base, attribute, :path),
- **field_options(attribute, metadata, required: (metadata.required && !metadata.path?)),
- class: "custom-file-input #{invalid(post, attribute)}",
- data: { target: 'file-preview.input', action: 'file-preview#update' })
+ **field_options(attribute,
+ metadata,
+ required: (metadata.required && !metadata.path?)),
+ class: "custom-file-input #{invalid(post, attribute)}",
+ data: { target: 'file-preview.input',
+ action: 'file-preview#update' })
= label_tag "#{base}_#{attribute}_path",
- post_label_t(attribute, :path, post: post), class: 'custom-file-label'
+ post_label_t(attribute, :path, post: post),
+ class: 'custom-file-label'
= render 'posts/attribute_feedback',
- post: post, attribute: [attribute, :path], metadata: metadata
+ post: post, attribute: [attribute, :path], metadata: metadata
.form-group
= label_tag "#{base}_#{attribute}_description",
- post_label_t(attribute, :description, post: post, required: false)
+ post_label_t(attribute, :description, post: post, required: false)
= text_field(*field_name_for(base, attribute, :description),
- value: metadata.value['description'],
- dir: dir, lang: locale,
- **field_options(attribute, metadata, required: false))
+ value: metadata.value['description'],
+ dir: dir, lang: locale,
+ **field_options(attribute, metadata, required: false))
= render 'posts/attribute_feedback',
- post: post, attribute: [attribute, :description], metadata: metadata
+ post: post, attribute: [attribute, :description], metadata: metadata
diff --git a/app/views/posts/attributes/_has_one.haml b/app/views/posts/attributes/_has_one.haml
new file mode 100644
index 00000000..f486367c
--- /dev/null
+++ b/app/views/posts/attributes/_has_one.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/attributes/_image.haml b/app/views/posts/attributes/_image.haml
index 241a78e8..ee988178 100644
--- a/app/views/posts/attributes/_image.haml
+++ b/app/views/posts/attributes/_image.haml
@@ -1,40 +1,51 @@
.form-group{ data: { controller: 'file-preview' } }
+ = label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
- if metadata.static_file
= image_tag url_for(metadata.static_file),
- alt: metadata.value['description'],
- class: 'img-fluid',
- data: { target: 'file-preview.preview' }
+ alt: metadata.value['description'],
+ class: 'img-fluid',
+ data: { target: 'file-preview.preview' }
-# Mantener el valor si no enviamos ninguna imagen
= hidden_field_tag "#{base}[#{attribute}][path]", metadata.value['path']
-# Las imágenes requeridas solo se pueden reemplazar
- unless metadata.required
.custom-control.custom-switch
- = check_box_tag "#{base}[#{attribute}][path]", '', false, id: "#{base}_#{attribute}_destroy", class: 'custom-control-input'
- = label_tag "#{base}_#{attribute}_destroy", t('posts.attributes.image.destroy'), class: 'custom-control-label'
+ = check_box_tag "#{base}[#{attribute}][path]",
+ '',
+ false,
+ id: "#{base}_#{attribute}_destroy",
+ class: 'custom-control-input'
+ = label_tag "#{base}_#{attribute}_destroy",
+ t('posts.attributes.image.destroy'),
+ class: 'custom-control-label'
- else
= image_tag '',
- alt: metadata.value['description'],
- class: 'img-fluid',
- data: { target: 'file-preview.preview' }
+ alt: metadata.value['description'],
+ class: 'img-fluid',
+ data: { target: 'file-preview.preview' }
.custom-file
= file_field(*field_name_for(base, attribute, :path),
- **field_options(attribute, metadata, required: (metadata.required && !metadata.path?)),
- class: "custom-file-input #{invalid(post, attribute)}",
- accept: ActiveStorage.web_image_content_types.join(','),
- data: { target: 'file-preview.input', action: 'file-preview#update' })
+ **field_options(attribute,
+ metadata,
+ required: (metadata.required && !metadata.path?)),
+ class: "custom-file-input #{invalid(post, attribute)}",
+ accept: ActiveStorage.web_image_content_types.join(','),
+ data: { target: 'file-preview.input',
+ action: 'file-preview#update' })
= label_tag "#{base}_#{attribute}_path",
- post_label_t(attribute, :path, post: post), class: 'custom-file-label'
+ post_label_t(attribute, :path, post: post),
+ class: 'custom-file-label'
= render 'posts/attribute_feedback',
- post: post, attribute: [attribute, :path], metadata: metadata
+ post: post, attribute: [attribute, :path], metadata: metadata
.form-group
= label_tag "#{base}_#{attribute}_description",
- post_label_t(attribute, :description, post: post, required: false)
+ post_label_t(attribute, :description, post: post, required: false)
= text_field(*field_name_for(base, attribute, :description),
- value: metadata.value['description'],
- dir: dir, lang: locale,
- **field_options(attribute, metadata, required: false))
+ value: metadata.value['description'],
+ dir: dir, lang: locale,
+ **field_options(attribute, metadata, required: false))
= render 'posts/attribute_feedback',
- post: post, attribute: [attribute, :description], metadata: metadata
+ post: post, attribute: [attribute, :description], metadata: metadata
diff --git a/app/views/posts/attributes/_locales.haml b/app/views/posts/attributes/_locales.haml
index 4978f6b4..c4632e3c 100644
--- a/app/views/posts/attributes/_locales.haml
+++ b/app/views/posts/attributes/_locales.haml
@@ -3,10 +3,9 @@
%legend= post_label_t(attribute, post: post)
= render 'posts/attribute_feedback',
- post: post, attribute: attribute, metadata: metadata
+ post: post, attribute: attribute, metadata: metadata
- site.locales.each do |locale|
- - next if post.lang.value == locale
- locale_t = t("locales.#{locale}.name", default: locale.to_s.humanize)
- value = metadata.value.find do |v|
- metadata.values[locale].values.include? v
@@ -15,5 +14,6 @@
= label_tag "#{base}_#{attribute}_#{locale}", locale_t
= select_tag("#{plain_field_name_for(base, attribute)}[]",
- options_for_select(metadata.values[locale], value),
- **field_options(attribute, metadata), include_blank: t('.empty'))
+ options_for_select(metadata.values[locale], value),
+ **field_options(attribute, metadata),
+ include_blank: t('.empty'))
diff --git a/app/views/posts/edit.haml b/app/views/posts/edit.haml
index e7e0260d..d03e08c7 100644
--- a/app/views/posts/edit.haml
+++ b/app/views/posts/edit.haml
@@ -1,9 +1,3 @@
.row.justify-content-center
- .col-md-8
- - if policy(@site).edit?
- = render 'layouts/details', summary: t('posts.edit.post') do
- = render 'posts/form', site: @site, post: @post
- = render 'layouts/details', summary: t('posts.edit.moderation_queue') do
- = render 'posts/moderation_queue', site: @site, post: @post, moderation_queue: @moderation_queue
- - else
- = render 'posts/form', site: @site, post: @post
+ .col-12.col-lg-8
+ = render 'posts/form', site: @site, post: @post
diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml
index 3de30aa3..989e53ee 100644
--- a/app/views/posts/index.haml
+++ b/app/views/posts/index.haml
@@ -6,70 +6,93 @@
- reorder_target = reorder_controller = {}
%main.row
- %aside.menu.col-md-3
+ %aside.menu.col-lg-3
.mb-3
- = render 'sites/header', site: @site
+ = render 'sites/header', site: @site, filter_params: @filter_params
= render 'sites/status', site: @site
= render 'sites/build', site: @site, class: 'btn-block'
= render 'sites/moderation_queue', site: @site, class: 'btn-block'
- %h3= t('posts.new')
- %table.table.table-sm.mb-3
- %tbody
- - @site.schema_organization.each do |schema, _|
- - schema = @site.layouts[schema]
- - next if schema.hidden?
- = render 'schemas/row', site: @site, schema: schema, filter: @filter_params
+ = render 'layouts/details', summary: t('posts.filters.title') do
+ %form{ method: :get }
+ .border.border-magenta.p-1
+ - @filter_params.each do |param, values|
+ - next if param == :layout
+
+ - [values].flatten.each do |value|
+ %input{ type: 'hidden',
+ name: values.is_a?(Array) ? "#{param}[]" : param,
+ value: value }
+ %legend.font-weight-bold.m-0.h6= 'Tipo de contenido'
+ - @site.schema_organization.each do |key, _|
+ .custom-control.custom-checkbox
+ - schema = @site.layouts[key]
+ = render 'schemas/filter', site: @site,
+ key: key,
+ schema: schema,
+ filter: @filter_params
+ %button.btn.btn-secondary.mt-3{ type: 'submit' }= t('posts.filters.submit')
+ = render 'layouts/details',
+ summary: t('posts.new'),
+ summary_class: 'h4 magenta font-weight-bold m-0 px-2',
+ details_class: 'details-agregar',
+ open: '+', closed: '+',
+ open_class: 'h1 magenta font-weight-bold m-0',
+ closed_class: 'h1 magenta font-weight-bold m-0' do
+ %table.table-sm.w-100
+ %tbody
+ - @site.schema_organization.each do |schema, _|
+ - schema = @site.layouts[schema]
+ - next if schema.hidden?
+
+ = render 'schemas/row', site: @site,
+ schema: schema,
+ filter: @filter_params
- if policy(@site_stat).index?
- = link_to t('stats.index.title'), site_stats_path(@site), class: 'btn btn-secondary'
+ = link_to t('stats.index.title'),
+ site_stats_path(@site),
+ class: 'btn btn-secondary'
- if policy(@site).edit?
- = link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn btn-secondary'
+ = link_to t('sites.edit.btn', site: @site.title),
+ edit_site_path(@site),
+ class: 'btn btn-secondary'
- if policy(@site).private?
- = link_to t('sites.private'), '../private/' + @site.name, class: 'btn btn-secondary', target: '_blank', rel: 'noopener'
+ = link_to t('sites.private'), "../private/#{@site.name}",
+ class: 'btn btn-secondary',
+ target: '_blank',
+ rel: 'noopener'
- if policy(SiteUsuarie.new(@site, current_usuarie)).index?
= render 'layouts/btn_with_tooltip',
- tooltip: t('usuaries.index.help.self'),
- text: t('usuaries.index.title'),
- type: 'info',
- link: site_usuaries_path(@site)
+ tooltip: t('usuaries.index.help.self'),
+ text: t('usuaries.index.title'),
+ type: 'info',
+ link: site_usuaries_path(@site)
- if @site.design.credits
= render 'bootstrap/alert' do
= sanitize_markdown @site.design.credits
- = link_to t('sites.donations.text'), t('sites.donations.url'), class: 'btn btn-secondary'
+ = link_to t('sites.donations.text'),
+ t('sites.donations.url'),
+ class: 'btn btn-secondary'
- if @site.design.designer_url
- = link_to t('sites.designer_url'), @site.design.designer_url, class: 'btn btn-secondary'
+ = link_to t('sites.designer_url'),
+ @site.design.designer_url,
+ class: 'btn btn-secondary'
%section.col
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
- %form{ action: site_posts_path }
- - @filter_params.each do |param, value|
- - next if param == 'q'
- %input{ type: 'hidden', name: param, value: value }
- .form-group.flex-grow-0.m-0
- %label.sr-only{for: 'q'}= t('.search')
- %input#q.form-control.border.border-magenta{ type: 'search', placeholder: t('.search'), name: 'q', value: @filter_params[:q] }
- %input.sr-only{ type: 'submit' }
- if @site.locales.size > 1
%nav#locales
- @site.locales.each do |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|
- - if param == 'layout'
- - value = @site.layouts[value.to_sym].humanized_name
- = link_to site_posts_path(@site, **@filter_params.reject { |k, _| k == param }),
- class: 'btn btn-secondary btn-sm',
- title: t('posts.remove_filter_help', filter: value),
- aria: { labelledby: "help-filter-#{param}" } do
- = value
- ×
+ = 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' : ''}"
+
- if @posts.empty?
%h2= t('posts.empty')
- else
@@ -78,11 +101,12 @@
%caption.sr-only= t('posts.caption')
%thead
%tr.sticky-top
- %th.border-0{ colspan: '4' }
+ %th.border-0.p-0.p-md-2-plus{ colspan: '4' }
.d-flex.flex-row.justify-content-between
%div
- if reorder_allowed
- = submit_tag t('posts.reorder.submit'), class: 'btn btn-secondary'
+ = submit_tag t('posts.reorder.submit'),
+ class: 'btn btn-secondary'
%button.btn.btn-secondary{ data: { action: 'reorder#unselect' } }
= t('posts.reorder.unselect')
%span.badge{ data: { target: 'reorder.counter' } } 0
@@ -90,11 +114,18 @@
%button.btn.btn-secondary{ data: { action: 'reorder#down' } }= t('posts.reorder.down')
%button.btn.btn-secondary{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
%button.btn.btn-secondary{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
+ %input{ type: 'hidden',
+ name: 'post[lang]',
+ value: @locale }
- if @site.pagination
%div
- = link_to_prev_page @posts, t('posts.prev'), class: 'btn btn-secondary'
- = link_to_next_page @posts, t('posts.next'), class: 'btn btn-secondary'
+ = link_to_prev_page @posts,
+ t('posts.prev'),
+ class: 'btn btn-secondary'
+ = link_to_next_page @posts,
+ t('posts.next'),
+ class: 'btn btn-secondary'
%tbody
- dir = @site.data.dig(params[:locale], 'dir')
- size = @posts.size
@@ -102,19 +133,23 @@
-#
TODO: Solo les usuaries cachean porque tenemos que separar
les botones por permisos.
+
- cache_if @usuarie, [post, I18n.locale] do
- checkbox_id = "checkbox-#{post.post_id}"
%tr{ id: post.post_id, data: reorder_target }
- if reorder_allowed
%td
.custom-control.custom-checkbox
- %input.custom-control-input{ id: checkbox_id, type: 'checkbox', autocomplete: 'off', data: { action: 'reorder#select' } }
+ %input.custom-control-input{ id: checkbox_id,
+ type: 'checkbox',
+ autocomplete: 'off',
+ data: { action: 'reorder#select' } }
%label.custom-control-label{ for: checkbox_id }
%span.sr-only= t('posts.reorder.select')
-# Orden más alto es mayor prioridad
= hidden_field 'post[reorder]', post.post_id,
- value: size - i,
- data: { reorder: true }
+ value: size - i,
+ data: { reorder: true }
%td.w-100{ class: dir }
= link_to site_post_path(@site, post.path) do
%span{ lang: post.locale, dir: dir }= post.title
@@ -122,7 +157,8 @@
%span.badge.badge-primary= I18n.t('posts.attributes.draft.label')
%br
%small
- = link_to @site.layouts[post.layout].humanized_name, site_posts_path(@site, **@filter_params.merge(layout: post.layout))
+ = 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
@@ -135,9 +171,23 @@
%td.text-nowrap
.d-flex.flex-row.align-items-start
- if @usuarie || policy(post).edit?
- = link_to t('posts.edit_post'), edit_site_post_path(@site, post.path), class: 'btn btn-secondary'
+ = link_to t('posts.edit_post'),
+ edit_site_post_path(@site, post.path),
+ class: 'btn btn-secondary'
- if @usuarie || policy(post).destroy?
- = link_to t('posts.destroy'), site_post_path(@site, post.path), class: 'btn btn-secondary', method: :delete, data: { confirm: t('posts.confirm_destroy') }
+ = link_to t('posts.destroy'),
+ site_post_path(@site, post.path),
+ class: 'btn btn-secondary',
+ method: :delete,
+ data: { confirm: t('posts.confirm_destroy') }
+ -#
+ Rescatar cualquier error en un post, notificarlo e
+ ignorar su renderización.
+ - rescue ActionView::Template::Error => e
+ - ExceptionNotifier.notify_exception(e.cause,
+ data: { site: @site.name,
+ post: @post.path.absolute,
+ usuarie: current_usuarie.id })
#footnotes{ hidden: true }
- @filter_params.each do |param, value|
diff --git a/app/views/posts/new.haml b/app/views/posts/new.haml
index 6ec252fe..d03e08c7 100644
--- a/app/views/posts/new.haml
+++ b/app/views/posts/new.haml
@@ -1,3 +1,3 @@
.row.justify-content-center
- .col-md-8
+ .col-12.col-lg-8
= render 'posts/form', site: @site, post: @post
diff --git a/app/views/posts/show.haml b/app/views/posts/show.haml
index 10fe64e3..155a2679 100644
--- a/app/views/posts/show.haml
+++ b/app/views/posts/show.haml
@@ -1,10 +1,10 @@
- dir = @site.data.dig(params[:locale], 'dir')
.row.justify-content-center
- .col-md-8
+ .col-12.col-lg-8
%article.content.table-responsive-md
= link_to t('posts.edit_post'),
- edit_site_post_path(@site, @post.id),
- class: 'btn btn-secondary btn-block'
+ edit_site_post_path(@site, @post.id),
+ class: 'btn btn-secondary btn-block'
%table.table.table-condensed
%thead
diff --git a/app/views/schemas/_add.haml b/app/views/schemas/_add.haml
index 0131a6bb..2bc6519c 100644
--- a/app/views/schemas/_add.haml
+++ b/app/views/schemas/_add.haml
@@ -1 +1,3 @@
-= link_to t('.add'), new_site_post_path(site, layout: schema.value), class: 'btn btn-secondary btn-sm m-0'
+= link_to t(schema.humanized_name),
+ new_site_post_path(site, layout: schema.value),
+ class: 'stretched-link black text-decoration-none'
diff --git a/app/views/schemas/_filter.haml b/app/views/schemas/_filter.haml
index c422c5b8..de4c03a7 100644
--- a/app/views/schemas/_filter.haml
+++ b/app/views/schemas/_filter.haml
@@ -1,4 +1,18 @@
-- if filter[:layout] == schema.name.to_s
- = link_to t('.remove'), site_posts_path(site, **filter.merge(layout: nil)), class: 'btn btn-primary btn-sm m-0'
-- else
- = link_to t('.filter'), site_posts_path(site, **filter.merge(layout: schema.value)), class: 'btn btn-secondary btn-sm m-0'
+%div
+ %input.custom-control-input.magenta{ type: 'checkbox',
+ id: schema,
+ name: 'layout[]',
+ class: '',
+ value: schema.name,
+ checked: filter[:layout]&.include?(key.to_s) }
+ %label.custom-control-label.font-weight-normal{ for: schema }= schema.humanized_name
+
+-# XXX: Solo un nivel de recursividad
+- unless local_assigns[:parent_schema]
+ - schema.schemas.each do |s|
+ - next if s.hidden?
+
+ = render 'schemas/filter', schema: s,
+ key: s.name,
+ site: site,
+ filter: filter
diff --git a/app/views/schemas/_row.haml b/app/views/schemas/_row.haml
index ece07727..494d664c 100644
--- a/app/views/schemas/_row.haml
+++ b/app/views/schemas/_row.haml
@@ -1,15 +1,15 @@
-%tr
- %th.w-100{ scope: 'row' }
+%tr.border-top.border-magenta
+ %th.font-weight-normal.w-100.position-relative{ scope: 'row' }
- if local_assigns[:parent_schema]
%span.text-muted —
- = schema.humanized_name
- %td.px-0.text-nowrap
- = render 'schemas/add', **local_assigns
- = render 'schemas/filter', **local_assigns
+ = render 'schemas/add', schema: schema, **local_assigns
-# XXX: Solo un nivel de recursividad
- unless local_assigns[:parent_schema]
- schema.schemas.each do |s|
- next if s.hidden?
- = render 'schemas/row', schema: s, site: site, filter: filter, parent_schema: schema
+ = render 'schemas/row', schema: s,
+ site: site,
+ filter: filter,
+ parent_schema: schema
diff --git a/app/views/sites/_form.haml b/app/views/sites/_form.haml
index ec2712bf..20c15fb6 100644
--- a/app/views/sites/_form.haml
+++ b/app/views/sites/_form.haml
@@ -8,7 +8,7 @@
- site.errors.messages.each_pair do |attr, error|
- attr = attr.to_s
- error.each do |e|
- %li= link_to t('activerecord.attributes.site.' + attr) + ' ' + e, '#' + attr
+ %li= link_to "#{t("activerecord.attributes.site.#{attr}")} #{e}", "##{attr}"
= form_for site, html: { class: form_class(site) } do |f|
- unless site.persisted?
@@ -21,11 +21,11 @@
No puede estar compuesto solo de números
= f.text_field :name,
- class: form_control(site, :name),
- required: true,
- pattern: '^([a-z0-9][a-z0-9\-]*)?[a-z0-9\.]$',
- minlength: 1,
- maxlength: 63
+ class: form_control(site, :name),
+ required: true,
+ pattern: '^([a-z0-9][a-z0-9\-]*)?[a-z0-9\.]$',
+ minlength: 1,
+ maxlength: 63
- if invalid? site, :name
.invalid-feedback= site.errors.messages[:name].join(', ')
@@ -33,7 +33,7 @@
%h2= f.label :title
%p.lead= t('.help.title')
= f.text_field :title, class: form_control(site, :title),
- required: true
+ required: true
- if invalid? site, :title
.invalid-feedback= site.errors.messages[:title].join(', ')
@@ -41,7 +41,7 @@
%h2= f.label :description
%p.lead= t('.help.description')
= f.text_area :description, class: form_control(site, :description),
- maxlength: 160, minlength: 10, required: true
+ maxlength: 160, minlength: 10, required: true
- if invalid? site, :description
.invalid-feedback= site.errors.messages[:description].join(', ')
%hr/
@@ -53,29 +53,29 @@
- if invalid? site, :design_id
= render 'bootstrap/alert' do
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
- layouts: site.incompatible_layouts.to_sentence)
+ layouts: site.incompatible_layouts.to_sentence)
.row.row-cols-1.row-cols-md-2.designs
-# Demasiado complejo para un f.collection_radio_buttons
- Design.all.order(priority: :desc).each do |design|
.design.col.d-flex.flex-column
.custom-control.custom-radio
= f.radio_button :design_id, design.id,
- checked: design.id == site.design_id,
- disabled: design.disabled,
- required: true, class: 'custom-control-input'
+ checked: design.id == site.design_id,
+ disabled: design.disabled,
+ required: true, class: 'custom-control-input'
= f.label "design_id_#{design.id}", design.name,
- class: 'custom-control-label'
+ class: 'custom-control-label'
.flex-fill
= sanitize_markdown design.description,
- tags: %w[p a strong em]
+ tags: %w[p a strong em]
.btn-group{ role: 'group', 'aria-label': t('.design.actions') }
- if design.url
= link_to t('.design.url'), design.url,
- target: '_blank', class: 'btn btn-secondary'
+ target: '_blank', class: 'btn btn-secondary'
- if design.license
= link_to t('.design.license'), design.license,
- target: '_blank', class: 'btn btn-secondary'
+ target: '_blank', class: 'btn btn-secondary'
%hr/
.form-group.licenses#license_id
@@ -83,6 +83,7 @@
%p.lead= t('.help.licencia')
- Licencia.all.find_each do |licencia|
- next if licencia.custom? && site.licencia != licencia
+
.row.license
.col
.media.mt-1
@@ -91,15 +92,20 @@
.media-body
.custom-control.custom-radio
= f.radio_button :licencia_id, licencia.id,
- checked: licencia.id == site.licencia_id,
- required: true, class: 'custom-control-input'
- = f.label "licencia_id_#{licencia.id}", class: 'custom-control-label' do
+ checked: licencia.id == site.licencia_id,
+ required: true, class: 'custom-control-input'
+ = f.label "licencia_id_#{licencia.id}",
+ class: 'custom-control-label' do
= licencia.name
= sanitize_markdown licencia.description,
- tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6]
+ tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6]
- unless licencia.custom?
- = link_to t('.licencia.url'), licencia.url, target: '_blank', class: 'btn btn-secondary', rel: 'noopener'
+ = link_to t('.licencia.url'),
+ licencia.url,
+ target: '_blank',
+ class: 'btn btn-secondary',
+ rel: 'noopener'
%hr/
@@ -158,9 +164,10 @@
%h2= t('.deploys.title')
%p.lead= t('.help.deploys')
- = f.fields_for :deploys do |deploy|
- = render "deploys/#{deploy.object.type.underscore}",
- deploy: deploy, site: site
+ - site.deployment_list.each do |deploy|
+ = f.fields_for :deploys, deploy do |deploy_fields|
+ = render "deploys/#{deploy.type.underscore}",
+ deploy: deploy_fields, site: site
.form-group
= f.submit submit, class: 'btn btn-secondary btn-lg btn-block'
diff --git a/app/views/sites/_header.haml b/app/views/sites/_header.haml
index c8931041..01b76083 100644
--- a/app/views/sites/_header.haml
+++ b/app/views/sites/_header.haml
@@ -1,3 +1,32 @@
.hyphens{ lang: site.default_locale }
%h1= site.title
%p.lead= site.description
+ %form.mb-3{ action: site_posts_path }
+ - filter_params.each do |param, values|
+ - next if param == :q
+
+ - [values].flatten.each do |value|
+ %input{ type: 'hidden',
+ name: values.is_a?(Array) ? "#{param}[]" : param,
+ value: value }
+ .form-group.flex-grow-0.m-0
+ %label.h3{ for: 'q' }= t('posts.index.search')
+ .input-group
+ %input.form-control.border.border-magenta.border-right-0#q{ type: 'search',
+ name: 'q',
+ value: filter_params[:q] }
+ .input-group-append
+ %span.input-group-text.background-white.magenta.border.border-magenta.border-top.border-left-0.border-right.border-bottom
+ %i.fa.fa-fw.fa-search
+ %input.sr-only{ type: 'submit' }
+ - filter_params.each do |param, values|
+ - [values].flatten.each do |value|
+ = link_to site_posts_path(site, **filter_params_by(filter_params, param, value)),
+ class: 'btn btn-secondary btn-sm',
+ title: t('posts.remove_filter_help', filter: value),
+ aria: { labelledby: "help-filter-#{param}" } do
+ - if param == :layout
+ = site.layouts[value.to_sym].humanized_name
+ - else
+ = value
+ ×
diff --git a/app/views/sites/edit.haml b/app/views/sites/edit.haml
index 4ae7308d..2013c6f6 100644
--- a/app/views/sites/edit.haml
+++ b/app/views/sites/edit.haml
@@ -1,5 +1,5 @@
.row.justify-content-center
- .col-md-8
+ .col-12.col-lg-8
%h1= t('.title', site: @site.name)
= render 'form', site: @site, submit: t('.submit')
diff --git a/app/views/sites/fetch.haml b/app/views/sites/fetch.haml
index 6d670d6f..9daaf189 100644
--- a/app/views/sites/fetch.haml
+++ b/app/views/sites/fetch.haml
@@ -1,5 +1,5 @@
.row.justify-content-center
- .col-md-8#pull
+ .col-12.col-lg-8#pull
%h1= t('.title')
%p.lead= sanitize_markdown t('.help.fetch'), tags: %w[em strong a]
@@ -10,7 +10,7 @@
- @commits.each do |commit|
.row.justify-content-center
- .col-md-8{ id: commit.oid }
+ .col-12.col-lg-8{ id: commit.oid }
%h1= commit.summary
%p.lead= render 'layouts/time', time: commit.time
@@ -19,12 +19,12 @@
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]
+ 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
+ .col-12.col-lg-8
= link_to t('.merge.request'), site_pull_path(@site),
- method: 'post', class: 'btn btn-secondary btn-lg'
+ method: 'post', class: 'btn btn-secondary btn-lg'
diff --git a/app/views/sites/index.haml b/app/views/sites/index.haml
index 1befa2d0..febc8548 100644
--- a/app/views/sites/index.haml
+++ b/app/views/sites/index.haml
@@ -1,5 +1,5 @@
%main.row
- %aside.col-md-3
+ %aside.col-12.col-lg-3
%h1= t('.title')
%p.lead= t('.help')
- if policy(Site).new?
diff --git a/app/views/sites/new.haml b/app/views/sites/new.haml
index 68c17882..23ee8b40 100644
--- a/app/views/sites/new.haml
+++ b/app/views/sites/new.haml
@@ -1,5 +1,5 @@
.row.justify-content-center
- .col-md-8
+ .col-12.col-lg-8
%h1= t('.title')
%p.lead= t('.help')
diff --git a/app/views/stats/index.haml b/app/views/stats/index.haml
index 1c1a31f1..0546bee4 100644
--- a/app/views/stats/index.haml
+++ b/app/views/stats/index.haml
@@ -11,10 +11,19 @@
%form.mb-5.form-inline{ method: 'get' }
- Stat::INTERVALS.each do |interval|
- = link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls], period_start: params[:period_start].to_date.try(:"beginning_of_#{interval}").to_date, period_end: params[:period_end]), class: "mb-0 btn #{@interval == interval ? 'btn-primary active' : 'btn-secondary' }"
+ = link_to t(".#{interval}"),
+ site_stats_path(interval: interval,
+ urls: params[:urls],
+ period_start: params[:period_start].to_date.try(:"beginning_of_#{interval}").to_date,
+ period_end: params[:period_end]),
+ class: "mb-0 btn #{@interval == interval ? 'btn-primary active' : 'btn-secondary'}"
- %input.form-control{ type: 'date', name: :period_start, value: params[:period_start] }
- %input.form-control{ type: 'date', name: :period_end, value: params[:period_end] }
+ %input.form-control{ type: 'date',
+ name: :period_start,
+ value: params[:period_start] }
+ %input.form-control{ type: 'date',
+ name: :period_end,
+ value: params[:period_end] }
%button.btn.btn-secondary.mb-0{ type: 'submit' }= t('.filter')
.mb-5
@@ -22,25 +31,38 @@
%p.lead= t('.host.description')
= line_chart site_stats_host_path(@chart_params), **@chart_options
- #custom-urls.mb-5
+ .mb-5#custom-urls
%h2= t('.urls.title')
%p.lead= t('.urls.description')
%form{ method: 'get', action: '#custom-urls' }
- %input{ type: 'hidden', name: 'interval', value: @interval }
- %input{ type: 'hidden', name: 'period_start', value: params[:period_start] }
- %input{ type: 'hidden', name: 'period_end', value: params[:period_end] }
+ %input{ type: 'hidden',
+ name: 'interval',
+ value: @interval }
+ %input{ type: 'hidden',
+ name: 'period_start',
+ value: params[:period_start] }
+ %input{ type: 'hidden',
+ name: 'period_end',
+ value: params[:period_end] }
.form-group
%label{ for: 'urls' }= t('.urls.label')
- %textarea#urls.form-control{ name: 'urls', autocomplete: 'on', required: true, rows: @normalized_urls.size + 1, aria_describedby: 'help-urls' }= @normalized_urls.join("\n")
- %small#help-urls.feedback.form-text.text-muted= t('.urls.help')
+ %textarea.form-control#urls{ name: 'urls',
+ autocomplete: 'on',
+ required: true,
+ rows: @normalized_urls.size + 1,
+ aria_describedby: 'help-urls' }= @normalized_urls.join("\n")
+ %small.feedback.form-text.text-muted#help-urls= t('.urls.help')
.form-group
%button.btn.btn-secondary{ type: 'submit' }= t('.urls.submit')
- if @normalized_urls.present?
- = line_chart site_stats_uris_path(urls: @normalized_urls, **@chart_params), **@chart_options
+ = line_chart site_stats_uris_path(urls: @normalized_urls,
+ **@chart_params),
+ **@chart_options
- .row.mb-5.row-cols-1.row-cols-md-2
+ .row.mb-5.row-cols-1.row-cols-lg-2
- @columns.each_pair do |column, values|
- next if values.blank?
+
.col.mb-5
%h2= t(".columns.#{column}.title")
%p.lead= t(".columns.#{column}.description")
@@ -57,7 +79,8 @@
%tbody
- values.each_pair do |col, val|
%tr
- %th{ scope: 'row', style: 'word-break: break-all' }= col.blank? ? t(".columns.#{column}.empty") : col
+ %th.break-all{ scope: 'row' }
+ = col.blank? ? t(".columns.#{column}.empty") : col
%td= val
.mb-5
%h2= t('.resources.title')
@@ -67,4 +90,5 @@
.mb-5
%h3= t(".resources.#{resource}.title")
%p.lead= t(".resources.#{resource}.description")
- = line_chart site_stats_resources_path(resource: resource, **@chart_params), **@chart_options.merge(StatsController::EXTRA_OPTIONS[resource])
+ = line_chart site_stats_resources_path(resource: resource, **@chart_params),
+ **@chart_options.merge(StatsController::EXTRA_OPTIONS[resource])
diff --git a/app/views/usuaries/index.haml b/app/views/usuaries/index.haml
index f972a91f..e0ee5e08 100644
--- a/app/views/usuaries/index.haml
+++ b/app/views/usuaries/index.haml
@@ -1,5 +1,5 @@
.row.justify-content-center
- .col.col-md-8
+ .col.col-lg-8
%h1= t('.title')
-# Una tabla de usuaries y otra de invitades, con acciones
@@ -8,16 +8,16 @@
.btn-group{ role: 'group', 'aria-label': t('.actions') }
- if @policy.invite?
= link_to t('.invite'),
- site_usuaries_invite_path(@site, invite_as: u.to_s),
- class: 'btn btn-secondary',
- data: { toggle: 'tooltip' },
- title: t('.help.invite', invite_as: u.to_s)
+ site_usuaries_invite_path(@site, invite_as: u.to_s),
+ class: 'btn btn-secondary',
+ data: { toggle: 'tooltip' },
+ title: t('.help.invite', invite_as: u.to_s)
- if policy(Collaboration.new(@site)).collaborate?
= link_to t('.public_invite'),
- site_collaborate_path(@site),
- class: 'btn btn-secondary',
- data: { toggle: 'tooltip' },
- title: t('.help.public_invite')
+ site_collaborate_path(@site),
+ class: 'btn btn-secondary',
+ data: { toggle: 'tooltip' },
+ title: t('.help.public_invite')
%p.lead= t(".help.#{u}")
%table.table.table-condensed
%tbody
@@ -34,28 +34,28 @@
%span.badge.badge-info= t('.invited')
%td
.btn-group{ role: 'group',
- aria: { label: t('.individual_actions') } }
+ aria: { label: t('.individual_actions') } }
- if @policy.demote? && @site.usuarie?(cuenta)
= link_to t('.demote.text'),
- site_usuarie_demote_path(@site, cuenta),
- class: 'btn btn-secondary',
- data: { toggle: 'tooltip',
- confirm: t('.demote.confirm') },
- title: t('.help.demote'),
- method: :patch
+ site_usuarie_demote_path(@site, cuenta),
+ class: 'btn btn-secondary',
+ data: { toggle: 'tooltip',
+ confirm: t('.demote.confirm') },
+ title: t('.help.demote'),
+ method: :patch
- if @policy.promote? && @site.invitade?(cuenta)
= link_to t('.promote.text'),
- site_usuarie_promote_path(@site, cuenta),
- class: 'btn btn-secondary',
- data: { toggle: 'tooltip',
- confirm: t('.promote.confirm') },
- title: t('.help.promote'),
- method: :patch
+ site_usuarie_promote_path(@site, cuenta),
+ class: 'btn btn-secondary',
+ data: { toggle: 'tooltip',
+ confirm: t('.promote.confirm') },
+ title: t('.help.promote'),
+ method: :patch
- if @policy.destroy?
= link_to t('.destroy.text'),
- site_usuarie_path(@site, cuenta),
- class: 'btn btn-secondary',
- data: { toggle: 'tooltip',
- confirm: t('.destroy.confirm') },
- title: t('.help.destroy'),
- method: :delete
+ site_usuarie_path(@site, cuenta),
+ class: 'btn btn-secondary',
+ data: { toggle: 'tooltip',
+ confirm: t('.destroy.confirm') },
+ title: t('.help.destroy'),
+ method: :delete
diff --git a/app/views/usuaries/invite.haml b/app/views/usuaries/invite.haml
index 2698fb8f..b2f47ccb 100644
--- a/app/views/usuaries/invite.haml
+++ b/app/views/usuaries/invite.haml
@@ -1,7 +1,7 @@
- invite_as = t("usuaries.invite_as.#{params[:invite_as]}")
.row.justify-content-center
- .col.col-md-8
+ .col.col-lg-8
%h1= t('.title', invite_as: invite_as)
= form_with url: site_usuaries_invite_path(@site), local: true do |f|
@@ -9,8 +9,8 @@
.form-group
= f.label :invitaciones do
= t('.invitaciones')
- %small.text-muted.form-text= t('.help.invitaciones',
- invite_as: invite_as)
+ %small.text-muted.form-text
+ = t('.help.invitaciones', invite_as: invite_as)
= f.text_area :invitaciones, class: 'form-control'
.form-group
= f.submit t('.submit'), class: 'btn btn-secondary'
diff --git a/bin/access_logs b/bin/access_logs
index cfeeb57a..b13dbc06 100755
--- a/bin/access_logs
+++ b/bin/access_logs
@@ -3,7 +3,7 @@ set -e
# Volcar y eliminar todos los access logs de dos días atrás
date="`dateadd today -1d`"
-file="/srv/http/_storage/${date}.psql.gz"
+file="/srv/_storage/${date}.psql.gz"
test -n "${date}"
test ! -f "${file}"
diff --git a/config/application.rb b/config/application.rb
index ed7e5a78..8dbdcbdd 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -68,7 +68,6 @@ module Sutty
config.active_record.schema_format = :sql
config.to_prepare do
- # Load application's model / class decorators
Dir.glob(File.join(File.dirname(__FILE__), '..', 'app', '**', '*_decorator.rb')).sort.each do |c|
Rails.configuration.cache_classes ? require(c) : load(c)
end
@@ -87,7 +86,7 @@ module Sutty
end
def nodes
- @nodes ||= ENV.fetch('SUTTY_NODES', '').split(',')
+ @nodes ||= ENV.fetch('SUTTY_NODES', 'anarres.sutty.nl').split(',')
end
end
end
diff --git a/config/database.yml b/config/database.yml
index cd599a24..41fae09c 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -26,8 +26,4 @@ test:
user: <%= ENV['USER'] %>
production:
- adapter: postgresql
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
- database: <%= ENV.fetch('DATABASE') { 'sutty' } %>
- user: sutty
- host: postgresql
+ url: <%= ENV['DATABASE_URL'] %>
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 05506587..d756dc68 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -46,10 +46,19 @@ Rails.application.configure do
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
- config.action_mailer.default_options = { from: ENV['DEFAULT_FROM'] }
+ config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', nil) }
config.action_mailer.default_url_options = { host: 'localhost',
port: 3000 }
+ config.middleware.use ExceptionNotification::Rack,
+ error_grouping: true,
+ email: {
+ email_prefix: '',
+ sender_address: ENV.fetch('DEFAULT_FROM', nil),
+ exception_recipients: ENV.fetch('EXCEPTION_TO', nil),
+ normalize_subject: true
+ }
+
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 5e9a2377..79916a79 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -334,10 +334,6 @@ en:
title: Synchronize to another Sutty node
success: Success!
error: Error
- deploy_distributed_press:
- title: Distributed Web
- success: Success!
- error: Error
help: You can contact us by replying to this e-mail
maintenance_mailer:
notice:
@@ -508,7 +504,7 @@ en:
storage network may continue retaining copies of the data
indefinitely.
- [Learn more](https://sutty.nl/learn-more-about-publish-to-dweb-functionality/)
+ [Learn more](https://sutty.nl/en/learn-more-about-publish-to-dweb-functionality/)
deploy_social_distributed_press:
title: 'Publish on the Fediverse'
help: |
@@ -631,8 +627,8 @@ en:
help: Please, look for the invalid fields to fix them
help:
name: "This will be the host name for your site, ie. **example**.sutty.nl. Choose an expression up to 63 characters. It can contain only lowercase letters, numbers and dashes, **and no spaces**. It can't start or end with a dash, or be entirely composed of numbers."
- title: 'The title can be anything you want'
- description: 'You site description that appears in search engines. Between 50 and 160 characters.'
+ title: 'The title can be anything you want.'
+ description: 'You site description that appears in search engines. Between 10 and 160 characters.'
design: 'Select the design for your site. We add more designs from time to time!'
licencia: 'Everything we publish has automatic copyright. This
means nobody can use our works without explicit permission. By
@@ -709,6 +705,9 @@ en:
next: Next page
empty: "There are no results for those search parameters."
caption: Post list
+ filters:
+ title: Filters
+ submit: Submit
attribute_ro:
file:
download: Download file
@@ -746,6 +745,12 @@ en:
image:
label: Image
destroy: Remove image
+ audio:
+ label: Audio file
+ logo:
+ label: Logo
+ download:
+ label: Archivo
belongs_to:
empty: "(Empty)"
predefined_value:
@@ -766,7 +771,7 @@ en:
date: 'date'
order: 'Order'
content: 'Text'
- new: 'Post types'
+ new: 'Add content'
remove_filter_help: 'Remove the filter: %{filter}'
categories: 'Everything'
index:
@@ -923,14 +928,14 @@ en:
queries:
show:
empty: '(empty)'
+ build_stats:
+ index:
+ title: "Publications"
schemas:
add:
add: 'Add'
filter:
filter: 'Filter'
remove: 'Back'
- build_stats:
- index:
- title: "Publications"
indexed_posts:
deleted: "Deleted indexed post %{path} from %{site} (records: %{records})"
diff --git a/config/locales/es.yml b/config/locales/es.yml
index a07b3799..87202ce5 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -317,10 +317,6 @@ es:
title: Fediverso
success: ¡Éxito!
error: Hubo un error
- deploy_reindex:
- title: Reindexación
- success: ¡Éxito!
- error: Hubo un error
deploy_localized_domain:
title: Dominio según idioma
success: ¡Éxito!
@@ -329,12 +325,12 @@ es:
title: Sincronizar al servidor alternativo
success: ¡Éxito!
error: Hubo un error
- deploy_full_rsync:
- title: Sincronizar a otro nodo de Sutty
+ deploy_reindex:
+ title: Reindexación
success: ¡Éxito!
error: Hubo un error
- deploy_distributed_press:
- title: Web distribuida
+ deploy_full_rsync:
+ title: Sincronizar a otro nodo de Sutty
success: ¡Éxito!
error: Hubo un error
help: Por cualquier duda, responde este correo para contactarte con nosotres.
@@ -637,7 +633,7 @@ es:
help:
name: 'El nombre de tu sitio que formará parte de la dirección (**ejemplo**.sutty.nl). Solo puede contener hasta 63 letras minúsculas, números y guiones, pero **sin espacios**. No puede empezar ni terminar con guión, ni estar compuesto enteramente por números.'
title: 'El título de tu sitio puede ser lo que quieras.'
- description: 'La descripción del sitio, que saldrá en buscadores. Entre 50 y 160 caracteres.'
+ description: 'La descripción del sitio, que saldrá en buscadores. Entre 10 y 160 caracteres.'
design: 'Elegí el diseño que va a tener tu sitio aquí. De tanto en tanto vamos sumando diseños nuevos.'
licencia: 'Todo lo que publicamos posee automáticamente derechos
de autore. Esto significa que nadie puede hacer uso de nuestras
@@ -717,6 +713,9 @@ es:
next: Página siguiente
empty: No hay artículos con estos parámetros de búsqueda.
caption: Lista de artículos
+ filters:
+ title: Filtros
+ submit: Aplicar
attribute_ro:
file:
download: Descargar archivo
@@ -754,6 +753,12 @@ es:
image:
label: Imagen
destroy: 'Eliminar imagen'
+ logo:
+ label: Logo
+ audio:
+ label: Audio
+ download:
+ label: Archivo
belongs_to:
empty: "(Vacío)"
predefined_value:
@@ -775,7 +780,7 @@ es:
order: 'Posición'
content: 'Cuerpo del artículo'
categories: 'Todos'
- new: 'Tipos de artículos'
+ new: 'Agregar contenido'
remove_filter_help: 'Quitar este filtro: %{filter}'
index:
search: 'Buscar'
@@ -931,14 +936,14 @@ es:
queries:
show:
empty: '(vacío)'
+ build_stats:
+ index:
+ title: "Publicaciones"
schemas:
add:
add: 'Agregar'
filter:
filter: 'Filtrar'
remove: 'Volver'
- build_stats:
- index:
- title: "Publicaciones"
indexed_posts:
deleted: "Eliminado artículo %{path} de %{site} (filas: %{records})"
diff --git a/db/migrate/20210504224343_create_indexed_posts.rb b/db/migrate/20210504224343_create_indexed_posts.rb
index 9cf21538..a88db1f3 100644
--- a/db/migrate/20210504224343_create_indexed_posts.rb
+++ b/db/migrate/20210504224343_create_indexed_posts.rb
@@ -27,7 +27,7 @@ class CreateIndexedPosts < ActiveRecord::Migration[6.1]
# Queremos mostrar el título por separado
t.string :title, default: ''
# También vamos a mostrar las categorías
- t.jsonb :front_matter, default: '{}'
+ t.jsonb :front_matter, default: {}
t.string :content, default: ''
t.tsvector :indexed_content
diff --git a/db/migrate/20231026211607_deprecate_deploy_reindex.rb b/db/migrate/20231026211607_deprecate_deploy_reindex.rb
new file mode 100644
index 00000000..945d01b4
--- /dev/null
+++ b/db/migrate/20231026211607_deprecate_deploy_reindex.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# Ya no es necesario reindexar por la fuerza
+class DeprecateDeployReindex < ActiveRecord::Migration[6.1]
+ def up
+ Deploy.where(type: 'DeployReindex').destroy_all
+ end
+
+ def down;end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 21cf04d0..61cbd6f9 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9,6 +9,27 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
+--
+-- Name: public; Type: SCHEMA; Schema: -; Owner: -
+--
+
+-- *not* creating schema, since initdb creates it
+
+
+--
+-- Name: dblink; Type: EXTENSION; Schema: -; Owner: -
+--
+
+CREATE EXTENSION IF NOT EXISTS dblink WITH SCHEMA public;
+
+
+--
+-- Name: EXTENSION dblink; Type: COMMENT; Schema: -; Owner: -
+--
+
+COMMENT ON EXTENSION dblink IS 'connect to other PostgreSQL databases from within a database';
+
+
--
-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: -
--
@@ -2600,6 +2621,13 @@ ALTER TABLE ONLY public.active_storage_attachments
ADD CONSTRAINT fk_rails_c3b3935057 FOREIGN KEY (blob_id) REFERENCES public.active_storage_blobs(id);
+--
+-- Name: publisher; Type: PUBLICATION; Schema: -; Owner: -
+--
+
+CREATE PUBLICATION publisher FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate');
+
+
--
-- PostgreSQL database dump complete
--
diff --git a/monit.conf b/monit.conf
index 2b7e50a8..78340482 100644
--- a/monit.conf
+++ b/monit.conf
@@ -15,7 +15,7 @@ check program fediblocks
if status != 0 then alert
check program access_logs
- with path "/srv/http/bin/access_logs" as uid "app" and gid "www-data"
+ with path "/srv/bin/access_logs" as uid "rails" and gid "www-data"
every "0 0 * * *"
if status != 0 then alert
diff --git a/public/assets/.sprockets-manifest-a1cbb907961024fc033716a7d30668dd.json b/public/assets/.sprockets-manifest-a1cbb907961024fc033716a7d30668dd.json
new file mode 100644
index 00000000..0d41cd78
--- /dev/null
+++ b/public/assets/.sprockets-manifest-a1cbb907961024fc033716a7d30668dd.json
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e718c91fc95c2200e15b2849cf21fbb7fdfd738dbdac8a5b81340ada0a568d0d
+size 11449
diff --git a/public/assets/.sprockets-manifest-c6294bb290dcb7473076f4de99ce9c00.json b/public/assets/.sprockets-manifest-c6294bb290dcb7473076f4de99ce9c00.json
deleted file mode 100644
index ecd1aee3..00000000
--- a/public/assets/.sprockets-manifest-c6294bb290dcb7473076f4de99ce9c00.json
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:53b13d54381374696503351fd6661242b1e22ea6f2078678bc560dfcfb701c8a
-size 10242
diff --git a/public/assets/application-20ccc989c289957de41530f1a6c63720c6980e71b85b2b73698ee2ad85e39788.css b/public/assets/application-20ccc989c289957de41530f1a6c63720c6980e71b85b2b73698ee2ad85e39788.css
new file mode 100644
index 00000000..109c05a1
--- /dev/null
+++ b/public/assets/application-20ccc989c289957de41530f1a6c63720c6980e71b85b2b73698ee2ad85e39788.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4c7d185f6b9de802352ba5905ed3becdf8248e231484006e3ed3efabbd15e9b6
+size 236932
diff --git a/public/assets/application-20ccc989c289957de41530f1a6c63720c6980e71b85b2b73698ee2ad85e39788.css.gz b/public/assets/application-20ccc989c289957de41530f1a6c63720c6980e71b85b2b73698ee2ad85e39788.css.gz
new file mode 100644
index 00000000..e1f69469
--- /dev/null
+++ b/public/assets/application-20ccc989c289957de41530f1a6c63720c6980e71b85b2b73698ee2ad85e39788.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d85bfc90e555d5a4d45424fe6b7c0b55baa560d4eb1db9ec0fe678d8dc126f7d
+size 32753
diff --git a/public/assets/application-4036902ca82a9c42d78f1ddc26a430aad6ff784e65914ef1219a2a1fc3150690.css b/public/assets/application-4036902ca82a9c42d78f1ddc26a430aad6ff784e65914ef1219a2a1fc3150690.css
new file mode 100644
index 00000000..710c9a9a
--- /dev/null
+++ b/public/assets/application-4036902ca82a9c42d78f1ddc26a430aad6ff784e65914ef1219a2a1fc3150690.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0e7171d20a358f5e98166d531672ebdb759f88d9fe13cc91486628be82e8fff0
+size 238993
diff --git a/public/assets/application-4036902ca82a9c42d78f1ddc26a430aad6ff784e65914ef1219a2a1fc3150690.css.gz b/public/assets/application-4036902ca82a9c42d78f1ddc26a430aad6ff784e65914ef1219a2a1fc3150690.css.gz
new file mode 100644
index 00000000..775069b9
--- /dev/null
+++ b/public/assets/application-4036902ca82a9c42d78f1ddc26a430aad6ff784e65914ef1219a2a1fc3150690.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:52ac8b1f629dc7b506caf0434217495ac158ccfdc0f474b341cd1bae80caa2e1
+size 33110
diff --git a/public/assets/application-5c98ae5b7e5b4444349f4c0175aa88fb76292284e1c68bfc4724151ceaf5113a.css b/public/assets/application-5c98ae5b7e5b4444349f4c0175aa88fb76292284e1c68bfc4724151ceaf5113a.css
new file mode 100644
index 00000000..1945b981
--- /dev/null
+++ b/public/assets/application-5c98ae5b7e5b4444349f4c0175aa88fb76292284e1c68bfc4724151ceaf5113a.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84981ad08b14786d825a89f6de709c8951b403f7dd5f5fa88f6a77e16760f284
+size 237180
diff --git a/public/assets/application-5c98ae5b7e5b4444349f4c0175aa88fb76292284e1c68bfc4724151ceaf5113a.css.gz b/public/assets/application-5c98ae5b7e5b4444349f4c0175aa88fb76292284e1c68bfc4724151ceaf5113a.css.gz
new file mode 100644
index 00000000..01e72c45
--- /dev/null
+++ b/public/assets/application-5c98ae5b7e5b4444349f4c0175aa88fb76292284e1c68bfc4724151ceaf5113a.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3df707f1055c66159e723d71cbfa6d501df539538a27e9fc63e789ff7df11e39
+size 32845
diff --git a/public/assets/application-66970ebaf46dd4256bcad9e73d5873abc104a84e04250f65e97902e27f4384ff.css b/public/assets/application-66970ebaf46dd4256bcad9e73d5873abc104a84e04250f65e97902e27f4384ff.css
new file mode 100644
index 00000000..f00dc228
--- /dev/null
+++ b/public/assets/application-66970ebaf46dd4256bcad9e73d5873abc104a84e04250f65e97902e27f4384ff.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bd3d4a89d43caf601fa759a513f49aa0747c60e9150651a04c26d0fd860c0cc9
+size 238892
diff --git a/public/assets/application-66970ebaf46dd4256bcad9e73d5873abc104a84e04250f65e97902e27f4384ff.css.gz b/public/assets/application-66970ebaf46dd4256bcad9e73d5873abc104a84e04250f65e97902e27f4384ff.css.gz
new file mode 100644
index 00000000..bde68d8f
--- /dev/null
+++ b/public/assets/application-66970ebaf46dd4256bcad9e73d5873abc104a84e04250f65e97902e27f4384ff.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:634e5c385d13e91be408691a232d23c0400b81a2ad143d7c0937c1362b0e18e0
+size 33084
diff --git a/public/assets/application-c675edc8163d1c94a6045443ef27d9982be94608a1c154d5d6ae413ce9c5c60a.css b/public/assets/application-c675edc8163d1c94a6045443ef27d9982be94608a1c154d5d6ae413ce9c5c60a.css
new file mode 100644
index 00000000..b0a8fba2
--- /dev/null
+++ b/public/assets/application-c675edc8163d1c94a6045443ef27d9982be94608a1c154d5d6ae413ce9c5c60a.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:02bb7f27db4d2a609e3a800df7d7e0c19abdc70d149af012a25958ea618e5988
+size 239097
diff --git a/public/assets/application-c675edc8163d1c94a6045443ef27d9982be94608a1c154d5d6ae413ce9c5c60a.css.gz b/public/assets/application-c675edc8163d1c94a6045443ef27d9982be94608a1c154d5d6ae413ce9c5c60a.css.gz
new file mode 100644
index 00000000..454cd9c4
--- /dev/null
+++ b/public/assets/application-c675edc8163d1c94a6045443ef27d9982be94608a1c154d5d6ae413ce9c5c60a.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4c9b3f35f17366137ffcda8065ee9d33f79774d4fc692ceddccffe7287b9f94a
+size 33100
diff --git a/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.gz b/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.gz
index f5afff36..5b4b25ab 100644
--- a/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.gz
+++ b/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.gz
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f690800fb0f0e2ce7b86b2564f2b4521030854c3eb13ccad730911181b906a1e
+oid sha256:a9c026060870ad426fdd5f04de97ab73f9e8d3fbcdb5fc389652c2dbb8d58cac
size 359
diff --git a/_sites/.keep b/public/assets/blazer/application-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js
similarity index 100%
rename from _sites/.keep
rename to public/assets/blazer/application-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js
diff --git a/public/assets/blazer/application-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js.gz b/public/assets/blazer/application-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js.gz
new file mode 100644
index 00000000..3ac4f898
--- /dev/null
+++ b/public/assets/blazer/application-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0a808f31c5704c32a1b85155b41b68f60e08929ab31352cd811138a2c70cacf3
+size 20
diff --git a/public/assets/blazer/application-c68598fc5732d248b857f9712a627ca58483ad188d6764320941b74fa71bbcac.css b/public/assets/blazer/application-c68598fc5732d248b857f9712a627ca58483ad188d6764320941b74fa71bbcac.css
new file mode 100644
index 00000000..3070f7a1
--- /dev/null
+++ b/public/assets/blazer/application-c68598fc5732d248b857f9712a627ca58483ad188d6764320941b74fa71bbcac.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c1c391e4691ea1afd08ba5daff66c790edd4b324af7dae390b616af36c7c2837
+size 134418
diff --git a/public/assets/blazer/application-c68598fc5732d248b857f9712a627ca58483ad188d6764320941b74fa71bbcac.css.gz b/public/assets/blazer/application-c68598fc5732d248b857f9712a627ca58483ad188d6764320941b74fa71bbcac.css.gz
new file mode 100644
index 00000000..f73b6993
--- /dev/null
+++ b/public/assets/blazer/application-c68598fc5732d248b857f9712a627ca58483ad188d6764320941b74fa71bbcac.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:858413c8cd07dff8ee14d3d8a2dbd98c6bad6a6dfd5b202a0e3bb00972dc9ef3
+size 23333
diff --git a/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.gz b/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.gz
index 44d30965..79704b9c 100644
--- a/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.gz
+++ b/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.gz
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:232dfba09dd29cfeff9e075a9ba30ed192f678b5946c1a1ec022e9887cc1dc8f
+oid sha256:ec5793d14d9c94d2e77f2402cc72cb02fbad30c2f9e694db24a1770c68b85ef2
size 20056
diff --git a/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.gz b/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.gz
index 49a5ccf2..ab024c17 100644
--- a/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.gz
+++ b/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.gz
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2879447637c85f780c62c9decfdc9b6847dfddc0c353ecf056a5563afd5d98c0
+oid sha256:3c0063a75b931092fa7dc19eab7df95f9b715c4444a164f3d63a37c3a7ac4b8f
size 26508
diff --git a/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.gz b/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.gz
index e77b3e37..5c760dc2 100644
--- a/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.gz
+++ b/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.gz
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c0668c774700aef315f25dcf8376be7aa88eec8b609b47f37c9ba53bbdb997ee
+oid sha256:54d10fbf4ee64ed8263cac863485e034c3f430afd686301c51cde3ecd49612b9
size 23360
diff --git a/public/assets/dark-591813c5ed25b766eda449e80715f9e51238c5defc9e38e60f02c31e11207839.css b/public/assets/dark-591813c5ed25b766eda449e80715f9e51238c5defc9e38e60f02c31e11207839.css
new file mode 100644
index 00000000..ec99e712
--- /dev/null
+++ b/public/assets/dark-591813c5ed25b766eda449e80715f9e51238c5defc9e38e60f02c31e11207839.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:df567b9eefa4ec9a50433a8e572abc260ed033b21bf7befd0ad52822270c28b6
+size 312
diff --git a/public/assets/dark-591813c5ed25b766eda449e80715f9e51238c5defc9e38e60f02c31e11207839.css.gz b/public/assets/dark-591813c5ed25b766eda449e80715f9e51238c5defc9e38e60f02c31e11207839.css.gz
new file mode 100644
index 00000000..f384cf5a
--- /dev/null
+++ b/public/assets/dark-591813c5ed25b766eda449e80715f9e51238c5defc9e38e60f02c31e11207839.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:771de7cf116906bf574d88d9138a236e4725dfdcc8a007d8a02573b3661528ed
+size 162
diff --git a/public/assets/dark-eb9f64ef7c7dac0cb3eb2b7bd511327d12625e00d4e11216441787288070f7dd.css b/public/assets/dark-eb9f64ef7c7dac0cb3eb2b7bd511327d12625e00d4e11216441787288070f7dd.css
new file mode 100644
index 00000000..4a76781b
--- /dev/null
+++ b/public/assets/dark-eb9f64ef7c7dac0cb3eb2b7bd511327d12625e00d4e11216441787288070f7dd.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:577591626adad685d7bdbaab0ae0590841b222586986876b3dacf157c52dffa3
+size 284
diff --git a/public/assets/dark-eb9f64ef7c7dac0cb3eb2b7bd511327d12625e00d4e11216441787288070f7dd.css.gz b/public/assets/dark-eb9f64ef7c7dac0cb3eb2b7bd511327d12625e00d4e11216441787288070f7dd.css.gz
new file mode 100644
index 00000000..74ba6f4f
--- /dev/null
+++ b/public/assets/dark-eb9f64ef7c7dac0cb3eb2b7bd511327d12625e00d4e11216441787288070f7dd.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6eff0565120128861b1f1c651ad7eeef39bc680b6b70bcc85aab4cd7abd5728f
+size 156
diff --git a/public/assets/dark-fb3fa48a29ed7f1bf5a9e28a71946b7a7a725aef3e7e96ad6c1a48133cfba2a4.css b/public/assets/dark-fb3fa48a29ed7f1bf5a9e28a71946b7a7a725aef3e7e96ad6c1a48133cfba2a4.css
new file mode 100644
index 00000000..ba01cf6c
--- /dev/null
+++ b/public/assets/dark-fb3fa48a29ed7f1bf5a9e28a71946b7a7a725aef3e7e96ad6c1a48133cfba2a4.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0fddb41356da3c564c20328f29aac7f13980c386b9d0731601c3adf476764295
+size 432
diff --git a/public/assets/dark-fb3fa48a29ed7f1bf5a9e28a71946b7a7a725aef3e7e96ad6c1a48133cfba2a4.css.gz b/public/assets/dark-fb3fa48a29ed7f1bf5a9e28a71946b7a7a725aef3e7e96ad6c1a48133cfba2a4.css.gz
new file mode 100644
index 00000000..0adccddc
--- /dev/null
+++ b/public/assets/dark-fb3fa48a29ed7f1bf5a9e28a71946b7a7a725aef3e7e96ad6c1a48133cfba2a4.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b2e82d47778ea7497888e28c5c92b22be7aea13c09b93b3a362778106d97a751
+size 189
diff --git a/public/assets/dark-ff88c7cf3bbaeac72c6342c94c6a9534da24bd3d91f6ebf86c56a739464f8095.css b/public/assets/dark-ff88c7cf3bbaeac72c6342c94c6a9534da24bd3d91f6ebf86c56a739464f8095.css
new file mode 100644
index 00000000..9ae9a755
--- /dev/null
+++ b/public/assets/dark-ff88c7cf3bbaeac72c6342c94c6a9534da24bd3d91f6ebf86c56a739464f8095.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e32f62213d4cd9e433d7f04fbabe103ec0e90af209ed204a9fff85984d353c7d
+size 332
diff --git a/public/assets/dark-ff88c7cf3bbaeac72c6342c94c6a9534da24bd3d91f6ebf86c56a739464f8095.css.gz b/public/assets/dark-ff88c7cf3bbaeac72c6342c94c6a9534da24bd3d91f6ebf86c56a739464f8095.css.gz
new file mode 100644
index 00000000..c05fd12e
--- /dev/null
+++ b/public/assets/dark-ff88c7cf3bbaeac72c6342c94c6a9534da24bd3d91f6ebf86c56a739464f8095.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d6f2ded0d74857c3c4887dd04c4ab67f1ce5c053cd9f091bba3f0ef7aa6f878e
+size 166
diff --git a/public/assets/editor-39df1ce9e0468043c2d1c998a38d45fac02beffa1b09088918b9c433c0750b4f.css b/public/assets/editor-39df1ce9e0468043c2d1c998a38d45fac02beffa1b09088918b9c433c0750b4f.css
new file mode 100644
index 00000000..9349b0ad
--- /dev/null
+++ b/public/assets/editor-39df1ce9e0468043c2d1c998a38d45fac02beffa1b09088918b9c433c0750b4f.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:403aaacf284c969542b423b9563f4cfa907a81cdf7443da106c3307bc22acd85
+size 2257
diff --git a/public/assets/editor-39df1ce9e0468043c2d1c998a38d45fac02beffa1b09088918b9c433c0750b4f.css.gz b/public/assets/editor-39df1ce9e0468043c2d1c998a38d45fac02beffa1b09088918b9c433c0750b4f.css.gz
new file mode 100644
index 00000000..9b6ecad0
--- /dev/null
+++ b/public/assets/editor-39df1ce9e0468043c2d1c998a38d45fac02beffa1b09088918b9c433c0750b4f.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:944d9e5d96f79b8acf12b5e4896d6f60bb9f6fbba5cfb200269ba2b263015f93
+size 687
diff --git a/public/assets/fonts-c51ac4112aa6f7493cea2408ae5778f034eccdd0565697f589654dabb363d79a.css b/public/assets/fonts-c51ac4112aa6f7493cea2408ae5778f034eccdd0565697f589654dabb363d79a.css
new file mode 100644
index 00000000..1ee2f722
--- /dev/null
+++ b/public/assets/fonts-c51ac4112aa6f7493cea2408ae5778f034eccdd0565697f589654dabb363d79a.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6cd9507ad59b2d60d4fefd2ee1e20b75c263872ce57bf8d62b6a7fd565d89279
+size 785
diff --git a/public/assets/fonts-c51ac4112aa6f7493cea2408ae5778f034eccdd0565697f589654dabb363d79a.css.gz b/public/assets/fonts-c51ac4112aa6f7493cea2408ae5778f034eccdd0565697f589654dabb363d79a.css.gz
new file mode 100644
index 00000000..6aa4a074
--- /dev/null
+++ b/public/assets/fonts-c51ac4112aa6f7493cea2408ae5778f034eccdd0565697f589654dabb363d79a.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e7ef626d9caeb98918792415865ddb3339176fd6f0d64866263b3e69cf1b90cf
+size 379
diff --git a/public/assets/new_editor-9962df2caf2874010146cfd735216677865b7e37bee68c59e997a9aa2198594d.css b/public/assets/new_editor-9962df2caf2874010146cfd735216677865b7e37bee68c59e997a9aa2198594d.css
new file mode 100644
index 00000000..65a56f63
--- /dev/null
+++ b/public/assets/new_editor-9962df2caf2874010146cfd735216677865b7e37bee68c59e997a9aa2198594d.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d967abb43a679cd18d84b114ad0616239a67241a8402185d2929e27b02e1a4b3
+size 171
diff --git a/public/assets/new_editor-9962df2caf2874010146cfd735216677865b7e37bee68c59e997a9aa2198594d.css.gz b/public/assets/new_editor-9962df2caf2874010146cfd735216677865b7e37bee68c59e997a9aa2198594d.css.gz
new file mode 100644
index 00000000..07501a46
--- /dev/null
+++ b/public/assets/new_editor-9962df2caf2874010146cfd735216677865b7e37bee68c59e997a9aa2198594d.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6d29a581f5f246d75366cda62749a4a9166ad9daeb6bfcc168d7bedc9d973dda
+size 115
diff --git a/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.gz b/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.gz
index dffee65e..11eacf60 100644
--- a/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.gz
+++ b/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.gz
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:19973f783284df5608ec79a4fa46dce362f5626112c218266f04329b01049c43
+oid sha256:b96644799494b8c2117ce68d763c392418dbb1b065ffc3d2c52061d9a046ed7f
size 943
diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest
new file mode 100644
index 00000000..f00037df
--- /dev/null
+++ b/public/manifest.webmanifest
@@ -0,0 +1,50 @@
+
+
+{
+ "name": "Sutty",
+ "short_name": "Sutty",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#fff",
+ "description": "Sutty crea sitios seguros, veloces y visibles",
+ "icons": [
+
+
+ {
+ "src": "assets/images/icon48.png",
+ "sizes": "48x48",
+ "type": "image/png"
+ },
+
+ {
+ "src": "assets/images/icon72.png",
+ "sizes": "72x72",
+ "type": "image/png"
+ },
+
+ {
+ "src": "assets/images/icon96.png",
+ "sizes": "96x96",
+ "type": "image/png"
+ },
+
+ {
+ "src": "assets/images/icon144.png",
+ "sizes": "144x144",
+ "type": "image/png"
+ },
+
+ {
+ "src": "assets/images/icon168.png",
+ "sizes": "168x168",
+ "type": "image/png"
+ },
+
+ {
+ "src": "assets/images/icon192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ }
+
+ ]
+}
diff --git a/public/packs/css/application-1224e21e.css b/public/packs/css/application-1224e21e.css
deleted file mode 100644
index 390ac1f2..00000000
--- a/public/packs/css/application-1224e21e.css
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a7ffc74f9219623a13902d9ac806b9e71cfdabc2428e1f6ae4015da56cb7c7d9
-size 49314
diff --git a/public/packs/css/application-1224e21e.css.br b/public/packs/css/application-1224e21e.css.br
deleted file mode 100644
index 1f5776e2..00000000
--- a/public/packs/css/application-1224e21e.css.br
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:68809099d5fd771490c8eee922fe59c2be223a7b3ea6bec6889bc26380dcc788
-size 10017
diff --git a/public/packs/css/application-1224e21e.css.gz b/public/packs/css/application-1224e21e.css.gz
deleted file mode 100644
index 3784a199..00000000
--- a/public/packs/css/application-1224e21e.css.gz
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:8058f9e1c5cfdf8de6d896fe9dd138b527e2409e1c467f79b376f75fa0c24b76
-size 12355
diff --git a/public/packs/css/application-e0d66cef.css b/public/packs/css/application-e0d66cef.css
new file mode 100644
index 00000000..cca00ffe
--- /dev/null
+++ b/public/packs/css/application-e0d66cef.css
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:185c95d8b69c7b6265a5e7a68ba926c2bd1f768fda8cf3be3368137ef6357730
+size 50273
diff --git a/public/packs/css/application-e0d66cef.css.br b/public/packs/css/application-e0d66cef.css.br
new file mode 100644
index 00000000..6e13a79f
--- /dev/null
+++ b/public/packs/css/application-e0d66cef.css.br
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9f5d0b5074f2b7fbbc9883beb27da4b4ca91652d3a0c931896a2fcaf97ec8142
+size 10185
diff --git a/public/packs/css/application-e0d66cef.css.gz b/public/packs/css/application-e0d66cef.css.gz
new file mode 100644
index 00000000..aced299a
--- /dev/null
+++ b/public/packs/css/application-e0d66cef.css.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8a7df2acc6a691d864ccff15bc7d86c6a19a9f780d92d6fd9d1e6db6ff5e25ec
+size 12559
diff --git a/public/packs/js/application-5e567763c88fb352be77.js b/public/packs/js/application-5e567763c88fb352be77.js
new file mode 100644
index 00000000..46562262
--- /dev/null
+++ b/public/packs/js/application-5e567763c88fb352be77.js
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:62d1f8867d88ec64d12cee2e86ad4fdd363ffe4a3516c0a5c6d65cd3219624a7
+size 1644364
diff --git a/public/packs/js/application-5e567763c88fb352be77.js.LICENSE.txt b/public/packs/js/application-5e567763c88fb352be77.js.LICENSE.txt
new file mode 100644
index 00000000..dfe27ce7
--- /dev/null
+++ b/public/packs/js/application-5e567763c88fb352be77.js.LICENSE.txt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7073b760337ff91f74933ece915ce12f8653f990f607a0925cc002dd610fa0f9
+size 1097
diff --git a/public/packs/js/application-5e567763c88fb352be77.js.br b/public/packs/js/application-5e567763c88fb352be77.js.br
new file mode 100644
index 00000000..ed58903d
--- /dev/null
+++ b/public/packs/js/application-5e567763c88fb352be77.js.br
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c91173b409bd6e84f40cefdc19d466c848e94d20087cb23a063d2b7bd54458d1
+size 366685
diff --git a/public/packs/js/application-5e567763c88fb352be77.js.gz b/public/packs/js/application-5e567763c88fb352be77.js.gz
new file mode 100644
index 00000000..b187d06b
--- /dev/null
+++ b/public/packs/js/application-5e567763c88fb352be77.js.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:908bd87788fff1388ed76e60646715aaac6c6f65d5e28620d338b1b39e3434f4
+size 483358
diff --git a/public/packs/js/application-5e567763c88fb352be77.js.map b/public/packs/js/application-5e567763c88fb352be77.js.map
new file mode 100644
index 00000000..094ae7c1
--- /dev/null
+++ b/public/packs/js/application-5e567763c88fb352be77.js.map
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bd0e2c4a400ba5fdb9d795dc0d533814d5180e89aa2ac2ba0954061e47d6f959
+size 6387599
diff --git a/public/packs/js/application-5e567763c88fb352be77.js.map.br b/public/packs/js/application-5e567763c88fb352be77.js.map.br
new file mode 100644
index 00000000..20280fd8
--- /dev/null
+++ b/public/packs/js/application-5e567763c88fb352be77.js.map.br
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7f4de443176d1e60abc706a51412d3bb450a48a4c4af564de168cdb13dcf55e6
+size 1375691
diff --git a/public/packs/js/application-5e567763c88fb352be77.js.map.gz b/public/packs/js/application-5e567763c88fb352be77.js.map.gz
new file mode 100644
index 00000000..6d5abc8e
--- /dev/null
+++ b/public/packs/js/application-5e567763c88fb352be77.js.map.gz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8821c193db88c59c72f379ee5c4dbce624dab28802722e623b7bbb4ec12ff7ff
+size 1701215
diff --git a/public/packs/js/application-d4a959210a82d3d1b10f.js b/public/packs/js/application-d4a959210a82d3d1b10f.js
deleted file mode 100644
index ae056684..00000000
--- a/public/packs/js/application-d4a959210a82d3d1b10f.js
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1d178fb353afcf2dedc8bba8ce4b978f0bc93f679479a7f67c0473e25324a72c
-size 1516360
diff --git a/public/packs/js/application-d4a959210a82d3d1b10f.js.LICENSE.txt b/public/packs/js/application-d4a959210a82d3d1b10f.js.LICENSE.txt
deleted file mode 100644
index 979d1ab9..00000000
--- a/public/packs/js/application-d4a959210a82d3d1b10f.js.LICENSE.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c3b9ae1697c4b8a404afe77afe035de28b7f4880e9f52caac82620bb8d8ed495
-size 854
diff --git a/public/packs/js/application-d4a959210a82d3d1b10f.js.br b/public/packs/js/application-d4a959210a82d3d1b10f.js.br
deleted file mode 100644
index b7a543a0..00000000
--- a/public/packs/js/application-d4a959210a82d3d1b10f.js.br
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:f37b681c0c2989dba2d59695f7d3d38c9357edab713b2b5899bf2c20dbed1f11
-size 333228
diff --git a/public/packs/js/application-d4a959210a82d3d1b10f.js.gz b/public/packs/js/application-d4a959210a82d3d1b10f.js.gz
deleted file mode 100644
index f800b3fd..00000000
--- a/public/packs/js/application-d4a959210a82d3d1b10f.js.gz
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a34e726274558a688517e19a1761f028072b7a6f614b4d1ec6f8609e61443bb4
-size 441095
diff --git a/public/packs/js/application-d4a959210a82d3d1b10f.js.map b/public/packs/js/application-d4a959210a82d3d1b10f.js.map
deleted file mode 100644
index 76a8fd29..00000000
--- a/public/packs/js/application-d4a959210a82d3d1b10f.js.map
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d5c9c622b3d7a39cf332a95f1877dc8d5cbec844fa99cb55c75e45dfed5531dd
-size 5988200
diff --git a/public/packs/js/application-d4a959210a82d3d1b10f.js.map.br b/public/packs/js/application-d4a959210a82d3d1b10f.js.map.br
deleted file mode 100644
index a9d2dce3..00000000
--- a/public/packs/js/application-d4a959210a82d3d1b10f.js.map.br
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d5242fa25b04407204920fb98a250ea9af7de4d3575ea1dcb79801f2c002fb8f
-size 1279231
diff --git a/public/packs/js/application-d4a959210a82d3d1b10f.js.map.gz b/public/packs/js/application-d4a959210a82d3d1b10f.js.map.gz
deleted file mode 100644
index ffbbaff0..00000000
--- a/public/packs/js/application-d4a959210a82d3d1b10f.js.map.gz
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d70208139d5de996bc6a01daacf3fc7e07edf975296795cae487e71e2c198e07
-size 1583975
diff --git a/public/packs/manifest.json b/public/packs/manifest.json
index d0f77c7e..5b28f687 100644
--- a/public/packs/manifest.json
+++ b/public/packs/manifest.json
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0e5e2ddeee2bb351e8f9e0b16d28fcebd7314227abdffa65e02e83755db591d6
+oid sha256:d51c7b68bc509554c0f29a511aa553c42324b94e9c8e5b7977d109cebcae7978
size 1426
diff --git a/public/packs/manifest.json.br b/public/packs/manifest.json.br
index 76978873..2090a5e8 100644
--- a/public/packs/manifest.json.br
+++ b/public/packs/manifest.json.br
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a7c9ab4526ce1ce929b4d0c242dee97cacc9f79fac73948c42a4167494e251e1
-size 321
+oid sha256:1069474766e7b968961892eb355a70602ee18cea6d00e3b803c4f23b385a686a
+size 320
diff --git a/public/packs/manifest.json.gz b/public/packs/manifest.json.gz
index c691abe7..93f3717a 100644
--- a/public/packs/manifest.json.gz
+++ b/public/packs/manifest.json.gz
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:caf56db4d1167dd81eadb2da3a1fb6d14bf9f668381d4aa8295f333bcd649f00
+oid sha256:d6ee7bca4e6e888c05b69c1ad646395ea87fe2861858b824b3df5d15a72b8ee8
size 365
diff --git a/test/controllers/posts_controller_test.rb b/test/controllers/posts_controller_test.rb
index b8c9f560..bc1c7e32 100644
--- a/test/controllers/posts_controller_test.rb
+++ b/test/controllers/posts_controller_test.rb
@@ -150,7 +150,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
end
posts = @site.posts(**lang)
- reorder = Hash[posts.map { |p| p.uuid.value }.shuffle.each_with_index.to_a]
+ reorder = posts.map { |p| p.uuid.value }.shuffle.each_with_index.to_a.to_h
post site_posts_reorder_url(@site),
headers: @authorization,
@@ -159,10 +159,16 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
@site = Site.find @site.id
assert_equal reorder,
- Hash[@site.posts(**lang).map do |p|
+ @site.posts(**lang).map do |p|
[p.uuid.value, p.order.value]
- end]
+ end.to_h
assert_equal I18n.t('post_service.reorder'),
@site.repository.rugged.head.target.message
end
+
+ test 'si hay algún error se recupera' do
+ File.write(File.join(@site.path, '_es', "#{Date.today}-#{SecureRandom.hex}.markdown"), '')
+
+ get site_posts_url(@site), headers: @authorization
+ end
end