mirror of
https://0xacab.org/sutty/sutty
synced 2025-02-23 15:11:49 +00:00
Merge branch 'rails' into issue-10464
This commit is contained in:
commit
0b4601c347
61 changed files with 564 additions and 168 deletions
|
@ -2,3 +2,4 @@
|
||||||
*
|
*
|
||||||
# Solo agregar lo que usamos en COPY
|
# Solo agregar lo que usamos en COPY
|
||||||
# !./archivo
|
# !./archivo
|
||||||
|
!./monit.conf
|
||||||
|
|
|
@ -17,6 +17,8 @@ RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pando
|
||||||
|
|
||||||
RUN apk add npm && npm install -g pnpm && apk del npm
|
RUN apk add npm && npm install -g pnpm && apk del npm
|
||||||
|
|
||||||
|
COPY ./monit.conf /etc/monit.d/sutty.conf
|
||||||
|
|
||||||
VOLUME "/srv"
|
VOLUME "/srv"
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -92,7 +92,7 @@ gem 'stackprof'
|
||||||
gem 'prometheus_exporter'
|
gem 'prometheus_exporter'
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
gem 'fast_jsonparser'
|
gem 'fast_jsonparser', '~> 0.5.0'
|
||||||
gem 'down'
|
gem 'down'
|
||||||
gem 'sourcemap'
|
gem 'sourcemap'
|
||||||
gem 'rack-cors'
|
gem 'rack-cors'
|
||||||
|
|
|
@ -295,7 +295,7 @@ GEM
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
jekyll-ignore-layouts (0.1.2)
|
jekyll-ignore-layouts (0.1.2)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
jekyll-images (0.3.0)
|
jekyll-images (0.3.2)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
ruby-filemagic (~> 0.7)
|
ruby-filemagic (~> 0.7)
|
||||||
ruby-vips (~> 2)
|
ruby-vips (~> 2)
|
||||||
|
|
8
Procfile
8
Procfile
|
@ -1,9 +1,3 @@
|
||||||
migrate: bundle exec rake db:prepare db:seed
|
cleanup: bundle exec rake cleanup:everything
|
||||||
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_"
|
|
||||||
stats: bundle exec rake stats:process_all
|
stats: bundle exec rake stats:process_all
|
||||||
distributed_press_renew_tokens: bundle exec rake distributed_press:tokens:renew
|
distributed_press_renew_tokens: bundle exec rake distributed_press:tokens:renew
|
||||||
|
|
|
@ -15,7 +15,7 @@ module Api
|
||||||
params: airbrake_params.to_h
|
params: airbrake_params.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
render status: 201, json: { id: 1, url: root_url }
|
render status: 201, json: { id: 1, url: '' }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Api
|
||||||
|
|
||||||
# Lista de nombres de dominios a emitir certificados
|
# Lista de nombres de dominios a emitir certificados
|
||||||
def index
|
def index
|
||||||
render json: sites_names + alternative_names + api_names
|
render json: sites_names + alternative_names + api_names + www_names
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sitios con hidden service de Tor
|
# Sitios con hidden service de Tor
|
||||||
|
@ -28,7 +28,7 @@ module Api
|
||||||
site = Site.find_by(name: params[:name])
|
site = Site.find_by(name: params[:name])
|
||||||
|
|
||||||
if site
|
if site
|
||||||
usuarie = GitAuthor.new email: 'tor@' + Site.domain, name: 'Tor'
|
usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor'
|
||||||
service = SiteService.new site: site, usuarie: usuarie,
|
service = SiteService.new site: site, usuarie: usuarie,
|
||||||
params: params
|
params: params
|
||||||
service.add_onion
|
service.add_onion
|
||||||
|
@ -39,14 +39,22 @@ module Api
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def canonicalize(name)
|
||||||
|
name.end_with?('.') ? name[0..-2] : "#{name}.#{Site.domain}"
|
||||||
|
end
|
||||||
|
|
||||||
# Nombres de los sitios
|
# Nombres de los sitios
|
||||||
def sites_names
|
def sites_names
|
||||||
Site.all.order(:name).pluck(:name)
|
Site.all.order(:name).pluck(:name).map do |name|
|
||||||
|
canonicalize name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Dominios alternativos
|
# Dominios alternativos
|
||||||
def alternative_names
|
def alternative_names
|
||||||
DeployAlternativeDomain.all.map(&:hostname)
|
(DeployAlternativeDomain.all.map(&:hostname) + DeployLocalizedDomain.all.map(&:hostname)).map do |name|
|
||||||
|
canonicalize name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtener todos los sitios con API habilitada, es decir formulario
|
# Obtener todos los sitios con API habilitada, es decir formulario
|
||||||
|
@ -56,7 +64,16 @@ module Api
|
||||||
def api_names
|
def api_names
|
||||||
Site.where(contact: true)
|
Site.where(contact: true)
|
||||||
.or(Site.where(colaboracion_anonima: true))
|
.or(Site.where(colaboracion_anonima: true))
|
||||||
.select("'api.' || name as name").map(&:name)
|
.select("'api.' || name as name").map(&:name).map do |name|
|
||||||
|
canonicalize name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Todos los dominios con WWW habilitado
|
||||||
|
def www_names
|
||||||
|
Site.where(id: DeployWww.all.pluck(:site_id)).select("'www.' || name as name").map(&:name).map do |name|
|
||||||
|
canonicalize name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,17 +46,19 @@ class ApplicationController < ActionController::Base
|
||||||
# defecto.
|
# defecto.
|
||||||
#
|
#
|
||||||
# Esto se refiere al idioma de la interfaz, no de los artículos.
|
# Esto se refiere al idioma de la interfaz, no de los artículos.
|
||||||
def current_locale(include_params: true, site: nil)
|
#
|
||||||
return params[:locale] if include_params && params[:locale].present?
|
# @return [String,Symbol]
|
||||||
|
def current_locale
|
||||||
|
session[:locale] = params[:change_locale_to] if params[:change_locale_to].present?
|
||||||
|
|
||||||
current_usuarie&.lang || I18n.locale
|
session[:locale] || current_usuarie&.lang || I18n.locale
|
||||||
end
|
end
|
||||||
|
|
||||||
# El idioma es el preferido por le usuarie, pero no necesariamente se
|
# El idioma es el preferido por le usuarie, pero no necesariamente se
|
||||||
# corresponde con el idioma de los artículos, porque puede querer
|
# corresponde con el idioma de los artículos, porque puede querer
|
||||||
# traducirlos.
|
# traducirlos.
|
||||||
def set_locale(&action)
|
def set_locale(&action)
|
||||||
I18n.with_locale(current_locale(include_params: false), &action)
|
I18n.with_locale(current_locale, &action)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Muestra una página 404
|
# Muestra una página 404
|
||||||
|
@ -88,4 +90,12 @@ class ApplicationController < ActionController::Base
|
||||||
def prepare_exception_notifier
|
def prepare_exception_notifier
|
||||||
request.env['exception_notifier.exception_data'] = { usuarie: current_usuarie }
|
request.env['exception_notifier.exception_data'] = { usuarie: current_usuarie }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Olvidar el idioma elegido antes de iniciar la sesión y reenviar a
|
||||||
|
# los sitios en el idioma de le usuarie.
|
||||||
|
def after_sign_in_path_for(resource)
|
||||||
|
session[:locale] = nil
|
||||||
|
|
||||||
|
sites_path
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,7 @@ class PostsController < ApplicationController
|
||||||
|
|
||||||
# Las URLs siempre llevan el idioma actual o el de le usuarie
|
# Las URLs siempre llevan el idioma actual o el de le usuarie
|
||||||
def default_url_options
|
def default_url_options
|
||||||
{ locale: current_locale }
|
{ locale: locale }
|
||||||
end
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
|
@ -68,9 +68,7 @@ class SitesController < ApplicationController
|
||||||
def enqueue
|
def enqueue
|
||||||
authorize site
|
authorize site
|
||||||
|
|
||||||
# XXX: Convertir en una máquina de estados?
|
SiteService.new(site: site).deploy
|
||||||
site.enqueue!
|
|
||||||
DeployJob.perform_async site.id
|
|
||||||
|
|
||||||
redirect_to site_posts_path(site, locale: site.default_locale)
|
redirect_to site_posts_path(site, locale: site.default_locale)
|
||||||
end
|
end
|
||||||
|
|
81
app/javascript/controllers/non_geo_controller.js
Normal file
81
app/javascript/controllers/non_geo_controller.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { Controller } from 'stimulus'
|
||||||
|
|
||||||
|
require("leaflet/dist/leaflet.css")
|
||||||
|
import L from 'leaflet'
|
||||||
|
delete L.Icon.Default.prototype._getIconUrl
|
||||||
|
|
||||||
|
L.Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||||
|
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||||
|
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ 'lat', 'lng', 'map', 'overlay' ]
|
||||||
|
|
||||||
|
async connect () {
|
||||||
|
this.marker()
|
||||||
|
|
||||||
|
this.latTarget.addEventListener('change', event => this.marker())
|
||||||
|
this.lngTarget.addEventListener('change', event => this.marker())
|
||||||
|
window.addEventListener('resize', event => this.map.invalidateSize())
|
||||||
|
|
||||||
|
this.map.on('click', event => {
|
||||||
|
this.latTarget.value = event.latlng.lat
|
||||||
|
this.lngTarget.value = event.latlng.lng
|
||||||
|
|
||||||
|
this.latTarget.dispatchEvent(new Event('change'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
marker () {
|
||||||
|
if (this._marker) this.map.removeLayer(this._marker)
|
||||||
|
|
||||||
|
this._marker = L.marker(this.coords).addTo(this.map)
|
||||||
|
|
||||||
|
return this._marker
|
||||||
|
}
|
||||||
|
|
||||||
|
get lat () {
|
||||||
|
const lat = parseFloat(this.latTarget.value)
|
||||||
|
|
||||||
|
return isNaN(lat) ? 0 : lat
|
||||||
|
}
|
||||||
|
|
||||||
|
get lng () {
|
||||||
|
const lng = parseFloat(this.lngTarget.value)
|
||||||
|
|
||||||
|
return isNaN(lng) ? 0 : lng
|
||||||
|
}
|
||||||
|
|
||||||
|
get coords () {
|
||||||
|
return [this.lat, this.lng]
|
||||||
|
}
|
||||||
|
|
||||||
|
get bounds () {
|
||||||
|
return [
|
||||||
|
[0, 0],
|
||||||
|
[
|
||||||
|
this.svgOverlay.viewBox.baseVal.height,
|
||||||
|
this.svgOverlay.viewBox.baseVal.width,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get map () {
|
||||||
|
if (!this._map) {
|
||||||
|
this._map = L.map(this.mapTarget, {
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 5
|
||||||
|
}).setView(this.coords, 0);
|
||||||
|
|
||||||
|
this._layer = L.tileLayer(`${this.element.dataset.site}public/map/{z}/{y}/{x}.png`, {
|
||||||
|
minNativeZoom: 0,
|
||||||
|
maxNativeZoom: 5,
|
||||||
|
noWrap: true
|
||||||
|
}).addTo(this._map);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._map
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,11 +103,7 @@ export default class extends Controller {
|
||||||
this.reorder()
|
this.reorder()
|
||||||
|
|
||||||
// Mantenemos el primero a la vista
|
// Mantenemos el primero a la vista
|
||||||
if ("scrollIntoViewIfNeeded" in rows[0].row) {
|
rows[0].row.scrollIntoView({ block: "center" });
|
||||||
rows[0].row.scrollIntoViewIfNeeded()
|
|
||||||
} else {
|
|
||||||
rows[0].row.scrollIntoView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
counter () {
|
counter () {
|
||||||
|
@ -146,7 +142,7 @@ export default class extends Controller {
|
||||||
this.reorder()
|
this.reorder()
|
||||||
|
|
||||||
// Mantenemos el primero a la vista
|
// Mantenemos el primero a la vista
|
||||||
rows[0].row.scrollIntoViewIfNeeded()
|
rows[0].row.scrollIntoView({ block: "center" });
|
||||||
}
|
}
|
||||||
|
|
||||||
bottom (event) {
|
bottom (event) {
|
||||||
|
@ -167,7 +163,7 @@ export default class extends Controller {
|
||||||
this.reorder()
|
this.reorder()
|
||||||
|
|
||||||
// Mantenemos el primero a la vista
|
// Mantenemos el primero a la vista
|
||||||
rows[0].row.scrollIntoViewIfNeeded()
|
rows[0].row.scrollIntoView({ block: "center" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -5,6 +5,8 @@ class DeployJob < ApplicationJob
|
||||||
class DeployException < StandardError; end
|
class DeployException < StandardError; end
|
||||||
class DeployTimedOutException < DeployException; end
|
class DeployTimedOutException < DeployException; end
|
||||||
|
|
||||||
|
discard_on ActiveRecord::RecordNotFound
|
||||||
|
|
||||||
# rubocop:disable Metrics/MethodLength
|
# rubocop:disable Metrics/MethodLength
|
||||||
def perform(site, notify: true, time: Time.now, output: false)
|
def perform(site, notify: true, time: Time.now, output: false)
|
||||||
@output = output
|
@output = output
|
||||||
|
|
|
@ -20,6 +20,18 @@ module ActiveStorage
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Solo copiamos el archivo si no existe
|
||||||
|
#
|
||||||
|
# @param :key [String]
|
||||||
|
# @param :io [IO]
|
||||||
|
# @param :checksum [String]
|
||||||
|
def upload(key, io, checksum: nil, **)
|
||||||
|
instrument :upload, key: key, checksum: checksum do
|
||||||
|
IO.copy_stream(io, make_path_for(key)) unless exist?(key)
|
||||||
|
ensure_integrity_of(key, checksum) if checksum
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Lo mismo que en DiskService agregando el nombre de archivo en la
|
# Lo mismo que en DiskService agregando el nombre de archivo en la
|
||||||
# firma. Esto permite que luego podamos guardar el archivo donde
|
# firma. Esto permite que luego podamos guardar el archivo donde
|
||||||
# corresponde.
|
# corresponde.
|
||||||
|
@ -67,7 +79,9 @@ module ActiveStorage
|
||||||
# @param :key [String]
|
# @param :key [String]
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def filename_for(key)
|
def filename_for(key)
|
||||||
ActiveStorage::Blob.where(key: key).limit(1).pluck(:filename).first
|
ActiveStorage::Blob.where(key: key).limit(1).pluck(:filename).first.tap do |filename|
|
||||||
|
raise ArgumentError, "Filename for key #{key} is blank" if filename.blank?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Crea una ruta para la llave con un nombre conocido.
|
# Crea una ruta para la llave con un nombre conocido.
|
||||||
|
|
|
@ -12,10 +12,11 @@ class DeployMailer < ApplicationMailer
|
||||||
include ActionView::Helpers::DateHelper
|
include ActionView::Helpers::DateHelper
|
||||||
|
|
||||||
# rubocop:disable Metrics/AbcSize
|
# rubocop:disable Metrics/AbcSize
|
||||||
def deployed(deploys)
|
def deployed(deploys = {})
|
||||||
usuarie = Usuarie.find(params[:usuarie])
|
usuarie = Usuarie.find(params[:usuarie])
|
||||||
site = usuarie.sites.find(params[:site])
|
site = usuarie.sites.find(params[:site])
|
||||||
hostname = site.hostname
|
hostname = site.hostname
|
||||||
|
deploys ||= {}
|
||||||
|
|
||||||
# Informamos a cada quien en su idioma y damos una dirección de
|
# Informamos a cada quien en su idioma y damos una dirección de
|
||||||
# respuesta porque a veces les usuaries nos escriben
|
# respuesta porque a veces les usuaries nos escriben
|
||||||
|
|
|
@ -30,6 +30,9 @@ class Deploy < ApplicationRecord
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Realizar tareas de limpieza.
|
||||||
|
def cleanup!; end
|
||||||
|
|
||||||
def time_start
|
def time_start
|
||||||
@start = Time.now
|
@start = Time.now
|
||||||
end
|
end
|
||||||
|
@ -46,6 +49,7 @@ class Deploy < ApplicationRecord
|
||||||
site.path
|
site.path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# XXX: Ver DeployLocal#bundle
|
||||||
def gems_dir
|
def gems_dir
|
||||||
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
|
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,6 +50,17 @@ class DeployLocal < Deploy
|
||||||
File.join(Rails.root, '_deploy', site.hostname)
|
File.join(Rails.root, '_deploy', site.hostname)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Libera espacio eliminando archivos temporales
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def cleanup!
|
||||||
|
FileUtils.rm_rf(gems_dir)
|
||||||
|
FileUtils.rm_rf(yarn_cache_dir)
|
||||||
|
FileUtils.rm_rf(File.join(site.path, 'node_modules'))
|
||||||
|
FileUtils.rm_rf(File.join(site.path, '.sass-cache'))
|
||||||
|
FileUtils.rm_rf(File.join(site.path, '.jekyll-cache'))
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def mkdir
|
def mkdir
|
||||||
|
@ -101,7 +112,7 @@ class DeployLocal < Deploy
|
||||||
File.exist? pnpm_lock
|
File.exist? pnpm_lock
|
||||||
end
|
end
|
||||||
|
|
||||||
def gem
|
def gem(output: false)
|
||||||
run %(gem install bundler --no-document), output: output
|
run %(gem install bundler --no-document), output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -119,8 +130,8 @@ class DeployLocal < Deploy
|
||||||
run 'pnpm install --production', output: output
|
run 'pnpm install --production', output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
def bundle
|
def bundle(output: false)
|
||||||
run %(bundle install --no-cache --path="#{gems_dir}"), output: output
|
run %(bundle install --no-cache --path="#{gems_dir}" --clean --without test development), output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
def jekyll_build(output: false)
|
def jekyll_build(output: false)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Reindexa los artículos al terminar la compilación
|
# Reindexa los artículos al terminar la compilación
|
||||||
class DeployReindex < Deploy
|
class DeployReindex < Deploy
|
||||||
def deploy
|
def deploy(**)
|
||||||
time_start
|
time_start
|
||||||
|
|
||||||
site.reset
|
site.reset
|
||||||
|
|
|
@ -72,6 +72,17 @@ class MetadataContent < MetadataTemplate
|
||||||
resource['controls'] = true
|
resource['controls'] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Elimina los estilos salvo los que asigne el editor
|
||||||
|
html.css('*').each do |element|
|
||||||
|
next if elements_with_style.include? element.name.downcase
|
||||||
|
|
||||||
|
element.remove_attribute('style')
|
||||||
|
end
|
||||||
|
|
||||||
html.to_s.html_safe
|
html.to_s.html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def elements_with_style
|
||||||
|
@elements_with_style ||= %w[div mark].freeze
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,6 @@ class MetadataFile < MetadataTemplate
|
||||||
|
|
||||||
errors << I18n.t("metadata.#{type}.site_invalid") if site.invalid?
|
errors << I18n.t("metadata.#{type}.site_invalid") if site.invalid?
|
||||||
errors << I18n.t("metadata.#{type}.path_required") if path_missing?
|
errors << I18n.t("metadata.#{type}.path_required") if path_missing?
|
||||||
errors << I18n.t("metadata.#{type}.no_file_for_description") if no_file_for_description?
|
|
||||||
errors << I18n.t("metadata.#{type}.attachment_missing") if path? && !static_file
|
errors << I18n.t("metadata.#{type}.attachment_missing") if path? && !static_file
|
||||||
|
|
||||||
errors.compact!
|
errors.compact!
|
||||||
|
@ -41,14 +40,14 @@ class MetadataFile < MetadataTemplate
|
||||||
end
|
end
|
||||||
|
|
||||||
# Asociar la imagen subida al sitio y obtener la ruta
|
# Asociar la imagen subida al sitio y obtener la ruta
|
||||||
#
|
# @return [Boolean]
|
||||||
# XXX: Si evitamos guardar cambios con changed? no tenemos forma de
|
|
||||||
# saber que un archivo subido manualmente se convirtió en
|
|
||||||
# un Attachment y cada vez que lo editemos vamos a subir una imagen
|
|
||||||
# repetida.
|
|
||||||
def save
|
def save
|
||||||
|
if value['path'].blank?
|
||||||
|
self[:value] = default_value
|
||||||
|
else
|
||||||
value['description'] = sanitize value['description']
|
value['description'] = sanitize value['description']
|
||||||
value['path'] = static_file ? relative_destination_path_with_filename.to_s : nil
|
value['path'] = relative_destination_path_with_filename.to_s if static_file
|
||||||
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -62,9 +61,6 @@ class MetadataFile < MetadataTemplate
|
||||||
# * El archivo es una ruta que apunta a un archivo asociado al sitio
|
# * El archivo es una ruta que apunta a un archivo asociado al sitio
|
||||||
# * El archivo es una ruta a un archivo dentro del repositorio
|
# * El archivo es una ruta a un archivo dentro del repositorio
|
||||||
#
|
#
|
||||||
# XXX: La última opción provoca archivos duplicados, pero es lo mejor
|
|
||||||
# que tenemos hasta que resolvamos https://0xacab.org/sutty/sutty/-/issues/213
|
|
||||||
#
|
|
||||||
# @todo encontrar una forma de obtener el attachment sin tener que
|
# @todo encontrar una forma de obtener el attachment sin tener que
|
||||||
# recurrir al último subido.
|
# recurrir al último subido.
|
||||||
#
|
#
|
||||||
|
@ -75,13 +71,7 @@ class MetadataFile < MetadataTemplate
|
||||||
when ActionDispatch::Http::UploadedFile
|
when ActionDispatch::Http::UploadedFile
|
||||||
site.static_files.last if site.static_files.attach(value['path'])
|
site.static_files.last if site.static_files.attach(value['path'])
|
||||||
when String
|
when String
|
||||||
if (blob_id = ActiveStorage::Blob.where(key: key_from_path).pluck(:id).first)
|
site.static_files.find_by(blob_id: blob_id) || migrate_static_file!
|
||||||
site.static_files.find_by(blob_id: blob_id)
|
|
||||||
elsif path? && pathname.exist? && site.static_files.attach(io: pathname.open, filename: pathname.basename)
|
|
||||||
site.static_files.last.tap do |s|
|
|
||||||
s.blob.update(key: key_from_path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -98,7 +88,7 @@ class MetadataFile < MetadataTemplate
|
||||||
#
|
#
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def key_from_path
|
def key_from_path
|
||||||
pathname.dirname.basename.to_s
|
@key_from_path ||= pathname.dirname.basename.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def path?
|
def path?
|
||||||
|
@ -127,13 +117,22 @@ class MetadataFile < MetadataTemplate
|
||||||
# devolvemos la ruta original, que puede ser el archivo que no existe
|
# devolvemos la ruta original, que puede ser el archivo que no existe
|
||||||
# o vacía si se está subiendo uno.
|
# o vacía si se está subiendo uno.
|
||||||
rescue Errno::ENOENT => e
|
rescue Errno::ENOENT => e
|
||||||
ExceptionNotifier.notify_exception(e)
|
ExceptionNotifier.notify_exception(e, data: { site: site.name, path: value['path'] })
|
||||||
|
|
||||||
value['path']
|
Pathname.new(File.join(site.path, value['path']))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Obtener la ruta relativa al sitio.
|
||||||
|
#
|
||||||
|
# Si algo falla, devolver la ruta original para no romper el archivo.
|
||||||
|
#
|
||||||
|
# @return [String, nil]
|
||||||
def relative_destination_path_with_filename
|
def relative_destination_path_with_filename
|
||||||
destination_path_with_filename.relative_path_from(Pathname.new(site.path).realpath)
|
destination_path_with_filename.relative_path_from(Pathname.new(site.path).realpath)
|
||||||
|
rescue ArgumentError => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name, path: value['path'] })
|
||||||
|
|
||||||
|
value['path']
|
||||||
end
|
end
|
||||||
|
|
||||||
def static_file_path
|
def static_file_path
|
||||||
|
@ -145,8 +144,31 @@ class MetadataFile < MetadataTemplate
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# No hay archivo pero se lo describió
|
# Obtiene el id del blob asociado
|
||||||
def no_file_for_description?
|
#
|
||||||
!path? && description?
|
# @return [Integer,nil]
|
||||||
|
def blob_id
|
||||||
|
@blob_id ||= ActiveStorage::Blob.where(key: key_from_path, service_name: site.name).pluck(:id).first
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera el blob para un archivo que ya se encuentra en el
|
||||||
|
# repositorio y lo agrega a la base de datos.
|
||||||
|
#
|
||||||
|
# @return [ActiveStorage::Attachment]
|
||||||
|
def migrate_static_file!
|
||||||
|
raise ArgumentError, 'El archivo no existe' unless path? && pathname.exist?
|
||||||
|
|
||||||
|
Site.transaction do
|
||||||
|
blob =
|
||||||
|
ActiveStorage::Blob.create_after_unfurling!(key: key_from_path,
|
||||||
|
io: pathname.open,
|
||||||
|
filename: pathname.basename,
|
||||||
|
service_name: site.name)
|
||||||
|
|
||||||
|
ActiveStorage::Attachment.create!(name: 'static_files', record: site, blob: blob)
|
||||||
|
end
|
||||||
|
rescue ArgumentError => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name, path: value['path'] })
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
3
app/models/metadata_non_geo.rb
Normal file
3
app/models/metadata_non_geo.rb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MetadataNonGeo < MetadataGeo; end
|
25
app/models/metadata_password.rb
Normal file
25
app/models/metadata_password.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Almacena una contraseña
|
||||||
|
class MetadataPassword < MetadataString
|
||||||
|
# Las contraseñas no son indexables
|
||||||
|
#
|
||||||
|
# @return [boolean]
|
||||||
|
def indexable?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
alias_method :original_sanitize, :sanitize
|
||||||
|
|
||||||
|
# Sanitizar la string y generar un hash Bcrypt
|
||||||
|
#
|
||||||
|
# @param :string [String]
|
||||||
|
# @return [String]
|
||||||
|
def sanitize(string)
|
||||||
|
string = original_sanitize string
|
||||||
|
|
||||||
|
::BCrypt::Password.create(string).to_s
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,12 +2,6 @@
|
||||||
|
|
||||||
# Este metadato permite generar rutas manuales.
|
# Este metadato permite generar rutas manuales.
|
||||||
class MetadataPermalink < MetadataString
|
class MetadataPermalink < MetadataString
|
||||||
# El valor por defecto una vez creado es la URL que le asigne Jekyll,
|
|
||||||
# de forma que nunca cambia aunque se cambie el título.
|
|
||||||
def default_value
|
|
||||||
document.url.sub(%r{\A/}, '') unless post.new?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Los permalinks nunca pueden ser privados
|
# Los permalinks nunca pueden ser privados
|
||||||
def private?
|
def private?
|
||||||
false
|
false
|
||||||
|
|
|
@ -25,7 +25,7 @@ require 'jekyll/utils'
|
||||||
class MetadataSlug < MetadataTemplate
|
class MetadataSlug < MetadataTemplate
|
||||||
# Trae el slug desde el título si existe o una string al azar
|
# Trae el slug desde el título si existe o una string al azar
|
||||||
def default_value
|
def default_value
|
||||||
title ? Jekyll::Utils.slugify(title) : SecureRandom.uuid
|
title ? Jekyll::Utils.slugify(title, mode: site.slugify_mode) : SecureRandom.uuid
|
||||||
end
|
end
|
||||||
|
|
||||||
def value
|
def value
|
||||||
|
@ -39,6 +39,6 @@ class MetadataSlug < MetadataTemplate
|
||||||
return if post.title&.private?
|
return if post.title&.private?
|
||||||
return if post.title&.value&.blank?
|
return if post.title&.value&.blank?
|
||||||
|
|
||||||
post.title&.value&.to_s
|
post.title&.value&.to_s&.unicode_normalize
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -134,7 +134,11 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
# En caso de que algún campo necesite realizar acciones antes de ser
|
# En caso de que algún campo necesite realizar acciones antes de ser
|
||||||
# guardado
|
# guardado
|
||||||
def save
|
def save
|
||||||
return true unless changed?
|
if !changed?
|
||||||
|
self[:value] = document_value if private?
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
self[:value] = sanitize value
|
self[:value] = sanitize value
|
||||||
self[:value] = encrypt(value) if private?
|
self[:value] = encrypt(value) if private?
|
||||||
|
|
|
@ -29,7 +29,7 @@ class Post
|
||||||
# TODO: Reemplazar cuando leamos el contenido del Document
|
# TODO: Reemplazar cuando leamos el contenido del Document
|
||||||
# a demanda?
|
# a demanda?
|
||||||
def find_layout(path)
|
def find_layout(path)
|
||||||
IO.foreach(path).lazy.grep(/^layout: /).take(1).first&.split(' ')&.last&.tr('\'', '')&.tr('"', '')&.to_sym
|
File.foreach(path).lazy.grep(/^layout: /).take(1).first&.split(' ')&.last&.tr('\'', '')&.tr('"', '')&.to_sym
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -90,16 +90,21 @@ class Post
|
||||||
'page' => document.to_liquid
|
'page' => document.to_liquid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# No tener errores de Liquid
|
||||||
|
site.jekyll.config['liquid']['strict_filters'] = false
|
||||||
|
site.jekyll.config['liquid']['strict_variables'] = false
|
||||||
|
|
||||||
# Renderizar lo estrictamente necesario y convertir a HTML para
|
# Renderizar lo estrictamente necesario y convertir a HTML para
|
||||||
# poder reemplazar valores.
|
# poder reemplazar valores.
|
||||||
html = Nokogiri::HTML document.renderer.render_document
|
html = Nokogiri::HTML document.renderer.render_document
|
||||||
# Las imágenes se cargan directamente desde el repositorio, porque
|
# Los archivos se cargan directamente desde el repositorio, porque
|
||||||
# no son públicas hasta que se publica el artículo.
|
# no son públicas hasta que se publica el artículo.
|
||||||
html.css('img').each do |img|
|
html.css('img,audio,video,iframe').each do |element|
|
||||||
next if %r{\Ahttps?://} =~ img.attributes['src']
|
src = element.attributes['src']
|
||||||
|
|
||||||
img.attributes['src'].value = Rails.application.routes.url_helpers.site_static_file_url(site,
|
next unless src&.value&.start_with? 'public/'
|
||||||
file: img.attributes['src'].value)
|
|
||||||
|
src.value = Rails.application.routes.url_helpers.site_static_file_url(site, file: src.value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Notificar a les usuaries que están viendo una previsualización
|
# Notificar a les usuaries que están viendo una previsualización
|
||||||
|
@ -108,12 +113,16 @@ class Post
|
||||||
|
|
||||||
# Cacofonía
|
# Cacofonía
|
||||||
html.to_html.html_safe
|
html.to_html.html_safe
|
||||||
|
rescue Liquid::Error => e
|
||||||
|
ExceptionNotifier.notify(e, data: { site: site.name, post: post.id })
|
||||||
|
|
||||||
|
''
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Devuelve una llave para poder guardar el post en una cache
|
# Devuelve una llave para poder guardar el post en una cache
|
||||||
def cache_key
|
def cache_key
|
||||||
'posts/' + uuid.value
|
"posts/#{uuid.value}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_version
|
def cache_version
|
||||||
|
@ -123,7 +132,7 @@ class Post
|
||||||
# Agregar el timestamp para saber si cambió, siguiendo el módulo
|
# Agregar el timestamp para saber si cambió, siguiendo el módulo
|
||||||
# ActiveRecord::Integration
|
# ActiveRecord::Integration
|
||||||
def cache_key_with_version
|
def cache_key_with_version
|
||||||
cache_key + '-' + cache_version
|
"#{cache_key}-#{cache_version}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Convertir a UUID?
|
# TODO: Convertir a UUID?
|
||||||
|
|
|
@ -14,9 +14,8 @@ class Post
|
||||||
#
|
#
|
||||||
# @return [IndexedPost]
|
# @return [IndexedPost]
|
||||||
def to_index
|
def to_index
|
||||||
IndexedPost.find_or_create_by(id: uuid.value).tap do |indexed_post|
|
IndexedPost.find_or_initialize_by(post_id: uuid.value, site_id: site.id).tap do |indexed_post|
|
||||||
indexed_post.layout = layout.name
|
indexed_post.layout = layout.name
|
||||||
indexed_post.site_id = site.id
|
|
||||||
indexed_post.path = path.basename
|
indexed_post.path = path.basename
|
||||||
indexed_post.locale = locale.value
|
indexed_post.locale = locale.value
|
||||||
indexed_post.dictionary = IndexedPost.to_dictionary(locale: locale.value)
|
indexed_post.dictionary = IndexedPost.to_dictionary(locale: locale.value)
|
||||||
|
@ -28,8 +27,6 @@ class Post
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Indexa o reindexa el Post
|
# Indexa o reindexa el Post
|
||||||
#
|
#
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
|
@ -41,6 +38,8 @@ class Post
|
||||||
to_index.destroy.destroyed?
|
to_index.destroy.destroyed?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
# Los metadatos que se almacenan como objetos JSON. Empezamos con
|
# Los metadatos que se almacenan como objetos JSON. Empezamos con
|
||||||
# las categorías porque se usan para filtrar en el listado de
|
# las categorías porque se usan para filtrar en el listado de
|
||||||
# artículos.
|
# artículos.
|
||||||
|
|
|
@ -180,10 +180,20 @@ class Site < ApplicationRecord
|
||||||
# Siempre tiene que tener algo porque las traducciones están
|
# Siempre tiene que tener algo porque las traducciones están
|
||||||
# incorporadas a los sitios de Sutty, aunque les usuaries no traduzcan
|
# incorporadas a los sitios de Sutty, aunque les usuaries no traduzcan
|
||||||
# sus sitios.
|
# sus sitios.
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
def locales
|
def locales
|
||||||
@locales ||= config.fetch('locales', I18n.available_locales).map(&:to_sym)
|
@locales ||= config.fetch('locales', I18n.available_locales).map(&:to_sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Modificar los locales disponibles
|
||||||
|
#
|
||||||
|
# @param :new_locales [Array]
|
||||||
|
# @return [Array]
|
||||||
|
def locales=(new_locales)
|
||||||
|
@locales = new_locales.map(&:to_sym).uniq
|
||||||
|
end
|
||||||
|
|
||||||
# Similar a site.i18n en jekyll-locales
|
# Similar a site.i18n en jekyll-locales
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
|
@ -251,6 +261,8 @@ class Site < ApplicationRecord
|
||||||
layout = layouts[Post.find_layout(doc.path)]
|
layout = layouts[Post.find_layout(doc.path)]
|
||||||
|
|
||||||
@posts[lang].build(document: doc, layout: layout, lang: lang)
|
@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
|
end
|
||||||
|
|
||||||
@posts[lang]
|
@posts[lang]
|
||||||
|
@ -426,7 +438,7 @@ class Site < ApplicationRecord
|
||||||
|
|
||||||
# El directorio donde se almacenan los sitios
|
# El directorio donde se almacenan los sitios
|
||||||
def self.site_path
|
def self.site_path
|
||||||
@site_path ||= ENV.fetch('SITE_PATH', Rails.root.join('_sites'))
|
@site_path ||= File.realpath(ENV.fetch('SITE_PATH', Rails.root.join('_sites')))
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.default
|
def self.default
|
||||||
|
@ -485,6 +497,7 @@ class Site < ApplicationRecord
|
||||||
config.title = title
|
config.title = title
|
||||||
config.url = url(slash: false)
|
config.url = url(slash: false)
|
||||||
config.hostname = hostname
|
config.hostname = hostname
|
||||||
|
config.locales = locales.map(&:to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
||||||
|
|
|
@ -33,11 +33,11 @@ class Site
|
||||||
def write
|
def write
|
||||||
return if persisted?
|
return if persisted?
|
||||||
|
|
||||||
@saved = Site::Writer.new(site: site, file: path,
|
@saved = Site::Writer.new(site: site, file: path, content: content.to_yaml).save.tap do |result|
|
||||||
content: content.to_yaml).save
|
|
||||||
# Actualizar el hash para no escribir dos veces
|
# Actualizar el hash para no escribir dos veces
|
||||||
@hash = content.hash
|
@hash = content.hash
|
||||||
end
|
end
|
||||||
|
end
|
||||||
alias save write
|
alias save write
|
||||||
|
|
||||||
# Detecta si la configuración cambió comparando con el valor inicial
|
# Detecta si la configuración cambió comparando con el valor inicial
|
||||||
|
|
|
@ -14,9 +14,7 @@ class Site
|
||||||
|
|
||||||
def index_posts!
|
def index_posts!
|
||||||
Site.transaction do
|
Site.transaction do
|
||||||
docs.each do |post|
|
docs.each(&:index!)
|
||||||
post.to_index.save
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -147,6 +147,23 @@ class Site
|
||||||
rugged.index.remove(relativize(file))
|
rugged.index.remove(relativize(file))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Garbage collection
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def gc
|
||||||
|
env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path }
|
||||||
|
cmd = 'git gc'
|
||||||
|
|
||||||
|
r = nil
|
||||||
|
Dir.chdir(path) do
|
||||||
|
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, _, t|
|
||||||
|
r = t.value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
r&.success?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las
|
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las
|
||||||
|
|
|
@ -9,6 +9,8 @@ class Usuarie < ApplicationRecord
|
||||||
validates_uniqueness_of :email
|
validates_uniqueness_of :email
|
||||||
validates_with EmailAddress::ActiveRecordValidator, field: :email
|
validates_with EmailAddress::ActiveRecordValidator, field: :email
|
||||||
|
|
||||||
|
before_create :lang_from_locale!
|
||||||
|
|
||||||
has_many :roles
|
has_many :roles
|
||||||
has_many :sites, through: :roles
|
has_many :sites, through: :roles
|
||||||
has_many :blazer_audits, foreign_key: 'user_id', class_name: 'Blazer::Audit'
|
has_many :blazer_audits, foreign_key: 'user_id', class_name: 'Blazer::Audit'
|
||||||
|
@ -38,4 +40,10 @@ class Usuarie < ApplicationRecord
|
||||||
increment_failed_attempts
|
increment_failed_attempts
|
||||||
lock_access! if attempts_exceeded? && !access_locked?
|
lock_access! if attempts_exceeded? && !access_locked?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def lang_from_locale!
|
||||||
|
self.lang = I18n.locale.to_s
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
47
app/services/cleanup_service.rb
Normal file
47
app/services/cleanup_service.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Realiza tareas de limpieza en todos los sitios, para optimizar y
|
||||||
|
# liberar espacio.
|
||||||
|
class CleanupService
|
||||||
|
# Días de antigüedad de los sitios
|
||||||
|
attr_reader :before
|
||||||
|
|
||||||
|
# @param :before [ActiveSupport::TimeWithZone] Cuánto tiempo lleva sin usarse un sitio.
|
||||||
|
def initialize(before: 30.days.ago)
|
||||||
|
@before = before
|
||||||
|
end
|
||||||
|
|
||||||
|
# Limpieza general
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def cleanup_everything!
|
||||||
|
cleanup_older_sites!
|
||||||
|
cleanup_newer_sites!
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra todos los sitios sin actualizar y realiza limpieza.
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def cleanup_older_sites!
|
||||||
|
Site.where('updated_at < ?', before).find_each do |site|
|
||||||
|
next unless File.directory? site.path
|
||||||
|
|
||||||
|
site.deploys.find_each(&:cleanup!)
|
||||||
|
|
||||||
|
site.repository.gc
|
||||||
|
site.touch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tareas para los sitios en uso
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def cleanup_newer_sites!
|
||||||
|
Site.where('updated_at >= ?', before).find_each do |site|
|
||||||
|
next unless File.directory? site.path
|
||||||
|
|
||||||
|
site.repository.gc
|
||||||
|
site.touch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,6 +3,11 @@
|
||||||
# Se encargar de guardar cambios en sitios
|
# Se encargar de guardar cambios en sitios
|
||||||
# TODO: Implementar rollback en la configuración
|
# TODO: Implementar rollback en la configuración
|
||||||
SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
|
def deploy
|
||||||
|
site.enqueue!
|
||||||
|
DeployJob.perform_async site.id
|
||||||
|
end
|
||||||
|
|
||||||
# Crea un sitio, agrega un rol nuevo y guarda los cambios a la
|
# Crea un sitio, agrega un rol nuevo y guarda los cambios a la
|
||||||
# configuración en el repositorio git
|
# configuración en el repositorio git
|
||||||
def create
|
def create
|
||||||
|
@ -11,7 +16,14 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
add_role temporal: false, rol: 'usuarie'
|
add_role temporal: false, rol: 'usuarie'
|
||||||
sync_nodes
|
sync_nodes
|
||||||
|
|
||||||
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
|
I18n.with_locale(usuarie.lang.to_sym || I18n.default_locale) do
|
||||||
|
# No se puede llamar a site.config antes de save porque el sitio
|
||||||
|
# todavía no existe.
|
||||||
|
#
|
||||||
|
# TODO: hacer que el repositorio se cree cuando es necesario, para
|
||||||
|
# que no haya estados intermedios.
|
||||||
|
site.locales = [usuarie.lang] + I18n.available_locales
|
||||||
|
|
||||||
site.save &&
|
site.save &&
|
||||||
site.config.write &&
|
site.config.write &&
|
||||||
commit_config(action: :create)
|
commit_config(action: :create)
|
||||||
|
@ -19,6 +31,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
|
|
||||||
add_licencias
|
add_licencias
|
||||||
|
|
||||||
|
deploy
|
||||||
|
|
||||||
site
|
site
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
%p= t('.greeting', recipient: @email)
|
%p= t('.greeting', recipient: @email)
|
||||||
%p= t('.instruction')
|
%p= t('.instruction')
|
||||||
%p= link_to t('.action'), confirmation_url(@resource, confirmation_token: @token)
|
%p= link_to t('.action'), confirmation_url(@resource, confirmation_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
\
|
\
|
||||||
= t('.instruction')
|
= t('.instruction')
|
||||||
\
|
\
|
||||||
= confirmation_url(@resource, confirmation_token: @token)
|
= confirmation_url(@resource, confirmation_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
%p= site.description
|
%p= site.description
|
||||||
|
|
||||||
%p= link_to t('devise.mailer.invitation_instructions.accept'),
|
%p= link_to t('devise.mailer.invitation_instructions.accept'),
|
||||||
accept_invitation_url(@resource, invitation_token: @token)
|
accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
||||||
- if @resource.invitation_due_at
|
- if @resource.invitation_due_at
|
||||||
%p= t('devise.mailer.invitation_instructions.accept_until',
|
%p= t('devise.mailer.invitation_instructions.accept_until',
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
\
|
\
|
||||||
= site.description
|
= site.description
|
||||||
\
|
\
|
||||||
= accept_invitation_url(@resource, invitation_token: @token)
|
= accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang)
|
||||||
\
|
\
|
||||||
- if @resource.invitation_due_at
|
- if @resource.invitation_due_at
|
||||||
= t('devise.mailer.invitation_instructions.accept_until',
|
= t('devise.mailer.invitation_instructions.accept_until',
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
%p= t('.greeting', recipient: @resource.email)
|
%p= t('.greeting', recipient: @resource.email)
|
||||||
%p= t('.instruction')
|
%p= t('.instruction')
|
||||||
%p= link_to t('.action'), edit_password_url(@resource, reset_password_token: @token)
|
%p= link_to t('.action'), edit_password_url(@resource, reset_password_token: @token, change_locale_to: @resource.lang)
|
||||||
%p= t('.instruction_2')
|
%p= t('.instruction_2')
|
||||||
%p= t('.instruction_3')
|
%p= t('.instruction_3')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
\
|
\
|
||||||
= t('.instruction')
|
= t('.instruction')
|
||||||
\
|
\
|
||||||
= edit_password_url(@resource, reset_password_token: @token)
|
= edit_password_url(@resource, reset_password_token: @token, change_locale_to: @resource.lang)
|
||||||
\
|
\
|
||||||
= t('.instruction_2')
|
= t('.instruction_2')
|
||||||
\
|
\
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
%p= t('.greeting', recipient: @resource.email)
|
%p= t('.greeting', recipient: @resource.email)
|
||||||
%p= t('.message')
|
%p= t('.message')
|
||||||
%p= t('.instruction')
|
%p= t('.instruction')
|
||||||
%p= link_to t('.action'), unlock_url(@resource, unlock_token: @token)
|
%p= link_to t('.action'), unlock_url(@resource, unlock_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
\
|
\
|
||||||
= t('.instruction')
|
= t('.instruction')
|
||||||
\
|
\
|
||||||
= unlock_url(@resource, unlock_token: @token)
|
= unlock_url(@resource, unlock_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
= form_for(resource,
|
= form_for(resource,
|
||||||
as: resource_name,
|
as: resource_name,
|
||||||
url: registration_path(resource_name)) do |f|
|
url: registration_path(resource_name, params: { locale: params[:locale] })) do |f|
|
||||||
|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,38 @@
|
||||||
%hr/
|
%hr/
|
||||||
|
|
||||||
|
- locale = params.permit(:locale)
|
||||||
|
|
||||||
- if controller_name != 'sessions'
|
- if controller_name != 'sessions'
|
||||||
= link_to t('.sign_in'), new_session_path(resource_name)
|
= link_to t('.sign_in'), new_session_path(resource_name, params: locale),
|
||||||
|
class: 'btn btn-lg btn-block btn-success'
|
||||||
%br/
|
%br/
|
||||||
|
|
||||||
- if devise_mapping.registerable? && controller_name != 'registrations'
|
- if devise_mapping.registerable? && controller_name != 'registrations'
|
||||||
= link_to t('.sign_up'), new_registration_path(resource_name),
|
= link_to t('.sign_up'), new_registration_path(resource_name, params: locale),
|
||||||
class: 'btn btn-lg btn-block btn-success'
|
class: 'btn btn-lg btn-block btn-success'
|
||||||
%br/
|
%br/
|
||||||
|
|
||||||
- if devise_mapping.recoverable?
|
- if devise_mapping.recoverable?
|
||||||
- unless %w[passwords registrations].include?(controller_name)
|
- unless %w[passwords registrations].include?(controller_name)
|
||||||
= link_to t('.forgot_your_password'),
|
= link_to t('.forgot_your_password'),
|
||||||
new_password_path(resource_name)
|
new_password_path(resource_name, params: locale)
|
||||||
%br/
|
%br/
|
||||||
|
|
||||||
- if devise_mapping.confirmable? && controller_name != 'confirmations'
|
- if devise_mapping.confirmable? && controller_name != 'confirmations'
|
||||||
= link_to t('.didn_t_receive_confirmation_instructions'),
|
= link_to t('.didn_t_receive_confirmation_instructions'),
|
||||||
new_confirmation_path(resource_name)
|
new_confirmation_path(resource_name, params: locale)
|
||||||
%br/
|
%br/
|
||||||
|
|
||||||
- if devise_mapping.lockable?
|
- if devise_mapping.lockable?
|
||||||
- if resource_class.unlock_strategy_enabled?(:email)
|
- if resource_class.unlock_strategy_enabled?(:email)
|
||||||
- if controller_name != 'unlocks'
|
- if controller_name != 'unlocks'
|
||||||
= link_to t('.didn_t_receive_unlock_instructions'),
|
= link_to t('.didn_t_receive_unlock_instructions'),
|
||||||
new_unlock_path(resource_name)
|
new_unlock_path(resource_name, params: locale)
|
||||||
%br/
|
%br/
|
||||||
|
|
||||||
- if devise_mapping.omniauthable?
|
- if devise_mapping.omniauthable?
|
||||||
- resource_class.omniauth_providers.each do |provider|
|
- resource_class.omniauth_providers.each do |provider|
|
||||||
= link_to t('.sign_in_with_provider',
|
= link_to t('.sign_in_with_provider',
|
||||||
provider: OmniAuth::Utils.camelize(provider)),
|
provider: OmniAuth::Utils.camelize(provider)),
|
||||||
omniauth_authorize_path(resource_name, provider)
|
omniauth_authorize_path(resource_name, provider, params: locale)
|
||||||
%br/
|
%br/
|
||||||
|
|
|
@ -22,3 +22,7 @@
|
||||||
%li.nav-item
|
%li.nav-item
|
||||||
= link_to t('.logout'), main_app.destroy_usuarie_session_path,
|
= link_to t('.logout'), main_app.destroy_usuarie_session_path,
|
||||||
method: :delete, role: 'button', class: 'btn'
|
method: :delete, role: 'button', class: 'btn'
|
||||||
|
- else
|
||||||
|
- I18n.available_locales.each do |locale|
|
||||||
|
- next if locale == I18n.locale
|
||||||
|
= link_to t(locale), "?change_locale_to=#{locale}"
|
||||||
|
|
6
app/views/posts/attribute_ro/_non_geo.haml
Normal file
6
app/views/posts/attribute_ro/_non_geo.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
- lat = metadata.value['lat']
|
||||||
|
- lng = metadata.value['lng']
|
||||||
|
%tr{ id: attribute }
|
||||||
|
%th= post_label_t(attribute, post: post)
|
||||||
|
%td
|
||||||
|
= "#{lat},#{lng}"
|
6
app/views/posts/attribute_ro/_password.haml
Normal file
6
app/views/posts/attribute_ro/_password.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
%tr{ id: attribute }
|
||||||
|
%th= post_label_t(attribute, post: post)
|
||||||
|
%td{ dir: dir, lang: locale }
|
||||||
|
= metadata.value
|
||||||
|
%br/
|
||||||
|
%small= t('.safety')
|
|
@ -1,4 +1,8 @@
|
||||||
.row{ data: { controller: 'geo' } }
|
.row{ data: { controller: 'geo' } }
|
||||||
|
.col-12.mb-3
|
||||||
|
%p.mb-0= post_label_t(attribute, post: post)
|
||||||
|
= render 'posts/attribute_feedback',
|
||||||
|
post: post, attribute: attribute, metadata: metadata
|
||||||
.col
|
.col
|
||||||
.form-group
|
.form-group
|
||||||
= label_tag "#{base}_#{attribute}_lat",
|
= label_tag "#{base}_#{attribute}_lat",
|
||||||
|
|
29
app/views/posts/attributes/_non_geo.haml
Normal file
29
app/views/posts/attributes/_non_geo.haml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
.row{ data: { controller: 'non-geo', site: site.url } }
|
||||||
|
.d-none{ hidden: true, data: { target: 'non-geo.overlay' }}
|
||||||
|
.col-12.mb-3
|
||||||
|
%p.mb-0= post_label_t(attribute, post: post)
|
||||||
|
%p= post_label_t(attribute, post: post)
|
||||||
|
= render 'posts/attribute_feedback',
|
||||||
|
post: post, attribute: attribute, metadata: metadata
|
||||||
|
.col
|
||||||
|
.form-group
|
||||||
|
= label_tag "#{base}_#{attribute}_lat",
|
||||||
|
post_label_t(attribute, :lat, post: post)
|
||||||
|
= text_field(*field_name_for(base, attribute, :lat),
|
||||||
|
value: metadata.value['lat'],
|
||||||
|
**field_options(attribute, metadata),
|
||||||
|
data: { target: 'non-geo.lat' })
|
||||||
|
= render 'posts/attribute_feedback',
|
||||||
|
post: post, attribute: [attribute, :lat], metadata: metadata
|
||||||
|
.col
|
||||||
|
.form-group
|
||||||
|
= label_tag "#{base}_#{attribute}_lng",
|
||||||
|
post_label_t(attribute, :lng, post: post)
|
||||||
|
= text_field(*field_name_for(base, attribute, :lng),
|
||||||
|
value: metadata.value['lng'],
|
||||||
|
**field_options(attribute, metadata),
|
||||||
|
data: { target: 'non-geo.lng' })
|
||||||
|
= render 'posts/attribute_feedback',
|
||||||
|
post: post, attribute: [attribute, :lng], metadata: metadata
|
||||||
|
.col-12.mb-3
|
||||||
|
%div{ data: { target: 'non-geo.map' }, style: 'height: 250px' }
|
7
app/views/posts/attributes/_password.haml
Normal file
7
app/views/posts/attributes/_password.haml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.form-group
|
||||||
|
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
|
||||||
|
= password_field base, attribute, value: metadata.value,
|
||||||
|
dir: dir, lang: locale,
|
||||||
|
**field_options(attribute, metadata)
|
||||||
|
= render 'posts/attribute_feedback',
|
||||||
|
post: post, attribute: attribute, metadata: metadata
|
|
@ -89,22 +89,22 @@
|
||||||
|
|
||||||
%div
|
%div
|
||||||
%tbody
|
%tbody
|
||||||
- dir = t("locales.#{@locale}.dir")
|
- dir = @site.data.dig(params[:locale], 'dir')
|
||||||
- size = @posts.size
|
- size = @posts.size
|
||||||
- @posts.each_with_index do |post, i|
|
- @posts.each_with_index do |post, i|
|
||||||
-#
|
-#
|
||||||
TODO: Solo les usuaries cachean porque tenemos que separar
|
TODO: Solo les usuaries cachean porque tenemos que separar
|
||||||
les botones por permisos.
|
les botones por permisos.
|
||||||
- cache_if @usuarie, [post, I18n.locale] do
|
- cache_if @usuarie, [post, I18n.locale] do
|
||||||
- checkbox_id = "checkbox-#{post.id}"
|
- checkbox_id = "checkbox-#{post.post_id}"
|
||||||
%tr{ id: post.id, data: { target: 'reorder.row' } }
|
%tr{ id: post.post_id, data: { target: 'reorder.row' } }
|
||||||
%td
|
%td
|
||||||
.custom-control.custom-checkbox
|
.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 }
|
%label.custom-control-label{ for: checkbox_id }
|
||||||
%span.sr-only= t('posts.reorder.select')
|
%span.sr-only= t('posts.reorder.select')
|
||||||
-# Orden más alto es mayor prioridad
|
-# Orden más alto es mayor prioridad
|
||||||
= hidden_field 'post[reorder]', post.id,
|
= hidden_field 'post[reorder]', post.post_id,
|
||||||
value: size - i,
|
value: size - i,
|
||||||
data: { reorder: true }
|
data: { reorder: true }
|
||||||
%td.w-100{ class: dir }
|
%td.w-100{ class: dir }
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- dir = t("locales.#{@locale}.dir")
|
- dir = @site.data.dig(params[:locale], 'dir')
|
||||||
.row.justify-content-center
|
.row.justify-content-center
|
||||||
.col-md-8
|
.col-md-8
|
||||||
%article.content.table-responsive-md
|
%article.content.table-responsive-md
|
||||||
|
@ -6,13 +6,6 @@
|
||||||
edit_site_post_path(@site, @post.id),
|
edit_site_post_path(@site, @post.id),
|
||||||
class: 'btn btn-block'
|
class: 'btn btn-block'
|
||||||
|
|
||||||
- unless @post.layout.ignored?
|
|
||||||
= link_to t('posts.preview.btn'),
|
|
||||||
site_post_preview_path(@site, @post.id),
|
|
||||||
class: 'btn btn-block',
|
|
||||||
target: '_blank',
|
|
||||||
rel: 'noopener'
|
|
||||||
|
|
||||||
%table.table.table-condensed
|
%table.table.table-condensed
|
||||||
%thead
|
%thead
|
||||||
%tr
|
%tr
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
|
|
||||||
%hr/
|
%hr/
|
||||||
|
|
||||||
|
- if site.persisted?
|
||||||
.form-group#tienda
|
.form-group#tienda
|
||||||
%h2= t('.tienda.title')
|
%h2= t('.tienda.title')
|
||||||
%p.lead
|
%p.lead
|
||||||
|
@ -124,7 +125,6 @@
|
||||||
|
|
||||||
%hr/
|
%hr/
|
||||||
|
|
||||||
- if site.persisted?
|
|
||||||
.form-group#contact
|
.form-group#contact
|
||||||
%h2= t('.contact.title')
|
%h2= t('.contact.title')
|
||||||
%p.lead= t('.contact.help')
|
%p.lead= t('.contact.help')
|
||||||
|
|
|
@ -13,6 +13,15 @@ en:
|
||||||
ar:
|
ar:
|
||||||
name: Arabic
|
name: Arabic
|
||||||
dir: rtl
|
dir: rtl
|
||||||
|
zh:
|
||||||
|
name: Chinese
|
||||||
|
dir: ltr
|
||||||
|
de:
|
||||||
|
name: German
|
||||||
|
dir: ltr
|
||||||
|
fr:
|
||||||
|
name: French
|
||||||
|
dir: ltr
|
||||||
login:
|
login:
|
||||||
email: E-mail address
|
email: E-mail address
|
||||||
password: Password
|
password: Password
|
||||||
|
@ -106,7 +115,7 @@ en:
|
||||||
success: Success!
|
success: Success!
|
||||||
error: Error
|
error: Error
|
||||||
deploy_localized_domain:
|
deploy_localized_domain:
|
||||||
title: Localized domain
|
title: Domain name by language
|
||||||
success: Success!
|
success: Success!
|
||||||
error: Error
|
error: Error
|
||||||
deploy_rsync:
|
deploy_rsync:
|
||||||
|
@ -465,6 +474,8 @@ en:
|
||||||
attribute_ro:
|
attribute_ro:
|
||||||
file:
|
file:
|
||||||
download: Download file
|
download: Download file
|
||||||
|
password:
|
||||||
|
safety: Passwords are stored safely
|
||||||
show:
|
show:
|
||||||
front_matter: Post metadata
|
front_matter: Post metadata
|
||||||
submit:
|
submit:
|
||||||
|
@ -526,7 +537,7 @@ en:
|
||||||
preview:
|
preview:
|
||||||
btn: 'Preliminary version'
|
btn: 'Preliminary version'
|
||||||
alert: 'Not every article type has a preliminary version'
|
alert: 'Not every article type has a preliminary version'
|
||||||
message: 'This is a preliminary version, use the Publish changes button back on the panel to publish the article onto your site.'
|
message: 'This is a preview of your post with some contextual elements from your site.'
|
||||||
open: 'Tip: You can add new options by typing them and pressing Enter'
|
open: 'Tip: You can add new options by typing them and pressing Enter'
|
||||||
private: '🔒 The values of this field will remain private'
|
private: '🔒 The values of this field will remain private'
|
||||||
select:
|
select:
|
||||||
|
|
|
@ -13,6 +13,15 @@ es:
|
||||||
ar:
|
ar:
|
||||||
name: Árabe
|
name: Árabe
|
||||||
dir: rtl
|
dir: rtl
|
||||||
|
zh:
|
||||||
|
name: Chino
|
||||||
|
dir: ltr
|
||||||
|
de:
|
||||||
|
name: Alemán
|
||||||
|
dir: ltr
|
||||||
|
fr:
|
||||||
|
name: Francés
|
||||||
|
dir: ltr
|
||||||
login:
|
login:
|
||||||
email: Correo electrónico
|
email: Correo electrónico
|
||||||
password: Contraseña
|
password: Contraseña
|
||||||
|
@ -106,7 +115,7 @@ es:
|
||||||
success: ¡Éxito!
|
success: ¡Éxito!
|
||||||
error: Hubo un error
|
error: Hubo un error
|
||||||
deploy_localized_domain:
|
deploy_localized_domain:
|
||||||
title: Dominio por idioma
|
title: Dominio según idioma
|
||||||
success: ¡Éxito!
|
success: ¡Éxito!
|
||||||
error: Hubo un error
|
error: Hubo un error
|
||||||
deploy_rsync:
|
deploy_rsync:
|
||||||
|
@ -473,6 +482,8 @@ es:
|
||||||
attribute_ro:
|
attribute_ro:
|
||||||
file:
|
file:
|
||||||
download: Descargar archivo
|
download: Descargar archivo
|
||||||
|
password:
|
||||||
|
safety: Las contraseñas se almacenan de forma segura
|
||||||
show:
|
show:
|
||||||
front_matter: Metadatos del artículo
|
front_matter: Metadatos del artículo
|
||||||
submit:
|
submit:
|
||||||
|
@ -534,7 +545,7 @@ es:
|
||||||
preview:
|
preview:
|
||||||
btn: 'Versión preliminar'
|
btn: 'Versión preliminar'
|
||||||
alert: 'No todos los tipos de artículos poseen vista preliminar :)'
|
alert: 'No todos los tipos de artículos poseen vista preliminar :)'
|
||||||
message: 'Esta es una versión preliminar, para que el artículo aparezca en tu sitio utiliza el botón Publicar cambios en el panel'
|
message: 'Esta es la vista previa de tu artículo, con algunos elementos contextuales del sitio'
|
||||||
open: 'Nota: Puedes agregar más opciones a medida que las escribes y presionas Entrar'
|
open: 'Nota: Puedes agregar más opciones a medida que las escribes y presionas Entrar'
|
||||||
private: '🔒 Los valores de este campo serán privados'
|
private: '🔒 Los valores de este campo serán privados'
|
||||||
select:
|
select:
|
||||||
|
|
|
@ -55,7 +55,7 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
# Gestionar artículos según idioma
|
# Gestionar artículos según idioma
|
||||||
nested do
|
nested do
|
||||||
scope '/(:locale)', constraint: /[a-z]{2}/ do
|
scope '/(:locale)', constraint: /[a-z]{2}(-[A-Z]{2})?/ do
|
||||||
post :'posts/reorder', to: 'posts#reorder'
|
post :'posts/reorder', to: 'posts#reorder'
|
||||||
resources :posts do
|
resources :posts do
|
||||||
get 'p/:page', action: :index, on: :collection
|
get 'p/:page', action: :index, on: :collection
|
||||||
|
|
8
db/migrate/20220428135113_add_slugify_mode_to_sites.rb
Normal file
8
db/migrate/20220428135113_add_slugify_mode_to_sites.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Permite a los sitios elegir el método de slugificación
|
||||||
|
class AddSlugifyModeToSites < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :sites, :slugify_mode, :string, default: 'default'
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Cambia el índice único para incluir el nombre del servicio, de forma
|
||||||
|
# que podamos tener varias copias del mismo sitio (por ejemplo para
|
||||||
|
# test) sin que falle la creación de archivos.
|
||||||
|
class ChangeBlobKeyUniquenessToIncludeServiceName < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
remove_index :active_storage_blobs, %i[key], unique: true
|
||||||
|
add_index :active_storage_blobs, %i[key service_name], unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# No podemos compartir el uuid entre indexed_posts y posts porque
|
||||||
|
# podemos tener sitios duplicados. Al menos hasta que los sitios de
|
||||||
|
# testeo estén integrados en el panel vamos a tener que generar otros
|
||||||
|
# UUID.
|
||||||
|
class IndexedPostsByUuidAndSiteId < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
add_column :indexed_posts, :post_id, :uuid, index: true
|
||||||
|
|
||||||
|
IndexedPost.transaction do
|
||||||
|
ActiveRecord::Base.connection.execute('update indexed_posts set post_id = id where post_id is null')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :indexed_posts, :post_id
|
||||||
|
end
|
||||||
|
end
|
11
lib/tasks/cleanup.rake
Normal file
11
lib/tasks/cleanup.rake
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
namespace :cleanup do
|
||||||
|
desc 'Cleanup sites'
|
||||||
|
task everything: :environment do
|
||||||
|
before = ENV.fetch('BEFORE', '30').to_i.days.ago
|
||||||
|
service = CleanupService.new(before: before)
|
||||||
|
|
||||||
|
service.cleanup_everything!
|
||||||
|
end
|
||||||
|
end
|
30
monit.conf
30
monit.conf
|
@ -1,29 +1,7 @@
|
||||||
check process sutty with pidfile /srv/tmp/puma.pid
|
# Limpiar mensualmente
|
||||||
start program = "/usr/local/bin/sutty start"
|
check program cleanup
|
||||||
stop program = "/usr/local/bin/sutty stop"
|
with path "/usr/bin/foreman run -f /srv/Procfile -d /srv cleanup" as uid "rails" gid "www-data"
|
||||||
|
every "0 3 1 * *"
|
||||||
check process prometheus with pidfile /tmp/prometheus.pid
|
|
||||||
start program = "/usr/local/bin/sutty prometheus start"
|
|
||||||
stop program = "/usr/local/bin/sutty prometheus start"
|
|
||||||
|
|
||||||
check program blazer_5m
|
|
||||||
with path "/usr/local/bin/sutty blazer 5m"
|
|
||||||
every 5 cycles
|
|
||||||
if status != 0 then alert
|
|
||||||
|
|
||||||
check program blazer_1h
|
|
||||||
with path "/usr/local/bin/sutty blazer 1h"
|
|
||||||
every 60 cycles
|
|
||||||
if status != 0 then alert
|
|
||||||
|
|
||||||
check program blazer_1d
|
|
||||||
with path "/usr/local/bin/sutty blazer 1d"
|
|
||||||
every 1440 cycles
|
|
||||||
if status != 0 then alert
|
|
||||||
|
|
||||||
check program blazer
|
|
||||||
with path "/usr/local/bin/sutty blazer"
|
|
||||||
every 61 cycles
|
|
||||||
if status != 0 then alert
|
if status != 0 then alert
|
||||||
|
|
||||||
check program access_logs
|
check program access_logs
|
||||||
|
|
Loading…
Reference in a new issue