mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-16 12:31:42 +00:00
Merge branch 'rails' of 0xacab.org:sutty/sutty into issue-10491
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
commit
e4fdd611ee
102 changed files with 1440 additions and 282 deletions
|
@ -2,3 +2,4 @@
|
|||
*
|
||||
# Solo agregar lo que usamos en COPY
|
||||
# !./archivo
|
||||
!./monit.conf
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# pwgen -1 32
|
||||
RAILS_MASTER_KEY=11111111111111111111111111111111
|
||||
RAILS_GROUPS=assets
|
||||
DELEGATE=athshe.sutty.nl
|
||||
HAINISH=../haini.sh/haini.sh
|
||||
DATABASE=
|
||||
DATABASE_URL=postgres://suttier@postgresql.sutty.local/sutty
|
||||
RAILS_ENV=development
|
||||
IMAP_SERVER=
|
||||
DEFAULT_FROM=
|
||||
|
|
71
.woodpecker.yml
Normal file
71
.woodpecker.yml
Normal file
|
@ -0,0 +1,71 @@
|
|||
pipeline:
|
||||
publish:
|
||||
image: "docker.io/woodpeckerci/plugin-docker-buildx"
|
||||
settings:
|
||||
registry: "gitea.nulo.in"
|
||||
username: "sutty"
|
||||
repo: "gitea.nulo.in/sutty/panel"
|
||||
tags:
|
||||
- "${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}"
|
||||
- "latest"
|
||||
build_args:
|
||||
- "RUBY_VERSION=${RUBY_VERSION}"
|
||||
- "RUBY_PATCH=${RUBY_PATCH}"
|
||||
- "ALPINE_VERSION=${ALPINE_VERSION}"
|
||||
- "BASE_IMAGE=gitea.nulo.in/sutty/rails"
|
||||
purge: false
|
||||
secrets:
|
||||
- "DOCKER_PASSWORD"
|
||||
when:
|
||||
branch:
|
||||
- "rails"
|
||||
- "panel.sutty.nl"
|
||||
event: "push"
|
||||
path:
|
||||
include:
|
||||
- "Dockerfile"
|
||||
- ".dockerignore"
|
||||
assets:
|
||||
image: "gitea.nulo.in/sutty/panel:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}"
|
||||
commands:
|
||||
- "apk add python2 dotenv openssh-client brotli"
|
||||
- "install -d -m 700 ~/.ssh/"
|
||||
- "echo \"$${KNOWN_HOSTS}\" | base64 -d >> ~/.ssh/known_hosts"
|
||||
- "chmod 600 ~/.ssh/known_hosts"
|
||||
- "eval $(ssh-agent -s)"
|
||||
- "echo \"$${SSH_KEY}\" | base64 -d | ssh-add -"
|
||||
- "ssh $${ORIGIN%:*}"
|
||||
- "git config user.name Woodpecker"
|
||||
- "git config user.email ci@sutty.coop.ar"
|
||||
- "git remote add upstream $${ORIGIN}"
|
||||
- "git checkout -B ${CI_COMMIT_BRANCH}"
|
||||
- "mv config/credentials.yml.enc.ci config/credentials.yml.enc"
|
||||
- "yarn"
|
||||
- "cp .env.example .env"
|
||||
- "dotenv bundle install --path=vendor"
|
||||
- "dotenv RAILS_ENV=production bundle exec rails webpacker:clobber"
|
||||
- "dotenv RAILS_ENV=production bundle exec rails assets:precompile"
|
||||
- "dotenv RAILS_ENV=production bundle exec rails assets:clean"
|
||||
- "find public -type f -print0 | xargs -r0 brotli -k9f"
|
||||
- "git add public && git commit -m \"ci: assets [skip ci]\""
|
||||
- "git pull upstream ${CI_COMMIT_BRANCH}"
|
||||
- "git push upstream ${CI_COMMIT_BRANCH}"
|
||||
secrets:
|
||||
- "SSH_KEY"
|
||||
- "KNOWN_HOSTS"
|
||||
- "ORIGIN"
|
||||
when:
|
||||
branch:
|
||||
- "rails"
|
||||
- "panel.sutty.nl"
|
||||
path:
|
||||
include:
|
||||
- "app/assets/**/*"
|
||||
- "app/javascript/**/*"
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
matrix:
|
||||
include:
|
||||
- ALPINE_VERSION: "3.14.10"
|
||||
RUBY_VERSION: "2.7"
|
||||
RUBY_PATCH: "8"
|
13
Dockerfile
13
Dockerfile
|
@ -1,5 +1,9 @@
|
|||
FROM registry.nulo.in/sutty/rails:3.13.6-2.7.5
|
||||
ARG PANDOC_VERSION=2.17.1.1
|
||||
ARG RUBY_VERSION=2.7
|
||||
ARG RUBY_PATCH=6
|
||||
ARG ALPINE_VERSION=3.13.10
|
||||
ARG BASE_IMAGE=registry.nulo.in/sutty/rails
|
||||
FROM ${BASE_IMAGE}:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}
|
||||
ARG PANDOC_VERSION=2.18
|
||||
ENV RAILS_ENV production
|
||||
|
||||
# Instalar las dependencias, separamos la librería de base de datos para
|
||||
|
@ -10,10 +14,13 @@ ENV RAILS_ENV production
|
|||
# principal
|
||||
RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \
|
||||
rsync git jpegoptim vips tectonic oxipng git-lfs openssh-client \
|
||||
yarn daemonize ruby-webrick
|
||||
yarn daemonize ruby-webrick postgresql-client dateutils file
|
||||
|
||||
RUN gem install --no-document --no-user-install foreman
|
||||
RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc
|
||||
RUN apk add npm && npm install -g pnpm@~7 && apk del npm
|
||||
|
||||
COPY ./monit.conf /etc/monit.d/sutty.conf
|
||||
|
||||
VOLUME "/srv"
|
||||
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -38,6 +38,8 @@ gem 'commonmarker'
|
|||
gem 'devise'
|
||||
gem 'devise-i18n'
|
||||
gem 'devise_invitable'
|
||||
gem 'distributed-press-api-client', '~> 0.2.2'
|
||||
gem 'njalla-api-client'
|
||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||
gem 'exception_notification'
|
||||
gem 'fast_blank'
|
||||
|
@ -89,7 +91,7 @@ gem 'stackprof'
|
|||
gem 'prometheus_exporter'
|
||||
|
||||
# debug
|
||||
gem 'fast_jsonparser'
|
||||
gem 'fast_jsonparser', '~> 0.5.0'
|
||||
gem 'down'
|
||||
gem 'sourcemap'
|
||||
gem 'rack-cors'
|
||||
|
|
46
Gemfile.lock
46
Gemfile.lock
|
@ -115,6 +115,7 @@ GEM
|
|||
xpath (>= 2.0, < 4.0)
|
||||
chartkick (4.1.2)
|
||||
childprocess (4.1.0)
|
||||
climate_control (1.2.0)
|
||||
coderay (1.1.3)
|
||||
colorator (1.1.0)
|
||||
commonmarker (0.21.2-x86_64-linux-musl)
|
||||
|
@ -153,12 +154,45 @@ GEM
|
|||
devise_invitable (2.0.5)
|
||||
actionmailer (>= 5.0)
|
||||
devise (>= 4.6)
|
||||
distributed-press-api-client (0.2.2)
|
||||
addressable (~> 2.3, >= 2.3.0)
|
||||
climate_control
|
||||
dry-schema
|
||||
httparty (~> 0.18)
|
||||
json (~> 2.1, >= 2.1.0)
|
||||
jwt (~> 2.6.0)
|
||||
dotenv (2.7.6)
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
down (5.2.4)
|
||||
addressable (~> 2.8)
|
||||
dry-configurable (1.0.1)
|
||||
dry-core (~> 1.0, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-core (1.0.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-inflector (1.0.0)
|
||||
dry-initializer (3.1.1)
|
||||
dry-logic (1.5.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-schema (1.13.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-configurable (~> 1.0, >= 1.0.1)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-initializer (~> 3.0)
|
||||
dry-logic (>= 1.5, < 2)
|
||||
dry-types (>= 1.7, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-types (1.7.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-inflector (~> 1.0, < 2)
|
||||
dry-logic (>= 1.4, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
ed25519 (1.2.4-x86_64-linux-musl)
|
||||
em-websocket (0.5.3)
|
||||
eventmachine (>= 0.12.9)
|
||||
|
@ -216,8 +250,8 @@ GEM
|
|||
thor
|
||||
hiredis (0.6.3-x86_64-linux-musl)
|
||||
http_parser.rb (0.8.0-x86_64-linux-musl)
|
||||
httparty (0.18.1)
|
||||
mime-types (~> 3.0)
|
||||
httparty (0.21.0)
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.8.11)
|
||||
concurrent-ruby (~> 1.0)
|
||||
|
@ -261,7 +295,7 @@ GEM
|
|||
jekyll (~> 4)
|
||||
jekyll-ignore-layouts (0.1.2)
|
||||
jekyll (~> 4)
|
||||
jekyll-images (0.3.0)
|
||||
jekyll-images (0.3.2)
|
||||
jekyll (~> 4)
|
||||
ruby-filemagic (~> 0.7)
|
||||
ruby-vips (~> 2)
|
||||
|
@ -292,6 +326,7 @@ GEM
|
|||
jekyll-write-and-commit-changes (0.2.1)
|
||||
jekyll (~> 4)
|
||||
rugged (~> 1)
|
||||
jwt (2.6.0)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
|
@ -352,6 +387,9 @@ GEM
|
|||
nokogiri (1.12.5-x86_64-linux-musl)
|
||||
mini_portile2 (~> 2.6.1)
|
||||
racc (~> 1.4)
|
||||
njalla-api-client (0.1.0)
|
||||
dry-schema
|
||||
httparty (~> 0.18)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.21.0)
|
||||
parser (3.0.2.0)
|
||||
|
@ -578,6 +616,7 @@ DEPENDENCIES
|
|||
devise
|
||||
devise-i18n
|
||||
devise_invitable
|
||||
distributed-press-api-client (~> 0.2.2)
|
||||
dotenv-rails
|
||||
down
|
||||
ed25519
|
||||
|
@ -612,6 +651,7 @@ DEPENDENCIES
|
|||
mini_magick
|
||||
mobility
|
||||
net-ssh
|
||||
njalla-api-client
|
||||
nokogiri
|
||||
pg
|
||||
pg_search
|
||||
|
|
2
Procfile
2
Procfile
|
@ -5,4 +5,6 @@ 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
|
||||
stats: bundle exec rake stats:process_all
|
||||
|
|
|
@ -25,6 +25,10 @@ $spacers: (
|
|||
2-plus: 0.75rem
|
||||
);
|
||||
|
||||
$sizes: (
|
||||
"70ch": 70ch,
|
||||
);
|
||||
|
||||
@import "bootstrap";
|
||||
@import "editor";
|
||||
|
||||
|
@ -410,6 +414,8 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
|
|||
@each $prop, $abbrev in (width: w, height: h) {
|
||||
@each $size, $length in $sizes {
|
||||
.#{$abbrev}-#{$grid-breakpoint}-#{$size} { #{$prop}: $length !important; }
|
||||
.min-#{$abbrev}-#{$grid-breakpoint}-#{$size} { min-#{$prop}: $length !important; }
|
||||
.max-#{$abbrev}-#{$grid-breakpoint}-#{$size} { max-#{$prop}: $length !important; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ module Api
|
|||
params: airbrake_params.to_h
|
||||
end
|
||||
|
||||
render status: 201, json: { id: 1, url: root_url }
|
||||
render status: 201, json: { id: 1, url: '' }
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -9,7 +9,7 @@ module Api
|
|||
|
||||
# Lista de nombres de dominios a emitir certificados
|
||||
def index
|
||||
render json: sites_names + alternative_names + api_names
|
||||
render json: sites_names + alternative_names + api_names + www_names
|
||||
end
|
||||
|
||||
# Sitios con hidden service de Tor
|
||||
|
@ -28,7 +28,7 @@ module Api
|
|||
site = Site.find_by(name: params[:name])
|
||||
|
||||
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,
|
||||
params: params
|
||||
service.add_onion
|
||||
|
@ -39,14 +39,22 @@ module Api
|
|||
|
||||
private
|
||||
|
||||
def canonicalize(name)
|
||||
name.end_with?('.') ? name[0..-2] : "#{name}.#{Site.domain}"
|
||||
end
|
||||
|
||||
# Nombres de los sitios
|
||||
def sites_names
|
||||
Site.all.order(:name).pluck(:name)
|
||||
Site.all.order(:name).pluck(:name).map do |name|
|
||||
canonicalize name
|
||||
end
|
||||
end
|
||||
|
||||
# Dominios alternativos
|
||||
def alternative_names
|
||||
DeployAlternativeDomain.all.map(&:hostname)
|
||||
(DeployAlternativeDomain.all.map(&:hostname) + DeployLocalizedDomain.all.map(&:hostname)).map do |name|
|
||||
canonicalize name
|
||||
end
|
||||
end
|
||||
|
||||
# Obtener todos los sitios con API habilitada, es decir formulario
|
||||
|
@ -56,7 +64,16 @@ module Api
|
|||
def api_names
|
||||
Site.where(contact: 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
|
||||
|
|
|
@ -46,17 +46,19 @@ class ApplicationController < ActionController::Base
|
|||
# defecto.
|
||||
#
|
||||
# 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
|
||||
|
||||
# El idioma es el preferido por le usuarie, pero no necesariamente se
|
||||
# corresponde con el idioma de los artículos, porque puede querer
|
||||
# traducirlos.
|
||||
def set_locale(&action)
|
||||
I18n.with_locale(current_locale(include_params: false), &action)
|
||||
I18n.with_locale(current_locale, &action)
|
||||
end
|
||||
|
||||
# Muestra una página 404
|
||||
|
@ -88,4 +90,12 @@ class ApplicationController < ActionController::Base
|
|||
def prepare_exception_notifier
|
||||
request.env['exception_notifier.exception_data'] = { usuarie: current_usuarie }
|
||||
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
|
||||
|
|
|
@ -12,7 +12,7 @@ class PostsController < ApplicationController
|
|||
|
||||
# Las URLs siempre llevan el idioma actual o el de le usuarie
|
||||
def default_url_options
|
||||
{ locale: current_locale }
|
||||
{ locale: locale }
|
||||
end
|
||||
|
||||
def index
|
||||
|
|
|
@ -68,9 +68,7 @@ class SitesController < ApplicationController
|
|||
def enqueue
|
||||
authorize site
|
||||
|
||||
# XXX: Convertir en una máquina de estados?
|
||||
site.enqueue!
|
||||
DeployJob.perform_async site.id
|
||||
SiteService.new(site: site).deploy
|
||||
|
||||
redirect_to site_posts_path(site, locale: site.default_locale)
|
||||
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
|
||||
}
|
||||
}
|
|
@ -3,9 +3,14 @@
|
|||
# Realiza el deploy de un sitio
|
||||
class DeployJob < ApplicationJob
|
||||
class DeployException < StandardError; end
|
||||
class DeployTimedOutException < DeployException; end
|
||||
|
||||
discard_on ActiveRecord::RecordNotFound
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def perform(site, notify = true, time = Time.now)
|
||||
def perform(site, notify: true, time: Time.now, output: false)
|
||||
@output = output
|
||||
|
||||
ActiveRecord::Base.connection_pool.with_connection do
|
||||
@site = Site.find(site)
|
||||
|
||||
|
@ -16,32 +21,39 @@ class DeployJob < ApplicationJob
|
|||
# hora original para poder ir haciendo timeouts.
|
||||
if @site.building?
|
||||
if 10.minutes.ago >= time
|
||||
@site.update status: 'waiting'
|
||||
raise DeployException,
|
||||
notify = false
|
||||
raise DeployTimedOutException,
|
||||
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
|
||||
end
|
||||
|
||||
DeployJob.perform_in(60, site, notify, time)
|
||||
DeployJob.perform_in(60, site, notify: notify, time: time, output: output)
|
||||
return
|
||||
end
|
||||
|
||||
@site.update status: 'building'
|
||||
# Asegurarse que DeployLocal sea el primero!
|
||||
@deployed = { deploy_local: deploy_locally }
|
||||
@deployed = {
|
||||
deploy_local: {
|
||||
status: deploy_locally,
|
||||
seconds: deploy_local.build_stats.last.seconds,
|
||||
size: deploy_local.size,
|
||||
urls: [deploy_local.url]
|
||||
}
|
||||
}
|
||||
|
||||
# No es opcional
|
||||
unless @deployed[:deploy_local]
|
||||
@site.update status: 'waiting'
|
||||
notify_usuaries if notify
|
||||
|
||||
unless @deployed[:deploy_local][:status]
|
||||
# Hacer fallar la tarea
|
||||
raise DeployException, deploy_local.build_stats.last.log
|
||||
raise DeployException, "#{@site.name}: Falló la compilación"
|
||||
end
|
||||
|
||||
deploy_others
|
||||
|
||||
# Volver a la espera
|
||||
@site.update status: 'waiting'
|
||||
rescue DeployTimedOutException => e
|
||||
notify_exception e
|
||||
rescue DeployException => e
|
||||
notify_exception e, deploy_local
|
||||
ensure
|
||||
@site&.update status: 'waiting'
|
||||
|
||||
notify_usuaries if notify
|
||||
end
|
||||
|
@ -50,17 +62,44 @@ class DeployJob < ApplicationJob
|
|||
|
||||
private
|
||||
|
||||
# @param :exception [StandardError]
|
||||
# @param :deploy [Deploy]
|
||||
def notify_exception(exception, deploy = nil)
|
||||
data = {
|
||||
site: @site.id,
|
||||
deploy: deploy&.type,
|
||||
log: deploy&.build_stats&.last&.log
|
||||
}
|
||||
|
||||
ExceptionNotifier.notify_exception(exception, data: data)
|
||||
end
|
||||
|
||||
def deploy_local
|
||||
@deploy_local ||= @site.deploys.find_by(type: 'DeployLocal')
|
||||
end
|
||||
|
||||
def deploy_locally
|
||||
deploy_local.deploy
|
||||
deploy_local.deploy(output: @output)
|
||||
end
|
||||
|
||||
def deploy_others
|
||||
@site.deploys.where.not(type: 'DeployLocal').find_each do |d|
|
||||
@deployed[d.type.underscore.to_sym] = d.deploy
|
||||
begin
|
||||
status = d.deploy(output: @output)
|
||||
seconds = d.build_stats.last.try(:seconds)
|
||||
rescue StandardError => e
|
||||
status = false
|
||||
seconds = 0
|
||||
|
||||
notify_exception e, d
|
||||
end
|
||||
|
||||
@deployed[d.type.underscore.to_sym] = {
|
||||
status: status,
|
||||
seconds: seconds || 0,
|
||||
size: d.size,
|
||||
urls: d.respond_to?(:urls) ? d.urls : [d.url].compact
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
# Notifica excepciones a una instancia de Gitlab, como incidencias
|
||||
# nuevas o como comentarios a las incidencias pre-existentes.
|
||||
class GitlabNotifierJob < ApplicationJob
|
||||
class GitlabNotifierError < StandardError; end
|
||||
|
||||
include ExceptionNotifier::BacktraceCleaner
|
||||
|
||||
# Variables que vamos a acceder luego
|
||||
|
@ -18,22 +20,28 @@ class GitlabNotifierJob < ApplicationJob
|
|||
@issue_data = { count: 1 }
|
||||
# Necesitamos saber si el issue ya existía
|
||||
@cached = false
|
||||
@issue = {}
|
||||
|
||||
# Traemos los datos desde la caché si existen, sino generamos un
|
||||
# issue nuevo e inicializamos la caché
|
||||
@issue_data = Rails.cache.fetch(cache_key) do
|
||||
issue = client.new_issue confidential: true, title: title, description: description, issue_type: 'incident'
|
||||
@issue = client.new_issue confidential: true, title: title, description: description, issue_type: 'incident'
|
||||
@cached = true
|
||||
|
||||
{
|
||||
count: 1,
|
||||
issue: issue['iid'],
|
||||
issue: @issue['iid'],
|
||||
user_agents: [user_agent].compact,
|
||||
params: [request&.filtered_parameters].compact,
|
||||
urls: [url].compact
|
||||
}
|
||||
end
|
||||
|
||||
unless @issue['iid']
|
||||
Rails.cache.delete(cache_key)
|
||||
raise GitlabNotifierError, @issue.dig('message', 'title')&.join(', ')
|
||||
end
|
||||
|
||||
# No seguimos actualizando si acabamos de generar el issue
|
||||
return if cached
|
||||
|
||||
|
@ -104,6 +112,7 @@ class GitlabNotifierJob < ApplicationJob
|
|||
# @return [String]
|
||||
def description
|
||||
@description ||= ''.dup.tap do |d|
|
||||
d << log_section
|
||||
d << request_section
|
||||
d << javascript_section
|
||||
d << javascript_footer
|
||||
|
@ -151,6 +160,19 @@ class GitlabNotifierJob < ApplicationJob
|
|||
@client ||= GitlabApiClient.new
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
def log_section
|
||||
return '' unless options[:log]
|
||||
|
||||
<<~LOG
|
||||
# Log
|
||||
|
||||
```
|
||||
#{options[:log]}
|
||||
```
|
||||
LOG
|
||||
end
|
||||
|
||||
# Muestra información de la petición
|
||||
#
|
||||
# @return [String]
|
||||
|
|
17
app/jobs/renew_distributed_press_tokens_job.rb
Normal file
17
app/jobs/renew_distributed_press_tokens_job.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Renueva los tokens de Distributed Press antes que se venzan,
|
||||
# activando los callbacks que hacen que se refresque el token.
|
||||
class RenewDistributedPressTokensJob < ApplicationJob
|
||||
# Renueva todos los tokens a punto de vencer o informa el error sin
|
||||
# detener la tarea si algo pasa.
|
||||
def perform
|
||||
DistributedPressPublisher.with_about_to_expire_tokens.find_each do |publisher|
|
||||
publisher.touch
|
||||
rescue DistributedPress::V1::Error => e
|
||||
data = { instance: publisher.instance, expires_at: publisher.client.token.expires_at }
|
||||
|
||||
ExceptionNotifier.notify_exception(e, data: data)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,6 +20,18 @@ module ActiveStorage
|
|||
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
|
||||
# firma. Esto permite que luego podamos guardar el archivo donde
|
||||
# corresponde.
|
||||
|
@ -67,7 +79,9 @@ module ActiveStorage
|
|||
# @param :key [String]
|
||||
# @return [String]
|
||||
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
|
||||
|
||||
# Crea una ruta para la llave con un nombre conocido.
|
||||
|
|
|
@ -8,21 +8,66 @@
|
|||
# TODO: Agregar firma GPG y header Autocrypt
|
||||
# TODO: Cifrar con GPG si le usuarie nos dio su llave
|
||||
class DeployMailer < ApplicationMailer
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include ActionView::Helpers::DateHelper
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def deployed(which_ones)
|
||||
@usuarie = Usuarie.find(params[:usuarie])
|
||||
@site = @usuarie.sites.find(params[:site])
|
||||
@deploys = which_ones
|
||||
@deploy_local = @site.deploys.find_by(type: 'DeployLocal')
|
||||
def deployed(deploys = {})
|
||||
usuarie = Usuarie.find(params[:usuarie])
|
||||
site = usuarie.sites.find(params[:site])
|
||||
hostname = site.hostname
|
||||
deploys ||= {}
|
||||
|
||||
# Informamos a cada quien en su idioma y damos una dirección de
|
||||
# respuesta porque a veces les usuaries nos escriben
|
||||
I18n.with_locale(@usuarie.lang) do
|
||||
mail(to: @usuarie.email,
|
||||
reply_to: "sutty@#{Site.domain}",
|
||||
subject: I18n.t('deploy_mailer.deployed.subject',
|
||||
site: @site.name))
|
||||
I18n.with_locale(usuarie.lang) do
|
||||
subject = t('.subject', site: site.name)
|
||||
|
||||
@hi = t('.hi')
|
||||
@explanation = t('.explanation', fqdn: hostname)
|
||||
@help = t('.help')
|
||||
|
||||
@headers = %w[type status url seconds size].map do |header|
|
||||
t(".th.#{header}")
|
||||
end
|
||||
|
||||
@table = deploys.each_pair.map do |deploy, value|
|
||||
{
|
||||
title: t(".#{deploy}.title"),
|
||||
status: t(".#{deploy}.#{value[:status] ? 'success' : 'error'}"),
|
||||
urls: value[:urls],
|
||||
seconds: {
|
||||
human: distance_of_time_in_words(value[:seconds].seconds),
|
||||
machine: "PT#{value[:seconds]}S"
|
||||
},
|
||||
size: number_to_human_size(value[:size], precision: 2)
|
||||
}
|
||||
end
|
||||
|
||||
@terminal_table = Terminal::Table.new do |t|
|
||||
t << @headers
|
||||
t.add_separator
|
||||
@table.each do |row|
|
||||
row[:urls].each do |url|
|
||||
t << (row.map do |k, v|
|
||||
case k
|
||||
when :seconds then v[:human]
|
||||
when :urls then url
|
||||
else v
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
mail(to: usuarie.email, reply_to: "sutty@#{Site.domain}", subject: subject)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
private
|
||||
|
||||
def t(key, **args)
|
||||
I18n.t("deploy_mailer.deployed#{key}", **args)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,11 @@ class Deploy < ApplicationRecord
|
|||
belongs_to :site
|
||||
has_many :build_stats, dependent: :destroy
|
||||
|
||||
def deploy
|
||||
def deploy(**)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def url
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
|
@ -23,6 +27,9 @@ class Deploy < ApplicationRecord
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Realizar tareas de limpieza.
|
||||
def cleanup!; end
|
||||
|
||||
def time_start
|
||||
@start = Time.now
|
||||
end
|
||||
|
@ -39,6 +46,7 @@ class Deploy < ApplicationRecord
|
|||
site.path
|
||||
end
|
||||
|
||||
# XXX: Ver DeployLocal#bundle
|
||||
def gems_dir
|
||||
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
|
||||
end
|
||||
|
@ -48,20 +56,26 @@ class Deploy < ApplicationRecord
|
|||
#
|
||||
# @param [String]
|
||||
# @return [Boolean]
|
||||
def run(cmd)
|
||||
def run(cmd, output: false)
|
||||
r = nil
|
||||
lines = []
|
||||
|
||||
time_start
|
||||
Dir.chdir(site.path) do
|
||||
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, o, t|
|
||||
r = t.value
|
||||
# XXX: Tenemos que leer línea por línea porque en salidas largas
|
||||
# se cuelga la IO
|
||||
# TODO: Enviar a un websocket para ver el proceso en vivo?
|
||||
o.each do |line|
|
||||
lines << line
|
||||
Thread.new do
|
||||
o.each do |line|
|
||||
lines << line
|
||||
|
||||
puts line if output
|
||||
end
|
||||
rescue IOError => e
|
||||
lines << e.message
|
||||
puts e.message if output
|
||||
end
|
||||
|
||||
r = t.value
|
||||
end
|
||||
end
|
||||
time_stop
|
||||
|
@ -75,6 +89,13 @@ class Deploy < ApplicationRecord
|
|||
r&.success?
|
||||
end
|
||||
|
||||
# Variables de entorno
|
||||
#
|
||||
# @return [Hash]
|
||||
def local_env
|
||||
@local_env ||= {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param [String]
|
||||
|
@ -82,4 +103,12 @@ class Deploy < ApplicationRecord
|
|||
def readable_cmd(cmd)
|
||||
cmd.split(' -', 2).first.tr(' ', '_')
|
||||
end
|
||||
|
||||
def deploy_local
|
||||
@deploy_local ||= site.deploys.find_by(type: 'DeployLocal')
|
||||
end
|
||||
|
||||
def non_local_deploys
|
||||
@non_local_deploys ||= site.deploys.where.not(type: 'DeployLocal')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ class DeployAlternativeDomain < Deploy
|
|||
store :values, accessors: %i[hostname], coder: JSON
|
||||
|
||||
# Generar un link simbólico del sitio principal al alternativo
|
||||
def deploy
|
||||
def deploy(**)
|
||||
File.symlink?(destination) ||
|
||||
File.symlink(site.hostname, destination).zero?
|
||||
end
|
||||
|
@ -18,6 +18,10 @@ class DeployAlternativeDomain < Deploy
|
|||
end
|
||||
|
||||
def destination
|
||||
File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, ''))
|
||||
@destination ||= File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, ''))
|
||||
end
|
||||
|
||||
def url
|
||||
"https://#{File.basename destination}"
|
||||
end
|
||||
end
|
||||
|
|
166
app/models/deploy_distributed_press.rb
Normal file
166
app/models/deploy_distributed_press.rb
Normal file
|
@ -0,0 +1,166 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'distributed_press/v1/client/site'
|
||||
require 'njalla/v1'
|
||||
|
||||
# Soportar Distributed Press APIv1
|
||||
#
|
||||
# Usa tokens de publicación efímeros para todas las acciones.
|
||||
#
|
||||
# Al ser creado, genera el sitio en la instancia de Distributed Press
|
||||
# configurada y almacena el ID.
|
||||
#
|
||||
# Al ser publicado, envía los archivos en un tarball y actualiza la
|
||||
# información.
|
||||
class DeployDistributedPress < Deploy
|
||||
store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON
|
||||
|
||||
before_create :create_remote_site!, :create_njalla_records!
|
||||
|
||||
# Actualiza la información y luego envía los cambios
|
||||
#
|
||||
# @param :output [Bool]
|
||||
# @return [Bool]
|
||||
def deploy
|
||||
status = false
|
||||
log = []
|
||||
|
||||
time_start
|
||||
|
||||
create_remote_site! if remote_site_id.blank?
|
||||
create_njalla_records! if remote_info['njalla'].blank?
|
||||
save
|
||||
|
||||
if remote_site_id.blank? || remote_info['njalla'].blank?
|
||||
raise DeployJob::DeployException, ''
|
||||
end
|
||||
|
||||
site_client.tap do |c|
|
||||
stdout = Thread.new(publisher.logger_out) do |io|
|
||||
until io.eof?
|
||||
line = io.gets
|
||||
|
||||
puts line if output
|
||||
log << line
|
||||
end
|
||||
end
|
||||
|
||||
update remote_info: c.show(publishing_site).to_h
|
||||
|
||||
status = c.publish(publishing_site, deploy_local.destination)
|
||||
|
||||
publisher.logger.close
|
||||
stdout.join
|
||||
end
|
||||
|
||||
time_stop
|
||||
|
||||
create_stat! status, log.join
|
||||
|
||||
status
|
||||
end
|
||||
|
||||
def limit; end
|
||||
|
||||
def size
|
||||
deploy_local.size
|
||||
end
|
||||
|
||||
def destination; end
|
||||
|
||||
# Devuelve las URLs de todos los protocolos
|
||||
def urls
|
||||
remote_info[:links].values.map do |protocol|
|
||||
[ protocol[:link], protocol[:gateway] ]
|
||||
end.flatten.compact.select do |link|
|
||||
link.include? '://'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# El cliente de la API
|
||||
#
|
||||
# TODO: cuando soportemos más, tiene que haber una relación entre
|
||||
# DeployDistributedPress y DistributedPressPublisher.
|
||||
#
|
||||
# @return [DistributedPressPublisher]
|
||||
def publisher
|
||||
@publisher ||= DistributedPressPublisher.first
|
||||
end
|
||||
|
||||
# El cliente para actualizar el sitio
|
||||
#
|
||||
# @return [DistributedPress::V1::Client::Site]
|
||||
def site_client
|
||||
DistributedPress::V1::Client::Site.new(publisher.client)
|
||||
end
|
||||
|
||||
# Genera el esquema de datos para poder publicar el sitio
|
||||
#
|
||||
# @return [DistributedPress::V1::Schemas::PublishingSite]
|
||||
def publishing_site
|
||||
DistributedPress::V1::Schemas::PublishingSite.new.call(id: remote_site_id)
|
||||
end
|
||||
|
||||
# Genera el esquema de datos para crear el sitio
|
||||
#
|
||||
# @return [DistributedPressPublisher::V1::Schemas::NewSite]
|
||||
def create_site
|
||||
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
|
||||
#
|
||||
# @return [nil]
|
||||
def create_remote_site!
|
||||
created_site = site_client.create(create_site)
|
||||
|
||||
self.remote_site_id = created_site[:id]
|
||||
self.remote_info = created_site.to_h
|
||||
rescue DistributedPress::V1::Error => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||
ensure
|
||||
nil
|
||||
end
|
||||
|
||||
# Crea los registros en Njalla
|
||||
#
|
||||
# @return [nil]
|
||||
def create_njalla_records!
|
||||
# XXX: Esto depende de nuestro DNS actual, cuando lo migremos hay
|
||||
# que eliminarlo.
|
||||
unless site.name.end_with? '.'
|
||||
self.remote_info['njalla'] = {}
|
||||
self.remote_info['njalla']['a'] = njalla.add_record(name: site.name, type: 'CNAME', content: "#{Site.domain}.").to_h
|
||||
self.remote_info['njalla']['ns'] = njalla.add_record(name: "_dnslink.#{site.name}", type: 'NS', content: "#{publisher.hostname}.").to_h
|
||||
end
|
||||
rescue HTTParty::Error => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||
self.remote_info['njalla'] = nil
|
||||
ensure
|
||||
nil
|
||||
end
|
||||
|
||||
# Registra lo que sucedió
|
||||
#
|
||||
# @param status [Bool]
|
||||
# @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
|
||||
nil
|
||||
end
|
||||
|
||||
# Actualizar registros en Njalla
|
||||
#
|
||||
# @return [Njalla::V1::Domain]
|
||||
def njalla
|
||||
@njalla ||=
|
||||
begin
|
||||
client = Njalla::V1::Client.new(token: ENV['NJALLA_TOKEN'])
|
||||
|
||||
Njalla::V1::Domain.new(domain: Site.domain, client: client)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Genera una versión onion
|
||||
class DeployHiddenService < DeployWww
|
||||
def deploy
|
||||
def deploy(**)
|
||||
return true if fqdn.blank?
|
||||
|
||||
super
|
||||
|
@ -13,6 +13,6 @@ class DeployHiddenService < DeployWww
|
|||
end
|
||||
|
||||
def url
|
||||
'http://' + fqdn
|
||||
"http://#{fqdn}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,12 +12,13 @@ class DeployLocal < Deploy
|
|||
#
|
||||
# Pasamos variables de entorno mínimas para no filtrar secretos de
|
||||
# Sutty
|
||||
def deploy
|
||||
def deploy(output: false)
|
||||
return false unless mkdir
|
||||
return false unless yarn
|
||||
return false unless bundle
|
||||
return false unless yarn(output: output)
|
||||
return false unless pnpm(output: output)
|
||||
return false unless bundle(output: output)
|
||||
|
||||
jekyll_build
|
||||
jekyll_build(output: output)
|
||||
end
|
||||
|
||||
# Sólo permitimos un deploy local
|
||||
|
@ -25,6 +26,10 @@ class DeployLocal < Deploy
|
|||
1
|
||||
end
|
||||
|
||||
def url
|
||||
site.url
|
||||
end
|
||||
|
||||
# Obtener el tamaño de todos los archivos y directorios (los
|
||||
# directorios son archivos :)
|
||||
def size
|
||||
|
@ -45,6 +50,17 @@ class DeployLocal < Deploy
|
|||
File.join(Rails.root, '_deploy', site.hostname)
|
||||
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
|
||||
|
||||
def mkdir
|
||||
|
@ -52,27 +68,34 @@ class DeployLocal < Deploy
|
|||
end
|
||||
|
||||
# Un entorno que solo tiene lo que necesitamos
|
||||
#
|
||||
# @return [Hash]
|
||||
def env
|
||||
# XXX: This doesn't support Windows paths :B
|
||||
paths = [File.dirname(`which bundle`), '/usr/bin', '/bin']
|
||||
paths = [File.dirname(`which bundle`), '/usr/local/bin', '/usr/bin', '/bin']
|
||||
|
||||
{
|
||||
'HOME' => home_dir,
|
||||
'PATH' => paths.join(':'),
|
||||
'SPREE_API_KEY' => site.tienda_api_key,
|
||||
'SPREE_URL' => site.tienda_url,
|
||||
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||
'JEKYLL_ENV' => Rails.env,
|
||||
'LANG' => ENV['LANG'],
|
||||
'YARN_CACHE_FOLDER' => yarn_cache_dir
|
||||
}
|
||||
# Las variables de entorno extra no pueden superponerse al local.
|
||||
extra_env.merge({
|
||||
'HOME' => home_dir,
|
||||
'PATH' => paths.join(':'),
|
||||
'SPREE_API_KEY' => site.tienda_api_key,
|
||||
'SPREE_URL' => site.tienda_url,
|
||||
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||
'JEKYLL_ENV' => Rails.env,
|
||||
'LANG' => ENV['LANG'],
|
||||
'YARN_CACHE_FOLDER' => yarn_cache_dir
|
||||
})
|
||||
end
|
||||
|
||||
def yarn_cache_dir
|
||||
Rails.root.join('_yarn_cache').to_s
|
||||
end
|
||||
|
||||
def pnpm_cache_dir
|
||||
Rails.root.join('_pnpm_cache').to_s
|
||||
end
|
||||
|
||||
def yarn_lock
|
||||
File.join(site.path, 'yarn.lock')
|
||||
end
|
||||
|
@ -81,23 +104,38 @@ class DeployLocal < Deploy
|
|||
File.exist? yarn_lock
|
||||
end
|
||||
|
||||
def gem
|
||||
run %(gem install bundler --no-document)
|
||||
def pnpm_lock
|
||||
File.join(site.path, 'pnpm-lock.yaml')
|
||||
end
|
||||
|
||||
def pnpm_lock?
|
||||
File.exist? pnpm_lock
|
||||
end
|
||||
|
||||
def gem(output: false)
|
||||
run %(gem install bundler --no-document), output: output
|
||||
end
|
||||
|
||||
# Corre yarn dentro del repositorio
|
||||
def yarn
|
||||
def yarn(output: false)
|
||||
return true unless yarn_lock?
|
||||
|
||||
run 'yarn install --production'
|
||||
run 'yarn install --production', output: output
|
||||
end
|
||||
|
||||
def bundle
|
||||
run %(bundle install --no-cache --path="#{gems_dir}")
|
||||
def pnpm(output: false)
|
||||
return true unless pnpm_lock?
|
||||
|
||||
run %(pnpm config set store-dir "#{pnpm_cache_dir}"), output: output
|
||||
run 'pnpm install --production', output: output
|
||||
end
|
||||
|
||||
def jekyll_build
|
||||
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}")
|
||||
def bundle(output: false)
|
||||
run %(bundle install --no-cache --path="#{gems_dir}" --clean --without test development), output: output
|
||||
end
|
||||
|
||||
def jekyll_build(output: false)
|
||||
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}"), output: output
|
||||
end
|
||||
|
||||
# no debería haber espacios ni caracteres especiales, pero por si
|
||||
|
@ -110,4 +148,17 @@ class DeployLocal < Deploy
|
|||
def remove_destination!
|
||||
FileUtils.rm_rf destination
|
||||
end
|
||||
|
||||
# Consigue todas las variables de entorno configuradas por otros
|
||||
# deploys.
|
||||
#
|
||||
# @return [Hash]
|
||||
def extra_env
|
||||
@extra_env ||=
|
||||
non_local_deploys.reduce({}) do |extra_env, deploy|
|
||||
extra_env.tap do |e|
|
||||
e.merge! deploy.local_env
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
12
app/models/deploy_localized_domain.rb
Normal file
12
app/models/deploy_localized_domain.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Soportar dominios localizados
|
||||
class DeployLocalizedDomain < DeployAlternativeDomain
|
||||
store :values, accessors: %i[hostname locale], coder: JSON
|
||||
|
||||
# Generar un link simbólico del sitio principal al alternativo
|
||||
def deploy(**)
|
||||
File.symlink?(destination) ||
|
||||
File.symlink(File.join(site.hostname, locale), destination).zero?
|
||||
end
|
||||
end
|
|
@ -7,8 +7,8 @@
|
|||
# jekyll-private-data
|
||||
class DeployPrivate < DeployLocal
|
||||
# No es necesario volver a instalar dependencias
|
||||
def deploy
|
||||
jekyll_build
|
||||
def deploy(output: false)
|
||||
jekyll_build(output: output)
|
||||
end
|
||||
|
||||
# Hacer el deploy a un directorio privado
|
||||
|
@ -16,6 +16,10 @@ class DeployPrivate < DeployLocal
|
|||
File.join(Rails.root, '_private', site.name)
|
||||
end
|
||||
|
||||
def url
|
||||
"#{ENV['PANEL_URL']}/sites/private/#{site.name}"
|
||||
end
|
||||
|
||||
# No usar recursos en compresión y habilitar los datos privados
|
||||
def env
|
||||
@env ||= super.merge({
|
||||
|
|
38
app/models/deploy_reindex.rb
Normal file
38
app/models/deploy_reindex.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
# 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
|
|
@ -5,8 +5,8 @@
|
|||
class DeployRsync < Deploy
|
||||
store :values, accessors: %i[destination host_keys], coder: JSON
|
||||
|
||||
def deploy
|
||||
ssh? && rsync
|
||||
def deploy(output: false)
|
||||
ssh? && rsync(output: output)
|
||||
end
|
||||
|
||||
# El espacio remoto es el mismo que el local
|
||||
|
@ -83,8 +83,8 @@ class DeployRsync < Deploy
|
|||
# Sincroniza hacia el directorio remoto
|
||||
#
|
||||
# @return [Boolean]
|
||||
def rsync
|
||||
run %(rsync -aviH --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/)
|
||||
def rsync(output: output)
|
||||
run %(rsync -aviH --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/), output: output
|
||||
end
|
||||
|
||||
# El origen es el destino de la compilación
|
||||
|
|
|
@ -6,7 +6,7 @@ class DeployWww < Deploy
|
|||
|
||||
before_destroy :remove_destination!
|
||||
|
||||
def deploy
|
||||
def deploy(**)
|
||||
File.symlink?(destination) ||
|
||||
File.symlink(site.hostname, destination).zero?
|
||||
end
|
||||
|
@ -27,6 +27,10 @@ class DeployWww < Deploy
|
|||
"www.#{site.hostname}"
|
||||
end
|
||||
|
||||
def url
|
||||
"https://www.#{site.hostname}/"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_destination!
|
||||
|
|
|
@ -12,7 +12,7 @@ class DeployZip < Deploy
|
|||
# y generar un zip accesible públicamente.
|
||||
#
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def deploy
|
||||
def deploy(**)
|
||||
FileUtils.rm_f path
|
||||
|
||||
time_start
|
||||
|
@ -49,6 +49,10 @@ class DeployZip < Deploy
|
|||
"#{site.hostname}.zip"
|
||||
end
|
||||
|
||||
def url
|
||||
"#{site.url}#{file}"
|
||||
end
|
||||
|
||||
def path
|
||||
File.join(destination, file)
|
||||
end
|
||||
|
|
84
app/models/distributed_press_publisher.rb
Normal file
84
app/models/distributed_press_publisher.rb
Normal file
|
@ -0,0 +1,84 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'distributed_press/v1'
|
||||
|
||||
# Almacena el token de autenticación y la URL, por ahora solo vamos
|
||||
# a tener uno, pero queda abierta la posibilidad de agregar más.
|
||||
class DistributedPressPublisher < ApplicationRecord
|
||||
# Cifrar la información del token en la base de datos
|
||||
has_encrypted :token
|
||||
|
||||
# La salida del log
|
||||
#
|
||||
# @return [IO]
|
||||
attr_reader :logger_out
|
||||
|
||||
# La instancia es única
|
||||
validates_uniqueness_of :instance
|
||||
|
||||
# El token es necesario
|
||||
validates_presence_of :token
|
||||
|
||||
# Mantener la fecha de vencimiento actualizada
|
||||
before_save :update_expires_at_from_token!, :update_token_from_client!
|
||||
|
||||
# Devuelve todos los tokens que vencen en una hora
|
||||
scope :with_about_to_expire_tokens, lambda {
|
||||
where('expires_at > ? and expires_at < ?', Time.now, Time.now + 1.hour)
|
||||
}
|
||||
|
||||
# Instancia un cliente de Distributed Press a partir del token. Al
|
||||
# cargar un token a punto de vencer se renueva automáticamente.
|
||||
#
|
||||
# @return [DistributedPress::V1::Client]
|
||||
def client
|
||||
@client ||= DistributedPress::V1::Client.new(url: instance, token: token, logger: logger)
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
def to_s
|
||||
"Distributed Press <#{instance}>"
|
||||
end
|
||||
|
||||
# Devuelve el hostname de la instancia
|
||||
#
|
||||
# @return [String]
|
||||
def hostname
|
||||
@hostname ||= URI.parse(instance).hostname
|
||||
end
|
||||
|
||||
# @return [Logger]
|
||||
def logger
|
||||
@logger ||=
|
||||
begin
|
||||
@logger_out, @logger_in = IO.pipe
|
||||
::Logger.new @logger_in, formatter: formatter
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def formatter
|
||||
@formatter ||= lambda do |_, _, _, msg|
|
||||
"#{msg}\n"
|
||||
end
|
||||
end
|
||||
|
||||
# Actualiza o desactiva la fecha de vencimiento a partir de la
|
||||
# información del token.
|
||||
#
|
||||
# @return [nil]
|
||||
def update_expires_at_from_token!
|
||||
self.expires_at = client.token.forever? ? nil : client.token.expires_at
|
||||
nil
|
||||
end
|
||||
|
||||
# Actualiza el token a partir del cliente, que ya actualiza el token
|
||||
# automáticamente.
|
||||
#
|
||||
# @return [nil]
|
||||
def update_token_from_client!
|
||||
self.token = client.token.to_s
|
||||
nil
|
||||
end
|
||||
end
|
|
@ -72,6 +72,17 @@ class MetadataContent < MetadataTemplate
|
|||
resource['controls'] = true
|
||||
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
|
||||
end
|
||||
|
||||
def elements_with_style
|
||||
@elements_with_style ||= %w[div mark].freeze
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,6 @@ class MetadataFile < MetadataTemplate
|
|||
|
||||
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}.no_file_for_description") if no_file_for_description?
|
||||
errors << I18n.t("metadata.#{type}.attachment_missing") if path? && !static_file
|
||||
|
||||
errors.compact!
|
||||
|
@ -41,14 +40,14 @@ class MetadataFile < MetadataTemplate
|
|||
end
|
||||
|
||||
# Asociar la imagen subida al sitio y obtener la ruta
|
||||
#
|
||||
# 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.
|
||||
# @return [Boolean]
|
||||
def save
|
||||
value['description'] = sanitize value['description']
|
||||
value['path'] = static_file ? relative_destination_path_with_filename.to_s : nil
|
||||
if value['path'].blank?
|
||||
self[:value] = default_value
|
||||
else
|
||||
value['description'] = sanitize value['description']
|
||||
value['path'] = relative_destination_path_with_filename.to_s if static_file
|
||||
end
|
||||
|
||||
true
|
||||
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 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
|
||||
# recurrir al último subido.
|
||||
#
|
||||
|
@ -75,13 +71,7 @@ class MetadataFile < MetadataTemplate
|
|||
when ActionDispatch::Http::UploadedFile
|
||||
site.static_files.last if site.static_files.attach(value['path'])
|
||||
when String
|
||||
if (blob_id = ActiveStorage::Blob.where(key: key_from_path).pluck(:id).first)
|
||||
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
|
||||
site.static_files.find_by(blob_id: blob_id) || migrate_static_file!
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -98,7 +88,7 @@ class MetadataFile < MetadataTemplate
|
|||
#
|
||||
# @return [String]
|
||||
def key_from_path
|
||||
pathname.dirname.basename.to_s
|
||||
@key_from_path ||= pathname.dirname.basename.to_s
|
||||
end
|
||||
|
||||
def path?
|
||||
|
@ -127,13 +117,22 @@ class MetadataFile < MetadataTemplate
|
|||
# devolvemos la ruta original, que puede ser el archivo que no existe
|
||||
# o vacía si se está subiendo uno.
|
||||
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
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
def static_file_path
|
||||
|
@ -145,8 +144,31 @@ class MetadataFile < MetadataTemplate
|
|||
end
|
||||
end
|
||||
|
||||
# No hay archivo pero se lo describió
|
||||
def no_file_for_description?
|
||||
!path? && description?
|
||||
# Obtiene el id del blob asociado
|
||||
#
|
||||
# @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
|
||||
|
|
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.
|
||||
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
|
||||
def private?
|
||||
false
|
||||
|
|
|
@ -25,7 +25,7 @@ require 'jekyll/utils'
|
|||
class MetadataSlug < MetadataTemplate
|
||||
# Trae el slug desde el título si existe o una string al azar
|
||||
def default_value
|
||||
title ? Jekyll::Utils.slugify(title) : SecureRandom.uuid
|
||||
title ? Jekyll::Utils.slugify(title, mode: site.slugify_mode) : SecureRandom.uuid
|
||||
end
|
||||
|
||||
def value
|
||||
|
@ -39,6 +39,6 @@ class MetadataSlug < MetadataTemplate
|
|||
return if post.title&.private?
|
||||
return if post.title&.value&.blank?
|
||||
|
||||
post.title&.value&.to_s
|
||||
post.title&.value&.to_s&.unicode_normalize
|
||||
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
|
||||
# guardado
|
||||
def save
|
||||
return true unless changed?
|
||||
if !changed?
|
||||
self[:value] = document_value if private?
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
self[:value] = sanitize value
|
||||
self[:value] = encrypt(value) if private?
|
||||
|
|
|
@ -29,7 +29,7 @@ class Post
|
|||
# TODO: Reemplazar cuando leamos el contenido del Document
|
||||
# a demanda?
|
||||
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
|
||||
|
||||
|
@ -90,16 +90,21 @@ class Post
|
|||
'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
|
||||
# poder reemplazar valores.
|
||||
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.
|
||||
html.css('img').each do |img|
|
||||
next if %r{\Ahttps?://} =~ img.attributes['src']
|
||||
html.css('img,audio,video,iframe').each do |element|
|
||||
src = element.attributes['src']
|
||||
|
||||
img.attributes['src'].value = Rails.application.routes.url_helpers.site_static_file_url(site,
|
||||
file: img.attributes['src'].value)
|
||||
next unless src&.value&.start_with? 'public/'
|
||||
|
||||
src.value = Rails.application.routes.url_helpers.site_static_file_url(site, file: src.value)
|
||||
end
|
||||
|
||||
# Notificar a les usuaries que están viendo una previsualización
|
||||
|
@ -108,12 +113,16 @@ class Post
|
|||
|
||||
# Cacofonía
|
||||
html.to_html.html_safe
|
||||
rescue Liquid::Error => e
|
||||
ExceptionNotifier.notify(e, data: { site: site.name, post: post.id })
|
||||
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
# Devuelve una llave para poder guardar el post en una cache
|
||||
def cache_key
|
||||
'posts/' + uuid.value
|
||||
"posts/#{uuid.value}"
|
||||
end
|
||||
|
||||
def cache_version
|
||||
|
@ -123,7 +132,7 @@ class Post
|
|||
# Agregar el timestamp para saber si cambió, siguiendo el módulo
|
||||
# ActiveRecord::Integration
|
||||
def cache_key_with_version
|
||||
cache_key + '-' + cache_version
|
||||
"#{cache_key}-#{cache_version}"
|
||||
end
|
||||
|
||||
# TODO: Convertir a UUID?
|
||||
|
|
|
@ -14,9 +14,8 @@ class Post
|
|||
#
|
||||
# @return [IndexedPost]
|
||||
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.site_id = site.id
|
||||
indexed_post.path = path.basename
|
||||
indexed_post.locale = locale.value
|
||||
indexed_post.dictionary = IndexedPost.to_dictionary(locale: locale.value)
|
||||
|
@ -28,8 +27,6 @@ class Post
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Indexa o reindexa el Post
|
||||
#
|
||||
# @return [Boolean]
|
||||
|
@ -41,6 +38,8 @@ class Post
|
|||
to_index.destroy.destroyed?
|
||||
end
|
||||
|
||||
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.
|
||||
|
|
|
@ -17,7 +17,7 @@ class Site < ApplicationRecord
|
|||
|
||||
# TODO: Hacer que los diferentes tipos de deploy se auto registren
|
||||
# @see app/services/site_service.rb
|
||||
DEPLOYS = %i[local private www zip hidden_service].freeze
|
||||
DEPLOYS = %i[local private www zip hidden_service distributed_press].freeze
|
||||
|
||||
validates :name, uniqueness: true, hostname: {
|
||||
allow_root_label: true
|
||||
|
@ -179,10 +179,20 @@ class Site < ApplicationRecord
|
|||
# Siempre tiene que tener algo porque las traducciones están
|
||||
# incorporadas a los sitios de Sutty, aunque les usuaries no traduzcan
|
||||
# sus sitios.
|
||||
#
|
||||
# @return [Array]
|
||||
def locales
|
||||
@locales ||= config.fetch('locales', I18n.available_locales).map(&:to_sym)
|
||||
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
|
||||
#
|
||||
# @return [Hash]
|
||||
|
@ -250,6 +260,8 @@ class Site < ApplicationRecord
|
|||
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]
|
||||
|
@ -425,7 +437,7 @@ class Site < ApplicationRecord
|
|||
|
||||
# El directorio donde se almacenan los sitios
|
||||
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
|
||||
|
||||
def self.default
|
||||
|
@ -484,6 +496,7 @@ class Site < ApplicationRecord
|
|||
config.title = title
|
||||
config.url = url(slash: false)
|
||||
config.hostname = hostname
|
||||
config.locales = locales.map(&:to_s)
|
||||
end
|
||||
|
||||
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
||||
|
|
|
@ -33,10 +33,10 @@ class Site
|
|||
def write
|
||||
return if persisted?
|
||||
|
||||
@saved = Site::Writer.new(site: site, file: path,
|
||||
content: content.to_yaml).save
|
||||
# Actualizar el hash para no escribir dos veces
|
||||
@hash = content.hash
|
||||
@saved = Site::Writer.new(site: site, file: path, content: content.to_yaml).save.tap do |result|
|
||||
# Actualizar el hash para no escribir dos veces
|
||||
@hash = content.hash
|
||||
end
|
||||
end
|
||||
alias save write
|
||||
|
||||
|
|
|
@ -14,9 +14,7 @@ class Site
|
|||
|
||||
def index_posts!
|
||||
Site.transaction do
|
||||
docs.each do |post|
|
||||
post.to_index.save
|
||||
end
|
||||
docs.each(&:index!)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -147,6 +147,23 @@ class Site
|
|||
rugged.index.remove(relativize(file))
|
||||
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
|
||||
|
||||
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las
|
||||
|
|
|
@ -9,6 +9,8 @@ class Usuarie < ApplicationRecord
|
|||
validates_uniqueness_of :email
|
||||
validates_with EmailAddress::ActiveRecordValidator, field: :email
|
||||
|
||||
before_create :lang_from_locale!
|
||||
|
||||
has_many :roles
|
||||
has_many :sites, through: :roles
|
||||
has_many :blazer_audits, foreign_key: 'user_id', class_name: 'Blazer::Audit'
|
||||
|
@ -38,4 +40,10 @@ class Usuarie < ApplicationRecord
|
|||
increment_failed_attempts
|
||||
lock_access! if attempts_exceeded? && !access_locked?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lang_from_locale!
|
||||
self.lang = I18n.locale.to_s
|
||||
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
|
||||
# TODO: Implementar rollback en la configuración
|
||||
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
|
||||
# configuración en el repositorio git
|
||||
def create
|
||||
|
@ -11,7 +16,14 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
|||
add_role temporal: false, rol: 'usuarie'
|
||||
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.config.write &&
|
||||
commit_config(action: :create)
|
||||
|
@ -19,6 +31,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
|||
|
||||
add_licencias
|
||||
|
||||
deploy
|
||||
|
||||
site
|
||||
end
|
||||
|
||||
|
|
2
app/views/bootstrap/_alert.haml
Normal file
2
app/views/bootstrap/_alert.haml
Normal file
|
@ -0,0 +1,2 @@
|
|||
.alert.alert-primary.mx-auto.content.max-w-md-70ch{ role: 'alert', class: local_assigns[:class] }
|
||||
= yield
|
|
@ -11,7 +11,6 @@
|
|||
url: site_collaborate_path(@site),
|
||||
method: :post) do |f|
|
||||
- unless current_usuarie
|
||||
= render 'layouts/flash'
|
||||
.form-group
|
||||
= f.label :email
|
||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
%h1= t('.hi')
|
||||
%h1= @hi
|
||||
|
||||
= sanitize_markdown t('.explanation', fqdn: @deploy_local.site.hostname),
|
||||
tags: %w[p a strong em]
|
||||
= sanitize_markdown @explanation, tags: %w[p a strong em]
|
||||
|
||||
%table
|
||||
%thead
|
||||
%tr
|
||||
%th= t('.th.type')
|
||||
%th= t('.th.status')
|
||||
- @headers.each do |header|
|
||||
%th= header
|
||||
%tbody
|
||||
- @deploys.each do |deploy, value|
|
||||
%tr
|
||||
%td= t(".#{deploy}.title")
|
||||
%td= value ? t(".#{deploy}.success") : t(".#{deploy}.error")
|
||||
- @table.each do |row|
|
||||
- row[:urls].each do |url|
|
||||
%tr
|
||||
%td= row[:title]
|
||||
%td= row[:status]
|
||||
%td= link_to_if url.present?, url, url
|
||||
%td
|
||||
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
|
||||
%td= row[:size]
|
||||
|
||||
= sanitize_markdown t('.help'), tags: %w[p a strong em]
|
||||
= sanitize_markdown @help, tags: %w[p a strong em]
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
= '# ' + t('.hi')
|
||||
= "# #{@hi}"
|
||||
\
|
||||
= t('.explanation', fqdn: @deploy_local.site.hostname)
|
||||
= @explanation
|
||||
\
|
||||
= Terminal::Table.new do |table|
|
||||
- table << [t('.th.type'), t('.th.status')]
|
||||
- table.add_separator
|
||||
- @deploys.each do |deploy, value|
|
||||
- table << [t(".#{deploy}.title"),
|
||||
value ? t(".#{deploy}.success") : t(".#{deploy}.error")]
|
||||
= @terminal_table
|
||||
\
|
||||
= t('.help')
|
||||
= @help
|
||||
|
|
21
app/views/deploys/_deploy_distributed_press.haml
Normal file
21
app/views/deploys/_deploy_distributed_press.haml
Normal file
|
@ -0,0 +1,21 @@
|
|||
-# Publicar a la web distribuida
|
||||
|
||||
.row
|
||||
.col
|
||||
= deploy.hidden_field :id
|
||||
= deploy.hidden_field :type
|
||||
.custom-control.custom-switch
|
||||
-#
|
||||
El checkbox invierte la lógica de destrucción porque queremos
|
||||
crear el deploy si está activado y destruirlo si está
|
||||
desactivado.
|
||||
= deploy.check_box :_destroy,
|
||||
{ checked: deploy.object.persisted?, class: 'custom-control-input' },
|
||||
'0', '1'
|
||||
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||
%h3= t('.title')
|
||||
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
||||
tags: %w[p strong em a]
|
||||
|
||||
|
||||
%hr/
|
1
app/views/deploys/_deploy_reindex.haml
Normal file
1
app/views/deploys/_deploy_reindex.haml
Normal file
|
@ -0,0 +1 @@
|
|||
-# NADA
|
|
@ -1,6 +1,8 @@
|
|||
= content_for :body do
|
||||
- 'black-bg'
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-4.align-self-center
|
||||
.sr-only
|
||||
|
@ -11,8 +13,6 @@
|
|||
url: confirmation_path(resource_name),
|
||||
html: { method: :post }) do |f|
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
:ruby
|
||||
value = if resource.pending_reconfirmation?
|
||||
resource.unconfirmed_email
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
= content_for :body do
|
||||
- 'black-bg'
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-5.align-self-center
|
||||
%h2= t 'devise.invitations.edit.header'
|
||||
|
@ -8,7 +10,6 @@
|
|||
as: resource_name,
|
||||
url: invitation_path(resource_name),
|
||||
html: { method: :put }) do |f|
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
= f.hidden_field :invitation_token, readonly: true
|
||||
- if f.object.class.require_password_on_accepting
|
||||
.form-group
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
= content_for :body do
|
||||
- 'black-bg'
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-5.align-self-center
|
||||
%h2= t 'devise.invitations.new.header'
|
||||
|
@ -8,7 +10,6 @@
|
|||
as: resource_name,
|
||||
url: invitation_path(resource_name),
|
||||
html: { method: :post }) do |f|
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
- resource.class.invite_key_fields.each do |field|
|
||||
.form-group
|
||||
= f.label field
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
%p= t('.greeting', recipient: @email)
|
||||
%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')
|
||||
\
|
||||
= 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= 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
|
||||
%p= t('devise.mailer.invitation_instructions.accept_until',
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
\
|
||||
= 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
|
||||
= t('devise.mailer.invitation_instructions.accept_until',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
%p= t('.greeting', recipient: @resource.email)
|
||||
%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_3')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
\
|
||||
= 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')
|
||||
\
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
%p= t('.greeting', recipient: @resource.email)
|
||||
%p= t('.message')
|
||||
%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')
|
||||
\
|
||||
= unlock_url(@resource, unlock_token: @token)
|
||||
= unlock_url(@resource, unlock_token: @token, change_locale_to: @resource.lang)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
= content_for :body do
|
||||
- 'black-bg'
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-5.align-self-center
|
||||
.sr-only
|
||||
|
@ -10,7 +12,6 @@
|
|||
= form_for(resource, as: resource_name,
|
||||
url: password_path(resource_name),
|
||||
html: { method: :put }) do |f|
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
= f.hidden_field :reset_password_token
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
= content_for :body do
|
||||
- 'black-bg'
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-5.align-self-center
|
||||
.sr-only
|
||||
|
@ -11,7 +13,6 @@
|
|||
as: resource_name,
|
||||
url: password_path(resource_name),
|
||||
html: { method: :post }) do |f|
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
.form-group
|
||||
= f.label :email, class: 'sr-only'
|
||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
= content_for :body do
|
||||
- 'black-bg'
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-6.align-self-center
|
||||
%h2= t('.title')
|
||||
|
@ -11,8 +13,6 @@
|
|||
url: registration_path(resource_name),
|
||||
html: { method: :put }) do |f|
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
.form-group
|
||||
= f.label :email
|
||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
= content_for :body do
|
||||
- 'black-bg'
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-5.align-self-center
|
||||
%h2= t('.sign_up')
|
||||
|
@ -8,9 +10,7 @@
|
|||
|
||||
= form_for(resource,
|
||||
as: resource_name,
|
||||
url: registration_path(resource_name)) do |f|
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
url: registration_path(resource_name, params: { locale: params[:locale] })) do |f|
|
||||
|
||||
.form-group
|
||||
= f.label :email, class: 'sr-only'
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-5.align-self-center
|
||||
= render 'layouts/flash'
|
||||
|
||||
.sr-only
|
||||
%h2= t('.sign_in')
|
||||
%p= t('.help')
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
- if resource.errors.any?
|
||||
#error_explanation
|
||||
%h2
|
||||
= I18n.t("errors.messages.not_saved", |
|
||||
count: resource.errors.count, |
|
||||
resource: resource.class.model_name.human.downcase) |
|
||||
%ul
|
||||
- resource.errors.full_messages.each do |message|
|
||||
%li= message
|
||||
= render 'bootstrap/alert' do
|
||||
- resource.errors.full_messages.each do |message|
|
||||
%p= message
|
||||
|
|
|
@ -1,35 +1,38 @@
|
|||
%hr/
|
||||
|
||||
- locale = params.permit(:locale)
|
||||
|
||||
- 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/
|
||||
|
||||
- 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'
|
||||
%br/
|
||||
|
||||
- if devise_mapping.recoverable?
|
||||
- unless %w[passwords registrations].include?(controller_name)
|
||||
= link_to t('.forgot_your_password'),
|
||||
new_password_path(resource_name)
|
||||
new_password_path(resource_name, params: locale)
|
||||
%br/
|
||||
|
||||
- if devise_mapping.confirmable? && controller_name != 'confirmations'
|
||||
= link_to t('.didn_t_receive_confirmation_instructions'),
|
||||
new_confirmation_path(resource_name)
|
||||
new_confirmation_path(resource_name, params: locale)
|
||||
%br/
|
||||
|
||||
- if devise_mapping.lockable?
|
||||
- if resource_class.unlock_strategy_enabled?(:email)
|
||||
- if controller_name != 'unlocks'
|
||||
= link_to t('.didn_t_receive_unlock_instructions'),
|
||||
new_unlock_path(resource_name)
|
||||
new_unlock_path(resource_name, params: locale)
|
||||
%br/
|
||||
|
||||
- if devise_mapping.omniauthable?
|
||||
- resource_class.omniauth_providers.each do |provider|
|
||||
= link_to t('.sign_in_with_provider',
|
||||
provider: OmniAuth::Utils.camelize(provider)),
|
||||
omniauth_authorize_path(resource_name, provider)
|
||||
omniauth_authorize_path(resource_name, provider, params: locale)
|
||||
%br/
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
= content_for :body do
|
||||
- 'black-bg'
|
||||
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
|
||||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-5.align-self-center
|
||||
.sr-only
|
||||
|
@ -11,7 +13,6 @@
|
|||
as: resource_name,
|
||||
url: unlock_path(resource_name),
|
||||
html: { method: :post }) do |f|
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
.form-group
|
||||
= f.label :email, class: 'sr-only'
|
||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-6.align-self-center
|
||||
.alert{role: 'alert', class: "alert-success"}
|
||||
= render 'bootstrap/alert' do
|
||||
= t('.confirmation_sent')
|
||||
|
|
|
@ -22,3 +22,7 @@
|
|||
%li.nav-item
|
||||
= link_to t('.logout'), main_app.destroy_usuarie_session_path,
|
||||
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}"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
- flash.each do |type, message|
|
||||
- unless type == 'js'
|
||||
.alert{ role: 'alert', class: "alert-#{type}" }= message
|
||||
= render 'bootstrap/alert' do
|
||||
= message
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
-# DEPRECADO
|
||||
.alert.alert-info.alert-dismissible.fade.show{role: 'alert'}
|
||||
- if help.respond_to? :each
|
||||
%ul
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
%body{ class: yield(:body) }
|
||||
.container-fluid#sutty
|
||||
= render 'layouts/breadcrumb'
|
||||
= render 'layouts/flash'
|
||||
|
||||
= yield
|
||||
|
||||
- if flash[:js]
|
||||
.js-flash.d-none{ data: flash[:js] }
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
- unless post.errors.empty?
|
||||
.alert.alert-danger
|
||||
%h4= t('.errors.title')
|
||||
%p= t('.errors.help')
|
||||
- title = t('.errors.title')
|
||||
- help = t('.errors.help')
|
||||
= render 'bootstrap/alert' do
|
||||
%h4= title
|
||||
%p= help
|
||||
|
||||
%ul
|
||||
- post.errors.each do |attribute, errors|
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.form-group
|
||||
= submit_tag t('.save'), class: 'btn submit-post'
|
||||
.invalid-help.alert.alert-danger.d-none
|
||||
= render 'bootstrap/alert', class: 'invalid-help d-none' do
|
||||
= site.config.fetch('invalid_help', t('.invalid_help'))
|
||||
.sending-help.alert.alert-success.d-none
|
||||
= render 'bootstrap/alert', class: 'sending-help d-none' do
|
||||
= site.config.fetch('sending_help', t('.sending_help'))
|
||||
|
|
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')
|
|
@ -6,7 +6,7 @@
|
|||
.editor{ id: attribute, data: { editor: '' } }
|
||||
-# Esto es para luego decirle al navegador que se olvide estas cosas.
|
||||
= hidden_field_tag 'storage_keys[]', "#{request.original_url}##{attribute}", data: { target: 'storage-key' }
|
||||
.alert.alert-info
|
||||
= render 'bootstrap/alert' do
|
||||
:markdown
|
||||
#{t('editor.alert')}
|
||||
= text_area_tag "#{base}[#{attribute}]", '',
|
||||
|
@ -123,7 +123,7 @@
|
|||
%label{ for: 'link-url' }= t('editor.url')
|
||||
%input.form-control{ type: 'url', id: 'link-url', name: 'link-url' }/
|
||||
|
||||
.editor-aviso-word.alert.alert-info
|
||||
= render 'bootstrap/alert', class: 'editor-aviso-word' do
|
||||
%p= t('editor.word')
|
||||
|
||||
.editor-content.form-control.h-auto.mt-1{ contenteditable: 'true' }
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
.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
|
||||
.form-group
|
||||
= 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
|
|
@ -34,14 +34,13 @@
|
|||
= render 'sites/build', site: @site
|
||||
|
||||
- if @site.design.credits
|
||||
.alert.alert-primary{ role: 'alert' }
|
||||
= render 'bootstrap/alert' do
|
||||
= sanitize_markdown @site.design.credits
|
||||
= link_to t('sites.donations.text'), t('sites.donations.url'), class: 'btn'
|
||||
- if @site.design.designer_url
|
||||
= link_to t('sites.designer_url'), @site.design.designer_url, class: 'btn'
|
||||
|
||||
%section.col
|
||||
= render 'layouts/flash'
|
||||
.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|
|
||||
|
@ -89,22 +88,22 @@
|
|||
|
||||
%div
|
||||
%tbody
|
||||
- dir = t("locales.#{@locale}.dir")
|
||||
- dir = @site.data.dig(params[:locale], 'dir')
|
||||
- size = @posts.size
|
||||
- @posts.each_with_index do |post, i|
|
||||
-#
|
||||
TODO: Solo les usuaries cachean porque tenemos que separar
|
||||
les botones por permisos.
|
||||
- cache_if @usuarie, [post, I18n.locale] do
|
||||
- checkbox_id = "checkbox-#{post.id}"
|
||||
%tr{ id: post.id, data: { target: 'reorder.row' } }
|
||||
- checkbox_id = "checkbox-#{post.post_id}"
|
||||
%tr{ id: post.post_id, data: { target: 'reorder.row' } }
|
||||
%td
|
||||
.custom-control.custom-checkbox
|
||||
%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.id,
|
||||
= hidden_field 'post[reorder]', post.post_id,
|
||||
value: size - i,
|
||||
data: { reorder: true }
|
||||
%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
|
||||
.col-md-8
|
||||
%article.content.table-responsive-md
|
||||
|
@ -6,13 +6,6 @@
|
|||
edit_site_post_path(@site, @post.id),
|
||||
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
|
||||
%thead
|
||||
%tr
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
- unless site.errors.empty?
|
||||
.alert.alert-info
|
||||
%h4= t('.errors.title')
|
||||
%p.lead= t('.errors.help')
|
||||
- title = t('.errors.title')
|
||||
- help = t('.errors.help')
|
||||
= render 'bootstrap/alert' do
|
||||
%h4= title
|
||||
%p.lead= help
|
||||
%ul
|
||||
- site.errors.messages.each_pair do |attr, error|
|
||||
- attr = attr.to_s
|
||||
|
@ -48,7 +50,7 @@
|
|||
%h2= t('.design.title')
|
||||
%p.lead= t('.help.design')
|
||||
- if invalid? site, :design_id
|
||||
.alert.alert-info
|
||||
= render 'bootstrap/alert' do
|
||||
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
|
||||
layouts: site.incompatible_layouts.to_sentence)
|
||||
.row.designs
|
||||
|
@ -104,27 +106,27 @@
|
|||
|
||||
%hr/
|
||||
|
||||
.form-group#tienda
|
||||
%h2= t('.tienda.title')
|
||||
%p.lead
|
||||
- if site.tienda?
|
||||
= t('.tienda.help')
|
||||
- else
|
||||
= t('.tienda.first_time_html')
|
||||
|
||||
.row
|
||||
.col
|
||||
.form-group
|
||||
= f.label :tienda_url
|
||||
= f.url_field :tienda_url, class: 'form-control'
|
||||
.col
|
||||
.form-group
|
||||
= f.label :tienda_api_key
|
||||
= f.text_field :tienda_api_key, class: 'form-control'
|
||||
|
||||
%hr/
|
||||
|
||||
- if site.persisted?
|
||||
.form-group#tienda
|
||||
%h2= t('.tienda.title')
|
||||
%p.lead
|
||||
- if site.tienda?
|
||||
= t('.tienda.help')
|
||||
- else
|
||||
= t('.tienda.first_time_html')
|
||||
|
||||
.row
|
||||
.col
|
||||
.form-group
|
||||
= f.label :tienda_url
|
||||
= f.url_field :tienda_url, class: 'form-control'
|
||||
.col
|
||||
.form-group
|
||||
= f.label :tienda_api_key
|
||||
= f.text_field :tienda_api_key, class: 'form-control'
|
||||
|
||||
%hr/
|
||||
|
||||
.form-group#contact
|
||||
%h2= t('.contact.title')
|
||||
%p.lead= t('.contact.help')
|
||||
|
|
1
config/credentials.yml.enc.ci
Normal file
1
config/credentials.yml.enc.ci
Normal file
|
@ -0,0 +1 @@
|
|||
1jEfzfldP9tT4+HWfhP48I9hw31gYCnnxHWpYjPrcTm/pgkFdiG+mDa6y31EOxzs50w6FEw2GO127BnyBSUIPIxuWY0cR96xL5pVrS3vjyzM84QN4lJF9ER0Tz1AQ9S7NJ54CelSkMfFt/rf+O4YM8cLtdSVsVC/HlGbp16p3D1pm4MFo5cQb0hEmlyyYlzEn4oJtsp/MCIwI4+z8oFhxKdMIxdbiw+KS/7PBRfMm1h5rdGORCnD69iVmnXseMvVtZn9A7N7uR6+gFlhxlD5yyEW0pwTj3tbu9NeIOVbtmYOL5ZhLW9REXtGTqR5Op/LN+ukIXbDNEScKltJXUdWfa9Pd/QjVT8IMURZ04POEMDgs1cw363yz4f+WQForhSco9oYLDOd5hTGRXoZ9fnjnfJSTjINM62hkfDY3w3+s844nNbjbj+lPTJHU/QjRhcuNqBDDxWUfwTmRIqm5zrelnHnZnuFmFwCNet6NChC6EFUAFjrals6kTSQllyMt4xImqA+HL7DnjWj6VURSH+nGQTA4tQvDdfbDwTzg/PvRkJcsy2dRd135RQdmRZ+8KXBviLabwdR256vaCqSO1j+jyeUPGLll35ghyLxncyBkkAKt1zaDRPDWgVafg0gJ3v7hVV5TYgToPzlv4w88KPCY7cBhkb1qGoXAhtO6iAuZYK9eyZd1gNQJKyqbcLqA5aTTX/ylfdbptWhaZ8ibB8KBgVyn2RmrOHEhB38rDSMHHNfK3Xs4/hhqMFIGHGTGCUYVmjCzhVFd15yRurU32d3YtP8W4L77H7qkFsF1gnvsZx+R084LcJqknwY94dmjtUE4x2u+Qh3ElFj--lr8JoUq1WH9xXNsB--mE8hxHADL7SbDWabAPY1+Q==
|
|
@ -104,7 +104,7 @@ en:
|
|||
new:
|
||||
sign_up: Sign up
|
||||
help: We only ask for an e-mail address and a password. The password is safely stored, no one else besides you knows it! You'll also receive an e-mail to confirm your account.
|
||||
signed_up: Welcome! You have signed up successfully.
|
||||
signed_up: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
|
||||
signed_up_but_inactive: You have signed up successfully. However, we could not sign you in because your account is not yet activated.
|
||||
signed_up_but_locked: You have signed up successfully. However, we could not sign you in because your account is locked.
|
||||
signed_up_but_unconfirmed: A message with a confirmation link has been sent to your email address. Please follow the link to activate your account.
|
||||
|
@ -138,7 +138,7 @@ en:
|
|||
errors:
|
||||
messages:
|
||||
already_confirmed: was already confirmed, please try signing in
|
||||
confirmation_period_expired: needs to be confirmed within %{period}, please request a new one
|
||||
confirmation_period_expired: "wasn't confirmed within %{period}. Please request a new confirmation link by using the \"Resend confirmation instructions\" button below and find it in your inbox."
|
||||
expired: has expired, please request a new one
|
||||
not_found: not found
|
||||
not_locked: was not locked
|
||||
|
|
|
@ -104,7 +104,7 @@ es:
|
|||
new:
|
||||
sign_up: Registrarme
|
||||
help: Para registrarte solo pedimos una dirección de correo y una contraseña. La contraseña se almacena de forma segura, ¡nadie más que vos la sabe! Recibirás un correo de confirmación de cuenta.
|
||||
signed_up: Bienvenide. Tu cuenta fue creada.
|
||||
signed_up: "Hemos enviado un mensaje con un enlace de confirmación a tu correo electrónico. Por favor, abrí el enlace para terminar de activar tu cuenta."
|
||||
signed_up_but_inactive: Tu cuenta ha sido creada correctamente. Sin embargo, no hemos podido iniciar la sesión porque tu cuenta aún no está activada.
|
||||
signed_up_but_locked: Tu cuenta ha sido creada correctamente. Sin embargo, no hemos podido iniciar la sesión porque que tu cuenta está bloqueada.
|
||||
signed_up_but_unconfirmed: Para recibir actualizaciones, se ha enviado un mensaje con un enlace de confirmación a tu correo electrónico. Abre el enlace para activar tu cuenta.
|
||||
|
@ -138,7 +138,7 @@ es:
|
|||
errors:
|
||||
messages:
|
||||
already_confirmed: ya ha sido confirmada, por favor intenta iniciar sesión
|
||||
confirmation_period_expired: necesita confirmarse dentro de %{period}, por favor solicita una nueva
|
||||
confirmation_period_expired: "quedó sin confirmar luego de %{period}. Por favor, usa el botón \"Reenviar instrucciones de confirmación\" y busca el nuevo link en tu casilla."
|
||||
expired: ha expirado, por favor solicita una nueva
|
||||
not_found: no se ha encontrado
|
||||
not_locked: no estaba bloqueada
|
||||
|
|
|
@ -16,6 +16,15 @@ en:
|
|||
ur:
|
||||
name: Urdu
|
||||
dir: rtl
|
||||
zh:
|
||||
name: Chinese
|
||||
dir: ltr
|
||||
de:
|
||||
name: German
|
||||
dir: ltr
|
||||
fr:
|
||||
name: French
|
||||
dir: ltr
|
||||
login:
|
||||
email: E-mail address
|
||||
password: Password
|
||||
|
@ -81,6 +90,9 @@ en:
|
|||
th:
|
||||
type: Type
|
||||
status: Status
|
||||
seconds: Duration
|
||||
size: Space used
|
||||
url: Address
|
||||
deploy_local:
|
||||
title: Build the site
|
||||
success: Success!
|
||||
|
@ -105,6 +117,18 @@ en:
|
|||
title: Alternative domain name
|
||||
success: Success!
|
||||
error: Error
|
||||
deploy_distributed_press:
|
||||
title: Distributed Web
|
||||
success: Success!
|
||||
error: Error
|
||||
deploy_reindex:
|
||||
title: Reindex
|
||||
success: Success!
|
||||
error: Error
|
||||
deploy_localized_domain:
|
||||
title: Domain name by language
|
||||
success: Success!
|
||||
error: Error
|
||||
deploy_rsync:
|
||||
title: Synchronize to backup server
|
||||
success: Success!
|
||||
|
@ -255,9 +279,26 @@ en:
|
|||
|
||||
Only accessible through [Tor
|
||||
Browser](https://www.torproject.org/download/)
|
||||
deploy_distributed_press:
|
||||
title: 'Publish to the distributed Web'
|
||||
help: |
|
||||
Make your site available through peer-to-peer protocols,
|
||||
Inter-Planetary File System (IPFS), Hypercore, and via
|
||||
BitTorrent, so your site is more resilient and can be available
|
||||
offline, including in community mesh networks.
|
||||
|
||||
**Important:** Only use this option if you would like your data
|
||||
to be permanently available. If you decide to undo this
|
||||
selection, a cleared version of the site will be shared in its
|
||||
place. However, it is possible that nodes on the distributed
|
||||
storage network may continue retaining copies of the data
|
||||
indefinitely.
|
||||
|
||||
[Learn more](https://ffdweb.org/building-distributed-press-a-publishing-tool-for-the-decentralized-web/)
|
||||
stats:
|
||||
index:
|
||||
title: Statistics
|
||||
filter: "Filter"
|
||||
help: |
|
||||
These statistics show information about how your site is generated and
|
||||
how many resources it uses.
|
||||
|
@ -433,6 +474,8 @@ en:
|
|||
attribute_ro:
|
||||
file:
|
||||
download: Download file
|
||||
password:
|
||||
safety: Passwords are stored safely
|
||||
show:
|
||||
front_matter: Post metadata
|
||||
submit:
|
||||
|
@ -494,7 +537,7 @@ en:
|
|||
preview:
|
||||
btn: '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'
|
||||
private: '🔒 The values of this field will remain private'
|
||||
select:
|
||||
|
|
|
@ -16,6 +16,15 @@ es:
|
|||
ur:
|
||||
name: Urdu
|
||||
dir: rtl
|
||||
zh:
|
||||
name: Chino
|
||||
dir: ltr
|
||||
de:
|
||||
name: Alemán
|
||||
dir: ltr
|
||||
fr:
|
||||
name: Francés
|
||||
dir: ltr
|
||||
login:
|
||||
email: Correo electrónico
|
||||
password: Contraseña
|
||||
|
@ -81,6 +90,9 @@ es:
|
|||
th:
|
||||
type: Tipo
|
||||
status: Estado
|
||||
seconds: Duración
|
||||
size: Espacio ocupado
|
||||
url: Dirección
|
||||
deploy_local:
|
||||
title: Generar el sitio
|
||||
success: ¡Éxito!
|
||||
|
@ -105,6 +117,18 @@ es:
|
|||
title: Dominio alternativo
|
||||
success: ¡Éxito!
|
||||
error: Hubo un error
|
||||
deploy_distributed_press:
|
||||
title: Web distribuida
|
||||
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!
|
||||
error: Hubo un error
|
||||
deploy_rsync:
|
||||
title: Sincronizar al servidor alternativo
|
||||
success: ¡Éxito!
|
||||
|
@ -260,9 +284,26 @@ es:
|
|||
|
||||
Sólo será accesible a través del [Navegador
|
||||
Tor](https://www.torproject.org/es/download/).
|
||||
deploy_distributed_press:
|
||||
title: 'Publicar a la Web distribuida'
|
||||
help: |
|
||||
Utiliza protocolos de pares, Inter-Planetary File System (IPFS),
|
||||
Hypercore y torrents, para que tu sitio sea más resiliente y
|
||||
esté disponible _offline_, inclusive en redes _mesh_
|
||||
comunitarias.
|
||||
|
||||
**Importante:** Sólo usa esta opción si te parece correcto que
|
||||
tu contenido esté disponible permanentemente. Cuando elijas
|
||||
des-hacer esta acción, una versión "vacía" del sitio será
|
||||
compartida en su lugar. Sin embargo, es posible que algunos
|
||||
nodos en la red de almacenamiento distribuida puedan retener
|
||||
copias de tu contenido indefinidamente.
|
||||
|
||||
[Saber más (en inglés)](https://ffdweb.org/building-distributed-press-a-publishing-tool-for-the-decentralized-web/)
|
||||
stats:
|
||||
index:
|
||||
title: Estadísticas
|
||||
filter: "Filtrar"
|
||||
help: |
|
||||
Las estadísticas visibilizan información sobre cómo se genera y
|
||||
cuántos recursos utiliza tu sitio.
|
||||
|
@ -441,6 +482,8 @@ es:
|
|||
attribute_ro:
|
||||
file:
|
||||
download: Descargar archivo
|
||||
password:
|
||||
safety: Las contraseñas se almacenan de forma segura
|
||||
show:
|
||||
front_matter: Metadatos del artículo
|
||||
submit:
|
||||
|
@ -502,7 +545,7 @@ es:
|
|||
preview:
|
||||
btn: 'Versión 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'
|
||||
private: '🔒 Los valores de este campo serán privados'
|
||||
select:
|
||||
|
|
|
@ -55,7 +55,7 @@ Rails.application.routes.draw do
|
|||
|
||||
# Gestionar artículos según idioma
|
||||
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'
|
||||
resources :posts do
|
||||
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
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Crea la tabla de publishers de Distributed Press que contiene las
|
||||
# instancias y tokens
|
||||
class CreateDistributedPressPublisher < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :distributed_press_publishers do |t|
|
||||
t.timestamps
|
||||
t.string :instance, unique: true
|
||||
t.text :token_ciphertext, null: false
|
||||
t.datetime :expires_at, null: true
|
||||
end
|
||||
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
|
10
lib/tasks/distributed_press.rake
Normal file
10
lib/tasks/distributed_press.rake
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
namespace :distributed_press do
|
||||
namespace :tokens do
|
||||
desc 'Renew tokens'
|
||||
task renew: :environment do
|
||||
RenewDistributedPressTokensJob.perform_now
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue