mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 04:56:21 +00:00
Merge branch 'issue-9357-1' of 0xacab.org:sutty/sutty into issue-9357-1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
commit
2573e7864b
171 changed files with 3912 additions and 715 deletions
|
@ -2,3 +2,4 @@
|
||||||
*
|
*
|
||||||
# Solo agregar lo que usamos en COPY
|
# Solo agregar lo que usamos en COPY
|
||||||
# !./archivo
|
# !./archivo
|
||||||
|
!./monit.conf
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
NODE_OPTIONS=--openssl-legacy-provider
|
NODE_OPTIONS=--openssl-legacy-provider
|
||||||
|
# pwgen -1 32
|
||||||
|
RAILS_MASTER_KEY=11111111111111111111111111111111
|
||||||
RAILS_GROUPS=assets
|
RAILS_GROUPS=assets
|
||||||
DELEGATE=athshe.sutty.nl
|
DELEGATE=athshe.sutty.nl
|
||||||
HAINISH=../haini.sh/haini.sh
|
HAINISH=../haini.sh/haini.sh
|
||||||
DATABASE=
|
DATABASE_URL=postgres://suttier@postgresql.sutty.local/sutty
|
||||||
RAILS_ENV=development
|
RAILS_ENV=development
|
||||||
IMAP_SERVER=
|
IMAP_SERVER=
|
||||||
DEFAULT_FROM=
|
DEFAULT_FROM=
|
||||||
|
|
33
.gitlab-ci.yml
Normal file
33
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
image: "gitea.nulo.in/sutty/panel:3.14.10-2.7.8-panel.sutty.nl"
|
||||||
|
variables:
|
||||||
|
RAILS_ENV: "production"
|
||||||
|
LC_ALL: "C.UTF-8"
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- "vendor/ruby"
|
||||||
|
assets:
|
||||||
|
stage: "build"
|
||||||
|
rules:
|
||||||
|
- if: "$CI_COMMIT_BRANCH == \"panel.sutty.nl\""
|
||||||
|
- if: "$CI_COMMIT_BRANCH"
|
||||||
|
changes:
|
||||||
|
compare_to: "refs/heads/rails"
|
||||||
|
paths:
|
||||||
|
- "package.json"
|
||||||
|
- "app/javascript/**/*"
|
||||||
|
- "app/assets/**/*"
|
||||||
|
before_script:
|
||||||
|
- "git config --global user.email \"${GIT_USER_EMAIL:-$GITLAB_USER_EMAIL}\""
|
||||||
|
- "git config --global user.name \"${GIT_USER_NAME:-$GITLAB_USER_NAME}\""
|
||||||
|
- "git remote set-url --push origin \"https://${GITLAB_USERNAME}:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git\""
|
||||||
|
- "apk add python2 dotenv brotli"
|
||||||
|
- "mv config/credentials.yml.enc.ci config/credentials.yml.enc"
|
||||||
|
- "cp .env.example .env"
|
||||||
|
- "dotenv bundle install --path=vendor"
|
||||||
|
script:
|
||||||
|
- "dotenv RAILS_ENV=production bundle exec rails webpacker:clobber"
|
||||||
|
- "dotenv RAILS_ENV=production bundle exec rails assets:precompile"
|
||||||
|
- "dotenv RAILS_ENV=production bundle exec rails assets:clean"
|
||||||
|
after_script:
|
||||||
|
- "git add public && git commit -m \"ci: assets [skip ci]\""
|
||||||
|
- "git push -o ci.skip"
|
9
.profile
Normal file
9
.profile
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Color_Off='\e[0m'
|
||||||
|
BPurple='\e[1;35m'
|
||||||
|
BBlue='\e[1;34m'
|
||||||
|
|
||||||
|
is_git() {
|
||||||
|
git rev-parse --abbrev-ref HEAD 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
PS1="\[${BPurple}\]\$(is_git) \[${BBlue}\]\W\[${Color_Off}\] >_ "
|
72
.woodpecker.yml
Normal file
72
.woodpecker.yml
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
pipeline:
|
||||||
|
publish:
|
||||||
|
image: "docker.io/woodpeckerci/plugin-docker-buildx"
|
||||||
|
settings:
|
||||||
|
registry: "gitea.nulo.in"
|
||||||
|
username: "sutty"
|
||||||
|
repo: "gitea.nulo.in/sutty/panel"
|
||||||
|
tags:
|
||||||
|
- "${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}-${CI_COMMIT_BRANCH}"
|
||||||
|
- "latest"
|
||||||
|
build_args:
|
||||||
|
- "RUBY_VERSION=${RUBY_VERSION}"
|
||||||
|
- "RUBY_PATCH=${RUBY_PATCH}"
|
||||||
|
- "ALPINE_VERSION=${ALPINE_VERSION}"
|
||||||
|
- "BASE_IMAGE=gitea.nulo.in/sutty/rails"
|
||||||
|
purge: false
|
||||||
|
secrets:
|
||||||
|
- "DOCKER_PASSWORD"
|
||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- "rails"
|
||||||
|
- "panel.sutty.nl"
|
||||||
|
event: "push"
|
||||||
|
path:
|
||||||
|
include:
|
||||||
|
- "Dockerfile"
|
||||||
|
- ".dockerignore"
|
||||||
|
- ".woodpecker.yml"
|
||||||
|
assets:
|
||||||
|
image: "gitea.nulo.in/sutty/panel:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}"
|
||||||
|
commands:
|
||||||
|
- "apk add python2 dotenv openssh-client brotli"
|
||||||
|
- "install -d -m 700 ~/.ssh/"
|
||||||
|
- "echo \"$${KNOWN_HOSTS}\" | base64 -d >> ~/.ssh/known_hosts"
|
||||||
|
- "chmod 600 ~/.ssh/known_hosts"
|
||||||
|
- "eval $(ssh-agent -s)"
|
||||||
|
- "echo \"$${SSH_KEY}\" | base64 -d | ssh-add -"
|
||||||
|
- "ssh $${ORIGIN%:*}"
|
||||||
|
- "git config user.name Woodpecker"
|
||||||
|
- "git config user.email ci@sutty.coop.ar"
|
||||||
|
- "git remote add upstream $${ORIGIN}"
|
||||||
|
- "git checkout -B ${CI_COMMIT_BRANCH}"
|
||||||
|
- "mv config/credentials.yml.enc.ci config/credentials.yml.enc"
|
||||||
|
- "yarn"
|
||||||
|
- "cp .env.example .env"
|
||||||
|
- "dotenv bundle install --path=vendor"
|
||||||
|
- "dotenv RAILS_ENV=production bundle exec rails webpacker:clobber"
|
||||||
|
- "dotenv RAILS_ENV=production bundle exec rails assets:precompile"
|
||||||
|
- "dotenv RAILS_ENV=production bundle exec rails assets:clean"
|
||||||
|
- "find public -type f -print0 | xargs -r0 brotli -k9f"
|
||||||
|
- "git add public && git commit -m \"ci: assets [skip ci]\""
|
||||||
|
- "git pull upstream ${CI_COMMIT_BRANCH}"
|
||||||
|
- "git push upstream ${CI_COMMIT_BRANCH}"
|
||||||
|
secrets:
|
||||||
|
- "SSH_KEY"
|
||||||
|
- "KNOWN_HOSTS"
|
||||||
|
- "ORIGIN"
|
||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- "rails"
|
||||||
|
- "panel.sutty.nl"
|
||||||
|
path:
|
||||||
|
include:
|
||||||
|
- "app/assets/**/*"
|
||||||
|
- "app/javascript/**/*"
|
||||||
|
- "package.json"
|
||||||
|
- "yarn.lock"
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- ALPINE_VERSION: "3.14.10"
|
||||||
|
RUBY_VERSION: "2.7"
|
||||||
|
RUBY_PATCH: "8"
|
15
Dockerfile
15
Dockerfile
|
@ -1,5 +1,9 @@
|
||||||
FROM registry.nulo.in/sutty/rails:3.13.6-2.7.5
|
ARG RUBY_VERSION=2.7
|
||||||
ARG PANDOC_VERSION=2.17.1.1
|
ARG RUBY_PATCH=6
|
||||||
|
ARG ALPINE_VERSION=3.13.10
|
||||||
|
ARG BASE_IMAGE=registry.nulo.in/sutty/rails
|
||||||
|
FROM ${BASE_IMAGE}:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}
|
||||||
|
ARG PANDOC_VERSION=2.18
|
||||||
ENV RAILS_ENV production
|
ENV RAILS_ENV production
|
||||||
|
|
||||||
# Instalar las dependencias, separamos la librería de base de datos para
|
# Instalar las dependencias, separamos la librería de base de datos para
|
||||||
|
@ -10,10 +14,15 @@ ENV RAILS_ENV production
|
||||||
# principal
|
# principal
|
||||||
RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \
|
RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \
|
||||||
rsync git jpegoptim vips tectonic oxipng git-lfs openssh-client \
|
rsync git jpegoptim vips tectonic oxipng git-lfs openssh-client \
|
||||||
yarn daemonize ruby-webrick
|
yarn daemonize ruby-webrick postgresql-client dateutils file
|
||||||
|
|
||||||
RUN gem install --no-document --no-user-install foreman
|
RUN gem install --no-document --no-user-install foreman
|
||||||
RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc
|
RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc
|
||||||
|
RUN apk add npm && npm install -g pnpm@~7 && apk del npm
|
||||||
|
|
||||||
|
COPY ./monit.conf /etc/monit.d/sutty.conf
|
||||||
|
|
||||||
|
RUN apk add npm && npm install -g pnpm && apk del npm
|
||||||
|
|
||||||
VOLUME "/srv"
|
VOLUME "/srv"
|
||||||
|
|
||||||
|
|
7
Gemfile
7
Gemfile
|
@ -16,6 +16,7 @@ gem 'rails', '~> 6.1.0'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
|
|
||||||
gem 'nokogiri'
|
gem 'nokogiri'
|
||||||
|
gem 'rgl'
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -31,6 +32,8 @@ gem 'commonmarker'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-i18n'
|
gem 'devise-i18n'
|
||||||
gem 'devise_invitable'
|
gem 'devise_invitable'
|
||||||
|
gem 'distributed-press-api-client', '~> 0.2.3'
|
||||||
|
gem 'njalla-api-client', '~> 0.2.0'
|
||||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||||
gem 'exception_notification'
|
gem 'exception_notification'
|
||||||
gem 'fast_blank'
|
gem 'fast_blank'
|
||||||
|
@ -61,7 +64,7 @@ gem 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update'
|
||||||
gem 'rubyzip'
|
gem 'rubyzip'
|
||||||
gem 'rugged'
|
gem 'rugged'
|
||||||
gem 'concurrent-ruby-ext'
|
gem 'concurrent-ruby-ext'
|
||||||
gem 'sucker_punch'
|
gem 'que'
|
||||||
gem 'symbol-fstring', require: 'fstring/all'
|
gem 'symbol-fstring', require: 'fstring/all'
|
||||||
gem 'terminal-table'
|
gem 'terminal-table'
|
||||||
gem 'validates_hostname'
|
gem 'validates_hostname'
|
||||||
|
@ -82,7 +85,7 @@ gem 'stackprof'
|
||||||
gem 'prometheus_exporter'
|
gem 'prometheus_exporter'
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
gem 'fast_jsonparser'
|
gem 'fast_jsonparser', '~> 0.5.0'
|
||||||
gem 'down'
|
gem 'down'
|
||||||
gem 'sourcemap'
|
gem 'sourcemap'
|
||||||
gem 'rack-cors'
|
gem 'rack-cors'
|
||||||
|
|
329
Gemfile.lock
329
Gemfile.lock
|
@ -27,78 +27,78 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://17.3.alpine.gems.sutty.nl/
|
remote: https://17.3.alpine.gems.sutty.nl/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.7)
|
actioncable (6.1.7.3)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.7)
|
actionmailbox (6.1.7.3)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.3)
|
||||||
activejob (= 6.1.7)
|
activejob (= 6.1.7.3)
|
||||||
activerecord (= 6.1.7)
|
activerecord (= 6.1.7.3)
|
||||||
activestorage (= 6.1.7)
|
activestorage (= 6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.7)
|
actionmailer (6.1.7.3)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.3)
|
||||||
actionview (= 6.1.7)
|
actionview (= 6.1.7.3)
|
||||||
activejob (= 6.1.7)
|
activejob (= 6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
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.7)
|
actionpack (6.1.7.3)
|
||||||
actionview (= 6.1.7)
|
actionview (= 6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
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.7)
|
actiontext (6.1.7.3)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.3)
|
||||||
activerecord (= 6.1.7)
|
activerecord (= 6.1.7.3)
|
||||||
activestorage (= 6.1.7)
|
activestorage (= 6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.7)
|
actionview (6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
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.7)
|
activejob (6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.7)
|
activemodel (6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
activerecord (6.1.7)
|
activerecord (6.1.7.3)
|
||||||
activemodel (= 6.1.7)
|
activemodel (= 6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
activestorage (6.1.7)
|
activestorage (6.1.7.3)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.3)
|
||||||
activejob (= 6.1.7)
|
activejob (= 6.1.7.3)
|
||||||
activerecord (= 6.1.7)
|
activerecord (= 6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.7)
|
activesupport (6.1.7.3)
|
||||||
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.8.1)
|
addressable (2.8.4)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
bcrypt (3.1.18-x86_64-linux-musl)
|
bcrypt (3.1.18-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.10.0)
|
benchmark-ips (2.12.0)
|
||||||
bindex (0.8.1-x86_64-linux-musl)
|
bindex (0.8.1-x86_64-linux-musl)
|
||||||
blazer (2.6.5)
|
blazer (2.6.5)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
chartkick (>= 3.2)
|
chartkick (>= 3.2)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
safely_block (>= 0.1.1)
|
safely_block (>= 0.1.1)
|
||||||
brakeman (5.4.0)
|
brakeman (5.4.1)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
capybara (2.18.0)
|
capybara (2.18.0)
|
||||||
addressable
|
addressable
|
||||||
|
@ -107,19 +107,20 @@ 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.2.1)
|
chartkick (5.0.1)
|
||||||
|
climate_control (1.2.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
colorator (1.1.0)
|
colorator (1.1.0)
|
||||||
commonmarker (0.23.6-x86_64-linux-musl)
|
commonmarker (0.23.9-x86_64-linux-musl)
|
||||||
concurrent-ruby (1.1.10)
|
concurrent-ruby (1.2.2)
|
||||||
concurrent-ruby-ext (1.1.10-x86_64-linux-musl)
|
concurrent-ruby-ext (1.2.2-x86_64-linux-musl)
|
||||||
concurrent-ruby (= 1.1.10)
|
concurrent-ruby (= 1.2.2)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
cssbundling-rails (1.1.2)
|
cssbundling-rails (1.1.2)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
database_cleaner (2.0.1)
|
database_cleaner (2.0.2)
|
||||||
database_cleaner-active_record (~> 2.0.0)
|
database_cleaner-active_record (>= 2, < 3)
|
||||||
database_cleaner-active_record (2.0.1)
|
database_cleaner-active_record (2.1.0)
|
||||||
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)
|
||||||
|
@ -137,24 +138,57 @@ GEM
|
||||||
rake (> 10, < 14)
|
rake (> 10, < 14)
|
||||||
ruby-statistics (>= 2.1)
|
ruby-statistics (>= 2.1)
|
||||||
thor (>= 0.19, < 2)
|
thor (>= 0.19, < 2)
|
||||||
devise (4.8.1)
|
devise (4.9.0)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-i18n (1.10.2)
|
devise-i18n (1.11.0)
|
||||||
devise (>= 4.8.0)
|
devise (>= 4.9.0)
|
||||||
devise_invitable (2.0.6)
|
devise_invitable (2.0.7)
|
||||||
actionmailer (>= 5.0)
|
actionmailer (>= 5.0)
|
||||||
devise (>= 4.6)
|
devise (>= 4.6)
|
||||||
|
distributed-press-api-client (0.2.3)
|
||||||
|
addressable (~> 2.3, >= 2.3.0)
|
||||||
|
climate_control
|
||||||
|
dry-schema
|
||||||
|
httparty (~> 0.18)
|
||||||
|
json (~> 2.1, >= 2.1.0)
|
||||||
|
jwt (~> 2.6.0)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
dotenv-rails (2.8.1)
|
dotenv-rails (2.8.1)
|
||||||
dotenv (= 2.8.1)
|
dotenv (= 2.8.1)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
down (5.4.0)
|
down (5.4.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
ed25519 (1.2.4-x86_64-linux-musl)
|
dry-configurable (1.0.1)
|
||||||
|
dry-core (~> 1.0, < 2)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-core (1.0.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-inflector (1.0.0)
|
||||||
|
dry-initializer (3.1.1)
|
||||||
|
dry-logic (1.5.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.0, < 2)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-schema (1.13.1)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-configurable (~> 1.0, >= 1.0.1)
|
||||||
|
dry-core (~> 1.0, < 2)
|
||||||
|
dry-initializer (~> 3.0)
|
||||||
|
dry-logic (>= 1.4, < 2)
|
||||||
|
dry-types (>= 1.7, < 2)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-types (1.7.1)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.0)
|
||||||
|
dry-inflector (~> 1.0)
|
||||||
|
dry-logic (~> 1.4)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
ed25519 (1.3.0-x86_64-linux-musl)
|
||||||
em-websocket (0.5.3)
|
em-websocket (0.5.3)
|
||||||
eventmachine (>= 0.12.9)
|
eventmachine (>= 0.12.9)
|
||||||
http_parser.rb (~> 0)
|
http_parser.rb (~> 0)
|
||||||
|
@ -170,7 +204,7 @@ GEM
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.2.0)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
fast_blank (1.0.1-x86_64-linux-musl)
|
fast_blank (1.0.1-x86_64-linux-musl)
|
||||||
fast_jsonparser (0.6.0-x86_64-linux-musl)
|
fast_jsonparser (0.5.0-x86_64-linux-musl)
|
||||||
ffi (1.15.5-x86_64-linux-musl)
|
ffi (1.15.5-x86_64-linux-musl)
|
||||||
flamegraph (0.9.5)
|
flamegraph (0.9.5)
|
||||||
foreman (0.87.2)
|
foreman (0.87.2)
|
||||||
|
@ -179,12 +213,12 @@ GEM
|
||||||
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 (1.0.0)
|
globalid (1.1.0)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 5.0)
|
||||||
groupdate (6.1.0)
|
groupdate (6.2.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
hairtrigger (0.2.24)
|
hairtrigger (1.0.0)
|
||||||
activerecord (>= 5.0, < 7)
|
activerecord (>= 6.0, < 8)
|
||||||
ruby2ruby (~> 2.4)
|
ruby2ruby (~> 2.4)
|
||||||
ruby_parser (~> 3.10)
|
ruby_parser (~> 3.10)
|
||||||
haml (6.1.1-x86_64-linux-musl)
|
haml (6.1.1-x86_64-linux-musl)
|
||||||
|
@ -193,7 +227,7 @@ GEM
|
||||||
tilt
|
tilt
|
||||||
haml-lint (0.999.999)
|
haml-lint (0.999.999)
|
||||||
haml_lint
|
haml_lint
|
||||||
haml_lint (0.43.0)
|
haml_lint (0.45.0)
|
||||||
haml (>= 4.0, < 6.2)
|
haml (>= 4.0, < 6.2)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
|
@ -212,10 +246,10 @@ GEM
|
||||||
thor
|
thor
|
||||||
hiredis (0.6.3-x86_64-linux-musl)
|
hiredis (0.6.3-x86_64-linux-musl)
|
||||||
http_parser.rb (0.8.0-x86_64-linux-musl)
|
http_parser.rb (0.8.0-x86_64-linux-musl)
|
||||||
httparty (0.18.1)
|
httparty (0.21.0)
|
||||||
mime-types (~> 3.0)
|
mini_mime (>= 1.0.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
i18n (1.12.0)
|
i18n (1.13.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
icalendar (2.8.0)
|
icalendar (2.8.0)
|
||||||
ice_cube (~> 0.16)
|
ice_cube (~> 0.16)
|
||||||
|
@ -223,7 +257,7 @@ GEM
|
||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
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.8.0)
|
inline_svg (1.9.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
jbuilder (2.11.5)
|
jbuilder (2.11.5)
|
||||||
|
@ -248,7 +282,7 @@ GEM
|
||||||
commonmarker (~> 0.22)
|
commonmarker (~> 0.22)
|
||||||
jekyll-data (1.1.2)
|
jekyll-data (1.1.2)
|
||||||
jekyll (>= 3.3, < 5.0.0)
|
jekyll (>= 3.3, < 5.0.0)
|
||||||
jekyll-images (0.4.0)
|
jekyll-images (0.4.1)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
ruby-filemagic (~> 0.7)
|
ruby-filemagic (~> 0.7)
|
||||||
ruby-vips (~> 2)
|
ruby-vips (~> 2)
|
||||||
|
@ -261,6 +295,7 @@ GEM
|
||||||
jsbundling-rails (1.1.1)
|
jsbundling-rails (1.1.1)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
json (2.6.3-x86_64-linux-musl)
|
json (2.6.3-x86_64-linux-musl)
|
||||||
|
jwt (2.6.0)
|
||||||
kaminari (1.2.2)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.2)
|
kaminari-actionview (= 1.2.2)
|
||||||
|
@ -282,22 +317,22 @@ GEM
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.8.1)
|
letter_opener (1.8.1)
|
||||||
launchy (>= 2.2, < 3)
|
launchy (>= 2.2, < 3)
|
||||||
liquid (4.0.3)
|
liquid (4.0.4)
|
||||||
listen (3.7.1)
|
listen (3.8.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loaf (0.10.0)
|
loaf (0.10.0)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
lockbox (1.1.1)
|
lockbox (1.2.0)
|
||||||
lograge (0.12.0)
|
lograge (0.12.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.19.1)
|
loofah (2.20.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.8.0)
|
mail (2.8.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
|
@ -306,15 +341,12 @@ GEM
|
||||||
memory_profiler (1.0.1)
|
memory_profiler (1.0.1)
|
||||||
mercenary (0.4.0)
|
mercenary (0.4.0)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mime-types (3.4.1)
|
|
||||||
mime-types-data (~> 3.2015)
|
|
||||||
mime-types-data (3.2022.0105)
|
|
||||||
mini_histogram (0.3.1)
|
mini_histogram (0.3.1)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.8.1)
|
mini_portile2 (2.8.1)
|
||||||
minitest (5.14.4)
|
minitest (5.18.0)
|
||||||
mobility (1.2.4)
|
mobility (1.2.9)
|
||||||
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)
|
||||||
|
@ -327,70 +359,75 @@ GEM
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.3.3)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.0.1)
|
net-ssh (7.1.0)
|
||||||
netaddr (2.0.6)
|
netaddr (2.0.6)
|
||||||
nio4r (2.5.8-x86_64-linux-musl)
|
nio4r (2.5.9-x86_64-linux-musl)
|
||||||
nokogiri (1.13.10-x86_64-linux-musl)
|
njalla-api-client (0.2.0)
|
||||||
|
dry-schema
|
||||||
|
httparty (~> 0.18)
|
||||||
|
nokogiri (1.14.4-x86_64-linux-musl)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
parallel (1.22.1)
|
pairing_heap (3.0.1)
|
||||||
parser (3.1.3.0)
|
parallel (1.23.0)
|
||||||
|
parser (3.2.1.1)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
pathutil (0.16.2)
|
pathutil (0.16.2)
|
||||||
forwardable-extended (~> 2.6)
|
forwardable-extended (~> 2.6)
|
||||||
pg (1.4.5-x86_64-linux-musl)
|
pg (1.5.3-x86_64-linux-musl)
|
||||||
pg_search (2.3.6)
|
pg_search (2.3.6)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
prometheus_exporter (2.0.6)
|
prometheus_exporter (2.0.8)
|
||||||
webrick
|
webrick
|
||||||
pry (0.14.1)
|
pry (0.14.2)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
public_suffix (5.0.1)
|
public_suffix (5.0.1)
|
||||||
puma (6.0.1-x86_64-linux-musl)
|
puma (6.2.2-x86_64-linux-musl)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.0)
|
pundit (2.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
que (2.2.0)
|
||||||
racc (1.6.2-x86_64-linux-musl)
|
racc (1.6.2-x86_64-linux-musl)
|
||||||
rack (2.2.5)
|
rack (2.2.6.4)
|
||||||
rack-cors (1.1.1)
|
rack-cors (2.0.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-mini-profiler (3.0.0)
|
rack-mini-profiler (3.0.0)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-proxy (0.7.4)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.3)
|
||||||
rails (6.1.7)
|
rails (6.1.7.3)
|
||||||
actioncable (= 6.1.7)
|
actioncable (= 6.1.7.3)
|
||||||
actionmailbox (= 6.1.7)
|
actionmailbox (= 6.1.7.3)
|
||||||
actionmailer (= 6.1.7)
|
actionmailer (= 6.1.7.3)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.3)
|
||||||
actiontext (= 6.1.7)
|
actiontext (= 6.1.7.3)
|
||||||
actionview (= 6.1.7)
|
actionview (= 6.1.7.3)
|
||||||
activejob (= 6.1.7)
|
activejob (= 6.1.7.3)
|
||||||
activemodel (= 6.1.7)
|
activemodel (= 6.1.7.3)
|
||||||
activerecord (= 6.1.7)
|
activerecord (= 6.1.7.3)
|
||||||
activestorage (= 6.1.7)
|
activestorage (= 6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.7)
|
railties (= 6.1.7.3)
|
||||||
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.4.4)
|
rails-html-sanitizer (1.5.0)
|
||||||
loofah (~> 2.19, >= 2.19.1)
|
loofah (~> 2.19, >= 2.19.1)
|
||||||
rails-i18n (7.0.6)
|
rails-i18n (7.0.6)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
rails_warden (0.6.0)
|
rails_warden (0.6.0)
|
||||||
warden (>= 1.2.0)
|
warden (>= 1.2.0)
|
||||||
railties (6.1.7)
|
railties (6.1.7.3)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.3)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.3)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
|
@ -399,9 +436,9 @@ GEM
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
redis (4.5.1)
|
redis (4.8.1)
|
||||||
redis-actionpack (5.2.0)
|
redis-actionpack (5.3.0)
|
||||||
actionpack (>= 5, < 7)
|
actionpack (>= 5, < 8)
|
||||||
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.3.0)
|
redis-activesupport (5.3.0)
|
||||||
|
@ -414,63 +451,67 @@ GEM
|
||||||
redis-actionpack (>= 5.0, < 6)
|
redis-actionpack (>= 5.0, < 6)
|
||||||
redis-activesupport (>= 5.0, < 6)
|
redis-activesupport (>= 5.0, < 6)
|
||||||
redis-store (>= 1.2, < 2)
|
redis-store (>= 1.2, < 2)
|
||||||
redis-store (1.9.1)
|
redis-store (1.9.2)
|
||||||
redis (>= 4, < 5)
|
redis (>= 4, < 6)
|
||||||
reek (6.1.3)
|
reek (6.1.4)
|
||||||
kwalify (~> 0.7.0)
|
kwalify (~> 0.7.0)
|
||||||
parser (~> 3.1.0)
|
parser (~> 3.2.0)
|
||||||
rainbow (>= 2.0, < 4.0)
|
rainbow (>= 2.0, < 4.0)
|
||||||
regexp_parser (2.6.1)
|
regexp_parser (2.8.0)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.0.1)
|
responders (3.1.0)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.0)
|
railties (>= 5.2)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
|
rgl (0.6.2)
|
||||||
|
pairing_heap (>= 0.3.0)
|
||||||
|
rexml (~> 3.2, >= 3.2.4)
|
||||||
|
stream (~> 0.5.3)
|
||||||
rouge (3.30.0)
|
rouge (3.30.0)
|
||||||
rubocop (1.41.1)
|
rubocop (1.42.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.1.2.1)
|
parser (>= 3.1.2.1)
|
||||||
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 (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.23.0, < 2.0)
|
rubocop-ast (>= 1.24.1, < 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.24.1)
|
rubocop-ast (1.28.0)
|
||||||
parser (>= 3.1.1.0)
|
parser (>= 3.2.1.0)
|
||||||
rubocop-rails (2.17.4)
|
rubocop-rails (2.18.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
ruby-filemagic (0.7.3-x86_64-linux-musl)
|
ruby-filemagic (0.7.3-x86_64-linux-musl)
|
||||||
ruby-progressbar (1.11.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-statistics (3.0.1)
|
ruby-statistics (3.0.1)
|
||||||
ruby-vips (2.1.4)
|
ruby-vips (2.1.4)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
ruby2ruby (2.5.0)
|
ruby2ruby (2.5.0)
|
||||||
ruby_parser (~> 3.1)
|
ruby_parser (~> 3.1)
|
||||||
sexp_processor (~> 4.6)
|
sexp_processor (~> 4.6)
|
||||||
ruby_parser (3.19.2)
|
ruby_parser (3.20.0)
|
||||||
sexp_processor (~> 4.16)
|
sexp_processor (~> 4.16)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
rugged (1.5.0.1-x86_64-linux-musl)
|
rugged (1.6.3-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)
|
||||||
sassc (2.4.0-x86_64-linux-musl)
|
sassc (2.4.0-x86_64-linux-musl)
|
||||||
ffi (~> 1.9)
|
ffi (~> 1.9)
|
||||||
selenium-webdriver (4.7.1)
|
selenium-webdriver (4.8.6)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 3.0)
|
rubyzip (>= 1.2.2, < 3.0)
|
||||||
websocket (~> 1.0)
|
websocket (~> 1.0)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sexp_processor (4.16.0)
|
sexp_processor (4.16.1)
|
||||||
simpleidn (0.2.1)
|
simpleidn (0.2.1)
|
||||||
unf (~> 0.1.4)
|
unf (~> 0.1.4)
|
||||||
sourcemap (0.1.1)
|
sourcemap (0.1.1)
|
||||||
spring (4.1.0)
|
spring (4.1.1)
|
||||||
spring-watcher-listen (2.1.0)
|
spring-watcher-listen (2.1.0)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
spring (>= 4)
|
spring (>= 4)
|
||||||
|
@ -481,27 +522,26 @@ GEM
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sqlite3 (1.5.4-x86_64-linux-musl)
|
sqlite3 (1.6.2-x86_64-linux-musl)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
stackprof (0.2.23-x86_64-linux-musl)
|
stackprof (0.2.25-x86_64-linux-musl)
|
||||||
sucker_punch (3.1.0)
|
stream (0.5.5)
|
||||||
concurrent-ruby (~> 1.0)
|
sutty-liquid (0.11.10)
|
||||||
sutty-liquid (0.7.4)
|
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
symbol-fstring (1.0.2-x86_64-linux-musl)
|
symbol-fstring (1.0.2-x86_64-linux-musl)
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.9.1)
|
temple (0.10.0)
|
||||||
terminal-table (2.0.0)
|
terminal-table (2.0.0)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
thor (1.2.1)
|
thor (1.2.1)
|
||||||
tilt (2.0.11)
|
tilt (2.1.0)
|
||||||
timecop (0.9.6)
|
timecop (0.9.6)
|
||||||
timeout (0.3.1)
|
timeout (0.3.2)
|
||||||
turbolinks (5.2.1)
|
turbolinks (5.2.1)
|
||||||
turbolinks-source (~> 5.2)
|
turbolinks-source (~> 5.2)
|
||||||
turbolinks-source (5.2.0)
|
turbolinks-source (5.2.0)
|
||||||
tzinfo (2.0.5)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
|
@ -517,19 +557,19 @@ GEM
|
||||||
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.3)
|
webpacker (5.4.4)
|
||||||
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.8.1)
|
||||||
websocket (1.2.9)
|
websocket (1.2.9)
|
||||||
websocket-driver (0.7.5-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.6.6)
|
zeitwerk (2.6.8)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x86_64-linux-musl
|
x86_64-linux-musl
|
||||||
|
@ -549,6 +589,7 @@ DEPENDENCIES
|
||||||
devise
|
devise
|
||||||
devise-i18n
|
devise-i18n
|
||||||
devise_invitable
|
devise_invitable
|
||||||
|
distributed-press-api-client (~> 0.2.3)
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
down
|
down
|
||||||
ed25519
|
ed25519
|
||||||
|
@ -556,7 +597,7 @@ DEPENDENCIES
|
||||||
exception_notification
|
exception_notification
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
fast_blank
|
fast_blank
|
||||||
fast_jsonparser
|
fast_jsonparser (~> 0.5.0)
|
||||||
flamegraph
|
flamegraph
|
||||||
foreman
|
foreman
|
||||||
friendly_id
|
friendly_id
|
||||||
|
@ -585,6 +626,7 @@ DEPENDENCIES
|
||||||
mini_magick
|
mini_magick
|
||||||
mobility
|
mobility
|
||||||
net-ssh
|
net-ssh
|
||||||
|
njalla-api-client (~> 0.2.0)
|
||||||
nokogiri
|
nokogiri
|
||||||
pg
|
pg
|
||||||
pg_search
|
pg_search
|
||||||
|
@ -592,6 +634,7 @@ DEPENDENCIES
|
||||||
pry
|
pry
|
||||||
puma
|
puma
|
||||||
pundit
|
pundit
|
||||||
|
que
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-mini-profiler
|
rack-mini-profiler
|
||||||
rails (~> 6.1.0)
|
rails (~> 6.1.0)
|
||||||
|
@ -600,6 +643,7 @@ DEPENDENCIES
|
||||||
redis (~> 4.0)
|
redis (~> 4.0)
|
||||||
redis-rails
|
redis-rails
|
||||||
reek
|
reek
|
||||||
|
rgl
|
||||||
rollups!
|
rollups!
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubyzip
|
rubyzip
|
||||||
|
@ -611,7 +655,6 @@ DEPENDENCIES
|
||||||
spring-watcher-listen
|
spring-watcher-listen
|
||||||
sqlite3
|
sqlite3
|
||||||
stackprof
|
stackprof
|
||||||
sucker_punch
|
|
||||||
sutty-liquid (>= 0.7.3)
|
sutty-liquid (>= 0.7.3)
|
||||||
symbol-fstring
|
symbol-fstring
|
||||||
terminal-table
|
terminal-table
|
||||||
|
@ -623,7 +666,7 @@ DEPENDENCIES
|
||||||
yaml_db!
|
yaml_db!
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.1.3p185
|
ruby 3.1.4p223
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.1
|
2.3.26
|
||||||
|
|
3
Procfile
3
Procfile
|
@ -5,4 +5,7 @@ blazer_1h: bundle exec rake blazer:run_checks SCHEDULE="1 hour"
|
||||||
blazer_1d: bundle exec rake blazer:run_checks SCHEDULE="1 day"
|
blazer_1d: bundle exec rake blazer:run_checks SCHEDULE="1 day"
|
||||||
blazer: bundle exec rake blazer:send_failing_checks
|
blazer: bundle exec rake blazer:send_failing_checks
|
||||||
prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
|
prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
|
||||||
|
distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
|
||||||
|
cleanup: bundle exec rake cleanup:everything
|
||||||
stats: bundle exec rake stats:process_all
|
stats: bundle exec rake stats:process_all
|
||||||
|
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que
|
||||||
|
|
|
@ -1,5 +1,36 @@
|
||||||
|
|
||||||
@import "./editor";
|
$colors: (
|
||||||
|
"black": $black,
|
||||||
|
"white": $white,
|
||||||
|
"cyan": $cyan,
|
||||||
|
"magenta": $magenta
|
||||||
|
);
|
||||||
|
|
||||||
|
// Redefinir variables de Bootstrap
|
||||||
|
$primary: $magenta;
|
||||||
|
$secondary: $black;
|
||||||
|
$jumbotron-bg: transparent;
|
||||||
|
$enable-rounded: false;
|
||||||
|
$form-feedback-valid-color: $black;
|
||||||
|
$form-feedback-invalid-color: $magenta;
|
||||||
|
$form-feedback-icon-valid-color: $black;
|
||||||
|
$component-active-bg: $magenta;
|
||||||
|
|
||||||
|
$spacers: (
|
||||||
|
2-plus: 0.75rem
|
||||||
|
);
|
||||||
|
|
||||||
|
$sizes: (
|
||||||
|
"70ch": 70ch,
|
||||||
|
);
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: var(--foreground);
|
||||||
|
color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "bootstrap";
|
||||||
|
@import "editor";
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
.editor-content {
|
.editor-content {
|
||||||
|
@ -106,6 +137,12 @@ ol.breadcrumb {
|
||||||
transition: all 3s;
|
transition: all 3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
legend {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mapable,
|
.mapable,
|
||||||
.taggable {
|
.taggable {
|
||||||
.input-map,
|
.input-map,
|
||||||
|
@ -146,8 +183,6 @@ svg {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
background-color: var(--foreground);
|
|
||||||
color: var(--background);
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin-right: 0.3rem;
|
margin-right: 0.3rem;
|
||||||
|
@ -325,6 +360,9 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.word-break-all { word-break: break-all !important; }
|
||||||
|
.hyphens { hyphens: auto; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Modificadores de Bootstrap que no tienen versión responsive.
|
* Modificadores de Bootstrap que no tienen versión responsive.
|
||||||
*/
|
*/
|
||||||
|
@ -347,6 +385,8 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
|
||||||
.text-#{$grid-breakpoint}-right { text-align: right !important; }
|
.text-#{$grid-breakpoint}-right { text-align: right !important; }
|
||||||
.text-#{$grid-breakpoint}-center { text-align: center !important; }
|
.text-#{$grid-breakpoint}-center { text-align: center !important; }
|
||||||
|
|
||||||
|
.word-break-#{$grid-breakpoint}-all { word-break: break-all !important; }
|
||||||
|
|
||||||
// posición
|
// posición
|
||||||
@each $position in $positions {
|
@each $position in $positions {
|
||||||
.position-#{$grid-breakpoint}-#{$position} { position: $position !important; }
|
.position-#{$grid-breakpoint}-#{$position} { position: $position !important; }
|
||||||
|
@ -356,6 +396,8 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
|
||||||
@each $prop, $abbrev in (width: w, height: h) {
|
@each $prop, $abbrev in (width: w, height: h) {
|
||||||
@each $size, $length in $sizes {
|
@each $size, $length in $sizes {
|
||||||
.#{$abbrev}-#{$grid-breakpoint}-#{$size} { #{$prop}: $length !important; }
|
.#{$abbrev}-#{$grid-breakpoint}-#{$size} { #{$prop}: $length !important; }
|
||||||
|
.min-#{$abbrev}-#{$grid-breakpoint}-#{$size} { min-#{$prop}: $length !important; }
|
||||||
|
.max-#{$abbrev}-#{$grid-breakpoint}-#{$size} { max-#{$prop}: $length !important; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ module Api
|
||||||
|
|
||||||
# Si todo salió bien, enviar los correos y redirigir al sitio.
|
# Si todo salió bien, enviar los correos y redirigir al sitio.
|
||||||
# El sitio nos dice a dónde tenemos que ir.
|
# El sitio nos dice a dónde tenemos que ir.
|
||||||
ContactJob.perform_async site.id,
|
ContactJob.perform_later site.id,
|
||||||
params[:form],
|
params[:form],
|
||||||
contact_params.to_h.symbolize_keys,
|
contact_params.to_h.symbolize_keys,
|
||||||
params[:redirect]
|
params[:redirect]
|
||||||
|
|
|
@ -15,7 +15,7 @@ module Api
|
||||||
params: airbrake_params.to_h
|
params: airbrake_params.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
render status: 201, json: { id: 1, url: root_url }
|
render status: 201, json: { id: 1, url: '' }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -9,44 +9,27 @@ module Api
|
||||||
|
|
||||||
# Lista de nombres de dominios a emitir certificados
|
# Lista de nombres de dominios a emitir certificados
|
||||||
def index
|
def index
|
||||||
render json: sites_names + alternative_names + api_names
|
render json: sites_names + alternative_names + api_names + www_names
|
||||||
end
|
|
||||||
|
|
||||||
# Sitios con hidden service de Tor
|
|
||||||
#
|
|
||||||
# @return [Array] lista de nombres de sitios sin onion aun
|
|
||||||
def hidden_services
|
|
||||||
render json: DeployHiddenService.where(values: nil).includes(:site).pluck(:name)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Tor va a enviar el onion junto con el nombre del sitio y tenemos
|
|
||||||
# que guardarlo en su deploy_hidden_service.
|
|
||||||
#
|
|
||||||
# @params [String] name
|
|
||||||
# @params [String] onion
|
|
||||||
def add_onion
|
|
||||||
site = Site.find_by(name: params[:name])
|
|
||||||
|
|
||||||
if site
|
|
||||||
usuarie = GitAuthor.new email: 'tor@' + Site.domain, name: 'Tor'
|
|
||||||
service = SiteService.new site: site, usuarie: usuarie,
|
|
||||||
params: params
|
|
||||||
service.add_onion
|
|
||||||
end
|
|
||||||
|
|
||||||
head :ok
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def canonicalize(name)
|
||||||
|
name.end_with?('.') ? name[0..-2] : "#{name}.#{Site.domain}"
|
||||||
|
end
|
||||||
|
|
||||||
# Nombres de los sitios
|
# Nombres de los sitios
|
||||||
def sites_names
|
def sites_names
|
||||||
Site.all.order(:name).pluck(:name)
|
Site.all.order(:name).pluck(:name).map do |name|
|
||||||
|
canonicalize name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Dominios alternativos
|
# Dominios alternativos
|
||||||
def alternative_names
|
def alternative_names
|
||||||
DeployAlternativeDomain.all.map(&:hostname)
|
(DeployAlternativeDomain.all.map(&:hostname) + DeployLocalizedDomain.all.map(&:hostname)).map do |name|
|
||||||
|
canonicalize name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtener todos los sitios con API habilitada, es decir formulario
|
# Obtener todos los sitios con API habilitada, es decir formulario
|
||||||
|
@ -56,7 +39,16 @@ module Api
|
||||||
def api_names
|
def api_names
|
||||||
Site.where(contact: true)
|
Site.where(contact: true)
|
||||||
.or(Site.where(colaboracion_anonima: true))
|
.or(Site.where(colaboracion_anonima: true))
|
||||||
.select("'api.' || name as name").map(&:name)
|
.select("'api.' || name as name").map(&:name).map do |name|
|
||||||
|
canonicalize name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Todos los dominios con WWW habilitado
|
||||||
|
def www_names
|
||||||
|
Site.where(id: DeployWww.all.pluck(:site_id)).select("'www.' || name as name").map(&:name).map do |name|
|
||||||
|
canonicalize name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
before_action :prepare_exception_notifier
|
before_action :prepare_exception_notifier
|
||||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||||
|
before_action :notify_unconfirmed_email, unless: :devise_controller?
|
||||||
around_action :set_locale
|
around_action :set_locale
|
||||||
|
|
||||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||||
|
@ -27,6 +28,15 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def notify_unconfirmed_email
|
||||||
|
return unless current_usuarie
|
||||||
|
return if current_usuarie.confirmed?
|
||||||
|
|
||||||
|
I18n.with_locale(current_usuarie.lang) do
|
||||||
|
flash[:notice] ||= I18n.t('devise.registrations.signed_up')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def uuid?(string)
|
def uuid?(string)
|
||||||
/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string
|
/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string
|
||||||
end
|
end
|
||||||
|
@ -46,17 +56,19 @@ class ApplicationController < ActionController::Base
|
||||||
# defecto.
|
# defecto.
|
||||||
#
|
#
|
||||||
# Esto se refiere al idioma de la interfaz, no de los artículos.
|
# Esto se refiere al idioma de la interfaz, no de los artículos.
|
||||||
def current_locale(include_params: true, site: nil)
|
#
|
||||||
return params[:locale] if include_params && params[:locale].present?
|
# @return [String,Symbol]
|
||||||
|
def current_locale
|
||||||
|
session[:locale] = params[:change_locale_to] if params[:change_locale_to].present?
|
||||||
|
|
||||||
current_usuarie&.lang || I18n.locale
|
session[:locale] || current_usuarie&.lang || I18n.locale
|
||||||
end
|
end
|
||||||
|
|
||||||
# El idioma es el preferido por le usuarie, pero no necesariamente se
|
# El idioma es el preferido por le usuarie, pero no necesariamente se
|
||||||
# corresponde con el idioma de los artículos, porque puede querer
|
# corresponde con el idioma de los artículos, porque puede querer
|
||||||
# traducirlos.
|
# traducirlos.
|
||||||
def set_locale(&action)
|
def set_locale(&action)
|
||||||
I18n.with_locale(current_locale(include_params: false), &action)
|
I18n.with_locale(current_locale, &action)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Muestra una página 404
|
# Muestra una página 404
|
||||||
|
@ -79,13 +91,26 @@ class ApplicationController < ActionController::Base
|
||||||
breadcrumb 'stats.index', root_path, match: :exact
|
breadcrumb 'stats.index', root_path, match: :exact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def site
|
||||||
|
@site ||= find_site
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def configure_permitted_parameters
|
def configure_permitted_parameters
|
||||||
|
devise_parameter_sanitizer.permit(:sign_up, keys: Usuarie::CONSENT_FIELDS)
|
||||||
devise_parameter_sanitizer.permit(:account_update, keys: %i[lang])
|
devise_parameter_sanitizer.permit(:account_update, keys: %i[lang])
|
||||||
end
|
end
|
||||||
|
|
||||||
def prepare_exception_notifier
|
def prepare_exception_notifier
|
||||||
request.env['exception_notifier.exception_data'] = { usuarie: current_usuarie }
|
request.env['exception_notifier.exception_data'] = { usuarie: current_usuarie }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Olvidar el idioma elegido antes de iniciar la sesión y reenviar a
|
||||||
|
# los sitios en el idioma de le usuarie.
|
||||||
|
def after_sign_in_path_for(resource)
|
||||||
|
session[:locale] = nil
|
||||||
|
|
||||||
|
sites_path
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
41
app/controllers/build_stats_controller.rb
Normal file
41
app/controllers/build_stats_controller.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# La lista de estados de compilación, por ahora solo mostramos el último
|
||||||
|
# estado.
|
||||||
|
class BuildStatsController < ApplicationController
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
include ActionView::Helpers::DateHelper
|
||||||
|
|
||||||
|
before_action :authenticate_usuarie!
|
||||||
|
|
||||||
|
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path
|
||||||
|
breadcrumb 'sites.index', :sites_path, match: :exact
|
||||||
|
breadcrumb -> { site.title }, -> { site_posts_path(site, locale: locale) }, match: :exact
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize SiteBuildStat.new(site)
|
||||||
|
breadcrumb I18n.t('build_stats.index.title'), ''
|
||||||
|
|
||||||
|
@headers = %w[type url seconds size].map do |header|
|
||||||
|
t("deploy_mailer.deployed.th.#{header}")
|
||||||
|
end
|
||||||
|
|
||||||
|
@table = site.deployment_list.map do |deploy|
|
||||||
|
type = deploy.class.name.underscore
|
||||||
|
urls = deploy.respond_to?(:urls) ? deploy.urls : [deploy.url].compact
|
||||||
|
urls = [nil] if urls.empty?
|
||||||
|
build_stat = deploy.build_stats.where(status: true).last
|
||||||
|
seconds = build_stat&.seconds || 0
|
||||||
|
|
||||||
|
{
|
||||||
|
title: t("deploy_mailer.deployed.#{type}.title"),
|
||||||
|
urls: urls,
|
||||||
|
seconds: {
|
||||||
|
human: distance_of_time_in_words(seconds),
|
||||||
|
machine: "PT#{seconds}S"
|
||||||
|
},
|
||||||
|
size: number_to_human_size(build_stat&.bytes || 0, precision: 2)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,7 +12,7 @@ class PostsController < ApplicationController
|
||||||
|
|
||||||
# Las URLs siempre llevan el idioma actual o el de le usuarie
|
# Las URLs siempre llevan el idioma actual o el de le usuarie
|
||||||
def default_url_options
|
def default_url_options
|
||||||
{ locale: current_locale }
|
{ locale: locale }
|
||||||
end
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -159,10 +159,6 @@ class PostsController < ApplicationController
|
||||||
end.transform_keys(&:to_sym)
|
end.transform_keys(&:to_sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
def site
|
|
||||||
@site ||= find_site
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -10,7 +10,7 @@ class SitesController < ApplicationController
|
||||||
# Ver un listado de sitios
|
# Ver un listado de sitios
|
||||||
def index
|
def index
|
||||||
authorize Site
|
authorize Site
|
||||||
@sites = current_usuarie.sites.order(:title)
|
@sites = current_usuarie.sites.order(updated_at: :desc)
|
||||||
|
|
||||||
fresh_when @sites
|
fresh_when @sites
|
||||||
end
|
end
|
||||||
|
@ -28,8 +28,6 @@ class SitesController < ApplicationController
|
||||||
|
|
||||||
@site = Site.new
|
@site = Site.new
|
||||||
authorize @site
|
authorize @site
|
||||||
|
|
||||||
@site.deploys.build type: 'DeployLocal'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -68,9 +66,7 @@ class SitesController < ApplicationController
|
||||||
def enqueue
|
def enqueue
|
||||||
authorize site
|
authorize site
|
||||||
|
|
||||||
# XXX: Convertir en una máquina de estados?
|
SiteService.new(site: site).deploy
|
||||||
site.enqueue!
|
|
||||||
DeployJob.perform_async site.id
|
|
||||||
|
|
||||||
redirect_to site_posts_path(site, locale: site.default_locale)
|
redirect_to site_posts_path(site, locale: site.default_locale)
|
||||||
end
|
end
|
||||||
|
|
|
@ -47,7 +47,7 @@ class UsuariesController < ApplicationController
|
||||||
@usuarie = Usuarie.find(params[:usuarie_id])
|
@usuarie = Usuarie.find(params[:usuarie_id])
|
||||||
|
|
||||||
if @site.usuaries.count > 1
|
if @site.usuaries.count > 1
|
||||||
@usuarie.rol_for_site(@site).update_attribute :rol, 'invitade'
|
@usuarie.rol_for_site(@site).update_attribute :rol, Rol::INVITADE
|
||||||
else
|
else
|
||||||
flash[:warning] = I18n.t('usuaries.index.demote.denied')
|
flash[:warning] = I18n.t('usuaries.index.demote.denied')
|
||||||
end
|
end
|
||||||
|
@ -61,7 +61,7 @@ class UsuariesController < ApplicationController
|
||||||
authorize SiteUsuarie.new(@site, current_usuarie)
|
authorize SiteUsuarie.new(@site, current_usuarie)
|
||||||
|
|
||||||
@usuarie = Usuarie.find(params[:usuarie_id])
|
@usuarie = Usuarie.find(params[:usuarie_id])
|
||||||
@usuarie.rol_for_site(@site).update_attribute :rol, 'usuarie'
|
@usuarie.rol_for_site(@site).update_attribute :rol, Rol::USUARIE
|
||||||
|
|
||||||
redirect_to site_usuaries_path
|
redirect_to site_usuaries_path
|
||||||
end
|
end
|
||||||
|
@ -72,6 +72,8 @@ class UsuariesController < ApplicationController
|
||||||
site_usuarie = SiteUsuarie.new(@site, current_usuarie)
|
site_usuarie = SiteUsuarie.new(@site, current_usuarie)
|
||||||
authorize site_usuarie
|
authorize site_usuarie
|
||||||
|
|
||||||
|
params[:invite_as] = invite_as
|
||||||
|
|
||||||
@policy = policy(site_usuarie)
|
@policy = policy(site_usuarie)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -81,27 +83,33 @@ class UsuariesController < ApplicationController
|
||||||
authorize SiteUsuarie.new(@site, current_usuarie)
|
authorize SiteUsuarie.new(@site, current_usuarie)
|
||||||
|
|
||||||
# Enviar la invitación si es necesario y agregar al sitio
|
# Enviar la invitación si es necesario y agregar al sitio
|
||||||
invitaciones.each do |invitacion|
|
invitaciones.each do |address|
|
||||||
# Si la cuenta no existe, envía una invitación por correo, sino,
|
next if Usuarie.where(id: @site.roles.pluck(:usuarie_id)).find_by_email(address)
|
||||||
# no se envía nada
|
|
||||||
#
|
|
||||||
# TODO: Enviar invitación igual! Podemos no usar el Mailer de
|
|
||||||
# DeviseInvitations y usar uno propio que contenga texto y se
|
|
||||||
# envíe de todas formas.
|
|
||||||
usuarie = Usuarie.invite! email: invitacion.address,
|
|
||||||
skip_invitation: true
|
|
||||||
|
|
||||||
# No invitar al sitio si ya estaba en la lista!
|
Usuarie.transaction do
|
||||||
#
|
usuarie = Usuarie.find_by_email(address)
|
||||||
# XXX: En este caso no estamos enviando ninguna invitación
|
usuarie ||= Usuarie.invite!({ email: address, skip_invitation: true }).tap do |u|
|
||||||
next if usuarie.sites.exists? @site.id
|
u.send :generate_invitation_token!
|
||||||
|
end
|
||||||
|
|
||||||
@site.roles << Rol.create(usuarie: usuarie, site: @site,
|
role = @site.roles.create(usuarie: usuarie, temporal: true, rol: invited_as)
|
||||||
temporal: true, rol: invited_as)
|
|
||||||
|
|
||||||
# Invitamos después de crear el rol para que el correo de
|
# XXX: La invitación tiene que ser enviada luego de crear el rol
|
||||||
# invitación pueda recibir el sitio.
|
if role.persisted?
|
||||||
usuarie.deliver_invitation
|
# Si es una cuenta manual que no está confirmada aun,
|
||||||
|
# aprovechar para reconfirmarla.
|
||||||
|
if !usuarie.confirmed? && !usuarie.created_by_invite?
|
||||||
|
usuarie.confirmation_token = nil
|
||||||
|
usuarie.send :generate_confirmation_token!
|
||||||
|
end
|
||||||
|
|
||||||
|
usuarie.deliver_invitation
|
||||||
|
else
|
||||||
|
raise ArgumentError, role.errors.full_messages
|
||||||
|
end
|
||||||
|
rescue ArgumentError => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: @site.name, address: address })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to site_usuaries_path(@site)
|
redirect_to site_usuaries_path(@site)
|
||||||
|
@ -142,6 +150,8 @@ class UsuariesController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
# Traer todas las invitaciones que al menos tengan usuarie y dominio
|
# Traer todas las invitaciones que al menos tengan usuarie y dominio
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
def invitaciones
|
def invitaciones
|
||||||
# XXX: Podríamos usar EmailAddress pero hace chequeos más lentos
|
# XXX: Podríamos usar EmailAddress pero hace chequeos más lentos
|
||||||
params[:invitaciones]&.tr("\r", '')&.split("\n")&.map do |m|
|
params[:invitaciones]&.tr("\r", '')&.split("\n")&.map do |m|
|
||||||
|
@ -150,17 +160,19 @@ class UsuariesController < ApplicationController
|
||||||
nil
|
nil
|
||||||
end.compact.select do |m|
|
end.compact.select do |m|
|
||||||
m.local && m.domain
|
m.local && m.domain
|
||||||
end
|
end.map(&:address)
|
||||||
end
|
end
|
||||||
|
|
||||||
# El tipo de invitación que tenemos que enviar, si alguien mandó
|
# El tipo de invitación que tenemos que enviar, si alguien mandó
|
||||||
# cualquier cosa, usamos el privilegio menor.
|
# cualquier cosa, usamos el privilegio menor.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
def invited_as
|
def invited_as
|
||||||
if Rol::ROLES.include?(params[:invited_as])
|
Rol.role?(params[:invited_as]) ? params[:invited_as] : Rol::INVITADE
|
||||||
params[:invited_as]
|
end
|
||||||
else
|
|
||||||
'invitade'
|
def invite_as
|
||||||
end
|
Rol.role?(params[:invite_as]&.singularize) ? params[:invite_as] : Rol::INVITADE.pluralize
|
||||||
end
|
end
|
||||||
|
|
||||||
def site
|
def site
|
||||||
|
|
81
app/javascript/controllers/non_geo_controller.js
Normal file
81
app/javascript/controllers/non_geo_controller.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { Controller } from 'stimulus'
|
||||||
|
|
||||||
|
require("leaflet/dist/leaflet.css")
|
||||||
|
import L from 'leaflet'
|
||||||
|
delete L.Icon.Default.prototype._getIconUrl
|
||||||
|
|
||||||
|
L.Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||||
|
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||||
|
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ 'lat', 'lng', 'map', 'overlay' ]
|
||||||
|
|
||||||
|
async connect () {
|
||||||
|
this.marker()
|
||||||
|
|
||||||
|
this.latTarget.addEventListener('change', event => this.marker())
|
||||||
|
this.lngTarget.addEventListener('change', event => this.marker())
|
||||||
|
window.addEventListener('resize', event => this.map.invalidateSize())
|
||||||
|
|
||||||
|
this.map.on('click', event => {
|
||||||
|
this.latTarget.value = event.latlng.lat
|
||||||
|
this.lngTarget.value = event.latlng.lng
|
||||||
|
|
||||||
|
this.latTarget.dispatchEvent(new Event('change'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
marker () {
|
||||||
|
if (this._marker) this.map.removeLayer(this._marker)
|
||||||
|
|
||||||
|
this._marker = L.marker(this.coords).addTo(this.map)
|
||||||
|
|
||||||
|
return this._marker
|
||||||
|
}
|
||||||
|
|
||||||
|
get lat () {
|
||||||
|
const lat = parseFloat(this.latTarget.value)
|
||||||
|
|
||||||
|
return isNaN(lat) ? 0 : lat
|
||||||
|
}
|
||||||
|
|
||||||
|
get lng () {
|
||||||
|
const lng = parseFloat(this.lngTarget.value)
|
||||||
|
|
||||||
|
return isNaN(lng) ? 0 : lng
|
||||||
|
}
|
||||||
|
|
||||||
|
get coords () {
|
||||||
|
return [this.lat, this.lng]
|
||||||
|
}
|
||||||
|
|
||||||
|
get bounds () {
|
||||||
|
return [
|
||||||
|
[0, 0],
|
||||||
|
[
|
||||||
|
this.svgOverlay.viewBox.baseVal.height,
|
||||||
|
this.svgOverlay.viewBox.baseVal.width,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get map () {
|
||||||
|
if (!this._map) {
|
||||||
|
this._map = L.map(this.mapTarget, {
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 5
|
||||||
|
}).setView(this.coords, 0);
|
||||||
|
|
||||||
|
this._layer = L.tileLayer(`${this.element.dataset.site}public/map/{z}/{y}/{x}.png`, {
|
||||||
|
minNativeZoom: 0,
|
||||||
|
maxNativeZoom: 5,
|
||||||
|
noWrap: true
|
||||||
|
}).addTo(this._map);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._map
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,11 +103,7 @@ export default class extends Controller {
|
||||||
this.reorder()
|
this.reorder()
|
||||||
|
|
||||||
// Mantenemos el primero a la vista
|
// Mantenemos el primero a la vista
|
||||||
if ("scrollIntoViewIfNeeded" in rows[0].row) {
|
rows[0].row.scrollIntoView({ block: "center" });
|
||||||
rows[0].row.scrollIntoViewIfNeeded()
|
|
||||||
} else {
|
|
||||||
rows[0].row.scrollIntoView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
counter () {
|
counter () {
|
||||||
|
@ -146,7 +142,7 @@ export default class extends Controller {
|
||||||
this.reorder()
|
this.reorder()
|
||||||
|
|
||||||
// Mantenemos el primero a la vista
|
// Mantenemos el primero a la vista
|
||||||
rows[0].row.scrollIntoViewIfNeeded()
|
rows[0].row.scrollIntoView({ block: "center" });
|
||||||
}
|
}
|
||||||
|
|
||||||
bottom (event) {
|
bottom (event) {
|
||||||
|
@ -167,7 +163,7 @@ export default class extends Controller {
|
||||||
this.reorder()
|
this.reorder()
|
||||||
|
|
||||||
// Mantenemos el primero a la vista
|
// Mantenemos el primero a la vista
|
||||||
rows[0].row.scrollIntoViewIfNeeded()
|
rows[0].row.scrollIntoView({ block: "center" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Base para trabajos
|
# Base para trabajos
|
||||||
class ApplicationJob < ActiveJob::Base
|
class ApplicationJob < ActiveJob::Base
|
||||||
include SuckerPunch::Job
|
include Que::ActiveJob::JobExtensions
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,6 @@ class BacktraceJob < ApplicationJob
|
||||||
|
|
||||||
EMPTY_SOURCEMAP = { 'mappings' => '' }.freeze
|
EMPTY_SOURCEMAP = { 'mappings' => '' }.freeze
|
||||||
|
|
||||||
queue_as :low_priority
|
|
||||||
|
|
||||||
attr_reader :params, :site_id
|
attr_reader :params, :site_id
|
||||||
|
|
||||||
def perform(site_id:, params:)
|
def perform(site_id:, params:)
|
||||||
|
|
|
@ -3,9 +3,26 @@
|
||||||
# Realiza el deploy de un sitio
|
# Realiza el deploy de un sitio
|
||||||
class DeployJob < ApplicationJob
|
class DeployJob < ApplicationJob
|
||||||
class DeployException < StandardError; end
|
class DeployException < StandardError; end
|
||||||
|
class DeployTimedOutException < DeployException; end
|
||||||
|
class DeployAlreadyRunningException < DeployException; end
|
||||||
|
|
||||||
|
discard_on ActiveRecord::RecordNotFound
|
||||||
|
|
||||||
|
# Lanzar lo antes posible
|
||||||
|
self.priority = 10
|
||||||
|
|
||||||
|
def handle_error(error)
|
||||||
|
case error
|
||||||
|
when DeployAlreadyRunningException then retry_in 1.minute
|
||||||
|
when DeployTimedOutException then expire
|
||||||
|
else super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# rubocop:disable Metrics/MethodLength
|
# rubocop:disable Metrics/MethodLength
|
||||||
def perform(site, notify = true, time = Time.now)
|
def perform(site, notify: true, time: Time.now, output: false)
|
||||||
|
@output = output
|
||||||
|
|
||||||
ActiveRecord::Base.connection_pool.with_connection do
|
ActiveRecord::Base.connection_pool.with_connection do
|
||||||
@site = Site.find(site)
|
@site = Site.find(site)
|
||||||
|
|
||||||
|
@ -15,53 +32,96 @@ class DeployJob < ApplicationJob
|
||||||
# Como el trabajo actual se aplaza al siguiente, arrastrar la
|
# Como el trabajo actual se aplaza al siguiente, arrastrar la
|
||||||
# hora original para poder ir haciendo timeouts.
|
# hora original para poder ir haciendo timeouts.
|
||||||
if @site.building?
|
if @site.building?
|
||||||
|
notify = false
|
||||||
|
|
||||||
if 10.minutes.ago >= time
|
if 10.minutes.ago >= time
|
||||||
@site.update status: 'waiting'
|
raise DeployTimedOutException,
|
||||||
raise DeployException,
|
|
||||||
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
|
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
|
||||||
|
else
|
||||||
|
raise DeployAlreadyRunningException
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@deployed = {}
|
||||||
|
@site.update status: 'building'
|
||||||
|
@site.deployment_list.each do |d|
|
||||||
|
begin
|
||||||
|
raise DeployException, 'Una dependencia falló' if failed_dependencies? d
|
||||||
|
|
||||||
|
status = d.deploy(output: @output)
|
||||||
|
seconds = d.build_stats.last.try(:seconds) || 0
|
||||||
|
size = d.size
|
||||||
|
urls = d.respond_to?(:urls) ? d.urls : [d.url].compact
|
||||||
|
rescue StandardError => e
|
||||||
|
status = false
|
||||||
|
seconds ||= 0
|
||||||
|
size ||= 0
|
||||||
|
# XXX: Hace que se vea la tabla
|
||||||
|
urls ||= [nil]
|
||||||
|
|
||||||
|
notify_exception e, d
|
||||||
end
|
end
|
||||||
|
|
||||||
DeployJob.perform_in(60, site, notify, time)
|
@deployed[d.type.underscore.to_sym] = {
|
||||||
return
|
status: status,
|
||||||
|
seconds: seconds,
|
||||||
|
size: size,
|
||||||
|
urls: urls
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@site.update status: 'building'
|
return unless @output
|
||||||
# Asegurarse que DeployLocal sea el primero!
|
|
||||||
@deployed = { deploy_local: deploy_locally }
|
|
||||||
|
|
||||||
# No es opcional
|
puts (Terminal::Table.new do |t|
|
||||||
unless @deployed[:deploy_local]
|
t << (%w[type] + @deployed.values.first.keys)
|
||||||
|
t.add_separator
|
||||||
|
@deployed.each do |type, row|
|
||||||
|
t << ([type.to_s] + row.values)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
ensure
|
||||||
|
if @site.present?
|
||||||
@site.update status: 'waiting'
|
@site.update status: 'waiting'
|
||||||
|
|
||||||
notify_usuaries if notify
|
notify_usuaries if notify
|
||||||
|
|
||||||
# Hacer fallar la tarea
|
puts "\a" if @output
|
||||||
raise DeployException, deploy_local.build_stats.last.log
|
|
||||||
end
|
end
|
||||||
|
|
||||||
deploy_others
|
|
||||||
|
|
||||||
# Volver a la espera
|
|
||||||
@site.update status: 'waiting'
|
|
||||||
|
|
||||||
notify_usuaries if notify
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def deploy_local
|
# Detecta si un método de publicación tiene dependencias fallidas
|
||||||
@deploy_local ||= @site.deploys.find_by(type: 'DeployLocal')
|
#
|
||||||
|
# @param :deploy [Deploy]
|
||||||
|
# @return [Boolean]
|
||||||
|
def failed_dependencies?(deploy)
|
||||||
|
failed_dependencies(deploy).present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def deploy_locally
|
# Obtiene las dependencias fallidas de un deploy
|
||||||
deploy_local.deploy
|
#
|
||||||
|
# @param :deploy [Deploy]
|
||||||
|
# @return [Array]
|
||||||
|
def failed_dependencies(deploy)
|
||||||
|
deploy.class::DEPENDENCIES & (@deployed.reject do |_, v|
|
||||||
|
v[:status]
|
||||||
|
end.keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
def deploy_others
|
# @param :exception [StandardError]
|
||||||
@site.deploys.where.not(type: 'DeployLocal').find_each do |d|
|
# @param :deploy [Deploy]
|
||||||
@deployed[d.type.underscore.to_sym] = d.deploy
|
def notify_exception(exception, deploy = nil)
|
||||||
end
|
data = {
|
||||||
|
site: @site.id,
|
||||||
|
deploy: deploy&.type,
|
||||||
|
log: deploy&.build_stats&.last&.log,
|
||||||
|
failed_dependencies: (failed_dependencies(deploy) if deploy)
|
||||||
|
}
|
||||||
|
|
||||||
|
ExceptionNotifier.notify_exception(exception, data: data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_usuaries
|
def notify_usuaries
|
||||||
|
|
|
@ -3,37 +3,43 @@
|
||||||
# Notifica excepciones a una instancia de Gitlab, como incidencias
|
# Notifica excepciones a una instancia de Gitlab, como incidencias
|
||||||
# nuevas o como comentarios a las incidencias pre-existentes.
|
# nuevas o como comentarios a las incidencias pre-existentes.
|
||||||
class GitlabNotifierJob < ApplicationJob
|
class GitlabNotifierJob < ApplicationJob
|
||||||
|
class GitlabNotifierError < StandardError; end
|
||||||
|
|
||||||
include ExceptionNotifier::BacktraceCleaner
|
include ExceptionNotifier::BacktraceCleaner
|
||||||
|
|
||||||
# Variables que vamos a acceder luego
|
# Variables que vamos a acceder luego
|
||||||
attr_reader :exception, :options, :issue_data, :cached
|
attr_reader :exception, :options, :issue_data, :cached
|
||||||
|
|
||||||
queue_as :low_priority
|
|
||||||
|
|
||||||
# @param [Exception] la excepción lanzada
|
# @param [Exception] la excepción lanzada
|
||||||
# @param [Hash] opciones de ExceptionNotifier
|
# @param [Hash] opciones de ExceptionNotifier
|
||||||
def perform(exception, **options)
|
def perform(exception, **options)
|
||||||
@exception = exception
|
@exception = exception
|
||||||
@options = options
|
@options = fix_options options
|
||||||
@issue_data = { count: 1 }
|
@issue_data = { count: 1 }
|
||||||
# Necesitamos saber si el issue ya existía
|
# Necesitamos saber si el issue ya existía
|
||||||
@cached = false
|
@cached = false
|
||||||
|
@issue = {}
|
||||||
|
|
||||||
# Traemos los datos desde la caché si existen, sino generamos un
|
# Traemos los datos desde la caché si existen, sino generamos un
|
||||||
# issue nuevo e inicializamos la caché
|
# issue nuevo e inicializamos la caché
|
||||||
@issue_data = Rails.cache.fetch(cache_key) do
|
@issue_data = Rails.cache.fetch(cache_key) do
|
||||||
issue = client.new_issue confidential: true, title: title, description: description, issue_type: 'incident'
|
@issue = client.new_issue confidential: true, title: title, description: description, issue_type: 'incident'
|
||||||
@cached = true
|
@cached = true
|
||||||
|
|
||||||
{
|
{
|
||||||
count: 1,
|
count: 1,
|
||||||
issue: issue['iid'],
|
issue: @issue['iid'],
|
||||||
user_agents: [user_agent].compact,
|
user_agents: [user_agent].compact,
|
||||||
params: [request&.filtered_parameters].compact,
|
params: [request&.filtered_parameters].compact,
|
||||||
urls: [url].compact
|
urls: [url].compact
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if @issue['iid'].blank? && issue_data[:issue].blank?
|
||||||
|
Rails.cache.delete(cache_key)
|
||||||
|
raise GitlabNotifierError, @issue.dig('message', 'title')&.join(', ')
|
||||||
|
end
|
||||||
|
|
||||||
# No seguimos actualizando si acabamos de generar el issue
|
# No seguimos actualizando si acabamos de generar el issue
|
||||||
return if cached
|
return if cached
|
||||||
|
|
||||||
|
@ -53,9 +59,9 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
Rails.cache.write(cache_key, issue_data)
|
Rails.cache.write(cache_key, issue_data)
|
||||||
# Si este trabajo genera una excepción va a entrar en un loop, así que
|
# Si este trabajo genera una excepción va a entrar en un loop, así que
|
||||||
# la notificamos por correo
|
# la notificamos por correo
|
||||||
rescue Exception => e
|
rescue StandardError => e
|
||||||
email_notification.call(e)
|
email_notification.call(e, data: @issue)
|
||||||
email_notification.call(exception, options)
|
email_notification.call(exception, data: @options)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -76,10 +82,15 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
exception.class.name,
|
exception.class.name,
|
||||||
Digest::SHA1.hexdigest(exception.message),
|
Digest::SHA1.hexdigest(exception.message),
|
||||||
Digest::SHA1.hexdigest(backtrace&.first.to_s),
|
Digest::SHA1.hexdigest(backtrace&.first.to_s),
|
||||||
Digest::SHA1.hexdigest(options.dig(:data, :params, 'errors').to_s)
|
Digest::SHA1.hexdigest(errors.to_s)
|
||||||
].join('/')
|
].join('/')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Array]
|
||||||
|
def errors
|
||||||
|
options.dig(:data, :params, 'errors') || []
|
||||||
|
end
|
||||||
|
|
||||||
# Define si es una excepción de javascript o local
|
# Define si es una excepción de javascript o local
|
||||||
#
|
#
|
||||||
# @see BacktraceJob
|
# @see BacktraceJob
|
||||||
|
@ -104,6 +115,7 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def description
|
def description
|
||||||
@description ||= ''.dup.tap do |d|
|
@description ||= ''.dup.tap do |d|
|
||||||
|
d << log_section
|
||||||
d << request_section
|
d << request_section
|
||||||
d << javascript_section
|
d << javascript_section
|
||||||
d << javascript_footer
|
d << javascript_footer
|
||||||
|
@ -117,6 +129,7 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def body
|
def body
|
||||||
@body ||= ''.dup.tap do |b|
|
@body ||= ''.dup.tap do |b|
|
||||||
|
b << log_section
|
||||||
b << request_section
|
b << request_section
|
||||||
b << javascript_footer
|
b << javascript_footer
|
||||||
b << data_section
|
b << data_section
|
||||||
|
@ -151,6 +164,21 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
@client ||= GitlabApiClient.new
|
@client ||= GitlabApiClient.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def log_section
|
||||||
|
return '' unless options.dig(:data, :log)
|
||||||
|
|
||||||
|
<<~LOG
|
||||||
|
|
||||||
|
# Build log
|
||||||
|
|
||||||
|
```
|
||||||
|
#{options[:data].delete(:log)}
|
||||||
|
```
|
||||||
|
|
||||||
|
LOG
|
||||||
|
end
|
||||||
|
|
||||||
# Muestra información de la petición
|
# Muestra información de la petición
|
||||||
#
|
#
|
||||||
# @return [String]
|
# @return [String]
|
||||||
|
@ -235,8 +263,8 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
|
|
||||||
## Data
|
## Data
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#{pp options[:data]}
|
#{options[:data].to_yaml}
|
||||||
```
|
```
|
||||||
|
|
||||||
DATA
|
DATA
|
||||||
|
@ -257,4 +285,16 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
def url
|
def url
|
||||||
@url ||= request&.url || options.dig(:data, :params, 'context', 'url')
|
@url ||= request&.url || options.dig(:data, :params, 'context', 'url')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Define llaves necesarias
|
||||||
|
#
|
||||||
|
# @param :options [Hash]
|
||||||
|
# @return [Hash]
|
||||||
|
def fix_options(options)
|
||||||
|
options = { data: options } unless options.is_a? Hash
|
||||||
|
options[:data] ||= {}
|
||||||
|
options[:data][:params] ||= {}
|
||||||
|
|
||||||
|
options
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
# bundle exec rails c
|
# bundle exec rails c
|
||||||
# m = Maintenance.create message_en: 'reason', message_es: 'razón',
|
# m = Maintenance.create message_en: 'reason', message_es: 'razón',
|
||||||
# estimated_from: Time.now, estimated_to: Time.now + 1.hour
|
# estimated_from: Time.now, estimated_to: Time.now + 1.hour
|
||||||
# MaintenanceJob.perform_async(maintenance_id: m.id)
|
# MaintenanceJob.perform_later(maintenance_id: m.id)
|
||||||
#
|
#
|
||||||
# Lo mismo para salir de mantenimiento, agregando el atributo
|
# Lo mismo para salir de mantenimiento, agregando el atributo
|
||||||
# are_we_back: true al crear el Maintenance.
|
# are_we_back: true al crear el Maintenance.
|
||||||
|
|
17
app/jobs/renew_distributed_press_tokens_job.rb
Normal file
17
app/jobs/renew_distributed_press_tokens_job.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Renueva los tokens de Distributed Press antes que se venzan,
|
||||||
|
# activando los callbacks que hacen que se refresque el token.
|
||||||
|
class RenewDistributedPressTokensJob < ApplicationJob
|
||||||
|
# Renueva todos los tokens a punto de vencer o informa el error sin
|
||||||
|
# detener la tarea si algo pasa.
|
||||||
|
def perform
|
||||||
|
DistributedPressPublisher.with_about_to_expire_tokens.find_each do |publisher|
|
||||||
|
publisher.save
|
||||||
|
rescue DistributedPress::V1::Error => e
|
||||||
|
data = { instance: publisher.instance, expires_at: publisher.client.token.expires_at }
|
||||||
|
|
||||||
|
ExceptionNotifier.notify_exception(e, data: data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
22
app/lib/active_job/serializers/exception_serializer.rb
Normal file
22
app/lib/active_job/serializers/exception_serializer.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'json/add/exception'
|
||||||
|
|
||||||
|
module ActiveJob
|
||||||
|
module Serializers
|
||||||
|
class ExceptionSerializer < ObjectSerializer # :nodoc:
|
||||||
|
def serialize(ex)
|
||||||
|
super('value' => { 'class' => ex.class.name, 'exception' => ex.as_json })
|
||||||
|
end
|
||||||
|
|
||||||
|
def deserialize(hash)
|
||||||
|
hash.dig('value', 'class').constantize.json_create(hash.dig('value', 'exception'))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def klass
|
||||||
|
Exception
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,11 +4,6 @@ module ActiveStorage
|
||||||
class Service
|
class Service
|
||||||
# Sube los archivos a cada repositorio y los agrega al LFS de su
|
# Sube los archivos a cada repositorio y los agrega al LFS de su
|
||||||
# repositorio git.
|
# 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
|
class JekyllService < Service::DiskService
|
||||||
# Genera un servicio para un sitio determinado
|
# Genera un servicio para un sitio determinado
|
||||||
#
|
#
|
||||||
|
@ -20,6 +15,21 @@ module ActiveStorage
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Solo copiamos el archivo si no existe
|
||||||
|
#
|
||||||
|
# @param :key [String]
|
||||||
|
# @param :io [IO]
|
||||||
|
# @param :checksum [String]
|
||||||
|
def upload(key, io, checksum: nil, **)
|
||||||
|
instrument :upload, key: key, checksum: checksum do
|
||||||
|
unless exist?(key)
|
||||||
|
IO.copy_stream(io, make_path_for(key))
|
||||||
|
LfsObjectService.new(site: site, blob: blob_for(key)).process
|
||||||
|
end
|
||||||
|
ensure_integrity_of(key, checksum) if checksum
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Lo mismo que en DiskService agregando el nombre de archivo en la
|
# Lo mismo que en DiskService agregando el nombre de archivo en la
|
||||||
# firma. Esto permite que luego podamos guardar el archivo donde
|
# firma. Esto permite que luego podamos guardar el archivo donde
|
||||||
# corresponde.
|
# corresponde.
|
||||||
|
@ -67,7 +77,9 @@ module ActiveStorage
|
||||||
# @param :key [String]
|
# @param :key [String]
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def filename_for(key)
|
def filename_for(key)
|
||||||
ActiveStorage::Blob.where(key: key).limit(1).pluck(:filename).first
|
blob_for(key).filename.to_s.tap do |filename|
|
||||||
|
raise ArgumentError, "Filename for key #{key} is blank" if filename.blank?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Crea una ruta para la llave con un nombre conocido.
|
# Crea una ruta para la llave con un nombre conocido.
|
||||||
|
@ -77,6 +89,15 @@ module ActiveStorage
|
||||||
def path_for(key)
|
def path_for(key)
|
||||||
File.join root, folder_for(key), filename_for(key)
|
File.join root, folder_for(key), filename_for(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Site]
|
||||||
|
def site
|
||||||
|
@site ||= Site.find_by_name(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def blob_for(key)
|
||||||
|
ActiveStorage::Blob.find_by(key: key, service_name: name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
21
app/lib/devise/failure_app_decorator.rb
Normal file
21
app/lib/devise/failure_app_decorator.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Devise
|
||||||
|
module FailureAppDecorator
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
include AbstractController::Callbacks
|
||||||
|
|
||||||
|
around_action :set_locale
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_locale(&action)
|
||||||
|
I18n.with_locale(session[:locale] || I18n.locale, &action)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Devise::FailureApp.include Devise::FailureAppDecorator
|
|
@ -11,7 +11,12 @@ module ExceptionNotifier
|
||||||
# @param [Exception]
|
# @param [Exception]
|
||||||
# @param [Hash]
|
# @param [Hash]
|
||||||
def call(exception, **options)
|
def call(exception, **options)
|
||||||
GitlabNotifierJob.perform_async(exception, **options)
|
case exception
|
||||||
|
when BacktraceJob::BacktraceException
|
||||||
|
GitlabNotifierJob.perform_later(exception, **options)
|
||||||
|
else
|
||||||
|
GitlabNotifierJob.perform_now(exception, **options)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
13
app/lib/hidden_service_client.rb
Normal file
13
app/lib/hidden_service_client.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'httparty'
|
||||||
|
|
||||||
|
class HiddenServiceClient
|
||||||
|
include HTTParty
|
||||||
|
|
||||||
|
base_uri ENV.fetch('HIDDEN_SERVICE', 'http://tor:3000')
|
||||||
|
|
||||||
|
def create(name)
|
||||||
|
self.class.get("/#{name}").body
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,21 +8,66 @@
|
||||||
# TODO: Agregar firma GPG y header Autocrypt
|
# TODO: Agregar firma GPG y header Autocrypt
|
||||||
# TODO: Cifrar con GPG si le usuarie nos dio su llave
|
# TODO: Cifrar con GPG si le usuarie nos dio su llave
|
||||||
class DeployMailer < ApplicationMailer
|
class DeployMailer < ApplicationMailer
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
include ActionView::Helpers::DateHelper
|
||||||
|
|
||||||
# rubocop:disable Metrics/AbcSize
|
# rubocop:disable Metrics/AbcSize
|
||||||
def deployed(which_ones)
|
def deployed(deploys = {})
|
||||||
@usuarie = Usuarie.find(params[:usuarie])
|
usuarie = Usuarie.find(params[:usuarie])
|
||||||
@site = @usuarie.sites.find(params[:site])
|
site = usuarie.sites.find(params[:site])
|
||||||
@deploys = which_ones
|
hostname = site.hostname
|
||||||
@deploy_local = @site.deploys.find_by(type: 'DeployLocal')
|
deploys ||= {}
|
||||||
|
|
||||||
# Informamos a cada quien en su idioma y damos una dirección de
|
# Informamos a cada quien en su idioma y damos una dirección de
|
||||||
# respuesta porque a veces les usuaries nos escriben
|
# respuesta porque a veces les usuaries nos escriben
|
||||||
I18n.with_locale(@usuarie.lang) do
|
I18n.with_locale(usuarie.lang) do
|
||||||
mail(to: @usuarie.email,
|
subject = t('.subject', site: site.name)
|
||||||
reply_to: "sutty@#{Site.domain}",
|
|
||||||
subject: I18n.t('deploy_mailer.deployed.subject',
|
@hi = t('.hi')
|
||||||
site: @site.name))
|
@explanation = t('.explanation', fqdn: hostname)
|
||||||
|
@help = t('.help')
|
||||||
|
|
||||||
|
@headers = %w[type status url seconds size].map do |header|
|
||||||
|
t(".th.#{header}")
|
||||||
|
end
|
||||||
|
|
||||||
|
@table = deploys.each_pair.map do |deploy, value|
|
||||||
|
{
|
||||||
|
title: t(".#{deploy}.title"),
|
||||||
|
status: t(".#{deploy}.#{value[:status] ? 'success' : 'error'}"),
|
||||||
|
urls: value[:urls],
|
||||||
|
seconds: {
|
||||||
|
human: distance_of_time_in_words(value[:seconds].seconds),
|
||||||
|
machine: "PT#{value[:seconds]}S"
|
||||||
|
},
|
||||||
|
size: number_to_human_size(value[:size], precision: 2)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@terminal_table = Terminal::Table.new do |t|
|
||||||
|
t << @headers
|
||||||
|
t.add_separator
|
||||||
|
@table.each do |row|
|
||||||
|
row[:urls].each do |url|
|
||||||
|
t << (row.map do |k, v|
|
||||||
|
case k
|
||||||
|
when :seconds then v[:human]
|
||||||
|
when :urls then url
|
||||||
|
else v
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mail(to: usuarie.email, reply_to: "sutty@#{Site.domain}", subject: subject)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/AbcSize
|
# rubocop:enable Metrics/AbcSize
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def t(key, **args)
|
||||||
|
I18n.t("deploy_mailer.deployed#{key}", **args)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
14
app/models/code_of_conduct.rb
Normal file
14
app/models/code_of_conduct.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Códigos de conducta
|
||||||
|
class CodeOfConduct < ApplicationRecord
|
||||||
|
extend Mobility
|
||||||
|
|
||||||
|
translates :title, type: :string, locale_accessors: true
|
||||||
|
translates :description, type: :text, locale_accessors: true
|
||||||
|
translates :content, type: :text, locale_accessors: true
|
||||||
|
|
||||||
|
validates :title, presence: true, uniqueness: true
|
||||||
|
validates :description, presence: true
|
||||||
|
validates :content, presence: true
|
||||||
|
end
|
26
app/models/concerns/usuarie/consent.rb
Normal file
26
app/models/concerns/usuarie/consent.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Usuarie
|
||||||
|
# Gestiona los campos de consentimiento
|
||||||
|
module Consent
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
CONSENT_FIELDS = %i[privacy_policy_accepted terms_of_service_accepted code_of_conduct_accepted available_for_feedback_accepted]
|
||||||
|
|
||||||
|
CONSENT_FIELDS.each do |field|
|
||||||
|
attribute field, :boolean
|
||||||
|
end
|
||||||
|
|
||||||
|
before_save :update_consent_fields!
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def update_consent_fields!
|
||||||
|
CONSENT_FIELDS.each do |field|
|
||||||
|
send(:"#{field}_at=", Time.now) if send(field).present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'open3'
|
require 'open3'
|
||||||
|
|
||||||
# Este modelo implementa los distintos tipos de alojamiento que provee
|
# Este modelo implementa los distintos tipos de alojamiento que provee
|
||||||
# Sutty.
|
# Sutty.
|
||||||
#
|
#
|
||||||
|
@ -11,7 +12,14 @@ class Deploy < ApplicationRecord
|
||||||
belongs_to :site
|
belongs_to :site
|
||||||
has_many :build_stats, dependent: :destroy
|
has_many :build_stats, dependent: :destroy
|
||||||
|
|
||||||
def deploy
|
DEPENDENCIES = []
|
||||||
|
SOFT_DEPENDENCIES = []
|
||||||
|
|
||||||
|
def deploy(**)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def url
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,6 +31,9 @@ class Deploy < ApplicationRecord
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Realizar tareas de limpieza.
|
||||||
|
def cleanup!; end
|
||||||
|
|
||||||
def time_start
|
def time_start
|
||||||
@start = Time.now
|
@start = Time.now
|
||||||
end
|
end
|
||||||
|
@ -39,6 +50,7 @@ class Deploy < ApplicationRecord
|
||||||
site.path
|
site.path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# XXX: Ver DeployLocal#bundle
|
||||||
def gems_dir
|
def gems_dir
|
||||||
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
|
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
|
||||||
end
|
end
|
||||||
|
@ -48,20 +60,26 @@ class Deploy < ApplicationRecord
|
||||||
#
|
#
|
||||||
# @param [String]
|
# @param [String]
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def run(cmd)
|
def run(cmd, output: false)
|
||||||
r = nil
|
r = nil
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
time_start
|
time_start
|
||||||
Dir.chdir(site.path) do
|
Dir.chdir(site.path) do
|
||||||
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, o, t|
|
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, o, t|
|
||||||
r = t.value
|
|
||||||
# XXX: Tenemos que leer línea por línea porque en salidas largas
|
|
||||||
# se cuelga la IO
|
|
||||||
# TODO: Enviar a un websocket para ver el proceso en vivo?
|
# TODO: Enviar a un websocket para ver el proceso en vivo?
|
||||||
o.each do |line|
|
Thread.new do
|
||||||
lines << line
|
o.each do |line|
|
||||||
|
lines << line
|
||||||
|
|
||||||
|
puts line if output
|
||||||
|
end
|
||||||
|
rescue IOError => e
|
||||||
|
lines << e.message
|
||||||
|
puts e.message if output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
r = t.value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
time_stop
|
time_stop
|
||||||
|
@ -75,6 +93,20 @@ class Deploy < ApplicationRecord
|
||||||
r&.success?
|
r&.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Variables de entorno
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def local_env
|
||||||
|
@local_env ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Trae todas las dependencias
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def self.all_dependencies
|
||||||
|
self::DEPENDENCIES | self::SOFT_DEPENDENCIES
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# @param [String]
|
# @param [String]
|
||||||
|
@ -82,4 +114,12 @@ class Deploy < ApplicationRecord
|
||||||
def readable_cmd(cmd)
|
def readable_cmd(cmd)
|
||||||
cmd.split(' -', 2).first.tr(' ', '_')
|
cmd.split(' -', 2).first.tr(' ', '_')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def deploy_local
|
||||||
|
@deploy_local ||= site.deploys.find_by(type: 'DeployLocal')
|
||||||
|
end
|
||||||
|
|
||||||
|
def non_local_deploys
|
||||||
|
@non_local_deploys ||= site.deploys.where.not(type: 'DeployLocal')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
class DeployAlternativeDomain < Deploy
|
class DeployAlternativeDomain < Deploy
|
||||||
store :values, accessors: %i[hostname], coder: JSON
|
store :values, accessors: %i[hostname], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# Generar un link simbólico del sitio principal al alternativo
|
# Generar un link simbólico del sitio principal al alternativo
|
||||||
def deploy
|
def deploy(**)
|
||||||
File.symlink?(destination) ||
|
File.symlink?(destination) ||
|
||||||
File.symlink(site.hostname, destination).zero?
|
File.symlink(site.hostname, destination).zero?
|
||||||
end
|
end
|
||||||
|
@ -18,6 +20,14 @@ class DeployAlternativeDomain < Deploy
|
||||||
end
|
end
|
||||||
|
|
||||||
def destination
|
def destination
|
||||||
File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, ''))
|
@destination ||= File.join(Rails.root, '_deploy', fqdn)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fqdn
|
||||||
|
hostname.gsub(/\.\z/, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def url
|
||||||
|
"https://#{File.basename destination}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
210
app/models/deploy_distributed_press.rb
Normal file
210
app/models/deploy_distributed_press.rb
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'distributed_press/v1/client/site'
|
||||||
|
require 'njalla/v1'
|
||||||
|
|
||||||
|
# Soportar Distributed Press APIv1
|
||||||
|
#
|
||||||
|
# Usa tokens de publicación efímeros para todas las acciones.
|
||||||
|
#
|
||||||
|
# Al ser creado, genera el sitio en la instancia de Distributed Press
|
||||||
|
# configurada y almacena el ID.
|
||||||
|
#
|
||||||
|
# Al ser publicado, envía los archivos en un tarball y actualiza la
|
||||||
|
# información.
|
||||||
|
class DeployDistributedPress < Deploy
|
||||||
|
store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON
|
||||||
|
|
||||||
|
before_create :create_remote_site!, :create_njalla_records!
|
||||||
|
before_destroy :delete_remote_site!, :delete_njalla_records!
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
|
# Actualiza la información y luego envía los cambios
|
||||||
|
#
|
||||||
|
# @param :output [Bool]
|
||||||
|
# @return [Bool]
|
||||||
|
def deploy
|
||||||
|
status = false
|
||||||
|
log = []
|
||||||
|
|
||||||
|
time_start
|
||||||
|
|
||||||
|
create_remote_site! if remote_site_id.blank?
|
||||||
|
create_njalla_records!
|
||||||
|
save
|
||||||
|
|
||||||
|
if remote_site_id.blank?
|
||||||
|
raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press'
|
||||||
|
end
|
||||||
|
|
||||||
|
if create_njalla_records? && remote_info[:njalla].blank?
|
||||||
|
raise DeployJob::DeployException, 'No se pudieron crear los registros necesarios en Njalla'
|
||||||
|
end
|
||||||
|
|
||||||
|
site_client.tap do |c|
|
||||||
|
stdout = Thread.new(publisher.logger_out) do |io|
|
||||||
|
until io.eof?
|
||||||
|
line = io.gets
|
||||||
|
|
||||||
|
puts line if output
|
||||||
|
log << line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
status = c.publish(publishing_site, deploy_local.destination)
|
||||||
|
|
||||||
|
if status
|
||||||
|
self.remote_info[:distributed_press] = c.show(publishing_site).to_h
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
publisher.logger.close
|
||||||
|
stdout.join
|
||||||
|
end
|
||||||
|
|
||||||
|
time_stop
|
||||||
|
|
||||||
|
create_stat! status, log.join
|
||||||
|
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
def limit; end
|
||||||
|
|
||||||
|
def size
|
||||||
|
deploy_local.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def destination; end
|
||||||
|
|
||||||
|
# Devuelve las URLs de todos los protocolos
|
||||||
|
def urls
|
||||||
|
gateway_urls
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @return [Array]
|
||||||
|
def gateway_urls
|
||||||
|
remote_info.dig(:distributed_press, :links)&.values&.map do |protocol|
|
||||||
|
[ protocol[:link], protocol[:gateway] ]
|
||||||
|
end&.flatten&.compact&.select do |link|
|
||||||
|
link.include? '://'
|
||||||
|
end || []
|
||||||
|
end
|
||||||
|
|
||||||
|
# El cliente de la API
|
||||||
|
#
|
||||||
|
# TODO: cuando soportemos más, tiene que haber una relación entre
|
||||||
|
# DeployDistributedPress y DistributedPressPublisher.
|
||||||
|
#
|
||||||
|
# @return [DistributedPressPublisher]
|
||||||
|
def publisher
|
||||||
|
@publisher ||= DistributedPressPublisher.last
|
||||||
|
end
|
||||||
|
|
||||||
|
# El cliente para actualizar el sitio
|
||||||
|
#
|
||||||
|
# @return [DistributedPress::V1::Client::Site]
|
||||||
|
def site_client
|
||||||
|
DistributedPress::V1::Client::Site.new(publisher.client)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera el esquema de datos para poder publicar el sitio
|
||||||
|
#
|
||||||
|
# @return [DistributedPress::V1::Schemas::PublishingSite]
|
||||||
|
def publishing_site
|
||||||
|
DistributedPress::V1::Schemas::PublishingSite.new.call(id: remote_site_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera el esquema de datos para crear el sitio
|
||||||
|
#
|
||||||
|
# @return [DistributedPressPublisher::V1::Schemas::NewSite]
|
||||||
|
def create_site
|
||||||
|
DistributedPress::V1::Schemas::NewSite.new.call(domain: hostname, protocols: { http: true, ipfs: true, hyper: true })
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crea el sitio en la instancia con el hostname especificado
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def create_remote_site!
|
||||||
|
created_site = site_client.create(create_site)
|
||||||
|
|
||||||
|
self.remote_site_id = created_site[:id]
|
||||||
|
self.remote_info ||= {}
|
||||||
|
self.remote_info[:distributed_press] = created_site.to_h
|
||||||
|
nil
|
||||||
|
rescue DistributedPress::V1::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crea los registros en Njalla
|
||||||
|
#
|
||||||
|
# XXX: Esto depende de nuestro DNS actual, cuando lo migremos hay
|
||||||
|
# que eliminarlo.
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def create_njalla_records!
|
||||||
|
return unless create_njalla_records?
|
||||||
|
|
||||||
|
self.remote_info ||= {}
|
||||||
|
self.remote_info[:njalla] ||= {}
|
||||||
|
self.remote_info[:njalla][:a] ||= njalla.add_record(name: site.name, type: 'CNAME', content: "#{Site.domain}.").to_h
|
||||||
|
self.remote_info[:njalla][:cname] ||= njalla.add_record(name: "www.#{site.name}", type: 'CNAME', content: "#{Site.domain}.").to_h
|
||||||
|
self.remote_info[:njalla][:ns] ||= njalla.add_record(name: "_dnslink.#{site.name}", type: 'NS', content: "#{publisher.hostname}.").to_h
|
||||||
|
|
||||||
|
nil
|
||||||
|
rescue HTTParty::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
self.remote_info.delete :njalla
|
||||||
|
ensure
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Registra lo que sucedió
|
||||||
|
#
|
||||||
|
# @param status [Bool]
|
||||||
|
# @param log [String]
|
||||||
|
# @return [nil]
|
||||||
|
def create_stat!(status, log)
|
||||||
|
build_stats.create action: publisher.to_s,log: log, seconds: time_spent_in_seconds, bytes: size, status: status
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_remote_site!
|
||||||
|
site_client.delete(publishing_site)
|
||||||
|
nil
|
||||||
|
rescue DistributedPress::V1::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_njalla_records!
|
||||||
|
return unless create_njalla_records?
|
||||||
|
|
||||||
|
%w[a ns cname].each do |type|
|
||||||
|
next if (id = remote_info.dig('njalla', type, 'id')).blank?
|
||||||
|
|
||||||
|
njalla.remove_record(id: id.to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Actualizar registros en Njalla
|
||||||
|
#
|
||||||
|
# @return [Njalla::V1::Domain]
|
||||||
|
def njalla
|
||||||
|
@njalla ||=
|
||||||
|
begin
|
||||||
|
client = Njalla::V1::Client.new(token: Rails.application.credentials.njalla)
|
||||||
|
|
||||||
|
Njalla::V1::Domain.new(domain: Site.domain, client: client)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si tenemos que crear registros en Njalla
|
||||||
|
def create_njalla_records?
|
||||||
|
!site.name.end_with?('.')
|
||||||
|
end
|
||||||
|
end
|
30
app/models/deploy_full_rsync.rb
Normal file
30
app/models/deploy_full_rsync.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DeployFullRsync < DeployRsync
|
||||||
|
SOFT_DEPENDENCIES = %i[
|
||||||
|
deploy_alternative_domain
|
||||||
|
deploy_localized_domain
|
||||||
|
deploy_hidden_service
|
||||||
|
deploy_www
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sincroniza las ubicaciones alternativas también, ignorando las que
|
||||||
|
# todavía no se generaron. Solo falla si ningún sitio fue
|
||||||
|
# sincronizado o si alguna sincronización falló.
|
||||||
|
#
|
||||||
|
# @param :output [Boolean]
|
||||||
|
# @return [Boolean]
|
||||||
|
def rsync(output: false)
|
||||||
|
result =
|
||||||
|
self.class.all_dependencies.map(&:to_s).map(&:classify).map do |dependency|
|
||||||
|
site.deploys.where(type: dependency).find_each.map do |deploy|
|
||||||
|
next unless File.exist? deploy.destination
|
||||||
|
|
||||||
|
run %(rsync -aviH --delete-after --timeout=5 #{Shellwords.escape deploy.destination} #{Shellwords.escape destination}), output: output
|
||||||
|
rescue StandardError
|
||||||
|
end
|
||||||
|
end.flatten.compact
|
||||||
|
|
||||||
|
result.present? && result.all?
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,17 +2,36 @@
|
||||||
|
|
||||||
# Genera una versión onion
|
# Genera una versión onion
|
||||||
class DeployHiddenService < DeployWww
|
class DeployHiddenService < DeployWww
|
||||||
def deploy
|
store :values, accessors: %i[onion], coder: JSON
|
||||||
return true if fqdn.blank?
|
|
||||||
|
|
||||||
super
|
before_create :create_hidden_service!
|
||||||
end
|
|
||||||
|
ONION_RE = /\A[a-z0-9]{56}\.onion\z/.freeze
|
||||||
|
|
||||||
def fqdn
|
def fqdn
|
||||||
values[:onion]
|
create_hidden_service! if onion.blank?
|
||||||
|
|
||||||
|
onion.tap do |onion|
|
||||||
|
raise ArgumentError, 'Aun no se generó la dirección .onion' if onion.blank?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
'http://' + fqdn
|
"http://#{fqdn}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_hidden_service!
|
||||||
|
onion_address = HiddenServiceClient.new.create(site.name)
|
||||||
|
|
||||||
|
if ONION_RE =~ onion_address
|
||||||
|
self.onion = onion_address
|
||||||
|
|
||||||
|
usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor'
|
||||||
|
params = { onion: onion_address, deploy: self }
|
||||||
|
|
||||||
|
SiteService.new(site: site, usuarie: usuarie, params: params).add_onion
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,12 +12,14 @@ class DeployLocal < Deploy
|
||||||
#
|
#
|
||||||
# Pasamos variables de entorno mínimas para no filtrar secretos de
|
# Pasamos variables de entorno mínimas para no filtrar secretos de
|
||||||
# Sutty
|
# Sutty
|
||||||
def deploy
|
def deploy(output: false)
|
||||||
return false unless mkdir
|
return false unless mkdir
|
||||||
return false unless yarn
|
return false unless git_lfs(output: output)
|
||||||
return false unless bundle
|
return false unless yarn(output: output)
|
||||||
|
return false unless pnpm(output: output)
|
||||||
|
return false unless bundle(output: output)
|
||||||
|
|
||||||
jekyll_build
|
jekyll_build(output: output)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sólo permitimos un deploy local
|
# Sólo permitimos un deploy local
|
||||||
|
@ -25,6 +27,10 @@ class DeployLocal < Deploy
|
||||||
1
|
1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def url
|
||||||
|
site.url
|
||||||
|
end
|
||||||
|
|
||||||
# Obtener el tamaño de todos los archivos y directorios (los
|
# Obtener el tamaño de todos los archivos y directorios (los
|
||||||
# directorios son archivos :)
|
# directorios son archivos :)
|
||||||
def size
|
def size
|
||||||
|
@ -45,6 +51,17 @@ class DeployLocal < Deploy
|
||||||
File.join(Rails.root, '_deploy', site.hostname)
|
File.join(Rails.root, '_deploy', site.hostname)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Libera espacio eliminando archivos temporales
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def cleanup!
|
||||||
|
FileUtils.rm_rf(gems_dir)
|
||||||
|
FileUtils.rm_rf(yarn_cache_dir)
|
||||||
|
FileUtils.rm_rf(File.join(site.path, 'node_modules'))
|
||||||
|
FileUtils.rm_rf(File.join(site.path, '.sass-cache'))
|
||||||
|
FileUtils.rm_rf(File.join(site.path, '.jekyll-cache'))
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def mkdir
|
def mkdir
|
||||||
|
@ -52,27 +69,35 @@ class DeployLocal < Deploy
|
||||||
end
|
end
|
||||||
|
|
||||||
# Un entorno que solo tiene lo que necesitamos
|
# Un entorno que solo tiene lo que necesitamos
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
def env
|
def env
|
||||||
# XXX: This doesn't support Windows paths :B
|
# XXX: This doesn't support Windows paths :B
|
||||||
paths = [File.dirname(`which bundle`), '/usr/bin', '/bin']
|
paths = [File.dirname(`which bundle`), '/usr/local/bin', '/usr/bin', '/bin']
|
||||||
|
|
||||||
{
|
# Las variables de entorno extra no pueden superponerse al local.
|
||||||
'HOME' => home_dir,
|
extra_env.merge({
|
||||||
'PATH' => paths.join(':'),
|
'HOME' => home_dir,
|
||||||
'SPREE_API_KEY' => site.tienda_api_key,
|
'PATH' => paths.join(':'),
|
||||||
'SPREE_URL' => site.tienda_url,
|
'SPREE_API_KEY' => site.tienda_api_key,
|
||||||
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
'SPREE_URL' => site.tienda_url,
|
||||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
||||||
'JEKYLL_ENV' => Rails.env,
|
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||||
'LANG' => ENV['LANG'],
|
'JEKYLL_ENV' => Rails.env,
|
||||||
'YARN_CACHE_FOLDER' => yarn_cache_dir
|
'LANG' => ENV['LANG'],
|
||||||
}
|
'YARN_CACHE_FOLDER' => yarn_cache_dir,
|
||||||
|
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def yarn_cache_dir
|
def yarn_cache_dir
|
||||||
Rails.root.join('_yarn_cache').to_s
|
Rails.root.join('_yarn_cache').to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pnpm_cache_dir
|
||||||
|
Rails.root.join('_pnpm_cache').to_s
|
||||||
|
end
|
||||||
|
|
||||||
def yarn_lock
|
def yarn_lock
|
||||||
File.join(site.path, 'yarn.lock')
|
File.join(site.path, 'yarn.lock')
|
||||||
end
|
end
|
||||||
|
@ -81,28 +106,43 @@ class DeployLocal < Deploy
|
||||||
File.exist? yarn_lock
|
File.exist? yarn_lock
|
||||||
end
|
end
|
||||||
|
|
||||||
def gem
|
def pnpm_lock
|
||||||
run %(gem install bundler --no-document)
|
File.join(site.path, 'pnpm-lock.yaml')
|
||||||
|
end
|
||||||
|
|
||||||
|
def pnpm_lock?
|
||||||
|
File.exist? pnpm_lock
|
||||||
|
end
|
||||||
|
|
||||||
|
def git_lfs(output: false)
|
||||||
|
run %(git lfs fetch), output: output
|
||||||
|
run %(git lfs checkout), output: output
|
||||||
|
end
|
||||||
|
|
||||||
|
def gem(output: false)
|
||||||
|
run %(gem install bundler --no-document), output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
# Corre yarn dentro del repositorio
|
# Corre yarn dentro del repositorio
|
||||||
def yarn
|
def yarn(output: false)
|
||||||
return true unless yarn_lock?
|
return true unless yarn_lock?
|
||||||
|
|
||||||
run 'yarn install --production'
|
run 'yarn install --production', output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
def bundle
|
def pnpm(output: false)
|
||||||
run %(bundle install --no-cache --path="#{gems_dir}")
|
return true unless pnpm_lock?
|
||||||
|
|
||||||
# Eliminar archivos que no se usan
|
run %(pnpm config set store-dir "#{pnpm_cache_dir}"), output: output
|
||||||
Dir.glob("#{gems_dir}/ruby/2.7.0/{cache/*,gems/*/{spec,test,ext,vendor}}").each do |dir|
|
run 'pnpm install --production', output: output
|
||||||
FileUtils.rm_rf(dir)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def jekyll_build
|
def bundle(output: false)
|
||||||
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}")
|
run %(bundle install --deployment --no-cache --path="#{gems_dir}" --clean --without test development), output: output
|
||||||
|
end
|
||||||
|
|
||||||
|
def jekyll_build(output: false)
|
||||||
|
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}"), output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
# no debería haber espacios ni caracteres especiales, pero por si
|
# no debería haber espacios ni caracteres especiales, pero por si
|
||||||
|
@ -115,4 +155,18 @@ class DeployLocal < Deploy
|
||||||
def remove_destination!
|
def remove_destination!
|
||||||
FileUtils.rm_rf destination
|
FileUtils.rm_rf destination
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Consigue todas las variables de entorno configuradas por otros
|
||||||
|
# deploys.
|
||||||
|
#
|
||||||
|
# @deprecated Solo tenía sentido para Distributed Press v0
|
||||||
|
# @return [Hash]
|
||||||
|
def extra_env
|
||||||
|
@extra_env ||=
|
||||||
|
non_local_deploys.reduce({}) do |extra_env, deploy|
|
||||||
|
extra_env.tap do |e|
|
||||||
|
e.merge! deploy.local_env
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
12
app/models/deploy_localized_domain.rb
Normal file
12
app/models/deploy_localized_domain.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Soportar dominios localizados
|
||||||
|
class DeployLocalizedDomain < DeployAlternativeDomain
|
||||||
|
store :values, accessors: %i[hostname locale], coder: JSON
|
||||||
|
|
||||||
|
# Generar un link simbólico del sitio principal al alternativo
|
||||||
|
def deploy(**)
|
||||||
|
File.symlink?(destination) ||
|
||||||
|
File.symlink(File.join(site.hostname, locale), destination).zero?
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,9 +6,11 @@
|
||||||
# XXX: La plantilla tiene que soportar esto con el plugin
|
# XXX: La plantilla tiene que soportar esto con el plugin
|
||||||
# jekyll-private-data
|
# jekyll-private-data
|
||||||
class DeployPrivate < DeployLocal
|
class DeployPrivate < DeployLocal
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# No es necesario volver a instalar dependencias
|
# No es necesario volver a instalar dependencias
|
||||||
def deploy
|
def deploy(output: false)
|
||||||
jekyll_build
|
jekyll_build(output: output)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Hacer el deploy a un directorio privado
|
# Hacer el deploy a un directorio privado
|
||||||
|
@ -16,6 +18,10 @@ class DeployPrivate < DeployLocal
|
||||||
File.join(Rails.root, '_private', site.name)
|
File.join(Rails.root, '_private', site.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def url
|
||||||
|
"#{ENV['PANEL_URL']}/sites/private/#{site.name}"
|
||||||
|
end
|
||||||
|
|
||||||
# No usar recursos en compresión y habilitar los datos privados
|
# No usar recursos en compresión y habilitar los datos privados
|
||||||
def env
|
def env
|
||||||
@env ||= super.merge({
|
@env ||= super.merge({
|
||||||
|
|
38
app/models/deploy_reindex.rb
Normal file
38
app/models/deploy_reindex.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Reindexa los artículos al terminar la compilación
|
||||||
|
class DeployReindex < Deploy
|
||||||
|
def deploy(**)
|
||||||
|
time_start
|
||||||
|
|
||||||
|
site.reset
|
||||||
|
|
||||||
|
Site.transaction do
|
||||||
|
site.indexed_posts.destroy_all
|
||||||
|
site.index_posts!
|
||||||
|
end
|
||||||
|
|
||||||
|
time_stop
|
||||||
|
|
||||||
|
build_stats.create action: 'reindex',
|
||||||
|
log: 'Reindex',
|
||||||
|
seconds: time_spent_in_seconds,
|
||||||
|
bytes: size,
|
||||||
|
status: true
|
||||||
|
site.touch
|
||||||
|
end
|
||||||
|
|
||||||
|
def size
|
||||||
|
0
|
||||||
|
end
|
||||||
|
|
||||||
|
def limit
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def hostname; end
|
||||||
|
|
||||||
|
def url; end
|
||||||
|
|
||||||
|
def destination; end
|
||||||
|
end
|
|
@ -3,10 +3,12 @@
|
||||||
# Sincroniza sitios a servidores remotos usando Rsync. El servidor
|
# Sincroniza sitios a servidores remotos usando Rsync. El servidor
|
||||||
# remoto tiene que tener rsync instalado.
|
# remoto tiene que tener rsync instalado.
|
||||||
class DeployRsync < Deploy
|
class DeployRsync < Deploy
|
||||||
store :values, accessors: %i[destination host_keys], coder: JSON
|
store :values, accessors: %i[hostname destination host_keys], coder: JSON
|
||||||
|
|
||||||
def deploy
|
DEPENDENCIES = %i[deploy_local deploy_zip]
|
||||||
ssh? && rsync
|
|
||||||
|
def deploy(output: false)
|
||||||
|
ssh? && rsync(output: output)
|
||||||
end
|
end
|
||||||
|
|
||||||
# El espacio remoto es el mismo que el local
|
# El espacio remoto es el mismo que el local
|
||||||
|
@ -23,6 +25,11 @@ class DeployRsync < Deploy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def url
|
||||||
|
"https://#{hostname}/"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Verificar la conexión SSH implementando Trust On First Use
|
# Verificar la conexión SSH implementando Trust On First Use
|
||||||
|
@ -31,6 +38,7 @@ class DeployRsync < Deploy
|
||||||
#
|
#
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def ssh?
|
def ssh?
|
||||||
|
return true if destination.start_with? 'rsync://'
|
||||||
user, host = user_host
|
user, host = user_host
|
||||||
ssh_available = false
|
ssh_available = false
|
||||||
|
|
||||||
|
@ -83,8 +91,8 @@ class DeployRsync < Deploy
|
||||||
# Sincroniza hacia el directorio remoto
|
# Sincroniza hacia el directorio remoto
|
||||||
#
|
#
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def rsync
|
def rsync(output: false)
|
||||||
run %(rsync -aviH --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/)
|
run %(rsync -aviH --delete-after --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/), output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
# El origen es el destino de la compilación
|
# El origen es el destino de la compilación
|
||||||
|
|
|
@ -4,9 +4,13 @@
|
||||||
class DeployWww < Deploy
|
class DeployWww < Deploy
|
||||||
store :values, accessors: %i[], coder: JSON
|
store :values, accessors: %i[], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
before_destroy :remove_destination!
|
before_destroy :remove_destination!
|
||||||
|
|
||||||
def deploy
|
def deploy(output: false)
|
||||||
|
puts "Creando symlink #{site.hostname} => #{destination}" if output
|
||||||
|
|
||||||
File.symlink?(destination) ||
|
File.symlink?(destination) ||
|
||||||
File.symlink(site.hostname, destination).zero?
|
File.symlink(site.hostname, destination).zero?
|
||||||
end
|
end
|
||||||
|
@ -27,6 +31,10 @@ class DeployWww < Deploy
|
||||||
"www.#{site.hostname}"
|
"www.#{site.hostname}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def url
|
||||||
|
"https://#{fqdn}/"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def remove_destination!
|
def remove_destination!
|
||||||
|
|
|
@ -8,28 +8,49 @@ require 'zip'
|
||||||
class DeployZip < Deploy
|
class DeployZip < Deploy
|
||||||
store :values, accessors: %i[], coder: JSON
|
store :values, accessors: %i[], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# Una vez que el sitio está generado, tomar todos los archivos y
|
# Una vez que el sitio está generado, tomar todos los archivos y
|
||||||
# y generar un zip accesible públicamente.
|
# y generar un zip accesible públicamente.
|
||||||
#
|
#
|
||||||
# rubocop:disable Metrics/MethodLength
|
# rubocop:disable Metrics/MethodLength
|
||||||
def deploy
|
def deploy(output: false)
|
||||||
FileUtils.rm_f path
|
FileUtils.rm_f path
|
||||||
|
|
||||||
time_start
|
time_start
|
||||||
Dir.chdir(destination) do
|
Zip::File.open(path, Zip::File::CREATE) do |zip|
|
||||||
Zip::File.open(path, Zip::File::CREATE) do |z|
|
Dir.glob(File.join(destination, '**', '**')).each do |file|
|
||||||
Dir.glob('./**/**').each do |f|
|
entry = Pathname.new(file).relative_path_from(destination).to_s
|
||||||
File.directory?(f) ? z.mkdir(f) : z.add(f, f)
|
|
||||||
|
if File.directory? file
|
||||||
|
log "Creando directorio #{entry}", output
|
||||||
|
|
||||||
|
zip.mkdir(entry)
|
||||||
|
else
|
||||||
|
log "Comprimiendo #{entry}", output
|
||||||
|
zip.add(entry, file)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
time_stop
|
time_stop
|
||||||
|
|
||||||
build_stats.create action: 'zip',
|
File.exist?(path).tap do |status|
|
||||||
seconds: time_spent_in_seconds,
|
build_stats.create action: 'zip',
|
||||||
bytes: size
|
seconds: time_spent_in_seconds,
|
||||||
|
bytes: size,
|
||||||
|
log: @log.join("\n"),
|
||||||
|
status: status
|
||||||
|
end
|
||||||
|
rescue Zip::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
|
||||||
File.exist? path
|
build_stats.create action: 'zip',
|
||||||
|
seconds: 0,
|
||||||
|
bytes: 0,
|
||||||
|
log: @log.join("\n"),
|
||||||
|
status: false
|
||||||
|
|
||||||
|
false
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
|
@ -41,15 +62,33 @@ class DeployZip < Deploy
|
||||||
File.size path
|
File.size path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
def destination
|
def destination
|
||||||
File.join(Rails.root, '_deploy', site.hostname)
|
Rails.root.join('_deploy', site.hostname).realpath.to_s
|
||||||
|
rescue Errno::ENOENT
|
||||||
|
Rails.root.join('_deploy', site.hostname).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def file
|
def file
|
||||||
"#{site.hostname}.zip"
|
"#{site.hostname}.zip"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def url
|
||||||
|
"#{site.url}#{file}"
|
||||||
|
end
|
||||||
|
|
||||||
def path
|
def path
|
||||||
File.join(destination, file)
|
File.join(destination, file)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @param :line [String]
|
||||||
|
# @param :output [Boolean]
|
||||||
|
def log(line, output)
|
||||||
|
@log ||= []
|
||||||
|
@log << line
|
||||||
|
|
||||||
|
puts line if output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
84
app/models/distributed_press_publisher.rb
Normal file
84
app/models/distributed_press_publisher.rb
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'distributed_press/v1'
|
||||||
|
|
||||||
|
# Almacena el token de autenticación y la URL, por ahora solo vamos
|
||||||
|
# a tener uno, pero queda abierta la posibilidad de agregar más.
|
||||||
|
class DistributedPressPublisher < ApplicationRecord
|
||||||
|
# Cifrar la información del token en la base de datos
|
||||||
|
has_encrypted :token
|
||||||
|
|
||||||
|
# La salida del log
|
||||||
|
#
|
||||||
|
# @return [IO]
|
||||||
|
attr_reader :logger_out
|
||||||
|
|
||||||
|
# La instancia es única
|
||||||
|
validates_uniqueness_of :instance
|
||||||
|
|
||||||
|
# El token es necesario
|
||||||
|
validates_presence_of :token
|
||||||
|
|
||||||
|
# Mantener la fecha de vencimiento actualizada
|
||||||
|
before_save :update_expires_at_from_token!, :update_token_from_client!
|
||||||
|
|
||||||
|
# Devuelve todos los tokens que vencen en una hora
|
||||||
|
scope :with_about_to_expire_tokens, lambda {
|
||||||
|
where('expires_at > ? and expires_at < ?', Time.now, Time.now + 1.hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Instancia un cliente de Distributed Press a partir del token. Al
|
||||||
|
# cargar un token a punto de vencer se renueva automáticamente.
|
||||||
|
#
|
||||||
|
# @return [DistributedPress::V1::Client]
|
||||||
|
def client
|
||||||
|
@client ||= DistributedPress::V1::Client.new(url: instance, token: token, logger: logger)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def to_s
|
||||||
|
"Distributed Press <#{instance}>"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devuelve el hostname de la instancia
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def hostname
|
||||||
|
@hostname ||= URI.parse(instance).hostname
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Logger]
|
||||||
|
def logger
|
||||||
|
@logger ||=
|
||||||
|
begin
|
||||||
|
@logger_out, @logger_in = IO.pipe
|
||||||
|
::Logger.new @logger_in, formatter: formatter
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def formatter
|
||||||
|
@formatter ||= lambda do |_, _, _, msg|
|
||||||
|
"#{msg}\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Actualiza o desactiva la fecha de vencimiento a partir de la
|
||||||
|
# información del token.
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def update_expires_at_from_token!
|
||||||
|
self.expires_at = client.token.forever? ? nil : client.token.expires_at
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Actualiza el token a partir del cliente, que ya actualiza el token
|
||||||
|
# automáticamente.
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def update_token_from_client!
|
||||||
|
self.token = client.token.to_s
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,6 +9,13 @@ Layout = Struct.new(:site, :name, :meta, :metadata, keyword_init: true) do
|
||||||
name.to_s
|
name.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Obtiene todos los layouts (schemas) dependientes de este.
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def schemas
|
||||||
|
@schemas ||= site.layouts.to_h.slice(*site.schema_organization[name]).values
|
||||||
|
end
|
||||||
|
|
||||||
def attributes
|
def attributes
|
||||||
@attributes ||= metadata.keys.map(&:to_sym)
|
@attributes ||= metadata.keys.map(&:to_sym)
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ class Licencia < ApplicationRecord
|
||||||
translates :name, type: :string, locale_accessors: true
|
translates :name, type: :string, locale_accessors: true
|
||||||
translates :url, type: :string, locale_accessors: true
|
translates :url, type: :string, locale_accessors: true
|
||||||
translates :description, type: :text, locale_accessors: true
|
translates :description, type: :text, locale_accessors: true
|
||||||
|
translates :short_description, type: :string, locale_accessors: true
|
||||||
translates :deed, type: :text, locale_accessors: true
|
translates :deed, type: :text, locale_accessors: true
|
||||||
|
|
||||||
has_many :sites
|
has_many :sites
|
||||||
|
@ -14,5 +15,10 @@ class Licencia < ApplicationRecord
|
||||||
validates :name, presence: true, uniqueness: true
|
validates :name, presence: true, uniqueness: true
|
||||||
validates :url, presence: true
|
validates :url, presence: true
|
||||||
validates :description, presence: true
|
validates :description, presence: true
|
||||||
|
validates :short_description, presence: true
|
||||||
validates :deed, presence: true
|
validates :deed, presence: true
|
||||||
|
|
||||||
|
def custom?
|
||||||
|
icons == 'custom'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ class LogEntry < ApplicationRecord
|
||||||
def resend
|
def resend
|
||||||
return if sent
|
return if sent
|
||||||
|
|
||||||
ContactJob.perform_async site_id, params[:form], params
|
ContactJob.perform_later site_id, params[:form], params
|
||||||
end
|
end
|
||||||
|
|
||||||
def params
|
def params
|
||||||
|
|
|
@ -72,6 +72,17 @@ class MetadataContent < MetadataTemplate
|
||||||
resource['controls'] = true
|
resource['controls'] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Elimina los estilos salvo los que asigne el editor
|
||||||
|
html.css('*').each do |element|
|
||||||
|
next if elements_with_style.include? element.name.downcase
|
||||||
|
|
||||||
|
element.remove_attribute('style')
|
||||||
|
end
|
||||||
|
|
||||||
html.to_s.html_safe
|
html.to_s.html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def elements_with_style
|
||||||
|
@elements_with_style ||= %w[div mark].freeze
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,6 @@ class MetadataFile < MetadataTemplate
|
||||||
|
|
||||||
errors << I18n.t("metadata.#{type}.site_invalid") if site.invalid?
|
errors << I18n.t("metadata.#{type}.site_invalid") if site.invalid?
|
||||||
errors << I18n.t("metadata.#{type}.path_required") if path_missing?
|
errors << I18n.t("metadata.#{type}.path_required") if path_missing?
|
||||||
errors << I18n.t("metadata.#{type}.no_file_for_description") if no_file_for_description?
|
|
||||||
errors << I18n.t("metadata.#{type}.attachment_missing") if path? && !static_file
|
errors << I18n.t("metadata.#{type}.attachment_missing") if path? && !static_file
|
||||||
|
|
||||||
errors.compact!
|
errors.compact!
|
||||||
|
@ -41,14 +40,14 @@ class MetadataFile < MetadataTemplate
|
||||||
end
|
end
|
||||||
|
|
||||||
# Asociar la imagen subida al sitio y obtener la ruta
|
# Asociar la imagen subida al sitio y obtener la ruta
|
||||||
#
|
# @return [Boolean]
|
||||||
# XXX: Si evitamos guardar cambios con changed? no tenemos forma de
|
|
||||||
# saber que un archivo subido manualmente se convirtió en
|
|
||||||
# un Attachment y cada vez que lo editemos vamos a subir una imagen
|
|
||||||
# repetida.
|
|
||||||
def save
|
def save
|
||||||
value['description'] = sanitize value['description']
|
if value['path'].blank?
|
||||||
value['path'] = static_file ? relative_destination_path_with_filename.to_s : nil
|
self[:value] = default_value
|
||||||
|
else
|
||||||
|
value['description'] = sanitize value['description']
|
||||||
|
value['path'] = relative_destination_path_with_filename.to_s if static_file
|
||||||
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -62,9 +61,6 @@ class MetadataFile < MetadataTemplate
|
||||||
# * El archivo es una ruta que apunta a un archivo asociado al sitio
|
# * El archivo es una ruta que apunta a un archivo asociado al sitio
|
||||||
# * El archivo es una ruta a un archivo dentro del repositorio
|
# * El archivo es una ruta a un archivo dentro del repositorio
|
||||||
#
|
#
|
||||||
# XXX: La última opción provoca archivos duplicados, pero es lo mejor
|
|
||||||
# que tenemos hasta que resolvamos https://0xacab.org/sutty/sutty/-/issues/213
|
|
||||||
#
|
|
||||||
# @todo encontrar una forma de obtener el attachment sin tener que
|
# @todo encontrar una forma de obtener el attachment sin tener que
|
||||||
# recurrir al último subido.
|
# recurrir al último subido.
|
||||||
#
|
#
|
||||||
|
@ -75,13 +71,7 @@ class MetadataFile < MetadataTemplate
|
||||||
when ActionDispatch::Http::UploadedFile
|
when ActionDispatch::Http::UploadedFile
|
||||||
site.static_files.last if site.static_files.attach(value['path'])
|
site.static_files.last if site.static_files.attach(value['path'])
|
||||||
when String
|
when String
|
||||||
if (blob_id = ActiveStorage::Blob.where(key: key_from_path).pluck(:id).first)
|
site.static_files.find_by(blob_id: blob_id) || migrate_static_file!
|
||||||
site.static_files.find_by(blob_id: blob_id)
|
|
||||||
elsif path? && pathname.exist? && site.static_files.attach(io: pathname.open, filename: pathname.basename)
|
|
||||||
site.static_files.last.tap do |s|
|
|
||||||
s.blob.update(key: key_from_path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -98,7 +88,7 @@ class MetadataFile < MetadataTemplate
|
||||||
#
|
#
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def key_from_path
|
def key_from_path
|
||||||
pathname.dirname.basename.to_s
|
@key_from_path ||= pathname.dirname.basename.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def path?
|
def path?
|
||||||
|
@ -127,13 +117,22 @@ class MetadataFile < MetadataTemplate
|
||||||
# devolvemos la ruta original, que puede ser el archivo que no existe
|
# devolvemos la ruta original, que puede ser el archivo que no existe
|
||||||
# o vacía si se está subiendo uno.
|
# o vacía si se está subiendo uno.
|
||||||
rescue Errno::ENOENT => e
|
rescue Errno::ENOENT => e
|
||||||
ExceptionNotifier.notify_exception(e)
|
ExceptionNotifier.notify_exception(e, data: { site: site.name, path: value['path'] })
|
||||||
|
|
||||||
value['path']
|
Pathname.new(File.join(site.path, value['path']))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Obtener la ruta relativa al sitio.
|
||||||
|
#
|
||||||
|
# Si algo falla, devolver la ruta original para no romper el archivo.
|
||||||
|
#
|
||||||
|
# @return [String, nil]
|
||||||
def relative_destination_path_with_filename
|
def relative_destination_path_with_filename
|
||||||
destination_path_with_filename.relative_path_from(Pathname.new(site.path).realpath)
|
destination_path_with_filename.relative_path_from(Pathname.new(site.path).realpath)
|
||||||
|
rescue ArgumentError => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name, path: value['path'] })
|
||||||
|
|
||||||
|
value['path']
|
||||||
end
|
end
|
||||||
|
|
||||||
def static_file_path
|
def static_file_path
|
||||||
|
@ -145,8 +144,31 @@ class MetadataFile < MetadataTemplate
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# No hay archivo pero se lo describió
|
# Obtiene el id del blob asociado
|
||||||
def no_file_for_description?
|
#
|
||||||
!path? && description?
|
# @return [Integer,nil]
|
||||||
|
def blob_id
|
||||||
|
@blob_id ||= ActiveStorage::Blob.where(key: key_from_path, service_name: site.name).pluck(:id).first
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera el blob para un archivo que ya se encuentra en el
|
||||||
|
# repositorio y lo agrega a la base de datos.
|
||||||
|
#
|
||||||
|
# @return [ActiveStorage::Attachment]
|
||||||
|
def migrate_static_file!
|
||||||
|
raise ArgumentError, 'El archivo no existe' unless path? && pathname.exist?
|
||||||
|
|
||||||
|
Site.transaction do
|
||||||
|
blob =
|
||||||
|
ActiveStorage::Blob.create_after_unfurling!(key: key_from_path,
|
||||||
|
io: pathname.open,
|
||||||
|
filename: pathname.basename,
|
||||||
|
service_name: site.name)
|
||||||
|
|
||||||
|
ActiveStorage::Attachment.create!(name: 'static_files', record: site, blob: blob)
|
||||||
|
end
|
||||||
|
rescue ArgumentError => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name, path: value['path'] })
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,22 +1,49 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Los valores de este metadato son artículos en otros idiomas
|
# Los valores de este metadato son artículos en otros idiomas
|
||||||
class MetadataLocales < MetadataTemplate
|
class MetadataLocales < MetadataHasAndBelongsToMany
|
||||||
def default_value
|
|
||||||
super || []
|
|
||||||
end
|
|
||||||
|
|
||||||
# Todos los valores posibles para cada idioma disponible
|
# Todos los valores posibles para cada idioma disponible
|
||||||
#
|
#
|
||||||
# TODO: Optimizar?
|
|
||||||
# TODO: Mantener sincronizados
|
|
||||||
#
|
|
||||||
# @return { lang: { title: uuid } }
|
# @return { lang: { title: uuid } }
|
||||||
def values
|
def values
|
||||||
@values ||= site.locales.map do |locale|
|
@values ||= site.locales.map do |locale|
|
||||||
[locale, site.posts(lang: locale).map do |post|
|
[locale, posts.where(lang: locale).map do |post|
|
||||||
[post.title.value, post.uuid.value]
|
[title(post), post.uuid.value]
|
||||||
end.to_h]
|
end.to_h]
|
||||||
end.to_h
|
end.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Siempre hay una relación inversa
|
||||||
|
#
|
||||||
|
# @return [True]
|
||||||
|
def inverse?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# El campo inverso se llama igual en el otro post
|
||||||
|
#
|
||||||
|
# @return [Symbol]
|
||||||
|
def inverse
|
||||||
|
:locales
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Obtiene todos los locales distintos a este post
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def other_locales
|
||||||
|
site.locales.reject do |locale|
|
||||||
|
locale == post.lang.value.to_sym
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene todos los posts de los otros locales con el mismo layout
|
||||||
|
#
|
||||||
|
# @return [PostRelation]
|
||||||
|
def posts
|
||||||
|
other_locales.map do |locale|
|
||||||
|
site.posts(lang: locale).where(layout: post.layout.value)
|
||||||
|
end.reduce(&:concat) || PostRelation.new(site: site, lang: 'any')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
3
app/models/metadata_non_geo.rb
Normal file
3
app/models/metadata_non_geo.rb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MetadataNonGeo < MetadataGeo; end
|
25
app/models/metadata_password.rb
Normal file
25
app/models/metadata_password.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Almacena una contraseña
|
||||||
|
class MetadataPassword < MetadataString
|
||||||
|
# Las contraseñas no son indexables
|
||||||
|
#
|
||||||
|
# @return [boolean]
|
||||||
|
def indexable?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
alias_method :original_sanitize, :sanitize
|
||||||
|
|
||||||
|
# Sanitizar la string y generar un hash Bcrypt
|
||||||
|
#
|
||||||
|
# @param :string [String]
|
||||||
|
# @return [String]
|
||||||
|
def sanitize(string)
|
||||||
|
string = original_sanitize string
|
||||||
|
|
||||||
|
::BCrypt::Password.create(string).to_s
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,12 +2,6 @@
|
||||||
|
|
||||||
# Este metadato permite generar rutas manuales.
|
# Este metadato permite generar rutas manuales.
|
||||||
class MetadataPermalink < MetadataString
|
class MetadataPermalink < MetadataString
|
||||||
# El valor por defecto una vez creado es la URL que le asigne Jekyll,
|
|
||||||
# de forma que nunca cambia aunque se cambie el título.
|
|
||||||
def default_value
|
|
||||||
document.url.sub(%r{\A/}, '') unless post.new?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Los permalinks nunca pueden ser privados
|
# Los permalinks nunca pueden ser privados
|
||||||
def private?
|
def private?
|
||||||
false
|
false
|
||||||
|
|
|
@ -25,7 +25,7 @@ require 'jekyll/utils'
|
||||||
class MetadataSlug < MetadataTemplate
|
class MetadataSlug < MetadataTemplate
|
||||||
# Trae el slug desde el título si existe o una string al azar
|
# Trae el slug desde el título si existe o una string al azar
|
||||||
def default_value
|
def default_value
|
||||||
title ? Jekyll::Utils.slugify(title) : SecureRandom.uuid
|
title ? Jekyll::Utils.slugify(title, mode: site.slugify_mode) : SecureRandom.uuid
|
||||||
end
|
end
|
||||||
|
|
||||||
def value
|
def value
|
||||||
|
@ -39,6 +39,6 @@ class MetadataSlug < MetadataTemplate
|
||||||
return if post.title&.private?
|
return if post.title&.private?
|
||||||
return if post.title&.value&.blank?
|
return if post.title&.value&.blank?
|
||||||
|
|
||||||
post.title&.value&.to_s
|
post.title&.value&.to_s&.unicode_normalize
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -134,7 +134,11 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
# En caso de que algún campo necesite realizar acciones antes de ser
|
# En caso de que algún campo necesite realizar acciones antes de ser
|
||||||
# guardado
|
# guardado
|
||||||
def save
|
def save
|
||||||
return true unless changed?
|
if !changed?
|
||||||
|
self[:value] = document_value if private?
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
self[:value] = sanitize value
|
self[:value] = sanitize value
|
||||||
self[:value] = encrypt(value) if private?
|
self[:value] = encrypt(value) if private?
|
||||||
|
|
|
@ -29,7 +29,7 @@ class Post
|
||||||
# TODO: Reemplazar cuando leamos el contenido del Document
|
# TODO: Reemplazar cuando leamos el contenido del Document
|
||||||
# a demanda?
|
# a demanda?
|
||||||
def find_layout(path)
|
def find_layout(path)
|
||||||
IO.foreach(path).lazy.grep(/^layout: /).take(1).first&.split(' ')&.last&.tr('\'', '')&.tr('"', '')&.to_sym
|
File.foreach(path).lazy.grep(/^layout: /).take(1).first&.split(' ')&.last&.tr('\'', '')&.tr('"', '')&.to_sym
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -90,16 +90,21 @@ class Post
|
||||||
'page' => document.to_liquid
|
'page' => document.to_liquid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# No tener errores de Liquid
|
||||||
|
site.jekyll.config['liquid']['strict_filters'] = false
|
||||||
|
site.jekyll.config['liquid']['strict_variables'] = false
|
||||||
|
|
||||||
# Renderizar lo estrictamente necesario y convertir a HTML para
|
# Renderizar lo estrictamente necesario y convertir a HTML para
|
||||||
# poder reemplazar valores.
|
# poder reemplazar valores.
|
||||||
html = Nokogiri::HTML document.renderer.render_document
|
html = Nokogiri::HTML document.renderer.render_document
|
||||||
# Las imágenes se cargan directamente desde el repositorio, porque
|
# Los archivos se cargan directamente desde el repositorio, porque
|
||||||
# no son públicas hasta que se publica el artículo.
|
# no son públicas hasta que se publica el artículo.
|
||||||
html.css('img').each do |img|
|
html.css('img,audio,video,iframe').each do |element|
|
||||||
next if %r{\Ahttps?://} =~ img.attributes['src']
|
src = element.attributes['src']
|
||||||
|
|
||||||
img.attributes['src'].value = Rails.application.routes.url_helpers.site_static_file_url(site,
|
next unless src&.value&.start_with? 'public/'
|
||||||
file: img.attributes['src'].value)
|
|
||||||
|
src.value = Rails.application.routes.url_helpers.site_static_file_url(site, file: src.value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Notificar a les usuaries que están viendo una previsualización
|
# Notificar a les usuaries que están viendo una previsualización
|
||||||
|
@ -108,12 +113,16 @@ class Post
|
||||||
|
|
||||||
# Cacofonía
|
# Cacofonía
|
||||||
html.to_html.html_safe
|
html.to_html.html_safe
|
||||||
|
rescue Liquid::Error => e
|
||||||
|
ExceptionNotifier.notify(e, data: { site: site.name, post: post.id })
|
||||||
|
|
||||||
|
''
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Devuelve una llave para poder guardar el post en una cache
|
# Devuelve una llave para poder guardar el post en una cache
|
||||||
def cache_key
|
def cache_key
|
||||||
'posts/' + uuid.value
|
"posts/#{uuid.value}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_version
|
def cache_version
|
||||||
|
@ -123,7 +132,7 @@ class Post
|
||||||
# Agregar el timestamp para saber si cambió, siguiendo el módulo
|
# Agregar el timestamp para saber si cambió, siguiendo el módulo
|
||||||
# ActiveRecord::Integration
|
# ActiveRecord::Integration
|
||||||
def cache_key_with_version
|
def cache_key_with_version
|
||||||
cache_key + '-' + cache_version
|
"#{cache_key}-#{cache_version}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Convertir a UUID?
|
# TODO: Convertir a UUID?
|
||||||
|
|
|
@ -14,9 +14,8 @@ class Post
|
||||||
#
|
#
|
||||||
# @return [IndexedPost]
|
# @return [IndexedPost]
|
||||||
def to_index
|
def to_index
|
||||||
IndexedPost.find_or_create_by(id: uuid.value).tap do |indexed_post|
|
IndexedPost.find_or_initialize_by(post_id: uuid.value, site_id: site.id).tap do |indexed_post|
|
||||||
indexed_post.layout = layout.name
|
indexed_post.layout = layout.name
|
||||||
indexed_post.site_id = site.id
|
|
||||||
indexed_post.path = path.basename
|
indexed_post.path = path.basename
|
||||||
indexed_post.locale = locale.value
|
indexed_post.locale = locale.value
|
||||||
indexed_post.dictionary = IndexedPost.to_dictionary(locale: locale.value)
|
indexed_post.dictionary = IndexedPost.to_dictionary(locale: locale.value)
|
||||||
|
@ -28,8 +27,6 @@ class Post
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Indexa o reindexa el Post
|
# Indexa o reindexa el Post
|
||||||
#
|
#
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
|
@ -41,6 +38,8 @@ class Post
|
||||||
to_index.destroy.destroyed?
|
to_index.destroy.destroyed?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
# Los metadatos que se almacenan como objetos JSON. Empezamos con
|
# Los metadatos que se almacenan como objetos JSON. Empezamos con
|
||||||
# las categorías porque se usan para filtrar en el listado de
|
# las categorías porque se usan para filtrar en el listado de
|
||||||
# artículos.
|
# artículos.
|
||||||
|
|
14
app/models/privacy_policy.rb
Normal file
14
app/models/privacy_policy.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Políticas de privacidad
|
||||||
|
class PrivacyPolicy < ApplicationRecord
|
||||||
|
extend Mobility
|
||||||
|
|
||||||
|
translates :title, type: :string, locale_accessors: true
|
||||||
|
translates :description, type: :text, locale_accessors: true
|
||||||
|
translates :content, type: :text, locale_accessors: true
|
||||||
|
|
||||||
|
validates :title, presence: true, uniqueness: true
|
||||||
|
validates :description, presence: true
|
||||||
|
validates :content, presence: true
|
||||||
|
end
|
|
@ -21,4 +21,8 @@ class Rol < ApplicationRecord
|
||||||
def usuarie?
|
def usuarie?
|
||||||
rol == USUARIE
|
rol == USUARIE
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.role?(rol)
|
||||||
|
ROLES.include? rol
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,9 @@ class Site < ApplicationRecord
|
||||||
include Site::Forms
|
include Site::Forms
|
||||||
include Site::FindAndReplace
|
include Site::FindAndReplace
|
||||||
include Site::Api
|
include Site::Api
|
||||||
|
include Site::DeployDependencies
|
||||||
|
include Site::BuildStats
|
||||||
|
include Site::LayoutOrdering
|
||||||
include Tienda
|
include Tienda
|
||||||
|
|
||||||
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
|
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
|
||||||
|
@ -17,7 +20,7 @@ class Site < ApplicationRecord
|
||||||
|
|
||||||
# TODO: Hacer que los diferentes tipos de deploy se auto registren
|
# TODO: Hacer que los diferentes tipos de deploy se auto registren
|
||||||
# @see app/services/site_service.rb
|
# @see app/services/site_service.rb
|
||||||
DEPLOYS = %i[local private www zip hidden_service].freeze
|
DEPLOYS = %i[local private www zip hidden_service distributed_press].freeze
|
||||||
|
|
||||||
validates :name, uniqueness: true, hostname: {
|
validates :name, uniqueness: true, hostname: {
|
||||||
allow_root_label: true
|
allow_root_label: true
|
||||||
|
@ -179,10 +182,20 @@ class Site < ApplicationRecord
|
||||||
# Siempre tiene que tener algo porque las traducciones están
|
# Siempre tiene que tener algo porque las traducciones están
|
||||||
# incorporadas a los sitios de Sutty, aunque les usuaries no traduzcan
|
# incorporadas a los sitios de Sutty, aunque les usuaries no traduzcan
|
||||||
# sus sitios.
|
# sus sitios.
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
def locales
|
def locales
|
||||||
@locales ||= config.fetch('locales', I18n.available_locales).map(&:to_sym)
|
@locales ||= config.fetch('locales', I18n.available_locales).map(&:to_sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Modificar los locales disponibles
|
||||||
|
#
|
||||||
|
# @param :new_locales [Array]
|
||||||
|
# @return [Array]
|
||||||
|
def locales=(new_locales)
|
||||||
|
@locales = new_locales.map(&:to_sym).uniq
|
||||||
|
end
|
||||||
|
|
||||||
# Similar a site.i18n en jekyll-locales
|
# Similar a site.i18n en jekyll-locales
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
|
@ -250,6 +263,8 @@ class Site < ApplicationRecord
|
||||||
layout = layouts[Post.find_layout(doc.path)]
|
layout = layouts[Post.find_layout(doc.path)]
|
||||||
|
|
||||||
@posts[lang].build(document: doc, layout: layout, lang: lang)
|
@posts[lang].build(document: doc, layout: layout, lang: lang)
|
||||||
|
rescue TypeError => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: name, site_id: id, path: doc.path })
|
||||||
end
|
end
|
||||||
|
|
||||||
@posts[lang]
|
@posts[lang]
|
||||||
|
@ -425,7 +440,7 @@ class Site < ApplicationRecord
|
||||||
|
|
||||||
# El directorio donde se almacenan los sitios
|
# El directorio donde se almacenan los sitios
|
||||||
def self.site_path
|
def self.site_path
|
||||||
@site_path ||= ENV.fetch('SITE_PATH', Rails.root.join('_sites'))
|
@site_path ||= File.realpath(ENV.fetch('SITE_PATH', Rails.root.join('_sites')))
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.default
|
def self.default
|
||||||
|
@ -484,6 +499,7 @@ class Site < ApplicationRecord
|
||||||
config.title = title
|
config.title = title
|
||||||
config.url = url(slash: false)
|
config.url = url(slash: false)
|
||||||
config.hostname = hostname
|
config.hostname = hostname
|
||||||
|
config.locales = locales.map(&:to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
||||||
|
@ -540,10 +556,35 @@ class Site < ApplicationRecord
|
||||||
Dir.chdir path, &block
|
Dir.chdir path, &block
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Instala las gemas cuando es necesario:
|
||||||
|
#
|
||||||
|
# * El sitio existe
|
||||||
|
# * No están instaladas
|
||||||
|
# * El archivo Gemfile se modificó
|
||||||
|
# * El archivo Gemfile.lock se modificó
|
||||||
def install_gems
|
def install_gems
|
||||||
return unless persisted?
|
return unless persisted?
|
||||||
return if Rails.root.join('_storage', 'gems', name).directory?
|
|
||||||
|
|
||||||
deploys.find_by_type('DeployLocal').send(:bundle)
|
deploys.find_by_type('DeployLocal').send(:git_lfs)
|
||||||
|
|
||||||
|
if !gem_dir? || gemfile_updated? || gemfile_lock_updated?
|
||||||
|
deploys.find_by_type('DeployLocal').send(:bundle)
|
||||||
|
touch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el repositorio de gemas existe
|
||||||
|
def gem_dir?
|
||||||
|
Rails.root.join('_storage', 'gems', name).directory?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el Gemfile fue modificado
|
||||||
|
def gemfile_updated?
|
||||||
|
updated_at < File.mtime(File.join(path, 'Gemfile'))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el Gemfile.lock fue modificado
|
||||||
|
def gemfile_lock_updated?
|
||||||
|
updated_at < File.mtime(File.join(path, 'Gemfile.lock'))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
111
app/models/site/build_stats.rb
Normal file
111
app/models/site/build_stats.rb
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Site
|
||||||
|
module BuildStats
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Devuelve el tiempo promedio de publicación para este sitio
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
def average_publication_time
|
||||||
|
build_stats.group(:action).average(:seconds).values.reduce(:+).round
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devuelve el tiempo promedio de compilación para sitios similares
|
||||||
|
# a este.
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
def average_publication_time_for_similar_sites
|
||||||
|
similar_deploys = Deploy.where(type: deploys.pluck(:type)).pluck(:id)
|
||||||
|
|
||||||
|
BuildStat.where(deploy_id: similar_deploys).group(:action).average(:seconds).values.reduce(:+).round
|
||||||
|
end
|
||||||
|
|
||||||
|
# Define si podemos calcular el tiempo promedio de publicación
|
||||||
|
# para este sitio
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def average_publication_time_calculable?
|
||||||
|
build_stats.jekyll.where(status: true).count > 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def similar_sites?
|
||||||
|
!design.no_theme?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el sitio todavía no ha sido publicado
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def not_published_yet?
|
||||||
|
build_stats.jekyll.where(status: true).count.zero?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cambios posibles luego de la última publicación exitosa:
|
||||||
|
#
|
||||||
|
# * Artículos modificados
|
||||||
|
# * Configuración modificada
|
||||||
|
# * Métodos de publicación añadidos
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def awaiting_publication?
|
||||||
|
waiting? && (post_pending? || deploy_pending? || configuration_pending?)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Se modificaron artículos después de publicar el sitio por última
|
||||||
|
# vez
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def post_pending?
|
||||||
|
last_indexed_post_time > last_publication_time
|
||||||
|
end
|
||||||
|
|
||||||
|
# Se modificó el sitio después de publicarlo por última vez
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def deploy_pending?
|
||||||
|
last_deploy_time > last_publication_time
|
||||||
|
end
|
||||||
|
|
||||||
|
# Se modificó la configuración del sitio
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def configuration_pending?
|
||||||
|
last_configuration_time > last_publication_time
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Encuentra la fecha del último artículo modificado. Si no hay
|
||||||
|
# ninguno, devuelve la fecha de modificación del sitio.
|
||||||
|
#
|
||||||
|
# @return [Time]
|
||||||
|
def last_indexed_post_time
|
||||||
|
indexed_posts.order(updated_at: :desc).select(:updated_at).first&.updated_at || updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra la fecha de última modificación de los métodos de
|
||||||
|
# publicación.
|
||||||
|
#
|
||||||
|
# @return [Time]
|
||||||
|
def last_deploy_time
|
||||||
|
deploys.order(created_at: :desc).select(:created_at).first&.created_at || updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra la fecha de última publicación exitosa, si no hay
|
||||||
|
# ninguno, devuelve la fecha de modificación del sitio.
|
||||||
|
#
|
||||||
|
# @return [Time]
|
||||||
|
def last_publication_time
|
||||||
|
build_stats.jekyll.where(status: true).order(created_at: :desc).select(:created_at).first&.created_at || updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fecha de última modificación de la configuración
|
||||||
|
#
|
||||||
|
# @return [Time]
|
||||||
|
def last_configuration_time
|
||||||
|
File.mtime(config.path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -31,12 +31,12 @@ class Site
|
||||||
|
|
||||||
# Escribe los cambios en el repositorio
|
# Escribe los cambios en el repositorio
|
||||||
def write
|
def write
|
||||||
return if persisted?
|
return true if persisted?
|
||||||
|
|
||||||
@saved = Site::Writer.new(site: site, file: path,
|
@saved = Site::Writer.new(site: site, file: path, content: content.to_yaml).save.tap do |result|
|
||||||
content: content.to_yaml).save
|
# Actualizar el hash para no escribir dos veces
|
||||||
# Actualizar el hash para no escribir dos veces
|
@hash = content.hash
|
||||||
@hash = content.hash
|
end
|
||||||
end
|
end
|
||||||
alias save write
|
alias save write
|
||||||
|
|
||||||
|
|
38
app/models/site/deploy_dependencies.rb
Normal file
38
app/models/site/deploy_dependencies.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rgl/adjacency'
|
||||||
|
require 'rgl/topsort'
|
||||||
|
|
||||||
|
class Site
|
||||||
|
module DeployDependencies
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Genera un grafo dirigido de todos los métodos de publicación
|
||||||
|
#
|
||||||
|
# @return [RGL::DirectedAdjacencyGraph]
|
||||||
|
def deployment_graph
|
||||||
|
@deployment_graph ||= RGL::DirectedAdjacencyGraph.new.tap do |graph|
|
||||||
|
deploys.each do |deploy|
|
||||||
|
graph.add_vertex deploy
|
||||||
|
end
|
||||||
|
|
||||||
|
deploys.each do |deploy|
|
||||||
|
deploy.class.all_dependencies.each do |dependency|
|
||||||
|
deploys.where(type: dependency.to_s.classify).each do |deploy_dependency|
|
||||||
|
graph.add_edge deploy_dependency, deploy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devuelve una lista ordenada de todos los métodos de publicación
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def deployment_list
|
||||||
|
@deployment_list ||= deployment_graph.topsort_iterator.to_a
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,9 +14,7 @@ class Site
|
||||||
|
|
||||||
def index_posts!
|
def index_posts!
|
||||||
Site.transaction do
|
Site.transaction do
|
||||||
docs.each do |post|
|
docs.each(&:index!)
|
||||||
post.to_index.save
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
38
app/models/site/layout_ordering.rb
Normal file
38
app/models/site/layout_ordering.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Site
|
||||||
|
# Obtiene un listado de layouts (schemas)
|
||||||
|
module LayoutOrdering
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
|
||||||
|
# Obtiene o genera un listado de layouts (schemas) con sus
|
||||||
|
# dependencias, para poder generar un árbol.
|
||||||
|
#
|
||||||
|
# Por defecto, si el sitio no lo soporta, se obtienen los layouts
|
||||||
|
# ordenados alfabéticamente por traducción.
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def schema_organization
|
||||||
|
@schema_organization ||=
|
||||||
|
begin
|
||||||
|
schema_organization = data.dig('schema', 'organization')
|
||||||
|
schema_organization&.symbolize_keys!
|
||||||
|
schema_organization&.transform_values! do |ary|
|
||||||
|
ary.map(&:to_sym)
|
||||||
|
end
|
||||||
|
|
||||||
|
schema_organization ||
|
||||||
|
begin
|
||||||
|
layouts = self.layouts.sort_by(&:humanized_name).map(&:name)
|
||||||
|
Hash[layouts.zip([].fill([], 0, layouts.size))]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Deprecar cuando renombremos layouts a schemas
|
||||||
|
alias layout_organization schema_organization
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -117,6 +117,9 @@ class Site
|
||||||
def commit(file:, usuarie:, message:, remove: false)
|
def commit(file:, usuarie:, message:, remove: false)
|
||||||
file = [file] unless file.respond_to? :each
|
file = [file] unless file.respond_to? :each
|
||||||
|
|
||||||
|
# Cargar el árbol actual
|
||||||
|
rugged.index.read_tree rugged.head.target.tree
|
||||||
|
|
||||||
file.each do |f|
|
file.each do |f|
|
||||||
remove ? rm(f) : add(f)
|
remove ? rm(f) : add(f)
|
||||||
end
|
end
|
||||||
|
@ -147,6 +150,23 @@ class Site
|
||||||
rugged.index.remove(relativize(file))
|
rugged.index.remove(relativize(file))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Garbage collection
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def gc
|
||||||
|
env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path }
|
||||||
|
cmd = 'git gc'
|
||||||
|
|
||||||
|
r = nil
|
||||||
|
Dir.chdir(path) do
|
||||||
|
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, _, t|
|
||||||
|
r = t.value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
r&.success?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las
|
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las
|
||||||
|
|
3
app/models/site_build_stat.rb
Normal file
3
app/models/site_build_stat.rb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
SiteBuildStat = Struct.new(:site)
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
# Usuarie de la plataforma
|
# Usuarie de la plataforma
|
||||||
class Usuarie < ApplicationRecord
|
class Usuarie < ApplicationRecord
|
||||||
|
include Usuarie::Consent
|
||||||
|
|
||||||
devise :invitable, :database_authenticatable,
|
devise :invitable, :database_authenticatable,
|
||||||
:recoverable, :rememberable, :validatable,
|
:recoverable, :rememberable, :validatable,
|
||||||
:confirmable, :lockable, :registerable
|
:confirmable, :lockable, :registerable
|
||||||
|
@ -9,6 +11,10 @@ class Usuarie < ApplicationRecord
|
||||||
validates_uniqueness_of :email
|
validates_uniqueness_of :email
|
||||||
validates_with EmailAddress::ActiveRecordValidator, field: :email
|
validates_with EmailAddress::ActiveRecordValidator, field: :email
|
||||||
|
|
||||||
|
before_create :lang_from_locale!
|
||||||
|
before_update :remove_confirmation_invitation_inconsistencies!
|
||||||
|
before_update :accept_invitation_after_confirmation!
|
||||||
|
|
||||||
has_many :roles
|
has_many :roles
|
||||||
has_many :sites, through: :roles
|
has_many :sites, through: :roles
|
||||||
has_many :blazer_audits, foreign_key: 'user_id', class_name: 'Blazer::Audit'
|
has_many :blazer_audits, foreign_key: 'user_id', class_name: 'Blazer::Audit'
|
||||||
|
@ -38,4 +44,38 @@ class Usuarie < ApplicationRecord
|
||||||
increment_failed_attempts
|
increment_failed_attempts
|
||||||
lock_access! if attempts_exceeded? && !access_locked?
|
lock_access! if attempts_exceeded? && !access_locked?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_devise_notification(notification, *args)
|
||||||
|
I18n.with_locale(lang) do
|
||||||
|
devise_mailer.send(notification, self, *args).deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Les usuaries necesitan link de invitación si no tenían cuenta
|
||||||
|
# y todavía no aceptaron la invitación anterior.
|
||||||
|
def needs_invitation_link?
|
||||||
|
created_by_invite? && !invitation_accepted?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def lang_from_locale!
|
||||||
|
self.lang = I18n.locale.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
# El invitation_token solo es necesario cuando fue creade por otre
|
||||||
|
# usuarie. De lo contrario lo que queremos es un proceso de
|
||||||
|
# confirmación.
|
||||||
|
def remove_confirmation_invitation_inconsistencies!
|
||||||
|
self.invitation_token = nil unless created_by_invite?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Si le usuarie (re)confirma su cuenta con una invitación pendiente,
|
||||||
|
# considerarla aceptada también.
|
||||||
|
def accept_invitation_after_confirmation!
|
||||||
|
if confirmed?
|
||||||
|
self.invitation_token = nil
|
||||||
|
self.invitation_accepted_at ||= Time.now.utc
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
16
app/policies/site_build_stat_policy.rb
Normal file
16
app/policies/site_build_stat_policy.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Quiénes pueden ver estados de compilación de un sitio
|
||||||
|
class SiteBuildStatPolicy
|
||||||
|
attr_reader :site_build_stat, :usuarie
|
||||||
|
|
||||||
|
def initialize(usuarie, site_build_stat)
|
||||||
|
@usuarie = usuarie
|
||||||
|
@site_build_stat = site_build_stat
|
||||||
|
end
|
||||||
|
|
||||||
|
# Todes les usuaries e invitades de este sitio
|
||||||
|
def index?
|
||||||
|
site_build_stat.site.usuarie?(usuarie) || site_build_stat.site.invitade?(usuarie)
|
||||||
|
end
|
||||||
|
end
|
47
app/services/cleanup_service.rb
Normal file
47
app/services/cleanup_service.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Realiza tareas de limpieza en todos los sitios, para optimizar y
|
||||||
|
# liberar espacio.
|
||||||
|
class CleanupService
|
||||||
|
# Días de antigüedad de los sitios
|
||||||
|
attr_reader :before
|
||||||
|
|
||||||
|
# @param :before [ActiveSupport::TimeWithZone] Cuánto tiempo lleva sin usarse un sitio.
|
||||||
|
def initialize(before: 30.days.ago)
|
||||||
|
@before = before
|
||||||
|
end
|
||||||
|
|
||||||
|
# Limpieza general
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def cleanup_everything!
|
||||||
|
cleanup_older_sites!
|
||||||
|
cleanup_newer_sites!
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra todos los sitios sin actualizar y realiza limpieza.
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def cleanup_older_sites!
|
||||||
|
Site.where('updated_at < ?', before).find_each do |site|
|
||||||
|
next unless File.directory? site.path
|
||||||
|
|
||||||
|
site.deploys.find_each(&:cleanup!)
|
||||||
|
|
||||||
|
site.repository.gc
|
||||||
|
site.touch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tareas para los sitios en uso
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def cleanup_newer_sites!
|
||||||
|
Site.where('updated_at >= ?', before).find_each do |site|
|
||||||
|
next unless File.directory? site.path
|
||||||
|
|
||||||
|
site.repository.gc
|
||||||
|
site.touch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
67
app/services/lfs_object_service.rb
Normal file
67
app/services/lfs_object_service.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Representa un objeto git LFS
|
||||||
|
class LfsObjectService
|
||||||
|
attr_reader :site, :blob
|
||||||
|
|
||||||
|
# @param :site [Site]
|
||||||
|
# @param :blob [ActiveStorage::Blob]
|
||||||
|
def initialize(site:, blob:)
|
||||||
|
@site = site
|
||||||
|
@blob = blob
|
||||||
|
end
|
||||||
|
|
||||||
|
def process
|
||||||
|
# Crear el directorio
|
||||||
|
FileUtils.mkdir_p(File.dirname(object_path))
|
||||||
|
|
||||||
|
# Mover el archivo
|
||||||
|
FileUtils.mv(path, object_path) unless File.exist? object_path
|
||||||
|
|
||||||
|
# Crear el pointer
|
||||||
|
Site::Writer.new(site: site, file: path, content: pointer).save
|
||||||
|
|
||||||
|
# Commitear el pointer
|
||||||
|
site.repository.commit(file: path, usuarie: author, message: File.basename(path))
|
||||||
|
|
||||||
|
# Eliminar el pointer
|
||||||
|
FileUtils.rm(path)
|
||||||
|
|
||||||
|
# Hacer link duro del objeto al archivo
|
||||||
|
FileUtils.ln(object_path, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def path
|
||||||
|
@path ||= blob.service.path_for(blob.key)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def digest
|
||||||
|
@digest ||= Digest::SHA256.file(path).hexdigest
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def object_path
|
||||||
|
@object_path ||= File.join(site.path, '.git', 'lfs', 'objects', digest[0..1], digest[2..3], digest)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Integer]
|
||||||
|
def size
|
||||||
|
@size ||= File.size(File.exist?(object_path) ? object_path : path)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def pointer
|
||||||
|
@pointer ||=
|
||||||
|
<<~POINTER
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:#{digest}
|
||||||
|
size #{size}
|
||||||
|
POINTER
|
||||||
|
end
|
||||||
|
|
||||||
|
def author
|
||||||
|
@author ||= GitAuthor.new email: "disk_service@#{Site.domain}", name: 'DiskService'
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,8 +12,14 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
post.usuaries << usuarie
|
post.usuaries << usuarie
|
||||||
params[:post][:draft] = true if site.invitade? usuarie
|
params[:post][:draft] = true if site.invitade? usuarie
|
||||||
|
|
||||||
|
params.require(:post).permit(:slug).tap do |p|
|
||||||
|
post.slug.value = p[:slug] if p[:slug].present?
|
||||||
|
end
|
||||||
|
|
||||||
commit(action: :created, file: update_related_posts) if post.update(post_params)
|
commit(action: :created, file: update_related_posts) if post.update(post_params)
|
||||||
|
|
||||||
|
update_site_license!
|
||||||
|
|
||||||
# Devolver el post aunque no se haya salvado para poder rescatar los
|
# Devolver el post aunque no se haya salvado para poder rescatar los
|
||||||
# errores
|
# errores
|
||||||
post
|
post
|
||||||
|
@ -40,6 +46,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
# relacionados.
|
# relacionados.
|
||||||
commit(action: :updated, file: update_related_posts) if post.update(post_params)
|
commit(action: :updated, file: update_related_posts) if post.update(post_params)
|
||||||
|
|
||||||
|
update_site_license!
|
||||||
|
|
||||||
# Devolver el post aunque no se haya salvado para poder rescatar los
|
# Devolver el post aunque no se haya salvado para poder rescatar los
|
||||||
# errores
|
# errores
|
||||||
post
|
post
|
||||||
|
@ -133,4 +141,12 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
p.path.absolute if p.save(validate: false)
|
p.path.absolute if p.save(validate: false)
|
||||||
end.compact << post.path.absolute
|
end.compact << post.path.absolute
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Si les usuaries modifican o crean una licencia, considerarla
|
||||||
|
# personalizada en el panel.
|
||||||
|
def update_site_license!
|
||||||
|
if site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom?
|
||||||
|
site.update licencia: Licencia.find_by_icons('custom')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,22 +3,39 @@
|
||||||
# Se encargar de guardar cambios en sitios
|
# Se encargar de guardar cambios en sitios
|
||||||
# TODO: Implementar rollback en la configuración
|
# TODO: Implementar rollback en la configuración
|
||||||
SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
|
def deploy
|
||||||
|
site.enqueue!
|
||||||
|
DeployJob.perform_later site.id
|
||||||
|
end
|
||||||
|
|
||||||
# Crea un sitio, agrega un rol nuevo y guarda los cambios a la
|
# Crea un sitio, agrega un rol nuevo y guarda los cambios a la
|
||||||
# configuración en el repositorio git
|
# configuración en el repositorio git
|
||||||
def create
|
def create
|
||||||
self.site = Site.new params
|
self.site = Site.new params
|
||||||
|
|
||||||
add_role temporal: false, rol: 'usuarie'
|
add_role temporal: false, rol: 'usuarie'
|
||||||
sync_nodes
|
site.deploys.build type: 'DeployLocal'
|
||||||
|
# Los sitios de testing no se sincronizan
|
||||||
|
sync_nodes unless site.name.end_with? '.testing'
|
||||||
|
|
||||||
|
I18n.with_locale(usuarie.lang.to_sym || I18n.default_locale) do
|
||||||
|
# No se puede llamar a site.config antes de save porque el sitio
|
||||||
|
# todavía no existe.
|
||||||
|
#
|
||||||
|
# TODO: hacer que el repositorio se cree cuando es necesario, para
|
||||||
|
# que no haya estados intermedios.
|
||||||
|
site.locales = [usuarie.lang] + I18n.available_locales
|
||||||
|
|
||||||
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
|
|
||||||
site.save &&
|
site.save &&
|
||||||
site.config.write &&
|
site.config.write &&
|
||||||
commit_config(action: :create)
|
commit_config(action: :create) &&
|
||||||
|
site.reset.nil? &&
|
||||||
|
add_licencias &&
|
||||||
|
add_code_of_conduct &&
|
||||||
|
add_privacy_policy &&
|
||||||
|
deploy
|
||||||
end
|
end
|
||||||
|
|
||||||
add_licencias
|
|
||||||
|
|
||||||
site
|
site
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -27,11 +44,11 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
|
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
|
||||||
site.update(params) &&
|
site.update(params) &&
|
||||||
site.config.write &&
|
site.config.write &&
|
||||||
commit_config(action: :update)
|
commit_config(action: :update) &&
|
||||||
|
site.reset.nil? &&
|
||||||
|
change_licencias
|
||||||
end
|
end
|
||||||
|
|
||||||
change_licencias
|
|
||||||
|
|
||||||
site
|
site
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,14 +65,11 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
# Agregar una dirección oculta de Tor al DeployHiddenService y a la
|
# Agregar una dirección oculta de Tor al DeployHiddenService y a la
|
||||||
# configuración del Site.
|
# configuración del Site.
|
||||||
def add_onion
|
def add_onion
|
||||||
onion = params[:onion].strip
|
onion = params[:onion]
|
||||||
deploy = DeployHiddenService.find_by(site: site)
|
deploy = params[:deploy]
|
||||||
|
|
||||||
return false unless !onion.blank? && deploy
|
return false unless !onion.blank? && deploy
|
||||||
|
|
||||||
deploy.values[:onion] = onion
|
|
||||||
deploy.save
|
|
||||||
|
|
||||||
site.config['onion-location'] = onion
|
site.config['onion-location'] = onion
|
||||||
site.config.write
|
site.config.write
|
||||||
|
|
||||||
|
@ -91,24 +105,28 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Crea la licencia del sitio para cada locale disponible en el sitio
|
# Crea la licencia del sitio para cada locale disponible en el sitio
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
def add_licencias
|
def add_licencias
|
||||||
site.locales.each do |locale|
|
return true unless site.layout? :license
|
||||||
next unless I18n.available_locales.include? locale
|
return true if site.licencia.custom?
|
||||||
|
|
||||||
Mobility.with_locale(locale) do
|
with_all_locales do |locale|
|
||||||
add_licencia lang: locale
|
add_licencia lang: locale
|
||||||
end
|
end.compact.map(&:valid?).all?
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Crea una licencia
|
||||||
|
#
|
||||||
|
# @return [Post]
|
||||||
def add_licencia(lang:)
|
def add_licencia(lang:)
|
||||||
params = ActionController::Parameters.new(
|
params = ActionController::Parameters.new(
|
||||||
post: {
|
post: {
|
||||||
|
layout: 'license',
|
||||||
|
slug: Jekyll::Utils.slugify(I18n.t('activerecord.models.licencia')),
|
||||||
lang: lang,
|
lang: lang,
|
||||||
title: site.licencia.name,
|
title: site.licencia.name,
|
||||||
description: I18n.t('sites.form.licencia.title'),
|
description: site.licencia.short_description,
|
||||||
author: %w[Sutty],
|
|
||||||
permalink: "#{I18n.t('activerecord.models.licencia').downcase}/",
|
|
||||||
content: CommonMarker.render_html(site.licencia.deed)
|
content: CommonMarker.render_html(site.licencia.deed)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -119,25 +137,27 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
# Encuentra la licencia a partir de su enlace permanente y le cambia
|
# Encuentra la licencia a partir de su enlace permanente y le cambia
|
||||||
# el contenido
|
# el contenido
|
||||||
#
|
#
|
||||||
# TODO: Crear un layout específico para licencias así es más certera
|
# @return [Boolean]
|
||||||
# la búsqueda.
|
|
||||||
def change_licencias
|
def change_licencias
|
||||||
site.locales.each do |locale|
|
return true unless site.layout? :license
|
||||||
next unless I18n.available_locales.include? locale
|
return true if site.licencia.custom?
|
||||||
|
|
||||||
Mobility.with_locale(locale) do
|
with_all_locales do |locale|
|
||||||
permalink = "#{I18n.t('activerecord.models.licencia').downcase}/"
|
post = site.posts(lang: locale).find_by(layout: 'license')
|
||||||
post = site.posts(lang: locale).find_by(permalink: permalink)
|
|
||||||
|
|
||||||
post ? change_licencia(post: post) : add_licencia(lang: locale)
|
change_licencia(post: post) if post
|
||||||
end
|
end.compact.map(&:valid?).all?
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Cambia una licencia
|
||||||
|
#
|
||||||
|
# @param :post [Post]
|
||||||
|
# @return [Post]
|
||||||
def change_licencia(post:)
|
def change_licencia(post:)
|
||||||
params = ActionController::Parameters.new(
|
params = ActionController::Parameters.new(
|
||||||
post: {
|
post: {
|
||||||
title: site.licencia.name,
|
title: site.licencia.name,
|
||||||
|
description: site.licencia.short_description,
|
||||||
content: CommonMarker.render_html(site.licencia.deed)
|
content: CommonMarker.render_html(site.licencia.deed)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -146,10 +166,69 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
params: params).update
|
params: params).update
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Agrega un código de conducta
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def add_code_of_conduct
|
||||||
|
return true unless site.layout?(:code_of_conduct) || site.layout?(:page)
|
||||||
|
|
||||||
|
# TODO: soportar más códigos de conducta
|
||||||
|
coc = CodeOfConduct.first
|
||||||
|
|
||||||
|
with_all_locales do |locale|
|
||||||
|
params = ActionController::Parameters.new(
|
||||||
|
post: {
|
||||||
|
layout: site.layout?(:code_of_conduct) ? 'code_of_conduct' : 'page',
|
||||||
|
lang: locale.to_s,
|
||||||
|
title: coc.title,
|
||||||
|
description: coc.description,
|
||||||
|
content: CommonMarker.render_html(coc.content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
PostService.new(site: site, usuarie: usuarie, params: params).create
|
||||||
|
end.compact.map(&:valid?).all?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Agrega política de privacidad
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def add_privacy_policy
|
||||||
|
return true unless site.layout?(:privacy_policy) || site.layout?(:page)
|
||||||
|
|
||||||
|
pp = PrivacyPolicy.first
|
||||||
|
|
||||||
|
with_all_locales do |locale|
|
||||||
|
params = ActionController::Parameters.new(
|
||||||
|
post: {
|
||||||
|
layout: site.layout?(:privacy_policy) ? 'privacy_policy' : 'page',
|
||||||
|
lang: locale.to_s,
|
||||||
|
title: pp.title,
|
||||||
|
description: pp.description,
|
||||||
|
content: CommonMarker.render_html(pp.content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
PostService.new(site: site, usuarie: usuarie, params: params).create
|
||||||
|
end.compact.map(&:valid?).all?
|
||||||
|
end
|
||||||
|
|
||||||
# Crea los deploys necesarios para sincronizar a otros nodos de Sutty
|
# Crea los deploys necesarios para sincronizar a otros nodos de Sutty
|
||||||
def sync_nodes
|
def sync_nodes
|
||||||
Rails.application.nodes.each do |node|
|
Rails.application.nodes.each do |node|
|
||||||
site.deploys.build(type: 'DeployRsync', destination: "sutty@#{node}:#{site.hostname}")
|
site.deploys.build(type: 'DeployFullRsync', destination: "rsync://rsyncd.#{node}/deploys/", hostname: node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def with_all_locales(&block)
|
||||||
|
site.locales.map do |locale|
|
||||||
|
next unless I18n.available_locales.include? locale
|
||||||
|
|
||||||
|
Mobility.with_locale(locale) do
|
||||||
|
yield locale
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
2
app/views/bootstrap/_alert.haml
Normal file
2
app/views/bootstrap/_alert.haml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.alert.alert-primary.mx-auto.content.max-w-md-70ch{ role: 'alert', class: local_assigns[:class] }
|
||||||
|
= yield
|
6
app/views/bootstrap/_custom_checkbox.haml
Normal file
6
app/views/bootstrap/_custom_checkbox.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
- help_id = "#{id}_help"
|
||||||
|
|
||||||
|
.custom-control.custom-checkbox
|
||||||
|
%input.custom-control-input{ id: id, type: 'checkbox', name: name, value: value, required: required }
|
||||||
|
%label.custom-control-label{ for: id, aria: { describedby: help_id } }= content
|
||||||
|
%small.form-text.text-muted{ id: help_id }= yield
|
20
app/views/build_stats/index.haml
Normal file
20
app/views/build_stats/index.haml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
%main.row
|
||||||
|
%aside.menu.col-md-3
|
||||||
|
= render 'sites/header', site: @site
|
||||||
|
.col
|
||||||
|
%h1= t('.title')
|
||||||
|
|
||||||
|
%table.table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
- @headers.each do |header|
|
||||||
|
%th{ scope: 'col' }= header
|
||||||
|
%tbody
|
||||||
|
- @table.each do |row|
|
||||||
|
- row[:urls].each do |url|
|
||||||
|
%tr
|
||||||
|
%th{ scope: 'row' }= row[:title]
|
||||||
|
%td= link_to_if url.present?, url, url, class: 'word-break-all'
|
||||||
|
%td
|
||||||
|
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
|
||||||
|
%td= row[:size]
|
|
@ -11,7 +11,6 @@
|
||||||
url: site_collaborate_path(@site),
|
url: site_collaborate_path(@site),
|
||||||
method: :post) do |f|
|
method: :post) do |f|
|
||||||
- unless current_usuarie
|
- unless current_usuarie
|
||||||
= render 'layouts/flash'
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :email
|
= f.label :email
|
||||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
%h1= t('.hi')
|
%h1= @hi
|
||||||
|
|
||||||
= sanitize_markdown t('.explanation', fqdn: @deploy_local.site.hostname),
|
= sanitize_markdown @explanation, tags: %w[p a strong em]
|
||||||
tags: %w[p a strong em]
|
|
||||||
|
|
||||||
%table
|
%table
|
||||||
%thead
|
%thead
|
||||||
%tr
|
%tr
|
||||||
%th= t('.th.type')
|
- @headers.each do |header|
|
||||||
%th= t('.th.status')
|
%th= header
|
||||||
%tbody
|
%tbody
|
||||||
- @deploys.each do |deploy, value|
|
- @table.each do |row|
|
||||||
%tr
|
- row[:urls].each do |url|
|
||||||
%td= t(".#{deploy}.title")
|
%tr
|
||||||
%td= value ? t(".#{deploy}.success") : t(".#{deploy}.error")
|
%td= row[:title]
|
||||||
|
%td= row[:status]
|
||||||
|
%td= link_to_if url.present?, url, url
|
||||||
|
%td
|
||||||
|
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
|
||||||
|
%td= row[:size]
|
||||||
|
|
||||||
= sanitize_markdown t('.help'), tags: %w[p a strong em]
|
= sanitize_markdown @help, tags: %w[p a strong em]
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
= '# ' + t('.hi')
|
= "# #{@hi}"
|
||||||
\
|
\
|
||||||
= t('.explanation', fqdn: @deploy_local.site.hostname)
|
= @explanation
|
||||||
\
|
\
|
||||||
= Terminal::Table.new do |table|
|
= @terminal_table
|
||||||
- table << [t('.th.type'), t('.th.status')]
|
|
||||||
- table.add_separator
|
|
||||||
- @deploys.each do |deploy, value|
|
|
||||||
- table << [t(".#{deploy}.title"),
|
|
||||||
value ? t(".#{deploy}.success") : t(".#{deploy}.error")]
|
|
||||||
\
|
\
|
||||||
= t('.help')
|
= @help
|
||||||
|
|
21
app/views/deploys/_deploy_distributed_press.haml
Normal file
21
app/views/deploys/_deploy_distributed_press.haml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
-# Publicar a la web distribuida
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col
|
||||||
|
= deploy.hidden_field :id
|
||||||
|
= deploy.hidden_field :type
|
||||||
|
.custom-control.custom-switch
|
||||||
|
-#
|
||||||
|
El checkbox invierte la lógica de destrucción porque queremos
|
||||||
|
crear el deploy si está activado y destruirlo si está
|
||||||
|
desactivado.
|
||||||
|
= deploy.check_box :_destroy,
|
||||||
|
{ checked: deploy.object.persisted?, class: 'custom-control-input' },
|
||||||
|
'0', '1'
|
||||||
|
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||||
|
%h3= t('.title')
|
||||||
|
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
||||||
|
tags: %w[p strong em a]
|
||||||
|
|
||||||
|
|
||||||
|
%hr/
|
1
app/views/deploys/_deploy_full_rsync.haml
Normal file
1
app/views/deploys/_deploy_full_rsync.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
-# nada
|
|
@ -17,7 +17,8 @@
|
||||||
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
|
|
||||||
- if deploy.object.fqdn
|
- begin
|
||||||
= sanitize_markdown t('.help_2', url: deploy.object.url),
|
= sanitize_markdown t('.help_2', url: deploy.object.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
|
- rescue ArgumentError
|
||||||
%hr/
|
%hr/
|
||||||
|
|
1
app/views/deploys/_deploy_localized_domain.haml
Normal file
1
app/views/deploys/_deploy_localized_domain.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
-# nada
|
1
app/views/deploys/_deploy_reindex.haml
Normal file
1
app/views/deploys/_deploy_reindex.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
-# NADA
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-4.align-self-center
|
.col-md-4.align-self-center
|
||||||
.sr-only
|
.sr-only
|
||||||
|
@ -11,8 +13,6 @@
|
||||||
url: confirmation_path(resource_name),
|
url: confirmation_path(resource_name),
|
||||||
html: { method: :post }) do |f|
|
html: { method: :post }) do |f|
|
||||||
|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
|
|
||||||
:ruby
|
:ruby
|
||||||
value = if resource.pending_reconfirmation?
|
value = if resource.pending_reconfirmation?
|
||||||
resource.unconfirmed_email
|
resource.unconfirmed_email
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
%h2= t 'devise.invitations.edit.header'
|
%h2= t 'devise.invitations.edit.header'
|
||||||
|
@ -8,7 +10,6 @@
|
||||||
as: resource_name,
|
as: resource_name,
|
||||||
url: invitation_path(resource_name),
|
url: invitation_path(resource_name),
|
||||||
html: { method: :put }) do |f|
|
html: { method: :put }) do |f|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
= f.hidden_field :invitation_token, readonly: true
|
= f.hidden_field :invitation_token, readonly: true
|
||||||
- if f.object.class.require_password_on_accepting
|
- if f.object.class.require_password_on_accepting
|
||||||
.form-group
|
.form-group
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
%h2= t 'devise.invitations.new.header'
|
%h2= t 'devise.invitations.new.header'
|
||||||
|
@ -8,7 +10,6 @@
|
||||||
as: resource_name,
|
as: resource_name,
|
||||||
url: invitation_path(resource_name),
|
url: invitation_path(resource_name),
|
||||||
html: { method: :post }) do |f|
|
html: { method: :post }) do |f|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
- resource.class.invite_key_fields.each do |field|
|
- resource.class.invite_key_fields.each do |field|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label field
|
= f.label field
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
%p= t('.greeting', recipient: @email)
|
%p= t('.greeting', recipient: @email)
|
||||||
%p= t('.instruction')
|
%p= t('.instruction')
|
||||||
%p= link_to t('.action'), confirmation_url(@resource, confirmation_token: @token)
|
%p= link_to t('.action'), confirmation_url(@resource, confirmation_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
\
|
\
|
||||||
= t('.instruction')
|
= t('.instruction')
|
||||||
\
|
\
|
||||||
= confirmation_url(@resource, confirmation_token: @token)
|
= confirmation_url(@resource, confirmation_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- site = @resource.sites.last
|
- site = @resource.roles.where(temporal: true).last&.site
|
||||||
|
|
||||||
%p= t('devise.mailer.invitation_instructions.hello',
|
%p= t('devise.mailer.invitation_instructions.hello',
|
||||||
email: @resource.email)
|
email: @resource.email)
|
||||||
|
@ -8,12 +8,17 @@
|
||||||
%h1= site.title
|
%h1= site.title
|
||||||
%p= site.description
|
%p= site.description
|
||||||
|
|
||||||
%p= link_to t('devise.mailer.invitation_instructions.accept'),
|
- if @resource.needs_invitation_link?
|
||||||
accept_invitation_url(@resource, invitation_token: @token)
|
%p= link_to t('devise.mailer.invitation_instructions.accept'),
|
||||||
|
accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
||||||
- if @resource.invitation_due_at
|
- if @resource.invitation_due_at
|
||||||
%p= t('devise.mailer.invitation_instructions.accept_until',
|
%p= t('devise.mailer.invitation_instructions.accept_until',
|
||||||
due_date: l(@resource.invitation_due_at,
|
due_date: l(@resource.invitation_due_at,
|
||||||
format: :'devise.mailer.invitation_instructions.accept_until_format'))
|
format: :'devise.mailer.invitation_instructions.accept_until_format'))
|
||||||
|
|
||||||
%p= t('devise.mailer.invitation_instructions.ignore')
|
%p= t('devise.mailer.invitation_instructions.ignore')
|
||||||
|
- elsif !@resource.confirmed? && @resource.confirmation_token
|
||||||
|
= confirmation_url(@resource, confirmation_token: @resource.confirmation_token, change_locale_to: @resource.lang)
|
||||||
|
- else
|
||||||
|
%p= link_to t('devise.mailer.invitation_instructions.sign_in'), root_url
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- site = @resource.sites.last
|
- site = @resource.roles.where(temporal: true).last&.site
|
||||||
|
|
||||||
= t('devise.mailer.invitation_instructions.hello', email: @resource.email)
|
= t('devise.mailer.invitation_instructions.hello', email: @resource.email)
|
||||||
\
|
\
|
||||||
|
@ -9,11 +9,17 @@
|
||||||
\
|
\
|
||||||
= site.description
|
= site.description
|
||||||
\
|
\
|
||||||
= accept_invitation_url(@resource, invitation_token: @token)
|
- if @resource.needs_invitation_link?
|
||||||
\
|
= accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang)
|
||||||
- if @resource.invitation_due_at
|
\
|
||||||
= t('devise.mailer.invitation_instructions.accept_until',
|
- if @resource.invitation_due_at
|
||||||
due_date: l(@resource.invitation_due_at,
|
= t('devise.mailer.invitation_instructions.accept_until',
|
||||||
format: :'devise.mailer.invitation_instructions.accept_until_format'))
|
due_date: l(@resource.invitation_due_at,
|
||||||
\
|
format: :'devise.mailer.invitation_instructions.accept_until_format'))
|
||||||
= t('devise.mailer.invitation_instructions.ignore')
|
\
|
||||||
|
= t('devise.mailer.invitation_instructions.ignore')
|
||||||
|
- elsif !@resource.confirmed? && @resource.confirmation_token
|
||||||
|
= confirmation_url(@resource, confirmation_token: @resource.confirmation_token, change_locale_to: @resource.lang)
|
||||||
|
- else
|
||||||
|
= root_url(change_locale_to: @resource.lang)
|
||||||
|
= t('devise.mailer.invitation_instructions.sign_in')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
%p= t('.greeting', recipient: @resource.email)
|
%p= t('.greeting', recipient: @resource.email)
|
||||||
%p= t('.instruction')
|
%p= t('.instruction')
|
||||||
%p= link_to t('.action'), edit_password_url(@resource, reset_password_token: @token)
|
%p= link_to t('.action'), edit_password_url(@resource, reset_password_token: @token, change_locale_to: @resource.lang)
|
||||||
%p= t('.instruction_2')
|
%p= t('.instruction_2')
|
||||||
%p= t('.instruction_3')
|
%p= t('.instruction_3')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
\
|
\
|
||||||
= t('.instruction')
|
= t('.instruction')
|
||||||
\
|
\
|
||||||
= edit_password_url(@resource, reset_password_token: @token)
|
= edit_password_url(@resource, reset_password_token: @token, change_locale_to: @resource.lang)
|
||||||
\
|
\
|
||||||
= t('.instruction_2')
|
= t('.instruction_2')
|
||||||
\
|
\
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
%p= t('.greeting', recipient: @resource.email)
|
%p= t('.greeting', recipient: @resource.email)
|
||||||
%p= t('.message')
|
%p= t('.message')
|
||||||
%p= t('.instruction')
|
%p= t('.instruction')
|
||||||
%p= link_to t('.action'), unlock_url(@resource, unlock_token: @token)
|
%p= link_to t('.action'), unlock_url(@resource, unlock_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
\
|
\
|
||||||
= t('.instruction')
|
= t('.instruction')
|
||||||
\
|
\
|
||||||
= unlock_url(@resource, unlock_token: @token)
|
= unlock_url(@resource, unlock_token: @token, change_locale_to: @resource.lang)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
.sr-only
|
.sr-only
|
||||||
|
@ -10,7 +12,6 @@
|
||||||
= form_for(resource, as: resource_name,
|
= form_for(resource, as: resource_name,
|
||||||
url: password_path(resource_name),
|
url: password_path(resource_name),
|
||||||
html: { method: :put }) do |f|
|
html: { method: :put }) do |f|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
|
|
||||||
= f.hidden_field :reset_password_token
|
= f.hidden_field :reset_password_token
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
.sr-only
|
.sr-only
|
||||||
|
@ -11,7 +13,6 @@
|
||||||
as: resource_name,
|
as: resource_name,
|
||||||
url: password_path(resource_name),
|
url: password_path(resource_name),
|
||||||
html: { method: :post }) do |f|
|
html: { method: :post }) do |f|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :email, class: 'sr-only'
|
= f.label :email, class: 'sr-only'
|
||||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue