mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-26 09:06:21 +00:00
Merge branch 'rails' into usar-sutty-editor
This commit is contained in:
commit
fda9047590
70 changed files with 1587 additions and 1036 deletions
|
@ -1,10 +1,4 @@
|
||||||
# Excluir todo
|
# Excluir todo
|
||||||
*
|
*
|
||||||
# Solo agregar lo que usamos en COPY
|
# Solo agregar lo que usamos en COPY
|
||||||
!./.git/
|
# !./archivo
|
||||||
!./rubygems-platform-musl.patch
|
|
||||||
!./Gemfile
|
|
||||||
!./Gemfile.lock
|
|
||||||
!./config/credentials.yml.enc
|
|
||||||
!./public/assets/
|
|
||||||
!./public/packs/
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
RAILS_GROUPS=assets
|
||||||
|
DELEGATE=athshe.sutty.nl
|
||||||
|
HAINISH=../haini.sh/haini.sh
|
||||||
DATABASE=
|
DATABASE=
|
||||||
RAILS_ENV=
|
RAILS_ENV=development
|
||||||
IMAP_SERVER=
|
IMAP_SERVER=
|
||||||
DEFAULT_FROM=
|
DEFAULT_FROM=
|
||||||
EXCEPTION_TO=
|
EXCEPTION_TO=
|
||||||
|
|
125
Dockerfile
125
Dockerfile
|
@ -1,128 +1,21 @@
|
||||||
# Este Dockerfile está armado pensando en una compilación lanzada desde
|
FROM registry.nulo.in/sutty/rails:3.13.6-2.7.5
|
||||||
# el mismo repositorio de trabajo. Cuando tengamos CI/CD algunas cosas
|
ARG PANDOC_VERSION=2.17.1.1
|
||||||
# como el tarball van a tener que cambiar porque ya vamos a haber hecho
|
|
||||||
# un clone/pull limpio.
|
|
||||||
FROM alpine:3.13.5 AS build
|
|
||||||
MAINTAINER "f <f@sutty.nl>"
|
|
||||||
|
|
||||||
ARG RAILS_MASTER_KEY
|
|
||||||
ARG BRANCH
|
|
||||||
|
|
||||||
# Un entorno base
|
|
||||||
ENV BRANCH=$BRANCH
|
|
||||||
ENV SECRET_KEY_BASE solo_es_necesaria_para_correr_rake
|
|
||||||
ENV RAILS_ENV production
|
ENV RAILS_ENV production
|
||||||
ENV RAILS_MASTER_KEY=$RAILS_MASTER_KEY
|
|
||||||
|
|
||||||
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake
|
|
||||||
RUN apk add --no-cache postgresql-libs git yarn brotli libssh2 python3
|
|
||||||
|
|
||||||
RUN test "2.7.3" = `ruby -e 'puts RUBY_VERSION'`
|
|
||||||
|
|
||||||
# https://github.com/rubygems/rubygems/issues/2918
|
|
||||||
# https://gitlab.alpinelinux.org/alpine/aports/issues/10808
|
|
||||||
RUN apk add --no-cache patch
|
|
||||||
COPY ./rubygems-platform-musl.patch /tmp/
|
|
||||||
RUN cd /usr/lib/ruby/2.7.0 && patch -Np 0 -i /tmp/rubygems-platform-musl.patch
|
|
||||||
|
|
||||||
# Agregar el usuario
|
|
||||||
RUN addgroup -g 82 -S www-data
|
|
||||||
RUN adduser -s /bin/sh -G www-data -h /home/app -D app
|
|
||||||
RUN install -dm750 -o app -g www-data /home/app/sutty
|
|
||||||
RUN gem install --no-document bundler
|
|
||||||
|
|
||||||
# Empezamos con la usuaria app
|
|
||||||
USER app
|
|
||||||
# Vamos a trabajar dentro de este directorio
|
|
||||||
WORKDIR /home/app/sutty
|
|
||||||
|
|
||||||
# Copiamos solo el Gemfile para poder instalar las gemas necesarias
|
|
||||||
COPY --chown=app:www-data ./Gemfile .
|
|
||||||
COPY --chown=app:www-data ./Gemfile.lock .
|
|
||||||
RUN bundle config set no-cache 'true'
|
|
||||||
RUN bundle install --path=./vendor --without='test development'
|
|
||||||
# Vaciar la caché
|
|
||||||
RUN rm vendor/ruby/2.7.0/cache/*.gem
|
|
||||||
|
|
||||||
# Copiar el repositorio git
|
|
||||||
COPY --chown=app:www-data ./.git/ ./.git/
|
|
||||||
# Hacer un clon limpio del repositorio en lugar de copiar todos los
|
|
||||||
# archivos
|
|
||||||
RUN cd .. && git clone sutty checkout
|
|
||||||
RUN cd ../checkout && git checkout $BRANCH
|
|
||||||
|
|
||||||
WORKDIR /home/app/checkout
|
|
||||||
# Traer las gemas:
|
|
||||||
RUN rm -rf ./vendor
|
|
||||||
RUN mv ../sutty/vendor ./vendor
|
|
||||||
RUN mv ../sutty/.bundle ./.bundle
|
|
||||||
|
|
||||||
# Instalar secretos
|
|
||||||
COPY --chown=app:root ./config/credentials.yml.enc ./config/
|
|
||||||
|
|
||||||
# Eliminar la necesidad de un runtime JS en producción, porque los
|
|
||||||
# assets ya están pre-compilados.
|
|
||||||
RUN sed -re "/(sassc|uglifier|bootstrap|coffee-rails)/d" -i Gemfile
|
|
||||||
RUN bundle clean
|
|
||||||
RUN rm -rf ./node_modules ./tmp/cache ./.git ./test ./doc
|
|
||||||
# Eliminar archivos innecesarios
|
|
||||||
USER root
|
|
||||||
RUN apk add --no-cache findutils
|
|
||||||
RUN find /home/app/checkout/vendor/ruby/2.7.0 -maxdepth 3 -type d -name test -o -name spec -o -name rubocop | xargs -r rm -rf
|
|
||||||
|
|
||||||
# Contenedor final
|
|
||||||
FROM sutty/monit:latest
|
|
||||||
ENV RAILS_ENV production
|
|
||||||
|
|
||||||
# Pandoc
|
|
||||||
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories
|
|
||||||
|
|
||||||
# Instalar las dependencias, separamos la librería de base de datos para
|
# Instalar las dependencias, separamos la librería de base de datos para
|
||||||
# poder reutilizar este primer paso desde otros contenedores
|
# poder reutilizar este primer paso desde otros contenedores
|
||||||
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake ruby-irb
|
#
|
||||||
RUN apk add --no-cache postgresql-libs libssh2 file rsync git jpegoptim vips
|
|
||||||
RUN apk add --no-cache ffmpeg imagemagick pandoc tectonic oxipng jemalloc
|
|
||||||
RUN apk add --no-cache git-lfs openssh-client patch
|
|
||||||
|
|
||||||
# Chequear que la versión de ruby sea la correcta
|
|
||||||
RUN test "2.7.3" = `ruby -e 'puts RUBY_VERSION'`
|
|
||||||
|
|
||||||
# https://github.com/rubygems/rubygems/issues/2918
|
|
||||||
# https://gitlab.alpinelinux.org/alpine/aports/issues/10808
|
|
||||||
COPY ./rubygems-platform-musl.patch /tmp/
|
|
||||||
RUN apk add --no-cache patch && cd /usr/lib/ruby/2.7.0 && patch -Np 0 -i /tmp/rubygems-platform-musl.patch && apk del patch
|
|
||||||
|
|
||||||
# Necesitamos yarn para que Jekyll pueda generar los sitios
|
# Necesitamos yarn para que Jekyll pueda generar los sitios
|
||||||
# XXX: Eliminarlo cuando extraigamos la generación de sitios del proceso
|
# XXX: Eliminarlo cuando extraigamos la generación de sitios del proceso
|
||||||
# principal
|
# principal
|
||||||
RUN apk add --no-cache yarn
|
RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \
|
||||||
# Instalar foreman para poder correr los servicios
|
rsync git jpegoptim vips tectonic oxipng git-lfs openssh-client \
|
||||||
RUN gem install --no-document --no-user-install bundler foreman
|
yarn daemonize ruby-webrick
|
||||||
|
|
||||||
# Agregar el grupo del servidor web y la usuaria
|
RUN gem install --no-document --no-user-install foreman
|
||||||
RUN addgroup -g 82 -S www-data
|
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 adduser -s /bin/sh -G www-data -h /srv/http -D app
|
|
||||||
|
|
||||||
# Convertirse en app para instalar
|
VOLUME "/srv"
|
||||||
USER app
|
|
||||||
COPY --from=build --chown=app:www-data /home/app/checkout /srv/http
|
|
||||||
COPY --chown=app:www-data ./.git/ ./.git/
|
|
||||||
RUN rm -rf /srv/http/_sites /srv/http/_deploy
|
|
||||||
RUN ln -s data/_storage /srv/http/_storage
|
|
||||||
RUN ln -s data/_sites /srv/http/_sites
|
|
||||||
RUN ln -s data/_deploy /srv/http/_deploy
|
|
||||||
RUN ln -s data/_private /srv/http/_private
|
|
||||||
|
|
||||||
# Volver a root para cerrar la compilación
|
|
||||||
USER root
|
|
||||||
# Instalar la configuración de monit
|
|
||||||
RUN install -m 640 -o root -g root /srv/http/monit.conf /etc/monit.d/sutty.conf
|
|
||||||
RUN apk add --no-cache daemonize ruby-webrick
|
|
||||||
RUN install -m 755 /srv/http/entrypoint.sh /usr/local/bin/sutty
|
|
||||||
|
|
||||||
# Mantener estos directorios!
|
|
||||||
VOLUME "/srv/http/data"
|
|
||||||
|
|
||||||
# El puerto de puma
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
EXPOSE 9394
|
EXPOSE 9394
|
||||||
|
|
22
Gemfile
22
Gemfile
|
@ -11,13 +11,18 @@ gem 'dotenv-rails', require: 'dotenv/rails-now'
|
||||||
gem 'rails', '~> 6'
|
gem 'rails', '~> 6'
|
||||||
# Use Puma as the app server
|
# Use Puma as the app server
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
# See https://github.com/rails/execjs#readme for more supported runtimes
|
|
||||||
# gem 'therubyracer', platforms: :ruby
|
# Solo incluir las gemas cuando estemos en desarrollo o compilando los
|
||||||
# Use SCSS for stylesheets
|
# assets. No es necesario instalarlas en producción.
|
||||||
gem 'sassc-rails'
|
#
|
||||||
# Use Uglifier as compressor for JavaScript assets
|
# XXX: Supuestamente Rails ya soporta RAILS_GROUPS, pero Bundler no.
|
||||||
gem 'uglifier', '>= 1.3.0'
|
if ENV['RAILS_GROUPS']&.split(',')&.include? 'assets'
|
||||||
gem 'bootstrap', '~> 4'
|
gem 'sassc-rails'
|
||||||
|
gem 'uglifier', '>= 1.3.0'
|
||||||
|
gem 'bootstrap', '~> 4'
|
||||||
|
end
|
||||||
|
|
||||||
|
gem 'nokogiri'
|
||||||
|
|
||||||
# Turbolinks makes navigating your web application faster. Read more:
|
# Turbolinks makes navigating your web application faster. Read more:
|
||||||
# https://github.com/turbolinks/turbolinks
|
# https://github.com/turbolinks/turbolinks
|
||||||
|
@ -28,6 +33,7 @@ gem 'jbuilder', '~> 2.5'
|
||||||
# Use ActiveModel has_secure_password
|
# Use ActiveModel has_secure_password
|
||||||
gem 'bcrypt', '~> 3.1.7'
|
gem 'bcrypt', '~> 3.1.7'
|
||||||
gem 'blazer'
|
gem 'blazer'
|
||||||
|
gem 'chartkick'
|
||||||
gem 'commonmarker'
|
gem 'commonmarker'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-i18n'
|
gem 'devise-i18n'
|
||||||
|
@ -58,6 +64,7 @@ gem 'rails-i18n'
|
||||||
gem 'rails_warden'
|
gem 'rails_warden'
|
||||||
gem 'redis', require: %w[redis redis/connection/hiredis]
|
gem 'redis', require: %w[redis redis/connection/hiredis]
|
||||||
gem 'redis-rails'
|
gem 'redis-rails'
|
||||||
|
gem 'rollups', git: 'https://github.com/ankane/rollup.git', branch: 'master'
|
||||||
gem 'rubyzip'
|
gem 'rubyzip'
|
||||||
gem 'rugged'
|
gem 'rugged'
|
||||||
gem 'concurrent-ruby-ext'
|
gem 'concurrent-ruby-ext'
|
||||||
|
@ -67,6 +74,7 @@ gem 'terminal-table'
|
||||||
gem 'validates_hostname'
|
gem 'validates_hostname'
|
||||||
gem 'webpacker'
|
gem 'webpacker'
|
||||||
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
|
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
|
||||||
|
gem 'kaminari'
|
||||||
|
|
||||||
# database
|
# database
|
||||||
gem 'hairtrigger'
|
gem 'hairtrigger'
|
||||||
|
|
347
Gemfile.lock
347
Gemfile.lock
|
@ -6,6 +6,15 @@ GIT
|
||||||
rails (>= 3.0)
|
rails (>= 3.0)
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/ankane/rollup.git
|
||||||
|
revision: 0ab6c603450175eb1004f7793e86486943cb9f72
|
||||||
|
branch: master
|
||||||
|
specs:
|
||||||
|
rollups (0.1.3)
|
||||||
|
activesupport (>= 5.1)
|
||||||
|
groupdate (>= 5.2)
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/fauno/email_address
|
remote: https://github.com/fauno/email_address
|
||||||
revision: 536b51f7071b68a55140c0c1726b4cd401d1c04d
|
revision: 536b51f7071b68a55140c0c1726b4cd401d1c04d
|
||||||
|
@ -18,66 +27,66 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://gems.sutty.nl/
|
remote: https://gems.sutty.nl/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.3.2)
|
actioncable (6.1.4.1)
|
||||||
actionpack (= 6.1.3.2)
|
actionpack (= 6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.3.2)
|
actionmailbox (6.1.4.1)
|
||||||
actionpack (= 6.1.3.2)
|
actionpack (= 6.1.4.1)
|
||||||
activejob (= 6.1.3.2)
|
activejob (= 6.1.4.1)
|
||||||
activerecord (= 6.1.3.2)
|
activerecord (= 6.1.4.1)
|
||||||
activestorage (= 6.1.3.2)
|
activestorage (= 6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.3.2)
|
actionmailer (6.1.4.1)
|
||||||
actionpack (= 6.1.3.2)
|
actionpack (= 6.1.4.1)
|
||||||
actionview (= 6.1.3.2)
|
actionview (= 6.1.4.1)
|
||||||
activejob (= 6.1.3.2)
|
activejob (= 6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.3.2)
|
actionpack (6.1.4.1)
|
||||||
actionview (= 6.1.3.2)
|
actionview (= 6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.3.2)
|
actiontext (6.1.4.1)
|
||||||
actionpack (= 6.1.3.2)
|
actionpack (= 6.1.4.1)
|
||||||
activerecord (= 6.1.3.2)
|
activerecord (= 6.1.4.1)
|
||||||
activestorage (= 6.1.3.2)
|
activestorage (= 6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.3.2)
|
actionview (6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
activejob (6.1.3.2)
|
activejob (6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.3.2)
|
activemodel (6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
activerecord (6.1.3.2)
|
activerecord (6.1.4.1)
|
||||||
activemodel (= 6.1.3.2)
|
activemodel (= 6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
activestorage (6.1.3.2)
|
activestorage (6.1.4.1)
|
||||||
actionpack (= 6.1.3.2)
|
actionpack (= 6.1.4.1)
|
||||||
activejob (= 6.1.3.2)
|
activejob (= 6.1.4.1)
|
||||||
activerecord (= 6.1.3.2)
|
activerecord (= 6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
marcel (~> 1.0.0)
|
marcel (~> 1.0.0)
|
||||||
mini_mime (~> 1.0.2)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.3.2)
|
activesupport (6.1.4.1)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
zeitwerk (~> 2.3)
|
zeitwerk (~> 2.3)
|
||||||
addressable (2.7.0)
|
addressable (2.8.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
adhesiones-jekyll-theme (0.2.1)
|
adhesiones-jekyll-theme (0.2.1)
|
||||||
jekyll (~> 4.0)
|
jekyll (~> 4.0)
|
||||||
|
@ -89,13 +98,13 @@ GEM
|
||||||
jekyll-relative-urls (~> 0.0)
|
jekyll-relative-urls (~> 0.0)
|
||||||
jekyll-seo-tag (~> 2.1)
|
jekyll-seo-tag (~> 2.1)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
autoprefixer-rails (10.2.5.0)
|
autoprefixer-rails (10.3.3.0)
|
||||||
execjs (< 2.8.0)
|
execjs (~> 2)
|
||||||
bcrypt (3.1.16-x86_64-linux-musl)
|
bcrypt (3.1.16-x86_64-linux-musl)
|
||||||
bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
|
bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
|
||||||
benchmark-ips (2.8.4)
|
benchmark-ips (2.9.2)
|
||||||
bindex (0.8.1-x86_64-linux-musl)
|
bindex (0.8.1-x86_64-linux-musl)
|
||||||
blazer (2.4.2)
|
blazer (2.4.7)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
chartkick (>= 3.2)
|
chartkick (>= 3.2)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
|
@ -104,7 +113,7 @@ GEM
|
||||||
autoprefixer-rails (>= 9.1.0)
|
autoprefixer-rails (>= 9.1.0)
|
||||||
popper_js (>= 1.14.3, < 2)
|
popper_js (>= 1.14.3, < 2)
|
||||||
sassc-rails (>= 2.0.0)
|
sassc-rails (>= 2.0.0)
|
||||||
brakeman (5.0.1)
|
brakeman (5.1.2)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
capybara (2.18.0)
|
capybara (2.18.0)
|
||||||
addressable
|
addressable
|
||||||
|
@ -113,15 +122,15 @@ GEM
|
||||||
rack (>= 1.0.0)
|
rack (>= 1.0.0)
|
||||||
rack-test (>= 0.5.4)
|
rack-test (>= 0.5.4)
|
||||||
xpath (>= 2.0, < 4.0)
|
xpath (>= 2.0, < 4.0)
|
||||||
chartkick (4.0.4)
|
chartkick (4.1.2)
|
||||||
childprocess (3.0.0)
|
childprocess (4.1.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
colorator (1.1.0)
|
colorator (1.1.0)
|
||||||
commonmarker (0.21.2-x86_64-linux-musl)
|
commonmarker (0.21.2-x86_64-linux-musl)
|
||||||
ruby-enum (~> 0.5)
|
ruby-enum (~> 0.5)
|
||||||
concurrent-ruby (1.1.8)
|
concurrent-ruby (1.1.9)
|
||||||
concurrent-ruby-ext (1.1.8-x86_64-linux-musl)
|
concurrent-ruby-ext (1.1.9-x86_64-linux-musl)
|
||||||
concurrent-ruby (= 1.1.8)
|
concurrent-ruby (= 1.1.9)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
database_cleaner (2.0.1)
|
database_cleaner (2.0.1)
|
||||||
database_cleaner-active_record (~> 2.0.0)
|
database_cleaner-active_record (~> 2.0.0)
|
||||||
|
@ -129,8 +138,8 @@ GEM
|
||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
dead_end (1.1.7)
|
dead_end (3.1.0)
|
||||||
derailed_benchmarks (2.1.0)
|
derailed_benchmarks (2.1.1)
|
||||||
benchmark-ips (~> 2)
|
benchmark-ips (~> 2)
|
||||||
dead_end
|
dead_end
|
||||||
get_process_mem (~> 0)
|
get_process_mem (~> 0)
|
||||||
|
@ -148,8 +157,8 @@ GEM
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-i18n (1.9.4)
|
devise-i18n (1.10.1)
|
||||||
devise (>= 4.7.1)
|
devise (>= 4.8.0)
|
||||||
devise_invitable (2.0.5)
|
devise_invitable (2.0.5)
|
||||||
actionmailer (>= 5.0)
|
actionmailer (>= 5.0)
|
||||||
devise (>= 4.6)
|
devise (>= 4.6)
|
||||||
|
@ -157,8 +166,8 @@ GEM
|
||||||
dotenv-rails (2.7.6)
|
dotenv-rails (2.7.6)
|
||||||
dotenv (= 2.7.6)
|
dotenv (= 2.7.6)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
down (5.2.1)
|
down (5.2.4)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.8)
|
||||||
ed25519 (1.2.4-x86_64-linux-musl)
|
ed25519 (1.2.4-x86_64-linux-musl)
|
||||||
editorial-autogestiva-jekyll-theme (0.3.4)
|
editorial-autogestiva-jekyll-theme (0.3.4)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
|
@ -179,48 +188,50 @@ GEM
|
||||||
jekyll-unique-urls (~> 0)
|
jekyll-unique-urls (~> 0)
|
||||||
jekyll-write-and-commit-changes (~> 0)
|
jekyll-write-and-commit-changes (~> 0)
|
||||||
sutty-liquid (~> 0)
|
sutty-liquid (~> 0)
|
||||||
em-websocket (0.5.2)
|
em-websocket (0.5.3)
|
||||||
eventmachine (>= 0.12.9)
|
eventmachine (>= 0.12.9)
|
||||||
http_parser.rb (~> 0.6.0)
|
http_parser.rb (~> 0)
|
||||||
errbase (0.2.1)
|
errbase (0.2.1)
|
||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
eventmachine (1.2.7-x86_64-linux-musl)
|
eventmachine (1.2.7-x86_64-linux-musl)
|
||||||
exception_notification (4.4.3)
|
exception_notification (4.4.3)
|
||||||
actionmailer (>= 4.0, < 7)
|
actionmailer (>= 4.0, < 7)
|
||||||
activesupport (>= 4.0, < 7)
|
activesupport (>= 4.0, < 7)
|
||||||
execjs (2.7.0)
|
execjs (2.8.1)
|
||||||
factory_bot (6.2.0)
|
factory_bot (6.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.2.0)
|
factory_bot_rails (6.2.0)
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.2.0)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
fast_blank (1.0.0-x86_64-linux-musl)
|
fast_blank (1.0.1-x86_64-linux-musl)
|
||||||
fast_jsonparser (0.5.0-x86_64-linux-musl)
|
fast_jsonparser (0.5.0-x86_64-linux-musl)
|
||||||
ffi (1.15.0-x86_64-linux-musl)
|
ffi (1.15.4-x86_64-linux-musl)
|
||||||
flamegraph (0.9.5)
|
flamegraph (0.9.5)
|
||||||
forwardable-extended (2.6.0)
|
forwardable-extended (2.6.0)
|
||||||
friendly_id (5.4.2)
|
friendly_id (5.4.2)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
get_process_mem (0.2.7)
|
get_process_mem (0.2.7)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
globalid (0.4.2)
|
globalid (0.6.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 5.0)
|
||||||
|
groupdate (5.2.2)
|
||||||
|
activesupport (>= 5)
|
||||||
hairtrigger (0.2.24)
|
hairtrigger (0.2.24)
|
||||||
activerecord (>= 5.0, < 7)
|
activerecord (>= 5.0, < 7)
|
||||||
ruby2ruby (~> 2.4)
|
ruby2ruby (~> 2.4)
|
||||||
ruby_parser (~> 3.10)
|
ruby_parser (~> 3.10)
|
||||||
haml (5.2.1)
|
haml (5.2.2)
|
||||||
temple (>= 0.8.0)
|
temple (>= 0.8.0)
|
||||||
tilt
|
tilt
|
||||||
haml-lint (0.999.999)
|
haml-lint (0.999.999)
|
||||||
haml_lint
|
haml_lint
|
||||||
haml_lint (0.37.0)
|
haml_lint (0.37.1)
|
||||||
haml (>= 4.0, < 5.3)
|
haml (>= 4.0, < 5.3)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
rubocop (>= 0.50.0)
|
rubocop (>= 0.50.0)
|
||||||
sysexits (~> 1.1)
|
sysexits (~> 1.1)
|
||||||
hamlit (2.15.0-x86_64-linux-musl)
|
hamlit (2.15.1-x86_64-linux-musl)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
|
@ -232,24 +243,24 @@ GEM
|
||||||
heapy (0.2.0)
|
heapy (0.2.0)
|
||||||
thor
|
thor
|
||||||
hiredis (0.6.3-x86_64-linux-musl)
|
hiredis (0.6.3-x86_64-linux-musl)
|
||||||
http_parser.rb (0.6.0-x86_64-linux-musl)
|
http_parser.rb (0.8.0-x86_64-linux-musl)
|
||||||
httparty (0.18.1)
|
httparty (0.18.1)
|
||||||
mime-types (~> 3.0)
|
mime-types (~> 3.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
i18n (1.8.10)
|
i18n (1.8.11)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
icalendar (2.7.1)
|
icalendar (2.7.1)
|
||||||
ice_cube (~> 0.16)
|
ice_cube (~> 0.16)
|
||||||
ice_cube (0.16.3)
|
ice_cube (0.16.4)
|
||||||
image_processing (1.12.1)
|
image_processing (1.12.1)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
inline_svg (1.7.2)
|
inline_svg (1.7.2)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
jbuilder (2.11.2)
|
jbuilder (2.11.3)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
jekyll (4.2.0)
|
jekyll (4.2.1)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
colorator (~> 1.0)
|
colorator (~> 1.0)
|
||||||
em-websocket (~> 0.5)
|
em-websocket (~> 0.5)
|
||||||
|
@ -264,8 +275,8 @@ GEM
|
||||||
rouge (~> 3.0)
|
rouge (~> 3.0)
|
||||||
safe_yaml (~> 1.0)
|
safe_yaml (~> 1.0)
|
||||||
terminal-table (~> 2.0)
|
terminal-table (~> 2.0)
|
||||||
jekyll-commonmark (1.3.1)
|
jekyll-commonmark (1.3.2)
|
||||||
commonmarker (~> 0.14)
|
commonmarker (~> 0.14, < 0.22)
|
||||||
jekyll (>= 3.7, < 5.0)
|
jekyll (>= 3.7, < 5.0)
|
||||||
jekyll-data (1.1.2)
|
jekyll-data (1.1.2)
|
||||||
jekyll (>= 3.3, < 5.0.0)
|
jekyll (>= 3.3, < 5.0.0)
|
||||||
|
@ -276,21 +287,19 @@ GEM
|
||||||
jekyll (>= 3.7, < 5.0)
|
jekyll (>= 3.7, < 5.0)
|
||||||
jekyll-hardlinks (0.1.2)
|
jekyll-hardlinks (0.1.2)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
jekyll-ignore-layouts (0.1.0)
|
jekyll-ignore-layouts (0.1.2)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
jekyll-images (0.2.7)
|
jekyll-images (0.3.0)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
ruby-filemagic (~> 0.7)
|
ruby-filemagic (~> 0.7)
|
||||||
ruby-vips (~> 2)
|
ruby-vips (~> 2)
|
||||||
jekyll-include-cache (0.2.1)
|
jekyll-include-cache (0.2.1)
|
||||||
jekyll (>= 3.7, < 5.0)
|
jekyll (>= 3.7, < 5.0)
|
||||||
jekyll-linked-posts (0.2.0)
|
jekyll-linked-posts (0.4.2)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
jekyll-locales (0.1.12)
|
jekyll-locales (0.1.13)
|
||||||
jekyll-lunr (0.2.0)
|
jekyll-lunr (0.3.0)
|
||||||
loofah (~> 2.4)
|
loofah (~> 2.4)
|
||||||
jekyll-node-modules (0.1.0)
|
|
||||||
jekyll (~> 4)
|
|
||||||
jekyll-order (0.1.4)
|
jekyll-order (0.1.4)
|
||||||
jekyll-relative-urls (0.0.6)
|
jekyll-relative-urls (0.0.6)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
|
@ -298,9 +307,9 @@ GEM
|
||||||
sassc (> 2.0.1, < 3.0)
|
sassc (> 2.0.1, < 3.0)
|
||||||
jekyll-seo-tag (2.7.1)
|
jekyll-seo-tag (2.7.1)
|
||||||
jekyll (>= 3.8, < 5.0)
|
jekyll (>= 3.8, < 5.0)
|
||||||
jekyll-spree-client (0.1.14)
|
jekyll-spree-client (0.1.19)
|
||||||
fast_blank (~> 1)
|
fast_blank (~> 1)
|
||||||
spree-api-client (~> 0.2)
|
spree-api-client (>= 0.2.4)
|
||||||
jekyll-turbolinks (0.0.5)
|
jekyll-turbolinks (0.0.5)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
turbolinks-source (~> 5)
|
turbolinks-source (~> 5)
|
||||||
|
@ -308,9 +317,21 @@ GEM
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
jekyll-watch (2.2.1)
|
jekyll-watch (2.2.1)
|
||||||
listen (~> 3.0)
|
listen (~> 3.0)
|
||||||
jekyll-write-and-commit-changes (0.1.2)
|
jekyll-write-and-commit-changes (0.2.1)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
rugged (~> 1)
|
rugged (~> 1)
|
||||||
|
kaminari (1.2.1)
|
||||||
|
activesupport (>= 4.1.0)
|
||||||
|
kaminari-actionview (= 1.2.1)
|
||||||
|
kaminari-activerecord (= 1.2.1)
|
||||||
|
kaminari-core (= 1.2.1)
|
||||||
|
kaminari-actionview (1.2.1)
|
||||||
|
actionview
|
||||||
|
kaminari-core (= 1.2.1)
|
||||||
|
kaminari-activerecord (1.2.1)
|
||||||
|
activerecord
|
||||||
|
kaminari-core (= 1.2.1)
|
||||||
|
kaminari-core (1.2.1)
|
||||||
kramdown (2.3.1)
|
kramdown (2.3.1)
|
||||||
rexml
|
rexml
|
||||||
kramdown-parser-gfm (1.1.0)
|
kramdown-parser-gfm (1.1.0)
|
||||||
|
@ -326,46 +347,46 @@ GEM
|
||||||
ruby_dep (~> 1.2)
|
ruby_dep (~> 1.2)
|
||||||
loaf (0.10.0)
|
loaf (0.10.0)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
lockbox (0.6.4)
|
lockbox (0.6.6)
|
||||||
lograge (0.11.2)
|
lograge (0.11.2)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.9.1)
|
loofah (2.12.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
marcel (1.0.1)
|
marcel (1.0.2)
|
||||||
memory_profiler (1.0.0)
|
memory_profiler (1.0.0)
|
||||||
mercenary (0.4.0)
|
mercenary (0.4.0)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mime-types (3.3.1)
|
mime-types (3.4.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2021.0225)
|
mime-types-data (3.2021.1115)
|
||||||
mini_histogram (0.3.1)
|
mini_histogram (0.3.1)
|
||||||
mini_magick (4.11.0)
|
mini_magick (4.11.0)
|
||||||
mini_mime (1.0.3)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.5.1)
|
mini_portile2 (2.6.1)
|
||||||
minima (2.5.1)
|
minima (2.5.1)
|
||||||
jekyll (>= 3.5, < 5.0)
|
jekyll (>= 3.5, < 5.0)
|
||||||
jekyll-feed (~> 0.9)
|
jekyll-feed (~> 0.9)
|
||||||
jekyll-seo-tag (~> 2.1)
|
jekyll-seo-tag (~> 2.1)
|
||||||
minitest (5.14.4)
|
minitest (5.14.4)
|
||||||
mobility (1.1.2)
|
mobility (1.2.4)
|
||||||
i18n (>= 0.6.10, < 2)
|
i18n (>= 0.6.10, < 2)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
net-ssh (6.1.0)
|
net-ssh (6.1.0)
|
||||||
netaddr (2.0.4)
|
netaddr (2.0.5)
|
||||||
nio4r (2.5.7-x86_64-linux-musl)
|
nio4r (2.5.8-x86_64-linux-musl)
|
||||||
nokogiri (1.11.5-x86_64-linux-musl)
|
nokogiri (1.12.5-x86_64-linux-musl)
|
||||||
mini_portile2 (~> 2.5.0)
|
mini_portile2 (~> 2.6.1)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
parallel (1.20.1)
|
parallel (1.21.0)
|
||||||
parser (3.0.1.1)
|
parser (3.0.2.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
pathutil (0.16.2)
|
pathutil (0.16.2)
|
||||||
forwardable-extended (~> 2.6)
|
forwardable-extended (~> 2.6)
|
||||||
|
@ -374,27 +395,27 @@ GEM
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
popper_js (1.16.0)
|
popper_js (1.16.0)
|
||||||
prometheus_exporter (0.7.0)
|
prometheus_exporter (1.0.0)
|
||||||
webrick
|
webrick
|
||||||
pry (0.14.1)
|
pry (0.14.1)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (5.3.1-x86_64-linux-musl)
|
puma (5.5.2-x86_64-linux-musl)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
racc (1.5.2-x86_64-linux-musl)
|
racc (1.6.0-x86_64-linux-musl)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-mini-profiler (2.3.2)
|
rack-mini-profiler (2.3.3)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-proxy (0.6.5)
|
rack-proxy (0.7.0)
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
radios-comunitarias-jekyll-theme (0.1.4)
|
radios-comunitarias-jekyll-theme (0.1.5)
|
||||||
jekyll (~> 4.0)
|
jekyll (~> 4.0)
|
||||||
jekyll-data (~> 1.1)
|
jekyll-data (~> 1.1)
|
||||||
jekyll-feed (~> 0.9)
|
jekyll-feed (~> 0.9)
|
||||||
|
@ -405,65 +426,66 @@ GEM
|
||||||
jekyll-relative-urls (~> 0.0)
|
jekyll-relative-urls (~> 0.0)
|
||||||
jekyll-seo-tag (~> 2.1)
|
jekyll-seo-tag (~> 2.1)
|
||||||
jekyll-turbolinks (~> 0)
|
jekyll-turbolinks (~> 0)
|
||||||
rails (6.1.3.2)
|
rails (6.1.4.1)
|
||||||
actioncable (= 6.1.3.2)
|
actioncable (= 6.1.4.1)
|
||||||
actionmailbox (= 6.1.3.2)
|
actionmailbox (= 6.1.4.1)
|
||||||
actionmailer (= 6.1.3.2)
|
actionmailer (= 6.1.4.1)
|
||||||
actionpack (= 6.1.3.2)
|
actionpack (= 6.1.4.1)
|
||||||
actiontext (= 6.1.3.2)
|
actiontext (= 6.1.4.1)
|
||||||
actionview (= 6.1.3.2)
|
actionview (= 6.1.4.1)
|
||||||
activejob (= 6.1.3.2)
|
activejob (= 6.1.4.1)
|
||||||
activemodel (= 6.1.3.2)
|
activemodel (= 6.1.4.1)
|
||||||
activerecord (= 6.1.3.2)
|
activerecord (= 6.1.4.1)
|
||||||
activestorage (= 6.1.3.2)
|
activestorage (= 6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.3.2)
|
railties (= 6.1.4.1)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.3.0)
|
rails-html-sanitizer (1.4.2)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
rails-i18n (6.0.0)
|
rails-i18n (6.0.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
rails_warden (0.6.0)
|
rails_warden (0.6.0)
|
||||||
warden (>= 1.2.0)
|
warden (>= 1.2.0)
|
||||||
railties (6.1.3.2)
|
railties (6.1.4.1)
|
||||||
actionpack (= 6.1.3.2)
|
actionpack (= 6.1.4.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 6.1.4.1)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.13)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (13.0.3)
|
rake (13.0.6)
|
||||||
rb-fsevent (0.11.0)
|
rb-fsevent (0.11.0)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
recursero-jekyll-theme (0.1.3)
|
recursero-jekyll-theme (0.2.0)
|
||||||
jekyll (~> 4.0)
|
jekyll (~> 4)
|
||||||
|
jekyll-commonmark (~> 1.3)
|
||||||
jekyll-data (~> 1.1)
|
jekyll-data (~> 1.1)
|
||||||
jekyll-feed (~> 0.9)
|
jekyll-dotenv (>= 0.2)
|
||||||
|
jekyll-feed (~> 0.15)
|
||||||
|
jekyll-ignore-layouts (~> 0)
|
||||||
jekyll-images (~> 0.2)
|
jekyll-images (~> 0.2)
|
||||||
jekyll-include-cache (~> 0)
|
jekyll-include-cache (~> 0)
|
||||||
jekyll-linked-posts (~> 0.2)
|
jekyll-linked-posts (~> 0)
|
||||||
jekyll-locales (~> 0.1)
|
jekyll-locales (~> 0.1)
|
||||||
jekyll-lunr (~> 0.1)
|
jekyll-lunr (~> 0.1)
|
||||||
jekyll-node-modules (~> 0.1)
|
jekyll-order (~> 0)
|
||||||
jekyll-order (~> 0.1)
|
jekyll-relative-urls (~> 0)
|
||||||
jekyll-relative-urls (~> 0.0)
|
jekyll-seo-tag (~> 2)
|
||||||
jekyll-seo-tag (~> 2.1)
|
|
||||||
jekyll-turbolinks (~> 0)
|
|
||||||
jekyll-unique-urls (~> 0.1)
|
jekyll-unique-urls (~> 0.1)
|
||||||
sutty-archives (~> 2.2)
|
sutty-archives (~> 2.2)
|
||||||
sutty-liquid (~> 0.1)
|
sutty-liquid (~> 0)
|
||||||
redis (4.2.5)
|
redis (4.5.1)
|
||||||
redis-actionpack (5.2.0)
|
redis-actionpack (5.2.0)
|
||||||
actionpack (>= 5, < 7)
|
actionpack (>= 5, < 7)
|
||||||
redis-rack (>= 2.1.0, < 3)
|
redis-rack (>= 2.1.0, < 3)
|
||||||
redis-store (>= 1.1.0, < 2)
|
redis-store (>= 1.1.0, < 2)
|
||||||
redis-activesupport (5.2.0)
|
redis-activesupport (5.2.1)
|
||||||
activesupport (>= 3, < 7)
|
activesupport (>= 3, < 7)
|
||||||
redis-store (>= 1.3, < 2)
|
redis-store (>= 1.3, < 2)
|
||||||
redis-rack (2.1.3)
|
redis-rack (2.1.3)
|
||||||
|
@ -482,19 +504,19 @@ GEM
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rouge (3.26.0)
|
rouge (3.26.1)
|
||||||
rubocop (1.15.0)
|
rubocop (1.23.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.0.0.0)
|
parser (>= 3.0.0.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 1.5.0, < 2.0)
|
rubocop-ast (>= 1.12.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
rubocop-ast (1.5.0)
|
rubocop-ast (1.13.0)
|
||||||
parser (>= 3.0.1.1)
|
parser (>= 3.0.1.1)
|
||||||
rubocop-rails (2.10.1)
|
rubocop-rails (2.12.4)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
|
@ -502,17 +524,17 @@ GEM
|
||||||
i18n
|
i18n
|
||||||
ruby-filemagic (0.7.2-x86_64-linux-musl)
|
ruby-filemagic (0.7.2-x86_64-linux-musl)
|
||||||
ruby-progressbar (1.11.0)
|
ruby-progressbar (1.11.0)
|
||||||
ruby-statistics (2.1.3)
|
ruby-statistics (3.0.0)
|
||||||
ruby-vips (2.1.2)
|
ruby-vips (2.1.4)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
ruby2ruby (2.4.4)
|
ruby2ruby (2.4.4)
|
||||||
ruby_parser (~> 3.1)
|
ruby_parser (~> 3.1)
|
||||||
sexp_processor (~> 4.6)
|
sexp_processor (~> 4.6)
|
||||||
ruby_dep (1.5.0)
|
ruby_dep (1.5.0)
|
||||||
ruby_parser (3.15.1)
|
ruby_parser (3.18.1)
|
||||||
sexp_processor (~> 4.9)
|
sexp_processor (~> 4.16)
|
||||||
rubyzip (2.3.0)
|
rubyzip (2.3.2)
|
||||||
rugged (1.1.0-x86_64-linux-musl)
|
rugged (1.2.0-x86_64-linux-musl)
|
||||||
safe_yaml (1.0.6)
|
safe_yaml (1.0.6)
|
||||||
safely_block (0.3.0)
|
safely_block (0.3.0)
|
||||||
errbase (>= 0.1.1)
|
errbase (>= 0.1.1)
|
||||||
|
@ -524,11 +546,12 @@ GEM
|
||||||
sprockets (> 3.0)
|
sprockets (> 3.0)
|
||||||
sprockets-rails
|
sprockets-rails
|
||||||
tilt
|
tilt
|
||||||
selenium-webdriver (3.142.7)
|
selenium-webdriver (4.1.0)
|
||||||
childprocess (>= 0.5, < 4.0)
|
childprocess (>= 0.5, < 5.0)
|
||||||
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2)
|
rubyzip (>= 1.2.2)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sexp_processor (4.15.2)
|
sexp_processor (4.16.0)
|
||||||
share-to-fediverse-jekyll-theme (0.1.4)
|
share-to-fediverse-jekyll-theme (0.1.4)
|
||||||
jekyll (~> 4.0)
|
jekyll (~> 4.0)
|
||||||
jekyll-data (~> 1.1)
|
jekyll-data (~> 1.1)
|
||||||
|
@ -540,7 +563,7 @@ GEM
|
||||||
simpleidn (0.2.1)
|
simpleidn (0.2.1)
|
||||||
unf (~> 0.1.4)
|
unf (~> 0.1.4)
|
||||||
sourcemap (0.1.1)
|
sourcemap (0.1.1)
|
||||||
spree-api-client (0.2.1)
|
spree-api-client (0.2.4)
|
||||||
fast_blank (~> 1)
|
fast_blank (~> 1)
|
||||||
httparty (~> 0.18.0)
|
httparty (~> 0.18.0)
|
||||||
spring (2.1.1)
|
spring (2.1.1)
|
||||||
|
@ -550,9 +573,9 @@ GEM
|
||||||
sprockets (4.0.2)
|
sprockets (4.0.2)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.2.2)
|
sprockets-rails (3.4.1)
|
||||||
actionpack (>= 4.0)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 5.2)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sqlite3 (1.4.2-x86_64-linux-musl)
|
sqlite3 (1.4.2-x86_64-linux-musl)
|
||||||
stackprof (0.2.17-x86_64-linux-musl)
|
stackprof (0.2.17-x86_64-linux-musl)
|
||||||
|
@ -577,14 +600,14 @@ GEM
|
||||||
jekyll-include-cache (~> 0)
|
jekyll-include-cache (~> 0)
|
||||||
jekyll-relative-urls (~> 0.0)
|
jekyll-relative-urls (~> 0.0)
|
||||||
jekyll-seo-tag (~> 2.1)
|
jekyll-seo-tag (~> 2.1)
|
||||||
sutty-liquid (0.7.3)
|
sutty-liquid (0.7.4)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
sutty-minima (2.5.0)
|
sutty-minima (2.5.0)
|
||||||
jekyll (>= 3.5, < 5.0)
|
jekyll (>= 3.5, < 5.0)
|
||||||
jekyll-feed (~> 0.9)
|
jekyll-feed (~> 0.9)
|
||||||
jekyll-seo-tag (~> 2.1)
|
jekyll-seo-tag (~> 2.1)
|
||||||
symbol-fstring (1.0.0-x86_64-linux-musl)
|
symbol-fstring (1.0.2-x86_64-linux-musl)
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.8.2)
|
temple (0.8.2)
|
||||||
terminal-table (2.0.0)
|
terminal-table (2.0.0)
|
||||||
|
@ -601,30 +624,30 @@ GEM
|
||||||
execjs (>= 0.3.0, < 3)
|
execjs (>= 0.3.0, < 3)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.7-x86_64-linux-musl)
|
unf_ext (0.0.8-x86_64-linux-musl)
|
||||||
unicode-display_width (1.7.0)
|
unicode-display_width (1.8.0)
|
||||||
validates_hostname (1.0.11)
|
validates_hostname (1.0.11)
|
||||||
activerecord (>= 3.0)
|
activerecord (>= 3.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.1.0)
|
web-console (4.2.0)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 6.0.0)
|
||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
webpacker (5.4.0)
|
webpacker (5.4.3)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
webrick (1.7.0)
|
webrick (1.7.0)
|
||||||
websocket-driver (0.7.3-x86_64-linux-musl)
|
websocket-driver (0.7.5-x86_64-linux-musl)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.4.2)
|
zeitwerk (2.5.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
@ -638,6 +661,7 @@ DEPENDENCIES
|
||||||
bootstrap (~> 4)
|
bootstrap (~> 4)
|
||||||
brakeman
|
brakeman
|
||||||
capybara (~> 2.13)
|
capybara (~> 2.13)
|
||||||
|
chartkick
|
||||||
commonmarker
|
commonmarker
|
||||||
concurrent-ruby-ext
|
concurrent-ruby-ext
|
||||||
database_cleaner
|
database_cleaner
|
||||||
|
@ -670,6 +694,7 @@ DEPENDENCIES
|
||||||
jekyll-data!
|
jekyll-data!
|
||||||
jekyll-images
|
jekyll-images
|
||||||
jekyll-include-cache
|
jekyll-include-cache
|
||||||
|
kaminari
|
||||||
letter_opener
|
letter_opener
|
||||||
listen (>= 3.0.5, < 3.2)
|
listen (>= 3.0.5, < 3.2)
|
||||||
loaf
|
loaf
|
||||||
|
@ -680,6 +705,7 @@ DEPENDENCIES
|
||||||
minima
|
minima
|
||||||
mobility
|
mobility
|
||||||
net-ssh
|
net-ssh
|
||||||
|
nokogiri
|
||||||
pg
|
pg
|
||||||
pg_search
|
pg_search
|
||||||
prometheus_exporter
|
prometheus_exporter
|
||||||
|
@ -695,6 +721,7 @@ DEPENDENCIES
|
||||||
recursero-jekyll-theme
|
recursero-jekyll-theme
|
||||||
redis
|
redis
|
||||||
redis-rails
|
redis-rails
|
||||||
|
rollups!
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubyzip
|
rubyzip
|
||||||
rugged
|
rugged
|
||||||
|
|
202
Makefile
202
Makefile
|
@ -1,145 +1,137 @@
|
||||||
.SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
# Incluir las variables de entorno
|
.DEFAULT_GOAL := help
|
||||||
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
|
|
||||||
root_dir := $(patsubst %/,%,$(dir $(mkfile_path)))
|
|
||||||
include $(root_dir)/.env
|
|
||||||
|
|
||||||
delegate := athshe
|
# Copiar el archivo de configuración y avisar cuando hay que
|
||||||
|
# actualizarlo.
|
||||||
|
.env: .env.example
|
||||||
|
@test -f $@ || cp -v $< $@
|
||||||
|
@test -f $@ && echo "Revisa $@ para actualizarlo con respecto a $<"
|
||||||
|
@test -f $@ && diff -auN --color $@ $<
|
||||||
|
|
||||||
assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type f)
|
include .env
|
||||||
|
|
||||||
alpine_version := 3.13
|
export
|
||||||
hain ?= ../haini.sh/haini.sh
|
|
||||||
|
|
||||||
env ?= staging
|
# XXX: El espacio antes del comentario cuenta como espacio
|
||||||
|
args ?=## Argumentos para Hain
|
||||||
|
commit ?= origin/rails## Commit desde el que actualizar
|
||||||
|
env ?= staging## Entorno del nodo delegado
|
||||||
|
sutty ?= $(SUTTY)## Dirección local
|
||||||
|
delegate ?= $(DELEGATE)## Cambia el nodo delegado
|
||||||
|
hain ?= ENV_FILE=.env $(HAINISH)## Ubicación de Hainish
|
||||||
|
|
||||||
|
# El nodo delegado tiene dos entornos, production y staging.
|
||||||
|
# Dependiendo del entorno que elijamos, se van a generar los assets y el
|
||||||
|
# contenedor y subirse a un servidor u otro. No utilizamos CI/CD (aún).
|
||||||
|
#
|
||||||
|
# Production es el entorno de panel.sutty.nl
|
||||||
ifeq ($(env),production)
|
ifeq ($(env),production)
|
||||||
container ?= sutty
|
container ?= panel
|
||||||
|
## TODO: Cambiar a otra cosa
|
||||||
branch ?= rails
|
branch ?= rails
|
||||||
public ?= public
|
public ?= public
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
# Staging es el entorno de panel.staging.sutty.nl
|
||||||
ifeq ($(env),staging)
|
ifeq ($(env),staging)
|
||||||
container := staging
|
container := staging
|
||||||
branch := staging
|
branch := staging
|
||||||
public := staging
|
public := staging
|
||||||
endif
|
endif
|
||||||
|
|
||||||
export
|
help: always ## Ayuda
|
||||||
|
@echo -e "Sutty\n" | sed -re "s/^.*/\x1B[38;5;197m&\x1B[0m/"
|
||||||
|
@echo -e "Servidor: https://panel.$(SUTTY_WITH_PORT)/\n"
|
||||||
|
@echo -e "Uso: make TAREA args=\"ARGUMENTOS\"\n"
|
||||||
|
@echo -e "Tareas:\n"
|
||||||
|
@grep -E "^[a-z\-]+:.*##" Makefile | sed -re "s/(.*):.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
|
||||||
|
@echo -e "\nArgumentos:\n"
|
||||||
|
@grep -E "^[a-z\-]+ \?=.*##" Makefile | sed -re "s/(.*) \?=.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
|
||||||
|
|
||||||
public/packs/manifest.json.br: $(assets)
|
assets: public/packs/manifest.json.br ## Compilar los assets
|
||||||
$(hain) 'cd /Sutty/sutty; PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean'
|
|
||||||
|
|
||||||
assets: public/packs/manifest.json.br
|
test: always ## Ejecutar los tests
|
||||||
|
$(MAKE) rake args="test RAILS_ENV=test $(args)"
|
||||||
|
|
||||||
tests := $(shell find test/ -name "*_test.rb")
|
postgresql: /etc/hosts ## Iniciar la base de datos
|
||||||
$(tests): always
|
|
||||||
$(hain) 'cd /Sutty/sutty; bundle exec rake test TEST="$@" RAILS_ENV=test'
|
|
||||||
|
|
||||||
test: always
|
|
||||||
$(hain) 'cd /Sutty/sutty; RAILS_ENV=test bundle exec rake test'
|
|
||||||
|
|
||||||
postgresql: /etc/hosts
|
|
||||||
pgrep postgres >/dev/null || $(hain) postgresql
|
pgrep postgres >/dev/null || $(hain) postgresql
|
||||||
|
|
||||||
serve: /etc/hosts postgresql
|
serve-js: /etc/hosts node_modules ## Iniciar el servidor de desarrollo de Javascript
|
||||||
|
$(hain) 'bundle exec ./bin/webpack-dev-server'
|
||||||
|
|
||||||
|
serve: /etc/hosts postgresql Gemfile.lock ## Iniciar el servidor de desarrollo de Rails
|
||||||
$(MAKE) rails args=server
|
$(MAKE) rails args=server
|
||||||
|
|
||||||
# make rails args="db:migrate"
|
rails: ## Corre rails dentro del entorno de desarrollo (pasar argumentos con args=).
|
||||||
rails:
|
|
||||||
$(MAKE) bundle args="exec rails $(args)"
|
$(MAKE) bundle args="exec rails $(args)"
|
||||||
|
|
||||||
rake:
|
rake: ## Corre rake dentro del entorno de desarrollo (pasar argumentos con args=).
|
||||||
$(MAKE) bundle args="exec rake $(args)"
|
$(MAKE) bundle args="exec rake $(args)"
|
||||||
|
|
||||||
bundle:
|
bundle: ## Corre bundle dentro del entorno de desarrollo (pasar argumentos con args=).
|
||||||
$(hain) 'cd /Sutty/sutty; bundle $(args)'
|
$(hain) 'bundle $(args)'
|
||||||
|
|
||||||
yarn:
|
psql := psql -h $(PG_HOST) -U $(PG_USER) -p $(PG_PORT) -d sutty
|
||||||
|
copy-table:
|
||||||
|
test -n "$(table)"
|
||||||
|
echo "truncate $(table) $(cascade);" | $(psql)
|
||||||
|
ssh $(delegate) docker exec postgresql pg_dump -U sutty -d sutty -t $(table) | $(psql)
|
||||||
|
|
||||||
|
psql:
|
||||||
|
$(psql)
|
||||||
|
|
||||||
|
rubocop: ## Yutea el código que está por ser commiteado
|
||||||
|
git status --porcelain \
|
||||||
|
| grep -E "^(A|M)" \
|
||||||
|
| sed "s/^...//" \
|
||||||
|
| grep ".rb$$" \
|
||||||
|
| ../haini.sh/haini.sh "xargs -r ./bin/rubocop --auto-correct"
|
||||||
|
|
||||||
|
audit: ## Encuentra dependencias con vulnerabilidades
|
||||||
|
$(hain) 'gem install bundler-audit'
|
||||||
|
$(hain) 'bundle audit --update'
|
||||||
|
|
||||||
|
brakeman: ## Busca posibles vulnerabilidades en Sutty
|
||||||
|
$(MAKE) bundle args='exec brakeman'
|
||||||
|
|
||||||
|
yarn: ## Tareas de yarn
|
||||||
$(hain) 'yarn $(args)'
|
$(hain) 'yarn $(args)'
|
||||||
|
|
||||||
# Servir JS con el dev server.
|
clean: ## Limpieza
|
||||||
# Esto acelera la compilación del javascript, tiene que correrse por separado
|
rm -rf _sites/test-* _deploy/test-* log/*.log tmp/cache tmp/letter_opener tmp/miniprofiler tmp/storage
|
||||||
# de serve.
|
|
||||||
serve-js: /etc/hosts
|
|
||||||
$(hain) 'cd /Sutty/sutty; bundle exec ./bin/webpack-dev-server'
|
|
||||||
|
|
||||||
# Limpiar los archivos de testeo
|
build: Gemfile.lock ## Generar la imagen Docker
|
||||||
clean:
|
|
||||||
rm -rf _sites/test-* _deploy/test-*
|
|
||||||
|
|
||||||
# Generar la imagen Docker
|
|
||||||
build: assets
|
|
||||||
time docker build --build-arg="BRANCH=$(branch)" --build-arg="RAILS_MASTER_KEY=`cat config/master.key`" -t sutty/$(container) .
|
time docker build --build-arg="BRANCH=$(branch)" --build-arg="RAILS_MASTER_KEY=`cat config/master.key`" -t sutty/$(container) .
|
||||||
docker tag sutty/$(container):latest sutty:keep
|
docker tag sutty/$(container):latest sutty:keep
|
||||||
|
@echo -e "\a"
|
||||||
|
|
||||||
save:
|
save: ## Subir la imagen Docker al nodo delegado
|
||||||
time docker save sutty/$(container):latest | ssh root@$(delegate).sutty.nl docker load
|
time docker save sutty/$(container):latest | ssh root@$(delegate) docker load
|
||||||
date +%F | xargs -I {} git tag -f $(container)-{}
|
date +%F | xargs -I {} git tag -f $(container)-{}
|
||||||
@echo -e "\a"
|
@echo -e "\a"
|
||||||
|
|
||||||
# proyectos.
|
ota-js: assets ## Actualizar Javascript en el nodo delegado
|
||||||
../gems/:
|
rsync -avi --delete-after --chown 1000:82 public/ root@$(delegate):/srv/sutty/srv/http/data/_$(public)/
|
||||||
mkdir -p $@
|
ssh root@$(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2"
|
||||||
|
|
||||||
# Crear el directorio donde se almacenan las gemas binarias
|
ota: ## Actualizar Rails en el nodo delegado
|
||||||
# TODO: Mover a un proyecto propio, porque lo utilizamos en todos los
|
ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl pull ; true
|
||||||
gem_dir := $(shell readlink -f ../gems)
|
ssh $(delegate) chown -R 1000:82 /srv/sutty/srv/http/panel.sutty.nl
|
||||||
gem_cache_dir := $(gem_dir)/cache
|
ssh $(delegate) docker exec $(container) rails reload
|
||||||
gem_binary_dir := $(gem_dir)/$(alpine_version)
|
|
||||||
ifeq ($(MAKECMDGOALS),build-gems)
|
|
||||||
gems := $(shell bundle show --paths | xargs -I {} sh -c 'find {}/ext/ -name extconf.rb &>/dev/null && basename {}')
|
|
||||||
gems := $(patsubst %-x86_64-linux,%,$(gems))
|
|
||||||
gems := $(patsubst %,$(gem_cache_dir)/%.gem,$(gems))
|
|
||||||
gems_musl := $(patsubst $(gem_cache_dir)/%.gem,$(gem_binary_dir)/%-x86_64-linux-musl.gem,$(gems))
|
|
||||||
endif
|
|
||||||
|
|
||||||
$(gem_binary_dir)/%-x86_64-linux-musl.gem:
|
# Todos los archivos de assets. Si alguno cambia, se van a recompilar
|
||||||
@docker run \
|
# los assets que luego se suben al nodo delegado.
|
||||||
-v $(gem_dir):/srv/gems \
|
assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type f)
|
||||||
-v `readlink -f ~/.ccache`:/home/builder/.ccache \
|
public/packs/manifest.json.br: $(assets)
|
||||||
-e HTTP_BASIC_USER=$(HTTP_BASIC_USER) \
|
$(hain) 'PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean'
|
||||||
-e HTTP_BASIC_PASSWORD=$(HTTP_BASIC_PASSWORD) \
|
|
||||||
-e GEM=`echo $(notdir $*) | sed -re "s/-[^-]+$$//"` \
|
|
||||||
-e VERSION=`echo $(notdir $*) | sed -re "s/.*-([^-]+)$$/\1/"` \
|
|
||||||
-e JOBS=2 \
|
|
||||||
--rm -it \
|
|
||||||
sutty/gem-compiler:latest || echo "No se pudo compilar $*"
|
|
||||||
|
|
||||||
# Compilar todas las gemas binarias y subirlas a gems.sutty.nl para que
|
# Correr un test en particular por ejemplo
|
||||||
# al crear el contenedor no tengamos que compilarlas cada vez
|
# `make test/models/usuarie_test.rb`
|
||||||
build-gems: $(gems_musl)
|
tests := $(shell find test/ -name "*_test.rb")
|
||||||
|
$(tests): always
|
||||||
cached_gems = $(wildcard $(gem_dir)/cache/*.gem)
|
$(MAKE) test args="TEST=$@"
|
||||||
rebuild_gems = $(patsubst $(gem_dir)/cache/%.gem,$(gem_dir)/$(alpine_version)/%-x86_64-linux-musl.gem,$(cached_gems))
|
|
||||||
rebuild-gems: $(rebuild_gems)
|
|
||||||
|
|
||||||
dirs := $(patsubst %,root/%,data sites deploy public)
|
|
||||||
|
|
||||||
$(dirs):
|
|
||||||
mkdir -p $@
|
|
||||||
|
|
||||||
ota: assets
|
|
||||||
sudo chgrp -R 82 public/
|
|
||||||
rsync -avi --delete-after public/ $(delegate):/srv/sutty/srv/http/data/_$(public)/
|
|
||||||
ssh $(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2"
|
|
||||||
|
|
||||||
# Hotfixes
|
|
||||||
#
|
|
||||||
# TODO: Reemplazar esto por git pull en el contenedor
|
|
||||||
commit ?= origin/rails
|
|
||||||
ota-rb:
|
|
||||||
umask 022; git format-patch $(commit)
|
|
||||||
scp ./0*.patch $(delegate):/tmp/
|
|
||||||
ssh $(delegate) mkdir -p /tmp/patches-$(commit)/
|
|
||||||
scp ./0*.patch $(delegate):/tmp/patches-$(commit)/
|
|
||||||
scp ./ota.sh $(delegate):/tmp/
|
|
||||||
ssh $(delegate) docker cp /tmp/patches-$(commit) $(container):/tmp/
|
|
||||||
ssh $(delegate) docker cp /tmp/ota.sh $(container):/usr/local/bin/ota
|
|
||||||
ssh $(delegate) docker exec $(container) apk add --no-cache patch
|
|
||||||
ssh $(delegate) docker exec $(container) ota $(commit)
|
|
||||||
rm ./0*.patch
|
|
||||||
|
|
||||||
|
# Agrega las direcciones locales al sistema
|
||||||
/etc/hosts: always
|
/etc/hosts: always
|
||||||
@echo "Chequeando si es necesario agregar el dominio local $(SUTTY)"
|
@echo "Chequeando si es necesario agregar el dominio local $(SUTTY)"
|
||||||
@grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@
|
@grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@
|
||||||
|
@ -147,4 +139,12 @@ ota-rb:
|
||||||
@grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 panel.$(SUTTY)" | sudo tee -a $@
|
@grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 panel.$(SUTTY)" | sudo tee -a $@
|
||||||
@grep -q " postgresql.$(SUTTY)$$" $@ || echo -e "127.0.0.1 postgresql.$(SUTTY)\n::1 postgresql.$(SUTTY)" | sudo tee -a $@
|
@grep -q " postgresql.$(SUTTY)$$" $@ || echo -e "127.0.0.1 postgresql.$(SUTTY)\n::1 postgresql.$(SUTTY)" | sudo tee -a $@
|
||||||
|
|
||||||
|
# Instala las dependencias de Javascript
|
||||||
|
node_modules: package.json
|
||||||
|
$(MAKE) yarn
|
||||||
|
|
||||||
|
# Instala las dependencias de Rails
|
||||||
|
Gemfile.lock: Gemfile
|
||||||
|
$(MAKE) bundle args=install
|
||||||
|
|
||||||
.PHONY: always
|
.PHONY: always
|
||||||
|
|
21
README.md
21
README.md
|
@ -15,6 +15,17 @@ Este repositorio es la plataforma _Ruby on Rails_ para alojar el
|
||||||
|
|
||||||
Para más información visita el [sitio de Sutty](https://sutty.nl/).
|
Para más información visita el [sitio de Sutty](https://sutty.nl/).
|
||||||
|
|
||||||
|
### Desarrollar
|
||||||
|
|
||||||
|
Todas las tareas se gestionan con `make`, por favor instala GNU Make
|
||||||
|
antes de comenzar.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make help
|
||||||
|
```
|
||||||
|
|
||||||
|
[Leer la documentación](https://docs.sutty.nl/)
|
||||||
|
|
||||||
## English
|
## English
|
||||||
|
|
||||||
Sutty is a platform for hosting safer, faster and more resilient
|
Sutty is a platform for hosting safer, faster and more resilient
|
||||||
|
@ -25,3 +36,13 @@ This repository is the Ruby on Rails platform that hosts the
|
||||||
self-managed [panel](https://panel.sutty.nl/).
|
self-managed [panel](https://panel.sutty.nl/).
|
||||||
|
|
||||||
For more information, visit [Sutty's website](https://sutty.nl/en/).
|
For more information, visit [Sutty's website](https://sutty.nl/en/).
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Every task is run via `make`, please install GNU Make before developing.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make help
|
||||||
|
```
|
||||||
|
|
||||||
|
[Read the documentation](https://docs.sutty.nl/en/)
|
||||||
|
|
|
@ -2,6 +2,13 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
*, *::before, *::after { box-sizing: inherit; }
|
*, *::before, *::after { box-sizing: inherit; }
|
||||||
|
|
||||||
|
// Arreglo temporal para que las cosas sean legibles en modo oscuro
|
||||||
|
--foreground: black;
|
||||||
|
--background: white;
|
||||||
|
--color: #f206f9;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6, p, li {
|
h1, h2, h3, h4, h5, h6, p, li {
|
||||||
min-height: 1.5rem;
|
min-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
@ -60,10 +67,18 @@
|
||||||
|
|
||||||
.editor-content {
|
.editor-content {
|
||||||
min-height: 480px;
|
min-height: 480px;
|
||||||
p, h1, h2, h3, h4, h5, h6, ul, li, figcaption { outline: #ccc solid thin; }
|
p, h1, h2, h3, h4, h5, h6, ul, li, blockquote, figcaption { outline: #ccc solid thin; }
|
||||||
|
blockquote {
|
||||||
|
border-left: #555 solid .25em;
|
||||||
|
padding: .75em;
|
||||||
|
}
|
||||||
strong, em, del, u, sub, sup, small { background: #0002; }
|
strong, em, del, u, sub, sup, small { background: #0002; }
|
||||||
a { background: #13fefe50; }
|
a { background: #13fefe50; }
|
||||||
[data-editor-selected] { outline: #f206f9 solid thick; }
|
[data-editor-selected] { outline: #f206f9 solid thick; }
|
||||||
|
p[data-multimedia-inner] {
|
||||||
|
// Ignorar clicks en el párrafo placeholder
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*[data-editor-loading] {
|
*[data-editor-loading] {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActiveStorage
|
||||||
|
# Modifica la creación de un blob antes de subir el archivo para que
|
||||||
|
# incluya el JekyllService adecuado.
|
||||||
|
module DirectUploadsControllerDecorator
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
def create
|
||||||
|
blob = ActiveStorage::Blob.create_before_direct_upload!(service_name: session[:service_name], **blob_args)
|
||||||
|
render json: direct_upload_json(blob)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Normalizar los caracteres unicode en los nombres de archivos
|
||||||
|
# para que puedan propagarse correctamente a través de todo el
|
||||||
|
# stack.
|
||||||
|
def blob_args
|
||||||
|
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys.tap do |ba|
|
||||||
|
ba[:filename] = ba[:filename].unicode_normalize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveStorage::DirectUploadsController.include ActiveStorage::DirectUploadsControllerDecorator
|
33
app/controllers/active_storage/disk_controller_decorator.rb
Normal file
33
app/controllers/active_storage/disk_controller_decorator.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActiveStorage
|
||||||
|
# Modificar {DiskController} para poder asociar el blob a un sitio
|
||||||
|
module DiskControllerDecorator
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Asociar el archivo subido al sitio correspondiente. Cada sitio
|
||||||
|
# tiene su propio servicio de subida de archivos.
|
||||||
|
def update
|
||||||
|
if (token = decode_verified_token)
|
||||||
|
if acceptable_content?(token)
|
||||||
|
named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.find_by_key token[:key]
|
||||||
|
site = Site.find_by_name token[:service_name]
|
||||||
|
|
||||||
|
site.static_files.attach(blob)
|
||||||
|
else
|
||||||
|
head :unprocessable_entity
|
||||||
|
end
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
rescue ActiveStorage::IntegrityError
|
||||||
|
head :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveStorage::DiskController.include ActiveStorage::DiskControllerDecorator
|
|
@ -6,19 +6,22 @@ module Api
|
||||||
class CspReportsController < BaseController
|
class CspReportsController < BaseController
|
||||||
skip_forgery_protection
|
skip_forgery_protection
|
||||||
|
|
||||||
|
# No queremos indicar que algo salió mal
|
||||||
|
rescue_from ActionController::ParameterMissing, with: :csp_report_created
|
||||||
|
|
||||||
# Crea un reporte de CSP intercambiando los guiones medios por
|
# Crea un reporte de CSP intercambiando los guiones medios por
|
||||||
# bajos
|
# bajos
|
||||||
#
|
#
|
||||||
# TODO: Aplicar rate_limit
|
# TODO: Aplicar rate_limit
|
||||||
def create
|
def create
|
||||||
csp = CspReport.new(csp_report_params.to_h.map do |k, v|
|
csp = CspReport.new(csp_report_params.to_h.transform_keys do |k|
|
||||||
[k.tr('-', '_'), v]
|
k.tr('-', '_')
|
||||||
end.to_h)
|
end)
|
||||||
|
|
||||||
csp.id = SecureRandom.uuid
|
csp.id = SecureRandom.uuid
|
||||||
csp.save
|
csp.save
|
||||||
|
|
||||||
render json: {}, status: :created
|
csp_report_created
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -39,6 +42,10 @@ module Api
|
||||||
:'column-number',
|
:'column-number',
|
||||||
:'source-file')
|
:'source-file')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def csp_report_created
|
||||||
|
render json: {}, status: :created
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,7 @@ class PostsController < ApplicationController
|
||||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||||
|
|
||||||
before_action :authenticate_usuarie!
|
before_action :authenticate_usuarie!
|
||||||
|
before_action :service_for_direct_upload, only: %i[new edit]
|
||||||
|
|
||||||
# TODO: Traer los comunes desde ApplicationController
|
# TODO: Traer los comunes desde ApplicationController
|
||||||
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path
|
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path
|
||||||
|
@ -22,21 +23,21 @@ class PostsController < ApplicationController
|
||||||
|
|
||||||
# XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es
|
# XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es
|
||||||
# más simple saber si hubo cambios.
|
# más simple saber si hubo cambios.
|
||||||
if stale?([current_usuarie, site, filter_params])
|
return unless stale?([current_usuarie, site, filter_params])
|
||||||
# Todos los artículos de este sitio para el idioma actual
|
|
||||||
@posts = site.indexed_posts.where(locale: locale)
|
|
||||||
# De este tipo
|
|
||||||
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
|
|
||||||
# Que estén dentro de la categoría
|
|
||||||
@posts = @posts.in_category(filter_params[:category]) if filter_params[:category]
|
|
||||||
# Aplicar los parámetros de búsqueda
|
|
||||||
@posts = @posts.search(locale, filter_params[:q]) if filter_params[:q].present?
|
|
||||||
# A los que este usuarie tiene acceso
|
|
||||||
@posts = PostPolicy::Scope.new(current_usuarie, @posts).resolve
|
|
||||||
|
|
||||||
# Filtrar los posts que les invitades no pueden ver
|
# Todos los artículos de este sitio para el idioma actual
|
||||||
@usuarie = site.usuarie? current_usuarie
|
@posts = site.indexed_posts.where(locale: locale)
|
||||||
end
|
# De este tipo
|
||||||
|
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
|
||||||
|
# Que estén dentro de la categoría
|
||||||
|
@posts = @posts.in_category(filter_params[:category]) if filter_params[:category]
|
||||||
|
# Aplicar los parámetros de búsqueda
|
||||||
|
@posts = @posts.search(locale, filter_params[:q]) if filter_params[:q].present?
|
||||||
|
# A los que este usuarie tiene acceso
|
||||||
|
@posts = PostPolicy::Scope.new(current_usuarie, @posts).resolve
|
||||||
|
|
||||||
|
# Filtrar los posts que les invitades no pueden ver
|
||||||
|
@usuarie = site.usuarie? current_usuarie
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -54,7 +55,7 @@ class PostsController < ApplicationController
|
||||||
|
|
||||||
def new
|
def new
|
||||||
authorize Post
|
authorize Post
|
||||||
@post = site.posts.build(lang: locale, layout: params[:layout])
|
@post = site.posts(lang: locale).build(layout: params[:layout])
|
||||||
|
|
||||||
breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), ''
|
breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), ''
|
||||||
end
|
end
|
||||||
|
@ -154,7 +155,9 @@ class PostsController < ApplicationController
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def filter_params
|
def filter_params
|
||||||
@filter_params ||= params.permit(:q, :category, :layout).to_h.select { |_, v| v.present? }
|
@filter_params ||= params.permit(:q, :category, :layout).to_hash.select do |_, v|
|
||||||
|
v.present?
|
||||||
|
end.transform_keys(&:to_sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
def site
|
def site
|
||||||
|
@ -164,4 +167,9 @@ class PostsController < ApplicationController
|
||||||
def post
|
def post
|
||||||
@post ||= site.posts(lang: locale).find(params[:post_id] || params[:id])
|
@post ||= site.posts(lang: locale).find(params[:post_id] || params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Recuerda el nombre del servicio de subida de archivos
|
||||||
|
def service_for_direct_upload
|
||||||
|
session[:service_name] = site.name.to_sym
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,16 +3,166 @@
|
||||||
# Estadísticas del sitio
|
# Estadísticas del sitio
|
||||||
class StatsController < ApplicationController
|
class StatsController < ApplicationController
|
||||||
include Pundit
|
include Pundit
|
||||||
|
include ActionView::Helpers::DateHelper
|
||||||
|
|
||||||
before_action :authenticate_usuarie!
|
before_action :authenticate_usuarie!
|
||||||
|
before_action :authorize_stats
|
||||||
|
|
||||||
|
EXTRA_OPTIONS = {
|
||||||
|
builds: {},
|
||||||
|
space_used: { bytes: true },
|
||||||
|
build_time: {}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# XXX: Permitir a Chart.js inyectar su propio CSS
|
||||||
|
content_security_policy only: :index do |policy|
|
||||||
|
policy.style_src :self, :unsafe_inline
|
||||||
|
policy.script_src :self, :unsafe_inline
|
||||||
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@chart_params = { interval: interval }
|
||||||
|
hostnames
|
||||||
|
last_stat
|
||||||
|
chart_options
|
||||||
|
normalized_urls
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera un gráfico de visitas por dominio asociado a este sitio
|
||||||
|
def host
|
||||||
|
return unless stale? [last_stat, hostnames, interval]
|
||||||
|
|
||||||
|
stats = Rollup.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series|
|
||||||
|
series.each do |serie|
|
||||||
|
serie[:name] = serie.dig(:dimensions, 'host')
|
||||||
|
serie[:data].transform_values! do |value|
|
||||||
|
value * nodes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: stats
|
||||||
|
end
|
||||||
|
|
||||||
|
def resources
|
||||||
|
return unless stale? [last_stat, interval, resource]
|
||||||
|
|
||||||
|
options = {
|
||||||
|
interval: interval,
|
||||||
|
dimensions: {
|
||||||
|
deploy_id: @site.deploys.where(type: 'DeployLocal').pluck(:id).first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render json: Rollup.series(resource, **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def uris
|
||||||
|
return unless stale? [last_stat, hostnames, interval, normalized_urls]
|
||||||
|
|
||||||
|
options = { host: hostnames, uri: normalized_paths }
|
||||||
|
stats = Rollup.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series|
|
||||||
|
series.each do |serie|
|
||||||
|
serie[:name] = serie[:dimensions].slice('host', 'uri').values.join.sub('/index.html', '/')
|
||||||
|
serie[:data].transform_values! do |value|
|
||||||
|
value * nodes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: stats
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def last_stat
|
||||||
|
@last_stat ||= Stat.last
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_stats
|
||||||
@site = find_site
|
@site = find_site
|
||||||
authorize SiteStat.new(@site)
|
authorize SiteStat.new(@site)
|
||||||
|
end
|
||||||
|
|
||||||
# Solo queremos el promedio de tiempo de compilación, no de
|
# TODO: Eliminar cuando mergeemos referer-origin
|
||||||
# instalación de dependencias.
|
def hostnames
|
||||||
stats = @site.build_stats.jekyll
|
@hostnames ||= [@site.hostname, @site.alternative_hostnames].flatten
|
||||||
@build_avg = stats.average(:seconds).to_f.round(2)
|
end
|
||||||
@build_max = stats.maximum(:seconds).to_f.round(2)
|
|
||||||
|
# Normalizar las URLs
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def normalized_urls
|
||||||
|
@normalized_urls ||= params.permit(:urls).try(:[],
|
||||||
|
:urls)&.split("\n")&.map(&:strip)&.select(&:present?)&.select do |uri|
|
||||||
|
uri.start_with? 'https://'
|
||||||
|
end&.map do |u|
|
||||||
|
# XXX: Eliminar
|
||||||
|
# @see {https://0xacab.org/sutty/containers/nginx/-/merge_requests/1}
|
||||||
|
next u unless u.end_with? '/'
|
||||||
|
|
||||||
|
"#{u}index.html"
|
||||||
|
end&.uniq || [@site.url, @site.urls].flatten.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalized_paths
|
||||||
|
@normalized_paths ||= normalized_urls.map do |u|
|
||||||
|
"/#{u.split('/', 4).last}"
|
||||||
|
end.map do |u|
|
||||||
|
URI.decode_www_form_component u
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Opciones por defecto para los gráficos.
|
||||||
|
#
|
||||||
|
# La invitación a volver dentro de X tiempo es para dar un estimado de
|
||||||
|
# cuándo habrá información disponible, porque Rollup genera intervalos
|
||||||
|
# completos (¿aunque dice que no?)
|
||||||
|
#
|
||||||
|
# La diferencia se calcula sumando el intervalo a la hora de última
|
||||||
|
# toma de estadísticas y restando el tiempo que pasó desde ese
|
||||||
|
# momento.
|
||||||
|
def chart_options
|
||||||
|
time = (last_stat&.created_at || Time.now) + 1.try(interval)
|
||||||
|
please_return_at = { please_return_at: distance_of_time_in_words(Time.now, time) }
|
||||||
|
|
||||||
|
@chart_options ||= {
|
||||||
|
locale: I18n.locale,
|
||||||
|
empty: I18n.t('stats.index.empty', **please_return_at),
|
||||||
|
loading: I18n.t('stats.index.loading'),
|
||||||
|
html: %(<div id="%{id}" class="d-flex align-items-center justify-content-center" style="height: %{height}; width: %{width};">%{loading}</div>)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene y valida los intervalos
|
||||||
|
#
|
||||||
|
# @return [Symbol]
|
||||||
|
def interval
|
||||||
|
@interval ||= begin
|
||||||
|
i = params[:interval]&.to_sym
|
||||||
|
Stat::INTERVALS.include?(i) ? i : :day
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource
|
||||||
|
@resource ||= begin
|
||||||
|
r = params[:resource].to_sym
|
||||||
|
Stat::RESOURCES.include?(r) ? r : :builds
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene la cantidad de nodos de Sutty, para poder calcular la
|
||||||
|
# cantidad de visitas.
|
||||||
|
#
|
||||||
|
# Como repartimos las visitas por nodo rotando las IPs en el
|
||||||
|
# nameserver y los resolvedores de DNS eligen un nameserver
|
||||||
|
# aleatoriamente, la cantidad de visitas se reparte
|
||||||
|
# equitativamente.
|
||||||
|
#
|
||||||
|
# XXX: Remover cuando podamos centralizar los AccessLog
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
def nodes
|
||||||
|
@nodes ||= ENV.fetch('NODES', 1).to_i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,11 +21,12 @@ function makeBlock(tag: string): EditorBlock {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const li: EditorBlock = makeBlock("li");
|
export const li: EditorBlock = makeBlock("li");
|
||||||
|
const paragraph: EditorBlock = makeBlock("p");
|
||||||
|
|
||||||
// XXX: si agregás algo acá, agregalo a blockNames
|
// XXX: si agregás algo acá, agregalo a blockNames
|
||||||
// (y probablemente le quieras hacer un botón en app/views/posts/attributes/_content.haml)
|
// (y probablemente le quieras hacer un botón en app/views/posts/attributes/_content.haml)
|
||||||
export const blocks: { [propName: string]: EditorBlock } = {
|
export const blocks: { [propName: string]: EditorBlock } = {
|
||||||
paragraph: makeBlock("p"),
|
paragraph,
|
||||||
h1: makeBlock("h1"),
|
h1: makeBlock("h1"),
|
||||||
h2: makeBlock("h2"),
|
h2: makeBlock("h2"),
|
||||||
h3: makeBlock("h3"),
|
h3: makeBlock("h3"),
|
||||||
|
@ -42,6 +43,11 @@ export const blocks: { [propName: string]: EditorBlock } = {
|
||||||
allowedChildren: ["li"],
|
allowedChildren: ["li"],
|
||||||
handleEmpty: li,
|
handleEmpty: li,
|
||||||
},
|
},
|
||||||
|
blockquote: {
|
||||||
|
...makeBlock("blockquote"),
|
||||||
|
allowedChildren: blockNames,
|
||||||
|
handleEmpty: paragraph,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setupButtons(editor: Editor): void {
|
export function setupButtons(editor: Editor): void {
|
||||||
|
|
|
@ -137,8 +137,10 @@ export function setupAuxiliaryToolbar(editor: Editor): void {
|
||||||
"click",
|
"click",
|
||||||
(event) => {
|
(event) => {
|
||||||
const files = editor.toolbar.auxiliary.multimedia.fileEl.files;
|
const files = editor.toolbar.auxiliary.multimedia.fileEl.files;
|
||||||
if (!files || !files.length)
|
if (!files || !files.length) {
|
||||||
throw new Error("no hay archivos para subir");
|
console.info("no hay archivos para subir");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
|
|
||||||
const selectedEl = editor.contentEl.querySelector<HTMLElement>(
|
const selectedEl = editor.contentEl.querySelector<HTMLElement>(
|
||||||
|
|
|
@ -10,6 +10,7 @@ export const blockNames = [
|
||||||
"h6",
|
"h6",
|
||||||
"unordered_list",
|
"unordered_list",
|
||||||
"ordered_list",
|
"ordered_list",
|
||||||
|
"blockquote",
|
||||||
];
|
];
|
||||||
export const markNames = [
|
export const markNames = [
|
||||||
"bold",
|
"bold",
|
||||||
|
|
|
@ -18,6 +18,7 @@ import 'etc'
|
||||||
import Rails from '@rails/ujs'
|
import Rails from '@rails/ujs'
|
||||||
import Turbolinks from 'turbolinks'
|
import Turbolinks from 'turbolinks'
|
||||||
import * as ActiveStorage from '@rails/activestorage'
|
import * as ActiveStorage from '@rails/activestorage'
|
||||||
|
import 'chartkick/chart.js'
|
||||||
|
|
||||||
Rails.start()
|
Rails.start()
|
||||||
Turbolinks.start()
|
Turbolinks.start()
|
||||||
|
|
|
@ -5,13 +5,23 @@ class DeployJob < ApplicationJob
|
||||||
class DeployException < StandardError; end
|
class DeployException < StandardError; end
|
||||||
|
|
||||||
# rubocop:disable Metrics/MethodLength
|
# rubocop:disable Metrics/MethodLength
|
||||||
def perform(site, notify = true)
|
def perform(site, notify = true, time = Time.now)
|
||||||
ActiveRecord::Base.connection_pool.with_connection do
|
ActiveRecord::Base.connection_pool.with_connection do
|
||||||
@site = Site.find(site)
|
@site = Site.find(site)
|
||||||
|
|
||||||
# Si ya hay una tarea corriendo, aplazar esta
|
# Si ya hay una tarea corriendo, aplazar esta. Si estuvo
|
||||||
|
# esperando más de 10 minutos, recuperar el estado anterior.
|
||||||
|
#
|
||||||
|
# Como el trabajo actual se aplaza al siguiente, arrastrar la
|
||||||
|
# hora original para poder ir haciendo timeouts.
|
||||||
if @site.building?
|
if @site.building?
|
||||||
DeployJob.perform_in(60, site, notify)
|
if 10.minutes.ago >= time
|
||||||
|
@site.update status: 'waiting'
|
||||||
|
raise DeployException,
|
||||||
|
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
|
||||||
|
end
|
||||||
|
|
||||||
|
DeployJob.perform_in(60, site, notify, time)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,8 +39,11 @@ class DeployJob < ApplicationJob
|
||||||
end
|
end
|
||||||
|
|
||||||
deploy_others
|
deploy_others
|
||||||
notify_usuaries if notify
|
|
||||||
|
# Volver a la espera
|
||||||
@site.update status: 'waiting'
|
@site.update status: 'waiting'
|
||||||
|
|
||||||
|
notify_usuaries if notify
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
55
app/jobs/periodic_job.rb
Normal file
55
app/jobs/periodic_job.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Una tarea que se corre periódicamente
|
||||||
|
class PeriodicJob < ApplicationJob
|
||||||
|
class RunAgainException < StandardError; end
|
||||||
|
|
||||||
|
STARTING_INTERVAL = Stat::INTERVALS.first
|
||||||
|
|
||||||
|
# Tener el sitio a mano
|
||||||
|
attr_reader :site
|
||||||
|
|
||||||
|
# Descartar y notificar si pasó algo más.
|
||||||
|
#
|
||||||
|
# XXX: En realidad deberíamos seguir reintentando?
|
||||||
|
discard_on(StandardError) do |_, error|
|
||||||
|
ExceptionNotifier.notify_exception(error)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Correr indefinidamente una vez por hora.
|
||||||
|
#
|
||||||
|
# XXX: El orden importa, si el descarte viene después, nunca se va a
|
||||||
|
# reintentar.
|
||||||
|
retry_on(PeriodicJob::RunAgainException, wait: 1.try(STARTING_INTERVAL), attempts: Float::INFINITY, jitter: 0)
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Las clases que implementen esta tienen que usar este método al
|
||||||
|
# terminar.
|
||||||
|
def run_again!
|
||||||
|
raise PeriodicJob::RunAgainException, 'Reintentando'
|
||||||
|
end
|
||||||
|
|
||||||
|
# El intervalo de inicio
|
||||||
|
#
|
||||||
|
# @return [Symbol]
|
||||||
|
def starting_interval
|
||||||
|
STARTING_INTERVAL
|
||||||
|
end
|
||||||
|
|
||||||
|
# La última recolección de estadísticas o empezar desde el principio
|
||||||
|
# de los tiempos.
|
||||||
|
#
|
||||||
|
# @return [Stat]
|
||||||
|
def last_stat
|
||||||
|
@last_stat ||= site.stats.where(name: stat_name).last ||
|
||||||
|
site.stats.build(created_at: Time.new(1970, 1, 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devuelve el comienzo del intervalo
|
||||||
|
#
|
||||||
|
# @return [Time]
|
||||||
|
def beginning_of_interval
|
||||||
|
@beginning_of_interval ||= last_stat.created_at.try(:"beginning_of_#{starting_interval}")
|
||||||
|
end
|
||||||
|
end
|
67
app/jobs/stat_collection_job.rb
Normal file
67
app/jobs/stat_collection_job.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Genera resúmenes de información para poder mostrar estadísticas y se
|
||||||
|
# corre regularmente a sí misma.
|
||||||
|
class StatCollectionJob < ApplicationJob
|
||||||
|
STAT_NAME = 'stat_collection_job'
|
||||||
|
|
||||||
|
def perform(site_id:, once: true)
|
||||||
|
@site = Site.find site_id
|
||||||
|
|
||||||
|
scope.rollup('builds', **options)
|
||||||
|
|
||||||
|
scope.rollup('space_used', **options) do |rollup|
|
||||||
|
rollup.average(:bytes)
|
||||||
|
end
|
||||||
|
|
||||||
|
scope.rollup('build_time', **options) do |rollup|
|
||||||
|
rollup.average(:seconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
# XXX: Es correcto promediar promedios?
|
||||||
|
Stat::INTERVALS.reduce do |previous, current|
|
||||||
|
rollup(name: 'builds', interval_previous: previous, interval: current)
|
||||||
|
rollup(name: 'space_used', interval_previous: previous, interval: current, operation: :average)
|
||||||
|
rollup(name: 'build_time', interval_previous: previous, interval: current, operation: :average)
|
||||||
|
|
||||||
|
current
|
||||||
|
end
|
||||||
|
|
||||||
|
# Registrar que se hicieron todas las recolecciones
|
||||||
|
site.stats.create! name: STAT_NAME
|
||||||
|
|
||||||
|
run_again! unless once
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Genera un rollup recursivo en base al período anterior y aplica una
|
||||||
|
# operación.
|
||||||
|
#
|
||||||
|
# @return [NilClass]
|
||||||
|
def rollup(name:, interval_previous:, interval:, operation: :sum)
|
||||||
|
Rollup.where(name: name, interval: interval_previous)
|
||||||
|
.where_dimensions(site_id: site.id)
|
||||||
|
.group("dimensions->'site_id'")
|
||||||
|
.rollup(name, interval: interval, update: true) do |rollup|
|
||||||
|
rollup.try(:operation, :value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Los registros a procesar
|
||||||
|
#
|
||||||
|
# @return [ActiveRecord::Relation]
|
||||||
|
def scope
|
||||||
|
@scope ||= site.build_stats
|
||||||
|
.jekyll
|
||||||
|
.where('created_at => ?', beginning_of_interval)
|
||||||
|
.group(:site_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Las opciones por defecto
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def options
|
||||||
|
@options ||= { interval: starting_interval, update: true }
|
||||||
|
end
|
||||||
|
end
|
106
app/jobs/uri_collection_job.rb
Normal file
106
app/jobs/uri_collection_job.rb
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Procesar una lista de URIs para una lista de dominios. Esto nos
|
||||||
|
# permite procesar estadísticas a demanda.
|
||||||
|
#
|
||||||
|
# Hay varias cosas acá que van a convertirse en métodos propios, como la
|
||||||
|
# detección de URIs de un sitio (aunque la versión actual detecta todas
|
||||||
|
# las páginas y no solo las de posts como tenemos planeado, hay que
|
||||||
|
# resolver eso).
|
||||||
|
#
|
||||||
|
# Los hostnames de un sitio van a poder obtenerse a partir de
|
||||||
|
# Site#hostnames con la garantía de que son únicos.
|
||||||
|
class UriCollectionJob < PeriodicJob
|
||||||
|
# Ignoramos imágenes porque suelen ser demasiadas y no aportan a las
|
||||||
|
# estadísticas.
|
||||||
|
IMAGES = %w[.png .jpg .jpeg .gif .webp].freeze
|
||||||
|
STAT_NAME = 'uri_collection_job'
|
||||||
|
|
||||||
|
def perform(site_id:, once: true)
|
||||||
|
@site = Site.find site_id
|
||||||
|
|
||||||
|
hostnames.each do |hostname|
|
||||||
|
uris.each do |uri|
|
||||||
|
return if File.exist? Rails.root.join('tmp', 'uri_collection_job_stop')
|
||||||
|
|
||||||
|
AccessLog.where(host: hostname, uri: uri)
|
||||||
|
.where('created_at >= ?', beginning_of_interval)
|
||||||
|
.completed_requests
|
||||||
|
.non_robots
|
||||||
|
.group(:host, :uri)
|
||||||
|
.rollup('host|uri', interval: starting_interval, update: true)
|
||||||
|
|
||||||
|
# Reducir las estadísticas calculadas aplicando un rollup sobre el
|
||||||
|
# intervalo más amplio.
|
||||||
|
Stat::INTERVALS.reduce do |previous, current|
|
||||||
|
Rollup.where(name: 'host|uri', interval: previous)
|
||||||
|
.where_dimensions(host: hostname, uri: uri)
|
||||||
|
.group("dimensions->'host'", "dimensions->'uri'")
|
||||||
|
.rollup('host|uri', interval: current, update: true) do |rollup|
|
||||||
|
rollup.sum(:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devolver el intervalo actual
|
||||||
|
current
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Recordar la última vez que se corrió la tarea
|
||||||
|
site.stats.create! name: STAT_NAME
|
||||||
|
|
||||||
|
run_again! unless once
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def stat_name
|
||||||
|
STAT_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
#
|
||||||
|
# TODO: Cambiar al mergear origin-referer
|
||||||
|
def destination
|
||||||
|
@destination ||= site.deploys.find_by(type: 'DeployLocal').destination
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Cambiar al mergear origin-referer
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def hostnames
|
||||||
|
@hostnames ||= site.deploys.map do |deploy|
|
||||||
|
case deploy
|
||||||
|
when DeployLocal
|
||||||
|
site.hostname
|
||||||
|
when DeployWww
|
||||||
|
deploy.fqdn
|
||||||
|
when DeployAlternativeDomain
|
||||||
|
deploy.hostname.dup.tap do |h|
|
||||||
|
h.replace(h.end_with?('.') ? h[0..-2] : "#{h}.#{Site.domain}")
|
||||||
|
end
|
||||||
|
when DeployHiddenService
|
||||||
|
deploy.onion
|
||||||
|
end
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
# Recolecta todas las URIs menos imágenes
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def uris
|
||||||
|
@uris ||= Dir.chdir destination do
|
||||||
|
(Dir.glob('**/*.html') + Dir.glob('public/**/*').reject do |p|
|
||||||
|
File.directory? p
|
||||||
|
end.reject do |p|
|
||||||
|
p = p.downcase
|
||||||
|
|
||||||
|
IMAGES.any? do |i|
|
||||||
|
p.end_with? i
|
||||||
|
end
|
||||||
|
end).map do |uri|
|
||||||
|
"/#{uri}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
app/lib/action_dispatch/http/uploaded_file_decorator.rb
Normal file
21
app/lib/action_dispatch/http/uploaded_file_decorator.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActionDispatch
|
||||||
|
module Http
|
||||||
|
# Normaliza los nombres de archivo para que se propaguen
|
||||||
|
# correctamente a través de todo el stack.
|
||||||
|
module UploadedFileDecorator
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Devolver el nombre de archivo con caracteres unicode
|
||||||
|
# normalizados
|
||||||
|
def original_filename
|
||||||
|
@original_filename.unicode_normalize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActionDispatch::Http::UploadedFile.include ActionDispatch::Http::UploadedFileDecorator
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActiveStorage
|
||||||
|
module Attached::Changes::CreateOneDecorator
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
private
|
||||||
|
|
||||||
|
# A partir de ahora todos los archivos se suben al servicio de
|
||||||
|
# cada sitio.
|
||||||
|
def attachment_service_name
|
||||||
|
record.name.to_sym
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveStorage::Attached::Changes::CreateOne.include ActiveStorage::Attached::Changes::CreateOneDecorator
|
82
app/lib/active_storage/service/jekyll_service.rb
Normal file
82
app/lib/active_storage/service/jekyll_service.rb
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActiveStorage
|
||||||
|
class Service
|
||||||
|
# Sube los archivos a cada repositorio y los agrega al LFS de su
|
||||||
|
# repositorio git.
|
||||||
|
#
|
||||||
|
# @todo: Implementar LFS. No nos gusta mucho la idea porque duplica
|
||||||
|
# el espacio en disco, pero es la única forma que tenemos (hasta que
|
||||||
|
# implementemos IPFS) para poder transferir los archivos junto con el
|
||||||
|
# sitio.
|
||||||
|
class JekyllService < Service::DiskService
|
||||||
|
# Genera un servicio para un sitio determinado
|
||||||
|
#
|
||||||
|
# @param :site [Site]
|
||||||
|
# @return [ActiveStorage::Service::JekyllService]
|
||||||
|
def self.build_for_site(site:)
|
||||||
|
new(root: File.join(site.path, 'public'), public: true).tap do |js|
|
||||||
|
js.name = site.name.to_sym
|
||||||
|
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.
|
||||||
|
#
|
||||||
|
# @param :key [String]
|
||||||
|
# @param :expires_in [Integer]
|
||||||
|
# @param :content_type [String]
|
||||||
|
# @param :content_length [Integer]
|
||||||
|
# @param :checksum [String]
|
||||||
|
# @return [String]
|
||||||
|
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
||||||
|
instrument :url, key: key do |payload|
|
||||||
|
verified_token_with_expiration = ActiveStorage.verifier.generate(
|
||||||
|
{
|
||||||
|
key: key,
|
||||||
|
content_type: content_type,
|
||||||
|
content_length: content_length,
|
||||||
|
checksum: checksum,
|
||||||
|
service_name: name,
|
||||||
|
filename: filename_for(key)
|
||||||
|
},
|
||||||
|
expires_in: expires_in,
|
||||||
|
purpose: :blob_token
|
||||||
|
)
|
||||||
|
|
||||||
|
generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host)
|
||||||
|
|
||||||
|
payload[:url] = generated_url
|
||||||
|
|
||||||
|
generated_url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Mantener retrocompatibilidad con cómo gestionamos los archivos
|
||||||
|
# subidos hasta ahora.
|
||||||
|
#
|
||||||
|
# @param :key [String]
|
||||||
|
# @return [String]
|
||||||
|
def folder_for(key)
|
||||||
|
key
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene el nombre de archivo para esta key
|
||||||
|
#
|
||||||
|
# @param :key [String]
|
||||||
|
# @return [String]
|
||||||
|
def filename_for(key)
|
||||||
|
ActiveStorage::Blob.where(key: key).limit(1).pluck(:filename).first
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crea una ruta para la llave con un nombre conocido.
|
||||||
|
#
|
||||||
|
# @param :key [String]
|
||||||
|
# @return [String]
|
||||||
|
def path_for(key)
|
||||||
|
File.join root, folder_for(key), filename_for(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
app/lib/active_storage/service/registry_decorator.rb
Normal file
31
app/lib/active_storage/service/registry_decorator.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActiveStorage
|
||||||
|
class Service
|
||||||
|
# Modificaciones a ActiveStorage::Service::Registry
|
||||||
|
module RegistryDecorator
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# El mismo comportamiento que #fetch con el agregado de generar
|
||||||
|
# un {JekyllService} para cada sitio.
|
||||||
|
def fetch(name)
|
||||||
|
services.fetch(name.to_sym) do |key|
|
||||||
|
if configurations.include?(key)
|
||||||
|
services[key] = configurator.build(key)
|
||||||
|
elsif (site = Site.find_by_name(key))
|
||||||
|
services[key] = ActiveStorage::Service::JekyllService.build_for_site(site: site)
|
||||||
|
elsif block_given?
|
||||||
|
yield key
|
||||||
|
else
|
||||||
|
raise KeyError, "Missing configuration for the #{key} Active Storage service. " \
|
||||||
|
"Configurations available for the #{configurations.keys.to_sentence} services."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveStorage::Service::Registry.include ActiveStorage::Service::RegistryDecorator
|
|
@ -1,4 +1,12 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccessLog < ApplicationRecord
|
class AccessLog < ApplicationRecord
|
||||||
|
# Las peticiones completas son las que terminaron bien y se
|
||||||
|
# respondieron con 200 OK o 304 Not Modified
|
||||||
|
#
|
||||||
|
# @see {https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
|
||||||
|
scope :completed_requests, -> { where(request_method: 'GET', request_completion: 'OK', status: [200, 304]) }
|
||||||
|
scope :non_robots, -> { where(crawler: false) }
|
||||||
|
scope :robots, -> { where(crawler: true) }
|
||||||
|
scope :pages, -> { where(sent_http_content_type: ['text/html', 'text/html; charset=utf-8', 'text/html; charset=UTF-8']) }
|
||||||
end
|
end
|
||||||
|
|
26
app/models/active_storage/blob_decorator.rb
Normal file
26
app/models/active_storage/blob_decorator.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActiveStorage
|
||||||
|
# Modificaciones a ActiveStorage::Blob
|
||||||
|
module BlobDecorator
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Permitir que llegue el nombre de archivo al servicio de subida de
|
||||||
|
# archivos.
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def service_metadata
|
||||||
|
if forcibly_serve_as_binary?
|
||||||
|
{ content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename }
|
||||||
|
elsif !allowed_inline?
|
||||||
|
{ content_type: content_type, disposition: :attachment, filename: filename }
|
||||||
|
else
|
||||||
|
{ content_type: content_type, filename: filename }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveStorage::Blob.include ActiveStorage::BlobDecorator
|
|
@ -62,10 +62,15 @@ class DeployLocal < Deploy
|
||||||
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
||||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||||
'JEKYLL_ENV' => Rails.env,
|
'JEKYLL_ENV' => Rails.env,
|
||||||
'LANG' => ENV['LANG']
|
'LANG' => ENV['LANG'],
|
||||||
|
'YARN_CACHE_FOLDER' => yarn_cache_dir
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def yarn_cache_dir
|
||||||
|
Rails.root.join('_yarn_cache').to_s
|
||||||
|
end
|
||||||
|
|
||||||
def yarn_lock
|
def yarn_lock
|
||||||
File.join(site.path, 'yarn.lock')
|
File.join(site.path, 'yarn.lock')
|
||||||
end
|
end
|
||||||
|
@ -80,9 +85,9 @@ class DeployLocal < Deploy
|
||||||
|
|
||||||
# Corre yarn dentro del repositorio
|
# Corre yarn dentro del repositorio
|
||||||
def yarn
|
def yarn
|
||||||
return unless yarn_lock?
|
return true unless yarn_lock?
|
||||||
|
|
||||||
run 'yarn'
|
run 'yarn install --production'
|
||||||
end
|
end
|
||||||
|
|
||||||
def bundle
|
def bundle
|
||||||
|
|
|
@ -13,6 +13,13 @@ class MetadataBelongsTo < MetadataRelatedPosts
|
||||||
''
|
''
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Obtiene el valor desde el documento.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def document_value
|
||||||
|
document.data[name.to_s]
|
||||||
|
end
|
||||||
|
|
||||||
def validate
|
def validate
|
||||||
super
|
super
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,19 @@ class MetadataBoolean < MetadataTemplate
|
||||||
# * false
|
# * false
|
||||||
# * true
|
# * true
|
||||||
def value
|
def value
|
||||||
return document.data.fetch(name.to_s, default_value) if self[:value].nil?
|
case self[:value]
|
||||||
return self[:value] unless self[:value].is_a? String
|
when NilClass
|
||||||
|
document.data.fetch(name.to_s, default_value)
|
||||||
|
when String
|
||||||
|
true_values.include? self[:value]
|
||||||
|
else
|
||||||
|
self[:value]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
self[:value] = true_values.include? self[:value]
|
# Siempre guardar el valor de este campo a menos que sea nulo
|
||||||
|
def empty?
|
||||||
|
value.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -56,7 +56,7 @@ class MetadataContent < MetadataTemplate
|
||||||
uri = URI element['src']
|
uri = URI element['src']
|
||||||
|
|
||||||
# No permitimos recursos externos
|
# No permitimos recursos externos
|
||||||
element.remove unless uri.hostname.end_with? Site.domain
|
element.remove unless uri.scheme == 'https' && uri.hostname.end_with?(Site.domain)
|
||||||
rescue URI::Error
|
rescue URI::Error
|
||||||
element.remove
|
element.remove
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,12 +13,18 @@ class MetadataFile < MetadataTemplate
|
||||||
value == default_value
|
value == default_value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# No hay valores sugeridos para archivos subidos.
|
||||||
|
#
|
||||||
|
# XXX: Esto ayuda a deserializar en {Site#everything_of}
|
||||||
|
def values; end
|
||||||
|
|
||||||
def validate
|
def validate
|
||||||
super
|
super
|
||||||
|
|
||||||
errors << I18n.t("metadata.#{type}.site_invalid") if site.invalid?
|
errors << I18n.t("metadata.#{type}.site_invalid") if site.invalid?
|
||||||
errors << I18n.t("metadata.#{type}.path_required") if path_missing?
|
errors << I18n.t("metadata.#{type}.path_required") if path_missing?
|
||||||
errors << I18n.t("metadata.#{type}.no_file_for_description") if no_file_for_description?
|
errors << I18n.t("metadata.#{type}.no_file_for_description") if no_file_for_description?
|
||||||
|
errors << I18n.t("metadata.#{type}.attachment_missing") if path? && !static_file
|
||||||
|
|
||||||
errors.compact!
|
errors.compact!
|
||||||
errors.empty?
|
errors.empty?
|
||||||
|
@ -34,12 +40,6 @@ class MetadataFile < MetadataTemplate
|
||||||
value['path'].is_a?(String)
|
value['path'].is_a?(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Determina si la ruta es opcional pero deja pasar si la ruta se
|
|
||||||
# especifica
|
|
||||||
def path_optional?
|
|
||||||
!required && !path?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Asociar la imagen subida al sitio y obtener la ruta
|
# Asociar la imagen subida al sitio y obtener la ruta
|
||||||
#
|
#
|
||||||
# XXX: Si evitamos guardar cambios con changed? no tenemos forma de
|
# XXX: Si evitamos guardar cambios con changed? no tenemos forma de
|
||||||
|
@ -48,13 +48,7 @@ class MetadataFile < MetadataTemplate
|
||||||
# repetida.
|
# repetida.
|
||||||
def save
|
def save
|
||||||
value['description'] = sanitize value['description']
|
value['description'] = sanitize value['description']
|
||||||
|
value['path'] = static_file ? relative_destination_path_with_filename.to_s : nil
|
||||||
if path?
|
|
||||||
hardlink
|
|
||||||
value['path'] = relative_destination_path
|
|
||||||
else
|
|
||||||
value['path'] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -71,101 +65,88 @@ class MetadataFile < MetadataTemplate
|
||||||
# XXX: La última opción provoca archivos duplicados, pero es lo mejor
|
# XXX: La última opción provoca archivos duplicados, pero es lo mejor
|
||||||
# que tenemos hasta que resolvamos https://0xacab.org/sutty/sutty/-/issues/213
|
# que tenemos hasta que resolvamos https://0xacab.org/sutty/sutty/-/issues/213
|
||||||
#
|
#
|
||||||
# @return [ActiveStorage::Attachment]
|
# @todo encontrar una forma de obtener el attachment sin tener que
|
||||||
|
# recurrir al último subido.
|
||||||
|
#
|
||||||
|
# @return [ActiveStorage::Attachment,nil]
|
||||||
def static_file
|
def static_file
|
||||||
return unless path?
|
|
||||||
|
|
||||||
@static_file ||=
|
@static_file ||=
|
||||||
case value['path']
|
case value['path']
|
||||||
when ActionDispatch::Http::UploadedFile
|
when ActionDispatch::Http::UploadedFile
|
||||||
site.static_files.last if site.static_files.attach(value['path'])
|
site.static_files.last if site.static_files.attach(value['path'])
|
||||||
when String
|
when String
|
||||||
if (blob = ActiveStorage::Blob.where(key: key_from_path).pluck(:id).first)
|
if (blob_id = ActiveStorage::Blob.where(key: key_from_path).pluck(:id).first)
|
||||||
site.static_files.find_by(blob_id: blob)
|
site.static_files.find_by(blob_id: blob_id)
|
||||||
elsif site.static_files.attach(io: path.open, filename: path.basename)
|
elsif path? && pathname.exist? && site.static_files.attach(io: pathname.open, filename: pathname.basename)
|
||||||
site.static_files.last
|
site.static_files.last.tap do |s|
|
||||||
|
s.blob.update(key: key_from_path)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Obtiene la ruta absoluta al archivo
|
||||||
|
#
|
||||||
|
# @return [Pathname]
|
||||||
|
def pathname
|
||||||
|
raise NoMethodError unless uploaded?
|
||||||
|
|
||||||
|
@pathname ||= Pathname.new(File.join(site.path, value['path']))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene la key del attachment a partir de la ruta
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
def key_from_path
|
def key_from_path
|
||||||
path.dirname.basename.to_s
|
pathname.dirname.basename.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def path?
|
def path?
|
||||||
value['path'].present?
|
value['path'].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def description?
|
||||||
|
value['description'].present?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filemagic
|
# Obtener la ruta al archivo relativa al sitio
|
||||||
@filemagic ||= FileMagic.new(FileMagic::MAGIC_MIME)
|
#
|
||||||
end
|
|
||||||
|
|
||||||
# @return [Pathname]
|
# @return [Pathname]
|
||||||
def path
|
|
||||||
@path ||= Pathname.new(File.join(site.path, value['path']))
|
|
||||||
end
|
|
||||||
|
|
||||||
def file
|
|
||||||
return unless path?
|
|
||||||
|
|
||||||
@file ||=
|
|
||||||
case value['path']
|
|
||||||
when ActionDispatch::Http::UploadedFile then value['path'].tempfile.path
|
|
||||||
when String then File.join(site.path, value['path'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Hacemos un link duro para colocar el archivo dentro del repositorio
|
|
||||||
# y no duplicar el espacio que ocupan. Esto requiere que ambos
|
|
||||||
# directorios estén dentro del mismo punto de montaje.
|
|
||||||
#
|
|
||||||
# XXX: Asumimos que el archivo destino no existe porque siempre
|
|
||||||
# contiene una key única.
|
|
||||||
#
|
|
||||||
# @return [Boolean]
|
|
||||||
def hardlink
|
|
||||||
return if hardlink?
|
|
||||||
return if File.exist? destination_path
|
|
||||||
|
|
||||||
FileUtils.mkdir_p(File.dirname(destination_path))
|
|
||||||
FileUtils.ln(uploaded_path, destination_path).zero?
|
|
||||||
end
|
|
||||||
|
|
||||||
def hardlink?
|
|
||||||
File.stat(uploaded_path).ino == File.stat(destination_path).ino
|
|
||||||
rescue Errno::ENOENT
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtener la ruta al archivo
|
|
||||||
# https://stackoverflow.com/a/53908358
|
|
||||||
def uploaded_relative_path
|
|
||||||
ActiveStorage::Blob.service.path_for(static_file.key)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [String]
|
|
||||||
def uploaded_path
|
|
||||||
Rails.root.join uploaded_relative_path
|
|
||||||
end
|
|
||||||
|
|
||||||
# La ruta del archivo mantiene el nombre original pero contiene el
|
|
||||||
# nombre interno y único del archivo para poder relacionarlo con el
|
|
||||||
# archivo subido en Sutty.
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
def relative_destination_path
|
|
||||||
@relative_destination_path ||= File.join('public', static_file.key, static_file.filename.to_s)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [String]
|
|
||||||
def destination_path
|
def destination_path
|
||||||
@destination_path ||= File.join(site.path, relative_destination_path)
|
Pathname.new(static_file_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Agrega el nombre de archivo a la ruta para tener retrocompatibilidad
|
||||||
|
#
|
||||||
|
# @return [Pathname]
|
||||||
|
def destination_path_with_filename
|
||||||
|
destination_path.realpath
|
||||||
|
# Si el archivo no llegara a existir, en lugar de hacer fallar todo,
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
value['path']
|
||||||
|
end
|
||||||
|
|
||||||
|
def relative_destination_path_with_filename
|
||||||
|
destination_path_with_filename.relative_path_from(Pathname.new(site.path).realpath)
|
||||||
|
end
|
||||||
|
|
||||||
|
def static_file_path
|
||||||
|
case static_file.blob.service.name
|
||||||
|
when :local
|
||||||
|
File.join(site.path, 'public', static_file.key, static_file.filename.to_s)
|
||||||
|
else
|
||||||
|
static_file.blob.service.path_for(static_file.key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# No hay archivo pero se lo describió
|
# No hay archivo pero se lo describió
|
||||||
def no_file_for_description?
|
def no_file_for_description?
|
||||||
value['description'].present? && value['path'].blank?
|
!path? && description?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
31
app/models/metadata_float.rb
Normal file
31
app/models/metadata_float.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Un campo numérico de punto flotante
|
||||||
|
class MetadataFloat < MetadataTemplate
|
||||||
|
# Nada
|
||||||
|
def default_value
|
||||||
|
super || nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def save
|
||||||
|
return true unless changed?
|
||||||
|
|
||||||
|
self[:value] = value.to_f
|
||||||
|
self[:value] = encrypt(value) if private?
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Indicarle al navegador que acepte números decimales
|
||||||
|
#
|
||||||
|
# @return [Float]
|
||||||
|
def step
|
||||||
|
0.05
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def decrypt(value)
|
||||||
|
super(value).to_f
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,7 +5,7 @@ class MetadataImage < MetadataFile
|
||||||
def validate
|
def validate
|
||||||
super
|
super
|
||||||
|
|
||||||
errors << I18n.t('metadata.image.not_an_image') unless image?
|
errors << I18n.t('metadata.image.not_an_image') if path? && !image?
|
||||||
|
|
||||||
errors.compact!
|
errors.compact!
|
||||||
errors.empty?
|
errors.empty?
|
||||||
|
@ -13,8 +13,6 @@ class MetadataImage < MetadataFile
|
||||||
|
|
||||||
# Determina si es una imagen
|
# Determina si es una imagen
|
||||||
def image?
|
def image?
|
||||||
return true unless file
|
static_file&.blob&.send(:web_image?)
|
||||||
|
|
||||||
filemagic.file(file).starts_with? 'image/'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,6 @@ class MetadataMarkdown < MetadataText
|
||||||
# markdown y se eliminan autolinks. Mejor es habilitar la generación
|
# markdown y se eliminan autolinks. Mejor es habilitar la generación
|
||||||
# SAFE de CommonMark en la configuración del sitio.
|
# SAFE de CommonMark en la configuración del sitio.
|
||||||
def sanitize(string)
|
def sanitize(string)
|
||||||
string
|
string.unicode_normalize
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,6 +25,6 @@ class MetadataMarkdownContent < MetadataText
|
||||||
# markdown y se eliminan autolinks. Mejor es deshabilitar la
|
# markdown y se eliminan autolinks. Mejor es deshabilitar la
|
||||||
# generación SAFE de CommonMark en la configuración del sitio.
|
# generación SAFE de CommonMark en la configuración del sitio.
|
||||||
def sanitize(string)
|
def sanitize(string)
|
||||||
string.tr("\r", '')
|
string.tr("\r", '').unicode_normalize
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
# Este metadato permite generar rutas manuales.
|
# Este metadato permite generar rutas manuales.
|
||||||
class MetadataPermalink < MetadataString
|
class MetadataPermalink < MetadataString
|
||||||
|
# El valor por defecto una vez creado es la URL que le asigne Jekyll,
|
||||||
|
# de forma que nunca cambia aunque se cambie el título.
|
||||||
|
def default_value
|
||||||
|
document.url.sub(%r{\A/}, '') unless post.new?
|
||||||
|
end
|
||||||
|
|
||||||
# Los permalinks nunca pueden ser privados
|
# Los permalinks nunca pueden ser privados
|
||||||
def private?
|
def private?
|
||||||
false
|
false
|
||||||
|
@ -13,7 +19,7 @@ class MetadataPermalink < MetadataString
|
||||||
# puntos suspensivos, la primera / para que siempre sea relativa y
|
# puntos suspensivos, la primera / para que siempre sea relativa y
|
||||||
# agregamos una / al final si la ruta no tiene extensión.
|
# agregamos una / al final si la ruta no tiene extensión.
|
||||||
def sanitize(value)
|
def sanitize(value)
|
||||||
value = value.strip.gsub('..', '/').gsub('./', '').squeeze('/')
|
value = value.strip.unicode_normalize.gsub('..', '/').gsub('./', '').squeeze('/')
|
||||||
value = value[1..-1] if value.start_with? '/'
|
value = value[1..-1] if value.start_with? '/'
|
||||||
value += '/' if File.extname(value).blank?
|
value += '/' if File.extname(value).blank?
|
||||||
|
|
||||||
|
|
24
app/models/metadata_predefined_value.rb
Normal file
24
app/models/metadata_predefined_value.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Un campo de texto seleccionado de una lista de valores posibles
|
||||||
|
class MetadataPredefinedValue < MetadataString
|
||||||
|
# Obtiene todos los valores desde el layout, en un formato compatible
|
||||||
|
# con options_for_select.
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def values
|
||||||
|
@values ||= layout.dig(:metadata, name, 'values', I18n.locale.to_s)&.invert || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Solo permite almacenar los valores predefinidos.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def sanitize(string)
|
||||||
|
v = super string
|
||||||
|
return '' unless values.values.include? v
|
||||||
|
|
||||||
|
v
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,7 +17,7 @@ class MetadataString < MetadataTemplate
|
||||||
def sanitize(string)
|
def sanitize(string)
|
||||||
return '' if string.blank?
|
return '' if string.blank?
|
||||||
|
|
||||||
sanitizer.sanitize(string.strip,
|
sanitizer.sanitize(string.strip.unicode_normalize,
|
||||||
tags: [],
|
tags: [],
|
||||||
attributes: []).strip.html_safe
|
attributes: []).strip.html_safe
|
||||||
end
|
end
|
||||||
|
|
|
@ -184,9 +184,12 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
return if string.nil?
|
return if string.nil?
|
||||||
return string unless string.is_a? String
|
return string unless string.is_a? String
|
||||||
|
|
||||||
sanitizer.sanitize(string.tr("\r", ''),
|
sanitizer
|
||||||
tags: allowed_tags,
|
.sanitize(string.tr("\r", '').unicode_normalize,
|
||||||
attributes: allowed_attributes).strip.html_safe
|
tags: allowed_tags,
|
||||||
|
attributes: allowed_attributes)
|
||||||
|
.strip
|
||||||
|
.html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitizer
|
def sanitizer
|
||||||
|
@ -199,7 +202,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed_tags
|
def allowed_tags
|
||||||
@allowed_tags ||= %w[strong em del u mark p h1 h2 h3 h4 h5 h6 ul ol li img iframe audio video div figure
|
@allowed_tags ||= %w[strong em del u mark p h1 h2 h3 h4 h5 h6 ul ol li img iframe audio video div figure blockquote
|
||||||
figcaption a sub sup small].freeze
|
figcaption a sub sup small].freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,360 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# Representa los distintos tipos de campos que pueden venir de una
|
|
||||||
# plantilla compleja
|
|
||||||
class Post
|
|
||||||
class TemplateField
|
|
||||||
attr_reader :post, :contents, :key
|
|
||||||
|
|
||||||
STRING_VALUES = %w[string text url number email password date year
|
|
||||||
image video audio document].freeze
|
|
||||||
|
|
||||||
# Tipo de valores que son archivos
|
|
||||||
FILE_TYPES = %w[image video audio document].freeze
|
|
||||||
|
|
||||||
def initialize(post, key, contents)
|
|
||||||
@post = post
|
|
||||||
@key = key
|
|
||||||
@contents = contents
|
|
||||||
end
|
|
||||||
|
|
||||||
def title
|
|
||||||
contents.dig('title') if complex?
|
|
||||||
end
|
|
||||||
|
|
||||||
def subtitle
|
|
||||||
contents.dig('subtitle') if complex?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtiene el valor
|
|
||||||
def value
|
|
||||||
complex? ? contents.dig('value') : contents
|
|
||||||
end
|
|
||||||
|
|
||||||
def max
|
|
||||||
return 0 if simple?
|
|
||||||
|
|
||||||
contents.fetch('max', 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
def min
|
|
||||||
return 0 if simple?
|
|
||||||
|
|
||||||
contents.fetch('min', 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: volver elegante!
|
|
||||||
def type
|
|
||||||
return @type if @type
|
|
||||||
|
|
||||||
if image?
|
|
||||||
@type = 'image'
|
|
||||||
elsif email?
|
|
||||||
@type = 'email'
|
|
||||||
elsif url?
|
|
||||||
@type = 'url'
|
|
||||||
elsif number?
|
|
||||||
@type = 'number'
|
|
||||||
elsif password?
|
|
||||||
@type = 'password'
|
|
||||||
elsif date?
|
|
||||||
@type = 'date'
|
|
||||||
elsif year?
|
|
||||||
@type = 'year'
|
|
||||||
elsif text_area?
|
|
||||||
@type = 'text_area'
|
|
||||||
elsif check_box_group?
|
|
||||||
@type = 'check_box_group'
|
|
||||||
elsif radio_group?
|
|
||||||
@type = 'radio_group'
|
|
||||||
elsif string?
|
|
||||||
@type = 'text'
|
|
||||||
# TODO: volver a hacer funcionar esto y ahorranos los multiple:
|
|
||||||
# false
|
|
||||||
elsif string? && contents.split('/', 2).count == 2
|
|
||||||
@type = 'select'
|
|
||||||
elsif nested?
|
|
||||||
@type = 'table'
|
|
||||||
elsif array?
|
|
||||||
@type = 'select'
|
|
||||||
elsif boolean?
|
|
||||||
@type = 'check_box'
|
|
||||||
end
|
|
||||||
|
|
||||||
@type
|
|
||||||
end
|
|
||||||
|
|
||||||
# Devuelve los valores vacíos según el tipo
|
|
||||||
def empty_value
|
|
||||||
if string?
|
|
||||||
''
|
|
||||||
elsif nested?
|
|
||||||
# TODO: devolver las keys también
|
|
||||||
{}
|
|
||||||
elsif array?
|
|
||||||
[]
|
|
||||||
elsif boolean?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def cols
|
|
||||||
complex? && contents.dig('cols')
|
|
||||||
end
|
|
||||||
|
|
||||||
def align
|
|
||||||
complex? && contents.dig('align')
|
|
||||||
end
|
|
||||||
|
|
||||||
# El campo es requerido si es complejo y se especifica que lo sea
|
|
||||||
def required?
|
|
||||||
complex? && contents.dig('required')
|
|
||||||
end
|
|
||||||
|
|
||||||
def boolean?
|
|
||||||
value.is_a?(FalseClass) || value.is_a?(TrueClass)
|
|
||||||
end
|
|
||||||
|
|
||||||
def string?
|
|
||||||
value.is_a? String
|
|
||||||
end
|
|
||||||
|
|
||||||
def text_area?
|
|
||||||
value == 'text'
|
|
||||||
end
|
|
||||||
|
|
||||||
def url?
|
|
||||||
value == 'url'
|
|
||||||
end
|
|
||||||
|
|
||||||
def email?
|
|
||||||
value == 'email' || value == 'mail'
|
|
||||||
end
|
|
||||||
alias mail? email?
|
|
||||||
|
|
||||||
def date?
|
|
||||||
value == 'date'
|
|
||||||
end
|
|
||||||
|
|
||||||
def password?
|
|
||||||
value == 'password'
|
|
||||||
end
|
|
||||||
|
|
||||||
def number?
|
|
||||||
value == 'number'
|
|
||||||
end
|
|
||||||
|
|
||||||
def year?
|
|
||||||
value == 'year'
|
|
||||||
end
|
|
||||||
|
|
||||||
def file?
|
|
||||||
string? && FILE_TYPES.include?(value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def image?
|
|
||||||
array? ? value.first == 'image' : value == 'image'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Si la plantilla es simple no está admitiendo Hashes como valores
|
|
||||||
def simple?
|
|
||||||
!complex?
|
|
||||||
end
|
|
||||||
|
|
||||||
def complex?
|
|
||||||
contents.is_a? Hash
|
|
||||||
end
|
|
||||||
|
|
||||||
# XXX Retrocompatibilidad
|
|
||||||
def to_s
|
|
||||||
key
|
|
||||||
end
|
|
||||||
|
|
||||||
# Convierte el campo en un parámetro
|
|
||||||
def to_param
|
|
||||||
if nested?
|
|
||||||
{ key.to_sym => {} }
|
|
||||||
elsif array? && multiple?
|
|
||||||
{ key.to_sym => [] }
|
|
||||||
else
|
|
||||||
key.to_sym
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Convierte la plantilla en el formato de front_matter
|
|
||||||
def to_front_matter
|
|
||||||
{ key => empty_value }
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_box_group?
|
|
||||||
array? && (complex? && contents.fetch('checkbox', false))
|
|
||||||
end
|
|
||||||
|
|
||||||
def radio_group?
|
|
||||||
array? && (complex? && contents.fetch('radio', false))
|
|
||||||
end
|
|
||||||
|
|
||||||
def array?
|
|
||||||
value.is_a? Array
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: detectar cuando es complejo y tomar el valor de :multiple
|
|
||||||
def multiple?
|
|
||||||
# si la plantilla es simple, es multiple cuando tenemos un array
|
|
||||||
return array? if simple?
|
|
||||||
|
|
||||||
array? && contents.fetch('multiple', true)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Detecta si el valor es una tabla de campos
|
|
||||||
def nested?
|
|
||||||
value.is_a?(Hash) || (array? && value.first.is_a?(Hash))
|
|
||||||
end
|
|
||||||
|
|
||||||
# Un campo acepta valores abiertos si no es un array con múltiples
|
|
||||||
# elementos
|
|
||||||
def open?
|
|
||||||
# Todos los valores simples son abiertos
|
|
||||||
return true unless complex?
|
|
||||||
return false unless array?
|
|
||||||
|
|
||||||
# La cosa se complejiza cuando tenemos valores complejos
|
|
||||||
#
|
|
||||||
# Si tenemos una lista cerrada de valores, necesitamos saber si el
|
|
||||||
# campo es abierto o cerrado. Si la lista tiene varios elementos,
|
|
||||||
# es una lista cerrada, opcionalmente abierta. Si la lista tiene
|
|
||||||
# un elemento, quiere decir que estamos autocompletando desde otro
|
|
||||||
# lado.
|
|
||||||
contents.fetch('open', value.count < 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
def closed?
|
|
||||||
!open?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Determina si los valores del campo serán públicos después
|
|
||||||
#
|
|
||||||
# XXX Esto es solo una indicación, el theme Jekyll tiene que
|
|
||||||
# respetarlos por su lado luego
|
|
||||||
def public?
|
|
||||||
# Todos los campos son públicos a menos que se indique lo
|
|
||||||
# contrario
|
|
||||||
simple? || contents.fetch('public', true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def private?
|
|
||||||
!public?
|
|
||||||
end
|
|
||||||
|
|
||||||
def human
|
|
||||||
h = key.humanize
|
|
||||||
|
|
||||||
h
|
|
||||||
end
|
|
||||||
|
|
||||||
def label
|
|
||||||
h = (complex? && contents.dig('label')) || human
|
|
||||||
h += ' *' if required?
|
|
||||||
|
|
||||||
h
|
|
||||||
end
|
|
||||||
|
|
||||||
def help
|
|
||||||
complex? && contents.dig('help')
|
|
||||||
end
|
|
||||||
|
|
||||||
def nested_fields
|
|
||||||
return unless nested?
|
|
||||||
|
|
||||||
v = value
|
|
||||||
v = value.first if array?
|
|
||||||
|
|
||||||
@nested_fields ||= v.map do |k, sv|
|
|
||||||
Post::TemplateField.new post, k, sv
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtiene los valores posibles para el campo de la plantilla
|
|
||||||
def values
|
|
||||||
return 'false' if value == false
|
|
||||||
return 'true' if value == true
|
|
||||||
# XXX por alguna razón `value` no refiere a value() :/
|
|
||||||
return '' if STRING_VALUES.include? value
|
|
||||||
# Las listas cerradas no necesitan mayor procesamiento
|
|
||||||
return value if array? && closed? && value.count > 1
|
|
||||||
# Y las vacías tampoco
|
|
||||||
return value if array? && value.empty?
|
|
||||||
# Ahorrarnos el trabajo
|
|
||||||
return @values if @values
|
|
||||||
|
|
||||||
# Duplicar el valor para no tener efectos secundarios luego (?)
|
|
||||||
value = self.value.dup
|
|
||||||
|
|
||||||
# Para obtener los valores posibles, hay que procesar la string y
|
|
||||||
# convertirla a parametros
|
|
||||||
|
|
||||||
# Si es una array de un solo elemento, es un indicador de que
|
|
||||||
# tenemos que rellenarla con los valores que indica.
|
|
||||||
#
|
|
||||||
# El primer valor es el que trae la string de autocompletado
|
|
||||||
values = array? ? value.shift : value
|
|
||||||
|
|
||||||
# Si el valor es un array con más de un elemento, queremos usar
|
|
||||||
# esas opciones. Pero si además es abierto, queremos traer los
|
|
||||||
# valores cargados anteriormente.
|
|
||||||
|
|
||||||
# Procesamos el valor, buscando : como separador de campos que
|
|
||||||
# queremos encontrar y luego los unimos
|
|
||||||
_value = (values&.split(':', 2) || []).map do |v|
|
|
||||||
# Tenemos hasta tres niveles de búsqueda
|
|
||||||
collection, attr, subattr = v.split('/', 3)
|
|
||||||
|
|
||||||
if collection == 'site'
|
|
||||||
# TODO: puede ser peligroso permitir acceder a cualquier
|
|
||||||
# atributo de site? No estamos trayendo nada fuera de
|
|
||||||
# lo normal
|
|
||||||
post.site.send(attr.to_sym)
|
|
||||||
# Si hay un subatributo, tenemos que averiguar todos los
|
|
||||||
# valores dentro de el
|
|
||||||
# TODO volver elegante!
|
|
||||||
# TODO volver recursivo!
|
|
||||||
elsif subattr
|
|
||||||
post.site.everything_of(attr, lang: collection)
|
|
||||||
.compact
|
|
||||||
.map { |sv| sv[subattr] }
|
|
||||||
.flatten
|
|
||||||
.compact
|
|
||||||
.uniq
|
|
||||||
else
|
|
||||||
post.site.everything_of(attr, lang: collection).compact
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Si el valor es abierto, sumar los valores auto-completados a
|
|
||||||
# lo pre-cargados.
|
|
||||||
#
|
|
||||||
# En este punto _value es un array de 1 o 2 arrays, si es de uno,
|
|
||||||
# value tambien tiene que serlo. Si es de 2, hay que unir cada
|
|
||||||
# una
|
|
||||||
if open?
|
|
||||||
if _value.count == 1
|
|
||||||
_value = [(_value.first + value).uniq]
|
|
||||||
elsif _value.count == 2
|
|
||||||
_value = _value.each_with_index.map do |v, i|
|
|
||||||
v + value.fetch(i, [])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Crea un array de arrays, útil para los select
|
|
||||||
# [ [ 1, a ], [ 2, b ] ]
|
|
||||||
# aunque si no hay un : en el autocompletado, el array queda
|
|
||||||
# [ [ 1, 1 ], [ 2, 2 ] ]
|
|
||||||
values = _value.empty? ? [] : _value.last.zip(_value.first)
|
|
||||||
|
|
||||||
# En última instancia, traer el valor por defecto y ahorrarnos
|
|
||||||
# volver a procesar
|
|
||||||
@values = values
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -26,7 +26,7 @@ class Site < ApplicationRecord
|
||||||
validates :design_id, presence: true
|
validates :design_id, presence: true
|
||||||
validates_inclusion_of :status, in: %w[waiting enqueued building]
|
validates_inclusion_of :status, in: %w[waiting enqueued building]
|
||||||
validates_presence_of :title
|
validates_presence_of :title
|
||||||
validates :description, length: { in: 50..160 }
|
validates :description, length: { in: 10..160 }
|
||||||
validate :deploy_local_presence
|
validate :deploy_local_presence
|
||||||
validate :compatible_layouts, on: :update
|
validate :compatible_layouts, on: :update
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ class Site < ApplicationRecord
|
||||||
belongs_to :design
|
belongs_to :design
|
||||||
belongs_to :licencia
|
belongs_to :licencia
|
||||||
|
|
||||||
|
has_many :stats
|
||||||
has_many :log_entries, dependent: :destroy
|
has_many :log_entries, dependent: :destroy
|
||||||
has_many :deploys, dependent: :destroy
|
has_many :deploys, dependent: :destroy
|
||||||
has_many :build_stats, through: :deploys
|
has_many :build_stats, through: :deploys
|
||||||
|
@ -56,7 +57,7 @@ class Site < ApplicationRecord
|
||||||
# Carga el sitio Jekyll una vez que se inicializa el modelo o después
|
# Carga el sitio Jekyll una vez que se inicializa el modelo o después
|
||||||
# de crearlo
|
# de crearlo
|
||||||
after_initialize :load_jekyll
|
after_initialize :load_jekyll
|
||||||
after_create :load_jekyll, :static_file_migration!
|
after_create :load_jekyll
|
||||||
# Cambiar el nombre del directorio
|
# Cambiar el nombre del directorio
|
||||||
before_update :update_name!
|
before_update :update_name!
|
||||||
before_save :add_private_key_if_missing!
|
before_save :add_private_key_if_missing!
|
||||||
|
@ -65,9 +66,6 @@ class Site < ApplicationRecord
|
||||||
|
|
||||||
accepts_nested_attributes_for :deploys, allow_destroy: true
|
accepts_nested_attributes_for :deploys, allow_destroy: true
|
||||||
|
|
||||||
# El sitio en Jekyll
|
|
||||||
attr_reader :jekyll
|
|
||||||
|
|
||||||
# XXX: Es importante incluir luego de los callbacks de :load_jekyll
|
# XXX: Es importante incluir luego de los callbacks de :load_jekyll
|
||||||
include Site::Index
|
include Site::Index
|
||||||
|
|
||||||
|
@ -180,29 +178,28 @@ class Site < ApplicationRecord
|
||||||
|
|
||||||
# Trae los datos del directorio _data dentro del sitio
|
# Trae los datos del directorio _data dentro del sitio
|
||||||
def data
|
def data
|
||||||
unless @jekyll.data.present?
|
unless jekyll.data.present?
|
||||||
@jekyll.reader.read_data
|
run_in_path do
|
||||||
|
jekyll.reader.read_data
|
||||||
# Define los valores por defecto según la llave buscada
|
jekyll.data['layouts'] ||= {}
|
||||||
@jekyll.data.default_proc = proc do |data, key|
|
|
||||||
data[key] = case key
|
|
||||||
when 'layout' then {}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@jekyll.data
|
jekyll.data
|
||||||
end
|
end
|
||||||
|
|
||||||
# Traer las colecciones. Todos los artículos van a estar dentro de
|
# Traer las colecciones. Todos los artículos van a estar dentro de
|
||||||
# colecciones.
|
# colecciones.
|
||||||
def collections
|
def collections
|
||||||
unless @read
|
unless @read
|
||||||
@jekyll.reader.read_collections
|
run_in_path do
|
||||||
|
jekyll.reader.read_collections
|
||||||
|
end
|
||||||
|
|
||||||
@read = true
|
@read = true
|
||||||
end
|
end
|
||||||
|
|
||||||
@jekyll.collections
|
jekyll.collections
|
||||||
end
|
end
|
||||||
|
|
||||||
# Traer la configuración de forma modificable
|
# Traer la configuración de forma modificable
|
||||||
|
@ -290,7 +287,9 @@ class Site < ApplicationRecord
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def theme_layouts
|
def theme_layouts
|
||||||
@jekyll.reader.read_layouts
|
run_in_path do
|
||||||
|
jekyll.reader.read_layouts
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Trae todos los valores disponibles para un campo
|
# Trae todos los valores disponibles para un campo
|
||||||
|
@ -332,6 +331,12 @@ class Site < ApplicationRecord
|
||||||
status == 'building'
|
status == 'building'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def jekyll
|
||||||
|
run_in_path do
|
||||||
|
@jekyll ||= Jekyll::Site.new(configuration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Cargar el sitio Jekyll
|
# Cargar el sitio Jekyll
|
||||||
#
|
#
|
||||||
# TODO: En lugar de leer todo junto de una vez, extraer la carga de
|
# TODO: En lugar de leer todo junto de una vez, extraer la carga de
|
||||||
|
@ -345,10 +350,7 @@ class Site < ApplicationRecord
|
||||||
|
|
||||||
def reload_jekyll!
|
def reload_jekyll!
|
||||||
reset
|
reset
|
||||||
|
jekyll
|
||||||
Dir.chdir(path) do
|
|
||||||
@jekyll = Jekyll::Site.new(configuration)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def reload
|
def reload
|
||||||
|
@ -472,11 +474,6 @@ class Site < ApplicationRecord
|
||||||
config.hostname = hostname
|
config.hostname = hostname
|
||||||
end
|
end
|
||||||
|
|
||||||
# Migra los archivos a Sutty
|
|
||||||
def static_file_migration!
|
|
||||||
Site::StaticFileMigration.new(site: self).migrate!
|
|
||||||
end
|
|
||||||
|
|
||||||
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
||||||
# y es la local
|
# y es la local
|
||||||
#
|
#
|
||||||
|
@ -526,4 +523,8 @@ class Site < ApplicationRecord
|
||||||
errors.add(:design_id,
|
errors.add(:design_id,
|
||||||
I18n.t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.error'))
|
I18n.t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.error'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def run_in_path(&block)
|
||||||
|
Dir.chdir path, &block
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Site
|
|
||||||
# Obtiene todos los archivos relacionados en artículos del sitio y los
|
|
||||||
# sube a Sutty.
|
|
||||||
class StaticFileMigration
|
|
||||||
# Tipos de metadatos que contienen archivos
|
|
||||||
STATIC_TYPES = %i[file image].freeze
|
|
||||||
|
|
||||||
attr_reader :site
|
|
||||||
|
|
||||||
def initialize(site:)
|
|
||||||
@site = site
|
|
||||||
end
|
|
||||||
|
|
||||||
def migrate!
|
|
||||||
modified = site.docs.map do |doc|
|
|
||||||
next unless STATIC_TYPES.map do |field|
|
|
||||||
next unless doc.attribute? field
|
|
||||||
next unless doc[field].path?
|
|
||||||
next unless doc[field].static_file
|
|
||||||
|
|
||||||
true
|
|
||||||
end.any?
|
|
||||||
|
|
||||||
log.write "#{doc.path.relative};no se pudo guardar\n" unless doc.save(validate: false)
|
|
||||||
|
|
||||||
doc.path.absolute
|
|
||||||
end.compact
|
|
||||||
|
|
||||||
log.close
|
|
||||||
|
|
||||||
return if modified.empty?
|
|
||||||
|
|
||||||
# TODO: Hacer la migración desde el servicio de creación de sitios?
|
|
||||||
site.repository.commit(file: modified,
|
|
||||||
message: I18n.t('sites.static_file_migration'),
|
|
||||||
usuarie: author)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def author
|
|
||||||
@author ||= GitAuthor.new email: "sutty@#{Site.domain}",
|
|
||||||
name: 'Sutty'
|
|
||||||
end
|
|
||||||
|
|
||||||
def log
|
|
||||||
@log ||= File.open(File.join(site.path, 'migration.csv'), 'w')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
10
app/models/stat.rb
Normal file
10
app/models/stat.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Registran cuándo fue la última recolección de datos.
|
||||||
|
class Stat < ApplicationRecord
|
||||||
|
# XXX: Los intervalos van en orden de mayor especificidad a menor
|
||||||
|
INTERVALS = %i[day month year].freeze
|
||||||
|
RESOURCES = %i[builds space_used build_time].freeze
|
||||||
|
|
||||||
|
belongs_to :site
|
||||||
|
end
|
|
@ -12,4 +12,16 @@ class SiteStatPolicy
|
||||||
def index?
|
def index?
|
||||||
site_stat.site.usuarie? usuarie
|
site_stat.site.usuarie? usuarie
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def host?
|
||||||
|
index?
|
||||||
|
end
|
||||||
|
|
||||||
|
def resources?
|
||||||
|
index?
|
||||||
|
end
|
||||||
|
|
||||||
|
def uris?
|
||||||
|
index?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -122,6 +122,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
# la búsqueda.
|
# la búsqueda.
|
||||||
def change_licencias
|
def change_licencias
|
||||||
site.locales.each do |locale|
|
site.locales.each do |locale|
|
||||||
|
next unless I18n.available_locales.include? locale
|
||||||
|
|
||||||
Mobility.with_locale(locale) do
|
Mobility.with_locale(locale) do
|
||||||
permalink = "#{I18n.t('activerecord.models.licencia').downcase}/"
|
permalink = "#{I18n.t('activerecord.models.licencia').downcase}/"
|
||||||
post = site.posts(lang: locale).find_by(permalink: permalink)
|
post = site.posts(lang: locale).find_by(permalink: permalink)
|
||||||
|
|
3
app/views/posts/attribute_ro/_float.haml
Normal file
3
app/views/posts/attribute_ro/_float.haml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
%tr{ id: attribute }
|
||||||
|
%th= post_label_t(attribute, post: post)
|
||||||
|
%td{ dir: dir, lang: locale }= metadata.value
|
3
app/views/posts/attribute_ro/_predefined_value.haml
Normal file
3
app/views/posts/attribute_ro/_predefined_value.haml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
%tr{ id: attribute }
|
||||||
|
%th= post_label_t(attribute, post: post)
|
||||||
|
%td{ dir: dir, lang: locale }= metadata.value
|
|
@ -95,6 +95,9 @@
|
||||||
%button.btn{ type: 'button', title: t('editor.right'), data: { editor_button: 'parentBlock-right' } }>
|
%button.btn{ type: 'button', title: t('editor.right'), data: { editor_button: 'parentBlock-right' } }>
|
||||||
%i.fa.fa-fw.fa-align-right>
|
%i.fa.fa-fw.fa-align-right>
|
||||||
%span.sr-only>= t('editor.right')
|
%span.sr-only>= t('editor.right')
|
||||||
|
%button.btn{ type: 'button', title: t('editor.blockquote'), data: { editor_button: 'block-blockquote' } }>
|
||||||
|
%i.fa.fa-fw.fa-quote-left>
|
||||||
|
%span.sr-only>= t('editor.blockquote')
|
||||||
|
|
||||||
-# HAML cringe
|
-# HAML cringe
|
||||||
.editor-auxiliary-toolbar.mt-1.scrollbar-black{ data: { editor_auxiliary_toolbar: '' } }
|
.editor-auxiliary-toolbar.mt-1.scrollbar-black{ data: { editor_auxiliary_toolbar: '' } }
|
||||||
|
|
6
app/views/posts/attributes/_float.haml
Normal file
6
app/views/posts/attributes/_float.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.form-group
|
||||||
|
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
|
||||||
|
= number_field base, attribute, value: metadata.value, step: metadata.step,
|
||||||
|
**field_options(attribute, metadata)
|
||||||
|
= render 'posts/attribute_feedback',
|
||||||
|
post: post, attribute: attribute, metadata: metadata
|
7
app/views/posts/attributes/_predefined_value.haml
Normal file
7
app/views/posts/attributes/_predefined_value.haml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.form-group
|
||||||
|
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
|
||||||
|
= select_tag(plain_field_name_for(base, attribute),
|
||||||
|
options_for_select(metadata.values, metadata.value),
|
||||||
|
**field_options(attribute, metadata), include_blank: t('.empty'))
|
||||||
|
= render 'posts/attribute_feedback',
|
||||||
|
post: post, attribute: attribute, metadata: metadata
|
|
@ -40,7 +40,7 @@
|
||||||
%section.col
|
%section.col
|
||||||
= render 'layouts/flash'
|
= render 'layouts/flash'
|
||||||
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
|
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
|
||||||
%form
|
%form{ action: site_posts_path }
|
||||||
- @filter_params.each do |param, value|
|
- @filter_params.each do |param, value|
|
||||||
- next if param == 'q'
|
- next if param == 'q'
|
||||||
%input{ type: 'hidden', name: param, value: value }
|
%input{ type: 'hidden', name: param, value: value }
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
- if @site.locales.size > 1
|
- if @site.locales.size > 1
|
||||||
%nav#locales
|
%nav#locales
|
||||||
- @site.locales.each do |locale|
|
- @site.locales.each do |locale|
|
||||||
= link_to t("locales.#{locale}.name"), site_posts_path(@site, **@filter_params.merge(locale: locale)),
|
= link_to @site.data.dig(locale.to_s, 'locale') || locale, site_posts_path(@site, **@filter_params.merge(locale: locale)),
|
||||||
class: "mr-2 mt-2 mb-2 #{locale == @locale ? 'active font-weight-bold' : ''}"
|
class: "mr-2 mt-2 mb-2 #{locale == @locale ? 'active font-weight-bold' : ''}"
|
||||||
.pl-2-plus
|
.pl-2-plus
|
||||||
- @filter_params.each do |param, value|
|
- @filter_params.each do |param, value|
|
||||||
|
@ -67,19 +67,24 @@
|
||||||
%h2= t('posts.empty')
|
%h2= t('posts.empty')
|
||||||
- else
|
- else
|
||||||
= form_tag site_posts_reorder_path, method: :post do
|
= form_tag site_posts_reorder_path, method: :post do
|
||||||
|
%input{ type: 'hidden', name: 'post[lang]', value: @locale }
|
||||||
%table.table{ data: { controller: 'reorder' } }
|
%table.table{ data: { controller: 'reorder' } }
|
||||||
%caption.sr-only= t('posts.caption')
|
%caption.sr-only= t('posts.caption')
|
||||||
%thead
|
%thead
|
||||||
%tr
|
%tr
|
||||||
%th.border-0.background-white.position-sticky{ style: 'top: 0; z-index: 2', colspan: '4' }
|
%th.border-0.background-white.position-sticky{ style: 'top: 0; z-index: 2', colspan: '4' }
|
||||||
= submit_tag t('posts.reorder.submit'), class: 'btn'
|
.d-flex.flex-row.justify-content-between
|
||||||
%button.btn{ data: { action: 'reorder#unselect' } }
|
%div
|
||||||
= t('posts.reorder.unselect')
|
= submit_tag t('posts.reorder.submit'), class: 'btn'
|
||||||
%span.badge{ data: { target: 'reorder.counter' } } 0
|
%button.btn{ data: { action: 'reorder#unselect' } }
|
||||||
%button.btn{ data: { action: 'reorder#up' } }= t('posts.reorder.up')
|
= t('posts.reorder.unselect')
|
||||||
%button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down')
|
%span.badge{ data: { target: 'reorder.counter' } } 0
|
||||||
%button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
|
%button.btn{ data: { action: 'reorder#up' } }= t('posts.reorder.up')
|
||||||
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
|
%button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down')
|
||||||
|
%button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
|
||||||
|
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
|
||||||
|
|
||||||
|
%div
|
||||||
%tbody
|
%tbody
|
||||||
- dir = t("locales.#{@locale}.dir")
|
- dir = t("locales.#{@locale}.dir")
|
||||||
- size = @posts.size
|
- size = @posts.size
|
||||||
|
@ -104,19 +109,19 @@
|
||||||
%span{ lang: post.locale, dir: dir }= post.title
|
%span{ lang: post.locale, dir: dir }= post.title
|
||||||
- if post.front_matter['draft'].present?
|
- if post.front_matter['draft'].present?
|
||||||
%span.badge.badge-primary= I18n.t('posts.attributes.draft.label')
|
%span.badge.badge-primary= I18n.t('posts.attributes.draft.label')
|
||||||
- if post.front_matter['categories'].present?
|
%br
|
||||||
%br
|
%small
|
||||||
%small
|
= link_to @site.layouts[post.layout].humanized_name, site_posts_path(@site, **@filter_params.merge(layout: post.layout))
|
||||||
- post.front_matter['categories'].each do |category|
|
- post.front_matter['categories']&.each do |category|
|
||||||
= link_to site_posts_path(@site, **@filter_params.merge(category: category)) do
|
= link_to site_posts_path(@site, **@filter_params.merge(category: category)) do
|
||||||
%span{ lang: post.locale, dir: dir }= category
|
%span{ lang: post.locale, dir: dir }= category
|
||||||
= '/' unless post.front_matter['categories'].last == category
|
= '/' unless post.front_matter['categories'].last == category
|
||||||
|
|
||||||
%td
|
%td.text-nowrap
|
||||||
= post.created_at.strftime('%F')
|
= post.created_at.strftime('%F')
|
||||||
%br/
|
%br/
|
||||||
= post.order
|
= post.order
|
||||||
%td
|
%td.text-nowrap
|
||||||
- if @usuarie || policy(post).edit?
|
- if @usuarie || policy(post).edit?
|
||||||
= link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block'
|
= link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block'
|
||||||
- if @usuarie || policy(post).destroy?
|
- if @usuarie || policy(post).destroy?
|
||||||
|
|
|
@ -38,4 +38,4 @@
|
||||||
|
|
||||||
- cache [metadata, I18n.locale] do
|
- cache [metadata, I18n.locale] do
|
||||||
%section.editor{ id: attr, dir: dir }
|
%section.editor{ id: attr, dir: dir }
|
||||||
= @post.public_send(attr).to_s.html_safe
|
= @post.public_send(attr).value.html_safe
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
%h2= f.label :description
|
%h2= f.label :description
|
||||||
%p.lead= t('.help.description')
|
%p.lead= t('.help.description')
|
||||||
= f.text_area :description, class: form_control(site, :description),
|
= f.text_area :description, class: form_control(site, :description),
|
||||||
maxlength: 160, minlength: 50, required: true
|
maxlength: 160, minlength: 10, required: true
|
||||||
- if invalid? site, :description
|
- if invalid? site, :description
|
||||||
.invalid-feedback= site.errors.messages[:description].join(', ')
|
.invalid-feedback= site.errors.messages[:description].join(', ')
|
||||||
%hr/
|
%hr/
|
||||||
|
|
|
@ -1,17 +1,43 @@
|
||||||
= render 'layouts/breadcrumb',
|
|
||||||
crumbs: [link_to(t('sites.index.title'), sites_path),
|
|
||||||
link_to(@site.name, site_path(@site)), t('.title')]
|
|
||||||
|
|
||||||
.row
|
.row
|
||||||
.col
|
.col
|
||||||
%h1= t('.title')
|
%h1= t('.title')
|
||||||
%p.lead= t('.help')
|
%p.lead= t('.help')
|
||||||
|
- if @last_stat
|
||||||
|
%p
|
||||||
|
%small
|
||||||
|
= t('.last_update')
|
||||||
|
%time{ datetime: @last_stat.created_at }
|
||||||
|
#{time_ago_in_words @last_stat.created_at}.
|
||||||
|
|
||||||
%table.table.table-condensed
|
.mb-5
|
||||||
%tbody
|
- Stat::INTERVALS.each do |interval|
|
||||||
%tr
|
= link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls]), class: "btn #{'btn-primary active' if @interval == interval}"
|
||||||
%td= t('.build.average')
|
|
||||||
%td= distance_of_time_in_words_if_more_than_a_minute @build_avg
|
.mb-5
|
||||||
%tr
|
%h2= t('.host.title', count: @hostnames.size)
|
||||||
%td= t('.build.maximum')
|
%p.lead= t('.host.description')
|
||||||
%td= distance_of_time_in_words_if_more_than_a_minute @build_max
|
= line_chart site_stats_host_path(@chart_params), **@chart_options
|
||||||
|
|
||||||
|
.mb-5
|
||||||
|
%h2= t('.urls.title')
|
||||||
|
%p.lead= t('.urls.description')
|
||||||
|
%form
|
||||||
|
%input{ type: 'hidden', name: 'interval', value: @interval }
|
||||||
|
.form-group
|
||||||
|
%label{ for: 'urls' }= t('.urls.label')
|
||||||
|
%textarea#urls.form-control{ name: 'urls', autocomplete: 'on', required: true, rows: @normalized_urls.size, aria_describedby: 'help-urls' }= @normalized_urls.join("\n")
|
||||||
|
%small#help-urls.feedback.form-text.text-muted= t('.urls.help')
|
||||||
|
.form-group
|
||||||
|
%button.btn{ type: 'submit' }= t('.urls.submit')
|
||||||
|
- if @normalized_urls.present?
|
||||||
|
= line_chart site_stats_uris_path(urls: params[:urls], **@chart_params), **@chart_options
|
||||||
|
|
||||||
|
.mb-5
|
||||||
|
%h2= t('.resources.title')
|
||||||
|
%p.lead= t('.resources.description')
|
||||||
|
|
||||||
|
- Stat::RESOURCES.each do |resource|
|
||||||
|
.mb-5
|
||||||
|
%h3= t(".resources.#{resource}.title")
|
||||||
|
%p.lead= t(".resources.#{resource}.description")
|
||||||
|
= line_chart site_stats_resources_path(resource: resource, **@chart_params), **@chart_options.merge(StatsController::EXTRA_OPTIONS[resource])
|
||||||
|
|
|
@ -38,6 +38,12 @@ module Sutty
|
||||||
|
|
||||||
config.active_storage.variant_processor = :vips
|
config.active_storage.variant_processor = :vips
|
||||||
|
|
||||||
|
config.to_prepare do
|
||||||
|
Dir.glob(File.join(File.dirname(__FILE__), '..', 'app', '**', '*_decorator.rb')).sort.each do |c|
|
||||||
|
Rails.configuration.cache_classes ? require(c) : load(c)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
config.after_initialize do
|
config.after_initialize do
|
||||||
ActiveStorage::DirectUploadsController.include ActiveStorage::AuthenticatedDirectUploadsController
|
ActiveStorage::DirectUploadsController.include ActiveStorage::AuthenticatedDirectUploadsController
|
||||||
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# TODO: Estamos procesando el análisis de los archivos en el momento
|
|
||||||
# porque queremos obtener la ruta del archivo en el momento y no
|
|
||||||
# después. Necesitaríamos poder generar el vínculo en el
|
|
||||||
# repositorio a destiempo, modificando el Job de ActiveStorage
|
|
||||||
ActiveStorage::AnalyzeJob.queue_adapter = :inline
|
|
|
@ -11,6 +11,8 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||||
inflect.singular 'licencias', 'licencia'
|
inflect.singular 'licencias', 'licencia'
|
||||||
inflect.plural 'rol', 'roles'
|
inflect.plural 'rol', 'roles'
|
||||||
inflect.singular 'roles', 'rol'
|
inflect.singular 'roles', 'rol'
|
||||||
|
inflect.plural 'rollup', 'rollups'
|
||||||
|
inflect.singular 'rollups', 'rollup'
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveSupport::Inflector.inflections(:es) do |inflect|
|
ActiveSupport::Inflector.inflections(:es) do |inflect|
|
||||||
|
@ -24,4 +26,6 @@ ActiveSupport::Inflector.inflections(:es) do |inflect|
|
||||||
inflect.singular 'roles', 'rol'
|
inflect.singular 'roles', 'rol'
|
||||||
inflect.plural 'licencia', 'licencias'
|
inflect.plural 'licencia', 'licencias'
|
||||||
inflect.singular 'licencias', 'licencia'
|
inflect.singular 'licencias', 'licencia'
|
||||||
|
inflect.plural 'rollup', 'rollups'
|
||||||
|
inflect.singular 'rollups', 'rollup'
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,8 +19,8 @@ en:
|
||||||
remember_me: 'Keeps session open for %{remember_for}'
|
remember_me: 'Keeps session open for %{remember_for}'
|
||||||
actions:
|
actions:
|
||||||
sr-help: "After this form you'll find links to recover your account and other actions."
|
sr-help: "After this form you'll find links to recover your account and other actions."
|
||||||
_true: Yes
|
_true: 'Yes'
|
||||||
_false: No
|
_false: 'No'
|
||||||
svg:
|
svg:
|
||||||
sutty:
|
sutty:
|
||||||
title: Sutty
|
title: Sutty
|
||||||
|
@ -41,10 +41,12 @@ en:
|
||||||
not_an_image: 'Not an image'
|
not_an_image: 'Not an image'
|
||||||
path_required: 'Missing image for upload'
|
path_required: 'Missing image for upload'
|
||||||
no_file_for_description: "Description with no associated image"
|
no_file_for_description: "Description with no associated image"
|
||||||
|
attachment_missing: "I couldn't save the image :("
|
||||||
file:
|
file:
|
||||||
site_invalid: 'The file cannot be stored if the site configuration is not valid'
|
site_invalid: 'The file cannot be stored if the site configuration is not valid'
|
||||||
path_required: "Missing file for upload"
|
path_required: "Missing file for upload"
|
||||||
no_file_for_description: "Description with no associated file"
|
no_file_for_description: "Description with no associated file"
|
||||||
|
attachment_missing: "I couldn't save the file :("
|
||||||
event:
|
event:
|
||||||
zone_missing: 'Inexistent timezone'
|
zone_missing: 'Inexistent timezone'
|
||||||
date_missing: 'Event date is required'
|
date_missing: 'Event date is required'
|
||||||
|
@ -163,7 +165,7 @@ en:
|
||||||
signature: 'With love, Sutty'
|
signature: 'With love, Sutty'
|
||||||
breadcrumb:
|
breadcrumb:
|
||||||
title: 'Your location in Sutty'
|
title: 'Your location in Sutty'
|
||||||
logout: Exit
|
logout: Log out
|
||||||
mutual_aid: Mutual aid
|
mutual_aid: Mutual aid
|
||||||
collaborations:
|
collaborations:
|
||||||
collaborate:
|
collaborate:
|
||||||
|
@ -252,9 +254,38 @@ en:
|
||||||
help: |
|
help: |
|
||||||
These statistics show information about how your site is generated and
|
These statistics show information about how your site is generated and
|
||||||
how many resources it uses.
|
how many resources it uses.
|
||||||
build:
|
last_update: 'Updated every hour. Last update on '
|
||||||
average: 'Average building time'
|
empty: 'There is no enough information yet. We invite you to come back in %{please_return_at}!'
|
||||||
maximum: 'Maximum building time'
|
loading: 'Loading...'
|
||||||
|
hour: 'Hourly'
|
||||||
|
day: 'Daily'
|
||||||
|
week: 'Weekly'
|
||||||
|
month: 'Monthly'
|
||||||
|
year: 'Yearly'
|
||||||
|
host:
|
||||||
|
title:
|
||||||
|
zero: 'Site visits'
|
||||||
|
one: 'Site visits'
|
||||||
|
other: 'Visits by domain name'
|
||||||
|
description: 'Counts visited pages on your site, grouped by domain names in use.'
|
||||||
|
urls:
|
||||||
|
title: 'Visits by URL'
|
||||||
|
description: 'Counts visits or downloads on any URL.'
|
||||||
|
label: 'URLs ("links")'
|
||||||
|
help: 'Copy and paste a single URL per line'
|
||||||
|
submit: 'Update graph'
|
||||||
|
resources:
|
||||||
|
title: 'Resource usage'
|
||||||
|
description: "In this section you can find statistics on your site's use of Sutty's shared resources"
|
||||||
|
builds:
|
||||||
|
title: 'Site publication'
|
||||||
|
description: 'Times you published your site.'
|
||||||
|
space_used:
|
||||||
|
title: 'Server disk usage'
|
||||||
|
description: 'Average storage space used by your site.'
|
||||||
|
build_time:
|
||||||
|
title: 'Publication time'
|
||||||
|
description: 'Average time your site takes to build.'
|
||||||
sites:
|
sites:
|
||||||
donations:
|
donations:
|
||||||
url: 'https://donaciones.sutty.nl/en/'
|
url: 'https://donaciones.sutty.nl/en/'
|
||||||
|
@ -376,6 +407,8 @@ en:
|
||||||
en: 'English'
|
en: 'English'
|
||||||
ar: 'Arabic'
|
ar: 'Arabic'
|
||||||
posts:
|
posts:
|
||||||
|
prev: Previous page
|
||||||
|
next: Next page
|
||||||
empty: "There are no results for those search parameters."
|
empty: "There are no results for those search parameters."
|
||||||
caption: Post list
|
caption: Post list
|
||||||
attribute_ro:
|
attribute_ro:
|
||||||
|
@ -413,6 +446,8 @@ en:
|
||||||
destroy: Remove image
|
destroy: Remove image
|
||||||
belongs_to:
|
belongs_to:
|
||||||
empty: "(Empty)"
|
empty: "(Empty)"
|
||||||
|
predefined_value:
|
||||||
|
empty: "(Empty)"
|
||||||
draft:
|
draft:
|
||||||
label: Draft
|
label: Draft
|
||||||
reorder:
|
reorder:
|
||||||
|
@ -535,6 +570,7 @@ en:
|
||||||
left: Left
|
left: Left
|
||||||
right: Right
|
right: Right
|
||||||
center: Center
|
center: Center
|
||||||
|
blockquote: Quote
|
||||||
color: Color
|
color: Color
|
||||||
text-color: Text color
|
text-color: Text color
|
||||||
multimedia: Media
|
multimedia: Media
|
||||||
|
|
|
@ -19,8 +19,8 @@ es:
|
||||||
remember_me: 'Mantiene la sesión abierta por %{remember_for}'
|
remember_me: 'Mantiene la sesión abierta por %{remember_for}'
|
||||||
actions:
|
actions:
|
||||||
sr-help: 'Después del formulario encontrarás vínculos para recuperar tu cuenta, entre otras acciones.'
|
sr-help: 'Después del formulario encontrarás vínculos para recuperar tu cuenta, entre otras acciones.'
|
||||||
_true: Sí
|
_true: 'Sí'
|
||||||
_false: No
|
_false: 'No'
|
||||||
svg:
|
svg:
|
||||||
sutty:
|
sutty:
|
||||||
title: Sutty
|
title: Sutty
|
||||||
|
@ -41,10 +41,12 @@ es:
|
||||||
not_an_image: 'No es una imagen'
|
not_an_image: 'No es una imagen'
|
||||||
path_required: 'Se necesita una imagen'
|
path_required: 'Se necesita una imagen'
|
||||||
no_file_for_description: 'Se envió una descripción sin imagen asociada'
|
no_file_for_description: 'Se envió una descripción sin imagen asociada'
|
||||||
|
attachment_missing: 'no pude guardar el archivo :('
|
||||||
file:
|
file:
|
||||||
site_invalid: 'El archivo no se puede almacenar si la configuración del sitio no es válida'
|
site_invalid: 'El archivo no se puede almacenar si la configuración del sitio no es válida'
|
||||||
path_required: 'Se necesita un archivo'
|
path_required: 'Se necesita un archivo'
|
||||||
no_file_for_description: 'se envió una descripción sin archivo asociado'
|
no_file_for_description: 'se envió una descripción sin archivo asociado'
|
||||||
|
attachment_missing: 'no pude guardar el archivo :('
|
||||||
event:
|
event:
|
||||||
zone_missing: 'El huso horario no es correcto'
|
zone_missing: 'El huso horario no es correcto'
|
||||||
date_missing: 'La fecha es obligatoria'
|
date_missing: 'La fecha es obligatoria'
|
||||||
|
@ -163,7 +165,7 @@ es:
|
||||||
signature: 'Con cariño, Sutty'
|
signature: 'Con cariño, Sutty'
|
||||||
breadcrumb:
|
breadcrumb:
|
||||||
title: 'Tu ubicación en Sutty'
|
title: 'Tu ubicación en Sutty'
|
||||||
logout: Salir
|
logout: Cerrar sesión
|
||||||
mutual_aid: Ayuda mutua
|
mutual_aid: Ayuda mutua
|
||||||
collaborations:
|
collaborations:
|
||||||
collaborate:
|
collaborate:
|
||||||
|
@ -180,6 +182,7 @@ es:
|
||||||
category: 'Categoría'
|
category: 'Categoría'
|
||||||
sites:
|
sites:
|
||||||
index: 'Este es el listado de sitios que puedes editar.'
|
index: 'Este es el listado de sitios que puedes editar.'
|
||||||
|
edit_posts: 'Aquí verás el listado de todos los artículos y podrás editarlos o crear nuevos'
|
||||||
enqueued: 'El sitio está en la cola de espera para ser generado.
|
enqueued: 'El sitio está en la cola de espera para ser generado.
|
||||||
Una vez que este proceso termine, recibirás un correo indicando el
|
Una vez que este proceso termine, recibirás un correo indicando el
|
||||||
estado y si todo fue bien, se publicarán los cambios en tu sitio
|
estado y si todo fue bien, se publicarán los cambios en tu sitio
|
||||||
|
@ -256,9 +259,38 @@ es:
|
||||||
help: |
|
help: |
|
||||||
Las estadísticas visibilizan información sobre cómo se genera y
|
Las estadísticas visibilizan información sobre cómo se genera y
|
||||||
cuántos recursos utiliza tu sitio.
|
cuántos recursos utiliza tu sitio.
|
||||||
build:
|
last_update: 'Actualizadas cada hora. Última actualización hace '
|
||||||
average: 'Tiempo promedio de generación'
|
empty: 'Todavía no hay información suficiente. Te invitamos a volver en %{please_return_at} :)'
|
||||||
maximum: 'Tiempo máximo de generación'
|
loading: 'Cargando...'
|
||||||
|
hour: 'Por hora'
|
||||||
|
day: 'Diarias'
|
||||||
|
week: 'Semanales'
|
||||||
|
month: 'Mensuales'
|
||||||
|
year: 'Anuales'
|
||||||
|
host:
|
||||||
|
title:
|
||||||
|
zero: 'Visitas del sitio'
|
||||||
|
one: 'Visitas del sitio'
|
||||||
|
other: 'Visitas agrupadas por nombre de dominio del sitio'
|
||||||
|
description: 'Cuenta la cantidad de páginas visitadas en tu sitio.'
|
||||||
|
urls:
|
||||||
|
title: 'Visitas por dirección'
|
||||||
|
description: 'Cantidad de visitas o descargas por dirección.'
|
||||||
|
label: 'Direcciones web (URL, "links", vínculos)'
|
||||||
|
help: 'Copia y pega una dirección por línea.'
|
||||||
|
submit: 'Actualizar gráfico'
|
||||||
|
resources:
|
||||||
|
title: 'Uso de recursos'
|
||||||
|
description: 'En esta sección podrás acceder a estadísticas del uso de recursos compartidos con otros sitios alojados en Sutty.'
|
||||||
|
builds:
|
||||||
|
title: 'Publicaciones del sitio'
|
||||||
|
description: 'Cantidad de veces que publicaste tu sitio.'
|
||||||
|
space_used:
|
||||||
|
title: 'Espacio utilizado en el servidor'
|
||||||
|
description: 'Espacio en disco que ocupa en promedio tu sitio.'
|
||||||
|
build_time:
|
||||||
|
title: 'Tiempo de publicación'
|
||||||
|
description: 'Tiempo promedio que toma en publicarse tu sitio.'
|
||||||
sites:
|
sites:
|
||||||
donations:
|
donations:
|
||||||
url: 'https://donaciones.sutty.nl/'
|
url: 'https://donaciones.sutty.nl/'
|
||||||
|
@ -383,6 +415,8 @@ es:
|
||||||
en: 'inglés'
|
en: 'inglés'
|
||||||
ar: 'árabe'
|
ar: 'árabe'
|
||||||
posts:
|
posts:
|
||||||
|
prev: Página anterior
|
||||||
|
next: Página siguiente
|
||||||
empty: No hay artículos con estos parámetros de búsqueda.
|
empty: No hay artículos con estos parámetros de búsqueda.
|
||||||
caption: Lista de artículos
|
caption: Lista de artículos
|
||||||
attribute_ro:
|
attribute_ro:
|
||||||
|
@ -420,6 +454,8 @@ es:
|
||||||
destroy: 'Eliminar imagen'
|
destroy: 'Eliminar imagen'
|
||||||
belongs_to:
|
belongs_to:
|
||||||
empty: "(Vacío)"
|
empty: "(Vacío)"
|
||||||
|
predefined_value:
|
||||||
|
empty: "(Vacío)"
|
||||||
draft:
|
draft:
|
||||||
label: Borrador
|
label: Borrador
|
||||||
reorder:
|
reorder:
|
||||||
|
@ -542,6 +578,7 @@ es:
|
||||||
left: Izquierda
|
left: Izquierda
|
||||||
right: Derecha
|
right: Derecha
|
||||||
center: Centro
|
center: Centro
|
||||||
|
blockquote: Cita
|
||||||
color: Color
|
color: Color
|
||||||
text-color: Color del texto
|
text-color: Color del texto
|
||||||
multimedia: Multimedia
|
multimedia: Multimedia
|
||||||
|
|
|
@ -57,9 +57,10 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
# Gestionar artículos según idioma
|
# Gestionar artículos según idioma
|
||||||
nested do
|
nested do
|
||||||
scope '(:locale)' do
|
scope '/(:locale)', constraint: /[a-z]{2}/ do
|
||||||
post :'posts/reorder', to: 'posts#reorder'
|
post :'posts/reorder', to: 'posts#reorder'
|
||||||
resources :posts do
|
resources :posts do
|
||||||
|
get 'p/:page', action: :index, on: :collection
|
||||||
get :preview, to: 'posts#preview'
|
get :preview, to: 'posts#preview'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -75,5 +76,8 @@ Rails.application.routes.draw do
|
||||||
post 'reorder_posts', to: 'sites#reorder_posts'
|
post 'reorder_posts', to: 'sites#reorder_posts'
|
||||||
|
|
||||||
resources :stats, only: [:index]
|
resources :stats, only: [:index]
|
||||||
|
get :'stats/host', to: 'stats#host'
|
||||||
|
get :'stats/uris', to: 'stats#uris'
|
||||||
|
get :'stats/resources', to: 'stats#resources'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
10
db/migrate/20210722191718_add_request_uri_to_access_logs.rb
Normal file
10
db/migrate/20210722191718_add_request_uri_to_access_logs.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Agrega la columna de request_uri a la tabla de logs
|
||||||
|
class AddRequestUriToAccessLogs < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
return unless Rails.env.production?
|
||||||
|
|
||||||
|
add_column :access_logs, :request_uri, :string, default: ''
|
||||||
|
end
|
||||||
|
end
|
15
db/migrate/20210807003928_create_rollups.rb
Normal file
15
db/migrate/20210807003928_create_rollups.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Crear la tabla de Rollups
|
||||||
|
class CreateRollups < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :rollups do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :interval, null: false
|
||||||
|
t.datetime :time, null: false
|
||||||
|
t.jsonb :dimensions, null: false, default: {}
|
||||||
|
t.float :value
|
||||||
|
end
|
||||||
|
add_index :rollups, %i[name interval time dimensions], unique: true
|
||||||
|
end
|
||||||
|
end
|
18
db/migrate/20210807004941_add_create_at_to_access_logs.rb
Normal file
18
db/migrate/20210807004941_add_create_at_to_access_logs.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Cambia los msec a datetime para poder agregar por tiempos
|
||||||
|
class AddCreateAtToAccessLogs < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
add_column :access_logs, :created_at, :datetime, precision: 6
|
||||||
|
|
||||||
|
create_trigger(compatibility: 1).on(:access_logs).before(:insert) do
|
||||||
|
'new.created_at := to_timestamp(new.msec)'
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.execute('update access_logs set created_at = to_timestamp(msec);')
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :access_logs, :created_at
|
||||||
|
end
|
||||||
|
end
|
10
db/migrate/20210926205448_add_uniqueness.rb
Normal file
10
db/migrate/20210926205448_add_uniqueness.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Agrega índices únicos que pensábamos que ya existían.
|
||||||
|
class AddUniqueness < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_index :designs, :name, unique: true
|
||||||
|
add_index :designs, :gem, unique: true
|
||||||
|
add_index :licencias, :name, unique: true
|
||||||
|
end
|
||||||
|
end
|
12
db/migrate/20211008201239_create_stats.rb
Normal file
12
db/migrate/20211008201239_create_stats.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Una tabla que lleva el recuento de recolección de estadísticas, solo
|
||||||
|
# es necesario para saber cuándo se hicieron, si se hicieron y usar como
|
||||||
|
# caché.
|
||||||
|
class CreateStats < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :stats do |t|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,10 +1,38 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
s_pid=/srv/tmp/puma.pid
|
||||||
|
p_pid=/tmp/prometheus.pid
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
sutty)
|
start)
|
||||||
su app -c "cd /srv/http && foreman start migrate"
|
su rails -c "cd /srv && foreman run migrate"
|
||||||
daemonize -c /srv/http -u app /usr/bin/foreman start sutty
|
daemonize -c /srv -u rails /usr/bin/foreman start sutty
|
||||||
|
;;
|
||||||
|
|
||||||
|
stop)
|
||||||
|
cat $s_pid | xargs -r kill
|
||||||
|
;;
|
||||||
|
|
||||||
|
reload)
|
||||||
|
cat $s_pid | xargs -r kill -USR2
|
||||||
|
;;
|
||||||
|
|
||||||
|
prometheus)
|
||||||
|
case $2 in
|
||||||
|
start)
|
||||||
|
rm -f $p_pid
|
||||||
|
daemonize -c /srv -p $p_pid -l $p_pid -u rails /usr/bin/foreman start prometheus
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
cat $p_pid | xargs -r kill
|
||||||
|
rm -f $p_pid
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
|
||||||
|
blazer)
|
||||||
|
test -z "$2" || b="_$2"
|
||||||
|
su rails -c "cd /srv && foreman run blazer$b"
|
||||||
;;
|
;;
|
||||||
prometheus) daemonize -c /srv/http -p /tmp/prometheus.pid -l /tmp/prometheus.pid -u app /usr/bin/foreman start prometheus ;;
|
|
||||||
esac
|
esac
|
||||||
|
|
22
monit.conf
22
monit.conf
|
@ -1,31 +1,27 @@
|
||||||
check process sutty with pidfile /srv/http/tmp/puma.pid
|
check process sutty with pidfile /srv/tmp/puma.pid
|
||||||
start program = "/usr/local/bin/sutty sutty"
|
start program = "/usr/local/bin/sutty start"
|
||||||
stop program = "/bin/sh -c 'cat /srv/http/tmp/puma.pid | xargs kill'"
|
stop program = "/usr/local/bin/sutty stop"
|
||||||
|
|
||||||
check process prometheus with pidfile /tmp/prometheus.pid
|
check process prometheus with pidfile /tmp/prometheus.pid
|
||||||
start program = "/usr/local/bin/sutty prometheus"
|
start program = "/usr/local/bin/sutty prometheus start"
|
||||||
stop program = "/bin/sh -c 'cat /tmp/prometheus.pid | xargs kill'"
|
stop program = "/usr/local/bin/sutty prometheus start"
|
||||||
|
|
||||||
check program blazer_5m
|
check program blazer_5m
|
||||||
with path "/bin/sh -c 'cd /srv/http && foreman start blazer_5m'"
|
with path "/usr/local/bin/sutty blazer 5m"
|
||||||
as uid "app" and gid "www-data"
|
|
||||||
every 5 cycles
|
every 5 cycles
|
||||||
if status != 0 then alert
|
if status != 0 then alert
|
||||||
|
|
||||||
check program blazer_1h
|
check program blazer_1h
|
||||||
with path "/bin/sh -c 'cd /srv/http && foreman start blazer_1h'"
|
with path "/usr/local/bin/sutty blazer 1h"
|
||||||
as uid "app" and gid "www-data"
|
|
||||||
every 60 cycles
|
every 60 cycles
|
||||||
if status != 0 then alert
|
if status != 0 then alert
|
||||||
|
|
||||||
check program blazer_1d
|
check program blazer_1d
|
||||||
with path "/bin/sh -c 'cd /srv/http && foreman start blazer_1d'"
|
with path "/usr/local/bin/sutty blazer 1d"
|
||||||
as uid "app" and gid "www-data"
|
|
||||||
every 1440 cycles
|
every 1440 cycles
|
||||||
if status != 0 then alert
|
if status != 0 then alert
|
||||||
|
|
||||||
check program blazer
|
check program blazer
|
||||||
with path "/bin/sh -c 'cd /srv/http && foreman start blazer'"
|
with path "/usr/local/bin/sutty blazer"
|
||||||
as uid "app" and gid "www-data"
|
|
||||||
every 61 cycles
|
every 61 cycles
|
||||||
if status != 0 then alert
|
if status != 0 then alert
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
"@rails/webpacker": "5.2.1",
|
"@rails/webpacker": "5.2.1",
|
||||||
"@suttyweb/editor": "^0.1.3",
|
"@suttyweb/editor": "^0.1.3",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
|
"chart.js": "^3.5.1",
|
||||||
|
"chartkick": "^4.0.5",
|
||||||
"circular-dependency-plugin": "^5.2.2",
|
"circular-dependency-plugin": "^5.2.2",
|
||||||
"commonmark": "^0.29.0",
|
"commonmark": "^0.29.0",
|
||||||
"fork-awesome": "^1.1.7",
|
"fork-awesome": "^1.1.7",
|
||||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -2124,6 +2124,25 @@ chalk@^4.1.0:
|
||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
chart.js@>=3.0.2, chart.js@^3.5.1:
|
||||||
|
version "3.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.5.1.tgz#73e24d23a4134a70ccdb5e79a917f156b6f3644a"
|
||||||
|
integrity sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==
|
||||||
|
|
||||||
|
chartjs-adapter-date-fns@>=2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
|
||||||
|
integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
|
||||||
|
|
||||||
|
chartkick@^4.0.5:
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/chartkick/-/chartkick-4.0.5.tgz#310a60c931e8ceedc39adee2ef8e9d1e474cb0e6"
|
||||||
|
integrity sha512-xKak4Fsgfvp1hj/LykRKkniDMaZASx2A4TdVc/sfsiNFFNf1m+D7PGwP1vgj1UsbsCjOCSfGWWyJpOYxkUCBug==
|
||||||
|
optionalDependencies:
|
||||||
|
chart.js ">=3.0.2"
|
||||||
|
chartjs-adapter-date-fns ">=2.0.0"
|
||||||
|
date-fns ">=2.0.0"
|
||||||
|
|
||||||
chokidar@^2.1.8:
|
chokidar@^2.1.8:
|
||||||
version "2.1.8"
|
version "2.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
|
||||||
|
@ -2749,6 +2768,11 @@ dashdash@^1.12.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
assert-plus "^1.0.0"
|
assert-plus "^1.0.0"
|
||||||
|
|
||||||
|
date-fns@>=2.0.0:
|
||||||
|
version "2.24.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.24.0.tgz#7d86dc0d93c87b76b63d213b4413337cfd1c105d"
|
||||||
|
integrity sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==
|
||||||
|
|
||||||
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
|
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
|
Loading…
Reference in a new issue