5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-15 21:51:43 +00:00

Merge branch 'rails' into issue-12994
All checks were successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful

This commit is contained in:
f 2024-01-02 15:00:47 -03:00
commit 58ce9a45d8
No known key found for this signature in database
224 changed files with 4467 additions and 1095 deletions

11
.editorconfig Normal file
View file

@ -0,0 +1,11 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
public/assets/** filter=lfs diff=lfs merge=lfs -text
public/packs/** filter=lfs diff=lfs merge=lfs -text

7
.gitignore vendored
View file

@ -34,12 +34,7 @@
/config/master.key /config/master.key
/config/credentials.yml.enc /config/credentials.yml.enc
/public/packs
/public/packs-test /public/packs-test
/public/assets
/public/assets-production
/public/packs
/public/packs-production
/node_modules /node_modules
/yarn-error.log /yarn-error.log
yarn-debug.log* yarn-debug.log*
@ -49,8 +44,6 @@ yarn-debug.log*
*.key *.key
*.crt *.crt
/public/packs
/public/packs-test
/node_modules /node_modules
/yarn-error.log /yarn-error.log
yarn-debug.log* yarn-debug.log*

33
.gitlab-ci.yml Normal file
View 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"

View file

@ -6,7 +6,7 @@ pipeline:
username: "sutty" username: "sutty"
repo: "gitea.nulo.in/sutty/panel" repo: "gitea.nulo.in/sutty/panel"
tags: tags:
- "${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}" - "${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}-${CI_COMMIT_BRANCH}"
- "latest" - "latest"
build_args: build_args:
- "RUBY_VERSION=${RUBY_VERSION}" - "RUBY_VERSION=${RUBY_VERSION}"
@ -20,13 +20,15 @@ pipeline:
branch: branch:
- "rails" - "rails"
- "panel.sutty.nl" - "panel.sutty.nl"
- "17.3.alpine.panel.sutty.nl"
event: "push" event: "push"
path: path:
include: include:
- "Dockerfile" - "Dockerfile"
- ".dockerignore" - ".dockerignore"
- ".woodpecker.yml"
assets: assets:
image: "gitea.nulo.in/sutty/panel:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}" image: "gitea.nulo.in/sutty/panel:3.14.10-2.7.8"
commands: commands:
- "apk add python2 dotenv openssh-client brotli" - "apk add python2 dotenv openssh-client brotli"
- "install -d -m 700 ~/.ssh/" - "install -d -m 700 ~/.ssh/"
@ -50,6 +52,9 @@ pipeline:
- "git add public && git commit -m \"ci: assets [skip ci]\"" - "git add public && git commit -m \"ci: assets [skip ci]\""
- "git pull upstream ${CI_COMMIT_BRANCH}" - "git pull upstream ${CI_COMMIT_BRANCH}"
- "git push upstream ${CI_COMMIT_BRANCH}" - "git push upstream ${CI_COMMIT_BRANCH}"
environment:
- "RUBY_VERSION=${RUBY_VERSION}"
- "GEMS_SOURCE=https://14.3.alpine.gems.sutty.nl"
secrets: secrets:
- "SSH_KEY" - "SSH_KEY"
- "KNOWN_HOSTS" - "KNOWN_HOSTS"
@ -64,8 +69,15 @@ pipeline:
- "app/javascript/**/*" - "app/javascript/**/*"
- "package.json" - "package.json"
- "yarn.lock" - "yarn.lock"
matrix:
ALPINE_VERSION: "3.14.10"
RUBY_VERSION: "2.7"
RUBY_PATCH: "8"
matrix: matrix:
include: include:
- ALPINE_VERSION: "3.17.3"
RUBY_VERSION: "3.1"
RUBY_PATCH: "4"
- ALPINE_VERSION: "3.14.10" - ALPINE_VERSION: "3.14.10"
RUBY_VERSION: "2.7" RUBY_VERSION: "2.7"
RUBY_PATCH: "8" RUBY_PATCH: "8"

33
Gemfile
View file

@ -1,14 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
puts 'Usa haini.sh para generar un entorno de trabajo reproducible' source ENV.fetch('GEMS_SOURCE', 'https://17.3.alpine.gems.sutty.nl')
source 'https://gems.sutty.nl'
ruby '~> 2.7' ruby "~> #{ENV.fetch('RUBY_VERSION', '3.1')}"
gem 'dotenv-rails', require: 'dotenv/rails-now' gem 'dotenv-rails', require: 'dotenv/rails-now'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6' gem 'rails', '~> 6.1.0'
# Use Puma as the app server # Use Puma as the app server
gem 'puma' gem 'puma'
@ -33,14 +32,14 @@ gem 'turbolinks', '~> 5'
gem 'jbuilder', '~> 2.5' gem 'jbuilder', '~> 2.5'
# Use ActiveModel has_secure_password # Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7' gem 'bcrypt', '~> 3.1.7'
gem 'safely_block', '~> 0.3.0'
gem 'blazer' gem 'blazer'
gem 'chartkick' gem 'chartkick'
gem 'commonmarker' 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 'distributed-press-api-client', '~> 0.3.0rc0'
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'
@ -51,10 +50,10 @@ gem 'image_processing'
gem 'icalendar' gem 'icalendar'
gem 'inline_svg' gem 'inline_svg'
gem 'httparty' gem 'httparty'
gem 'safe_yaml' gem 'safe_yaml', require: false
gem 'jekyll', '~> 4.2' gem 'jekyll', '~> 4.2.0'
gem 'jekyll-data' gem 'jekyll-data'
gem 'jekyll-commonmark' gem 'jekyll-commonmark', '~> 1.4.0'
gem 'jekyll-images' gem 'jekyll-images'
gem 'jekyll-include-cache' gem 'jekyll-include-cache'
gem 'sutty-liquid', '>= 0.7.3' gem 'sutty-liquid', '>= 0.7.3'
@ -65,19 +64,21 @@ gem 'mobility'
gem 'pundit' gem 'pundit'
gem 'rails-i18n' gem 'rails-i18n'
gem 'rails_warden' gem 'rails_warden'
gem 'redis', require: %w[redis redis/connection/hiredis] gem 'redis', '~> 4.0', require: %w[redis redis/connection/hiredis]
gem 'redis-rails' gem 'redis-rails'
gem 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update' gem 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update'
gem 'rubyzip' gem 'rubyzip'
gem 'rugged' gem 'rugged', '1.5.0.1'
gem 'git_clone_url'
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'
gem 'webpacker' gem 'webpacker'
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
gem 'kaminari' gem 'kaminari'
gem 'device_detector'
# database # database
gem 'hairtrigger' gem 'hairtrigger'
@ -111,7 +112,7 @@ group :development, :test do
gem 'pry' gem 'pry'
# Adds support for Capybara system testing and selenium driver # Adds support for Capybara system testing and selenium driver
gem 'capybara', '~> 2.13' gem 'capybara', '~> 2.13'
gem 'selenium-webdriver' gem 'selenium-webdriver', '~> 4.8.0'
gem 'sqlite3' gem 'sqlite3'
end end
@ -119,11 +120,11 @@ group :development do
gem 'brakeman' gem 'brakeman'
gem 'haml-lint', require: false gem 'haml-lint', require: false
gem 'letter_opener' gem 'letter_opener'
gem 'listen', '>= 3.0.5', '< 3.2' gem 'listen'
gem 'rubocop-rails' gem 'rubocop-rails'
gem 'spring' gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0' gem 'spring-watcher-listen'
gem 'web-console', '>= 3.3.0' gem 'web-console'
end end
group :test do group :test do

View file

@ -25,86 +25,86 @@ GIT
groupdate (>= 5.2) groupdate (>= 5.2)
GEM GEM
remote: https://gems.sutty.nl/ remote: https://17.3.alpine.gems.sutty.nl/
specs: specs:
actioncable (6.1.4.1) actioncable (6.1.7.3)
actionpack (= 6.1.4.1) actionpack (= 6.1.7.3)
activesupport (= 6.1.4.1) activesupport (= 6.1.7.3)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.4.1) actionmailbox (6.1.7.3)
actionpack (= 6.1.4.1) actionpack (= 6.1.7.3)
activejob (= 6.1.4.1) activejob (= 6.1.7.3)
activerecord (= 6.1.4.1) activerecord (= 6.1.7.3)
activestorage (= 6.1.4.1) activestorage (= 6.1.7.3)
activesupport (= 6.1.4.1) activesupport (= 6.1.7.3)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.4.1) actionmailer (6.1.7.3)
actionpack (= 6.1.4.1) actionpack (= 6.1.7.3)
actionview (= 6.1.4.1) actionview (= 6.1.7.3)
activejob (= 6.1.4.1) activejob (= 6.1.7.3)
activesupport (= 6.1.4.1) 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.4.1) actionpack (6.1.7.3)
actionview (= 6.1.4.1) actionview (= 6.1.7.3)
activesupport (= 6.1.4.1) 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.4.1) actiontext (6.1.7.3)
actionpack (= 6.1.4.1) actionpack (= 6.1.7.3)
activerecord (= 6.1.4.1) activerecord (= 6.1.7.3)
activestorage (= 6.1.4.1) activestorage (= 6.1.7.3)
activesupport (= 6.1.4.1) activesupport (= 6.1.7.3)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.4.1) actionview (6.1.7.3)
activesupport (= 6.1.4.1) 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.4.1) activejob (6.1.7.3)
activesupport (= 6.1.4.1) activesupport (= 6.1.7.3)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.4.1) activemodel (6.1.7.3)
activesupport (= 6.1.4.1) activesupport (= 6.1.7.3)
activerecord (6.1.4.1) activerecord (6.1.7.3)
activemodel (= 6.1.4.1) activemodel (= 6.1.7.3)
activesupport (= 6.1.4.1) activesupport (= 6.1.7.3)
activestorage (6.1.4.1) activestorage (6.1.7.3)
actionpack (= 6.1.4.1) actionpack (= 6.1.7.3)
activejob (= 6.1.4.1) activejob (= 6.1.7.3)
activerecord (= 6.1.4.1) activerecord (= 6.1.7.3)
activesupport (= 6.1.4.1) activesupport (= 6.1.7.3)
marcel (~> 1.0.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (6.1.4.1) 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.0) addressable (2.8.4)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2) ast (2.4.2)
autoprefixer-rails (10.3.3.0) autoprefixer-rails (10.4.13.0)
execjs (~> 2) execjs (~> 2)
bcrypt (3.1.16-x86_64-linux-musl) bcrypt (3.1.19-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.9.2) benchmark-ips (2.12.0)
bindex (0.8.1-x86_64-linux-musl) bindex (0.8.1-x86_64-linux-musl)
blazer (2.4.7) 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)
bootstrap (4.6.0) bootstrap (4.6.2)
autoprefixer-rails (>= 9.1.0) autoprefixer-rails (>= 9.1.0)
popper_js (>= 1.14.3, < 2) popper_js (>= 1.16.1, < 2)
sassc-rails (>= 2.0.0) sassc-rails (>= 2.0.0)
brakeman (5.1.2) brakeman (5.4.1)
builder (3.2.4) builder (3.2.4)
capybara (2.18.0) capybara (2.18.0)
addressable addressable
@ -113,25 +113,24 @@ 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.1.2) chartkick (5.0.2)
childprocess (4.1.0)
climate_control (1.2.0) climate_control (1.2.0)
coderay (1.1.3) coderay (1.1.3)
colorator (1.1.0) colorator (1.1.0)
commonmarker (0.21.2-x86_64-linux-musl) commonmarker (0.23.10-x86_64-linux-musl)
ruby-enum (~> 0.5) concurrent-ruby (1.2.2)
concurrent-ruby (1.1.9) concurrent-ruby-ext (1.2.2-x86_64-linux-musl)
concurrent-ruby-ext (1.1.9-x86_64-linux-musl) concurrent-ruby (= 1.2.2)
concurrent-ruby (= 1.1.9)
crass (1.0.6) crass (1.0.6)
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)
dead_end (3.1.0) date (3.3.3-x86_64-linux-musl)
derailed_benchmarks (2.1.1) dead_end (4.0.0)
derailed_benchmarks (2.1.2)
benchmark-ips (~> 2) benchmark-ips (~> 2)
dead_end dead_end
get_process_mem (~> 0) get_process_mem (~> 0)
@ -143,29 +142,30 @@ 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.0) device_detector (1.1.1)
devise (4.9.2)
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.1) devise-i18n (1.11.0)
devise (>= 4.8.0) devise (>= 4.9.0)
devise_invitable (2.0.5) devise_invitable (2.0.8)
actionmailer (>= 5.0) actionmailer (>= 5.0)
devise (>= 4.6) devise (>= 4.6)
distributed-press-api-client (0.2.2) distributed-press-api-client (0.3.0rc0)
addressable (~> 2.3, >= 2.3.0) addressable (~> 2.3, >= 2.3.0)
climate_control climate_control
dry-schema dry-schema
httparty (~> 0.18) httparty (~> 0.18)
json (~> 2.1, >= 2.1.0) json (~> 2.1, >= 2.1.0)
jwt (~> 2.6.0) jwt (~> 2.6.0)
dotenv (2.7.6) dotenv (2.8.1)
dotenv-rails (2.7.6) dotenv-rails (2.8.1)
dotenv (= 2.7.6) dotenv (= 2.8.1)
railties (>= 3.2) railties (>= 3.2)
down (5.2.4) down (5.4.1)
addressable (~> 2.8) addressable (~> 2.8)
dry-configurable (1.0.1) dry-configurable (1.0.1)
dry-core (~> 1.0, < 2) dry-core (~> 1.0, < 2)
@ -179,65 +179,68 @@ GEM
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2) dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
dry-schema (1.13.0) dry-schema (1.13.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
dry-configurable (~> 1.0, >= 1.0.1) dry-configurable (~> 1.0, >= 1.0.1)
dry-core (~> 1.0, < 2) dry-core (~> 1.0, < 2)
dry-initializer (~> 3.0) dry-initializer (~> 3.0)
dry-logic (>= 1.5, < 2) dry-logic (>= 1.4, < 2)
dry-types (>= 1.7, < 2) dry-types (>= 1.7, < 2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
dry-types (1.7.0) dry-types (1.7.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2) dry-core (~> 1.0)
dry-inflector (~> 1.0, < 2) dry-inflector (~> 1.0)
dry-logic (>= 1.4, < 2) dry-logic (~> 1.4)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
ed25519 (1.2.4-x86_64-linux-musl) 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)
errbase (0.2.1) errbase (0.2.2)
erubi (1.10.0) erubi (1.12.0)
eventmachine (1.2.7-x86_64-linux-musl) eventmachine (1.2.7-x86_64-linux-musl)
exception_notification (4.4.3) exception_notification (4.5.0)
actionmailer (>= 4.0, < 7) actionmailer (>= 5.2, < 8)
activesupport (>= 4.0, < 7) activesupport (>= 5.2, < 8)
execjs (2.8.1) execjs (2.8.1)
factory_bot (6.2.0) factory_bot (6.2.1)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.2.0) factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
fast_blank (1.0.1-x86_64-linux-musl) fast_blank (1.0.1-x86_64-linux-musl)
fast_jsonparser (0.5.0-x86_64-linux-musl) fast_jsonparser (0.5.0-x86_64-linux-musl)
ffi (1.15.4-x86_64-linux-musl) ffi (1.15.5-x86_64-linux-musl)
flamegraph (0.9.5) flamegraph (0.9.5)
forwardable-extended (2.6.0) forwardable-extended (2.6.0)
friendly_id (5.4.2) friendly_id (5.5.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
get_process_mem (0.2.7) get_process_mem (0.2.7)
ffi (~> 1.0) ffi (~> 1.0)
globalid (0.6.0) git_clone_url (2.0.0)
uri-ssh_git (>= 2.0)
globalid (1.1.0)
activesupport (>= 5.0) activesupport (>= 5.0)
groupdate (6.1.0) groupdate (6.2.1)
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 (5.2.2) haml (6.1.2-x86_64-linux-musl)
temple (>= 0.8.0) temple (>= 0.8.2)
thor
tilt tilt
haml-lint (0.999.999) haml-lint (0.999.999)
haml_lint haml_lint
haml_lint (0.37.1) haml_lint (0.45.0)
haml (>= 4.0, < 5.3) haml (>= 4.0, < 6.2)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
rubocop (>= 0.50.0) rubocop (>= 0.50.0)
sysexits (~> 1.1) sysexits (~> 1.1)
hamlit (2.15.1-x86_64-linux-musl) hamlit (3.0.3-x86_64-linux-musl)
temple (>= 0.8.2) temple (>= 0.8.2)
thor thor
tilt tilt
@ -253,20 +256,21 @@ GEM
httparty (0.21.0) httparty (0.21.0)
mini_mime (>= 1.0.0) mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
i18n (1.8.11) i18n (1.14.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
icalendar (2.7.1) icalendar (2.8.0)
ice_cube (~> 0.16) ice_cube (~> 0.16)
ice_cube (0.16.4) ice_cube (0.16.4)
image_processing (1.12.1) 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.7.2) inline_svg (1.9.0)
activesupport (>= 3.0) activesupport (>= 3.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
jbuilder (2.11.3) jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jekyll (4.2.1) jekyll (4.2.2)
addressable (~> 2.4) addressable (~> 2.4)
colorator (~> 1.0) colorator (~> 1.0)
em-websocket (~> 0.5) em-websocket (~> 0.5)
@ -281,242 +285,216 @@ GEM
rouge (~> 3.0) rouge (~> 3.0)
safe_yaml (~> 1.0) safe_yaml (~> 1.0)
terminal-table (~> 2.0) terminal-table (~> 2.0)
jekyll-commonmark (1.3.2) jekyll-commonmark (1.4.0)
commonmarker (~> 0.14, < 0.22) commonmarker (~> 0.22)
jekyll (>= 3.7, < 5.0)
jekyll-data (1.1.2) jekyll-data (1.1.2)
jekyll (>= 3.3, < 5.0.0) jekyll (>= 3.3, < 5.0.0)
jekyll-dotenv (0.2.0) jekyll-images (0.4.1)
dotenv (~> 2.7)
jekyll (~> 4)
jekyll-feed (0.15.1)
jekyll (>= 3.7, < 5.0)
jekyll-hardlinks (0.1.2)
jekyll (~> 4)
jekyll-ignore-layouts (0.1.2)
jekyll (~> 4)
jekyll-images (0.3.2)
jekyll (~> 4) jekyll (~> 4)
ruby-filemagic (~> 0.7) ruby-filemagic (~> 0.7)
ruby-vips (~> 2) ruby-vips (~> 2)
jekyll-include-cache (0.2.1) jekyll-include-cache (0.2.1)
jekyll (>= 3.7, < 5.0) jekyll (>= 3.7, < 5.0)
jekyll-linked-posts (0.4.2) jekyll-sass-converter (2.2.0)
jekyll (~> 4)
jekyll-locales (0.1.13)
jekyll-lunr (0.3.0)
loofah (~> 2.4)
jekyll-order (0.1.4)
jekyll-relative-urls (0.0.6)
jekyll (~> 4)
jekyll-sass-converter (2.1.0)
sassc (> 2.0.1, < 3.0) sassc (> 2.0.1, < 3.0)
jekyll-seo-tag (2.7.1)
jekyll (>= 3.8, < 5.0)
jekyll-spree-client (0.1.19)
fast_blank (~> 1)
spree-api-client (>= 0.2.4)
jekyll-turbolinks (0.0.5)
jekyll (~> 4)
turbolinks-source (~> 5)
jekyll-unique-urls (0.1.1)
jekyll (~> 4)
jekyll-watch (2.2.1) jekyll-watch (2.2.1)
listen (~> 3.0) listen (~> 3.0)
jekyll-write-and-commit-changes (0.2.1) json (2.6.3-x86_64-linux-musl)
jekyll (~> 4)
rugged (~> 1)
jwt (2.6.0) jwt (2.6.0)
kaminari (1.2.1) kaminari (1.2.2)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1) kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.1) kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.1) kaminari-core (= 1.2.2)
kaminari-actionview (1.2.1) kaminari-actionview (1.2.2)
actionview actionview
kaminari-core (= 1.2.1) kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.1) kaminari-activerecord (1.2.2)
activerecord activerecord
kaminari-core (= 1.2.1) kaminari-core (= 1.2.2)
kaminari-core (1.2.1) kaminari-core (1.2.2)
kramdown (2.3.1) kramdown (2.4.0)
rexml rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
launchy (2.5.0) launchy (2.5.2)
addressable (~> 2.7) addressable (~> 2.8)
letter_opener (1.7.0) letter_opener (1.8.1)
launchy (~> 2.2) launchy (>= 2.2, < 3)
liquid (4.0.3) liquid (4.0.4)
listen (3.1.5) listen (3.8.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.10)
ruby_dep (~> 1.2)
loaf (0.10.0) loaf (0.10.0)
railties (>= 3.2) railties (>= 3.2)
lockbox (0.6.6) lockbox (1.2.0)
lograge (0.11.2) 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.12.0) loofah (2.21.3)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.12.0)
mail (2.7.1) mail (2.8.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.2) marcel (1.0.2)
memory_profiler (1.0.0) 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.2021.1115)
mini_histogram (0.3.1) mini_histogram (0.3.1)
mini_magick (4.11.0) mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.6.1) mini_portile2 (2.8.2)
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)
net-ssh (6.1.0) net-imap (0.3.4)
netaddr (2.0.5) date
nio4r (2.5.8-x86_64-linux-musl) net-protocol
nokogiri (1.12.5-x86_64-linux-musl) net-pop (0.1.2)
mini_portile2 (~> 2.6.1) net-protocol
net-protocol (0.2.1)
timeout
net-smtp (0.3.3)
net-protocol
net-ssh (7.1.0)
netaddr (2.0.6)
nio4r (2.5.9-x86_64-linux-musl)
nokogiri (1.15.4-x86_64-linux-musl)
mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
njalla-api-client (0.2.0)
dry-schema
httparty (~> 0.18)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pairing_heap (3.0.0) pairing_heap (3.0.1)
parallel (1.21.0) parallel (1.23.0)
parser (3.0.2.0) parser (3.2.2.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.2.3-x86_64-linux-musl) pg (1.5.3-x86_64-linux-musl)
pg_search (2.3.5) pg_search (2.3.6)
activerecord (>= 5.2) activerecord (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
popper_js (1.16.0) popper_js (1.16.1)
prometheus_exporter (1.0.0) 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 (4.0.6) public_suffix (5.0.3)
puma (5.5.2-x86_64-linux-musl) puma (6.3.1-x86_64-linux-musl)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.1) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
racc (1.6.0-x86_64-linux-musl) que (2.2.1)
rack (2.2.3) racc (1.7.1-x86_64-linux-musl)
rack-cors (1.1.1) rack (2.2.7)
rack-cors (2.0.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-mini-profiler (2.3.3) rack-mini-profiler (3.1.0)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-proxy (0.7.0) 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.4.1) rails (6.1.7.3)
actioncable (= 6.1.4.1) actioncable (= 6.1.7.3)
actionmailbox (= 6.1.4.1) actionmailbox (= 6.1.7.3)
actionmailer (= 6.1.4.1) actionmailer (= 6.1.7.3)
actionpack (= 6.1.4.1) actionpack (= 6.1.7.3)
actiontext (= 6.1.4.1) actiontext (= 6.1.7.3)
actionview (= 6.1.4.1) actionview (= 6.1.7.3)
activejob (= 6.1.4.1) activejob (= 6.1.7.3)
activemodel (= 6.1.4.1) activemodel (= 6.1.7.3)
activerecord (= 6.1.4.1) activerecord (= 6.1.7.3)
activestorage (= 6.1.4.1) activestorage (= 6.1.7.3)
activesupport (= 6.1.4.1) activesupport (= 6.1.7.3)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.4.1) 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.2) rails-html-sanitizer (1.5.0)
loofah (~> 2.3) loofah (~> 2.19, >= 2.19.1)
rails-i18n (6.0.0) rails-i18n (7.0.7)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7) 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.4.1) railties (6.1.7.3)
actionpack (= 6.1.4.1) actionpack (= 6.1.7.3)
activesupport (= 6.1.4.1) activesupport (= 6.1.7.3)
method_source method_source
rake (>= 0.13) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
rainbow (3.0.0) rainbow (3.1.1)
rake (13.0.6) rake (13.0.6)
rb-fsevent (0.11.0) 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.2.1) redis-activesupport (5.3.0)
activesupport (>= 3, < 7) activesupport (>= 3, < 8)
redis-store (>= 1.3, < 2) redis-store (>= 1.3, < 2)
redis-rack (2.1.3) redis-rack (2.1.4)
rack (>= 2.0.8, < 3) rack (>= 2.0.8, < 3)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-rails (5.0.2) redis-rails (5.0.2)
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.0) redis-store (1.9.2)
redis (>= 4, < 5) redis (>= 4, < 6)
regexp_parser (2.1.1) regexp_parser (2.8.0)
request_store (1.5.0) 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) rgl (0.6.3)
pairing_heap (>= 0.3.0) pairing_heap (>= 0.3.0)
rexml (~> 3.2, >= 3.2.4) rexml (~> 3.2, >= 3.2.4)
stream (~> 0.5.3) stream (~> 0.5.3)
rouge (3.26.1) rouge (3.30.0)
rubocop (1.23.0) rubocop (1.42.0)
json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.0.0.0) 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 rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.12.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.13.0) rubocop-ast (1.28.1)
parser (>= 3.0.1.1) parser (>= 3.2.1.0)
rubocop-rails (2.12.4) rubocop-rails (2.19.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
ruby-enum (0.9.0) ruby-filemagic (0.7.3-x86_64-linux-musl)
i18n ruby-progressbar (1.13.0)
ruby-filemagic (0.7.2-x86_64-linux-musl) ruby-statistics (3.0.2)
ruby-progressbar (1.11.0)
ruby-statistics (3.0.0)
ruby-vips (2.1.4) ruby-vips (2.1.4)
ffi (~> 1.12) ffi (~> 1.12)
ruby2ruby (2.4.4) ruby2ruby (2.5.0)
ruby_parser (~> 3.1) ruby_parser (~> 3.1)
sexp_processor (~> 4.6) sexp_processor (~> 4.6)
ruby_dep (1.5.0) ruby_parser (3.20.1)
ruby_parser (3.18.1)
sexp_processor (~> 4.16) sexp_processor (~> 4.16)
rubyzip (2.3.2) rubyzip (2.3.2)
rugged (1.2.0-x86_64-linux-musl) rugged (1.5.0.1-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)
@ -528,59 +506,55 @@ GEM
sprockets (> 3.0) sprockets (> 3.0)
sprockets-rails sprockets-rails
tilt tilt
selenium-webdriver (4.1.0) selenium-webdriver (4.8.6)
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
semantic_range (3.0.0) semantic_range (3.0.0)
sexp_processor (4.16.0) sexp_processor (4.17.0)
simpleidn (0.2.1) simpleidn (0.2.1)
unf (~> 0.1.4) unf (~> 0.1.4)
sourcemap (0.1.1) sourcemap (0.1.1)
spree-api-client (0.2.4) spring (4.1.1)
fast_blank (~> 1) spring-watcher-listen (2.1.0)
httparty (~> 0.18.0)
spring (2.1.1)
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0) spring (>= 4)
sprockets (4.0.2) sprockets (4.2.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (>= 2.2.4, < 4)
sprockets-rails (3.4.1) sprockets-rails (3.4.2)
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.4.2-x86_64-linux-musl) sqlite3 (1.6.3-x86_64-linux-musl)
stackprof (0.2.17-x86_64-linux-musl) mini_portile2 (~> 2.8.0)
stackprof (0.2.25-x86_64-linux-musl)
stream (0.5.5) stream (0.5.5)
sucker_punch (3.0.1) sutty-liquid (0.11.11)
concurrent-ruby (~> 1.0)
sutty-archives (2.5.4)
jekyll (>= 3.6, < 5.0)
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.8.2) temple (0.10.1)
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.1.0) thor (1.2.2)
tilt (2.0.10) tilt (2.1.0)
timecop (0.9.4) timecop (0.9.6)
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.4) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
uglifier (4.2.0) uglifier (4.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8-x86_64-linux-musl) unf_ext (0.0.8.2-x86_64-linux-musl)
unicode-display_width (1.8.0) unicode-display_width (1.8.0)
validates_hostname (1.0.11) uri-ssh_git (2.0.0)
validates_hostname (1.0.13)
activerecord (>= 3.0) activerecord (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
warden (1.2.9) warden (1.2.9)
@ -590,21 +564,21 @@ 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-driver (0.7.5-x86_64-linux-musl) websocket (1.2.9)
websocket-driver (0.7.6-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.5.1) zeitwerk (2.6.8)
PLATFORMS PLATFORMS
ruby
x86_64-linux-musl x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
@ -619,10 +593,11 @@ DEPENDENCIES
concurrent-ruby-ext concurrent-ruby-ext
database_cleaner database_cleaner
derailed_benchmarks derailed_benchmarks
device_detector
devise devise
devise-i18n devise-i18n
devise_invitable devise_invitable
distributed-press-api-client (~> 0.2.3) distributed-press-api-client (~> 0.3.0rc0)
dotenv-rails dotenv-rails
down down
ed25519 ed25519
@ -630,9 +605,10 @@ DEPENDENCIES
exception_notification exception_notification
factory_bot_rails factory_bot_rails
fast_blank fast_blank
fast_jsonparser fast_jsonparser (~> 0.5.0)
flamegraph flamegraph
friendly_id friendly_id
git_clone_url
hairtrigger hairtrigger
haml-lint haml-lint
hamlit-rails hamlit-rails
@ -642,14 +618,14 @@ DEPENDENCIES
image_processing image_processing
inline_svg inline_svg
jbuilder (~> 2.5) jbuilder (~> 2.5)
jekyll (~> 4.2) jekyll (~> 4.2.0)
jekyll-commonmark jekyll-commonmark (~> 1.4.0)
jekyll-data jekyll-data
jekyll-images jekyll-images
jekyll-include-cache jekyll-include-cache
kaminari kaminari
letter_opener letter_opener
listen (>= 3.0.5, < 3.2) listen
loaf loaf
lockbox lockbox
lograge lograge
@ -657,7 +633,6 @@ DEPENDENCIES
mini_magick mini_magick
mobility mobility
net-ssh net-ssh
njalla-api-client
nokogiri nokogiri
pg pg
pg_search pg_search
@ -665,27 +640,28 @@ DEPENDENCIES
pry pry
puma puma
pundit pundit
que
rack-cors rack-cors
rack-mini-profiler rack-mini-profiler
rails (~> 6) rails (~> 6.1.0)
rails-i18n rails-i18n
rails_warden rails_warden
redis redis (~> 4.0)
redis-rails redis-rails
rgl rgl
rollups! rollups!
rubocop-rails rubocop-rails
rubyzip rubyzip
rugged rugged (= 1.5.0.1)
safe_yaml safe_yaml
safely_block (~> 0.3.0)
sassc-rails sassc-rails
selenium-webdriver selenium-webdriver (~> 4.8.0)
sourcemap sourcemap
spring spring
spring-watcher-listen (~> 2.0.0) 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
@ -693,12 +669,12 @@ DEPENDENCIES
turbolinks (~> 5) turbolinks (~> 5)
uglifier (>= 1.3.0) uglifier (>= 1.3.0)
validates_hostname validates_hostname
web-console (>= 3.3.0) web-console
webpacker webpacker
yaml_db! yaml_db!
RUBY VERSION RUBY VERSION
ruby 2.7.1p83 ruby 3.1.4p223
BUNDLED WITH BUNDLED WITH
2.2.2 2.4.17

View file

@ -48,8 +48,6 @@ help: always ## Ayuda
@echo -e "\nArgumentos:\n" @echo -e "\nArgumentos:\n"
@grep -E "^[a-z\-]+ \?=.*##" Makefile | sed -re "s/(.*) \?=.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/" @grep -E "^[a-z\-]+ \?=.*##" Makefile | sed -re "s/(.*) \?=.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
assets: public/packs/manifest.json.br ## Compilar los assets
test: always ## Ejecutar los tests test: always ## Ejecutar los tests
$(MAKE) rake args="test RAILS_ENV=test $(args)" $(MAKE) rake args="test RAILS_ENV=test $(args)"
@ -71,14 +69,14 @@ rake: ## Corre rake dentro del entorno de desarrollo (pasar argumentos con args=
bundle: ## Corre bundle dentro del entorno de desarrollo (pasar argumentos con args=). bundle: ## Corre bundle dentro del entorno de desarrollo (pasar argumentos con args=).
$(hain) 'bundle $(args)' $(hain) 'bundle $(args)'
psql := psql -h $(PG_HOST) -U $(PG_USER) -p $(PG_PORT) -d sutty psql := psql $(DATABASE_URL)
copy-table: copy-table:
test -n "$(table)" test -n "$(table)"
echo "truncate $(table) $(cascade);" | $(psql) echo "truncate $(table) $(cascade);" | $(psql)
ssh $(delegate) docker exec postgresql pg_dump -U sutty -d sutty -t $(table) | $(psql) ssh $(delegate) docker exec postgresql pg_dump -U sutty -d sutty -t $(table) | $(psql)
psql: psql:
$(psql) $(hain) $(psql)
rubocop: ## Yutea el código que está por ser commiteado rubocop: ## Yutea el código que está por ser commiteado
git status --porcelain \ git status --porcelain \
@ -110,21 +108,13 @@ save: ## Subir la imagen Docker al nodo delegado
date +%F | xargs -I {} git tag -f $(container)-{} date +%F | xargs -I {} git tag -f $(container)-{}
@echo -e "\a" @echo -e "\a"
ota-js: assets ## Actualizar Javascript en el nodo delegado
rsync -avi --delete-after --chown 1000:82 public/ root@$(delegate):/srv/sutty/srv/http/data/_$(public)/
ssh root@$(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2"
ota: ## Actualizar Rails en el nodo delegado ota: ## Actualizar Rails en el nodo delegado
ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl pull ; true git push
ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl pull
ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl lfs prune
ssh $(delegate) chown -R 1000:82 /srv/sutty/srv/http/panel.sutty.nl ssh $(delegate) chown -R 1000:82 /srv/sutty/srv/http/panel.sutty.nl
ssh $(delegate) docker exec $(container) rails reload ssh $(delegate) docker exec $(container) rails reload
# Todos los archivos de assets. Si alguno cambia, se van a recompilar
# los assets que luego se suben al nodo delegado.
assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type f)
public/packs/manifest.json.br: $(assets)
$(hain) 'PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean'
# Correr un test en particular por ejemplo # Correr un test en particular por ejemplo
# `make test/models/usuarie_test.rb` # `make test/models/usuarie_test.rb`
tests := $(shell find test/ -name "*_test.rb") tests := $(shell find test/ -name "*_test.rb")

View file

@ -7,5 +7,6 @@ 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 distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
cleanup: bundle exec rake cleanup:everything cleanup: bundle exec rake cleanup:everything
emergency_cleanup: bundle exec rake cleanup:everything BEFORE=7
stats: bundle exec rake stats:process_all stats: bundle exec rake stats:process_all
distributed_press_renew_tokens: bundle exec rake distributed_press:tokens:renew que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que

View file

@ -16,7 +16,7 @@ $primary: $magenta;
$secondary: $black; $secondary: $black;
$jumbotron-bg: transparent; $jumbotron-bg: transparent;
$enable-rounded: false; $enable-rounded: false;
$form-feedback-valid-color: $cyan; $form-feedback-valid-color: $black;
$form-feedback-invalid-color: $magenta; $form-feedback-invalid-color: $magenta;
$form-feedback-icon-valid-color: $black; $form-feedback-icon-valid-color: $black;
$component-active-bg: $magenta; $component-active-bg: $magenta;
@ -29,6 +29,11 @@ $sizes: (
"70ch": 70ch, "70ch": 70ch,
); );
.btn {
background-color: var(--foreground);
color: var(--background);
}
@import "bootstrap"; @import "bootstrap";
@import "editor"; @import "editor";
@ -204,8 +209,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;
@ -383,6 +386,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.
*/ */
@ -405,6 +411,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; }

View file

@ -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]

View file

@ -9,10 +9,10 @@ module Api
# Generar un stacktrace en segundo plano y enviarlo por correo # Generar un stacktrace en segundo plano y enviarlo por correo
# solo si la API key es verificable. Del otro lado siempre # solo si la API key es verificable. Del otro lado siempre
# respondemos con lo mismo. # respondemos con lo mismo.
def create def create
if site&.airbrake_valid? airbrake_token if (site&.airbrake_valid? airbrake_token) && !detected_device.bot?
BacktraceJob.perform_later site_id: params[:site_id], BacktraceJob.perform_later site_id: params[:site_id],
params: airbrake_params.to_h params: airbrake_params.to_h
end end
render status: 201, json: { id: 1, url: '' } render status: 201, json: { id: 1, url: '' }
@ -34,6 +34,11 @@ module Api
def airbrake_token def airbrake_token
@airbrake_token ||= params[:key] @airbrake_token ||= params[:key]
end end
# @return [DeviceDetector]
def detected_device
@detected_device ||= DeviceDetector.new(request.headers['User-Agent'], request.headers)
end
end end
end end
end end

View file

@ -0,0 +1,77 @@
# frozen_string_literal: true
module Api
module V1
# Recibe webhooks y lanza un PullJob
class WebhooksController < BaseController
# responde con forbidden si falla la validación del token
rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer
# Trae los cambios a partir de un post de Webhooks:
# (Gitlab, Github, Gitea, etc)
#
# @return [nil]
def pull
message = I18n.with_locale(site.default_locale) do
I18n.t('webhooks.pull.message')
end
GitPullJob.perform_later(site, usuarie, message)
head :ok
end
private
# encuentra el sitio a partir de la url
def site
@site ||= Site.find_by_name!(params[:site_id])
end
# valida el token que envía la plataforma del webhook
#
# @return [String]
def token
@token ||=
begin
# Gitlab
if request.headers['X-Gitlab-Token'].present?
request.headers['X-Gitlab-Token']
# Github
elsif request.headers['X-Hub-Signature-256'].present?
token_from_signature(request.headers['X-Hub-Signature-256'], 'sha256=')
# Gitea
elsif request.headers['X-Gitea-Signature'].present?
token_from_signature(request.headers['X-Gitea-Signature'])
else
raise ActiveRecord::RecordNotFound, 'proveedor no soportado'
end
end
end
# valida token a partir de firma de webhook
#
# @return [String, Boolean]
def token_from_signature(signature, prepend = '')
payload = request.body.read
site.roles.where(temporal: false, rol: 'usuarie').pluck(:token).find do |token|
new_signature = prepend + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), token, payload)
ActiveSupport::SecurityUtils.secure_compare(new_signature, signature.to_s)
end.tap do |t|
raise ActiveRecord::RecordNotFound, 'token no encontrado' if t.nil?
end
end
# encuentra le usuarie
def usuarie
@usuarie ||= site.roles.find_by!(temporal: false, rol: 'usuarie', token: token).usuarie
end
# respuesta de error a plataformas
def platforms_answer(exception)
ExceptionNotifier.notify_exception(exception, data: { headers: request.headers.to_h }
head :forbidden
end
end
end
end

View file

@ -59,7 +59,11 @@ class ApplicationController < ActionController::Base
# #
# @return [String,Symbol] # @return [String,Symbol]
def current_locale def current_locale
session[:locale] = params[:change_locale_to] if params[:change_locale_to].present? locale = params[:change_locale_to]
if locale.present? && I18n.locale_available?(locale)
session[:locale] = params[:change_locale_to]
end
session[:locale] || current_usuarie&.lang || I18n.locale session[:locale] || current_usuarie&.lang || I18n.locale
end end
@ -91,6 +95,10 @@ 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

View file

@ -0,0 +1,46 @@
# 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.urls.map do |url|
URI.parse(url)
rescue URI::Error
nil
end.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

View file

@ -5,6 +5,7 @@ class EnvController < ActionController::Base
def index def index
@site = Site.find_by_name('panel') @site = Site.find_by_name('panel')
stale? @site
stale? @site if @site
end end
end end

View file

@ -24,6 +24,7 @@ class PostsController < ApplicationController
# Todos los artículos de este sitio para el idioma actual # Todos los artículos de este sitio para el idioma actual
@posts = site.indexed_posts.where(locale: locale) @posts = site.indexed_posts.where(locale: locale)
@posts = @posts.page(filter_params.delete(:page)) if site.pagination
# De este tipo # De este tipo
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout] @posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
# Que estén dentro de la categoría # Que estén dentro de la categoría
@ -154,15 +155,11 @@ class PostsController < ApplicationController
# #
# @return [Hash] # @return [Hash]
def filter_params def filter_params
@filter_params ||= params.permit(:q, :category, :layout).to_hash.select do |_, v| @filter_params ||= params.permit(:q, :category, :layout, :page).to_hash.select do |_, v|
v.present? v.present?
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

View file

@ -57,6 +57,7 @@ class SitesController < ApplicationController
usuarie: current_usuarie) usuarie: current_usuarie)
if service.update.valid? if service.update.valid?
flash[:notice] = I18n.t('sites.update.post')
redirect_to site_posts_path(site, locale: site.default_locale) redirect_to site_posts_path(site, locale: site.default_locale)
else else
render 'edit' render 'edit'

View file

@ -96,6 +96,13 @@ class UsuariesController < ApplicationController
# XXX: La invitación tiene que ser enviada luego de crear el rol # XXX: La invitación tiene que ser enviada luego de crear el rol
if role.persisted? if role.persisted?
# 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 usuarie.deliver_invitation
else else
raise ArgumentError, role.errors.full_messages raise ArgumentError, role.errors.full_messages

View file

@ -2,11 +2,21 @@
import { Notifier } from '@airbrake/browser' import { Notifier } from '@airbrake/browser'
window.airbrake = new Notifier({ try {
projectId: window.env.AIRBRAKE_SITE_ID, window.airbrake = new Notifier({
projectKey: window.env.AIRBRAKE_API_KEY, projectId: window.env.AIRBRAKE_PROJECT_ID,
host: window.env.PANEL_URL projectKey: window.env.AIRBRAKE_PROJECT_KEY,
}) host: window.env.PANEL_URL
});
console.originalError = console.error;
console.error = (...e) => {
window.airbrake.notify(e.join(" "));
return console.originalError(...e);
};
} catch(e) {
console.error(e);
}
import 'core-js/stable' import 'core-js/stable'
import 'regenerator-runtime/runtime' import 'regenerator-runtime/runtime'

View file

@ -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

View file

@ -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:)

8
app/jobs/cleanup_job.rb Normal file
View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# Realiza tareas de limpieza en segundo plano
class CleanupJob < ApplicationJob
def perform(before = nil)
CleanupService.new(before: before).cleanup_everything!
end
end

View file

@ -4,9 +4,21 @@
class DeployJob < ApplicationJob class DeployJob < ApplicationJob
class DeployException < StandardError; end class DeployException < StandardError; end
class DeployTimedOutException < DeployException; end class DeployTimedOutException < DeployException; end
class DeployAlreadyRunningException < DeployException; end
discard_on ActiveRecord::RecordNotFound 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, output: false) def perform(site, notify: true, time: Time.now, output: false)
@output = output @output = output
@ -20,14 +32,14 @@ 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
notify = false
raise DeployTimedOutException, raise DeployTimedOutException,
"#{@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
DeployJob.perform_in(60, site, notify: notify, time: time, output: output)
return
end end
@deployed = {} @deployed = {}
@ -39,7 +51,15 @@ class DeployJob < ApplicationJob
status = d.deploy(output: @output) status = d.deploy(output: @output)
seconds = d.build_stats.last.try(:seconds) || 0 seconds = d.build_stats.last.try(:seconds) || 0
size = d.size size = d.size
urls = d.respond_to?(:urls) ? d.urls : [d.url].compact urls = d.urls.map do |url|
URI.parse url
rescue URI::Error
nil
end.compact
if d == @site.deployment_list.last && !status
raise DeployException, 'Falló la compilación'
end
rescue StandardError => e rescue StandardError => e
status = false status = false
seconds ||= 0 seconds ||= 0
@ -67,8 +87,6 @@ class DeployJob < ApplicationJob
t << ([type.to_s] + row.values) t << ([type.to_s] + row.values)
end end
end) end)
rescue DeployTimedOutException => e
notify_exception e
ensure ensure
if @site.present? if @site.present?
@site.update status: 'waiting' @site.update status: 'waiting'

16
app/jobs/git_pull_job.rb Normal file
View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
# Permite traer los cambios desde webhooks
class GitPullJob < ApplicationJob
# @param :site [Site]
# @param :usuarie [Usuarie]
# @return [nil]
def perform(site, usuarie)
return unless site.repository.origin
return unless site.repository.fetch.positive?
site.repository.merge(usuarie)
site.reindex_changes!
end
end

11
app/jobs/git_push_job.rb Normal file
View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
# Permite pushear los cambios cada vez que se
# hacen commits en un sitio
class GitPushJob < ApplicationJob
# @param :site [Site]
# @return [nil]
def perform(site)
site.repository.push if site.repository.origin
end
end

View file

@ -10,8 +10,6 @@ class GitlabNotifierJob < ApplicationJob
# 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)

View file

@ -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.

View file

@ -7,7 +7,7 @@ class RenewDistributedPressTokensJob < ApplicationJob
# detener la tarea si algo pasa. # detener la tarea si algo pasa.
def perform def perform
DistributedPressPublisher.with_about_to_expire_tokens.find_each do |publisher| DistributedPressPublisher.with_about_to_expire_tokens.find_each do |publisher|
publisher.touch publisher.save
rescue DistributedPress::V1::Error => e rescue DistributedPress::V1::Error => e
data = { instance: publisher.instance, expires_at: publisher.client.token.expires_at } data = { instance: publisher.instance, expires_at: publisher.client.token.expires_at }

View 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

View file

@ -8,10 +8,15 @@ module ExceptionNotifier
# Recibe la excepción y empieza la tarea de notificación en segundo # Recibe la excepción y empieza la tarea de notificación en segundo
# plano. # plano.
# #
# @param [Exception] # @param :exception [Exception]
# @param [Hash] # @param :options [Hash]
def call(exception, **options) def call(exception, options, &block)
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

View file

@ -52,7 +52,7 @@ class DeployMailer < ApplicationMailer
t << (row.map do |k, v| t << (row.map do |k, v|
case k case k
when :seconds then v[:human] when :seconds then v[:human]
when :urls then url when :urls then url.to_s
else v else v
end end
end) end)

View file

@ -23,6 +23,11 @@ class Deploy < ApplicationRecord
raise NotImplementedError raise NotImplementedError
end end
# @return [Array]
def urls
[url].compact
end
def limit def limit
raise NotImplementedError raise NotImplementedError
end end
@ -55,6 +60,22 @@ class Deploy < ApplicationRecord
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name) @gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
end end
# Un entorno que solo tiene lo que necesitamos
#
# @return [Hash]
def env
# XXX: This doesn't support Windows paths :B
paths = [File.dirname(`which bundle`), '/usr/local/bin', '/usr/bin', '/bin']
# Las variables de entorno extra no pueden superponerse al local.
extra_env.merge({
'HOME' => home_dir,
'PATH' => paths.join(':'),
'JEKYLL_ENV' => Rails.env,
'LANG' => ENV['LANG'],
})
end
# Corre un comando, lo registra en la base de datos y devuelve el # Corre un comando, lo registra en la base de datos y devuelve el
# estado. # estado.
# #
@ -65,22 +86,20 @@ class Deploy < ApplicationRecord
lines = [] lines = []
time_start time_start
Dir.chdir(site.path) do Open3.popen2e(env, cmd, unsetenv_others: true, chdir: site.path) do |_, o, t|
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, o, t| # TODO: Enviar a un websocket para ver el proceso en vivo?
# TODO: Enviar a un websocket para ver el proceso en vivo? Thread.new do
Thread.new do o.each do |line|
o.each do |line| lines << line
lines << line
puts line if output puts line if output
end
rescue IOError => e
lines << e.message
puts e.message if output
end end
rescue IOError => e
r = t.value lines << e.message
puts e.message if output
end end
r = t.value
end end
time_stop time_stop
@ -100,6 +119,11 @@ class Deploy < ApplicationRecord
@local_env ||= {} @local_env ||= {}
end end
# Devuelve opciones para jekyll build
#
# @return [String,nil]
def flags_for_build(**args); end
# Trae todas las dependencias # Trae todas las dependencias
# #
# @return [Array] # @return [Array]
@ -109,6 +133,21 @@ class Deploy < ApplicationRecord
private private
# Escribe el contenido en un archivo temporal y ejecuta el bloque
# provisto con el archivo como parámetro
#
# @param :content [String]
def with_tempfile(content, &block)
Tempfile.create(SecureRandom.hex) do |file|
file.write content.to_s
file.rewind
file.close
# @yieldparam :file [File]
yield file
end
end
# @param [String] # @param [String]
# @return [String] # @return [String]
def readable_cmd(cmd) def readable_cmd(cmd)
@ -119,7 +158,14 @@ class Deploy < ApplicationRecord
@deploy_local ||= site.deploys.find_by(type: 'DeployLocal') @deploy_local ||= site.deploys.find_by(type: 'DeployLocal')
end end
def non_local_deploys # Consigue todas las variables de entorno configuradas por otros
@non_local_deploys ||= site.deploys.where.not(type: 'DeployLocal') # deploys.
#
# @return [Hash]
def extra_env
@extra_env ||=
site.deployment_list.reduce({}) do |extra, deploy|
extra.merge deploy.local_env
end
end end
end end

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'distributed_press/v1/client/site' require 'distributed_press/v1/client/site'
require 'njalla/v1'
# Soportar Distributed Press APIv1 # Soportar Distributed Press APIv1
# #
@ -15,8 +14,8 @@ require 'njalla/v1'
class DeployDistributedPress < Deploy class DeployDistributedPress < Deploy
store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON
before_create :create_remote_site!, :create_njalla_records! before_create :create_remote_site!
before_destroy :delete_remote_site!, :delete_njalla_records! before_destroy :delete_remote_site!
DEPENDENCIES = %i[deploy_local] DEPENDENCIES = %i[deploy_local]
@ -31,17 +30,12 @@ class DeployDistributedPress < Deploy
time_start time_start
create_remote_site! if remote_site_id.blank? create_remote_site! if remote_site_id.blank?
create_njalla_records!
save save
if remote_site_id.blank? if remote_site_id.blank?
raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press' raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press'
end 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| site_client.tap do |c|
stdout = Thread.new(publisher.logger_out) do |io| stdout = Thread.new(publisher.logger_out) do |io|
until io.eof? until io.eof?
@ -52,7 +46,12 @@ class DeployDistributedPress < Deploy
end end
end end
status = c.publish(publishing_site, deploy_local.destination) begin
status = c.publish(publishing_site, deploy_local.destination)
rescue DistributedPress::V1::Error => e
ExceptionNotifier.notify_exception(e, data: { site: site.name })
status = false
end
if status if status
self.remote_info[:distributed_press] = c.show(publishing_site).to_h self.remote_info[:distributed_press] = c.show(publishing_site).to_h
@ -80,25 +79,18 @@ class DeployDistributedPress < Deploy
# Devuelve las URLs de todos los protocolos # Devuelve las URLs de todos los protocolos
def urls def urls
protocol_urls + gateway_urls gateway_urls
end end
private private
# @return [Array]
def gateway_urls def gateway_urls
remote_info.dig(:distributed_press, :links).values.map do |protocol| remote_info.dig(:distributed_press, :links)&.values&.map do |protocol|
[ protocol[:link], protocol[:gateway] ] [ protocol[:link], protocol[:gateway] ]
end.flatten.compact.select do |link| end&.flatten&.compact&.select do |link|
link.include? '://' link.include? '://'
end end || []
end
def protocol_urls
remote_info.dig(:distributed_press, :protocols).select do |_, enabled|
enabled
end.map do |protocol, _|
"#{protocol}://#{site.hostname}"
end
end end
# El cliente de la API # El cliente de la API
@ -147,29 +139,6 @@ class DeployDistributedPress < Deploy
nil nil
end 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ó # Registra lo que sucedió
# #
# @param status [Bool] # @param status [Bool]
@ -187,31 +156,4 @@ class DeployDistributedPress < Deploy
ExceptionNotifier.notify_exception(e, data: { site: site.name }) ExceptionNotifier.notify_exception(e, data: { site: site.name })
nil nil
end 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 end

View file

@ -27,8 +27,4 @@ class DeployFullRsync < DeployRsync
result.present? && result.all? result.present? && result.all?
end end
def url
"https://#{user_host.last}/"
end
end end

View file

@ -14,6 +14,7 @@ class DeployLocal < Deploy
# Sutty # Sutty
def deploy(output: false) def deploy(output: false)
return false unless mkdir return false unless mkdir
return false unless git_lfs(output: output)
return false unless yarn(output: output) return false unless yarn(output: output)
return false unless pnpm(output: output) return false unless pnpm(output: output)
return false unless bundle(output: output) return false unless bundle(output: output)
@ -61,34 +62,26 @@ class DeployLocal < Deploy
FileUtils.rm_rf(File.join(site.path, '.jekyll-cache')) FileUtils.rm_rf(File.join(site.path, '.jekyll-cache'))
end end
# Opciones necesarias para la compilación del sitio
#
# @return [Hash]
def local_env
@local_env ||= {
'SPREE_API_KEY' => site.tienda_api_key,
'SPREE_URL' => site.tienda_url,
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
'YARN_CACHE_FOLDER' => yarn_cache_dir,
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
}
end
private private
def mkdir def mkdir
FileUtils.mkdir_p destination FileUtils.mkdir_p destination
end end
# Un entorno que solo tiene lo que necesitamos
#
# @return [Hash]
def env
# XXX: This doesn't support Windows paths :B
paths = [File.dirname(`which bundle`), '/usr/local/bin', '/usr/bin', '/bin']
# Las variables de entorno extra no pueden superponerse al local.
extra_env.merge({
'HOME' => home_dir,
'PATH' => paths.join(':'),
'SPREE_API_KEY' => site.tienda_api_key,
'SPREE_URL' => site.tienda_url,
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
'JEKYLL_ENV' => Rails.env,
'LANG' => ENV['LANG'],
'YARN_CACHE_FOLDER' => yarn_cache_dir,
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
})
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
@ -113,6 +106,11 @@ class DeployLocal < Deploy
File.exist? pnpm_lock File.exist? pnpm_lock
end end
def git_lfs(output: false)
run %(git lfs fetch), output: output
run %(git lfs checkout), output: output
end
def gem(output: false) def gem(output: false)
run %(gem install bundler --no-document), output: output run %(gem install bundler --no-document), output: output
end end
@ -132,11 +130,20 @@ class DeployLocal < Deploy
end end
def bundle(output: false) def bundle(output: false)
run %(bundle install --no-cache --path="#{gems_dir}" --clean --without test development), output: output run %(bundle config set --local clean 'true'), output: output
run %(bundle config set --local deployment 'true'), output: output
run %(bundle config set --local path '#{gems_dir}'), output: output
run %(bundle config set --local without 'test development'), output: output
run %(bundle config set --local cache_all 'false'), output: output
run %(bundle install), output: output
end end
def jekyll_build(output: false) def jekyll_build(output: false)
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}"), output: output with_tempfile(site.private_key_pem) do |file|
flags = extra_flags(private_key: file)
run %(bundle exec jekyll build --trace --profile #{flags} --destination "#{escaped_destination}"), output: output
end
end end
# no debería haber espacios ni caracteres especiales, pero por si # no debería haber espacios ni caracteres especiales, pero por si
@ -150,17 +157,13 @@ class DeployLocal < Deploy
FileUtils.rm_rf destination FileUtils.rm_rf destination
end end
# Consigue todas las variables de entorno configuradas por otros # Genera opciones extra desde los otros deploys
# deploys.
# #
# @deprecated Solo tenía sentido para Distributed Press v0 # @param :args [Hash]
# @return [Hash] # @return [String]
def extra_env def extra_flags(**args)
@extra_env ||= site.deployment_list.map do |deploy|
non_local_deploys.reduce({}) do |extra_env, deploy| deploy.flags_for_build(**args)
extra_env.tap do |e| end.compact.join(' ')
e.merge! deploy.local_env
end
end
end end
end end

View file

@ -38,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

View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'distributed_press/v1/social/client'
# Publicar novedades al Fediverso
class DeploySocialDistributedPress < Deploy
# Solo luego de publicar remotamente
DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync]
# Envía las notificaciones
def deploy(output: false)
with_tempfile(site.private_key_pem) do |file|
key = Shellwords.escape file.path
dest = Shellwords.escape destination
run %(bundle exec jekyll notify --trace --key #{key} --destination "#{dest}"), output: output
end
end
# Igual que DeployLocal
#
# @return [String]
def destination
File.join(Rails.root, '_deploy', site.hostname)
end
# Solo uno
#
# @return [Integer]
def limit
1
end
# Espacio ocupado, pero no podemos calcularlo
#
# @return [Integer]
def size
0
end
# El perfil de actor
#
# @return [String,nil]
def url
site.data.dig('activity_pub', 'actor')
end
# Genera la opción de llave privada para jekyll build
#
# @params :args [Hash]
# @return [String]
def flags_for_build(**args)
"--key #{Shellwords.escape args[:private_key].path}"
end
end

View file

@ -36,6 +36,15 @@ class IndexedPost < ApplicationRecord
belongs_to :site belongs_to :site
# Encuentra el post original
#
# @return [nil,Post]
def post
return if post_id.blank?
@post ||= site.posts(lang: locale).find(post_id, uuid: true)
end
# Convertir locale a direccionario de PG # Convertir locale a direccionario de PG
# #
# @param [String,Symbol] # @param [String,Symbol]

View file

@ -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

View file

@ -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

View file

@ -4,8 +4,12 @@
# #
# Esto es increíblemente difícil de lograr que salga bien! # Esto es increíblemente difícil de lograr que salga bien!
class MetadataBoolean < MetadataTemplate class MetadataBoolean < MetadataTemplate
# El valor por defecto es una versión booleana de lo que diga (o no
# diga) el esquema
#
# @return [Boolean]
def default_value def default_value
false !!super
end end
# Los checkboxes son especiales porque la especificación de HTML # Los checkboxes son especiales porque la especificación de HTML

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
# Fecha y hora de creación
class MetadataCreatedAt < MetadataTemplate
# Por defecto la hora actual, pero por retrocompatibilidad, queremos
# la fecha de publicación
def default_value
if post.date.value.to_date < Time.now.to_date
post.date.value
else
Time.now
end
end
# Nunca cambia
def value=(new_value)
value
end
end

View file

@ -34,7 +34,7 @@ class MetadataRelatedPosts < MetadataArray
end end
def title(post) def title(post)
"#{post&.title&.value || post&.slug&.value} (#{post.layout.humanized_name})" "#{post&.title&.value || post&.slug&.value} #{post&.date&.value.strftime('%F')} (#{post.layout.humanized_name})"
end end
# Encuentra el filtro # Encuentra el filtro

View file

@ -202,7 +202,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
def allowed_attributes def allowed_attributes
@allowed_attributes ||= %w[style href src alt controls data-align data-multimedia data-multimedia-inner id @allowed_attributes ||= %w[style href src alt controls data-align data-multimedia data-multimedia-inner id
name].freeze name start].freeze
end end
def allowed_tags def allowed_tags

View file

@ -12,7 +12,7 @@ class Post
DEFAULT_ATTRIBUTES = %i[site document layout].freeze DEFAULT_ATTRIBUTES = %i[site document layout].freeze
# Otros atributos que no vienen en los metadatos # Otros atributos que no vienen en los metadatos
PRIVATE_ATTRIBUTES = %i[path slug attributes errors].freeze PRIVATE_ATTRIBUTES = %i[path slug attributes errors].freeze
PUBLIC_ATTRIBUTES = %i[lang date uuid].freeze PUBLIC_ATTRIBUTES = %i[lang date uuid created_at].freeze
ATTR_SUFFIXES = %w[? =].freeze ATTR_SUFFIXES = %w[? =].freeze
attr_reader :attributes, :errors, :layout, :site, :document attr_reader :attributes, :errors, :layout, :site, :document
@ -217,6 +217,11 @@ class Post
post: self, required: true) post: self, required: true)
end end
# La fecha de creación inmodificable del post
def created_at
@metadata[:created_at] ||= MetadataCreatedAt.new(document: document, site: site, layout: layout, name: :created_at, type: :created_at, post: self, required: true)
end
# Detecta si es un atributo válido o no, a partir de la tabla de la # Detecta si es un atributo válido o no, a partir de la tabla de la
# plantilla # plantilla
def attribute?(mid) def attribute?(mid)
@ -267,6 +272,7 @@ class Post
# Y que no se procese liquid # Y que no se procese liquid
yaml['liquid'] = false yaml['liquid'] = false
yaml['usuaries'] = usuaries.map(&:id).uniq yaml['usuaries'] = usuaries.map(&:id).uniq
yaml['created_at'] = created_at.value
yaml['last_modified_at'] = modified_at yaml['last_modified_at'] = modified_at
"#{yaml.to_yaml}---\n\n#{body}" "#{yaml.to_yaml}---\n\n#{body}"

View file

@ -14,6 +14,8 @@ class Rol < ApplicationRecord
validates_inclusion_of :rol, in: ROLES validates_inclusion_of :rol, in: ROLES
before_save :add_token_if_missing!
def invitade? def invitade?
rol == INVITADE rol == INVITADE
end end
@ -25,4 +27,11 @@ class Rol < ApplicationRecord
def self.role?(rol) def self.role?(rol)
ROLES.include? rol ROLES.include? rol
end end
private
# Asegurarse que tenga un token
def add_token_if_missing!
self.token ||= SecureRandom.hex(64)
end
end end

View file

@ -9,6 +9,8 @@ class Site < ApplicationRecord
include Site::Api include Site::Api
include Site::DeployDependencies include Site::DeployDependencies
include Site::BuildStats include Site::BuildStats
include Site::LayoutOrdering
include Site::SocialDistributedPress
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,10 +19,6 @@ class Site < ApplicationRecord
# protege de acceso al panel de Sutty! # protege de acceso al panel de Sutty!
encrypts :private_key encrypts :private_key
# TODO: Hacer que los diferentes tipos de deploy se auto registren
# @see app/services/site_service.rb
DEPLOYS = %i[local private www zip hidden_service distributed_press].freeze
validates :name, uniqueness: true, hostname: { validates :name, uniqueness: true, hostname: {
allow_root_label: true allow_root_label: true
} }
@ -446,6 +444,10 @@ class Site < ApplicationRecord
find_by(name: "#{Site.domain}.") find_by(name: "#{Site.domain}.")
end end
def self.one_at_a_time
@@one_at_a_time ||= Thread::Mutex.new
end
def reset def reset
@read = false @read = false
@layouts = nil @layouts = nil
@ -472,7 +474,10 @@ class Site < ApplicationRecord
def clone_skel! def clone_skel!
return if jekyll? return if jekyll?
Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path Rugged::Repository.clone_at(ENV['SKEL_SUTTY'], path, checkout_branch: design.gem)
# Necesita un bloque
repository.rugged.remotes.rename('origin', 'upstream') {}
end end
# Elimina el directorio del sitio # Elimina el directorio del sitio
@ -496,8 +501,8 @@ class Site < ApplicationRecord
config.theme = design.gem unless design.no_theme? config.theme = design.gem unless design.no_theme?
config.description = description config.description = description
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) config.locales = locales.map(&:to_s)
end end
@ -552,7 +557,9 @@ class Site < ApplicationRecord
end end
def run_in_path(&block) def run_in_path(&block)
Dir.chdir path, &block Site.one_at_a_time.synchronize do
Dir.chdir path, &block
end
end end
# Instala las gemas cuando es necesario: # Instala las gemas cuando es necesario:
@ -564,6 +571,8 @@ class Site < ApplicationRecord
def install_gems def install_gems
return unless persisted? return unless persisted?
deploys.find_by_type('DeployLocal').send(:git_lfs)
if !gem_dir? || gemfile_updated? || gemfile_lock_updated? if !gem_dir? || gemfile_updated? || gemfile_lock_updated?
deploys.find_by_type('DeployLocal').send(:bundle) deploys.find_by_type('DeployLocal').send(:bundle)
touch touch

View file

@ -40,6 +40,72 @@ class Site
def not_published_yet? def not_published_yet?
build_stats.jekyll.where(status: true).count.zero? build_stats.jekyll.where(status: true).count.zero?
end 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 end
end end

View file

@ -37,7 +37,7 @@ class Site
author = GitAuthor.new email: "sutty@#{Site.domain}", name: 'Sutty' author = GitAuthor.new email: "sutty@#{Site.domain}", name: 'Sutty'
repository.commit(file: modified, repository.commit(add: modified,
message: I18n.t('sites.find_and_replace'), message: I18n.t('sites.find_and_replace'),
usuarie: author) usuarie: author)
end end

View file

@ -1,22 +1,125 @@
# frozen_string_literal: true # frozen_string_literal: true
# Indexa todos los artículos de un sitio
#
# TODO: Hacer opcional
class Site class Site
# Indexa todos los artículos de un sitio
#
# TODO: Hacer opcional
module Index module Index
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
# TODO: Debería ser un Job?
after_create :index_posts!
has_many :indexed_posts, dependent: :destroy has_many :indexed_posts, dependent: :destroy
MODIFIED_STATUSES = %i[added modified].freeze
DELETED_STATUSES = %i[deleted].freeze
LOCALE_FROM_PATH = /\A_/.freeze
def index_posts! def index_posts!
Site.transaction do Site.transaction do
docs.each(&:index!) docs.each(&:index!)
update(last_indexed_commit: repository.head_commit.oid)
end end
end end
# Encuentra los artículos modificados entre dos commits y los
# reindexa.
def reindex_changes!
return unless reindexable?
Site.transaction do
remove_deleted_posts!
reindex_modified_posts!
update(last_indexed_commit: repository.head_commit.oid)
end
end
# No hacer nada si el repositorio no cambió o no hubo cambios
# necesarios
def reindexable?
return false if last_indexed_commit.blank?
return false if last_indexed_commit == repository.head_commit.oid
!indexable_posts.empty?
end
private
# Trae el último commit indexado desde el repositorio
#
# @return [Rugged::Commit]
def indexed_commit
@indexed_commit ||= repository.rugged.lookup(last_indexed_commit)
end
# Calcula la diferencia entre el último commit indexado y el
# actual
#
# XXX: Esto no tiene en cuenta modificaciones en la historia como
# cambio de ramas, reverts y etc, solo asume que se mueve hacia
# adelante en la misma rama o las dos ramas están relacionadas.
#
# @return [Rugged::Diff]
def diff_with_head
@diff_with_head ||= indexed_commit.diff(repository.head_commit)
end
# Obtiene todos los archivos a reindexar
#
# @return [Array<Rugged::Delta>]
def indexable_posts
@indexable_posts ||=
diff_with_head.each_delta.select do |delta|
locales.any? do |locale|
delta.old_file[:path].start_with? "_#{locale}/"
end
end
end
# Elimina los artículos eliminados o que cambiaron de ubicación
# del índice
def remove_deleted_posts!
indexable_posts.select do |delta|
DELETED_STATUSES.include? delta.status
end.each do |delta|
locale, path = locale_and_path_from(delta.old_file[:path])
indexed_posts.destroy_by(locale: locale, path: path).tap do |destroyed_posts|
next unless destroyed_posts.empty?
Rails.logger.info I18n.t('indexed_posts.deleted', site: name, path: path, records: destroyed_posts.count)
end
end
end
# Reindexa artículos que cambiaron de ubicación, se agregaron
# o fueron modificados
def reindex_modified_posts!
indexable_posts.select do |delta|
MODIFIED_STATUSES.include? delta.status
end.each do |delta|
locale, path = locale_and_path_from(delta.new_file[:path])
posts(lang: locale).find(path).index!
end
end
# Obtiene el idioma y la ruta del post a partir de la ubicación en
# el disco.
#
# Las rutas vienen en ASCII-9BIT desde Rugged, pero en realidad
# son UTF-8
#
# @return [Array<String>]
def locale_and_path_from(path)
locale, path = path.force_encoding('utf-8').split(File::SEPARATOR, 2)
[
locale.sub(LOCALE_FROM_PATH, ''),
File.basename(path, '.*')
]
end
end end
end end
end end

View 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

View file

@ -29,7 +29,7 @@ class Site
# Obtiene el origin # Obtiene el origin
# #
# @return [Rugged::Remote] # @return [Rugged::Remote, nil]
def origin def origin
@origin ||= rugged.remotes.find do |remote| @origin ||= rugged.remotes.find do |remote|
remote.name == 'origin' remote.name == 'origin'
@ -54,7 +54,7 @@ class Site
# Incorpora los cambios en el repositorio actual # Incorpora los cambios en el repositorio actual
# #
# @return [Rugged::Commit] # @return [Rugged::Commit]
def merge(usuarie) def merge(usuarie, message = I18n.t('sites.fetch.merge.message'))
merge = rugged.merge_commits(head_commit, remote_head_commit) merge = rugged.merge_commits(head_commit, remote_head_commit)
# No hacemos nada si hay conflictos, pero notificarnos # No hacemos nada si hay conflictos, pero notificarnos
@ -69,12 +69,16 @@ class Site
.create(rugged, update_ref: 'HEAD', .create(rugged, update_ref: 'HEAD',
parents: [head_commit, remote_head_commit], parents: [head_commit, remote_head_commit],
tree: merge.write_tree(rugged), tree: merge.write_tree(rugged),
message: I18n.t('sites.fetch.merge.message'), message: message,
author: author(usuarie), committer: committer) author: author(usuarie), committer: committer)
# Forzamos el checkout para mover el HEAD al último commit y # Forzamos el checkout para mover el HEAD al último commit y
# escribir los cambios # escribir los cambios
rugged.checkout 'HEAD', strategy: :force rugged.checkout 'HEAD', strategy: :force
git_sh("git", "lfs", "fetch", "origin", default_branch)
# reemplaza los pointers por los archivos correspondientes
git_sh("git", "lfs", "checkout")
commit commit
end end
@ -114,14 +118,21 @@ class Site
end end
# Guarda los cambios en git # Guarda los cambios en git
def commit(file:, usuarie:, message:, remove: false) #
file = [file] unless file.respond_to? :each # @param :add [Array] Archivos a agregar
# @param :rm [Array] Archivos a eliminar
# @param :usuarie [Usuarie] Quién hace el commit
# @param :message [String] Mensaje
def commit(add: [], rm: [], usuarie:, message:)
# Cargar el árbol actual # Cargar el árbol actual
rugged.index.read_tree rugged.head.target.tree rugged.index.read_tree rugged.head.target.tree
file.each do |f| add.each do |file|
remove ? rm(f) : add(f) rugged.index.add(relativize(file))
end
rm.each do |file|
rugged.index.remove(relativize(file))
end end
# Escribir los cambios para que el repositorio se vea tal cual # Escribir los cambios para que el repositorio se vea tal cual
@ -142,41 +153,58 @@ class Site
{ name: 'Sutty', email: "sutty@#{Site.domain}", time: Time.now } { name: 'Sutty', email: "sutty@#{Site.domain}", time: Time.now }
end end
def add(file)
rugged.index.add(relativize(file))
end
def rm(file)
rugged.index.remove(relativize(file))
end
# Garbage collection # Garbage collection
# #
# @return [Boolean] # @return [Boolean]
def gc def gc
env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path } git_sh("git", "gc")
cmd = 'git gc' end
r = nil # Pushea cambios al repositorio remoto
Dir.chdir(path) do #
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, _, t| # @param :remote [Rugged::Remote]
r = t.value # @return [Boolean, nil]
end def push(remote = origin)
end remote.push(rugged.head.canonical_name, credentials: credentials_for(remote))
git_sh('git', 'lfs', 'push', remote.name, default_branch)
r&.success?
end end
private private
# @deprecated
def credentials
@credentials ||= credentials_for(origin)
end
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las # Si Sutty tiene una llave privada de tipo ED25519, devuelve las
# credenciales necesarias para trabajar con repositorios remotos. # credenciales necesarias para trabajar con repositorios remotos.
# #
# @param :remote [Rugged::Remote]
# @return [Nil, Rugged::Credentials::SshKey] # @return [Nil, Rugged::Credentials::SshKey]
def credentials def credentials_for(remote)
return unless File.exist? private_key return unless File.exist? private_key
@credentials ||= Rugged::Credentials::SshKey.new username: 'git', publickey: public_key, privatekey: private_key Rugged::Credentials::SshKey.new username: username_for(remote), publickey: public_key, privatekey: private_key
end
# Obtiene el nombre de usuario para el repositorio remoto, por
# defecto git
#
# @param :remote [Rugged::Remote]
# @return [String]
def username_for(remote)
username = parse_url(remote.url)&.user if remote.respond_to? :url
username || 'git'
end
# @param :url [String]
# @return [URI, nil]
def parse_url(url)
GitCloneUrl.parse(url)
rescue URI::Error => e
ExceptionNotifier.notify_exception(e, data: { path: path, url: url })
nil
end end
# @return [String] # @return [String]
@ -192,5 +220,20 @@ class Site
def relativize(file) def relativize(file)
Pathname.new(file).relative_path_from(Pathname.new(path)).to_s Pathname.new(file).relative_path_from(Pathname.new(path)).to_s
end end
# Ejecuta un comando de git
#
# @param :args [Array]
# @return [Boolean]
def git_sh(*args)
env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path }
r = nil
Open3.popen2e(env, *args, unsetenv_others: true, chdir: path) do |_, _, t|
r = t.value
end
r&.success?
end
end end
end end

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class Site
# Agrega soporte para Social Distributed Press en los sitios
module SocialDistributedPress
extend ActiveSupport::Concern
included do
encrypts :private_key_pem
before_save :generate_private_key_pem!, unless: :private_key_pem?
private
# Genera la llave privada y la almacena
#
# @return [nil]
def generate_private_key_pem!
self.private_key_pem ||= DistributedPress::V1::Social::Client.new(public_key_url: nil, key_size: 2048).private_key.export
end
end
end
end

View file

@ -0,0 +1,3 @@
# frozen_string_literal: true
SiteBuildStat = Struct.new(:site)

View file

@ -10,8 +10,11 @@ class Usuarie < ApplicationRecord
validates_uniqueness_of :email validates_uniqueness_of :email
validates_with EmailAddress::ActiveRecordValidator, field: :email validates_with EmailAddress::ActiveRecordValidator, field: :email
validate :locale_available!
before_create :lang_from_locale! 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
@ -49,9 +52,42 @@ class Usuarie < ApplicationRecord
end end
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 private
def lang_from_locale! def lang_from_locale!
self.lang = I18n.locale.to_s self.lang = I18n.locale.to_s
end 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
# Muestra un error si el idioma no está disponible al cambiar el
# idioma de la cuenta.
#
# @return [nil]
def locale_available!
return if I18n.locale_available? self.lang
errors.add(:lang, I18n.t('activerecord.errors.models.usuarie.attributes.lang.not_available'))
nil
end
end end

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
# Política de acceso a artículos
class IndexedPostPolicy
attr_reader :indexed_post, :usuarie, :site
def initialize(usuarie, indexed_post)
@usuarie = usuarie
@indexed_post = indexed_post
@site = indexed_post.site
end
def index?
true
end
# Les invitades solo pueden ver sus propios posts
def show?
site.usuarie?(usuarie) || site.indexed_posts.by_usuarie(usuarie.id).find_by_post_id(indexed_post.post_id).present?
end
def preview?
show?
end
def new?
create?
end
def create?
true
end
def edit?
update?
end
# Les invitades solo pueden modificar sus propios artículos
def update?
show?
end
# Solo las usuarias pueden eliminar artículos. Les invitades pueden
# borrar sus propios artículos
def destroy?
update?
end
# Las usuarias pueden ver todos los posts
#
# Les invitades solo pueden ver sus propios posts
class Scope
attr_reader :usuarie, :scope
def initialize(usuarie, scope)
@usuarie = usuarie
@scope = scope
end
def resolve
return scope if scope&.first&.site&.usuarie? usuarie
scope.by_usuarie(usuarie.id)
end
end
end

View 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

View file

@ -23,9 +23,11 @@ class CleanupService
# #
# @return [nil] # @return [nil]
def cleanup_older_sites! def cleanup_older_sites!
Site.where('updated_at < ?', before).find_each do |site| Site.where('updated_at < ?', before).order(updated_at: :desc).find_each do |site|
next unless File.directory? site.path next unless File.directory? site.path
Rails.logger.info "Limpiando dependencias, archivos temporales y repositorio git de #{site.name}"
site.deploys.find_each(&:cleanup!) site.deploys.find_each(&:cleanup!)
site.repository.gc site.repository.gc
@ -37,9 +39,11 @@ class CleanupService
# #
# @return [nil] # @return [nil]
def cleanup_newer_sites! def cleanup_newer_sites!
Site.where('updated_at >= ?', before).find_each do |site| Site.where('updated_at >= ?', before).order(updated_at: :desc).find_each do |site|
next unless File.directory? site.path next unless File.directory? site.path
Rails.logger.info "Limpiando repositorio git de #{site.name}"
site.repository.gc site.repository.gc
site.touch site.touch
end end

View file

@ -22,7 +22,7 @@ class LfsObjectService
Site::Writer.new(site: site, file: path, content: pointer).save Site::Writer.new(site: site, file: path, content: pointer).save
# Commitear el pointer # Commitear el pointer
site.repository.commit(file: path, usuarie: author, message: File.basename(path)) site.repository.commit(add: [path], usuarie: author, message: File.basename(path))
# Eliminar el pointer # Eliminar el pointer
FileUtils.rm(path) FileUtils.rm(path)

View file

@ -16,7 +16,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
post.slug.value = p[:slug] if p[:slug].present? post.slug.value = p[:slug] if p[:slug].present?
end end
commit(action: :created, file: update_related_posts) if post.update(post_params) commit(action: :created, add: update_related_posts) if post.update(post_params)
update_site_license! update_site_license!
@ -34,7 +34,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
# Los artículos anónimos siempre son borradores # Los artículos anónimos siempre son borradores
params[:draft] = true params[:draft] = true
commit(action: :created) if post.update(anon_post_params) commit(action: :created, add: [post.path.absolute]) if post.update(anon_post_params)
post post
end end
@ -42,11 +42,17 @@ 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
# Es importante que el artículo se guarde primero y luego los # Eliminar ("mover") el archivo si cambió de ubicación.
# relacionados. if post.update(post_params)
commit(action: :updated, file: update_related_posts) if post.update(post_params) rm = []
rm << post.path.value_was if post.path.changed?
update_site_license! # Es importante que el artículo se guarde primero y luego los
# relacionados.
commit(action: :updated, add: update_related_posts, rm: rm)
update_site_license!
end
# 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
@ -56,7 +62,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
def destroy def destroy
post.destroy! post.destroy!
commit(action: :destroyed) if post.destroyed? commit(action: :destroyed, rm: [post.path.absolute]) if post.destroyed?
post post
end end
@ -85,17 +91,19 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
# TODO: Implementar transacciones! # TODO: Implementar transacciones!
posts.save_all(validate: false) && posts.save_all(validate: false) &&
commit(action: :reorder, file: files) commit(action: :reorder, add: files)
end end
private private
def commit(action:, file: nil) def commit(action:, add: [], rm: [])
site.repository.commit(file: file || post.path.absolute, site.repository.commit(add: add,
rm: rm,
usuarie: usuarie, usuarie: usuarie,
remove: action == :destroyed,
message: I18n.t("post_service.#{action}", message: I18n.t("post_service.#{action}",
title: post&.title&.value)) title: post&.title&.value))
GitPushJob.perform_later(site)
end end
# Solo permitir cambiar estos atributos de cada articulo # Solo permitir cambiar estos atributos de cada articulo

View file

@ -5,7 +5,7 @@
SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
def deploy def deploy
site.enqueue! site.enqueue!
DeployJob.perform_async site.id DeployJob.perform_later site.id
end 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
@ -15,7 +15,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
add_role temporal: false, rol: 'usuarie' add_role temporal: false, rol: 'usuarie'
site.deploys.build type: 'DeployLocal' site.deploys.build type: 'DeployLocal'
sync_nodes # 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 I18n.with_locale(usuarie.lang.to_sym || I18n.default_locale) do
# No se puede llamar a site.config antes de save porque el sitio # No se puede llamar a site.config antes de save porque el sitio
@ -32,6 +33,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
add_licencias && add_licencias &&
add_code_of_conduct && add_code_of_conduct &&
add_privacy_policy && add_privacy_policy &&
site.index_posts! &&
deploy deploy
end end
@ -53,9 +55,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
# Genera los Deploy necesarios para el sitio a menos que ya los tenga. # Genera los Deploy necesarios para el sitio a menos que ya los tenga.
def build_deploys def build_deploys
Site::DEPLOYS.map { |deploy| "Deploy#{deploy.to_s.camelcase}" } Deploy.subclasses.each do |deploy|
.each do |deploy| next if site.deploys.find_by type: deploy.name
next if site.deploys.find_by type: deploy
site.deploys.build type: deploy site.deploys.build type: deploy
end end
@ -93,9 +94,11 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
def commit_config(action:) def commit_config(action:)
site.repository site.repository
.commit(usuarie: usuarie, .commit(usuarie: usuarie,
file: site.config.path, add: [site.config.path],
message: I18n.t("site_service.#{action}", message: I18n.t("site_service.#{action}",
name: site.name)) name: site.name))
GitPushJob.perform_later(site)
end end
def add_role(temporal: true, rol: 'invitade') def add_role(temporal: true, rol: 'invitade')
@ -215,7 +218,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
# 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: 'DeployFullRsync', destination: "sutty@#{node}:") site.deploys.build(type: 'DeployFullRsync', destination: "rsync://rsyncd.#{node}/deploys/", hostname: node)
end end
end end

View 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.scheme.present?), url.to_s, url.to_s, class: 'word-break-all'
%td
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
%td= row[:size]

View file

@ -13,7 +13,7 @@
%tr %tr
%td= row[:title] %td= row[:title]
%td= row[:status] %td= row[:status]
%td= link_to_if url.present?, url, url %td= link_to_if (url.present? && url.scheme.present?), url.to_s, url.to_s
%td %td
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human] %time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
%td= row[:size] %td= row[:size]

View 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'),
tags: %w[p strong em a]
%hr/

View file

@ -8,7 +8,7 @@
%h1= site.title %h1= site.title
%p= site.description %p= site.description
- if @resource.created_by_invite? && !@resource.invitation_accepted? - if @resource.needs_invitation_link?
%p= link_to t('devise.mailer.invitation_instructions.accept'), %p= link_to t('devise.mailer.invitation_instructions.accept'),
accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang) accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang)
@ -18,5 +18,7 @@
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 - else
%p= link_to t('devise.mailer.invitation_instructions.sign_in'), root_url %p= link_to t('devise.mailer.invitation_instructions.sign_in'), root_url

View file

@ -9,7 +9,7 @@
\ \
= site.description = site.description
\ \
- if @resource.created_by_invite? && !@resource.invitation_accepted? - if @resource.needs_invitation_link?
= accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang) = accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang)
\ \
- if @resource.invitation_due_at - if @resource.invitation_due_at
@ -18,6 +18,8 @@
format: :'devise.mailer.invitation_instructions.accept_until_format')) 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 - else
= root_url(change_locale_to: @resource.lang) = root_url(change_locale_to: @resource.lang)
= t('devise.mailer.invitation_instructions.sign_in') = t('devise.mailer.invitation_instructions.sign_in')

View file

@ -1,7 +1,8 @@
= cache @site do - if @site
:plain = cache @site do
window.env = { :plain
AIRBRAKE_SITE_ID: #{@site.id}, window.env = {
AIRBRAKE_API_KEY: "#{@site.airbrake_api_key}", AIRBRAKE_SITE_ID: #{@site.id},
PANEL_URL: "#{ENV['PANEL_URL']}" AIRBRAKE_API_KEY: "#{@site.airbrake_api_key}",
} PANEL_URL: "#{ENV['PANEL_URL']}"
}

View file

@ -19,6 +19,10 @@
= link_to t('.tienda'), @site.tienda_url, = link_to t('.tienda'), @site.tienda_url,
role: 'button', class: 'btn' role: 'button', class: 'btn'
%li.nav-item
= link_to t('.contact_us'), t('.contact_us_href'),
class: 'btn', rel: 'me', target: '_blank'
%li.nav-item %li.nav-item
= link_to t('.logout'), main_app.destroy_usuarie_session_path, = link_to t('.logout'), main_app.destroy_usuarie_session_path,
method: :delete, role: 'button', class: 'btn' method: :delete, role: 'button', class: 'btn'
@ -26,4 +30,4 @@
- params.permit! - params.permit!
- I18n.available_locales.each do |locale| - I18n.available_locales.each do |locale|
- next if locale == I18n.locale - next if locale == I18n.locale
= link_to t(locale), params.to_h.merge(change_locale_to: locale) = link_to t("switch_locale.#{locale}"), params.to_h.merge(change_locale_to: locale)

View file

@ -1,4 +1,4 @@
- flash.each do |type, message| - flash.each do |type, message|
- unless type == 'js' - unless type == 'js'
= render 'bootstrap/alert' do = render 'bootstrap/alert' do
= message = sanitize_markdown message, tags: %w[a strong em]

View file

@ -0,0 +1,7 @@
- unless current_usuarie
- params.permit!
- I18n.available_locales.each do |locale|
- url = url_for(**params.to_h.merge(change_locale_to: locale), only_path: false)
- if locale == I18n.default_locale
%link{ rel: 'alternate', hreflang: 'x-default', href: url }
%link{ rel: 'alternate', hreflang: locale, href: url }

View file

@ -4,7 +4,7 @@
%meta{ charset: 'UTF-8' }/ %meta{ charset: 'UTF-8' }/
%meta{ content: 'text/html; charset=UTF-8', %meta{ content: 'text/html; charset=UTF-8',
'http-equiv': 'Content-Type' }/ 'http-equiv': 'Content-Type' }/
%meta{ name: 'color-scheme', content: 'light dark' }/ %meta{ name: 'color-scheme', content: 'light' }/
%meta{ name: 'viewport', %meta{ name: 'viewport',
content: 'width=device-width, initial-scale=1.0' }/ content: 'width=device-width, initial-scale=1.0' }/
%meta{ name: 'referrer', content: 'same-origin' }/ %meta{ name: 'referrer', content: 'same-origin' }/
@ -18,6 +18,7 @@
= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' = stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload'
= favicon_link_tag 'sutty_cuadrada.png', rel: 'apple-touch-icon', type: 'image/png' = favicon_link_tag 'sutty_cuadrada.png', rel: 'apple-touch-icon', type: 'image/png'
= render 'layouts/link_rel_alternate'
%body{ class: yield(:body) } %body{ class: yield(:body) }
.container-fluid#sutty .container-fluid#sutty

View file

@ -1,6 +1,8 @@
- invalid_help = site.config.fetch('invalid_help', t('.invalid_help'))
- sending_help = site.config.fetch('sending_help', t('.sending_help'))
.form-group .form-group
= submit_tag t('.save'), class: 'btn submit-post' = submit_tag t('.save'), class: 'btn submit-post'
= render 'bootstrap/alert', class: 'invalid-help d-none' do = render 'bootstrap/alert', class: 'invalid-help d-none' do
= site.config.fetch('invalid_help', t('.invalid_help')) = invalid_help
= render 'bootstrap/alert', class: 'sending-help d-none' do = render 'bootstrap/alert', class: 'sending-help d-none' do
= site.config.fetch('sending_help', t('.sending_help')) = sending_help

View file

@ -0,0 +1,4 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td{ dir: dir, lang: locale }
%time{ datetime: metadata.value.xmlschema }= l metadata.value

View file

@ -0,0 +1 @@
-# nada

View file

@ -22,7 +22,7 @@
= file_field(*field_name_for(base, attribute, :path), = file_field(*field_name_for(base, attribute, :path),
**field_options(attribute, metadata, required: (metadata.required && !metadata.path?)), **field_options(attribute, metadata, required: (metadata.required && !metadata.path?)),
class: "custom-file-input #{invalid(post, attribute)}", class: "custom-file-input #{invalid(post, attribute)}",
accept: 'image/*', data: { preview: "#{attribute}-preview" }) accept: ActiveStorage.web_image_content_types.join(','), data: { preview: "#{attribute}-preview" })
= label_tag "#{base}_#{attribute}_path", = label_tag "#{base}_#{attribute}_path",
post_label_t(attribute, :path, post: post), class: 'custom-file-label' post_label_t(attribute, :path, post: post), class: 'custom-file-label'
= render 'posts/attribute_feedback', = render 'posts/attribute_feedback',

View file

@ -1,21 +1,18 @@
%main.row %main.row
%aside.menu.col-md-3 %aside.menu.col-md-3
%h1= @site.title = render 'sites/header', site: @site
%p.lead= @site.description
- cache_if @usuarie, [@site, I18n.locale] do = render 'sites/status', site: @site
= render 'sites/status', site: @site
= render 'sites/build', site: @site, class: 'btn-block'
%h3= t('posts.new') %h3= t('posts.new')
%table.mb-3 %table.table.table-sm.mb-3
- @site.layouts.sort_by(&:humanized_name).each do |layout| %tbody
- next if layout.hidden? - @site.schema_organization.each do |schema, _|
%tr - schema = @site.layouts[schema]
%th= layout.humanized_name - next if schema.hidden?
%td.pl-3= link_to t('posts.add'), new_site_post_path(@site, layout: layout.value), class: 'btn btn-secondary btn-sm' = render 'schemas/row', site: @site, schema: schema, filter: @filter_params
- if @filter_params[:layout] == layout.name.to_s
%td= link_to t('posts.remove_filter'), site_posts_path(@site, **@filter_params.merge(layout: nil)), class: 'btn btn-primary btn-sm'
- else
%td= link_to t('posts.filter'), site_posts_path(@site, **@filter_params.merge(layout: layout.value)), class: 'btn btn-secondary btn-sm'
- if policy(@site_stat).index? - if policy(@site_stat).index?
= link_to t('stats.index.title'), site_stats_path(@site), class: 'btn' = link_to t('stats.index.title'), site_stats_path(@site), class: 'btn'
@ -33,8 +30,6 @@
type: 'info', type: 'info',
link: site_usuaries_path(@site) link: site_usuaries_path(@site)
= render 'sites/build', site: @site
- if @site.design.credits - if @site.design.credits
= render 'bootstrap/alert' do = render 'bootstrap/alert' do
= sanitize_markdown @site.design.credits = sanitize_markdown @site.design.credits
@ -49,7 +44,8 @@
- next if param == 'q' - next if param == 'q'
%input{ type: 'hidden', name: param, value: value } %input{ type: 'hidden', name: param, value: value }
.form-group.flex-grow-0.m-0 .form-group.flex-grow-0.m-0
%input.form-control.border.border-magenta{ type: 'search', placeholder: 'Buscar', name: 'q', value: @filter_params[:q] } %label.sr-only{for: 'q'}= t('.search')
%input#q.form-control.border.border-magenta{ type: 'search', placeholder: t('.search'), name: 'q', value: @filter_params[:q] }
%input.sr-only{ type: 'submit' } %input.sr-only{ type: 'submit' }
- if @site.locales.size > 1 - if @site.locales.size > 1
@ -88,7 +84,10 @@
%button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top') %button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom') %button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
%div - if @site.pagination
%div
= link_to_prev_page @posts, t('posts.prev'), class: 'btn'
= link_to_next_page @posts, t('posts.next'), class: 'btn'
%tbody %tbody
- dir = @site.data.dig(params[:locale], 'dir') - dir = @site.data.dig(params[:locale], 'dir')
- size = @posts.size - size = @posts.size

View file

@ -0,0 +1 @@
= link_to t('.add'), new_site_post_path(site, layout: schema.value), class: 'btn btn-secondary btn-sm m-0'

View file

@ -0,0 +1,4 @@
- if filter[:layout] == schema.name.to_s
= link_to t('.remove'), site_posts_path(site, **filter.merge(layout: nil)), class: 'btn btn-primary btn-sm m-0'
- else
= link_to t('.filter'), site_posts_path(site, **filter.merge(layout: schema.value)), class: 'btn btn-secondary btn-sm m-0'

View file

@ -0,0 +1,13 @@
%tr
%th.w-100{ scope: 'row' }
- if local_assigns[:parent_schema]
%span.text-muted &mdash;
= schema.humanized_name
%td.px-0.text-nowrap
= render 'schemas/add', **local_assigns
= render 'schemas/filter', **local_assigns
-# XXX: Solo un nivel de recursividad
- unless local_assigns[:parent_schema]
- schema.schemas.each do |s|
= render 'schemas/row', schema: s, site: site, filter: filter, parent_schema: schema

View file

@ -3,7 +3,7 @@
method: :post, method: :post,
class: 'form-inline inline' do class: 'form-inline inline' do
= submit_tag site.enqueued? ? t('sites.enqueued') : t('sites.enqueue'), = submit_tag site.enqueued? ? t('sites.enqueued') : t('sites.enqueue'),
class: 'btn no-border-radius', class: "btn no-border-radius #{local_assigns[:class]}",
title: site.enqueued? ? t('help.sites.enqueued') : t('help.sites.enqueue'), title: site.enqueued? ? t('help.sites.enqueued') : t('help.sites.enqueue'),
data: { disable_with: t('sites.enqueued') }, data: { disable_with: t('sites.enqueued') },
disabled: site.enqueued? disabled: site.enqueued?

View file

@ -46,36 +46,37 @@
.invalid-feedback= site.errors.messages[:description].join(', ') .invalid-feedback= site.errors.messages[:description].join(', ')
%hr/ %hr/
.form-group#design_id - unless site.persisted?
%h2= t('.design.title') .form-group#design_id
%p.lead= t('.help.design') %h2= t('.design.title')
- if invalid? site, :design_id %p.lead= t('.help.design')
= render 'bootstrap/alert' do - if invalid? site, :design_id
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help', = render 'bootstrap/alert' do
layouts: site.incompatible_layouts.to_sentence) = t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
.row.row-cols-1.row-cols-md-2.designs layouts: site.incompatible_layouts.to_sentence)
-# Demasiado complejo para un f.collection_radio_buttons .row.row-cols-1.row-cols-md-2.designs
- Design.all.find_each do |design| -# Demasiado complejo para un f.collection_radio_buttons
.design.col.d-flex.flex-column - Design.all.order(priority: :desc).each do |design|
.custom-control.custom-radio .design.col.d-flex.flex-column
= f.radio_button :design_id, design.id, .custom-control.custom-radio
checked: design.id == site.design_id, = f.radio_button :design_id, design.id,
disabled: design.disabled, checked: design.id == site.design_id,
required: true, class: 'custom-control-input' disabled: design.disabled,
= f.label "design_id_#{design.id}", design.name, required: true, class: 'custom-control-input'
class: 'custom-control-label' = f.label "design_id_#{design.id}", design.name,
.flex-fill class: 'custom-control-label'
= sanitize_markdown design.description, .flex-fill
tags: %w[p a strong em] = sanitize_markdown design.description,
tags: %w[p a strong em]
.btn-group{ role: 'group', 'aria-label': t('.design.actions') } .btn-group{ role: 'group', 'aria-label': t('.design.actions') }
- if design.url - if design.url
= link_to t('.design.url'), design.url, = link_to t('.design.url'), design.url,
target: '_blank', class: 'btn' target: '_blank', class: 'btn'
- if design.license - if design.license
= link_to t('.design.license'), design.license, = link_to t('.design.license'), design.license,
target: '_blank', class: 'btn' target: '_blank', class: 'btn'
%hr/ %hr/
.form-group.licenses#license_id .form-group.licenses#license_id
%h2= t('.licencia.title') %h2= t('.licencia.title')

View file

@ -0,0 +1,3 @@
.hyphens{ lang: site.default_locale }
%h1= site.title
%p.lead= site.description

View file

@ -1,7 +1,9 @@
- link = nil - link = nil
- if site.not_published_yet? - if site.not_published_yet?
- message = t('.not_published_yet') - message = t('.not_published_yet')
- if site.building? - elsif site.awaiting_publication?
- message = t('.awaiting_publication')
- elsif site.building?
- if site.average_publication_time_calculable? - if site.average_publication_time_calculable?
- average_building_time = site.average_publication_time - average_building_time = site.average_publication_time
- elsif !site.similar_sites? - elsif !site.similar_sites?
@ -16,4 +18,4 @@
- link = true - link = true
= render 'bootstrap/alert' do = render 'bootstrap/alert' do
= link_to_if link, message.html_safe, site.url, class: 'alert-link' = link_to_if link, message.html_safe, site_build_stats_path(site), class: 'alert-link'

View file

@ -54,10 +54,4 @@
text: t('usuaries.index.title'), text: t('usuaries.index.title'),
type: 'info', type: 'info',
link: site_usuaries_path(site) link: site_usuaries_path(site)
- if policy(site).pull? && site.repository.needs_pull?
= render 'layouts/btn_with_tooltip',
tooltip: t('help.sites.pull'),
text: t('.pull'),
type: 'info',
link: site_pull_path(site)
= render 'sites/build', site: site = render 'sites/build', site: site

View file

@ -37,6 +37,15 @@ module Sutty
.rescue_responses['Pundit::NotAuthorizedError'] = :forbidden .rescue_responses['Pundit::NotAuthorizedError'] = :forbidden
config.active_storage.variant_processor = :vips config.active_storage.variant_processor = :vips
config.active_storage.web_image_content_types << 'image/webp'
# Que
config.action_mailer.deliver_later_queue_name = :default
config.active_storage.queues.analysis = :default
config.active_storage.queues.purge = :default
config.active_job.queue_adapter = :que
config.active_record.schema_format = :sql
config.to_prepare do config.to_prepare do
# Load application's model / class decorators # Load application's model / class decorators

View file

@ -64,11 +64,6 @@ Rails.application.configure do
# Use a different cache store in production. # Use a different cache store in production.
config.cache_store = :redis_cache_store, { url: ENV['REDIS_SERVER'] } config.cache_store = :redis_cache_store, { url: ENV['REDIS_SERVER'] }
# Use a real queuing backend for Active Job (and separate queues per
# environment)
config.active_job.queue_adapter = :sucker_punch
config.active_job.queue_name_prefix = "sutty_#{Rails.env}"
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors. # Ignore bad email addresses and do not raise email delivery errors.
@ -147,7 +142,7 @@ Rails.application.configure do
} }
config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', "noreply@sutty.nl") } config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', "noreply@sutty.nl") }
config.middleware.use ExceptionNotification::Rack, gitlab: {} config.middleware.use ExceptionNotification::Rack, gitlab: {}, ignore_exceptions: ['DeployJob::DeployAlreadyRunningException']
Rails.application.routes.default_url_options[:host] = "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}" Rails.application.routes.default_url_options[:host] = "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}"
Rails.application.routes.default_url_options[:protocol] = 'https' Rails.application.routes.default_url_options[:protocol] = 'https'

View file

@ -91,6 +91,15 @@ module Jekyll
spec.name == name spec.name == name
end end
unless spec
I18n.with_locale(locale) do
raise Jekyll::Errors::InvalidThemeName, I18n.t('activerecord.errors.models.site.attributes.design_id.missing_gem', theme: name)
rescue Jekyll::Errors::InvalidThemeName => e
ExceptionNotifier.notify_exception(e, data: { theme: name, site: File.basename(site.source) })
raise
end
end
ruby_version = Gem::Version.new(RUBY_VERSION) ruby_version = Gem::Version.new(RUBY_VERSION)
ruby_version.canonical_segments[2] = 0 ruby_version.canonical_segments[2] = 0
base_path = Rails.root.join('_storage', 'gems', File.basename(site.source), 'ruby', base_path = Rails.root.join('_storage', 'gems', File.basename(site.source), 'ruby',
@ -114,6 +123,11 @@ module Jekyll
private private
def gemspec; end def gemspec; end
# @return [Symbol]
def locale
@locale ||= (site.config['locale'] || site.config['lang'] || I18n.locale).to_sym
end
end end
# No necesitamos los archivos de la plantilla # No necesitamos los archivos de la plantilla

View file

@ -0,0 +1,3 @@
DeviceDetector.configure do |config|
config.max_cache_keys = 5_000 # to check if not too much
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
ActiveJob::Serializers.add_serializers ActiveJob::Serializers::ExceptionSerializer
# Notificar los errores
Que.error_notifier = proc do |error, job|
ExceptionNotifier.notify_exception(error, data: (job.dup || {}))
end

View file

@ -1,6 +0,0 @@
# frozen_string_literal: true
# Enviar una notificación cuando falla una tarea
SuckerPunch.exception_handler = lambda { |ex, _, args|
ExceptionNotifier.notify_exception(ex, data: args.last)
}

View file

@ -22,7 +22,7 @@ es:
someone_invited_you: "Alguien te ha invitado a colaborar en %{url}, podés aceptar la invitación con el enlace a continuación." someone_invited_you: "Alguien te ha invitado a colaborar en %{url}, podés aceptar la invitación con el enlace a continuación."
accept: "Aceptar la invitación" accept: "Aceptar la invitación"
accept_until: "La invitación vencerá el %{due_date}." accept_until: "La invitación vencerá el %{due_date}."
ignore: "Si no querés aceptar la invitación, por favor ignora este correo. Tu cuenta no será creada hasta que aceptes la invitación y configures una contraseña." ignore: "Si no querés aceptar la invitación, por favor ignorá este correo. Tu cuenta no será creada hasta que aceptes la invitación y configures una contraseña."
sign_in: "Iniciá sesión con tu cuenta para aceptar o rechazar la invitación." sign_in: "Iniciá sesión con tu cuenta para aceptar o rechazar la invitación."
time: time:
formats: formats:

View file

@ -2,8 +2,10 @@ en:
dark: Dark dark: Dark
dir: ltr dir: ltr
en: English en: English
es: Castellano es: Castellano
es-AR: Castellano rioplatense es-AR: Castellano rioplatense
switch_locale:
es: "Cambiar a castellano"
locales: locales:
es: es:
name: Castillian Spanish name: Castillian Spanish
@ -51,7 +53,7 @@ en:
cant_be_empty: 'This field cannot be empty' cant_be_empty: 'This field cannot be empty'
image: image:
site_invalid: 'The image cannot be stored if the site configuration is not valid' site_invalid: 'The image cannot be stored if the site configuration is not valid'
not_an_image: 'Not an image' not_an_image: 'Not a web image. Accepted formats: PNG, JPEG, GIF, WEBP'
path_required: 'Missing image for upload' path_required: 'Missing image for upload'
no_file_for_description: "Description with no associated image" no_file_for_description: "Description with no associated image"
attachment_missing: "I couldn't save the image :(" attachment_missing: "I couldn't save the image :("
@ -122,6 +124,10 @@ en:
title: Distributed Web title: Distributed Web
success: Success! success: Success!
error: Error error: Error
deploy_social_distributed_press:
title: Fediverse
success: Success!
error: Error
deploy_reindex: deploy_reindex:
title: Reindex title: Reindex
success: Success! success: Success!
@ -166,6 +172,7 @@ en:
usuarie: User usuarie: User
licencia: License licencia: License
design: Design design: Design
indexed_post: Indexed post
attributes: attributes:
usuarie: usuarie:
email: 'E-mail address' email: 'E-mail address'
@ -190,9 +197,14 @@ en:
deploys: deploys:
deploy_local_presence: 'We need to be build the site!' deploy_local_presence: 'We need to be build the site!'
design_id: design_id:
missing_gem: "Site is configured to use %{theme} theme, but the corresponding gem is missing from Gemfile"
layout_incompatible: layout_incompatible:
error: "Design can't be changed because there are posts with incompatible layouts" error: "Design can't be changed because there are posts with incompatible layouts"
help: "Your site has posts with layouts only compatible with the current design. If you change it, the site won't work as you expect. If you're trying out designs, you can delete posts in the following incompatible layouts:: %{layouts}." help: "Your site has posts with layouts only compatible with the current design. If you change it, the site won't work as you expect. If you're trying out designs, you can delete posts in the following incompatible layouts:: %{layouts}."
usuarie:
attributes:
lang:
not_available: "This language is not yet available, would you help us by translating Sutty into it?"
errors: errors:
argument_error: 'Argument `%{argument}` must be an instance of %{class}' argument_error: 'Argument `%{argument}` must be an instance of %{class}'
unknown_locale: 'Unknown %{locale} locale' unknown_locale: 'Unknown %{locale} locale'
@ -207,6 +219,8 @@ en:
title: 'Your location in Sutty' title: 'Your location in Sutty'
logout: Log out logout: Log out
mutual_aid: Mutual aid mutual_aid: Mutual aid
contact_us: "Contact us"
contact_us_href: "https://sutty.nl/en/#contact"
collaborations: collaborations:
collaborate: collaborate:
submit: Register submit: Register
@ -303,7 +317,15 @@ en:
storage network may continue retaining copies of the data storage network may continue retaining copies of the data
indefinitely. indefinitely.
[Learn more](https://ffdweb.org/building-distributed-press-a-publishing-tool-for-the-decentralized-web/) [Learn more](https://sutty.nl/learn-more-about-publish-to-dweb-functionality/)
deploy_social_distributed_press:
title: 'Publish on the Fediverse'
help: |
By using the ActivityPub protocol, people on the Fediverse
([Mastodon](https://joinmastodon.org/servers),
[Pixelfed](https://pixelfed.social/site/about), and
[others](https://fediverse.party/)) can follow your site,
receive news and interact with them.
stats: stats:
index: index:
title: Statistics title: Statistics
@ -363,9 +385,10 @@ en:
static_file_migration: 'File migration' static_file_migration: 'File migration'
find_and_replace: 'Search and replace' find_and_replace: 'Search and replace'
status: status:
building: "Your site is building, please wait <time datetime=\"PT%{seconds}S\">%{average_time}</time> to refresh this page..." building: "Your site is building, refresh this page in <time datetime=\"PT%{seconds}S\">%{average_time}</time>."
not_published_yet: "Your site is being published for the first time, please wait up to 1 minute..." not_published_yet: "Your site is being published for the first time, please wait up to 1 minute..."
available: "Your site is available! Click here to visit it." available: "Your site is available! Click here to find all the different ways to visit it."
awaiting_publication: "There are unpublished changes. Click the button below and wait a moment to find them on your site."
index: index:
title: 'My Sites' title: 'My Sites'
pull: 'Upgrade' pull: 'Upgrade'
@ -403,15 +426,17 @@ en:
title: 'Edit %{site}' title: 'Edit %{site}'
submit: 'Save changes' submit: 'Save changes'
btn: 'Configuration' btn: 'Configuration'
update:
post: "Your changes have been saved. **If you enabled a publication method, don't forget to publish changes.**"
form: form:
errors: errors:
title: There were errors and we couldn't save your changes :( title: There were errors and we couldn't save your changes :(
help: Please, look for the invalid fields to fix them help: Please, look for the invalid fields to fix them
help: help:
name: "The name of your site. It can only include numbers and letters." name: "This will be the host name for your site, ie. **example**.sutty.nl. Choose an expression up to 63 characters. It can contain only lowercase letters, numbers and dashes, **and no spaces**. It can't start or end with a dash, or be entirely composed of numbers."
title: 'The title can be anything you want' title: 'The title can be anything you want'
description: 'You site description that appears in search engines. Between 50 and 160 characters.' description: 'You site description that appears in search engines. Between 50 and 160 characters.'
design: 'Select the design for your site. You can change it later. We add more designs from time to time!' design: 'Select the design for your site. We add more designs from time to time!'
licencia: 'Everything we publish has automatic copyright. This licencia: 'Everything we publish has automatic copyright. This
means nobody can use our works without explicit permission. By means nobody can use our works without explicit permission. By
using licenses, we stablish conditions by which we want to share using licenses, we stablish conditions by which we want to share
@ -431,7 +456,7 @@ en:
title: 'Design' title: 'Design'
actions: 'Information about this design' actions: 'Information about this design'
url: 'Demo' url: 'Demo'
licencia: 'License' license: 'License'
licencia: licencia:
title: 'License for the site and everything published on it' title: 'License for the site and everything published on it'
url: 'Read the license' url: 'Read the license'
@ -462,6 +487,9 @@ en:
success: 'Site upgrade has been completed. Your next build will run this upgrade :)' success: 'Site upgrade has been completed. Your next build will run this upgrade :)'
error: "There was an error when trying to upgrade your site. This could be due to conflicts that couldn't be solved automatically. A report of the issue has already been sent to our admins. Sorry for the inconvenience! :(" error: "There was an error when trying to upgrade your site. This could be due to conflicts that couldn't be solved automatically. A report of the issue has already been sent to our admins. Sorry for the inconvenience! :("
message: 'Skeleton upgrade' message: 'Skeleton upgrade'
webhooks:
pull:
message: 'Webhooks pull'
footer: footer:
powered_by: 'is developed by' powered_by: 'is developed by'
i18n: i18n:
@ -508,6 +536,8 @@ en:
feedback: 'This field cannot be empty!' feedback: 'This field cannot be empty!'
uuid: uuid:
label: 'Unique identifier' label: 'Unique identifier'
created_at:
label: 'Created at'
geo: geo:
uri: 'Open in app' uri: 'Open in app'
osm: 'Open in web map' osm: 'Open in web map'
@ -517,7 +547,7 @@ en:
file: file:
destroy: Remove file destroy: Remove file
image: image:
label: Imagen label: Image
destroy: Remove image destroy: Remove image
belongs_to: belongs_to:
empty: "(Empty)" empty: "(Empty)"
@ -540,12 +570,10 @@ en:
order: 'Order' order: 'Order'
content: 'Text' content: 'Text'
new: 'Post types' new: 'Post types'
add: 'Add'
filter: 'Filter'
remove_filter: 'Back'
remove_filter_help: 'Remove the filter: %{filter}' remove_filter_help: 'Remove the filter: %{filter}'
categories: 'Everything' categories: 'Everything'
index: 'Posts' index:
search: 'Search'
edit: 'Edit' edit: 'Edit'
preview: preview:
btn: 'Preliminary version' btn: 'Preliminary version'
@ -683,7 +711,7 @@ en:
new: 'Create' new: 'Create'
edit: 'Configure' edit: 'Configure'
posts: posts:
new: 'New %{layout}' new: 'Add %{layout}'
edit: 'Editing' edit: 'Editing'
usuaries: usuaries:
index: 'Users' index: 'Users'
@ -698,3 +726,14 @@ en:
queries: queries:
show: show:
empty: '(empty)' empty: '(empty)'
schemas:
add:
add: 'Add'
filter:
filter: 'Filter'
remove: 'Back'
build_stats:
index:
title: "Publications"
indexed_posts:
deleted: "Deleted indexed post %{path} from %{site} (records: %{records})"

View file

@ -4,6 +4,8 @@ es:
en: English en: English
es-AR: Castellano Rioplatense es-AR: Castellano Rioplatense
dir: ltr dir: ltr
switch_locale:
en: "Switch to English"
locales: locales:
es: es:
name: Castellano name: Castellano
@ -51,7 +53,7 @@ es:
cant_be_empty: 'El campo no puede estar vacío' cant_be_empty: 'El campo no puede estar vacío'
image: image:
site_invalid: 'La imagen no se puede almacenar si la configuración del sitio no es válida' site_invalid: 'La imagen no se puede almacenar si la configuración del sitio no es válida'
not_an_image: 'No es una imagen' not_an_image: 'No es una imagen en formato web. Formatos aceptados: PNG, JPEG, GIF, WEBP'
path_required: 'Se necesita una imagen' path_required: 'Se necesita una imagen'
no_file_for_description: 'Se envió una descripción sin imagen asociada' no_file_for_description: 'Se envió una descripción sin imagen asociada'
attachment_missing: 'no pude guardar el archivo :(' attachment_missing: 'no pude guardar el archivo :('
@ -122,6 +124,10 @@ es:
title: Web distribuida title: Web distribuida
success: ¡Éxito! success: ¡Éxito!
error: Hubo un error error: Hubo un error
deploy_social_distributed_press:
title: Fediverso
success: ¡Éxito!
error: Hubo un error
deploy_reindex: deploy_reindex:
title: Reindexación title: Reindexación
success: ¡Éxito! success: ¡Éxito!
@ -166,6 +172,7 @@ es:
usuarie: Usuarie usuarie: Usuarie
licencia: Licencia licencia: Licencia
design: Diseño design: Diseño
indexed_post: Artículo indexado
attributes: attributes:
usuarie: usuarie:
email: 'Correo electrónico' email: 'Correo electrónico'
@ -190,9 +197,14 @@ es:
deploys: deploys:
deploy_local_presence: '¡Necesitamos poder generar el sitio!' deploy_local_presence: '¡Necesitamos poder generar el sitio!'
design_id: design_id:
missing_gem: "El sitio usa la plantilla %{theme} pero la gema correspondiente no se encuentra en el Gemfile"
layout_incompatible: layout_incompatible:
error: 'No se puede cambiar la plantilla porque hay artículos con formatos incompatibles' error: 'No se puede cambiar la plantilla porque hay artículos con formatos incompatibles'
help: 'En tu sitio hay artículos que solo son compatibles con el diseño actual, si cambias la plantilla el sitio no funcionará como esperas. Si estás probando plantillas, puedes eliminar los artículos en los formatos incompatibles: %{layouts}.' help: 'En tu sitio hay artículos que solo son compatibles con el diseño actual, si cambias la plantilla el sitio no funcionará como esperas. Si estás probando plantillas, puedes eliminar los artículos en los formatos incompatibles: %{layouts}.'
usuarie:
attributes:
lang:
not_available: "Este idioma todavía no está disponible, ¿nos ayudas a agregarlo y mantenerlo?"
errors: errors:
argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}' argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}'
unknown_locale: 'El idioma %{locale} es desconocido' unknown_locale: 'El idioma %{locale} es desconocido'
@ -207,6 +219,8 @@ es:
title: 'Tu ubicación en Sutty' title: 'Tu ubicación en Sutty'
logout: Cerrar sesión logout: Cerrar sesión
mutual_aid: Ayuda mutua mutual_aid: Ayuda mutua
contact_us: "Contacto"
contact_us_href: "https://sutty.nl/#contacto"
collaborations: collaborations:
collaborate: collaborate:
submit: Registrarme submit: Registrarme
@ -308,7 +322,15 @@ es:
nodos en la red de almacenamiento distribuida puedan retener nodos en la red de almacenamiento distribuida puedan retener
copias de tu contenido indefinidamente. copias de tu contenido indefinidamente.
[Saber más (en inglés)](https://ffdweb.org/building-distributed-press-a-publishing-tool-for-the-decentralized-web/) [Saber más](https://sutty.nl/saber-mas-sobre-publicar-a-la-web-distribuida/)
deploy_social_distributed_press:
title: 'Publicar al Fediverso'
help: |
Utilizando el protocolo ActivityPub, otras personas en el
Fediverso ([Mastodon](https://joinmastodon.org/servers),
[Pixelfed](https://pixelfed.social/site/about) y
[otros](https://fediverse.party/)) pueden seguir a tu sitio,
recibir novedades e interactuar con ellas.
stats: stats:
index: index:
title: Estadísticas title: Estadísticas
@ -368,9 +390,10 @@ es:
static_file_migration: 'Migración de archivos' static_file_migration: 'Migración de archivos'
find_and_replace: 'Búsqueda y reemplazo' find_and_replace: 'Búsqueda y reemplazo'
status: status:
building: "Tu sitio se está publicando, por favor espera <time datetime=\"PT%{seconds}S\">%{average_time}</time> para recargar esta página..." building: "Tu sitio se está publicando, recargá esta página en <time datetime=\"PT%{seconds}S\">%{average_time}</time>."
not_published_yet: "Tu sitio se está publicando por primera vez, por favor espera hasta un minuto..." not_published_yet: "Tu sitio se está publicando por primera vez, por favor espera hasta un minuto..."
available: "¡Tu sitio está disponible! Cliquea aquí para visitarlo." available: "¡Tu sitio está disponible! Cliqueá aquí para encontrar todas las formas en que podés visitarlo."
awaiting_publication: "Hay cambios sin publicar, cliqueá el botón debajo y espera un momento para encontrarlos en tu sitio."
index: index:
title: 'Mis sitios' title: 'Mis sitios'
pull: 'Actualizar' pull: 'Actualizar'
@ -409,15 +432,17 @@ es:
title: 'Editar %{site}' title: 'Editar %{site}'
submit: 'Guardar cambios' submit: 'Guardar cambios'
btn: 'Configuración' btn: 'Configuración'
update:
post: "Tus cambios han sido guardados. **Si agregaste un método de publicación, no te olvides de publicar cambios.**"
form: form:
errors: errors:
title: Hubo errores y no pudimos guardar tus cambios :( title: Hubo errores y no pudimos guardar tus cambios :(
help: Por favor, busca los campos marcados como no válidos para resolverlos help: Por favor, busca los campos marcados como no válidos para resolverlos
help: help:
name: 'El nombre de tu sitio que formará parte de la dirección (**ejemplo**.sutty.nl). Solo puede contener letras minúsculas, números y guiones.' name: 'El nombre de tu sitio que formará parte de la dirección (**ejemplo**.sutty.nl). Solo puede contener hasta 63 letras minúsculas, números y guiones, pero **sin espacios**. No puede empezar ni terminar con guión, ni estar compuesto enteramente por números.'
title: 'El título de tu sitio puede ser lo que quieras.' title: 'El título de tu sitio puede ser lo que quieras.'
description: 'La descripción del sitio, que saldrá en buscadores. Entre 50 y 160 caracteres.' description: 'La descripción del sitio, que saldrá en buscadores. Entre 50 y 160 caracteres.'
design: 'Elegí el diseño que va a tener tu sitio aquí. Podés cambiarlo luego. De tanto en tanto vamos sumando diseños nuevos.' design: 'Elegí el diseño que va a tener tu sitio aquí. De tanto en tanto vamos sumando diseños nuevos.'
licencia: 'Todo lo que publicamos posee automáticamente derechos licencia: 'Todo lo que publicamos posee automáticamente derechos
de autore. Esto significa que nadie puede hacer uso de nuestras de autore. Esto significa que nadie puede hacer uso de nuestras
obras sin permiso explícito. Con las licencias establecemos obras sin permiso explícito. Con las licencias establecemos
@ -470,6 +495,9 @@ es:
success: 'Ya se incorporaron los cambios en el sitio, se aplicarán en la próxima compilación que hagas :)' success: 'Ya se incorporaron los cambios en el sitio, se aplicarán en la próxima compilación que hagas :)'
error: 'Hubo un error al incorporar los cambios en el sitio. Esto puede deberse a conflictos entre cambios que no se pueden resolver automáticamente. Hemos enviado un reporte del problema a les administradores de Sutty para que estén al tanto de la situación. ¡Lo sentimos! :(' error: 'Hubo un error al incorporar los cambios en el sitio. Esto puede deberse a conflictos entre cambios que no se pueden resolver automáticamente. Hemos enviado un reporte del problema a les administradores de Sutty para que estén al tanto de la situación. ¡Lo sentimos! :('
message: 'Actualización del esqueleto' message: 'Actualización del esqueleto'
webhooks:
pull:
message: 'Traer los cambios a partir de un evento remoto'
footer: footer:
powered_by: 'es desarrollada por' powered_by: 'es desarrollada por'
i18n: i18n:
@ -516,6 +544,8 @@ es:
feedback: '¡Este campo no puede estar vacío!' feedback: '¡Este campo no puede estar vacío!'
uuid: uuid:
label: 'Identificador único' label: 'Identificador único'
created_at:
label: 'Fecha de creación'
geo: geo:
uri: 'Abrir en aplicación' uri: 'Abrir en aplicación'
osm: 'Abrir en mapa web' osm: 'Abrir en mapa web'
@ -549,11 +579,9 @@ es:
content: 'Cuerpo del artículo' content: 'Cuerpo del artículo'
categories: 'Todos' categories: 'Todos'
new: 'Tipos de artículos' new: 'Tipos de artículos'
add: 'Agregar'
filter: 'Filtrar'
remove_filter: 'Volver'
remove_filter_help: 'Quitar este filtro: %{filter}' remove_filter_help: 'Quitar este filtro: %{filter}'
index: 'Artículos' index:
search: 'Buscar'
edit: 'Editar' edit: 'Editar'
preview: preview:
btn: 'Versión preliminar' btn: 'Versión preliminar'
@ -691,7 +719,7 @@ es:
new: 'Crear' new: 'Crear'
edit: 'Configurar' edit: 'Configurar'
posts: posts:
new: 'Nuevo %{layout}' new: 'Agregar %{layout}'
edit: 'Editando' edit: 'Editando'
usuaries: usuaries:
index: 'Usuaries' index: 'Usuaries'
@ -706,3 +734,14 @@ es:
queries: queries:
show: show:
empty: '(vacío)' empty: '(vacío)'
schemas:
add:
add: 'Agregar'
filter:
filter: 'Filtrar'
remove: 'Volver'
build_stats:
index:
title: "Publicaciones"
indexed_posts:
deleted: "Eliminado artículo %{path} de %{site} (filas: %{records})"

View file

@ -17,6 +17,8 @@ Rails.application.routes.draw do
get :'contact/cookie', to: 'invitades#contact_cookie' get :'contact/cookie', to: 'invitades#contact_cookie'
post :'contact/:form', to: 'contact#receive', as: :contact post :'contact/:form', to: 'contact#receive', as: :contact
post :'webhooks/pull', to: 'webhooks#pull'
end end
end end
end end
@ -75,5 +77,7 @@ Rails.application.routes.draw do
get :'stats/host', to: 'stats#host' get :'stats/host', to: 'stats#host'
get :'stats/uris', to: 'stats#uris' get :'stats/uris', to: 'stats#uris'
get :'stats/resources', to: 'stats#resources' get :'stats/resources', to: 'stats#resources'
resources :build_stats, only: %i[index]
end end
end end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Que
class CreateQueTables < ActiveRecord::Migration[6.1]
def up
Que.migrate! version: 7
end
def down
Que.migrate! version: 0
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Agrega las columnas de calculo de emisiones de CO2
class AddSustainabilityToAccessLogs < ActiveRecord::Migration[6.1]
def change
%i[datacenter_co2 network_co2 consumer_device_co2 production_co2 total_co2].each do |column|
add_column :access_logs, column, :decimal, limit: 53
end
end
end

View file

@ -0,0 +1,5 @@
class AddPriorityToDesigns < ActiveRecord::Migration[6.1]
def change
add_column :designs, :priority, :integer
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
# Envía los cambios a través de rsyncd
class ChangeFullRsyncDestination < ActiveRecord::Migration[6.1]
def up
DeployFullRsync.find_each do |deploy|
Rails.application.nodes.each do |node|
deploy.destination = "rsync://rsyncd.#{node}/deploys/"
deploy.save
end
end
end
def down
DeployFullRsync.find_each do |deploy|
Rails.application.nodes.each do |node|
deploy.destination = "sutty@#{node}:"
deploy.save
end
end
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# Agrega la columna de nodo a los logs
class AddNodeToAccessLogs < ActiveRecord::Migration[6.1]
def change
add_column :access_logs, :node, :string, index: true
end
end

Some files were not shown because too many files have changed in this diff Show more