diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..b0bc1626 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.env.example b/.env.example index fb086224..f3cf48d9 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,9 @@ +# pwgen -1 32 +RAILS_MASTER_KEY=11111111111111111111111111111111 RAILS_GROUPS=assets DELEGATE=athshe.sutty.nl HAINISH=../haini.sh/haini.sh -DATABASE= +DATABASE_URL=postgres://suttier@postgresql.sutty.local/sutty RAILS_ENV=development IMAP_SERVER= DEFAULT_FROM= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..0ede410e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +public/assets/** filter=lfs diff=lfs merge=lfs -text +public/packs/** filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 496b66cb..e6f2adbb 100644 --- a/.gitignore +++ b/.gitignore @@ -34,12 +34,7 @@ /config/master.key /config/credentials.yml.enc -/public/packs /public/packs-test -/public/assets -/public/assets-production -/public/packs -/public/packs-production /node_modules /yarn-error.log yarn-debug.log* @@ -49,8 +44,6 @@ yarn-debug.log* *.key *.crt -/public/packs -/public/packs-test /node_modules /yarn-error.log yarn-debug.log* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..f8994356 --- /dev/null +++ b/.gitlab-ci.yml @@ -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" diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 00000000..b5806bf3 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,83 @@ +pipeline: + publish: + image: "docker.io/woodpeckerci/plugin-docker-buildx" + settings: + registry: "gitea.nulo.in" + username: "sutty" + repo: "gitea.nulo.in/sutty/panel" + tags: + - "${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}-${CI_COMMIT_BRANCH}" + - "latest" + build_args: + - "RUBY_VERSION=${RUBY_VERSION}" + - "RUBY_PATCH=${RUBY_PATCH}" + - "ALPINE_VERSION=${ALPINE_VERSION}" + - "BASE_IMAGE=gitea.nulo.in/sutty/rails" + purge: false + secrets: + - "DOCKER_PASSWORD" + when: + branch: + - "rails" + - "panel.sutty.nl" + - "17.3.alpine.panel.sutty.nl" + event: "push" + path: + include: + - "Dockerfile" + - ".dockerignore" + - ".woodpecker.yml" + assets: + image: "gitea.nulo.in/sutty/panel:3.14.10-2.7.8" + commands: + - "apk add python2 dotenv openssh-client brotli" + - "install -d -m 700 ~/.ssh/" + - "echo \"$${KNOWN_HOSTS}\" | base64 -d >> ~/.ssh/known_hosts" + - "chmod 600 ~/.ssh/known_hosts" + - "eval $(ssh-agent -s)" + - "echo \"$${SSH_KEY}\" | base64 -d | ssh-add -" + - "ssh $${ORIGIN%:*}" + - "git config user.name Woodpecker" + - "git config user.email ci@sutty.coop.ar" + - "git remote add upstream $${ORIGIN}" + - "git checkout -B ${CI_COMMIT_BRANCH}" + - "mv config/credentials.yml.enc.ci config/credentials.yml.enc" + - "yarn" + - "cp .env.example .env" + - "dotenv bundle install --path=vendor" + - "dotenv RAILS_ENV=production bundle exec rails webpacker:clobber" + - "dotenv RAILS_ENV=production bundle exec rails assets:precompile" + - "dotenv RAILS_ENV=production bundle exec rails assets:clean" + - "find public -type f -print0 | xargs -r0 brotli -k9f" + - "git add public && git commit -m \"ci: assets [skip ci]\"" + - "git pull upstream ${CI_COMMIT_BRANCH}" + - "git push upstream ${CI_COMMIT_BRANCH}" + environment: + - "RUBY_VERSION=${RUBY_VERSION}" + - "GEMS_SOURCE=https://14.3.alpine.gems.sutty.nl" + secrets: + - "SSH_KEY" + - "KNOWN_HOSTS" + - "ORIGIN" + when: + branch: + - "rails" + - "panel.sutty.nl" + path: + include: + - "app/assets/**/*" + - "app/javascript/**/*" + - "package.json" + - "yarn.lock" + matrix: + ALPINE_VERSION: "3.14.10" + RUBY_VERSION: "2.7" + RUBY_PATCH: "8" +matrix: + include: + - ALPINE_VERSION: "3.17.3" + RUBY_VERSION: "3.1" + RUBY_PATCH: "4" + - ALPINE_VERSION: "3.14.10" + RUBY_VERSION: "2.7" + RUBY_PATCH: "8" diff --git a/Dockerfile b/Dockerfile index 342c2750..e0f1dc9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,9 @@ -FROM registry.nulo.in/sutty/rails:3.13.6-2.7.5 -ARG PANDOC_VERSION=2.17.1.1 +ARG RUBY_VERSION=2.7 +ARG RUBY_PATCH=6 +ARG ALPINE_VERSION=3.13.10 +ARG BASE_IMAGE=registry.nulo.in/sutty/rails +FROM ${BASE_IMAGE}:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH} +ARG PANDOC_VERSION=2.18 ENV RAILS_ENV production # Instalar las dependencias, separamos la librería de base de datos para @@ -10,13 +14,16 @@ ENV RAILS_ENV production # principal RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \ rsync git jpegoptim vips tectonic oxipng git-lfs openssh-client \ - yarn daemonize ruby-webrick + yarn daemonize ruby-webrick postgresql-client dateutils file RUN gem install --no-document --no-user-install foreman RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc +RUN apk add npm && npm install -g pnpm@~7 && apk del npm COPY ./monit.conf /etc/monit.d/sutty.conf +RUN apk add npm && npm install -g pnpm && apk del npm + VOLUME "/srv" EXPOSE 3000 diff --git a/Gemfile b/Gemfile index f3799638..bf9e875c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,13 @@ # frozen_string_literal: true -puts 'Usa haini.sh para generar un entorno de trabajo reproducible' -source 'https://gems.sutty.nl' +source ENV.fetch('GEMS_SOURCE', 'https://17.3.alpine.gems.sutty.nl') -ruby '~> 2.7' +ruby "~> #{ENV.fetch('RUBY_VERSION', '3.1')}" gem 'dotenv-rails', require: 'dotenv/rails-now' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 6' +gem 'rails', '~> 6.1.0' # Use Puma as the app server gem 'puma' @@ -23,6 +22,7 @@ if ENV['RAILS_GROUPS']&.split(',')&.include? 'assets' end gem 'nokogiri' +gem 'rgl' # Turbolinks makes navigating your web application faster. Read more: # https://github.com/turbolinks/turbolinks @@ -32,12 +32,14 @@ gem 'turbolinks', '~> 5' gem 'jbuilder', '~> 2.5' # Use ActiveModel has_secure_password gem 'bcrypt', '~> 3.1.7' +gem 'safely_block', '~> 0.3.0' gem 'blazer' gem 'chartkick' gem 'commonmarker' gem 'devise' gem 'devise-i18n' gem 'devise_invitable' +gem 'distributed-press-api-client', '~> 0.3.0rc0' gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n' gem 'exception_notification' gem 'fast_blank' @@ -48,10 +50,10 @@ gem 'image_processing' gem 'icalendar' gem 'inline_svg' gem 'httparty' -gem 'safe_yaml' -gem 'jekyll', '~> 4.2' +gem 'safe_yaml', require: false +gem 'jekyll', '~> 4.2.0' gem 'jekyll-data' -gem 'jekyll-commonmark' +gem 'jekyll-commonmark', '~> 1.4.0' gem 'jekyll-images' gem 'jekyll-include-cache' gem 'sutty-liquid', '>= 0.7.3' @@ -62,19 +64,21 @@ gem 'mobility' gem 'pundit' gem 'rails-i18n' 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 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update' gem 'rubyzip' -gem 'rugged' +gem 'rugged', '1.5.0.1' +gem 'git_clone_url' gem 'concurrent-ruby-ext' -gem 'sucker_punch' +gem 'que' gem 'symbol-fstring', require: 'fstring/all' gem 'terminal-table' gem 'validates_hostname' gem 'webpacker' gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' gem 'kaminari' +gem 'device_detector' # database gem 'hairtrigger' @@ -108,7 +112,7 @@ group :development, :test do gem 'pry' # Adds support for Capybara system testing and selenium driver gem 'capybara', '~> 2.13' - gem 'selenium-webdriver' + gem 'selenium-webdriver', '~> 4.8.0' gem 'sqlite3' end @@ -116,11 +120,11 @@ group :development do gem 'brakeman' gem 'haml-lint', require: false gem 'letter_opener' - gem 'listen', '>= 3.0.5', '< 3.2' + gem 'listen' gem 'rubocop-rails' gem 'spring' - gem 'spring-watcher-listen', '~> 2.0.0' - gem 'web-console', '>= 3.3.0' + gem 'spring-watcher-listen' + gem 'web-console' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index abaf45c9..3faad5e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -25,86 +25,86 @@ GIT groupdate (>= 5.2) GEM - remote: https://gems.sutty.nl/ + remote: https://17.3.alpine.gems.sutty.nl/ specs: - actioncable (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) + actioncable (6.1.7.3) + actionpack (= 6.1.7.3) + activesupport (= 6.1.7.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + actionmailbox (6.1.7.3) + actionpack (= 6.1.7.3) + activejob (= 6.1.7.3) + activerecord (= 6.1.7.3) + activestorage (= 6.1.7.3) + activesupport (= 6.1.7.3) mail (>= 2.7.1) - actionmailer (6.1.4.1) - actionpack (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activesupport (= 6.1.4.1) + actionmailer (6.1.7.3) + actionpack (= 6.1.7.3) + actionview (= 6.1.7.3) + activejob (= 6.1.7.3) + activesupport (= 6.1.7.3) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.1) - actionview (= 6.1.4.1) - activesupport (= 6.1.4.1) + actionpack (6.1.7.3) + actionview (= 6.1.7.3) + activesupport (= 6.1.7.3) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.1) - actionpack (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + actiontext (6.1.7.3) + actionpack (= 6.1.7.3) + activerecord (= 6.1.7.3) + activestorage (= 6.1.7.3) + activesupport (= 6.1.7.3) nokogiri (>= 1.8.5) - actionview (6.1.4.1) - activesupport (= 6.1.4.1) + actionview (6.1.7.3) + activesupport (= 6.1.7.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.4.1) - activesupport (= 6.1.4.1) + activejob (6.1.7.3) + activesupport (= 6.1.7.3) globalid (>= 0.3.6) - activemodel (6.1.4.1) - activesupport (= 6.1.4.1) - activerecord (6.1.4.1) - activemodel (= 6.1.4.1) - activesupport (= 6.1.4.1) - activestorage (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activesupport (= 6.1.4.1) - marcel (~> 1.0.0) + activemodel (6.1.7.3) + activesupport (= 6.1.7.3) + activerecord (6.1.7.3) + activemodel (= 6.1.7.3) + activesupport (= 6.1.7.3) + activestorage (6.1.7.3) + actionpack (= 6.1.7.3) + activejob (= 6.1.7.3) + activerecord (= 6.1.7.3) + activesupport (= 6.1.7.3) + marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.1) + activesupport (6.1.7.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) - autoprefixer-rails (10.3.3.0) + autoprefixer-rails (10.4.13.0) 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) - benchmark-ips (2.9.2) + benchmark-ips (2.12.0) bindex (0.8.1-x86_64-linux-musl) - blazer (2.4.7) + blazer (2.6.5) activerecord (>= 5) chartkick (>= 3.2) railties (>= 5) safely_block (>= 0.1.1) - bootstrap (4.6.0) + bootstrap (4.6.2) autoprefixer-rails (>= 9.1.0) - popper_js (>= 1.14.3, < 2) + popper_js (>= 1.16.1, < 2) sassc-rails (>= 2.0.0) - brakeman (5.1.2) + brakeman (5.4.1) builder (3.2.4) capybara (2.18.0) addressable @@ -113,24 +113,24 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (>= 2.0, < 4.0) - chartkick (4.1.2) - childprocess (4.1.0) + chartkick (5.0.2) + climate_control (1.2.0) coderay (1.1.3) colorator (1.1.0) - commonmarker (0.21.2-x86_64-linux-musl) - ruby-enum (~> 0.5) - concurrent-ruby (1.1.9) - concurrent-ruby-ext (1.1.9-x86_64-linux-musl) - concurrent-ruby (= 1.1.9) + commonmarker (0.23.10-x86_64-linux-musl) + concurrent-ruby (1.2.2) + concurrent-ruby-ext (1.2.2-x86_64-linux-musl) + concurrent-ruby (= 1.2.2) crass (1.0.6) - database_cleaner (2.0.1) - database_cleaner-active_record (~> 2.0.0) - database_cleaner-active_record (2.0.1) + database_cleaner (2.0.2) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.1.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - dead_end (3.1.0) - derailed_benchmarks (2.1.1) + date (3.3.3-x86_64-linux-musl) + dead_end (4.0.0) + derailed_benchmarks (2.1.2) benchmark-ips (~> 2) dead_end get_process_mem (~> 0) @@ -142,68 +142,105 @@ GEM rake (> 10, < 14) ruby-statistics (>= 2.1) thor (>= 0.19, < 2) - devise (4.8.0) + device_detector (1.1.1) + devise (4.9.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-i18n (1.10.1) - devise (>= 4.8.0) - devise_invitable (2.0.5) + devise-i18n (1.11.0) + devise (>= 4.9.0) + devise_invitable (2.0.8) actionmailer (>= 5.0) devise (>= 4.6) - dotenv (2.7.6) - dotenv-rails (2.7.6) - dotenv (= 2.7.6) + distributed-press-api-client (0.3.0rc0) + addressable (~> 2.3, >= 2.3.0) + climate_control + dry-schema + httparty (~> 0.18) + json (~> 2.1, >= 2.1.0) + jwt (~> 2.6.0) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) railties (>= 3.2) - down (5.2.4) + down (5.4.1) addressable (~> 2.8) - ed25519 (1.2.4-x86_64-linux-musl) + dry-configurable (1.0.1) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-core (1.0.0) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + dry-inflector (1.0.0) + dry-initializer (3.1.1) + dry-logic (1.5.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-schema (1.13.1) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.0, < 2) + dry-initializer (~> 3.0) + dry-logic (>= 1.4, < 2) + dry-types (>= 1.7, < 2) + zeitwerk (~> 2.6) + dry-types (1.7.1) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + ed25519 (1.3.0-x86_64-linux-musl) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) - errbase (0.2.1) - erubi (1.10.0) + errbase (0.2.2) + erubi (1.12.0) eventmachine (1.2.7-x86_64-linux-musl) - exception_notification (4.4.3) - actionmailer (>= 4.0, < 7) - activesupport (>= 4.0, < 7) + exception_notification (4.5.0) + actionmailer (>= 5.2, < 8) + activesupport (>= 5.2, < 8) execjs (2.8.1) - factory_bot (6.2.0) + factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) fast_blank (1.0.1-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) forwardable-extended (2.6.0) - friendly_id (5.4.2) + friendly_id (5.5.0) activerecord (>= 4.0.0) get_process_mem (0.2.7) 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) - groupdate (6.1.0) + groupdate (6.2.1) activesupport (>= 5.2) - hairtrigger (0.2.24) - activerecord (>= 5.0, < 7) + hairtrigger (1.0.0) + activerecord (>= 6.0, < 8) ruby2ruby (~> 2.4) ruby_parser (~> 3.10) - haml (5.2.2) - temple (>= 0.8.0) + haml (6.1.2-x86_64-linux-musl) + temple (>= 0.8.2) + thor tilt haml-lint (0.999.999) haml_lint - haml_lint (0.37.1) - haml (>= 4.0, < 5.3) + haml_lint (0.45.0) + haml (>= 4.0, < 6.2) parallel (~> 1.10) rainbow rubocop (>= 0.50.0) sysexits (~> 1.1) - hamlit (2.15.1-x86_64-linux-musl) + hamlit (3.0.3-x86_64-linux-musl) temple (>= 0.8.2) thor tilt @@ -216,23 +253,24 @@ GEM thor hiredis (0.6.3-x86_64-linux-musl) http_parser.rb (0.8.0-x86_64-linux-musl) - httparty (0.18.1) - mime-types (~> 3.0) + httparty (0.21.0) + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.8.11) + i18n (1.14.1) concurrent-ruby (~> 1.0) - icalendar (2.7.1) + icalendar (2.8.0) ice_cube (~> 0.16) ice_cube (0.16.4) - image_processing (1.12.1) + image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - inline_svg (1.7.2) + inline_svg (1.9.0) activesupport (>= 3.0) nokogiri (>= 1.6) - jbuilder (2.11.3) + jbuilder (2.11.5) + actionview (>= 5.0.0) activesupport (>= 5.0.0) - jekyll (4.2.1) + jekyll (4.2.2) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) @@ -247,233 +285,216 @@ GEM rouge (~> 3.0) safe_yaml (~> 1.0) terminal-table (~> 2.0) - jekyll-commonmark (1.3.2) - commonmarker (~> 0.14, < 0.22) - jekyll (>= 3.7, < 5.0) + jekyll-commonmark (1.4.0) + commonmarker (~> 0.22) jekyll-data (1.1.2) jekyll (>= 3.3, < 5.0.0) - jekyll-dotenv (0.2.0) - 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-images (0.4.1) jekyll (~> 4) ruby-filemagic (~> 0.7) ruby-vips (~> 2) jekyll-include-cache (0.2.1) jekyll (>= 3.7, < 5.0) - jekyll-linked-posts (0.4.2) - 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) + jekyll-sass-converter (2.2.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) listen (~> 3.0) - jekyll-write-and-commit-changes (0.2.1) - jekyll (~> 4) - rugged (~> 1) - kaminari (1.2.1) + json (2.6.3-x86_64-linux-musl) + jwt (2.6.0) + kaminari (1.2.2) activesupport (>= 4.1.0) - kaminari-actionview (= 1.2.1) - kaminari-activerecord (= 1.2.1) - kaminari-core (= 1.2.1) - kaminari-actionview (1.2.1) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) actionview - kaminari-core (= 1.2.1) - kaminari-activerecord (1.2.1) + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) activerecord - kaminari-core (= 1.2.1) - kaminari-core (1.2.1) - kramdown (2.3.1) + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - launchy (2.5.0) - addressable (~> 2.7) - letter_opener (1.7.0) - launchy (~> 2.2) - liquid (4.0.3) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) + launchy (2.5.2) + addressable (~> 2.8) + letter_opener (1.8.1) + launchy (>= 2.2, < 3) + liquid (4.0.4) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) loaf (0.10.0) railties (>= 3.2) - lockbox (0.6.6) - lograge (0.11.2) + lockbox (1.2.0) + lograge (0.12.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.12.0) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp marcel (1.0.2) - memory_profiler (1.0.0) + memory_profiler (1.0.1) mercenary (0.4.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_magick (4.11.0) + mini_magick (4.12.0) mini_mime (1.1.2) - mini_portile2 (2.6.1) - minitest (5.14.4) - mobility (1.2.4) + mini_portile2 (2.8.2) + minitest (5.18.0) + mobility (1.2.9) i18n (>= 0.6.10, < 2) request_store (~> 1.0) multi_xml (0.6.0) - net-ssh (6.1.0) - netaddr (2.0.5) - nio4r (2.5.8-x86_64-linux-musl) - nokogiri (1.12.5-x86_64-linux-musl) - mini_portile2 (~> 2.6.1) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + 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) orm_adapter (0.5.0) - parallel (1.21.0) - parser (3.0.2.0) + pairing_heap (3.0.1) + parallel (1.23.0) + parser (3.2.2.1) ast (~> 2.4.1) pathutil (0.16.2) forwardable-extended (~> 2.6) - pg (1.2.3-x86_64-linux-musl) - pg_search (2.3.5) + pg (1.5.3-x86_64-linux-musl) + pg_search (2.3.6) activerecord (>= 5.2) activesupport (>= 5.2) - popper_js (1.16.0) - prometheus_exporter (1.0.0) + popper_js (1.16.1) + prometheus_exporter (2.0.8) webrick - pry (0.14.1) + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - public_suffix (4.0.6) - puma (5.5.2-x86_64-linux-musl) + public_suffix (5.0.3) + puma (6.3.1-x86_64-linux-musl) nio4r (~> 2.0) - pundit (2.1.1) + pundit (2.3.0) activesupport (>= 3.0.0) - racc (1.6.0-x86_64-linux-musl) - rack (2.2.3) - rack-cors (1.1.1) + que (2.2.1) + racc (1.7.1-x86_64-linux-musl) + rack (2.2.7) + rack-cors (2.0.1) rack (>= 2.0.0) - rack-mini-profiler (2.3.3) + rack-mini-profiler (3.1.0) rack (>= 1.2.0) - rack-proxy (0.7.0) + rack-proxy (0.7.6) rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.4.1) - actioncable (= 6.1.4.1) - actionmailbox (= 6.1.4.1) - actionmailer (= 6.1.4.1) - actionpack (= 6.1.4.1) - actiontext (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activemodel (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + rack-test (2.1.0) + rack (>= 1.3) + rails (6.1.7.3) + actioncable (= 6.1.7.3) + actionmailbox (= 6.1.7.3) + actionmailer (= 6.1.7.3) + actionpack (= 6.1.7.3) + actiontext (= 6.1.7.3) + actionview (= 6.1.7.3) + activejob (= 6.1.7.3) + activemodel (= 6.1.7.3) + activerecord (= 6.1.7.3) + activestorage (= 6.1.7.3) + activesupport (= 6.1.7.3) bundler (>= 1.15.0) - railties (= 6.1.4.1) + railties (= 6.1.7.3) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) - loofah (~> 2.3) - rails-i18n (6.0.0) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) + rails-i18n (7.0.7) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 7) + railties (>= 6.0.0, < 8) rails_warden (0.6.0) warden (>= 1.2.0) - railties (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) + railties (6.1.7.3) + actionpack (= 6.1.7.3) + activesupport (= 6.1.7.3) method_source - rake (>= 0.13) + rake (>= 12.2) thor (~> 1.0) - rainbow (3.0.0) + rainbow (3.1.1) rake (13.0.6) - rb-fsevent (0.11.0) + rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - redis (4.5.1) - redis-actionpack (5.2.0) - actionpack (>= 5, < 7) + redis (4.8.1) + redis-actionpack (5.3.0) + actionpack (>= 5, < 8) redis-rack (>= 2.1.0, < 3) redis-store (>= 1.1.0, < 2) - redis-activesupport (5.2.1) - activesupport (>= 3, < 7) + redis-activesupport (5.3.0) + activesupport (>= 3, < 8) redis-store (>= 1.3, < 2) - redis-rack (2.1.3) + redis-rack (2.1.4) rack (>= 2.0.8, < 3) redis-store (>= 1.2, < 2) redis-rails (5.0.2) redis-actionpack (>= 5.0, < 6) redis-activesupport (>= 5.0, < 6) redis-store (>= 1.2, < 2) - redis-store (1.9.0) - redis (>= 4, < 5) - regexp_parser (2.1.1) - request_store (1.5.0) + redis-store (1.9.2) + redis (>= 4, < 6) + regexp_parser (2.8.0) + request_store (1.5.1) rack (>= 1.4) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) rexml (3.2.5) - rouge (3.26.1) - rubocop (1.23.0) + rgl (0.6.3) + pairing_heap (>= 0.3.0) + rexml (~> 3.2, >= 3.2.4) + stream (~> 0.5.3) + rouge (3.30.0) + rubocop (1.42.0) + json (~> 2.3) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.1.2.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.12.0, < 2.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.24.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.13.0) - parser (>= 3.0.1.1) - rubocop-rails (2.12.4) + rubocop-ast (1.28.1) + parser (>= 3.2.1.0) + rubocop-rails (2.19.1) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 1.7.0, < 2.0) - ruby-enum (0.9.0) - i18n - ruby-filemagic (0.7.2-x86_64-linux-musl) - ruby-progressbar (1.11.0) - ruby-statistics (3.0.0) + rubocop (>= 1.33.0, < 2.0) + ruby-filemagic (0.7.3-x86_64-linux-musl) + ruby-progressbar (1.13.0) + ruby-statistics (3.0.2) ruby-vips (2.1.4) ffi (~> 1.12) - ruby2ruby (2.4.4) + ruby2ruby (2.5.0) ruby_parser (~> 3.1) sexp_processor (~> 4.6) - ruby_dep (1.5.0) - ruby_parser (3.18.1) + ruby_parser (3.20.1) sexp_processor (~> 4.16) 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) safely_block (0.3.0) errbase (>= 0.1.1) @@ -485,58 +506,55 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (4.1.0) - childprocess (>= 0.5, < 5.0) + selenium-webdriver (4.8.6) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) semantic_range (3.0.0) - sexp_processor (4.16.0) + sexp_processor (4.17.0) simpleidn (0.2.1) unf (~> 0.1.4) sourcemap (0.1.1) - spree-api-client (0.2.4) - fast_blank (~> 1) - httparty (~> 0.18.0) - spring (2.1.1) - spring-watcher-listen (2.0.1) + spring (4.1.1) + spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) - spring (>= 1.2, < 3.0) - sprockets (4.0.2) + spring (>= 4) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.1) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - sqlite3 (1.4.2-x86_64-linux-musl) - stackprof (0.2.17-x86_64-linux-musl) - sucker_punch (3.0.1) - concurrent-ruby (~> 1.0) - sutty-archives (2.5.4) - jekyll (>= 3.6, < 5.0) - sutty-liquid (0.7.4) + sqlite3 (1.6.3-x86_64-linux-musl) + mini_portile2 (~> 2.8.0) + stackprof (0.2.25-x86_64-linux-musl) + stream (0.5.5) + sutty-liquid (0.11.11) fast_blank (~> 1.0) jekyll (~> 4) symbol-fstring (1.0.2-x86_64-linux-musl) sysexits (1.2.0) - temple (0.8.2) + temple (0.10.1) terminal-table (2.0.0) unicode-display_width (~> 1.1, >= 1.1.1) - thor (1.1.0) - tilt (2.0.10) - timecop (0.9.4) + thor (1.2.2) + tilt (2.1.0) + timecop (0.9.6) + timeout (0.3.2) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - tzinfo (2.0.4) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) unf (0.1.4) 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) - validates_hostname (1.0.11) + uri-ssh_git (2.0.0) + validates_hostname (1.0.13) activerecord (>= 3.0) activesupport (>= 3.0) warden (1.2.9) @@ -546,21 +564,21 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webpacker (5.4.3) + webpacker (5.4.4) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - webrick (1.7.0) - websocket-driver (0.7.5-x86_64-linux-musl) + webrick (1.8.1) + websocket (1.2.9) + websocket-driver (0.7.6-x86_64-linux-musl) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.5.1) + zeitwerk (2.6.8) PLATFORMS - ruby x86_64-linux-musl DEPENDENCIES @@ -575,9 +593,11 @@ DEPENDENCIES concurrent-ruby-ext database_cleaner derailed_benchmarks + device_detector devise devise-i18n devise_invitable + distributed-press-api-client (~> 0.3.0rc0) dotenv-rails down ed25519 @@ -585,9 +605,10 @@ DEPENDENCIES exception_notification factory_bot_rails fast_blank - fast_jsonparser + fast_jsonparser (~> 0.5.0) flamegraph friendly_id + git_clone_url hairtrigger haml-lint hamlit-rails @@ -597,14 +618,14 @@ DEPENDENCIES image_processing inline_svg jbuilder (~> 2.5) - jekyll (~> 4.2) - jekyll-commonmark + jekyll (~> 4.2.0) + jekyll-commonmark (~> 1.4.0) jekyll-data jekyll-images jekyll-include-cache kaminari letter_opener - listen (>= 3.0.5, < 3.2) + listen loaf lockbox lograge @@ -619,26 +640,28 @@ DEPENDENCIES pry puma pundit + que rack-cors rack-mini-profiler - rails (~> 6) + rails (~> 6.1.0) rails-i18n rails_warden - redis + redis (~> 4.0) redis-rails + rgl rollups! rubocop-rails rubyzip - rugged + rugged (= 1.5.0.1) safe_yaml + safely_block (~> 0.3.0) sassc-rails - selenium-webdriver + selenium-webdriver (~> 4.8.0) sourcemap spring - spring-watcher-listen (~> 2.0.0) + spring-watcher-listen sqlite3 stackprof - sucker_punch sutty-liquid (>= 0.7.3) symbol-fstring terminal-table @@ -646,12 +669,12 @@ DEPENDENCIES turbolinks (~> 5) uglifier (>= 1.3.0) validates_hostname - web-console (>= 3.3.0) + web-console webpacker yaml_db! RUBY VERSION - ruby 2.7.1p83 + ruby 3.1.4p223 BUNDLED WITH - 2.2.2 + 2.4.17 diff --git a/Makefile b/Makefile index 584d07d1..f295a3e0 100644 --- a/Makefile +++ b/Makefile @@ -48,8 +48,6 @@ help: always ## Ayuda @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/" -assets: public/packs/manifest.json.br ## Compilar los assets - test: always ## Ejecutar los tests $(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=). $(hain) 'bundle $(args)' -psql := psql -h $(PG_HOST) -U $(PG_USER) -p $(PG_PORT) -d sutty +psql := psql $(DATABASE_URL) copy-table: test -n "$(table)" echo "truncate $(table) $(cascade);" | $(psql) ssh $(delegate) docker exec postgresql pg_dump -U sutty -d sutty -t $(table) | $(psql) psql: - $(psql) + $(hain) $(psql) rubocop: ## Yutea el código que está por ser commiteado git status --porcelain \ @@ -110,21 +108,13 @@ save: ## Subir la imagen Docker al nodo delegado date +%F | xargs -I {} git tag -f $(container)-{} @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 - 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) 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 # `make test/models/usuarie_test.rb` tests := $(shell find test/ -name "*_test.rb") diff --git a/Procfile b/Procfile index 45fe1df7..9a278908 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,11 @@ +migrate: bundle exec rake db:prepare db:seed +sutty: bundle exec puma config.ru +blazer_5m: bundle exec rake blazer:run_checks SCHEDULE="5 minutes" +blazer_1h: bundle exec rake blazer:run_checks SCHEDULE="1 hour" +blazer_1d: bundle exec rake blazer:run_checks SCHEDULE="1 day" +blazer: bundle exec rake blazer:send_failing_checks +prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_" +distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew cleanup: bundle exec rake cleanup:everything stats: bundle exec rake stats:process_all +que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 2f52829b..eb953c30 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -16,7 +16,7 @@ $primary: $magenta; $secondary: $black; $jumbotron-bg: transparent; $enable-rounded: false; -$form-feedback-valid-color: $cyan; +$form-feedback-valid-color: $black; $form-feedback-invalid-color: $magenta; $form-feedback-icon-valid-color: $black; $component-active-bg: $magenta; @@ -29,6 +29,11 @@ $sizes: ( "70ch": 70ch, ); +.btn { + background-color: var(--foreground); + color: var(--background); +} + @import "bootstrap"; @import "editor"; @@ -158,6 +163,12 @@ ol.breadcrumb { transition: all 3s; } +fieldset { + legend { + font-size: 1rem; + } +} + .mapable, .taggable { .input-map, @@ -198,8 +209,6 @@ svg { } .btn { - background-color: var(--foreground); - color: var(--background); border: none; border-radius: 0; margin-right: 0.3rem; @@ -377,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. */ @@ -399,6 +411,8 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1); .text-#{$grid-breakpoint}-right { text-align: right !important; } .text-#{$grid-breakpoint}-center { text-align: center !important; } + .word-break-#{$grid-breakpoint}-all { word-break: break-all !important; } + // posición @each $position in $positions { .position-#{$grid-breakpoint}-#{$position} { position: $position !important; } diff --git a/app/controllers/api/v1/contact_controller.rb b/app/controllers/api/v1/contact_controller.rb index deacf4a7..d949dc30 100644 --- a/app/controllers/api/v1/contact_controller.rb +++ b/app/controllers/api/v1/contact_controller.rb @@ -18,7 +18,7 @@ module Api # Si todo salió bien, enviar los correos y redirigir al sitio. # El sitio nos dice a dónde tenemos que ir. - ContactJob.perform_async site.id, + ContactJob.perform_later site.id, params[:form], contact_params.to_h.symbolize_keys, params[:redirect] diff --git a/app/controllers/api/v1/notices_controller.rb b/app/controllers/api/v1/notices_controller.rb index 436c78b5..3d74a48f 100644 --- a/app/controllers/api/v1/notices_controller.rb +++ b/app/controllers/api/v1/notices_controller.rb @@ -9,10 +9,10 @@ module Api # Generar un stacktrace en segundo plano y enviarlo por correo # solo si la API key es verificable. Del otro lado siempre # respondemos con lo mismo. - def create - if site&.airbrake_valid? airbrake_token + def create + if (site&.airbrake_valid? airbrake_token) && !detected_device.bot? BacktraceJob.perform_later site_id: params[:site_id], - params: airbrake_params.to_h + params: airbrake_params.to_h end render status: 201, json: { id: 1, url: '' } @@ -34,6 +34,11 @@ module Api def airbrake_token @airbrake_token ||= params[:key] end + + # @return [DeviceDetector] + def detected_device + @detected_device ||= DeviceDetector.new(request.headers['User-Agent'], request.headers) + end end end end diff --git a/app/controllers/api/v1/sites_controller.rb b/app/controllers/api/v1/sites_controller.rb index 10a92907..ae64cf74 100644 --- a/app/controllers/api/v1/sites_controller.rb +++ b/app/controllers/api/v1/sites_controller.rb @@ -12,31 +12,6 @@ module Api render json: sites_names + alternative_names + api_names + www_names end - # Sitios con hidden service de Tor - # - # @return [Array] lista de nombres de sitios sin onion aun - def hidden_services - render json: DeployHiddenService.where(values: nil).includes(:site).pluck(:name) - end - - # Tor va a enviar el onion junto con el nombre del sitio y tenemos - # que guardarlo en su deploy_hidden_service. - # - # @params [String] name - # @params [String] onion - def add_onion - site = Site.find_by(name: params[:name]) - - if site - usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor' - service = SiteService.new site: site, usuarie: usuarie, - params: params - service.add_onion - end - - head :ok - end - private def canonicalize(name) diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb new file mode 100644 index 00000000..36e6a6d1 --- /dev/null +++ b/app/controllers/api/v1/webhooks_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e80c279d..2746ab10 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base before_action :prepare_exception_notifier before_action :configure_permitted_parameters, if: :devise_controller? + before_action :notify_unconfirmed_email, unless: :devise_controller? around_action :set_locale rescue_from Pundit::NilPolicyError, with: :page_not_found @@ -27,6 +28,15 @@ class ApplicationController < ActionController::Base private + def notify_unconfirmed_email + return unless current_usuarie + return if current_usuarie.confirmed? + + I18n.with_locale(current_usuarie.lang) do + flash[:notice] ||= I18n.t('devise.registrations.signed_up') + end + end + def uuid?(string) /[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string end @@ -49,7 +59,11 @@ class ApplicationController < ActionController::Base # # @return [String,Symbol] 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 end @@ -81,9 +95,14 @@ class ApplicationController < ActionController::Base breadcrumb 'stats.index', root_path, match: :exact end + def site + @site ||= find_site + end + protected def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up, keys: Usuarie::CONSENT_FIELDS) devise_parameter_sanitizer.permit(:account_update, keys: %i[lang]) end diff --git a/app/controllers/build_stats_controller.rb b/app/controllers/build_stats_controller.rb new file mode 100644 index 00000000..339f17f0 --- /dev/null +++ b/app/controllers/build_stats_controller.rb @@ -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 diff --git a/app/controllers/env_controller.rb b/app/controllers/env_controller.rb index 0f34e15f..de61c704 100644 --- a/app/controllers/env_controller.rb +++ b/app/controllers/env_controller.rb @@ -5,6 +5,7 @@ class EnvController < ActionController::Base def index @site = Site.find_by_name('panel') - stale? @site + + stale? @site if @site end end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 3c529c24..057c3068 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -24,6 +24,7 @@ class PostsController < ApplicationController # Todos los artículos de este sitio para el idioma actual @posts = site.indexed_posts.where(locale: locale) + @posts = @posts.page(filter_params.delete(:page)) if site.pagination # De este tipo @posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout] # Que estén dentro de la categoría @@ -154,15 +155,11 @@ class PostsController < ApplicationController # # @return [Hash] 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? end.transform_keys(&:to_sym) end - def site - @site ||= find_site - end - def post @post ||= site.posts(lang: locale).find(params[:post_id] || params[:id]) end diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index f0eff0dc..17287eb0 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -10,7 +10,7 @@ class SitesController < ApplicationController # Ver un listado de sitios def index authorize Site - @sites = current_usuarie.sites.order(:title) + @sites = current_usuarie.sites.order(updated_at: :desc) fresh_when @sites end @@ -28,8 +28,6 @@ class SitesController < ApplicationController @site = Site.new authorize @site - - @site.deploys.build type: 'DeployLocal' end def create @@ -59,6 +57,7 @@ class SitesController < ApplicationController usuarie: current_usuarie) if service.update.valid? + flash[:notice] = I18n.t('sites.update.post') redirect_to site_posts_path(site, locale: site.default_locale) else render 'edit' diff --git a/app/controllers/usuaries_controller.rb b/app/controllers/usuaries_controller.rb index 6d02a35a..6924c860 100644 --- a/app/controllers/usuaries_controller.rb +++ b/app/controllers/usuaries_controller.rb @@ -47,7 +47,7 @@ class UsuariesController < ApplicationController @usuarie = Usuarie.find(params[:usuarie_id]) if @site.usuaries.count > 1 - @usuarie.rol_for_site(@site).update_attribute :rol, 'invitade' + @usuarie.rol_for_site(@site).update_attribute :rol, Rol::INVITADE else flash[:warning] = I18n.t('usuaries.index.demote.denied') end @@ -61,7 +61,7 @@ class UsuariesController < ApplicationController authorize SiteUsuarie.new(@site, current_usuarie) @usuarie = Usuarie.find(params[:usuarie_id]) - @usuarie.rol_for_site(@site).update_attribute :rol, 'usuarie' + @usuarie.rol_for_site(@site).update_attribute :rol, Rol::USUARIE redirect_to site_usuaries_path end @@ -72,6 +72,8 @@ class UsuariesController < ApplicationController site_usuarie = SiteUsuarie.new(@site, current_usuarie) authorize site_usuarie + params[:invite_as] = invite_as + @policy = policy(site_usuarie) end @@ -81,27 +83,33 @@ class UsuariesController < ApplicationController authorize SiteUsuarie.new(@site, current_usuarie) # Enviar la invitación si es necesario y agregar al sitio - invitaciones.each do |invitacion| - # Si la cuenta no existe, envía una invitación por correo, sino, - # no se envía nada - # - # TODO: Enviar invitación igual! Podemos no usar el Mailer de - # DeviseInvitations y usar uno propio que contenga texto y se - # envíe de todas formas. - usuarie = Usuarie.invite! email: invitacion.address, - skip_invitation: true + invitaciones.each do |address| + next if Usuarie.where(id: @site.roles.pluck(:usuarie_id)).find_by_email(address) - # No invitar al sitio si ya estaba en la lista! - # - # XXX: En este caso no estamos enviando ninguna invitación - next if usuarie.sites.exists? @site.id + Usuarie.transaction do + usuarie = Usuarie.find_by_email(address) + usuarie ||= Usuarie.invite!({ email: address, skip_invitation: true }).tap do |u| + u.send :generate_invitation_token! + end - @site.roles << Rol.create(usuarie: usuarie, site: @site, - temporal: true, rol: invited_as) + role = @site.roles.create(usuarie: usuarie, temporal: true, rol: invited_as) - # Invitamos después de crear el rol para que el correo de - # invitación pueda recibir el sitio. - usuarie.deliver_invitation + # XXX: La invitación tiene que ser enviada luego de crear el rol + 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 + else + raise ArgumentError, role.errors.full_messages + end + rescue ArgumentError => e + ExceptionNotifier.notify_exception(e, data: { site: @site.name, address: address }) + end end redirect_to site_usuaries_path(@site) @@ -142,6 +150,8 @@ class UsuariesController < ApplicationController private # Traer todas las invitaciones que al menos tengan usuarie y dominio + # + # @return [Array] def invitaciones # XXX: Podríamos usar EmailAddress pero hace chequeos más lentos params[:invitaciones]&.tr("\r", '')&.split("\n")&.map do |m| @@ -150,17 +160,19 @@ class UsuariesController < ApplicationController nil end.compact.select do |m| m.local && m.domain - end + end.map(&:address) end # El tipo de invitación que tenemos que enviar, si alguien mandó # cualquier cosa, usamos el privilegio menor. + # + # @return [String] def invited_as - if Rol::ROLES.include?(params[:invited_as]) - params[:invited_as] - else - 'invitade' - end + Rol.role?(params[:invited_as]) ? params[:invited_as] : Rol::INVITADE + end + + def invite_as + Rol.role?(params[:invite_as]&.singularize) ? params[:invite_as] : Rol::INVITADE.pluralize end def site diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 492ca736..9cbc30bf 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -2,11 +2,21 @@ import { Notifier } from '@airbrake/browser' -window.airbrake = new Notifier({ - projectId: window.env.AIRBRAKE_SITE_ID, - projectKey: window.env.AIRBRAKE_API_KEY, - host: window.env.PANEL_URL -}) +try { + window.airbrake = new Notifier({ + projectId: window.env.AIRBRAKE_PROJECT_ID, + 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 'regenerator-runtime/runtime' diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index f3673f0a..06690c53 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -2,7 +2,7 @@ # Base para trabajos class ApplicationJob < ActiveJob::Base - include SuckerPunch::Job + include Que::ActiveJob::JobExtensions private diff --git a/app/jobs/backtrace_job.rb b/app/jobs/backtrace_job.rb index 86a9b2a6..97e6007b 100644 --- a/app/jobs/backtrace_job.rb +++ b/app/jobs/backtrace_job.rb @@ -6,8 +6,6 @@ class BacktraceJob < ApplicationJob EMPTY_SOURCEMAP = { 'mappings' => '' }.freeze - queue_as :low_priority - attr_reader :params, :site_id def perform(site_id:, params:) diff --git a/app/jobs/deploy_job.rb b/app/jobs/deploy_job.rb index e018533c..3044b59f 100644 --- a/app/jobs/deploy_job.rb +++ b/app/jobs/deploy_job.rb @@ -4,9 +4,21 @@ class DeployJob < ApplicationJob class DeployException < StandardError; end class DeployTimedOutException < DeployException; end + class DeployAlreadyRunningException < DeployException; end discard_on ActiveRecord::RecordNotFound + # Lanzar lo antes posible + self.priority = 10 + + def handle_error(error) + case error + when DeployAlreadyRunningException then retry_in 1.minute + when DeployTimedOutException then expire + else super + end + end + # rubocop:disable Metrics/MethodLength def perform(site, notify: true, time: Time.now, output: false) @output = output @@ -20,89 +32,106 @@ class DeployJob < ApplicationJob # Como el trabajo actual se aplaza al siguiente, arrastrar la # hora original para poder ir haciendo timeouts. if @site.building? + notify = false + if 10.minutes.ago >= time - notify = false raise DeployTimedOutException, "#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original" + else + raise DeployAlreadyRunningException + end + end + + @deployed = {} + @site.update status: 'building' + @site.deployment_list.each do |d| + begin + raise DeployException, 'Una dependencia falló' if failed_dependencies? d + + status = d.deploy(output: @output) + seconds = d.build_stats.last.try(:seconds) || 0 + size = d.size + urls = d.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 + status = false + seconds ||= 0 + size ||= 0 + # XXX: Hace que se vea la tabla + urls ||= [nil] + + notify_exception e, d end - DeployJob.perform_in(60, site, notify: notify, time: time, output: output) - return - end - - @site.update status: 'building' - # Asegurarse que DeployLocal sea el primero! - @deployed = { - deploy_local: { - status: deploy_locally, - seconds: deploy_local.build_stats.last.seconds, - size: deploy_local.size, - urls: [deploy_local.url] + @deployed[d.type.underscore.to_sym] = { + status: status, + seconds: seconds, + size: size, + urls: urls } - } - - # No es opcional - unless @deployed[:deploy_local][:status] - # Hacer fallar la tarea - raise DeployException, "#{@site.name}: Falló la compilación" end - deploy_others - rescue DeployTimedOutException => e - notify_exception e - rescue DeployException => e - notify_exception e, deploy_local - ensure - @site&.update status: 'waiting' + return unless @output - notify_usuaries if notify + puts (Terminal::Table.new do |t| + t << (%w[type] + @deployed.values.first.keys) + t.add_separator + @deployed.each do |type, row| + t << ([type.to_s] + row.values) + end + end) + ensure + if @site.present? + @site.update status: 'waiting' + + notify_usuaries if notify + + puts "\a" if @output + end end end # rubocop:enable Metrics/MethodLength private + # Detecta si un método de publicación tiene dependencias fallidas + # + # @param :deploy [Deploy] + # @return [Boolean] + def failed_dependencies?(deploy) + failed_dependencies(deploy).present? + end + + # Obtiene las dependencias fallidas de un deploy + # + # @param :deploy [Deploy] + # @return [Array] + def failed_dependencies(deploy) + deploy.class::DEPENDENCIES & (@deployed.reject do |_, v| + v[:status] + end.keys) + end + # @param :exception [StandardError] # @param :deploy [Deploy] def notify_exception(exception, deploy = nil) data = { site: @site.id, deploy: deploy&.type, - log: deploy&.build_stats&.last&.log + log: deploy&.build_stats&.last&.log, + failed_dependencies: (failed_dependencies(deploy) if deploy) } ExceptionNotifier.notify_exception(exception, data: data) end - def deploy_local - @deploy_local ||= @site.deploys.find_by(type: 'DeployLocal') - end - - def deploy_locally - deploy_local.deploy(output: @output) - end - - def deploy_others - @site.deploys.where.not(type: 'DeployLocal').find_each do |d| - begin - status = d.deploy(output: @output) - seconds = d.build_stats.last.try(:seconds) - rescue StandardError => e - status = false - seconds = 0 - - notify_exception e, d - end - - @deployed[d.type.underscore.to_sym] = { - status: status, - seconds: seconds || 0, - size: d.size, - urls: d.respond_to?(:urls) ? d.urls : [d.url].compact - } - end - end - def notify_usuaries @site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie| DeployMailer.with(usuarie: usuarie, site: @site.id) diff --git a/app/jobs/git_pull_job.rb b/app/jobs/git_pull_job.rb new file mode 100644 index 00000000..58a4e6b1 --- /dev/null +++ b/app/jobs/git_pull_job.rb @@ -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 diff --git a/app/jobs/git_push_job.rb b/app/jobs/git_push_job.rb new file mode 100644 index 00000000..3c62bee2 --- /dev/null +++ b/app/jobs/git_push_job.rb @@ -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 \ No newline at end of file diff --git a/app/jobs/gitlab_notifier_job.rb b/app/jobs/gitlab_notifier_job.rb index 701c6789..7308a4ef 100644 --- a/app/jobs/gitlab_notifier_job.rb +++ b/app/jobs/gitlab_notifier_job.rb @@ -10,13 +10,11 @@ class GitlabNotifierJob < ApplicationJob # Variables que vamos a acceder luego attr_reader :exception, :options, :issue_data, :cached - queue_as :low_priority - # @param [Exception] la excepción lanzada # @param [Hash] opciones de ExceptionNotifier def perform(exception, **options) @exception = exception - @options = options + @options = fix_options options @issue_data = { count: 1 } # Necesitamos saber si el issue ya existía @cached = false @@ -37,7 +35,7 @@ class GitlabNotifierJob < ApplicationJob } end - unless @issue['iid'] + if @issue['iid'].blank? && issue_data[:issue].blank? Rails.cache.delete(cache_key) raise GitlabNotifierError, @issue.dig('message', 'title')&.join(', ') end @@ -61,9 +59,9 @@ class GitlabNotifierJob < ApplicationJob Rails.cache.write(cache_key, issue_data) # Si este trabajo genera una excepción va a entrar en un loop, así que # la notificamos por correo - rescue Exception => e - email_notification.call(e) - email_notification.call(exception, options) + rescue StandardError => e + email_notification.call(e, data: @issue) + email_notification.call(exception, data: @options) end private @@ -84,10 +82,15 @@ class GitlabNotifierJob < ApplicationJob exception.class.name, Digest::SHA1.hexdigest(exception.message), Digest::SHA1.hexdigest(backtrace&.first.to_s), - Digest::SHA1.hexdigest(options.dig(:data, :params, 'errors').to_s) + Digest::SHA1.hexdigest(errors.to_s) ].join('/') end + # @return [Array] + def errors + options.dig(:data, :params, 'errors') || [] + end + # Define si es una excepción de javascript o local # # @see BacktraceJob @@ -126,6 +129,7 @@ class GitlabNotifierJob < ApplicationJob # @return [String] def body @body ||= ''.dup.tap do |b| + b << log_section b << request_section b << javascript_footer b << data_section @@ -162,14 +166,16 @@ class GitlabNotifierJob < ApplicationJob # @return [String] def log_section - return '' unless options[:log] + return '' unless options.dig(:data, :log) <<~LOG - # Log - ``` - #{options[:log]} - ``` + # Build log + + ``` + #{options[:data].delete(:log)} + ``` + LOG end @@ -257,8 +263,8 @@ class GitlabNotifierJob < ApplicationJob ## Data - ``` - #{pp options[:data]} + ```yaml + #{options[:data].to_yaml} ``` DATA @@ -279,4 +285,16 @@ class GitlabNotifierJob < ApplicationJob def url @url ||= request&.url || options.dig(:data, :params, 'context', 'url') end + + # Define llaves necesarias + # + # @param :options [Hash] + # @return [Hash] + def fix_options(options) + options = { data: options } unless options.is_a? Hash + options[:data] ||= {} + options[:data][:params] ||= {} + + options + end end diff --git a/app/jobs/maintenance_job.rb b/app/jobs/maintenance_job.rb index 4c411d0e..c7a962f9 100644 --- a/app/jobs/maintenance_job.rb +++ b/app/jobs/maintenance_job.rb @@ -10,7 +10,7 @@ # bundle exec rails c # m = Maintenance.create message_en: 'reason', message_es: 'razón', # 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 # are_we_back: true al crear el Maintenance. diff --git a/app/jobs/renew_distributed_press_tokens_job.rb b/app/jobs/renew_distributed_press_tokens_job.rb new file mode 100644 index 00000000..86086ac7 --- /dev/null +++ b/app/jobs/renew_distributed_press_tokens_job.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Renueva los tokens de Distributed Press antes que se venzan, +# activando los callbacks que hacen que se refresque el token. +class RenewDistributedPressTokensJob < ApplicationJob + # Renueva todos los tokens a punto de vencer o informa el error sin + # detener la tarea si algo pasa. + def perform + DistributedPressPublisher.with_about_to_expire_tokens.find_each do |publisher| + publisher.save + rescue DistributedPress::V1::Error => e + data = { instance: publisher.instance, expires_at: publisher.client.token.expires_at } + + ExceptionNotifier.notify_exception(e, data: data) + end + end +end diff --git a/app/lib/active_job/serializers/exception_serializer.rb b/app/lib/active_job/serializers/exception_serializer.rb new file mode 100644 index 00000000..42b55835 --- /dev/null +++ b/app/lib/active_job/serializers/exception_serializer.rb @@ -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 diff --git a/app/lib/active_storage/service/jekyll_service.rb b/app/lib/active_storage/service/jekyll_service.rb index 88ffa83c..e6c5fda6 100644 --- a/app/lib/active_storage/service/jekyll_service.rb +++ b/app/lib/active_storage/service/jekyll_service.rb @@ -4,11 +4,6 @@ module ActiveStorage class Service # Sube los archivos a cada repositorio y los agrega al LFS de su # repositorio git. - # - # @todo: Implementar LFS. No nos gusta mucho la idea porque duplica - # el espacio en disco, pero es la única forma que tenemos (hasta que - # implementemos IPFS) para poder transferir los archivos junto con el - # sitio. class JekyllService < Service::DiskService # Genera un servicio para un sitio determinado # @@ -27,7 +22,10 @@ module ActiveStorage # @param :checksum [String] def upload(key, io, checksum: nil, **) instrument :upload, key: key, checksum: checksum do - IO.copy_stream(io, make_path_for(key)) unless exist?(key) + unless exist?(key) + IO.copy_stream(io, make_path_for(key)) + LfsObjectService.new(site: site, blob: blob_for(key)).process + end ensure_integrity_of(key, checksum) if checksum end end @@ -79,7 +77,7 @@ module ActiveStorage # @param :key [String] # @return [String] def filename_for(key) - ActiveStorage::Blob.where(key: key).limit(1).pluck(:filename).first.tap do |filename| + blob_for(key).filename.to_s.tap do |filename| raise ArgumentError, "Filename for key #{key} is blank" if filename.blank? end end @@ -91,6 +89,15 @@ module ActiveStorage def path_for(key) File.join root, folder_for(key), filename_for(key) end + + # @return [Site] + def site + @site ||= Site.find_by_name(name) + end + + def blob_for(key) + ActiveStorage::Blob.find_by(key: key, service_name: name) + end end end end diff --git a/app/lib/devise/failure_app_decorator.rb b/app/lib/devise/failure_app_decorator.rb new file mode 100644 index 00000000..f17cb482 --- /dev/null +++ b/app/lib/devise/failure_app_decorator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Devise + module FailureAppDecorator + extend ActiveSupport::Concern + + included do + include AbstractController::Callbacks + + around_action :set_locale + + private + + def set_locale(&action) + I18n.with_locale(session[:locale] || I18n.locale, &action) + end + end + end +end + +Devise::FailureApp.include Devise::FailureAppDecorator diff --git a/app/lib/exception_notifier/gitlab_notifier.rb b/app/lib/exception_notifier/gitlab_notifier.rb index 18bfc6d4..947e4e44 100644 --- a/app/lib/exception_notifier/gitlab_notifier.rb +++ b/app/lib/exception_notifier/gitlab_notifier.rb @@ -8,10 +8,15 @@ module ExceptionNotifier # Recibe la excepción y empieza la tarea de notificación en segundo # plano. # - # @param [Exception] - # @param [Hash] - def call(exception, **options) - GitlabNotifierJob.perform_async(exception, **options) + # @param :exception [Exception] + # @param :options [Hash] + def call(exception, options, &block) + case exception + when BacktraceJob::BacktraceException + GitlabNotifierJob.perform_later(exception, **options) + else + GitlabNotifierJob.perform_now(exception, **options) + end end end end diff --git a/app/lib/hidden_service_client.rb b/app/lib/hidden_service_client.rb new file mode 100644 index 00000000..5715a869 --- /dev/null +++ b/app/lib/hidden_service_client.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'httparty' + +class HiddenServiceClient + include HTTParty + + base_uri ENV.fetch('HIDDEN_SERVICE', 'http://tor:3000') + + def create(name) + self.class.get("/#{name}").body + end +end diff --git a/app/mailers/deploy_mailer.rb b/app/mailers/deploy_mailer.rb index b7b464cb..37748b42 100644 --- a/app/mailers/deploy_mailer.rb +++ b/app/mailers/deploy_mailer.rb @@ -52,7 +52,7 @@ class DeployMailer < ApplicationMailer t << (row.map do |k, v| case k when :seconds then v[:human] - when :urls then url + when :urls then url.to_s else v end end) diff --git a/app/models/code_of_conduct.rb b/app/models/code_of_conduct.rb new file mode 100644 index 00000000..87c24c7f --- /dev/null +++ b/app/models/code_of_conduct.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Códigos de conducta +class CodeOfConduct < ApplicationRecord + extend Mobility + + translates :title, type: :string, locale_accessors: true + translates :description, type: :text, locale_accessors: true + translates :content, type: :text, locale_accessors: true + + validates :title, presence: true, uniqueness: true + validates :description, presence: true + validates :content, presence: true +end diff --git a/app/models/concerns/usuarie/consent.rb b/app/models/concerns/usuarie/consent.rb new file mode 100644 index 00000000..14e67fbc --- /dev/null +++ b/app/models/concerns/usuarie/consent.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Usuarie + # Gestiona los campos de consentimiento + module Consent + extend ActiveSupport::Concern + + included do + CONSENT_FIELDS = %i[privacy_policy_accepted terms_of_service_accepted code_of_conduct_accepted available_for_feedback_accepted] + + CONSENT_FIELDS.each do |field| + attribute field, :boolean + end + + before_save :update_consent_fields! + + private + + def update_consent_fields! + CONSENT_FIELDS.each do |field| + send(:"#{field}_at=", Time.now) if send(field).present? + end + end + end + end +end diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 9d5c1d27..1f087eb3 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'open3' + # Este modelo implementa los distintos tipos de alojamiento que provee # Sutty. # @@ -11,6 +12,9 @@ class Deploy < ApplicationRecord belongs_to :site has_many :build_stats, dependent: :destroy + DEPENDENCIES = [] + SOFT_DEPENDENCIES = [] + def deploy(**) raise NotImplementedError end @@ -19,6 +23,11 @@ class Deploy < ApplicationRecord raise NotImplementedError end + # @return [Array] + def urls + [url].compact + end + def limit raise NotImplementedError end @@ -51,6 +60,22 @@ class Deploy < ApplicationRecord @gems_dir ||= Rails.root.join('_storage', 'gems', site.name) 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 # estado. # @@ -61,22 +86,20 @@ class Deploy < ApplicationRecord lines = [] time_start - Dir.chdir(site.path) do - Open3.popen2e(env, cmd, unsetenv_others: true) do |_, o, t| - # TODO: Enviar a un websocket para ver el proceso en vivo? - Thread.new do - o.each do |line| - lines << line + Open3.popen2e(env, cmd, unsetenv_others: true, chdir: site.path) do |_, o, t| + # TODO: Enviar a un websocket para ver el proceso en vivo? + Thread.new do + o.each do |line| + lines << line - puts line if output - end - rescue IOError => e - lines << e.message - puts e.message if output + puts line if output end - - r = t.value + rescue IOError => e + lines << e.message + puts e.message if output end + + r = t.value end time_stop @@ -89,11 +112,60 @@ class Deploy < ApplicationRecord r&.success? end + # Variables de entorno + # + # @return [Hash] + def local_env + @local_env ||= {} + end + + # Devuelve opciones para jekyll build + # + # @return [String,nil] + def flags_for_build(**args); end + + # Trae todas las dependencias + # + # @return [Array] + def self.all_dependencies + self::DEPENDENCIES | self::SOFT_DEPENDENCIES + end + 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] # @return [String] def readable_cmd(cmd) cmd.split(' -', 2).first.tr(' ', '_') end + + def deploy_local + @deploy_local ||= site.deploys.find_by(type: 'DeployLocal') + end + + # Consigue todas las variables de entorno configuradas por otros + # deploys. + # + # @return [Hash] + def extra_env + @extra_env ||= + site.deployment_list.reduce({}) do |extra, deploy| + extra.merge deploy.local_env + end + end end diff --git a/app/models/deploy_alternative_domain.rb b/app/models/deploy_alternative_domain.rb index 5ad381d5..75b69180 100644 --- a/app/models/deploy_alternative_domain.rb +++ b/app/models/deploy_alternative_domain.rb @@ -4,6 +4,8 @@ class DeployAlternativeDomain < Deploy store :values, accessors: %i[hostname], coder: JSON + DEPENDENCIES = %i[deploy_local] + # Generar un link simbólico del sitio principal al alternativo def deploy(**) File.symlink?(destination) || @@ -18,7 +20,11 @@ class DeployAlternativeDomain < Deploy end def destination - @destination ||= File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, '')) + @destination ||= File.join(Rails.root, '_deploy', fqdn) + end + + def fqdn + hostname.gsub(/\.\z/, '') end def url diff --git a/app/models/deploy_distributed_press.rb b/app/models/deploy_distributed_press.rb new file mode 100644 index 00000000..da8fe209 --- /dev/null +++ b/app/models/deploy_distributed_press.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require 'distributed_press/v1/client/site' + +# Soportar Distributed Press APIv1 +# +# Usa tokens de publicación efímeros para todas las acciones. +# +# Al ser creado, genera el sitio en la instancia de Distributed Press +# configurada y almacena el ID. +# +# Al ser publicado, envía los archivos en un tarball y actualiza la +# información. +class DeployDistributedPress < Deploy + store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON + + before_create :create_remote_site! + before_destroy :delete_remote_site! + + DEPENDENCIES = %i[deploy_local] + + # Actualiza la información y luego envía los cambios + # + # @param :output [Bool] + # @return [Bool] + def deploy + status = false + log = [] + + time_start + + create_remote_site! if remote_site_id.blank? + save + + if remote_site_id.blank? + raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press' + end + + site_client.tap do |c| + stdout = Thread.new(publisher.logger_out) do |io| + until io.eof? + line = io.gets + + puts line if output + log << line + end + end + + 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 + self.remote_info[:distributed_press] = c.show(publishing_site).to_h + save + end + + publisher.logger.close + stdout.join + end + + time_stop + + create_stat! status, log.join + + status + end + + def limit; end + + def size + deploy_local.size + end + + def destination; end + + # Devuelve las URLs de todos los protocolos + def urls + gateway_urls + end + + private + + # @return [Array] + def gateway_urls + remote_info.dig(:distributed_press, :links)&.values&.map do |protocol| + [ protocol[:link], protocol[:gateway] ] + end&.flatten&.compact&.select do |link| + link.include? '://' + end || [] + end + + # El cliente de la API + # + # TODO: cuando soportemos más, tiene que haber una relación entre + # DeployDistributedPress y DistributedPressPublisher. + # + # @return [DistributedPressPublisher] + def publisher + @publisher ||= DistributedPressPublisher.last + end + + # El cliente para actualizar el sitio + # + # @return [DistributedPress::V1::Client::Site] + def site_client + DistributedPress::V1::Client::Site.new(publisher.client) + end + + # Genera el esquema de datos para poder publicar el sitio + # + # @return [DistributedPress::V1::Schemas::PublishingSite] + def publishing_site + DistributedPress::V1::Schemas::PublishingSite.new.call(id: remote_site_id) + end + + # Genera el esquema de datos para crear el sitio + # + # @return [DistributedPressPublisher::V1::Schemas::NewSite] + def create_site + DistributedPress::V1::Schemas::NewSite.new.call(domain: hostname, protocols: { http: true, ipfs: true, hyper: true }) + end + + # Crea el sitio en la instancia con el hostname especificado + # + # @return [nil] + def create_remote_site! + created_site = site_client.create(create_site) + + self.remote_site_id = created_site[:id] + self.remote_info ||= {} + self.remote_info[:distributed_press] = created_site.to_h + nil + rescue DistributedPress::V1::Error => e + ExceptionNotifier.notify_exception(e, data: { site: site.name }) + nil + end + + # Registra lo que sucedió + # + # @param status [Bool] + # @param log [String] + # @return [nil] + def create_stat!(status, log) + build_stats.create action: publisher.to_s,log: log, seconds: time_spent_in_seconds, bytes: size, status: status + nil + end + + def delete_remote_site! + site_client.delete(publishing_site) + nil + rescue DistributedPress::V1::Error => e + ExceptionNotifier.notify_exception(e, data: { site: site.name }) + nil + end +end diff --git a/app/models/deploy_full_rsync.rb b/app/models/deploy_full_rsync.rb new file mode 100644 index 00000000..b417470a --- /dev/null +++ b/app/models/deploy_full_rsync.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class DeployFullRsync < DeployRsync + SOFT_DEPENDENCIES = %i[ + deploy_alternative_domain + deploy_localized_domain + deploy_hidden_service + deploy_www + ] + + # Sincroniza las ubicaciones alternativas también, ignorando las que + # todavía no se generaron. Solo falla si ningún sitio fue + # sincronizado o si alguna sincronización falló. + # + # @param :output [Boolean] + # @return [Boolean] + def rsync(output: false) + result = + self.class.all_dependencies.map(&:to_s).map(&:classify).map do |dependency| + site.deploys.where(type: dependency).find_each.map do |deploy| + next unless File.exist? deploy.destination + + run %(rsync -aviH --delete-after --timeout=5 #{Shellwords.escape deploy.destination} #{Shellwords.escape destination}), output: output + rescue StandardError + end + end.flatten.compact + + result.present? && result.all? + end +end diff --git a/app/models/deploy_hidden_service.rb b/app/models/deploy_hidden_service.rb index dc9549f5..25c0c217 100644 --- a/app/models/deploy_hidden_service.rb +++ b/app/models/deploy_hidden_service.rb @@ -2,17 +2,36 @@ # Genera una versión onion class DeployHiddenService < DeployWww - def deploy(**) - return true if fqdn.blank? + store :values, accessors: %i[onion], coder: JSON - super - end + before_create :create_hidden_service! + + ONION_RE = /\A[a-z0-9]{56}\.onion\z/.freeze def fqdn - values[:onion] + create_hidden_service! if onion.blank? + + onion.tap do |onion| + raise ArgumentError, 'Aun no se generó la dirección .onion' if onion.blank? + end end def url "http://#{fqdn}" end + + private + + def create_hidden_service! + onion_address = HiddenServiceClient.new.create(site.name) + + if ONION_RE =~ onion_address + self.onion = onion_address + + usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor' + params = { onion: onion_address, deploy: self } + + SiteService.new(site: site, usuarie: usuarie, params: params).add_onion + end + end end diff --git a/app/models/deploy_local.rb b/app/models/deploy_local.rb index 00e0985d..bf1b7680 100644 --- a/app/models/deploy_local.rb +++ b/app/models/deploy_local.rb @@ -14,7 +14,9 @@ class DeployLocal < Deploy # Sutty def deploy(output: false) return false unless mkdir + return false unless git_lfs(output: output) return false unless yarn(output: output) + return false unless pnpm(output: output) return false unless bundle(output: output) jekyll_build(output: output) @@ -60,34 +62,34 @@ class DeployLocal < Deploy FileUtils.rm_rf(File.join(site.path, '.jekyll-cache')) 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 def mkdir FileUtils.mkdir_p destination end - # Un entorno que solo tiene lo que necesitamos - def env - # XXX: This doesn't support Windows paths :B - paths = [File.dirname(`which bundle`), '/usr/bin', '/bin'] - - { - 'HOME' => home_dir, - 'PATH' => paths.join(':'), - 'SPREE_API_KEY' => site.tienda_api_key, - 'SPREE_URL' => site.tienda_url, - 'AIRBRAKE_PROJECT_ID' => site.id.to_s, - 'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key, - 'JEKYLL_ENV' => Rails.env, - 'LANG' => ENV['LANG'], - 'YARN_CACHE_FOLDER' => yarn_cache_dir - } - end - def yarn_cache_dir Rails.root.join('_yarn_cache').to_s end + def pnpm_cache_dir + Rails.root.join('_pnpm_cache').to_s + end + def yarn_lock File.join(site.path, 'yarn.lock') end @@ -96,6 +98,19 @@ class DeployLocal < Deploy File.exist? yarn_lock end + def pnpm_lock + File.join(site.path, 'pnpm-lock.yaml') + end + + def pnpm_lock? + File.exist? pnpm_lock + end + + def git_lfs(output: false) + run %(git lfs fetch), output: output + run %(git lfs checkout), output: output + end + def gem(output: false) run %(gem install bundler --no-document), output: output end @@ -107,12 +122,28 @@ class DeployLocal < Deploy run 'yarn install --production', output: output end + def pnpm(output: false) + return true unless pnpm_lock? + + run %(pnpm config set store-dir "#{pnpm_cache_dir}"), output: output + run 'pnpm install --production', output: output + end + def 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 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 # no debería haber espacios ni caracteres especiales, pero por si @@ -125,4 +156,14 @@ class DeployLocal < Deploy def remove_destination! FileUtils.rm_rf destination end + + # Genera opciones extra desde los otros deploys + # + # @param :args [Hash] + # @return [String] + def extra_flags(**args) + site.deployment_list.map do |deploy| + deploy.flags_for_build(**args) + end.compact.join(' ') + end end diff --git a/app/models/deploy_private.rb b/app/models/deploy_private.rb index d3bfb50d..1fa42648 100644 --- a/app/models/deploy_private.rb +++ b/app/models/deploy_private.rb @@ -6,6 +6,8 @@ # XXX: La plantilla tiene que soportar esto con el plugin # jekyll-private-data class DeployPrivate < DeployLocal + DEPENDENCIES = %i[deploy_local] + # No es necesario volver a instalar dependencias def deploy(output: false) jekyll_build(output: output) diff --git a/app/models/deploy_rsync.rb b/app/models/deploy_rsync.rb index a658de6b..fcc5a65d 100644 --- a/app/models/deploy_rsync.rb +++ b/app/models/deploy_rsync.rb @@ -3,7 +3,9 @@ # Sincroniza sitios a servidores remotos usando Rsync. El servidor # remoto tiene que tener rsync instalado. class DeployRsync < Deploy - store :values, accessors: %i[destination host_keys], coder: JSON + store :values, accessors: %i[hostname destination host_keys], coder: JSON + + DEPENDENCIES = %i[deploy_local deploy_zip] def deploy(output: false) ssh? && rsync(output: output) @@ -23,6 +25,11 @@ class DeployRsync < Deploy end end + # @return [String] + def url + "https://#{hostname}/" + end + private # Verificar la conexión SSH implementando Trust On First Use @@ -31,6 +38,7 @@ class DeployRsync < Deploy # # @return [Boolean] def ssh? + return true if destination.start_with? 'rsync://' user, host = user_host ssh_available = false @@ -83,8 +91,8 @@ class DeployRsync < Deploy # Sincroniza hacia el directorio remoto # # @return [Boolean] - def rsync(output: output) - run %(rsync -aviH --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/), output: output + def rsync(output: false) + run %(rsync -aviH --delete-after --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/), output: output end # El origen es el destino de la compilación diff --git a/app/models/deploy_social_distributed_press.rb b/app/models/deploy_social_distributed_press.rb new file mode 100644 index 00000000..db555ab7 --- /dev/null +++ b/app/models/deploy_social_distributed_press.rb @@ -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 diff --git a/app/models/deploy_www.rb b/app/models/deploy_www.rb index dff769a6..bb25cc64 100644 --- a/app/models/deploy_www.rb +++ b/app/models/deploy_www.rb @@ -4,9 +4,13 @@ class DeployWww < Deploy store :values, accessors: %i[], coder: JSON + DEPENDENCIES = %i[deploy_local] + before_destroy :remove_destination! - def deploy(**) + def deploy(output: false) + puts "Creando symlink #{site.hostname} => #{destination}" if output + File.symlink?(destination) || File.symlink(site.hostname, destination).zero? end @@ -28,7 +32,7 @@ class DeployWww < Deploy end def url - "https://www.#{site.hostname}/" + "https://#{fqdn}/" end private diff --git a/app/models/deploy_zip.rb b/app/models/deploy_zip.rb index f1c94083..85005470 100644 --- a/app/models/deploy_zip.rb +++ b/app/models/deploy_zip.rb @@ -8,28 +8,49 @@ require 'zip' class DeployZip < Deploy store :values, accessors: %i[], coder: JSON + DEPENDENCIES = %i[deploy_local] + # Una vez que el sitio está generado, tomar todos los archivos y # y generar un zip accesible públicamente. # # rubocop:disable Metrics/MethodLength - def deploy(**) + def deploy(output: false) FileUtils.rm_f path - time_start - Dir.chdir(destination) do - Zip::File.open(path, Zip::File::CREATE) do |z| - Dir.glob('./**/**').each do |f| - File.directory?(f) ? z.mkdir(f) : z.add(f, f) + Zip::File.open(path, Zip::File::CREATE) do |zip| + Dir.glob(File.join(destination, '**', '**')).each do |file| + entry = Pathname.new(file).relative_path_from(destination).to_s + + if File.directory? file + log "Creando directorio #{entry}", output + + zip.mkdir(entry) + else + log "Comprimiendo #{entry}", output + zip.add(entry, file) end end end + time_stop - build_stats.create action: 'zip', - seconds: time_spent_in_seconds, - bytes: size + File.exist?(path).tap do |status| + build_stats.create action: 'zip', + seconds: time_spent_in_seconds, + bytes: size, + log: @log.join("\n"), + status: status + end + rescue Zip::Error => e + ExceptionNotifier.notify_exception(e, data: { site: site.name }) - File.exist? path + build_stats.create action: 'zip', + seconds: 0, + bytes: 0, + log: @log.join("\n"), + status: false + + false end # rubocop:enable Metrics/MethodLength @@ -41,8 +62,11 @@ class DeployZip < Deploy File.size path end + # @return [String] def destination - File.join(Rails.root, '_deploy', site.hostname) + Rails.root.join('_deploy', site.hostname).realpath.to_s + rescue Errno::ENOENT + Rails.root.join('_deploy', site.hostname).to_s end def file @@ -56,4 +80,15 @@ class DeployZip < Deploy def path File.join(destination, file) end + + private + + # @param :line [String] + # @param :output [Boolean] + def log(line, output) + @log ||= [] + @log << line + + puts line if output + end end diff --git a/app/models/distributed_press_publisher.rb b/app/models/distributed_press_publisher.rb new file mode 100644 index 00000000..6139db93 --- /dev/null +++ b/app/models/distributed_press_publisher.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'distributed_press/v1' + +# Almacena el token de autenticación y la URL, por ahora solo vamos +# a tener uno, pero queda abierta la posibilidad de agregar más. +class DistributedPressPublisher < ApplicationRecord + # Cifrar la información del token en la base de datos + has_encrypted :token + + # La salida del log + # + # @return [IO] + attr_reader :logger_out + + # La instancia es única + validates_uniqueness_of :instance + + # El token es necesario + validates_presence_of :token + + # Mantener la fecha de vencimiento actualizada + before_save :update_expires_at_from_token!, :update_token_from_client! + + # Devuelve todos los tokens que vencen en una hora + scope :with_about_to_expire_tokens, lambda { + where('expires_at > ? and expires_at < ?', Time.now, Time.now + 1.hour) + } + + # Instancia un cliente de Distributed Press a partir del token. Al + # cargar un token a punto de vencer se renueva automáticamente. + # + # @return [DistributedPress::V1::Client] + def client + @client ||= DistributedPress::V1::Client.new(url: instance, token: token, logger: logger) + end + + # @return [String] + def to_s + "Distributed Press <#{instance}>" + end + + # Devuelve el hostname de la instancia + # + # @return [String] + def hostname + @hostname ||= URI.parse(instance).hostname + end + + # @return [Logger] + def logger + @logger ||= + begin + @logger_out, @logger_in = IO.pipe + ::Logger.new @logger_in, formatter: formatter + end + end + + private + + def formatter + @formatter ||= lambda do |_, _, _, msg| + "#{msg}\n" + end + end + + # Actualiza o desactiva la fecha de vencimiento a partir de la + # información del token. + # + # @return [nil] + def update_expires_at_from_token! + self.expires_at = client.token.forever? ? nil : client.token.expires_at + nil + end + + # Actualiza el token a partir del cliente, que ya actualiza el token + # automáticamente. + # + # @return [nil] + def update_token_from_client! + self.token = client.token.to_s + nil + end +end diff --git a/app/models/indexed_post.rb b/app/models/indexed_post.rb index 7f6865f6..184cd05f 100644 --- a/app/models/indexed_post.rb +++ b/app/models/indexed_post.rb @@ -36,6 +36,15 @@ class IndexedPost < ApplicationRecord 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 # # @param [String,Symbol] diff --git a/app/models/layout.rb b/app/models/layout.rb index c70829fa..efca66ee 100644 --- a/app/models/layout.rb +++ b/app/models/layout.rb @@ -9,6 +9,13 @@ Layout = Struct.new(:site, :name, :meta, :metadata, keyword_init: true) do name.to_s 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 @attributes ||= metadata.keys.map(&:to_sym) end diff --git a/app/models/licencia.rb b/app/models/licencia.rb index c0eb1c80..65009f46 100644 --- a/app/models/licencia.rb +++ b/app/models/licencia.rb @@ -7,6 +7,7 @@ class Licencia < ApplicationRecord translates :name, type: :string, locale_accessors: true translates :url, type: :string, locale_accessors: true translates :description, type: :text, locale_accessors: true + translates :short_description, type: :string, locale_accessors: true translates :deed, type: :text, locale_accessors: true has_many :sites @@ -14,5 +15,10 @@ class Licencia < ApplicationRecord validates :name, presence: true, uniqueness: true validates :url, presence: true validates :description, presence: true + validates :short_description, presence: true validates :deed, presence: true + + def custom? + icons == 'custom' + end end diff --git a/app/models/log_entry.rb b/app/models/log_entry.rb index 1824da55..9685e0d0 100644 --- a/app/models/log_entry.rb +++ b/app/models/log_entry.rb @@ -11,7 +11,7 @@ class LogEntry < ApplicationRecord def resend return if sent - ContactJob.perform_async site_id, params[:form], params + ContactJob.perform_later site_id, params[:form], params end def params diff --git a/app/models/metadata_boolean.rb b/app/models/metadata_boolean.rb index 90c002a7..9932c6fd 100644 --- a/app/models/metadata_boolean.rb +++ b/app/models/metadata_boolean.rb @@ -4,8 +4,12 @@ # # Esto es increíblemente difícil de lograr que salga bien! 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 - false + !!super end # Los checkboxes son especiales porque la especificación de HTML diff --git a/app/models/metadata_created_at.rb b/app/models/metadata_created_at.rb new file mode 100644 index 00000000..d31b3a1c --- /dev/null +++ b/app/models/metadata_created_at.rb @@ -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 diff --git a/app/models/metadata_locales.rb b/app/models/metadata_locales.rb index 4d540efc..37b50286 100644 --- a/app/models/metadata_locales.rb +++ b/app/models/metadata_locales.rb @@ -1,22 +1,49 @@ # frozen_string_literal: true # Los valores de este metadato son artículos en otros idiomas -class MetadataLocales < MetadataTemplate - def default_value - super || [] - end - +class MetadataLocales < MetadataHasAndBelongsToMany # Todos los valores posibles para cada idioma disponible # - # TODO: Optimizar? - # TODO: Mantener sincronizados - # # @return { lang: { title: uuid } } def values @values ||= site.locales.map do |locale| - [locale, site.posts(lang: locale).map do |post| - [post.title.value, post.uuid.value] + [locale, posts.where(lang: locale).map do |post| + [title(post), post.uuid.value] end.to_h] end.to_h end + + # Siempre hay una relación inversa + # + # @return [True] + def inverse? + true + end + + # El campo inverso se llama igual en el otro post + # + # @return [Symbol] + def inverse + :locales + end + + private + + # Obtiene todos los locales distintos a este post + # + # @return [Array] + def other_locales + site.locales.reject do |locale| + locale == post.lang.value.to_sym + end + end + + # Obtiene todos los posts de los otros locales con el mismo layout + # + # @return [PostRelation] + def posts + other_locales.map do |locale| + site.posts(lang: locale).where(layout: post.layout.value) + end.reduce(&:concat) || PostRelation.new(site: site, lang: 'any') + end end diff --git a/app/models/metadata_related_posts.rb b/app/models/metadata_related_posts.rb index 092f219a..42d1381b 100644 --- a/app/models/metadata_related_posts.rb +++ b/app/models/metadata_related_posts.rb @@ -34,7 +34,7 @@ class MetadataRelatedPosts < MetadataArray end 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 # Encuentra el filtro diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index 5de54be1..b324e71c 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -202,7 +202,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, def allowed_attributes @allowed_attributes ||= %w[style href src alt controls data-align data-multimedia data-multimedia-inner id - name].freeze + name start].freeze end def allowed_tags diff --git a/app/models/post.rb b/app/models/post.rb index 5cc1c5ea..fab9ab06 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -12,7 +12,7 @@ class Post DEFAULT_ATTRIBUTES = %i[site document layout].freeze # Otros atributos que no vienen en los metadatos 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_reader :attributes, :errors, :layout, :site, :document @@ -217,6 +217,11 @@ class Post post: self, required: true) 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 # plantilla def attribute?(mid) @@ -267,6 +272,7 @@ class Post # Y que no se procese liquid yaml['liquid'] = false yaml['usuaries'] = usuaries.map(&:id).uniq + yaml['created_at'] = created_at.value yaml['last_modified_at'] = modified_at "#{yaml.to_yaml}---\n\n#{body}" diff --git a/app/models/privacy_policy.rb b/app/models/privacy_policy.rb new file mode 100644 index 00000000..8805daa9 --- /dev/null +++ b/app/models/privacy_policy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Políticas de privacidad +class PrivacyPolicy < ApplicationRecord + extend Mobility + + translates :title, type: :string, locale_accessors: true + translates :description, type: :text, locale_accessors: true + translates :content, type: :text, locale_accessors: true + + validates :title, presence: true, uniqueness: true + validates :description, presence: true + validates :content, presence: true +end diff --git a/app/models/rol.rb b/app/models/rol.rb index 5879d666..37332400 100644 --- a/app/models/rol.rb +++ b/app/models/rol.rb @@ -14,6 +14,8 @@ class Rol < ApplicationRecord validates_inclusion_of :rol, in: ROLES + before_save :add_token_if_missing! + def invitade? rol == INVITADE end @@ -21,4 +23,15 @@ class Rol < ApplicationRecord def usuarie? rol == USUARIE end + + def self.role?(rol) + ROLES.include? rol + end + + private + + # Asegurarse que tenga un token + def add_token_if_missing! + self.token ||= SecureRandom.hex(64) + end end diff --git a/app/models/site.rb b/app/models/site.rb index d0557f78..4403bc00 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -7,6 +7,10 @@ class Site < ApplicationRecord include Site::Forms include Site::FindAndReplace include Site::Api + include Site::DeployDependencies + include Site::BuildStats + include Site::LayoutOrdering + include Site::SocialDistributedPress include Tienda # Cifrar la llave privada que cifra y decifra campos ocultos. Sutty @@ -15,10 +19,6 @@ class Site < ApplicationRecord # protege de acceso al panel de Sutty! 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].freeze - validates :name, uniqueness: true, hostname: { allow_root_label: true } @@ -444,6 +444,10 @@ class Site < ApplicationRecord find_by(name: "#{Site.domain}.") end + def self.one_at_a_time + @@one_at_a_time ||= Thread::Mutex.new + end + def reset @read = false @layouts = nil @@ -471,6 +475,9 @@ class Site < ApplicationRecord return if jekyll? Rugged::Repository.clone_at(ENV['SKEL_SUTTY'], path, checkout_branch: design.gem) + + # Necesita un bloque + repository.rugged.remotes.rename('origin', 'upstream') {} end # Elimina el directorio del sitio @@ -494,8 +501,8 @@ class Site < ApplicationRecord config.theme = design.gem unless design.no_theme? config.description = description config.title = title - config.url = url(slash: false) - config.hostname = hostname + config.url ||= url(slash: false) + config.hostname ||= hostname config.locales = locales.map(&:to_s) end @@ -550,13 +557,40 @@ class Site < ApplicationRecord end def run_in_path(&block) - Dir.chdir path, &block + Site.one_at_a_time.synchronize do + Dir.chdir path, &block + end end + # Instala las gemas cuando es necesario: + # + # * El sitio existe + # * No están instaladas + # * El archivo Gemfile se modificó + # * El archivo Gemfile.lock se modificó def install_gems return unless persisted? - return if Rails.root.join('_storage', 'gems', name).directory? - deploys.find_by_type('DeployLocal').send(:bundle) + deploys.find_by_type('DeployLocal').send(:git_lfs) + + if !gem_dir? || gemfile_updated? || gemfile_lock_updated? + deploys.find_by_type('DeployLocal').send(:bundle) + touch + end + end + + # Detecta si el repositorio de gemas existe + def gem_dir? + Rails.root.join('_storage', 'gems', name).directory? + end + + # Detecta si el Gemfile fue modificado + def gemfile_updated? + updated_at < File.mtime(File.join(path, 'Gemfile')) + end + + # Detecta si el Gemfile.lock fue modificado + def gemfile_lock_updated? + updated_at < File.mtime(File.join(path, 'Gemfile.lock')) end end diff --git a/app/models/site/build_stats.rb b/app/models/site/build_stats.rb new file mode 100644 index 00000000..071b1eab --- /dev/null +++ b/app/models/site/build_stats.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +class Site + module BuildStats + extend ActiveSupport::Concern + + included do + # Devuelve el tiempo promedio de publicación para este sitio + # + # @return [Integer] + def average_publication_time + build_stats.group(:action).average(:seconds).values.reduce(:+).round + end + + # Devuelve el tiempo promedio de compilación para sitios similares + # a este. + # + # @return [Integer] + def average_publication_time_for_similar_sites + similar_deploys = Deploy.where(type: deploys.pluck(:type)).pluck(:id) + + BuildStat.where(deploy_id: similar_deploys).group(:action).average(:seconds).values.reduce(:+).round + end + + # Define si podemos calcular el tiempo promedio de publicación + # para este sitio + # + # @return [Boolean] + def average_publication_time_calculable? + build_stats.jekyll.where(status: true).count > 1 + end + + def similar_sites? + !design.no_theme? + end + + # Detecta si el sitio todavía no ha sido publicado + # + # @return [Boolean] + def not_published_yet? + build_stats.jekyll.where(status: true).count.zero? + end + + # Cambios posibles luego de la última publicación exitosa: + # + # * Artículos modificados + # * Configuración modificada + # * Métodos de publicación añadidos + # + # @return [Boolean] + def awaiting_publication? + waiting? && (post_pending? || deploy_pending? || configuration_pending?) + end + + # Se modificaron artículos después de publicar el sitio por última + # vez + # + # @return [Boolean] + def post_pending? + last_indexed_post_time > last_publication_time + end + + # Se modificó el sitio después de publicarlo por última vez + # + # @return [Boolean] + def deploy_pending? + last_deploy_time > last_publication_time + end + + # Se modificó la configuración del sitio + # + # @return [Boolean] + def configuration_pending? + last_configuration_time > last_publication_time + end + + private + + # Encuentra la fecha del último artículo modificado. Si no hay + # ninguno, devuelve la fecha de modificación del sitio. + # + # @return [Time] + def last_indexed_post_time + indexed_posts.order(updated_at: :desc).select(:updated_at).first&.updated_at || updated_at + end + + # Encuentra la fecha de última modificación de los métodos de + # publicación. + # + # @return [Time] + def last_deploy_time + deploys.order(created_at: :desc).select(:created_at).first&.created_at || updated_at + end + + # Encuentra la fecha de última publicación exitosa, si no hay + # ninguno, devuelve la fecha de modificación del sitio. + # + # @return [Time] + def last_publication_time + build_stats.jekyll.where(status: true).order(created_at: :desc).select(:created_at).first&.created_at || updated_at + end + + # Fecha de última modificación de la configuración + # + # @return [Time] + def last_configuration_time + File.mtime(config.path) + end + end + end +end diff --git a/app/models/site/config.rb b/app/models/site/config.rb index d2e78d98..fb9175c1 100644 --- a/app/models/site/config.rb +++ b/app/models/site/config.rb @@ -31,7 +31,7 @@ class Site # Escribe los cambios en el repositorio def write - return if persisted? + return true if persisted? @saved = Site::Writer.new(site: site, file: path, content: content.to_yaml).save.tap do |result| # Actualizar el hash para no escribir dos veces diff --git a/app/models/site/deploy_dependencies.rb b/app/models/site/deploy_dependencies.rb new file mode 100644 index 00000000..ed80a8af --- /dev/null +++ b/app/models/site/deploy_dependencies.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rgl/adjacency' +require 'rgl/topsort' + +class Site + module DeployDependencies + extend ActiveSupport::Concern + + included do + # Genera un grafo dirigido de todos los métodos de publicación + # + # @return [RGL::DirectedAdjacencyGraph] + def deployment_graph + @deployment_graph ||= RGL::DirectedAdjacencyGraph.new.tap do |graph| + deploys.each do |deploy| + graph.add_vertex deploy + end + + deploys.each do |deploy| + deploy.class.all_dependencies.each do |dependency| + deploys.where(type: dependency.to_s.classify).each do |deploy_dependency| + graph.add_edge deploy_dependency, deploy + end + end + end + end + end + + # Devuelve una lista ordenada de todos los métodos de publicación + # + # @return [Array] + def deployment_list + @deployment_list ||= deployment_graph.topsort_iterator.to_a + end + end + end +end diff --git a/app/models/site/find_and_replace.rb b/app/models/site/find_and_replace.rb index 2670159d..5da673c2 100644 --- a/app/models/site/find_and_replace.rb +++ b/app/models/site/find_and_replace.rb @@ -37,7 +37,7 @@ class Site author = GitAuthor.new email: "sutty@#{Site.domain}", name: 'Sutty' - repository.commit(file: modified, + repository.commit(add: modified, message: I18n.t('sites.find_and_replace'), usuarie: author) end diff --git a/app/models/site/index.rb b/app/models/site/index.rb index e11095e3..cfa4030a 100644 --- a/app/models/site/index.rb +++ b/app/models/site/index.rb @@ -1,22 +1,125 @@ # frozen_string_literal: true -# Indexa todos los artículos de un sitio -# -# TODO: Hacer opcional class Site + # Indexa todos los artículos de un sitio + # + # TODO: Hacer opcional module Index extend ActiveSupport::Concern included do - # TODO: Debería ser un Job? - after_create :index_posts! 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! Site.transaction do docs.each(&:index!) + + update(last_indexed_commit: repository.head_commit.oid) 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] + 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] + 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 diff --git a/app/models/site/layout_ordering.rb b/app/models/site/layout_ordering.rb new file mode 100644 index 00000000..9fecbf21 --- /dev/null +++ b/app/models/site/layout_ordering.rb @@ -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 diff --git a/app/models/site/repository.rb b/app/models/site/repository.rb index f63288d4..acbf6553 100644 --- a/app/models/site/repository.rb +++ b/app/models/site/repository.rb @@ -29,7 +29,7 @@ class Site # Obtiene el origin # - # @return [Rugged::Remote] + # @return [Rugged::Remote, nil] def origin @origin ||= rugged.remotes.find do |remote| remote.name == 'origin' @@ -54,7 +54,7 @@ class Site # Incorpora los cambios en el repositorio actual # # @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) # No hacemos nada si hay conflictos, pero notificarnos @@ -69,12 +69,16 @@ class Site .create(rugged, update_ref: 'HEAD', parents: [head_commit, remote_head_commit], tree: merge.write_tree(rugged), - message: I18n.t('sites.fetch.merge.message'), + message: message, author: author(usuarie), committer: committer) # Forzamos el checkout para mover el HEAD al último commit y # escribir los cambios 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 end @@ -114,11 +118,21 @@ class Site end # 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 + rugged.index.read_tree rugged.head.target.tree - file.each do |f| - remove ? rm(f) : add(f) + add.each do |file| + rugged.index.add(relativize(file)) + end + + rm.each do |file| + rugged.index.remove(relativize(file)) end # Escribir los cambios para que el repositorio se vea tal cual @@ -139,41 +153,58 @@ class Site { name: 'Sutty', email: "sutty@#{Site.domain}", time: Time.now } end - def add(file) - rugged.index.add(relativize(file)) - end - - def rm(file) - rugged.index.remove(relativize(file)) - end - # Garbage collection # # @return [Boolean] def gc - env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path } - cmd = 'git gc' + git_sh("git", "gc") + end - r = nil - Dir.chdir(path) do - Open3.popen2e(env, cmd, unsetenv_others: true) do |_, _, t| - r = t.value - end - end - - r&.success? + # Pushea cambios al repositorio remoto + # + # @param :remote [Rugged::Remote] + # @return [Boolean, nil] + def push(remote = origin) + remote.push(rugged.head.canonical_name, credentials: credentials_for(remote)) + git_sh('git', 'lfs', 'push', remote.name, default_branch) end private + # @deprecated + def credentials + @credentials ||= credentials_for(origin) + end + # Si Sutty tiene una llave privada de tipo ED25519, devuelve las # credenciales necesarias para trabajar con repositorios remotos. # + # @param :remote [Rugged::Remote] # @return [Nil, Rugged::Credentials::SshKey] - def credentials + def credentials_for(remote) 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 # @return [String] @@ -189,5 +220,20 @@ class Site def relativize(file) Pathname.new(file).relative_path_from(Pathname.new(path)).to_s 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 diff --git a/app/models/site/social_distributed_press.rb b/app/models/site/social_distributed_press.rb new file mode 100644 index 00000000..5d469f03 --- /dev/null +++ b/app/models/site/social_distributed_press.rb @@ -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 diff --git a/app/models/site_build_stat.rb b/app/models/site_build_stat.rb new file mode 100644 index 00000000..1a63a0bb --- /dev/null +++ b/app/models/site_build_stat.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +SiteBuildStat = Struct.new(:site) diff --git a/app/models/usuarie.rb b/app/models/usuarie.rb index c87d82f9..42f20c0b 100644 --- a/app/models/usuarie.rb +++ b/app/models/usuarie.rb @@ -2,14 +2,19 @@ # Usuarie de la plataforma class Usuarie < ApplicationRecord + include Usuarie::Consent + devise :invitable, :database_authenticatable, :recoverable, :rememberable, :validatable, :confirmable, :lockable, :registerable validates_uniqueness_of :email validates_with EmailAddress::ActiveRecordValidator, field: :email + validate :locale_available! before_create :lang_from_locale! + before_update :remove_confirmation_invitation_inconsistencies! + before_update :accept_invitation_after_confirmation! has_many :roles has_many :sites, through: :roles @@ -41,9 +46,48 @@ class Usuarie < ApplicationRecord lock_access! if attempts_exceeded? && !access_locked? end + def send_devise_notification(notification, *args) + I18n.with_locale(lang) do + devise_mailer.send(notification, self, *args).deliver_later + end + end + + # Les usuaries necesitan link de invitación si no tenían cuenta + # y todavía no aceptaron la invitación anterior. + def needs_invitation_link? + created_by_invite? && !invitation_accepted? + end + private def lang_from_locale! self.lang = I18n.locale.to_s end + + # El invitation_token solo es necesario cuando fue creade por otre + # usuarie. De lo contrario lo que queremos es un proceso de + # confirmación. + def remove_confirmation_invitation_inconsistencies! + self.invitation_token = nil unless created_by_invite? + end + + # Si le usuarie (re)confirma su cuenta con una invitación pendiente, + # considerarla aceptada también. + def accept_invitation_after_confirmation! + if confirmed? + self.invitation_token = nil + self.invitation_accepted_at ||= Time.now.utc + end + end + + # 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 diff --git a/app/policies/indexed_post_policy.rb b/app/policies/indexed_post_policy.rb new file mode 100644 index 00000000..e0151c7a --- /dev/null +++ b/app/policies/indexed_post_policy.rb @@ -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 diff --git a/app/policies/site_build_stat_policy.rb b/app/policies/site_build_stat_policy.rb new file mode 100644 index 00000000..03f09d21 --- /dev/null +++ b/app/policies/site_build_stat_policy.rb @@ -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 diff --git a/app/services/lfs_object_service.rb b/app/services/lfs_object_service.rb new file mode 100644 index 00000000..c885936a --- /dev/null +++ b/app/services/lfs_object_service.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# Representa un objeto git LFS +class LfsObjectService + attr_reader :site, :blob + + # @param :site [Site] + # @param :blob [ActiveStorage::Blob] + def initialize(site:, blob:) + @site = site + @blob = blob + end + + def process + # Crear el directorio + FileUtils.mkdir_p(File.dirname(object_path)) + + # Mover el archivo + FileUtils.mv(path, object_path) unless File.exist? object_path + + # Crear el pointer + Site::Writer.new(site: site, file: path, content: pointer).save + + # Commitear el pointer + site.repository.commit(add: [path], usuarie: author, message: File.basename(path)) + + # Eliminar el pointer + FileUtils.rm(path) + + # Hacer link duro del objeto al archivo + FileUtils.ln(object_path, path) + end + + # @return [String] + def path + @path ||= blob.service.path_for(blob.key) + end + + # @return [String] + def digest + @digest ||= Digest::SHA256.file(path).hexdigest + end + + # @return [String] + def object_path + @object_path ||= File.join(site.path, '.git', 'lfs', 'objects', digest[0..1], digest[2..3], digest) + end + + # @return [Integer] + def size + @size ||= File.size(File.exist?(object_path) ? object_path : path) + end + + # @return [String] + def pointer + @pointer ||= + <<~POINTER + version https://git-lfs.github.com/spec/v1 + oid sha256:#{digest} + size #{size} + POINTER + end + + def author + @author ||= GitAuthor.new email: "disk_service@#{Site.domain}", name: 'DiskService' + end +end diff --git a/app/services/post_service.rb b/app/services/post_service.rb index e448bb4c..4631a9a4 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -12,7 +12,13 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do post.usuaries << usuarie params[:post][:draft] = true if site.invitade? usuarie - commit(action: :created, file: update_related_posts) if post.update(post_params) + params.require(:post).permit(:slug).tap do |p| + post.slug.value = p[:slug] if p[:slug].present? + end + + commit(action: :created, add: update_related_posts) if post.update(post_params) + + update_site_license! # Devolver el post aunque no se haya salvado para poder rescatar los # errores @@ -28,7 +34,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do # Los artículos anónimos siempre son borradores 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 end @@ -36,9 +42,17 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do post.usuaries << usuarie params[:post][:draft] = true if site.invitade? usuarie - # Es importante que el artículo se guarde primero y luego los - # relacionados. - commit(action: :updated, file: update_related_posts) if post.update(post_params) + # Eliminar ("mover") el archivo si cambió de ubicación. + if post.update(post_params) + rm = [] + rm << post.path.value_was if post.path.changed? + + # 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 # errores @@ -48,7 +62,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do def destroy post.destroy! - commit(action: :destroyed) if post.destroyed? + commit(action: :destroyed, rm: [post.path.absolute]) if post.destroyed? post end @@ -77,17 +91,19 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do # TODO: Implementar transacciones! posts.save_all(validate: false) && - commit(action: :reorder, file: files) + commit(action: :reorder, add: files) end private - def commit(action:, file: nil) - site.repository.commit(file: file || post.path.absolute, + def commit(action:, add: [], rm: []) + site.repository.commit(add: add, + rm: rm, usuarie: usuarie, - remove: action == :destroyed, message: I18n.t("post_service.#{action}", title: post&.title&.value)) + + GitPushJob.perform_later(site) end # Solo permitir cambiar estos atributos de cada articulo @@ -133,4 +149,12 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do p.path.absolute if p.save(validate: false) end.compact << post.path.absolute end + + # Si les usuaries modifican o crean una licencia, considerarla + # personalizada en el panel. + def update_site_license! + if site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom? + site.update licencia: Licencia.find_by_icons('custom') + end + end end diff --git a/app/services/site_service.rb b/app/services/site_service.rb index f5f415e7..5c37cfe3 100644 --- a/app/services/site_service.rb +++ b/app/services/site_service.rb @@ -5,7 +5,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do def deploy site.enqueue! - DeployJob.perform_async site.id + DeployJob.perform_later site.id end # Crea un sitio, agrega un rol nuevo y guarda los cambios a la @@ -14,7 +14,9 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do self.site = Site.new params add_role temporal: false, rol: 'usuarie' - sync_nodes + site.deploys.build type: 'DeployLocal' + # Los sitios de testing no se sincronizan + sync_nodes unless site.name.end_with? '.testing' I18n.with_locale(usuarie.lang.to_sym || I18n.default_locale) do # No se puede llamar a site.config antes de save porque el sitio @@ -26,13 +28,15 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do site.save && site.config.write && - commit_config(action: :create) + commit_config(action: :create) && + site.reset.nil? && + add_licencias && + add_code_of_conduct && + add_privacy_policy && + site.index_posts! && + deploy end - add_licencias - - deploy - site end @@ -41,19 +45,18 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do site.update(params) && site.config.write && - commit_config(action: :update) + commit_config(action: :update) && + site.reset.nil? && + change_licencias end - change_licencias - site end # Genera los Deploy necesarios para el sitio a menos que ya los tenga. def build_deploys - Site::DEPLOYS.map { |deploy| "Deploy#{deploy.to_s.camelcase}" } - .each do |deploy| - next if site.deploys.find_by type: deploy + Deploy.subclasses.each do |deploy| + next if site.deploys.find_by type: deploy.name site.deploys.build type: deploy end @@ -62,14 +65,11 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do # Agregar una dirección oculta de Tor al DeployHiddenService y a la # configuración del Site. def add_onion - onion = params[:onion].strip - deploy = DeployHiddenService.find_by(site: site) + onion = params[:onion] + deploy = params[:deploy] return false unless !onion.blank? && deploy - deploy.values[:onion] = onion - deploy.save - site.config['onion-location'] = onion site.config.write @@ -94,9 +94,11 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do def commit_config(action:) site.repository .commit(usuarie: usuarie, - file: site.config.path, + add: [site.config.path], message: I18n.t("site_service.#{action}", name: site.name)) + + GitPushJob.perform_later(site) end def add_role(temporal: true, rol: 'invitade') @@ -105,24 +107,28 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do end # Crea la licencia del sitio para cada locale disponible en el sitio + # + # @return [Boolean] def add_licencias - site.locales.each do |locale| - next unless I18n.available_locales.include? locale + return true unless site.layout? :license + return true if site.licencia.custom? - Mobility.with_locale(locale) do - add_licencia lang: locale - end - end + with_all_locales do |locale| + add_licencia lang: locale + end.compact.map(&:valid?).all? end + # Crea una licencia + # + # @return [Post] def add_licencia(lang:) params = ActionController::Parameters.new( post: { + layout: 'license', + slug: Jekyll::Utils.slugify(I18n.t('activerecord.models.licencia')), lang: lang, title: site.licencia.name, - description: I18n.t('sites.form.licencia.title'), - author: %w[Sutty], - permalink: "#{I18n.t('activerecord.models.licencia').downcase}/", + description: site.licencia.short_description, content: CommonMarker.render_html(site.licencia.deed) } ) @@ -133,25 +139,27 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do # Encuentra la licencia a partir de su enlace permanente y le cambia # el contenido # - # TODO: Crear un layout específico para licencias así es más certera - # la búsqueda. + # @return [Boolean] def change_licencias - site.locales.each do |locale| - next unless I18n.available_locales.include? locale + return true unless site.layout? :license + return true if site.licencia.custom? - Mobility.with_locale(locale) do - permalink = "#{I18n.t('activerecord.models.licencia').downcase}/" - post = site.posts(lang: locale).find_by(permalink: permalink) + with_all_locales do |locale| + post = site.posts(lang: locale).find_by(layout: 'license') - post ? change_licencia(post: post) : add_licencia(lang: locale) - end - end + change_licencia(post: post) if post + end.compact.map(&:valid?).all? end + # Cambia una licencia + # + # @param :post [Post] + # @return [Post] def change_licencia(post:) params = ActionController::Parameters.new( post: { title: site.licencia.name, + description: site.licencia.short_description, content: CommonMarker.render_html(site.licencia.deed) } ) @@ -160,10 +168,69 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do params: params).update end + # Agrega un código de conducta + # + # @return [Boolean] + def add_code_of_conduct + return true unless site.layout?(:code_of_conduct) || site.layout?(:page) + + # TODO: soportar más códigos de conducta + coc = CodeOfConduct.first + + with_all_locales do |locale| + params = ActionController::Parameters.new( + post: { + layout: site.layout?(:code_of_conduct) ? 'code_of_conduct' : 'page', + lang: locale.to_s, + title: coc.title, + description: coc.description, + content: CommonMarker.render_html(coc.content) + } + ) + + PostService.new(site: site, usuarie: usuarie, params: params).create + end.compact.map(&:valid?).all? + end + + # Agrega política de privacidad + # + # @return [Boolean] + def add_privacy_policy + return true unless site.layout?(:privacy_policy) || site.layout?(:page) + + pp = PrivacyPolicy.first + + with_all_locales do |locale| + params = ActionController::Parameters.new( + post: { + layout: site.layout?(:privacy_policy) ? 'privacy_policy' : 'page', + lang: locale.to_s, + title: pp.title, + description: pp.description, + content: CommonMarker.render_html(pp.content) + } + ) + + PostService.new(site: site, usuarie: usuarie, params: params).create + end.compact.map(&:valid?).all? + end + # Crea los deploys necesarios para sincronizar a otros nodos de Sutty def sync_nodes Rails.application.nodes.each do |node| - site.deploys.build(type: 'DeployRsync', destination: "sutty@#{node}:#{site.hostname}") + site.deploys.build(type: 'DeployFullRsync', destination: "rsync://rsyncd.#{node}/deploys/", hostname: node) + end + end + + private + + def with_all_locales(&block) + site.locales.map do |locale| + next unless I18n.available_locales.include? locale + + Mobility.with_locale(locale) do + yield locale + end end end end diff --git a/app/views/bootstrap/_custom_checkbox.haml b/app/views/bootstrap/_custom_checkbox.haml new file mode 100644 index 00000000..0c3ff3a6 --- /dev/null +++ b/app/views/bootstrap/_custom_checkbox.haml @@ -0,0 +1,6 @@ +- help_id = "#{id}_help" + +.custom-control.custom-checkbox + %input.custom-control-input{ id: id, type: 'checkbox', name: name, value: value, required: required } + %label.custom-control-label{ for: id, aria: { describedby: help_id } }= content + %small.form-text.text-muted{ id: help_id }= yield diff --git a/app/views/build_stats/index.haml b/app/views/build_stats/index.haml new file mode 100644 index 00000000..de04d84d --- /dev/null +++ b/app/views/build_stats/index.haml @@ -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] diff --git a/app/views/deploy_mailer/deployed.html.haml b/app/views/deploy_mailer/deployed.html.haml index f5afe5de..762bf4db 100644 --- a/app/views/deploy_mailer/deployed.html.haml +++ b/app/views/deploy_mailer/deployed.html.haml @@ -13,7 +13,7 @@ %tr %td= row[:title] %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 %time{ datetime: row[:seconds][:machine] }= row[:seconds][:human] %td= row[:size] diff --git a/app/views/deploys/_deploy_distributed_press.haml b/app/views/deploys/_deploy_distributed_press.haml new file mode 100644 index 00000000..d7d54db0 --- /dev/null +++ b/app/views/deploys/_deploy_distributed_press.haml @@ -0,0 +1,21 @@ +-# Publicar a la web distribuida + +.row + .col + = deploy.hidden_field :id + = deploy.hidden_field :type + .custom-control.custom-switch + -# + El checkbox invierte la lógica de destrucción porque queremos + crear el deploy si está activado y destruirlo si está + desactivado. + = deploy.check_box :_destroy, + { checked: deploy.object.persisted?, class: 'custom-control-input' }, + '0', '1' + = deploy.label :_destroy, class: 'custom-control-label' do + %h3= t('.title') + = sanitize_markdown t('.help', public_url: deploy.object.site.url), + tags: %w[p strong em a] + + +%hr/ diff --git a/app/views/deploys/_deploy_full_rsync.haml b/app/views/deploys/_deploy_full_rsync.haml new file mode 100644 index 00000000..0aab9802 --- /dev/null +++ b/app/views/deploys/_deploy_full_rsync.haml @@ -0,0 +1 @@ +-# nada diff --git a/app/views/deploys/_deploy_hidden_service.haml b/app/views/deploys/_deploy_hidden_service.haml index d6388123..9ebda012 100644 --- a/app/views/deploys/_deploy_hidden_service.haml +++ b/app/views/deploys/_deploy_hidden_service.haml @@ -17,7 +17,8 @@ = sanitize_markdown t('.help', public_url: deploy.object.site.url), tags: %w[p strong em a] - - if deploy.object.fqdn + - begin = sanitize_markdown t('.help_2', url: deploy.object.url), tags: %w[p strong em a] + - rescue ArgumentError %hr/ diff --git a/app/views/deploys/_deploy_localized_domain.haml b/app/views/deploys/_deploy_localized_domain.haml new file mode 100644 index 00000000..0aab9802 --- /dev/null +++ b/app/views/deploys/_deploy_localized_domain.haml @@ -0,0 +1 @@ +-# nada diff --git a/app/views/deploys/_deploy_social_distributed_press.haml b/app/views/deploys/_deploy_social_distributed_press.haml new file mode 100644 index 00000000..5c73b262 --- /dev/null +++ b/app/views/deploys/_deploy_social_distributed_press.haml @@ -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/ diff --git a/app/views/devise/mailer/invitation_instructions.html.haml b/app/views/devise/mailer/invitation_instructions.html.haml index a2434abd..e87d99d9 100644 --- a/app/views/devise/mailer/invitation_instructions.html.haml +++ b/app/views/devise/mailer/invitation_instructions.html.haml @@ -1,4 +1,4 @@ -- site = @resource.sites.last +- site = @resource.roles.where(temporal: true).last&.site %p= t('devise.mailer.invitation_instructions.hello', email: @resource.email) @@ -8,12 +8,17 @@ %h1= site.title %p= site.description -%p= link_to t('devise.mailer.invitation_instructions.accept'), - accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang) +- if @resource.needs_invitation_link? + %p= link_to t('devise.mailer.invitation_instructions.accept'), + accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang) -- if @resource.invitation_due_at - %p= t('devise.mailer.invitation_instructions.accept_until', - due_date: l(@resource.invitation_due_at, - format: :'devise.mailer.invitation_instructions.accept_until_format')) + - if @resource.invitation_due_at + %p= t('devise.mailer.invitation_instructions.accept_until', + due_date: l(@resource.invitation_due_at, + format: :'devise.mailer.invitation_instructions.accept_until_format')) -%p= t('devise.mailer.invitation_instructions.ignore') + %p= t('devise.mailer.invitation_instructions.ignore') +- elsif !@resource.confirmed? && @resource.confirmation_token + = confirmation_url(@resource, confirmation_token: @resource.confirmation_token, change_locale_to: @resource.lang) +- else + %p= link_to t('devise.mailer.invitation_instructions.sign_in'), root_url diff --git a/app/views/devise/mailer/invitation_instructions.text.haml b/app/views/devise/mailer/invitation_instructions.text.haml index 27b1580c..5cb007de 100644 --- a/app/views/devise/mailer/invitation_instructions.text.haml +++ b/app/views/devise/mailer/invitation_instructions.text.haml @@ -1,4 +1,4 @@ -- site = @resource.sites.last +- site = @resource.roles.where(temporal: true).last&.site = t('devise.mailer.invitation_instructions.hello', email: @resource.email) \ @@ -9,11 +9,17 @@ \ = site.description \ -= accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang) -\ -- if @resource.invitation_due_at - = t('devise.mailer.invitation_instructions.accept_until', - due_date: l(@resource.invitation_due_at, - format: :'devise.mailer.invitation_instructions.accept_until_format')) -\ -= t('devise.mailer.invitation_instructions.ignore') +- if @resource.needs_invitation_link? + = accept_invitation_url(@resource, invitation_token: @token, change_locale_to: @resource.lang) + \ + - if @resource.invitation_due_at + = t('devise.mailer.invitation_instructions.accept_until', + due_date: l(@resource.invitation_due_at, + format: :'devise.mailer.invitation_instructions.accept_until_format')) + \ + = t('devise.mailer.invitation_instructions.ignore') +- elsif !@resource.confirmed? && @resource.confirmation_token + = confirmation_url(@resource, confirmation_token: @resource.confirmation_token, change_locale_to: @resource.lang) +- else + = root_url(change_locale_to: @resource.lang) + = t('devise.mailer.invitation_instructions.sign_in') diff --git a/app/views/devise/registrations/new.haml b/app/views/devise/registrations/new.haml index e6bda964..26fc8e18 100644 --- a/app/views/devise/registrations/new.haml +++ b/app/views/devise/registrations/new.haml @@ -4,7 +4,7 @@ = render 'devise/shared/error_messages', resource: resource .row.align-items-center.justify-content-center.full-height - .col-md-5.align-self-center + .col-md-6.align-self-center %h2= t('.sign_up') %p= t('.help') @@ -39,6 +39,21 @@ min: @minimum_password_length, aria: { describedby: 'minimum-password-length' }, placeholder: t("#{password}_confirmation") + + .form-group + - Usuarie::CONSENT_FIELDS.each do |field| + - required = t(".#{field}.required", default: '').present? + - id = "usuarie_#{field}" + - name = "usuarie[#{field}]" + - content = t(".#{field}.label") + - href = t(".#{field}.href", default: '') + - help_content = t(".#{field}.help") + = render 'bootstrap/custom_checkbox', id: id, name: name, content: content, required: required, value: "1" do + - if href.present? + = link_to help_content, href, target: '_blank', rel: 'noopener' + - else + = help_content + .actions = f.submit t('.sign_up'), class: 'btn btn-lg btn-block' diff --git a/app/views/env/index.js.haml b/app/views/env/index.js.haml index f4bd69cf..597ba53f 100644 --- a/app/views/env/index.js.haml +++ b/app/views/env/index.js.haml @@ -1,7 +1,8 @@ -= cache @site do - :plain - window.env = { - AIRBRAKE_SITE_ID: #{@site.id}, - AIRBRAKE_API_KEY: "#{@site.airbrake_api_key}", - PANEL_URL: "#{ENV['PANEL_URL']}" - } +- if @site + = cache @site do + :plain + window.env = { + AIRBRAKE_SITE_ID: #{@site.id}, + AIRBRAKE_API_KEY: "#{@site.airbrake_api_key}", + PANEL_URL: "#{ENV['PANEL_URL']}" + } diff --git a/app/views/layouts/_breadcrumb.haml b/app/views/layouts/_breadcrumb.haml index 099ddde4..11f7f005 100644 --- a/app/views/layouts/_breadcrumb.haml +++ b/app/views/layouts/_breadcrumb.haml @@ -19,10 +19,15 @@ = link_to t('.tienda'), @site.tienda_url, 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 = link_to t('.logout'), main_app.destroy_usuarie_session_path, method: :delete, role: 'button', class: 'btn' - else + - params.permit! - I18n.available_locales.each do |locale| - next if locale == I18n.locale - = link_to t(locale), "?change_locale_to=#{locale}" + = link_to t("switch_locale.#{locale}"), params.to_h.merge(change_locale_to: locale) diff --git a/app/views/layouts/_flash.haml b/app/views/layouts/_flash.haml index 7bd7ec0b..2d65ff78 100644 --- a/app/views/layouts/_flash.haml +++ b/app/views/layouts/_flash.haml @@ -1,4 +1,4 @@ - flash.each do |type, message| - unless type == 'js' = render 'bootstrap/alert' do - = message + = sanitize_markdown message, tags: %w[a strong em] diff --git a/app/views/layouts/_link_rel_alternate.haml b/app/views/layouts/_link_rel_alternate.haml new file mode 100644 index 00000000..64a70977 --- /dev/null +++ b/app/views/layouts/_link_rel_alternate.haml @@ -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 } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 965a856f..d2113398 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,7 +4,7 @@ %meta{ charset: 'UTF-8' }/ %meta{ content: 'text/html; charset=UTF-8', 'http-equiv': 'Content-Type' }/ - %meta{ name: 'color-scheme', content: 'light dark' }/ + %meta{ name: 'color-scheme', content: 'light' }/ %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' }/ %meta{ name: 'referrer', content: 'same-origin' }/ @@ -17,6 +17,7 @@ = javascript_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' + = render 'layouts/link_rel_alternate' %body{ class: yield(:body) } .container-fluid#sutty diff --git a/app/views/posts/_submit.haml b/app/views/posts/_submit.haml index cad43320..944694c1 100644 --- a/app/views/posts/_submit.haml +++ b/app/views/posts/_submit.haml @@ -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 = submit_tag t('.save'), class: 'btn submit-post' = 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 - = site.config.fetch('sending_help', t('.sending_help')) + = sending_help diff --git a/app/views/posts/attribute_ro/_created_at.haml b/app/views/posts/attribute_ro/_created_at.haml new file mode 100644 index 00000000..04f6d716 --- /dev/null +++ b/app/views/posts/attribute_ro/_created_at.haml @@ -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 diff --git a/app/views/posts/attribute_ro/_locales.haml b/app/views/posts/attribute_ro/_locales.haml index 3ac22933..16ecb532 100644 --- a/app/views/posts/attribute_ro/_locales.haml +++ b/app/views/posts/attribute_ro/_locales.haml @@ -1,9 +1,10 @@ -%tr{ id: attribute } - %th= post_label_t(attribute, post: post) - %td - %ul - - metadata.value.each do |uuid| - - p = site.docs.find(uuid, uuid: true) - %li{ dir: t("locales.#{p.lang.value}.dir"), lang: p.lang.value } - = link_to p.title.value, - site_post_path(site, p.id, locale: p.lang.value) +- if site.locales.count > 1 + %tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + %ul + - metadata.value.each do |uuid| + - p = site.docs.find(uuid, uuid: true) + %li{ dir: t("locales.#{p.lang.value}.dir"), lang: p.lang.value } + = link_to p.title.value, + site_post_path(site, p.id, locale: p.lang.value) diff --git a/app/views/posts/attributes/_created_at.haml b/app/views/posts/attributes/_created_at.haml new file mode 100644 index 00000000..0aab9802 --- /dev/null +++ b/app/views/posts/attributes/_created_at.haml @@ -0,0 +1 @@ +-# nada diff --git a/app/views/posts/attributes/_image.haml b/app/views/posts/attributes/_image.haml index f4d9bb3d..84fe56fd 100644 --- a/app/views/posts/attributes/_image.haml +++ b/app/views/posts/attributes/_image.haml @@ -22,7 +22,7 @@ = file_field(*field_name_for(base, attribute, :path), **field_options(attribute, metadata, required: (metadata.required && !metadata.path?)), 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", post_label_t(attribute, :path, post: post), class: 'custom-file-label' = render 'posts/attribute_feedback', diff --git a/app/views/posts/attributes/_locales.haml b/app/views/posts/attributes/_locales.haml index 8dd7adf6..4978f6b4 100644 --- a/app/views/posts/attributes/_locales.haml +++ b/app/views/posts/attributes/_locales.haml @@ -1,39 +1,19 @@ --# - - Crea un input-map para cada idioma por separado. Podríamos hacer uno - solo que tenga todos los idiomas pero puede ser una interfaz confusa. - - TODO: Esto permite seleccionar más de una traducción por idioma... - -- site.locales.each do |locale| - -# Ignorar el idioma actual - - next if post.lang.value == locale - - locale_t = t("locales.#{locale}.name") - - values = metadata.value.select do |x| - - metadata.values[locale].values.include? x - - .form-group - = label_tag "#{base}_#{attribute}_#{locale}", locale_t - - .mapable{ dir: t("locales.#{locale}.dir"), lang: locale, - data: { values: values.to_json, - 'default-values': metadata.values[locale].to_json, - name: "#{base}[#{attribute}][]", - list: id_for_datalist(attribute, locale), - button: t('posts.attributes.add'), - remove: 'false', legend: locale_t, - described: id_for_help(attribute, locale) } } - - = text_field(*field_name_for(base, attribute, '[]'), - value: values.join(', '), - dir: t("locales.#{locale}.dir"), lang: locale, - **field_options(attribute, metadata)) +- if site.locales.count > 1 + %fieldset + %legend= post_label_t(attribute, post: post) = render 'posts/attribute_feedback', - post: post, - attribute: [attribute, 'mapable'].flatten, - metadata: metadata + post: post, attribute: attribute, metadata: metadata - %datalist{ id: id_for_datalist(attribute, locale) } - - metadata.values[locale].keys.each do |value| - %option{ value: value } + - site.locales.each do |locale| + - next if post.lang.value == locale + - locale_t = t("locales.#{locale}.name", default: locale.to_s.humanize) + - value = metadata.value.find do |v| + - metadata.values[locale].values.include? v + + .form-group + = label_tag "#{base}_#{attribute}_#{locale}", locale_t + + = select_tag("#{plain_field_name_for(base, attribute)}[]", + options_for_select(metadata.values[locale], value), + **field_options(attribute, metadata), include_blank: t('.empty')) diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml index dbeddaad..ae53aa7a 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -1,19 +1,18 @@ %main.row %aside.menu.col-md-3 - %h1= link_to @site.title, @site.url - %p.lead= @site.description + = render 'sites/header', site: @site + + = render 'sites/status', site: @site + + = render 'sites/build', site: @site, class: 'btn-block' %h3= t('posts.new') - %table.mb-3 - - @site.layouts.each do |layout| - - next if layout.hidden? - %tr - %th= layout.humanized_name - %td.pl-3= link_to t('posts.add'), new_site_post_path(@site, layout: layout.value), class: 'btn btn-secondary btn-sm' - - 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' + %table.table.table-sm.mb-3 + %tbody + - @site.schema_organization.each do |schema, _| + - schema = @site.layouts[schema] + - next if schema.hidden? + = render 'schemas/row', site: @site, schema: schema, filter: @filter_params - if policy(@site_stat).index? = link_to t('stats.index.title'), site_stats_path(@site), class: 'btn' @@ -31,8 +30,6 @@ type: 'info', link: site_usuaries_path(@site) - = render 'sites/build', site: @site - - if @site.design.credits = render 'bootstrap/alert' do = sanitize_markdown @site.design.credits @@ -47,7 +44,8 @@ - next if param == 'q' %input{ type: 'hidden', name: param, value: value } .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' } - if @site.locales.size > 1 @@ -86,7 +84,10 @@ %button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top') %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 - dir = @site.data.dig(params[:locale], 'dir') - size = @posts.size diff --git a/app/views/schemas/_add.haml b/app/views/schemas/_add.haml new file mode 100644 index 00000000..0131a6bb --- /dev/null +++ b/app/views/schemas/_add.haml @@ -0,0 +1 @@ += link_to t('.add'), new_site_post_path(site, layout: schema.value), class: 'btn btn-secondary btn-sm m-0' diff --git a/app/views/schemas/_filter.haml b/app/views/schemas/_filter.haml new file mode 100644 index 00000000..c422c5b8 --- /dev/null +++ b/app/views/schemas/_filter.haml @@ -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' diff --git a/app/views/schemas/_row.haml b/app/views/schemas/_row.haml new file mode 100644 index 00000000..1d1fca87 --- /dev/null +++ b/app/views/schemas/_row.haml @@ -0,0 +1,13 @@ +%tr + %th.w-100{ scope: 'row' } + - if local_assigns[:parent_schema] + %span.text-muted — + = 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 diff --git a/app/views/sites/_build.haml b/app/views/sites/_build.haml index 6bc4d11b..5911e908 100644 --- a/app/views/sites/_build.haml +++ b/app/views/sites/_build.haml @@ -3,7 +3,7 @@ method: :post, class: 'form-inline inline' do = 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'), data: { disable_with: t('sites.enqueued') }, disabled: site.enqueued? diff --git a/app/views/sites/_form.haml b/app/views/sites/_form.haml index 757b8b48..0dcccbe3 100644 --- a/app/views/sites/_form.haml +++ b/app/views/sites/_form.haml @@ -46,45 +46,48 @@ .invalid-feedback= site.errors.messages[:description].join(', ') %hr/ - .form-group#design_id - %h2= t('.design.title') - %p.lead= t('.help.design') - - if invalid? site, :design_id - = render 'bootstrap/alert' do - = t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help', - layouts: site.incompatible_layouts.to_sentence) - .row.designs - -# Demasiado complejo para un f.collection_radio_buttons - - Design.all.find_each do |design| - .design.col-md-4.d-flex.flex-column - .custom-control.custom-radio - = f.radio_button :design_id, design.id, - checked: design.id == site.design_id, - disabled: design.disabled, - required: true, class: 'custom-control-input' - = f.label "design_id_#{design.id}", design.name, - class: 'custom-control-label' - .flex-fill - = sanitize_markdown design.description, - tags: %w[p a strong em] + - unless site.persisted? + .form-group#design_id + %h2= t('.design.title') + %p.lead= t('.help.design') + - if invalid? site, :design_id + = render 'bootstrap/alert' do + = t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help', + layouts: site.incompatible_layouts.to_sentence) + .row.row-cols-1.row-cols-md-2.designs + -# Demasiado complejo para un f.collection_radio_buttons + - Design.all.order(priority: :desc).each do |design| + .design.col.d-flex.flex-column + .custom-control.custom-radio + = f.radio_button :design_id, design.id, + checked: design.id == site.design_id, + disabled: design.disabled, + required: true, class: 'custom-control-input' + = f.label "design_id_#{design.id}", design.name, + class: 'custom-control-label' + .flex-fill + = sanitize_markdown design.description, + tags: %w[p a strong em] - .btn-group{ role: 'group', 'aria-label': t('.design.actions') } - - if design.url - = link_to t('.design.url'), design.url, - target: '_blank', class: 'btn' - - if design.license - = link_to t('.design.license'), design.license, - target: '_blank', class: 'btn' - %hr/ + .btn-group{ role: 'group', 'aria-label': t('.design.actions') } + - if design.url + = link_to t('.design.url'), design.url, + target: '_blank', class: 'btn' + - if design.license + = link_to t('.design.license'), design.license, + target: '_blank', class: 'btn' + %hr/ .form-group.licenses#license_id %h2= t('.licencia.title') %p.lead= t('.help.licencia') - Licencia.all.find_each do |licencia| + - next if licencia.custom? && site.licencia != licencia .row.license .col .media.mt-1 - = image_tag licencia.icons, alt: licencia.name, class: 'mr-3 mt-4' + - unless licencia.custom? + = image_tag licencia.icons, alt: licencia.name, class: 'mr-3 mt-4' .media-body .custom-control.custom-radio = f.radio_button :licencia_id, licencia.id, @@ -95,8 +98,8 @@ = sanitize_markdown licencia.description, tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6] - = link_to t('.licencia.url'), licencia.url, - target: '_blank', class: 'btn' + - unless licencia.custom? + = link_to t('.licencia.url'), licencia.url, target: '_blank', class: 'btn', rel: 'noopener' %hr/ @@ -158,9 +161,6 @@ = f.fields_for :deploys do |deploy| = render "deploys/#{deploy.object.type.underscore}", deploy: deploy, site: site - - else - = f.fields_for :deploys do |deploy| - = deploy.hidden_field :type .form-group = f.submit submit, class: 'btn btn-lg btn-block' diff --git a/app/views/sites/_header.haml b/app/views/sites/_header.haml new file mode 100644 index 00000000..c8931041 --- /dev/null +++ b/app/views/sites/_header.haml @@ -0,0 +1,3 @@ +.hyphens{ lang: site.default_locale } + %h1= site.title + %p.lead= site.description diff --git a/app/views/sites/_status.haml b/app/views/sites/_status.haml new file mode 100644 index 00000000..6a610e73 --- /dev/null +++ b/app/views/sites/_status.haml @@ -0,0 +1,21 @@ +- link = nil +- if site.not_published_yet? + - message = t('.not_published_yet') +- elsif site.awaiting_publication? + - message = t('.awaiting_publication') +- elsif site.building? + - if site.average_publication_time_calculable? + - average_building_time = site.average_publication_time + - elsif !site.similar_sites? + - average_building_time = 60 + - else + - average_building_time = site.average_publication_time_for_similar_sites + + - average_publication_time_human = distance_of_time_in_words average_building_time + - message = t('.building', average_time: average_publication_time_human, seconds: average_building_time) +- else + - message = t('.available') + - link = true + += render 'bootstrap/alert' do + = link_to_if link, message.html_safe, site_build_stats_path(site), class: 'alert-link' diff --git a/app/views/sites/index.haml b/app/views/sites/index.haml index 56178775..b7231292 100644 --- a/app/views/sites/index.haml +++ b/app/views/sites/index.haml @@ -54,10 +54,4 @@ text: t('usuaries.index.title'), type: 'info', 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 diff --git a/app/views/stats/index.haml b/app/views/stats/index.haml index 49fbd023..88e86aa3 100644 --- a/app/views/stats/index.haml +++ b/app/views/stats/index.haml @@ -27,6 +27,8 @@ %p.lead= t('.urls.description') %form{ method: 'get', action: '#custom-urls' } %input{ type: 'hidden', name: 'interval', value: @interval } + %input{ type: 'hidden', name: 'period_start', value: params[:period_start] } + %input{ type: 'hidden', name: 'period_end', value: params[:period_end] } .form-group %label{ for: 'urls' }= t('.urls.label') %textarea#urls.form-control{ name: 'urls', autocomplete: 'on', required: true, rows: @normalized_urls.size + 1, aria_describedby: 'help-urls' }= @normalized_urls.join("\n") diff --git a/config/application.rb b/config/application.rb index 97ab244c..606ccaf4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -37,6 +37,15 @@ module Sutty .rescue_responses['Pundit::NotAuthorizedError'] = :forbidden 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 # Load application's model / class decorators diff --git a/config/credentials.yml.enc.ci b/config/credentials.yml.enc.ci new file mode 100644 index 00000000..4add450d --- /dev/null +++ b/config/credentials.yml.enc.ci @@ -0,0 +1 @@ +1jEfzfldP9tT4+HWfhP48I9hw31gYCnnxHWpYjPrcTm/pgkFdiG+mDa6y31EOxzs50w6FEw2GO127BnyBSUIPIxuWY0cR96xL5pVrS3vjyzM84QN4lJF9ER0Tz1AQ9S7NJ54CelSkMfFt/rf+O4YM8cLtdSVsVC/HlGbp16p3D1pm4MFo5cQb0hEmlyyYlzEn4oJtsp/MCIwI4+z8oFhxKdMIxdbiw+KS/7PBRfMm1h5rdGORCnD69iVmnXseMvVtZn9A7N7uR6+gFlhxlD5yyEW0pwTj3tbu9NeIOVbtmYOL5ZhLW9REXtGTqR5Op/LN+ukIXbDNEScKltJXUdWfa9Pd/QjVT8IMURZ04POEMDgs1cw363yz4f+WQForhSco9oYLDOd5hTGRXoZ9fnjnfJSTjINM62hkfDY3w3+s844nNbjbj+lPTJHU/QjRhcuNqBDDxWUfwTmRIqm5zrelnHnZnuFmFwCNet6NChC6EFUAFjrals6kTSQllyMt4xImqA+HL7DnjWj6VURSH+nGQTA4tQvDdfbDwTzg/PvRkJcsy2dRd135RQdmRZ+8KXBviLabwdR256vaCqSO1j+jyeUPGLll35ghyLxncyBkkAKt1zaDRPDWgVafg0gJ3v7hVV5TYgToPzlv4w88KPCY7cBhkb1qGoXAhtO6iAuZYK9eyZd1gNQJKyqbcLqA5aTTX/ylfdbptWhaZ8ibB8KBgVyn2RmrOHEhB38rDSMHHNfK3Xs4/hhqMFIGHGTGCUYVmjCzhVFd15yRurU32d3YtP8W4L77H7qkFsF1gnvsZx+R084LcJqknwY94dmjtUE4x2u+Qh3ElFj--lr8JoUq1WH9xXNsB--mE8hxHADL7SbDWabAPY1+Q== \ No newline at end of file diff --git a/config/environments/production.rb b/config/environments/production.rb index d121bdbd..5e089ff9 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -64,11 +64,6 @@ Rails.application.configure do # Use a different cache store in production. 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 # 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.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[:protocol] = 'https' diff --git a/config/initializers/core_extensions.rb b/config/initializers/core_extensions.rb index 1516a43a..d6039e16 100644 --- a/config/initializers/core_extensions.rb +++ b/config/initializers/core_extensions.rb @@ -91,6 +91,15 @@ module Jekyll spec.name == name 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.canonical_segments[2] = 0 base_path = Rails.root.join('_storage', 'gems', File.basename(site.source), 'ruby', @@ -114,6 +123,11 @@ module Jekyll private def gemspec; end + + # @return [Symbol] + def locale + @locale ||= (site.config['locale'] || site.config['lang'] || I18n.locale).to_sym + end end # No necesitamos los archivos de la plantilla diff --git a/config/initializers/device_detector.rb b/config/initializers/device_detector.rb new file mode 100644 index 00000000..e6f6118a --- /dev/null +++ b/config/initializers/device_detector.rb @@ -0,0 +1,3 @@ +DeviceDetector.configure do |config| + config.max_cache_keys = 5_000 # to check if not too much +end \ No newline at end of file diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 0e18b987..6002ee65 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -13,6 +13,10 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.singular 'roles', 'rol' inflect.plural 'rollup', 'rollups' inflect.singular 'rollups', 'rollup' + inflect.plural 'code_of_conduct', 'codes_of_conduct' + inflect.singular 'codes_of_conduct', 'code_of_conduct' + inflect.plural 'privacy_policy', 'privacy_policies' + inflect.singular 'privacy_policies', 'privacy_policy' end ActiveSupport::Inflector.inflections(:es) do |inflect| @@ -28,4 +32,8 @@ ActiveSupport::Inflector.inflections(:es) do |inflect| inflect.singular 'licencias', 'licencia' inflect.plural 'rollup', 'rollups' inflect.singular 'rollups', 'rollup' + inflect.plural 'code_of_conduct', 'codes_of_conduct' + inflect.singular 'codes_of_conduct', 'code_of_conduct' + inflect.plural 'privacy_policy', 'privacy_policies' + inflect.singular 'privacy_policies', 'privacy_policy' end diff --git a/config/initializers/que.rb b/config/initializers/que.rb new file mode 100644 index 00000000..eb898ae7 --- /dev/null +++ b/config/initializers/que.rb @@ -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 diff --git a/config/initializers/sucker_punch.rb b/config/initializers/sucker_punch.rb deleted file mode 100644 index 865af32d..00000000 --- a/config/initializers/sucker_punch.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -# Enviar una notificación cuando falla una tarea -SuckerPunch.exception_handler = lambda { |ex, _klass, _args| - ExceptionNotifier.notify_exception(ex) -} diff --git a/config/locales/devise.views.en.yml b/config/locales/devise.views.en.yml index 793f3a0a..a524cf7c 100644 --- a/config/locales/devise.views.en.yml +++ b/config/locales/devise.views.en.yml @@ -104,6 +104,25 @@ en: new: sign_up: Sign up help: We only ask for an e-mail address and a password. The password is safely stored, no one else besides you knows it! You'll also receive an e-mail to confirm your account. + privacy_policy_accepted: + label: "I understand and accept the privacy policy" + help: "Read privacy policy" + href: "https://sutty.nl/en/privacy-policy/" + required: true + terms_of_service_accepted: + label: "My sites won't promote hate speech" + help: "Read terms of service" + href: "https://sutty.nl/en/terms-of-service/" + required: true + code_of_conduct_accepted: + label: "I want a more inclusive Internet" + help: "Read codes for sharing" + href: "https://sutty.nl/en/code-of-conduct/" + required: true + available_for_feedback_accepted: + label: "I'm available to provide feedback" + help: "We may contact you occasionaly" + required: false signed_up: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." signed_up_but_inactive: You have signed up successfully. However, we could not sign you in because your account is not yet activated. signed_up_but_locked: You have signed up successfully. However, we could not sign you in because your account is locked. diff --git a/config/locales/devise.views.es.yml b/config/locales/devise.views.es.yml index b745fc5f..4575c628 100644 --- a/config/locales/devise.views.es.yml +++ b/config/locales/devise.views.es.yml @@ -104,6 +104,24 @@ es: new: sign_up: Registrarme help: Para registrarte solo pedimos una dirección de correo y una contraseña. La contraseña se almacena de forma segura, ¡nadie más que vos la sabe! Recibirás un correo de confirmación de cuenta. + privacy_policy_accepted: + label: "Comprendo y acepto la política de privacidad" + help: "Leer política de privacidad" + href: "https://sutty.nl/politica-de-privacidad/" + required: "true" + terms_of_service_accepted: + label: "Mis sitios no promueven el discurso de odio" + help: "Leer términos de servicio" + href: "https://sutty.nl/terminos-de-servicio/" + required: "true" + code_of_conduct_accepted: + label: "Quiero una Internet más inclusiva" + help: "Leer códigos para compartir" + href: "https://sutty.nl/codigo-de-convivencia/" + required: "true" + available_for_feedback_accepted: + label: "Estoy disponible para ofrecer retroalimentación" + help: "Te contactaremos ocasionalmente" signed_up: "Hemos enviado un mensaje con un enlace de confirmación a tu correo electrónico. Por favor, abrí el enlace para terminar de activar tu cuenta." signed_up_but_inactive: Tu cuenta ha sido creada correctamente. Sin embargo, no hemos podido iniciar la sesión porque tu cuenta aún no está activada. signed_up_but_locked: Tu cuenta ha sido creada correctamente. Sin embargo, no hemos podido iniciar la sesión porque que tu cuenta está bloqueada. diff --git a/config/locales/devise_invitable.en.yml b/config/locales/devise_invitable.en.yml index f6bfee40..39238140 100644 --- a/config/locales/devise_invitable.en.yml +++ b/config/locales/devise_invitable.en.yml @@ -23,6 +23,7 @@ en: accept: "Accept invitation" accept_until: "This invitation will be due in %{due_date}." ignore: "If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password." + sign_in: "Sign in to your account to accept or decline the invitation." time: formats: devise: diff --git a/config/locales/devise_invitable.es.yml b/config/locales/devise_invitable.es.yml index 144d6df6..860ee4f8 100644 --- a/config/locales/devise_invitable.es.yml +++ b/config/locales/devise_invitable.es.yml @@ -22,7 +22,8 @@ es: 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_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." time: formats: devise: diff --git a/config/locales/en.yml b/config/locales/en.yml index 567ab6bb..53dd25c6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,8 +1,10 @@ en: dir: ltr en: English - es: Castellano - es-AR: Castellano rioplatense + es: Castellano + es-AR: Castellano rioplatense + switch_locale: + es: "Cambiar a castellano" locales: es: name: Castillian Spanish @@ -13,6 +15,9 @@ en: ar: name: Arabic dir: rtl + ur: + name: Urdu + dir: rtl zh: name: Chinese dir: ltr @@ -47,7 +52,7 @@ en: cant_be_empty: 'This field cannot be empty' image: 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' no_file_for_description: "Description with no associated image" attachment_missing: "I couldn't save the image :(" @@ -114,6 +119,14 @@ en: title: Alternative domain name success: Success! error: Error + deploy_distributed_press: + title: Distributed Web + success: Success! + error: Error + deploy_social_distributed_press: + title: Fediverse + success: Success! + error: Error deploy_reindex: title: Reindex success: Success! @@ -126,6 +139,14 @@ en: title: Synchronize to backup server success: Success! error: Error + deploy_full_rsync: + title: Synchronize to another Sutty node + success: Success! + error: Error + deploy_distributed_press: + title: Distributed Web + success: Success! + error: Error help: You can contact us by replying to this e-mail maintenance_mailer: notice: @@ -150,6 +171,7 @@ en: usuarie: User licencia: License design: Design + indexed_post: Indexed post attributes: usuarie: email: 'E-mail address' @@ -174,9 +196,14 @@ en: deploys: deploy_local_presence: 'We need to be build the site!' design_id: + missing_gem: "Site is configured to use %{theme} theme, but the corresponding gem is missing from Gemfile" layout_incompatible: 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}." + usuarie: + attributes: + lang: + not_available: "This language is not yet available, would you help us by translating Sutty into it?" errors: argument_error: 'Argument `%{argument}` must be an instance of %{class}' unknown_locale: 'Unknown %{locale} locale' @@ -191,6 +218,8 @@ en: title: 'Your location in Sutty' logout: Log out mutual_aid: Mutual aid + contact_us: "Contact us" + contact_us_href: "https://sutty.nl/en/#contact" collaborations: collaborate: submit: Register @@ -272,6 +301,30 @@ en: Only accessible through [Tor Browser](https://www.torproject.org/download/) + deploy_distributed_press: + title: 'Publish to the distributed Web' + help: | + Make your site available through peer-to-peer protocols, + Inter-Planetary File System (IPFS), Hypercore, and via + BitTorrent, so your site is more resilient and can be available + offline, including in community mesh networks. + + **Important:** Only use this option if you would like your data + to be permanently available. If you decide to undo this + selection, a cleared version of the site will be shared in its + place. However, it is possible that nodes on the distributed + storage network may continue retaining copies of the data + indefinitely. + + [Learn more](https://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: index: title: Statistics @@ -330,6 +383,11 @@ en: designer_url: 'Support the designer' static_file_migration: 'File migration' find_and_replace: 'Search and replace' + status: + building: "Your site is building, refresh this page in ." + 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 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: title: 'My Sites' pull: 'Upgrade' @@ -367,15 +425,17 @@ en: title: 'Edit %{site}' submit: 'Save changes' btn: 'Configuration' + update: + post: "Your changes have been saved. **If you enabled a publication method, don't forget to publish changes.**" form: errors: title: There were errors and we couldn't save your changes :( help: Please, look for the invalid fields to fix them 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' 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 means nobody can use our works without explicit permission. By using licenses, we stablish conditions by which we want to share @@ -395,7 +455,7 @@ en: title: 'Design' actions: 'Information about this design' url: 'Demo' - licencia: 'License' + license: 'License' licencia: title: 'License for the site and everything published on it' url: 'Read the license' @@ -426,6 +486,9 @@ en: 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! :(" message: 'Skeleton upgrade' + webhooks: + pull: + message: 'Webhooks pull' footer: powered_by: 'is developed by' i18n: @@ -472,6 +535,8 @@ en: feedback: 'This field cannot be empty!' uuid: label: 'Unique identifier' + created_at: + label: 'Created at' geo: uri: 'Open in app' osm: 'Open in web map' @@ -481,7 +546,7 @@ en: file: destroy: Remove file image: - label: Imagen + label: Image destroy: Remove image belongs_to: empty: "(Empty)" @@ -504,12 +569,10 @@ en: order: 'Order' content: 'Text' new: 'Post types' - add: 'Add' - filter: 'Filter' - remove_filter: 'Back' remove_filter_help: 'Remove the filter: %{filter}' categories: 'Everything' - index: 'Posts' + index: + search: 'Search' edit: 'Edit' preview: btn: 'Preliminary version' @@ -647,7 +710,7 @@ en: new: 'Create' edit: 'Configure' posts: - new: 'New %{layout}' + new: 'Add %{layout}' edit: 'Editing' usuaries: index: 'Users' @@ -662,3 +725,14 @@ en: queries: show: 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})" diff --git a/config/locales/es.yml b/config/locales/es.yml index 0a829e4a..7a8ac738 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -3,6 +3,8 @@ es: en: English es-AR: Castellano Rioplatense dir: ltr + switch_locale: + en: "Switch to English" locales: es: name: Castellano @@ -13,6 +15,9 @@ es: ar: name: Árabe dir: rtl + ur: + name: Urdu + dir: rtl zh: name: Chino dir: ltr @@ -47,7 +52,7 @@ es: cant_be_empty: 'El campo no puede estar vacío' image: 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' no_file_for_description: 'Se envió una descripción sin imagen asociada' attachment_missing: 'no pude guardar el archivo :(' @@ -114,6 +119,14 @@ es: title: Dominio alternativo success: ¡Éxito! error: Hubo un error + deploy_distributed_press: + title: Web distribuida + success: ¡Éxito! + error: Hubo un error + deploy_social_distributed_press: + title: Fediverso + success: ¡Éxito! + error: Hubo un error deploy_reindex: title: Reindexación success: ¡Éxito! @@ -126,6 +139,14 @@ es: title: Sincronizar al servidor alternativo success: ¡Éxito! error: Hubo un error + deploy_full_rsync: + title: Sincronizar a otro nodo de Sutty + success: ¡Éxito! + error: Hubo un error + deploy_distributed_press: + title: Web distribuida + success: ¡Éxito! + error: Hubo un error help: Por cualquier duda, responde este correo para contactarte con nosotres. maintenance_mailer: notice: @@ -150,6 +171,7 @@ es: usuarie: Usuarie licencia: Licencia design: Diseño + indexed_post: Artículo indexado attributes: usuarie: email: 'Correo electrónico' @@ -174,9 +196,14 @@ es: deploys: deploy_local_presence: '¡Necesitamos poder generar el sitio!' design_id: + missing_gem: "El sitio usa la plantilla %{theme} pero la gema correspondiente no se encuentra en el Gemfile" layout_incompatible: 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}.' + usuarie: + attributes: + lang: + not_available: "Este idioma todavía no está disponible, ¿nos ayudas a agregarlo y mantenerlo?" errors: argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}' unknown_locale: 'El idioma %{locale} es desconocido' @@ -191,6 +218,8 @@ es: title: 'Tu ubicación en Sutty' logout: Cerrar sesión mutual_aid: Ayuda mutua + contact_us: "Contacto" + contact_us_href: "https://sutty.nl/#contacto" collaborations: collaborate: submit: Registrarme @@ -277,6 +306,30 @@ es: Sólo será accesible a través del [Navegador Tor](https://www.torproject.org/es/download/). + deploy_distributed_press: + title: 'Publicar a la Web distribuida' + help: | + Utiliza protocolos de pares, Inter-Planetary File System (IPFS), + Hypercore y torrents, para que tu sitio sea más resiliente y + esté disponible _offline_, inclusive en redes _mesh_ + comunitarias. + + **Importante:** Sólo usa esta opción si te parece correcto que + tu contenido esté disponible permanentemente. Cuando elijas + des-hacer esta acción, una versión "vacía" del sitio será + compartida en su lugar. Sin embargo, es posible que algunos + nodos en la red de almacenamiento distribuida puedan retener + copias de tu contenido indefinidamente. + + [Saber más](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: index: title: Estadísticas @@ -335,6 +388,11 @@ es: designer_url: 'Apoyá a le(s) diseñadore(s)' static_file_migration: 'Migración de archivos' find_and_replace: 'Búsqueda y reemplazo' + status: + building: "Tu sitio se está publicando, recargá esta página en ." + not_published_yet: "Tu sitio se está publicando por primera vez, por favor espera hasta un minuto..." + 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: title: 'Mis sitios' pull: 'Actualizar' @@ -373,15 +431,17 @@ es: title: 'Editar %{site}' submit: 'Guardar cambios' 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: errors: title: Hubo errores y no pudimos guardar tus cambios :( help: Por favor, busca los campos marcados como no válidos para resolverlos 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.' 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 de autore. Esto significa que nadie puede hacer uso de nuestras obras sin permiso explícito. Con las licencias establecemos @@ -434,6 +494,9 @@ es: 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! :(' message: 'Actualización del esqueleto' + webhooks: + pull: + message: 'Traer los cambios a partir de un evento remoto' footer: powered_by: 'es desarrollada por' i18n: @@ -480,6 +543,8 @@ es: feedback: '¡Este campo no puede estar vacío!' uuid: label: 'Identificador único' + created_at: + label: 'Fecha de creación' geo: uri: 'Abrir en aplicación' osm: 'Abrir en mapa web' @@ -513,11 +578,9 @@ es: content: 'Cuerpo del artículo' categories: 'Todos' new: 'Tipos de artículos' - add: 'Agregar' - filter: 'Filtrar' - remove_filter: 'Volver' remove_filter_help: 'Quitar este filtro: %{filter}' - index: 'Artículos' + index: + search: 'Buscar' edit: 'Editar' preview: btn: 'Versión preliminar' @@ -655,7 +718,7 @@ es: new: 'Crear' edit: 'Configurar' posts: - new: 'Nuevo %{layout}' + new: 'Agregar %{layout}' edit: 'Editando' usuaries: index: 'Usuaries' @@ -670,3 +733,14 @@ es: queries: show: 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})" diff --git a/config/routes.rb b/config/routes.rb index a132135a..f2487066 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,14 +11,14 @@ Rails.application.routes.draw do namespace :v1 do resources :csp_reports, only: %i[create] - get :'sites/hidden_services', to: 'sites#hidden_services' - post :'sites/add_onion', to: 'sites#add_onion' resources :sites, only: %i[index], constraints: { site_id: /[a-z0-9\-.]+/, id: /[a-z0-9\-.]+/ } do get :'invitades/cookie', to: 'invitades#cookie' post :'posts/:layout', to: 'posts#create', as: :posts get :'contact/cookie', to: 'invitades#contact_cookie' post :'contact/:form', to: 'contact#receive', as: :contact + + post :'webhooks/pull', to: 'webhooks#pull' end end end @@ -77,5 +77,7 @@ Rails.application.routes.draw do get :'stats/host', to: 'stats#host' get :'stats/uris', to: 'stats#uris' get :'stats/resources', to: 'stats#resources' + + resources :build_stats, only: %i[index] end end diff --git a/db/migrate/20230119165420_create_distributed_press_publisher.rb b/db/migrate/20230119165420_create_distributed_press_publisher.rb new file mode 100644 index 00000000..8d8de37a --- /dev/null +++ b/db/migrate/20230119165420_create_distributed_press_publisher.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Crea la tabla de publishers de Distributed Press que contiene las +# instancias y tokens +class CreateDistributedPressPublisher < ActiveRecord::Migration[6.1] + def change + create_table :distributed_press_publishers do |t| + t.timestamps + t.string :instance, unique: true + t.text :token_ciphertext, null: false + t.datetime :expires_at, null: true + end + end +end diff --git a/db/migrate/20230318183722_rename_deploy_rsync_to_deploy_full_rsync.rb b/db/migrate/20230318183722_rename_deploy_rsync_to_deploy_full_rsync.rb new file mode 100644 index 00000000..689dc559 --- /dev/null +++ b/db/migrate/20230318183722_rename_deploy_rsync_to_deploy_full_rsync.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Cambia todos los DeployRsync propios de Sutty a DeployFullRsync que se +# encarga de sincronizar todo. +class RenameDeployRsyncToDeployFullRsync < ActiveRecord::Migration[6.1] + def up + DeployRsync.all.find_each do |deploy| + dest = deploy.destination.split(':', 2).first + + next unless nodes.include? dest + + deploy.destination = "#{dest}:" + deploy.type = 'DeployFullRsync' + + deploy.save + end + end + + def down + DeployFullRsync.all.find_each do |deploy| + next unless nodes.include? deploy.destination.split(':', 2).first + + deploy.destination = "#{deploy.destination}#{deploy.site.hostname}" + deploy.type = 'DeployRsync' + + deploy.save + end + end + + private + + def nodes + @nodes ||= Rails.application.nodes.map do |node| + "sutty@#{node}" + end + end +end diff --git a/db/migrate/20230322214924_add_code_of_conduct.rb b/db/migrate/20230322214924_add_code_of_conduct.rb new file mode 100644 index 00000000..f859b08c --- /dev/null +++ b/db/migrate/20230322214924_add_code_of_conduct.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Crea códigos de conducta +class AddCodeOfConduct < ActiveRecord::Migration[6.1] + def up + create_table :codes_of_conduct do |t| + t.timestamps + t.string :title + t.text :description + t.text :content + end + + # XXX: En lugar de ponerlo en las seeds + YAML.safe_load(File.read('db/seeds/codes_of_conduct.yml')).each do |coc| + CodeOfConduct.new(**coc).save! + end + end + + def down + drop_table :codes_of_conduct + end +end diff --git a/db/migrate/20230322231344_add_privacy_policy.rb b/db/migrate/20230322231344_add_privacy_policy.rb new file mode 100644 index 00000000..e0d7ae59 --- /dev/null +++ b/db/migrate/20230322231344_add_privacy_policy.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Agrega políticas de privacidad +class AddPrivacyPolicy < ActiveRecord::Migration[6.1] + def up + create_table :privacy_policies do |t| + t.timestamps + t.string :title + t.text :description + t.text :content + end + + # XXX: En lugar de ponerlo en las seeds + YAML.safe_load(File.read('db/seeds/privacy_policies.yml')).each do |pp| + PrivacyPolicy.new(**pp).save! + end + end + + def down + drop_table :privacy_policies + end +end diff --git a/db/migrate/20230325163802_add_short_description_to_licencias.rb b/db/migrate/20230325163802_add_short_description_to_licencias.rb new file mode 100644 index 00000000..efcc01e4 --- /dev/null +++ b/db/migrate/20230325163802_add_short_description_to_licencias.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Agrega descripciones cortas a las licencias +class AddShortDescriptionToLicencias < ActiveRecord::Migration[6.1] + def up + add_column :licencias, :short_description, :string + + YAML.safe_load_file('db/seeds/licencias.yml').each do |licencia| + Licencia.find_by_icons(licencia['icons']).update licencia + end + end + + def down + remove_column :licencias, :short_description + end +end diff --git a/db/migrate/20230328200129_add_consent_to_usuaries.rb b/db/migrate/20230328200129_add_consent_to_usuaries.rb new file mode 100644 index 00000000..1e85864d --- /dev/null +++ b/db/migrate/20230328200129_add_consent_to_usuaries.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Agrega consentimientos a les usuaries. No usamos un loop de +# Usuarie::CONSENT_FIELDS porque quizás agreguemos campos luego. +class AddConsentToUsuaries < ActiveRecord::Migration[6.1] + def change + add_column :usuaries, :privacy_policy_accepted_at, :datetime + add_column :usuaries, :terms_of_service_accepted_at, :datetime + add_column :usuaries, :code_of_conduct_accepted_at, :datetime + add_column :usuaries, :available_for_feedback_accepted_at, :datetime + end +end diff --git a/db/migrate/20230328213242_remove_acepta_politicas_de_privacidad_from_usuaries.rb b/db/migrate/20230328213242_remove_acepta_politicas_de_privacidad_from_usuaries.rb new file mode 100644 index 00000000..7ca562bf --- /dev/null +++ b/db/migrate/20230328213242_remove_acepta_politicas_de_privacidad_from_usuaries.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Elimina un campo que nunca se usó +class RemoveAceptaPoliticasDePrivacidadFromUsuaries < ActiveRecord::Migration[6.1] + def change + remove_column :usuaries, :acepta_politicas_de_privacidad, :boolean, default: false + end +end diff --git a/db/migrate/20230328231029_create_que_tables.rb b/db/migrate/20230328231029_create_que_tables.rb new file mode 100644 index 00000000..1ed929f7 --- /dev/null +++ b/db/migrate/20230328231029_create_que_tables.rb @@ -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 diff --git a/db/migrate/20230411185406_add_sustainability_to_access_logs.rb b/db/migrate/20230411185406_add_sustainability_to_access_logs.rb new file mode 100644 index 00000000..80f16fb5 --- /dev/null +++ b/db/migrate/20230411185406_add_sustainability_to_access_logs.rb @@ -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 diff --git a/db/migrate/20230415153231_add_priority_to_designs.rb b/db/migrate/20230415153231_add_priority_to_designs.rb new file mode 100644 index 00000000..7fc45558 --- /dev/null +++ b/db/migrate/20230415153231_add_priority_to_designs.rb @@ -0,0 +1,5 @@ +class AddPriorityToDesigns < ActiveRecord::Migration[6.1] + def change + add_column :designs, :priority, :integer + end +end diff --git a/db/migrate/20230421182627_change_full_rsync_destination.rb b/db/migrate/20230421182627_change_full_rsync_destination.rb new file mode 100644 index 00000000..3a22aea6 --- /dev/null +++ b/db/migrate/20230421182627_change_full_rsync_destination.rb @@ -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 diff --git a/db/migrate/20230424174544_add_node_to_access_logs.rb b/db/migrate/20230424174544_add_node_to_access_logs.rb new file mode 100644 index 00000000..805fbc27 --- /dev/null +++ b/db/migrate/20230424174544_add_node_to_access_logs.rb @@ -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 diff --git a/db/migrate/20230519143500_add_pagination_to_site.rb b/db/migrate/20230519143500_add_pagination_to_site.rb new file mode 100644 index 00000000..387dc588 --- /dev/null +++ b/db/migrate/20230519143500_add_pagination_to_site.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Agrega la opción de paginación a los sitios +class AddPaginationToSite < ActiveRecord::Migration[6.1] + def change + add_column :sites, :pagination, :boolean, default: false + end +end diff --git a/db/migrate/20230731195050_add_token_to_roles.rb b/db/migrate/20230731195050_add_token_to_roles.rb new file mode 100644 index 00000000..c38b0526 --- /dev/null +++ b/db/migrate/20230731195050_add_token_to_roles.rb @@ -0,0 +1,12 @@ +class AddTokenToRoles < ActiveRecord::Migration[6.1] + def up + add_column :roles, :token, :string + Rol.find_each do |m| + m.update_column( :token, SecureRandom.hex(64) ) + end + end + + def down + remove_column :roles, :token + end +end diff --git a/db/migrate/20230829204127_add_private_key_pem_ciphertext_to_sites.rb b/db/migrate/20230829204127_add_private_key_pem_ciphertext_to_sites.rb new file mode 100644 index 00000000..9f26f21a --- /dev/null +++ b/db/migrate/20230829204127_add_private_key_pem_ciphertext_to_sites.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Almacena las llaves privadas de cada sitio +class AddPrivateKeyPemCiphertextToSites < ActiveRecord::Migration[6.1] + # Agrega la columna cifrada + def change + add_column :sites, :private_key_pem_ciphertext, :text + end +end diff --git a/db/migrate/20230921155401_site_rename_origin_to_upstream.rb b/db/migrate/20230921155401_site_rename_origin_to_upstream.rb new file mode 100644 index 00000000..864f4c4a --- /dev/null +++ b/db/migrate/20230921155401_site_rename_origin_to_upstream.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Renombrar todos los repositorios que apunten a skel como su origin +class SiteRenameOriginToUpstream < ActiveRecord::Migration[6.1] + # Renombrar + def up + Site.find_each do |site| + next unless site.repository.origin&.url == ENV['SKEL_SUTTY'] + + site.repository.rugged.remotes.rename('origin', 'upstream') do |_| + Rails.logger.info "#{site.name}: renamed origin to upstream" + end + rescue Rugged::Error, Rugged::OSError => e + Rails.logger.warn "#{site.name}: #{e.message}" + end + end + + # No se puede deshacer + def down; end +end diff --git a/db/migrate/20230927153926_add_last_indexed_commit_to_sites.rb b/db/migrate/20230927153926_add_last_indexed_commit_to_sites.rb new file mode 100644 index 00000000..71e08f37 --- /dev/null +++ b/db/migrate/20230927153926_add_last_indexed_commit_to_sites.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Almacenar el último commit indexado +class AddLastIndexedCommitToSites < ActiveRecord::Migration[6.1] + def up + add_column :sites, :last_indexed_commit, :string, null: true + + Site.find_each do |site| + site.update_columns(last_indexed_commit: site.repository.head_commit.oid) + rescue Rugged::Error, Rugged::OSError => e + puts "Falló #{site.name}, ignorando: #{e.message}" + end + end + + def down + remove_column :sites, :last_indexed_commit + end +end diff --git a/db/schema.rb b/db/schema.rb deleted file mode 100644 index a395329d..00000000 --- a/db/schema.rb +++ /dev/null @@ -1,398 +0,0 @@ -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# This file is the source Rails uses to define your schema when running `bin/rails -# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to -# be faster and is potentially less error prone than running all of your -# migrations from scratch. Old migrations may fail to apply correctly if those -# migrations use external dependencies or application code. -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 2021_10_22_225449) do - - # These are extensions that must be enabled in order to support this database - enable_extension "pg_trgm" - enable_extension "pgcrypto" - enable_extension "plpgsql" - - create_table "access_logs", id: :uuid, default: nil, force: :cascade do |t| - t.string "host" - t.float "msec" - t.string "server_protocol" - t.string "request_method" - t.string "request_completion" - t.string "uri" - t.string "query_string" - t.integer "status" - t.string "sent_http_content_type" - t.string "sent_http_content_encoding" - t.string "sent_http_etag" - t.string "sent_http_last_modified" - t.string "http_accept" - t.string "http_accept_encoding" - t.string "http_accept_language" - t.string "http_pragma" - t.string "http_cache_control" - t.string "http_if_none_match" - t.string "http_dnt" - t.string "http_user_agent" - t.string "http_origin" - t.float "request_time" - t.integer "bytes_sent" - t.integer "body_bytes_sent" - t.integer "request_length" - t.string "http_connection" - t.string "pipe" - t.integer "connection_requests" - t.string "geoip2_data_country_name" - t.string "geoip2_data_city_name" - t.string "ssl_server_name" - t.string "ssl_protocol" - t.string "ssl_early_data" - t.string "ssl_session_reused" - t.string "ssl_curves" - t.string "ssl_ciphers" - t.string "ssl_cipher" - t.string "sent_http_x_xss_protection" - t.string "sent_http_x_frame_options" - t.string "sent_http_x_content_type_options" - t.string "sent_http_strict_transport_security" - t.string "nginx_version" - t.integer "pid" - t.string "remote_user" - t.boolean "crawler", default: false - t.string "http_referer" - t.datetime "created_at", precision: 6 - t.index ["geoip2_data_city_name"], name: "index_access_logs_on_geoip2_data_city_name" - t.index ["geoip2_data_country_name"], name: "index_access_logs_on_geoip2_data_country_name" - t.index ["host"], name: "index_access_logs_on_host" - t.index ["http_origin"], name: "index_access_logs_on_http_origin" - t.index ["http_user_agent"], name: "index_access_logs_on_http_user_agent" - t.index ["status"], name: "index_access_logs_on_status" - t.index ["uri"], name: "index_access_logs_on_uri" - end - - create_table "action_text_rich_texts", force: :cascade do |t| - t.string "name", null: false - t.text "body" - t.string "record_type", null: false - t.bigint "record_id", null: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true - end - - create_table "active_storage_attachments", force: :cascade do |t| - t.string "name", null: false - t.string "record_type", null: false - t.bigint "record_id", null: false - t.bigint "blob_id", null: false - t.datetime "created_at", null: false - t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" - t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true - end - - create_table "active_storage_blobs", force: :cascade do |t| - t.string "key", null: false - t.string "filename", null: false - t.string "content_type" - t.text "metadata" - t.bigint "byte_size", null: false - t.string "checksum", null: false - t.datetime "created_at", null: false - t.string "service_name", null: false - t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true - end - - create_table "active_storage_variant_records", force: :cascade do |t| - t.bigint "blob_id", null: false - t.string "variation_digest", null: false - t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true - end - - create_table "blazer_audits", force: :cascade do |t| - t.bigint "user_id" - t.bigint "query_id" - t.text "statement" - t.string "data_source" - t.datetime "created_at" - t.index ["query_id"], name: "index_blazer_audits_on_query_id" - t.index ["user_id"], name: "index_blazer_audits_on_user_id" - end - - create_table "blazer_checks", force: :cascade do |t| - t.bigint "creator_id" - t.bigint "query_id" - t.string "state" - t.string "schedule" - t.text "emails" - t.text "slack_channels" - t.string "check_type" - t.text "message" - t.datetime "last_run_at" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["creator_id"], name: "index_blazer_checks_on_creator_id" - t.index ["query_id"], name: "index_blazer_checks_on_query_id" - end - - create_table "blazer_dashboard_queries", force: :cascade do |t| - t.bigint "dashboard_id" - t.bigint "query_id" - t.integer "position" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["dashboard_id"], name: "index_blazer_dashboard_queries_on_dashboard_id" - t.index ["query_id"], name: "index_blazer_dashboard_queries_on_query_id" - end - - create_table "blazer_dashboards", force: :cascade do |t| - t.bigint "creator_id" - t.text "name" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["creator_id"], name: "index_blazer_dashboards_on_creator_id" - end - - create_table "blazer_queries", force: :cascade do |t| - t.bigint "creator_id" - t.string "name" - t.text "description" - t.text "statement" - t.string "data_source" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["creator_id"], name: "index_blazer_queries_on_creator_id" - end - - create_table "build_stats", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "deploy_id" - t.bigint "bytes" - t.float "seconds" - t.string "action", null: false - t.text "log" - t.boolean "status", default: false - t.index ["deploy_id"], name: "index_build_stats_on_deploy_id" - end - - create_table "csp_reports", id: :uuid, default: nil, force: :cascade do |t| - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.string "disposition" - t.string "referrer" - t.string "blocked_uri" - t.string "document_uri" - t.string "effective_directive" - t.string "original_policy" - t.string "script_sample" - t.string "status_code" - t.string "violated_directive" - t.integer "column_number" - t.integer "line_number" - t.string "source_file" - end - - create_table "deploys", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "site_id" - t.string "type" - t.text "values" - t.index ["site_id"], name: "index_deploys_on_site_id" - t.index ["type"], name: "index_deploys_on_type" - end - - create_table "designs", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "name" - t.text "description" - t.string "gem" - t.string "url" - t.string "license" - t.boolean "disabled", default: false - t.text "credits" - t.string "designer_url" - end - - create_table "indexed_posts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.bigint "site_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.string "locale", default: "simple" - t.string "layout", null: false - t.string "path", null: false - t.string "title", default: "" - t.jsonb "front_matter", default: "{}" - t.string "content", default: "" - t.tsvector "indexed_content" - t.integer "order", default: 0 - t.string "dictionary" - t.index ["front_matter"], name: "index_indexed_posts_on_front_matter", using: :gin - t.index ["indexed_content"], name: "index_indexed_posts_on_indexed_content", using: :gin - t.index ["layout"], name: "index_indexed_posts_on_layout" - t.index ["locale"], name: "index_indexed_posts_on_locale" - t.index ["site_id"], name: "index_indexed_posts_on_site_id" - end - - create_table "licencias", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "name" - t.text "description" - t.text "deed" - t.string "url" - t.string "icons" - end - - create_table "log_entries", force: :cascade do |t| - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.bigint "site_id" - t.text "text" - t.boolean "sent", default: false - t.index ["site_id"], name: "index_log_entries_on_site_id" - end - - create_table "maintenances", force: :cascade do |t| - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.text "message" - t.datetime "estimated_from" - t.datetime "estimated_to" - t.boolean "are_we_back", default: false - end - - create_table "mobility_string_translations", force: :cascade do |t| - t.string "locale", null: false - t.string "key", null: false - t.string "value" - t.string "translatable_type" - t.bigint "translatable_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_string_translations_on_translatable_attribute" - t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_string_translations_on_keys", unique: true - t.index ["translatable_type", "key", "value", "locale"], name: "index_mobility_string_translations_on_query_keys" - end - - create_table "mobility_text_translations", force: :cascade do |t| - t.string "locale", null: false - t.string "key", null: false - t.text "value" - t.string "translatable_type" - t.bigint "translatable_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_text_translations_on_translatable_attribute" - t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_text_translations_on_keys", unique: true - end - - create_table "roles", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "site_id" - t.bigint "usuarie_id" - t.string "rol" - t.boolean "temporal" - t.index ["site_id", "usuarie_id"], name: "index_roles_on_site_id_and_usuarie_id", unique: true - t.index ["site_id"], name: "index_roles_on_site_id" - t.index ["usuarie_id"], name: "index_roles_on_usuarie_id" - end - - create_table "rollups", force: :cascade do |t| - t.string "name", null: false - t.string "interval", null: false - t.datetime "time", null: false - t.jsonb "dimensions", default: {}, null: false - t.float "value" - t.index ["name", "interval", "time", "dimensions"], name: "index_rollups_on_name_and_interval_and_time_and_dimensions", unique: true - end - - create_table "sites", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "name" - t.bigint "design_id" - t.bigint "licencia_id" - t.string "status", default: "waiting" - t.text "description" - t.string "title" - t.boolean "colaboracion_anonima", default: false - t.boolean "contact", default: false - t.string "private_key_ciphertext" - t.boolean "acepta_invitades", default: false - t.string "tienda_api_key_ciphertext", default: "" - t.string "tienda_url", default: "" - t.string "api_key_ciphertext" - t.index ["design_id"], name: "index_sites_on_design_id" - t.index ["licencia_id"], name: "index_sites_on_licencia_id" - t.index ["name"], name: "index_sites_on_name", unique: true - end - - create_table "stats", force: :cascade do |t| - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.bigint "site_id" - t.string "name", null: false - t.index ["name"], name: "index_stats_on_name", using: :hash - t.index ["site_id"], name: "index_stats_on_site_id" - end - - create_table "usuaries", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.string "confirmation_token" - t.datetime "confirmed_at" - t.datetime "confirmation_sent_at" - t.string "unconfirmed_email" - t.integer "failed_attempts", default: 0, null: false - t.string "unlock_token" - t.datetime "locked_at" - t.boolean "acepta_politicas_de_privacidad", default: false - t.string "invitation_token" - t.datetime "invitation_created_at" - t.datetime "invitation_sent_at" - t.datetime "invitation_accepted_at" - t.integer "invitation_limit" - t.string "invited_by_type" - t.bigint "invited_by_id" - t.integer "invitations_count", default: 0 - t.string "lang", default: "es" - t.index ["confirmation_token"], name: "index_usuaries_on_confirmation_token", unique: true - t.index ["email"], name: "index_usuaries_on_email", unique: true - t.index ["invitation_token"], name: "index_usuaries_on_invitation_token", unique: true - t.index ["invitations_count"], name: "index_usuaries_on_invitations_count" - t.index ["invited_by_id"], name: "index_usuaries_on_invited_by_id" - t.index ["invited_by_type", "invited_by_id"], name: "index_usuaries_on_invited_by" - t.index ["reset_password_token"], name: "index_usuaries_on_reset_password_token", unique: true - t.index ["unlock_token"], name: "index_usuaries_on_unlock_token", unique: true - end - - add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" - add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" - create_trigger("indexed_posts_before_insert_update_row_tr", :compatibility => 1). - on("indexed_posts"). - before(:insert, :update) do - <<-SQL_ACTIONS -new.indexed_content := to_tsvector(('pg_catalog.' || new.dictionary)::regconfig, coalesce(new.title, '') || ' -' || coalesce(new.content,'')); - SQL_ACTIONS - end - - create_trigger("access_logs_before_insert_row_tr", :compatibility => 1). - on("access_logs"). - before(:insert) do - "new.created_at := to_timestamp(new.msec);" - end - -end diff --git a/db/seeds.rb b/db/seeds.rb index 214bbcb0..b9ef96a1 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -16,12 +16,14 @@ licencias.each do |l| licencia.update l end -unless Rails.env.test? - YAML.safe_load(File.read('db/seeds/sites.yml')).each do |site| - site = Site.find_or_create_by name: site['name'] - - site.update licencia: Licencia.first, design: Design.first, - title: site.name, description: 'x' * 50, - deploys: site.deploys.empty? ? [DeployLocal.new] : site.deploys +if CodeOfConduct.count.zero? + YAML.safe_load(File.read('db/seeds/codes_of_conduct.yml')).each do |coc| + CodeOfConduct.new(**coc).save! end -end +end + +if PrivacyPolicy.count.zero? + YAML.safe_load(File.read('db/seeds/privacy_policies.yml')).each do |pp| + PrivacyPolicy.new(**pp).save! + end +end diff --git a/db/seeds/codes_of_conduct.yml b/db/seeds/codes_of_conduct.yml new file mode 100644 index 00000000..64582072 --- /dev/null +++ b/db/seeds/codes_of_conduct.yml @@ -0,0 +1,613 @@ +--- +- title_en: "Codes for sharing" + title_es: "Códigos para compartir" + description_en: "Codes of conduct allow inclusive communities." + description_es: "Los códigos de convivencia nos permiten alojar comunidades inclusivas." + content_en: | + # Code for sharing + + > This code of conduct is based in "[Códigos para compartir, hackear, + > piratear en + > libertad](https://utopia.partidopirata.com.ar/zines/codigos_para_compartir.html)" + > published by [Partido Interdimensional + > Pirata](https://partidopirata.com.ar/). + + > We use gender neutral pronouns to include all peoples. In this sense, + > we encourage different forms, strategies and tools used to embody + > practices that aren't anthropocentric, sexist, cis-sexist in our + > language. + + ## Introduction + + This is an example code that strives to give a consensual frame to + enable asistance, permanence and confortable stay to everyone using and + inhabiting [Sutty](https://sutty.nl/), and to welcome new users and + potential allies as well. It sets the floor for desirable and + acceptable, and undesirable and intolerable conducts for its community. + You can use it with or without changes, adapting it to your activities. + This code is in permanent and collective mutation and feeds, copies and + inspires on the following sources: + + * + + * + + * + + * + + We strive to sustain and foment an open community, that invites and + attains participation from more people, in all their diversity. We know + that spaces related to computing and free software are mostly inhabited + by middle class cis white males, even when there's an acknowledgement of + the need to close the gender gap. In this sense, this is our little + contribution, made from collective practices on multiple dimentions, + reflections, readings, and experiences that grow every day. + + ## That everyone needs to be well treated + + Every being that we share space with deserves good treatment, respect + and compassion. Here we share basic criteria for introduction and care. + + ### Towards humans + + Everyone is deserving of care and greetings and we have a right to + assume good intentions from others. + + When we refer to other humans, we try to be careful and respectul + towards their gender identity. For this, these principles are useful: + + * Don't assume, judge or try to "interpret" the gender of others. + + * Don't use gender beforehand. It's related to the previous item, but + puts emphasis towards naturalized gendering behaviour (ie. assuming + someone wearing a dress uses female pronouns...). The proposal is to + discard them. + + * If a person explicits their pronouns and mode in which they want to be + referenced, we respect them, by listening and trying to use their + prefered pronouns. + + * When presentations don't include pronouns, we can ask respectfully + for prefered pronouns. But be careful! This question must be asked + to everyone, otherwise the "suspicion" is loaded towards a person, and + it can become a form of harrassment. + + * Do we need to know the gender of a person to relate with them? Maybe + a better practice is to evade gendering others. But if this means to + use "them" compulsively, some people may be made to feel bad. (For + instance, trans\* people who use female or male pronouns may feel + upset or outed when refering to them as "them", specially if they're + the only ones to be gendered like this in a group! + + * When in doubt, asking and apologizing respectfully is a good way of + being careful towards each other. + + ## Important points to guarantee our space from being expulsive + + **Listening to and between everyone in a caring climate** + + * Listen to what everyone has to say, being mindful that everyone has + something valuable to communicate. + + * For active listening, we prefer to ask first, before making judgement. + + * Sometimes being silent is a condition for others to be able to talk. + To listen is an exercise that requires practice. Also talking. + + * We're interested in what everyone has to say. If you're more trained + in participating, talking, and having opinions, take into account that + not everyone does. Give them space if they want to take it. But + remember that encouraging is not the same as pressuring! + + * We try to check and stop offensive practices to add to the respectful + climate. This doesn't mean to be submissive or to agree to + everything. At the least, it sets a floor of respect towards enabling + a dialogue when necessary. + + * It's at the very least disrepectful to repeat damaging behaviour when + it was already identified as such. It can make others unconfortable, + or hurt and expel them. We'll make this point every time that is + needed and tolerable. + + * We avoid this behaviour ourselves and we help others to notice their + own. + + * When raising attention becomes insufficient, we need to review this + agreements to keep the coexistence. This implies to act in accord to + them, and that this code can be revised and updated when deemed + necessary (there's no consensus). + + * One of the ways in which free software spaces can be and are expulsive + is with attitudes that don't contemplate diversity in knowledges and + interlocutors. By appealing to technicisms, many comrades are kept + out of what's happening, and no one verifies if everyone is keeping up + with the conversation. + + Our fervent recommendation is to be attentive to this dynamic so we + can avoid or revert them. + + * The counter to the previous situation is "mansplaining": a cis-male + person assuming the authoritative place of knowledge to (over-)explain + everything to others, in a patronizing way and without taking into + account what others want to listen or not, to say, what already know + or do, etc. + + * We believe there's no "authoritative voice" to have an opinion and to + participate. Free culture is for everyone to share. + + * "Sharing is caring" v. "Google is your friend". Meritocracy and other + traditional codes in cyber-communities work against free culture. We + support the pirate culture that onboards ever more pirates to their + ships. We believe that culture is for everyone and we defy elitisms. + + * We don't assume that other people shares our likings, beliefs, class + position, sexuality, etc. We can be violent when we misread others. + We recommend to ask respectfully and to avoid comments or jokes that + can be hurtful to others. + + * We speak and act gently and inclusively. + + * We respect different points of view, experiences, beliefs, etc. and we + take them into account when we act collectively so it reflects in our + attitudes. + + * We welcome criticism, specially the constructive kind ;) + + * We focus in what's best for the community, without losing warmth, + respect and diversity amongst ourselves. + + * We show empathy toward others. We want to share and communicate. + + * It's useful to think everyone has different abilities, stories, + experiences... It's possible to not understand some comments. We try + to avoid acting in bad faith and to use every accessibility tool we + can. + + * The last item includes neurodiverse people and those that have + experienced trauma. Sometimes, sarcasm or irony is not well received + or understood by others. We take this into our strategies to include + everyone in our communications. Even more, if we think some topics + could be sensitive to others (memory-triggering, phobias, untolerable, + explicit violence or body images, etc.), we use content warnings (cw) + before what we wanted to share. For instance: "cw: comments about + sexual and physical violence". This allows everyone to opt in to the + content instead of being taken by surprise. + + * We're respectful of limits established by others (personal space, + physical contact, interaction mood, privacy, being photographed, etc.) + + * We want to and believe in welcoming more pirates! + + ## Consent for documenting and sharing in media + + * If you're going to take pictures or record video, ask consent from + people involved. + + * If there're minors, ask their responsible families. + + ## Our commitment against harassment + + In the interest of fomenting an open, diverse and welcoming community, + we contributors and admins make a commitment against harassment in our + projects and community for everyone, without regard of age, body + diversity, capacity, neuro-diversity, ethnicity, gender identity and + expression, experience level, nationality, physical appearance, + religion, sexual identity or orientation. + + Examples of unacceptable behaviour from participants: + + * Offensive comments about gender/s, gender identity and expression, + sexual orientation, capacity, mental sickness, neuro-(a)tipicality, + physical appearance, body size, ethnicity or religion. + + * Unwelcomed comments related to personal and life choices, including + amongst others, those related to food, health, children upbringing, + drug use and employment. + + * Insulting or despective comments and personal or political attacks. + **Trolling**. + + * Assuming others' gender. If you're in doubt, ask politely about + pronouns. Don't use the name(s) that people don't use anymore, use + the name, _nickname_ or pseudonym that they prefer. Do you really + need the name, ID number, biometric data, birth certificate of others? + + * Sexual comments, images or behaviour, unneeded or in spaces where they + weren't appropiate. + + * Unconsented physical contact or repeated after being asked to stop. + + * Threatening others. + + * Inciting violence towards others, including self-damage. + + * Deliberate intimidation. + + * Stalking. + + * To harass by photographing or recording without consent, including + uploading personal information to the Internet. + + * Interrupting a conversation constantly. + + * Making unwanted sexual comments. + + * Unappropiate patterns of social contact, like asking/assuming + inappropiate intimacy levels with others. + + * Trying to interact with a person after being asked not to. + + * Exposing deliberately any aspect of a person identity without consent, + except when necessary for protecting others against intentional abuse. + + * Making public any kind of private conversation. + + * Other kinds of conduct that can be considered inappropiate in an + environment of camaraderie. + + * Repeating attitudes that others find offensive or violatory of this + code. + + ## Consequences + + * Any person that has been asked to stop offensive behaviour is expected + to respond immediately, even when in disagreement. + + * Admins can take any action deemed necessary and adequate, including + expelling the person or removing their site without advertence. This + decision is taken by consensus between admins and is reserved for + extreme cases that compromise the community or the permanence of + others without feeling wronged or threatened. + + * Admins reserve the right to forbid participation to any future + activity or publication. + + As we mentioned before, this code is in permanent collective mutation. + It's main objective is to generate an inclusive and non-expulsive + environment that is also transparent and open without [missing + stairs](https://en.wikipedia.org/wiki/Missing_stair) ("the missing stair + from a house that everyone knows about but no one wants to take + responsibility for"). It's important to adapt it to different + activities and that it nurtures from contributions from its users. + Receiving your comments and input will help us to achieve this + objective. + + ## Let's keep in contact! + + Si pasaste por alguna situación que quieras compartir --te hayas animado + o no a decirlo en el momento--, podés ponerte en contacto con nosotres. + + Con respecto a quejas o avisos acerca de situaciones de violencia, + acoso, abuso o repetición de conductas que se advirtieron como + intolerables, tomamos la responsabilidad de tenerlas en cuenta y + trabajar en ellas para que el resultado sea el favorable al espíritu de + colectiva que elegimos y describimos aquí. Si bien consideramos que las + prácticas punitivistas no van con nosotres, nuestra decisión explícita + es escuchar a la persona que se manifiesta como violentada o víctima y + acompañarla. + + You can contact us if you were part of a situation you want to share + --even if you didn't pointed it in the moment. + + In regards to complaints or notices about violence, harassment, abuse or + repeated untolerable conducts, we take the responsibility of working on + them for a favorable result towards the collective spirit we defined + here. Even when we don't condone punitivist practices, our explicit + decision is for the victim to be listened and accompanied. + content_es: | + # Códigos para compartir + + > Este código de convivencia está basado en los "[Códigos para + > compartir, hackear, piratear en + > libertad](https://utopia.partidopirata.com.ar/zines/codigos_para_compartir.html)" + > publicados por el [Partido Interdimensional + > Pirata](https://partidopirata.com.ar/). + + > Utilizamos preferentemente la 'e' para referirnos a las personas en + > general. En ese sentido, alentamos las diferentes formas, estrategias + > y herramientas para incorporar prácticas no antropocéntricas, + > sexistas, ni cisexistas en la lengua. Otras alternativas que apoyamos + > --y eventualmente usamos-- son el uso del femenino, la letra e, arrobas, + > equis, asteriscos, etc. + + ## Introducción + + Este es un ejemplo de código que busca aportar un marco de consenso para + garantizar la asistencia, permanencia y cómoda estadía de todas las + personas que habitan y utilizan Sutty, así como para bienvenir a nueves + usuaries y potenciales aliades. Para esto, fija un piso de conductas + deseables, aceptables, indeseables y/o intolerables para la comunidad. + Podés usarlo sin cambios o modificarlo para adaptarlo a tus actividades. + Este código está en permanente mutación colectiva y se alimenta, copia e + inspira de las siguientes fuentes: + + * + + * + + * + + * + + Procuramos mantener y fomentar una comunidad abierta, que invite y logre + la participación de cada vez más personas, en toda su diversidad. + Sabemos que los espacios de Software Libre, informática, sistemas, etc. + son habitados mayormente por varones cis, blancos y de clase media, pese + al reconocimiento de la necesidad de eliminar la brecha de géneros. En + este sentido, este es nuestro pequeño aporte, hecho de prácticas + colectivas de múltiples dimensiones, reflexiones, lecturas, experiencias + que crecen día a día. + + ## Que todes les seres sean bien tratades + + Cada ser con el que compartamos el espacio es merecedore de buen trato, + respeto y compasión. En otras palabras, compartimos a continuación los + criterios básicos de presentación y cuidados. + + Para les humanes + + Todes somos dignes de cuidados y de saludos y tenemos derecho a suponer + las buenas intenciones de le otre. + + Para referirnos a otres humanes, trataremos de ser cuidadoses y + respuestuoses de su identidad de género. Para ello, son útiles los + siguientes principios: + + * No presuponer, juzgar o "interpretar" el género de le otre. + + * No generizar de antemano. Se desprende del punto anterior, pero hace + especial énfasis en comportamientos naturalizados de generización (EJ: + presuponer que porque una persona usa un vestido se nombra en + femenino...). La propuesta es desecharlos. + + * Si la persona explicita sus pronombres y modos en que quiere ser + referenciade, lo respetamos, escuchando y procurando referirnos a elle + usando sus pronombres elegidos. + + * Si no se incluye en la presentación los pronombres preferidos, podemos + preguntar respetuosamente qué pronombres se usan. ¡Pero atención! Es + una pregunta que debe dirigirse a todes por igual, de lo contrario, + carga la "sospecha" sobre la persona señalada y puede resultar en una + forma de hostigamiento. + + * ¿Es necesario conocer el género de una persona para relacionarnos o + referirnos a ella? Quizás una buena práctica es evitar generizar para + todas las personas. Pero si esto implica el uso compulsivo de la "e" + para todes, puede ser que alguna persona se sienta molesta. (Por + ejemplo, las personas trans\* que se identifican en femenino o + masculino suelen sentirse molestas y "sacadas del clóset" u *outeadas* + si se refieren a ellas con la "e", ¡en especial si son las únicas en + ser generizadas de esta forma en un grupo!). + + * Ante cualquier duda, preguntar respetuosamente y disculparse + respetuosamente es una buena idea para ayudar a cuidarnos. + + ## Puntos importantes para garantizar que nuestro espacio no resulte expulsivo + + **Escucharnos a todas y entre todas en un clima de cuidados** + + * Escuchar lo que cada quien tiene para decir, conscientes de que todes + tenemos algo valioso para comunicar(nos). + + * Para la escucha activa, preferimos preguntar primero, en lugar de + hacer juicios. + + * Hacer silencio a veces es la condición para que otres puedan animarse + a hablar. Escuchar es un ejercicio que requiere práctica. También lo + es hablar. + + * Nos interesa lo que todes tengan para decir. Por lo tanto, si estás + más entrenade en el ejercicio de participar, hablar, opinar, tené en + cuenta que quizás haya otres que no lo estén tanto: darles el espacio + si quieren tomarlo. ¡Pero recordá que incentivar no es lo mismo que + presionar! + + * Tratamos de revisar y discontinuar alguna práctica que pueda haber + resultado ofensiva, para sumar al clima de respeto. Sin embargo, esto + no significa "bajar la cabeza" o estar necesariamente de acuerdo. Al + menos, fija un piso de respeto para comenzar un diálogo en el caso en + que sea necesario. + + * Es --al menos-- una falta de respeto repetir un comportamiento dañino + que ya se identificó como tal. Puede incomodar, lastimar y expulsar a + otres, por lo que preferimos llamar la atención sobre este punto todas + las veces que sea necesario y tolerable. + + * Evitamos esto nosotres y ayudamos a otres a darse cuenta cuando lo + están haciendo. + + * En los casos en los que los llamados de atención resulten + insuficientes, hemos de revisar estos acuerdos para sostener la + convivencia. Eso implica actuar de acuerdo a ellos. Y también que + estos códigos pueden ser revisados y actualizados en caso de que se + considere necesario (deje de haber consenso). + + * Una manera en la que los espacios de Software Libre y tecnologías + pueden y suelen ser expulsivos es mediante actitudes que no contemplan + la diversidad de saberes e interlocutor\*s. So pretexto de incluir + tecnicismos, muches compañeres quedan al margen de lo que está + sucediendo, muchas veces, sin que nadie tenga la mínima delicadeza de + verificar que todes estén siguiendo la conversación. + + Recomendamos fervientemente estar atentes a estas dinámicas para poder + evitarlas y/o revertirlas. + + * La otra cara de la situación anterior es el famoso _mansplaining_: un + tipo cis poniéndose en el lugar de la autoridad del saber para + (sobre-)explicar todo a le otre, de manera paternalista y sin tener en + cuenta lo que le otre quiere o no escuchar, decir, lo que sabe o hace, + etc. + + * Creemos que no hace falta ser "una voz autorizada" para opinar y + participar. La cultura libre se comparte entre todes. + + * "Compartir es bueno" vs. "Google es tu amigo". La meritocracia y + ciertos códigos tradicionales de ciertas ciber-comunidades suelen + operar de manera contraria a la propuesta de la cultura libre de + compartir. Apoyamos la cultura piratil que suma más piratas a los + barcos. Creemos que la cultura es para todes y desafiamos las + prácticas elitistas. + + * No damos por sentado que la persona con la que estamos interactuando + comparte gustos, creencias, pertenencias de clase, sexualidad, etc. + Podemos ser violentes si hacemos una lectura equivocada de le otre. + Recomendamos siempre preguntar de manera respetuosa y evitar + comentarios o chistes que puedan herir a les otres. + + * Usamos lenguaje amable e inclusivo y mostramos conductas amables e + inclusivas. + + * Respetamos los diferentes puntos de vista, experiencias, creencias, + etc. y lo tenemos en cuenta cuando estamos en grupo para verlo + reflejado en nuestras actitudes. + + * Aceptamos las críticas. En especial las constructivas ;) + + * Nos enfocamos en lo que es mejor para la comunidad, sin por ello + perder de vista la calidez, el respeto y la diversidad entre cada une + de nosotres. + + * Mostramos empatía con les otres. Queremos comunicarnos y compartir. + + * Es útil tener en cuenta que las personas tenemos capacidades, + historias, recorridos... diferentes. Es posible que algunos + comentarios no sean comprendidos. Trataremos de evitar la mala fe y + sumar todas las herramientas de accesibilidad para todas las personas. + + * El punto anterior incluye a personas neurodiversas y con experiencias + de trauma. A veces el sarcasmo o la ironía no es bien recibido o + comprendido por todes. Será útil tenerlo en cuenta para buscar + estrategias que no excluyan a las personas de nuestros intercambios. + Por otro lado, si creemos que determinados temas pueden ser sensibles + (desencadenantes de recuerdos, fobias, difíciles de tolerar o cargados + de violencia o imágenes corporales muy explícitas, por ejemplo) para + algunas personas y nos valemos de las advertencias de contenido o + _content warning_ (cw) (ej: "cw: comentarios de violencia sexual y + violencia física") antes del contenido a introducir. Esto permite que + cada cual pueda elegir si acceder o no a esos contenidos y que no le + tomen por sorpresa. + + * Respetamos los límites que establecen otras personas (espacio + personal, contacto físico, ganas de interactuar, no querer dar datos + de contacto o ser fotografiades, etc.) + + * ¡Queremos y (creemos) en sumar piratas! + + ## Consentimiento para documentar o compartir en medios + + * Si vas a publicar video o fotos, obtené el consentimiento de las + personas. + + * Si hay menores, consultalo con su familia responsable. + + ## Nuestro compromiso contra el acoso + + En el interés de fomentar una comunidad abierta, diversa y hospitalaria, + nosotres como contribuyentes y administradores nos comprometemos a hacer + de la participación en nuestro proyecto y nuestra comunidad una + experiencia libre de acoso para todes, independientemente de la edad, + diversidad corporal, capacidades, neuro-diversidad, etnia, identidad y + expresión de género, nivel de experiencia, nacionalidad, apariencia + física, raza, religión, identidad u orientación sexual y otras. + + Ejemplos de comportamiento inaceptable por parte de participantes: + + * Comentarios ofensivos relacionados con el/los género/s, la identidad + y expresión de género, la orientación sexual, las capacidades, las + enfermedades mentales, la neuro(a)tipicalidad, la apariencia física, + el tamaño corporal, la raza o la religión. + + * Comentarios indeseados relacionados con las elecciones y las prácticas + de estilo de vida de una persona, incluidas, entre otras, las + relacionadas con alimentos, salud, crianza de les hijes, drogas y + empleo. + + * Comentarios insultantes o despectivos (_trolling_) y ataques + personales o políticos. + + * Dar por sentado el género de las demás personas. En caso de duda, + preguntá educadamente por los pronombres. No uses nombres con los que + las personas no se identifican, usá el nombre, _nickname_ o apodo que + hayan elegido (¿Realmente necesitás el nombre y el número de DNI, + datos biométricos, carta natal, etc.?). + + * Comentarios, imágenes o comportamientos sexuales innecesarios o fuera + de lugar en espacios en los que no son apropiados. + + * Contacto físico sin consentimiento o reiterado tras un pedido de cese. + En el mismo sentido, invasión del espacio corporal (y espacios en + general). + + * Amenazas contra otras personas. + + * Incitación a la violencia contra otra persona, que también incluye + alentar a una persona a autolesionarse. + + * Intimidación deliberada. + + * Acechar (_stalkear_) o perseguir. + + * Acosar fotografiando o grabando sin consentimiento, incluyendo también + subir información personal a Internet sobre alguien para acosarle. + + * Interrumpir constantemente en una conversación. + + * Hacer comentarios sexuales indeseados. + + * Patrones de contacto social inapropiados, como por ejemplo + pedir/suponer niveles de intimidad inapropiados con les demás. + + * Seguir tratando de entablar conversación con una persona cuando se te + pidió que no lo hagas. + + * Divulgar deliberadamente cualquier aspecto de la identidad de una + persona sin su consentimiento, excepto que sea necesario para proteger + a otras personas de abuso intencional. + + * Hacer pública una conversación privada de cualquier tipo. + + * Otros tipos de conducta que pudieran considerarse inapropiadas en un + entorno de camaradería. + + * Reiteración de actitudes que les participantes señalen como ofensivas + o violatorias de este código. + + ## Consecuencias + + * Se espera que la persona a la que se la haya pedido que cese un + comportamiento que infringe este código acate el pedido de forma + inmediata, incluso si no está de acuerdo con este. + + * Les administradores pueden tomar cualquier acción que juzguen + necesaria y adecuada, incluyendo expulsar a la persona o dar de baja + sus sitios sin advertencia. Esta decisión la toman les administradores + en consenso y se reserva para casos extremos que comprometan la + continuidad de la comunidad o bien la posibilidad de permanencia en + ella de otres participantes sin sentirse agraviades o amenazades. + + * Les administradores se reservan el derecho a prohibir la asistencia a + cualquier actividad futura o publicación de sitios. + + Como mencionamos antes, este código está en permanente mutación + colectiva. El objetivo principal es generar un ambiente inclusivo y no + expulsivo, un ambiente transparente y abierto en el que no haya + escalones faltantes ("el escalón que falta en la escalera y todo el + mundo sabe y avisa pero nadie se quiere hacer cargo"). Es importante que + se adapte a las actividades y se nutra de las contribuciones de les + usuaries. Recibir tus comentarios y aportes nos ayudará a cumplir con + su objetivo principal. + + ## ¡Sigamos en contacto! + + Si pasaste por alguna situación que quieras compartir --te hayas animado + o no a decirlo en el momento--, podés ponerte en contacto con nosotres. + + Con respecto a quejas o avisos acerca de situaciones de violencia, + acoso, abuso o repetición de conductas que se advirtieron como + intolerables, tomamos la responsabilidad de tenerlas en cuenta y + trabajar en ellas para que el resultado sea el favorable al espíritu de + colectiva que elegimos y describimos aquí. Si bien consideramos que las + prácticas punitivistas no van con nosotres, nuestra decisión explícita + es escuchar a la persona que se manifiesta como violentada o víctima y + acompañarla. diff --git a/db/seeds/designs.yml b/db/seeds/designs.yml index 126a9b12..d1ae458e 100644 --- a/db/seeds/designs.yml +++ b/db/seeds/designs.yml @@ -6,6 +6,7 @@ disabled: true description_en: "Upload your own theme. [This feature is in development, help us!](https://sutty.nl/en/#contact)" description_es: "Subir tu propio diseño. [Esta posibilidad está en desarrollo, ¡ayudanos!](https://sutty.nl/#contacto)" + priority: '0' - name_en: 'I want you to develop a site for me' name_es: 'Quiero que desarrollen mi sitio' gem: 'sutty-theme-custom' @@ -13,22 +14,25 @@ disabled: true description_en: "If you want us to develop your site, you're welcome to [contact us!](https://sutty.nl/en/#contact) :)" description_es: "Si querés que desarrollemos tu sitio, [escribinos](https://sutty.nl/#contacto) :)" + priority: '2' - name_en: 'Minima' name_es: 'Mínima' gem: 'sutty-minima' - url: 'https://0xacab.org/sutty/jekyll/minima' + url: 'https://minima.sutty.nl/' description_en: "Sutty Minima is based on [Minima](https://jekyll.github.io/minima/), a blog-focused theme for Jekyll." description_es: 'Sutty Mínima es una plantilla para blogs basada en [Mínima](https://jekyll.github.io/minima/).' license: 'https://0xacab.org/sutty/jekyll/minima/-/blob/master/LICENSE.txt' + priority: '100' - name_en: 'Sutty' name_es: 'Sutty' gem: 'sutty-jekyll-theme' - url: 'https://rubygems.org/gems/sutty-jekyll-theme/' + url: "https://anarres.sutty.nl" description_en: "The Sutty design" description_es: 'El diseño de Sutty' license: 'https://0xacab.org/sutty/jekyll/sutty-jekyll-theme/-/blob/master/LICENSE.txt' credits_es: 'Sutty es parte de la economía solidaria :)' credits_en: 'Sutty is a solidarity economy project!' + priority: '90' - name_en: 'Self-managed Book Publisher' name_es: 'Editorial Autogestiva' gem: 'editorial-autogestiva-jekyll-theme' @@ -38,6 +42,7 @@ license: 'https://0xacab.org/sutty/jekyll/editorial-autogestiva-jekyll-theme/-/blob/master/LICENSE.txt' credits_es: 'Esta plantilla fue inspirada en el trabajo de las [editoriales autogestivas](https://sutty.nl/plantillas-para-crear-cat%C3%A1logos-de-editoriales-autogestivas/)' credits_en: 'This theme is inspired by [independent publishing projects](https://sutty.nl/en/new-template-for-publishing-projects/)' + priority: '50' - name_en: 'Donations' name_es: 'Donaciones' gem: 'sutty-donaciones-jekyll-theme' @@ -47,6 +52,7 @@ license: 'https://0xacab.org/sutty/jekyll/sutty-donaciones-jekyll-theme/-/blob/master/LICENSE.txt' credits_es: 'Diseñamos esta plantilla para [visibilizar campañas de donaciones](https://sutty.nl/plantilla-para-donaciones/) durante la cuarentena.' credits_en: 'We designed this theme to increase [visibility for donation requests](https://sutty.nl/template-for-donations/) during the quarantine.' + priority: '80' - name_en: 'Support campaign' name_es: 'Adhesiones' gem: 'adhesiones-jekyll-theme' @@ -57,6 +63,7 @@ credits_es: 'Desarrollamos esta plantilla junto con [Librenauta](https://sutty.nl/plantilla-para-campa%C3%B1as-de-adhesiones/)' credits_en: 'This template was made in collaboration with Librenauta' designer_url: 'https://copiona.com/donaunbit/' + priority: '60' - name_en: 'Community Radio' name_es: 'Radio comunitaria' gem: 'radios-comunitarias-jekyll-theme' @@ -67,6 +74,15 @@ credits_es: 'Desarrollamos esta plantilla junto con Librenauta en 15 horas :)' credits_en: 'This template was made in collaboration with Librenauta in 15 hours!' designer_url: 'https://copiona.com/donaunbit/' + priority: '70' +- name_en: 'Magazine' + name_es: 'Revista' + gem: 'compost-jekyll-theme' + url: 'https://two.compost.digital/' + description_en: 'A theme to create multimedia publications, based in COMPOST magazine' + description_es: 'Plantilla para crear publicaciones multimedia, basada en COMPOST magazine' + license: 'https://0xacab.org/sutty/jekyll/compost-jekyll-theme/-/blob/no-masters/LICENSE.txt' + priority: '40' - name_en: 'Resource toolkit' name_es: 'Recursero' gem: 'recursero-jekyll-theme' @@ -74,10 +90,12 @@ disabled: true description_en: "We're working towards adding more themes for you to use. [Contact us!](https://sutty.nl/en/#contact)" description_es: "Estamos trabajando para que puedas tener más diseños. [¡Escribinos!](https://sutty.nl/#contacto)" -- name_en: 'Other themes' - name_es: 'Mi propio diseño' + priority: '3' +- name_en: 'More themes' + name_es: 'Más plantillas' gem: 'sutty-theme-own' url: 'https://jekyllthemes.org' disabled: true description_en: "We're working towards adding more themes for you to use. [Contact us!](https://sutty.nl/en/#contact)" description_es: "Estamos trabajando para que puedas tener más diseños. [¡Escribinos!](https://sutty.nl/#contacto)" + priority: '1' diff --git a/db/seeds/licencias.yml b/db/seeds/licencias.yml index cbe3bace..f6b76296 100644 --- a/db/seeds/licencias.yml +++ b/db/seeds/licencias.yml @@ -1,6 +1,19 @@ --- +- name_en: "Custom license" + name_es: "Licencia personalizada" + url_en: "" + url_es: "" + icons: "custom" + short_description_en: "" + short_description_es: "" + description_en: "The license terms are provided by you." + description_es: "Los términos de la licencia fueron provistos por vos." + deed_en: "" + deed_es: "" - name_en: 'Peer Production License' name_es: 'Licencia de Producción de Pares' + short_description_en: "This work is licensed under a Peer Production License" + short_description_es: "Esta obra está bajo una Licencia de Producción de Pares" icons: "/images/ppl.png" url_en: 'https://wiki.p2pfoundation.net/Peer_Production_License' url_es: 'https://endefensadelsl.org/ppl_es.html' @@ -100,6 +113,8 @@ hacerlo es enlazar a esta página. - icons: "/images/by.png" + short_description_en: "This work is licensed under a Creative Commons Attribution 4.0 International License." + short_description_es: "Esta obra está bajo una Licencia Creative Commons Atribución 4.0 Internacional." name_en: 'Creative Commons Attribution 4.0 International (CC BY 4.0)' description_en: "This license gives everyone the freedom to use, adapt, and redistribute the contents of your site by requiring @@ -194,6 +209,8 @@ - icons: "/images/sa.png" name_en: "Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)" name_es: "Creative Commons Atribución-CompartirIgual 4.0 Internacional (CC BY-SA 4.0)" + short_description_en: "This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License." + short_description_es: "Esta obra está bajo una Licencia Creative Commons Atribución-CompartirIgual 4.0 Internacional." url_en: 'https://creativecommons.org/licenses/by-sa/4.0/' url_es: 'https://creativecommons.org/licenses/by-sa/4.0/deed.es' description_en: "This license is the same as the CC-BY 4.0 but it adds diff --git a/db/seeds/privacy_policies.yml b/db/seeds/privacy_policies.yml new file mode 100644 index 00000000..98ce8379 --- /dev/null +++ b/db/seeds/privacy_policies.yml @@ -0,0 +1,113 @@ +--- +- title_en: "Privacy Policy" + title_es: "Políticas de privacidad" + description_en: "With what care does this site handles personal data of its users and visitors?" + description_es: "¿Cuáles son los cuidados de este sitio con respecto a sus usuaries y visitantes?" + content_en: | + > We use "them" as neutral pronoun to refer to people regardless of + > gender identity. + + This document details Sutty's privacy policy, including web site, + platform, other infrastructure (support channels, etc.) and web sites + generated by users. + + ## This is too long! + + * Sutty doesn't collect any kind of personal data. + + * Sutty may only collect statistical data that doesn't identify + individuals. + + ## Analytic data + + Sutty may only collect data for analytics (number of visits, duration, + etc.), not associated to personal data. + + Analytical data collected for every web site can only be used internally + by Sutty. Sutty doesn't share any data privately with any third + parties. Selected analytical data could be used publicly. + + Sutty doesn't recommend personal data collection in any way, but it + doesn't monitor if its users use third party services with their own + privacy policies. We recommend users and visitors to inform themselves + before using third parties analytics services. + + ## No personal data collection + + Sutty doesn't collect IP addresses from users nor visitors in any way. + + Sutty doesn't ask for personal data for registering user accounts in its + platform. + + Sutty only uses session "cookies" to identify users during their use of + the platform. It doesn't use "cookies" to identify visitors of web + sites hosted by Sutty. + + The only exception where Sutty could collect personal data is during + service payment. Digital safety measures will be taken to keep this + information and to discard it if possible after needed. + + Users will be notified when their personal data is removed. + + If users decide to host their web sites with third parties, they must + inform themselves about the corresponding privacy policies. Sutty only + recommends third parties with privacy policies compatible with these. + content_es: | + > Utilizamos la e como pronombre neutro para referirnos a personas + > independientemente de su identidad de género, por ejemplo “usuarie”. + + Este documento detalla la política de privacidad de Sutty, incluyendo + sitio web, plataforma de edición, infraestructura relacionada (salas de + chat, etc.) y sitios creados por sus usuaries a través de la plataforma, + en adelante "Sutty". + + ## ¡Esto es demasiado largo! + + Un resumen: + + * Sutty no recolecta datos personales de ningún tipo + + * Sutty solo recolectaría datos analíticos que no identifican a + personas + + ## Datos analíticos + + La única recolección de datos realizada por Sutty es con fines + analíticos (cantidad de visitas, duración, etc.), no asociados a datos + personales. + + Los datos analíticos recolectados por cada sitio podrán ser utilizados + internamente por Sutty. Sutty no comparte datos analíticos con + terceros en forma privada. Datos analíticos seleccionados podrán ser + utilizados públicamente. + + Sutty no recomienda la recolección de datos personales de ninguna forma, + pero no monitorea que les usuaries utilicen servicios de terceros con + sus propias políticas de privacidad. Recomendamos a les usuaries y + visitantes informarse antes de utilizar servicios de estadísticas de + terceros. + + ## No registro de datos personales + + Sutty no registra direcciones IP de usuaries ni de visitantes de ninguna + forma. + + Sutty no solicita datos personales para el registro de cuentas de + usuarie en su plataforma. + + Sutty solo utiliza “cookies” de sesión para identificar usuaries + mientras utilicen la plataforma. No se utilizan “cookies” para + identificar visitantes a los sitios alojados por Sutty. + + El único caso en el que Sutty podría solicitar datos personales es + durante el pago de servicios. Se tomarán medidas de seguridad digital + para salvaguardar esta información y descartar lo que sea posible una + vez que ya no sea necesaria. + + Se notificará a les usuaries cuando su información personal sea + eliminada. + + Si les usuaries deciden alojar sus sitios con terceros, deberán + informarse de las políticas de privacidad correspondientes. Sutty + recomienda servicios de terceros con políticas de privacidad coherentes + con estas. diff --git a/db/structure.sql b/db/structure.sql new file mode 100644 index 00000000..e0d8f710 --- /dev/null +++ b/db/structure.sql @@ -0,0 +1,2323 @@ +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- + +-- *not* creating schema, since initdb creates it + + +-- +-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; + + +-- +-- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams'; + + +-- +-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; + + +-- +-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions'; + + +-- +-- Name: que_validate_tags(jsonb); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.que_validate_tags(tags_array jsonb) RETURNS boolean + LANGUAGE sql + AS $$ + SELECT bool_and( + jsonb_typeof(value) = 'string' + AND + char_length(value::text) <= 100 + ) + FROM jsonb_array_elements(tags_array) +$$; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: que_jobs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.que_jobs ( + priority smallint DEFAULT 100 NOT NULL, + run_at timestamp with time zone DEFAULT now() NOT NULL, + id bigint NOT NULL, + job_class text NOT NULL, + error_count integer DEFAULT 0 NOT NULL, + last_error_message text, + queue text DEFAULT 'default'::text NOT NULL, + last_error_backtrace text, + finished_at timestamp with time zone, + expired_at timestamp with time zone, + args jsonb DEFAULT '[]'::jsonb NOT NULL, + data jsonb DEFAULT '{}'::jsonb NOT NULL, + job_schema_version integer NOT NULL, + kwargs jsonb DEFAULT '{}'::jsonb NOT NULL, + CONSTRAINT error_length CHECK (((char_length(last_error_message) <= 500) AND (char_length(last_error_backtrace) <= 10000))), + CONSTRAINT job_class_length CHECK ((char_length( +CASE job_class + WHEN 'ActiveJob::QueueAdapters::QueAdapter::JobWrapper'::text THEN ((args -> 0) ->> 'job_class'::text) + ELSE job_class +END) <= 200)), + CONSTRAINT queue_length CHECK ((char_length(queue) <= 100)), + CONSTRAINT valid_args CHECK ((jsonb_typeof(args) = 'array'::text)), + CONSTRAINT valid_data CHECK (((jsonb_typeof(data) = 'object'::text) AND ((NOT (data ? 'tags'::text)) OR ((jsonb_typeof((data -> 'tags'::text)) = 'array'::text) AND (jsonb_array_length((data -> 'tags'::text)) <= 5) AND public.que_validate_tags((data -> 'tags'::text)))))) +) +WITH (fillfactor='90'); + + +-- +-- Name: TABLE que_jobs; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.que_jobs IS '7'; + + +-- +-- Name: access_logs_before_insert_row_tr(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.access_logs_before_insert_row_tr() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + new.created_at := to_timestamp(new.msec); + RETURN NEW; +END; +$$; + + +-- +-- Name: indexed_posts_before_insert_update_row_tr(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.indexed_posts_before_insert_update_row_tr() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + new.indexed_content := to_tsvector(('pg_catalog.' || new.dictionary)::regconfig, coalesce(new.title, '') || ' + ' || coalesce(new.content,'')); + RETURN NEW; +END; +$$; + + +-- +-- Name: que_determine_job_state(public.que_jobs); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.que_determine_job_state(job public.que_jobs) RETURNS text + LANGUAGE sql + AS $$ + SELECT + CASE + WHEN job.expired_at IS NOT NULL THEN 'expired' + WHEN job.finished_at IS NOT NULL THEN 'finished' + WHEN job.error_count > 0 THEN 'errored' + WHEN job.run_at > CURRENT_TIMESTAMP THEN 'scheduled' + ELSE 'ready' + END +$$; + + +-- +-- Name: que_job_notify(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.que_job_notify() RETURNS trigger + LANGUAGE plpgsql + AS $$ + DECLARE + locker_pid integer; + sort_key json; + BEGIN + -- Don't do anything if the job is scheduled for a future time. + IF NEW.run_at IS NOT NULL AND NEW.run_at > now() THEN + RETURN null; + END IF; + + -- Pick a locker to notify of the job's insertion, weighted by their number + -- of workers. Should bounce pseudorandomly between lockers on each + -- invocation, hence the md5-ordering, but still touch each one equally, + -- hence the modulo using the job_id. + SELECT pid + INTO locker_pid + FROM ( + SELECT *, last_value(row_number) OVER () + 1 AS count + FROM ( + SELECT *, row_number() OVER () - 1 AS row_number + FROM ( + SELECT * + FROM public.que_lockers ql, generate_series(1, ql.worker_count) AS id + WHERE + listening AND + queues @> ARRAY[NEW.queue] AND + ql.job_schema_version = NEW.job_schema_version + ORDER BY md5(pid::text || id::text) + ) t1 + ) t2 + ) t3 + WHERE NEW.id % count = row_number; + + IF locker_pid IS NOT NULL THEN + -- There's a size limit to what can be broadcast via LISTEN/NOTIFY, so + -- rather than throw errors when someone enqueues a big job, just + -- broadcast the most pertinent information, and let the locker query for + -- the record after it's taken the lock. The worker will have to hit the + -- DB in order to make sure the job is still visible anyway. + SELECT row_to_json(t) + INTO sort_key + FROM ( + SELECT + 'job_available' AS message_type, + NEW.queue AS queue, + NEW.priority AS priority, + NEW.id AS id, + -- Make sure we output timestamps as UTC ISO 8601 + to_char(NEW.run_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS run_at + ) t; + + PERFORM pg_notify('que_listener_' || locker_pid::text, sort_key::text); + END IF; + + RETURN null; + END +$$; + + +-- +-- Name: que_state_notify(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.que_state_notify() RETURNS trigger + LANGUAGE plpgsql + AS $$ + DECLARE + row record; + message json; + previous_state text; + current_state text; + BEGIN + IF TG_OP = 'INSERT' THEN + previous_state := 'nonexistent'; + current_state := public.que_determine_job_state(NEW); + row := NEW; + ELSIF TG_OP = 'DELETE' THEN + previous_state := public.que_determine_job_state(OLD); + current_state := 'nonexistent'; + row := OLD; + ELSIF TG_OP = 'UPDATE' THEN + previous_state := public.que_determine_job_state(OLD); + current_state := public.que_determine_job_state(NEW); + + -- If the state didn't change, short-circuit. + IF previous_state = current_state THEN + RETURN null; + END IF; + + row := NEW; + ELSE + RAISE EXCEPTION 'Unrecognized TG_OP: %', TG_OP; + END IF; + + SELECT row_to_json(t) + INTO message + FROM ( + SELECT + 'job_change' AS message_type, + row.id AS id, + row.queue AS queue, + + coalesce(row.data->'tags', '[]'::jsonb) AS tags, + + to_char(row.run_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS run_at, + to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS time, + + CASE row.job_class + WHEN 'ActiveJob::QueueAdapters::QueAdapter::JobWrapper' THEN + coalesce( + row.args->0->>'job_class', + 'ActiveJob::QueueAdapters::QueAdapter::JobWrapper' + ) + ELSE + row.job_class + END AS job_class, + + previous_state AS previous_state, + current_state AS current_state + ) t; + + PERFORM pg_notify('que_state', message::text); + + RETURN null; + END +$$; + + +-- +-- Name: access_logs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.access_logs ( + id uuid DEFAULT public.gen_random_uuid() NOT NULL, + host character varying, + msec double precision, + server_protocol character varying, + request_method character varying, + request_completion character varying, + uri character varying, + query_string character varying, + status integer, + sent_http_content_type character varying, + sent_http_content_encoding character varying, + sent_http_etag character varying, + sent_http_last_modified character varying, + http_accept character varying, + http_accept_encoding character varying, + http_accept_language character varying, + http_pragma character varying, + http_cache_control character varying, + http_if_none_match character varying, + http_dnt character varying, + http_user_agent character varying, + http_origin character varying, + request_time double precision, + bytes_sent integer, + body_bytes_sent integer, + request_length integer, + http_connection character varying, + pipe character varying, + connection_requests integer, + geoip2_data_country_name character varying, + geoip2_data_city_name character varying, + ssl_server_name character varying, + ssl_protocol character varying, + ssl_early_data character varying, + ssl_session_reused character varying, + ssl_curves character varying, + ssl_ciphers character varying, + ssl_cipher character varying, + sent_http_x_xss_protection character varying, + sent_http_x_frame_options character varying, + sent_http_x_content_type_options character varying, + sent_http_strict_transport_security character varying, + nginx_version character varying, + pid integer, + remote_user character varying, + crawler boolean DEFAULT false, + http_referer character varying, + created_at timestamp(6) without time zone, + request_uri character varying DEFAULT ''::character varying, + datacenter_co2 numeric, + network_co2 numeric, + consumer_device_co2 numeric, + production_co2 numeric, + total_co2 numeric, + node character varying +); + + +-- +-- Name: action_text_rich_texts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.action_text_rich_texts ( + id bigint NOT NULL, + name character varying NOT NULL, + body text, + record_type character varying NOT NULL, + record_id integer NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: action_text_rich_texts_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.action_text_rich_texts_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: action_text_rich_texts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.action_text_rich_texts_id_seq OWNED BY public.action_text_rich_texts.id; + + +-- +-- Name: active_storage_attachments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.active_storage_attachments ( + id bigint NOT NULL, + name character varying NOT NULL, + record_type character varying NOT NULL, + record_id integer NOT NULL, + blob_id integer NOT NULL, + created_at timestamp without time zone NOT NULL +); + + +-- +-- Name: active_storage_attachments_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.active_storage_attachments_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: active_storage_attachments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.active_storage_attachments_id_seq OWNED BY public.active_storage_attachments.id; + + +-- +-- Name: active_storage_blobs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.active_storage_blobs ( + id bigint NOT NULL, + key character varying NOT NULL, + filename character varying NOT NULL, + content_type character varying, + metadata text, + byte_size bigint NOT NULL, + checksum character varying NOT NULL, + created_at timestamp without time zone NOT NULL, + service_name character varying NOT NULL +); + + +-- +-- Name: active_storage_blobs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.active_storage_blobs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: active_storage_blobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.active_storage_blobs_id_seq OWNED BY public.active_storage_blobs.id; + + +-- +-- Name: active_storage_variant_records; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.active_storage_variant_records ( + id bigint NOT NULL, + blob_id bigint NOT NULL, + variation_digest character varying NOT NULL +); + + +-- +-- Name: active_storage_variant_records_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.active_storage_variant_records_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: active_storage_variant_records_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.active_storage_variant_records_id_seq OWNED BY public.active_storage_variant_records.id; + + +-- +-- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.ar_internal_metadata ( + key character varying NOT NULL, + value character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: blazer_audits; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.blazer_audits ( + id bigint NOT NULL, + user_id bigint, + query_id bigint, + statement text, + data_source character varying, + created_at timestamp without time zone +); + + +-- +-- Name: blazer_audits_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.blazer_audits_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: blazer_audits_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.blazer_audits_id_seq OWNED BY public.blazer_audits.id; + + +-- +-- Name: blazer_checks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.blazer_checks ( + id bigint NOT NULL, + creator_id bigint, + query_id bigint, + state character varying, + schedule character varying, + emails text, + slack_channels text, + check_type character varying, + message text, + last_run_at timestamp without time zone, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: blazer_checks_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.blazer_checks_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: blazer_checks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.blazer_checks_id_seq OWNED BY public.blazer_checks.id; + + +-- +-- Name: blazer_dashboard_queries; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.blazer_dashboard_queries ( + id bigint NOT NULL, + dashboard_id bigint, + query_id bigint, + "position" integer, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: blazer_dashboard_queries_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.blazer_dashboard_queries_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: blazer_dashboard_queries_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.blazer_dashboard_queries_id_seq OWNED BY public.blazer_dashboard_queries.id; + + +-- +-- Name: blazer_dashboards; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.blazer_dashboards ( + id bigint NOT NULL, + creator_id bigint, + name text, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: blazer_dashboards_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.blazer_dashboards_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: blazer_dashboards_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.blazer_dashboards_id_seq OWNED BY public.blazer_dashboards.id; + + +-- +-- Name: blazer_queries; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.blazer_queries ( + id bigint NOT NULL, + creator_id bigint, + name character varying, + description text, + statement text, + data_source character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: blazer_queries_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.blazer_queries_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: blazer_queries_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.blazer_queries_id_seq OWNED BY public.blazer_queries.id; + + +-- +-- Name: build_stats; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.build_stats ( + id bigint NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + deploy_id integer, + bytes bigint, + seconds double precision, + action character varying NOT NULL, + log text, + status boolean DEFAULT false +); + + +-- +-- Name: build_stats_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.build_stats_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: build_stats_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.build_stats_id_seq OWNED BY public.build_stats.id; + + +-- +-- Name: codes_of_conduct; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.codes_of_conduct ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + title character varying, + description text, + content text +); + + +-- +-- Name: codes_of_conduct_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.codes_of_conduct_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: codes_of_conduct_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.codes_of_conduct_id_seq OWNED BY public.codes_of_conduct.id; + + +-- +-- Name: csp_reports; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.csp_reports ( + id uuid DEFAULT public.gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + disposition character varying, + referrer character varying, + blocked_uri character varying, + document_uri character varying, + effective_directive character varying, + original_policy character varying, + script_sample character varying, + status_code character varying, + violated_directive character varying, + column_number integer, + line_number integer, + source_file character varying +); + + +-- +-- Name: deploys; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.deploys ( + id bigint NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + site_id integer, + type character varying, + "values" text +); + + +-- +-- Name: deploys_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.deploys_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: deploys_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.deploys_id_seq OWNED BY public.deploys.id; + + +-- +-- Name: designs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.designs ( + id bigint NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + name character varying, + description text, + gem character varying, + url character varying, + license character varying, + disabled boolean DEFAULT false, + credits text, + designer_url character varying, + priority integer +); + + +-- +-- Name: designs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.designs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: designs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.designs_id_seq OWNED BY public.designs.id; + + +-- +-- Name: distributed_press_publishers; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.distributed_press_publishers ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + instance character varying, + token_ciphertext text NOT NULL, + expires_at timestamp without time zone +); + + +-- +-- Name: distributed_press_publishers_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.distributed_press_publishers_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: distributed_press_publishers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.distributed_press_publishers_id_seq OWNED BY public.distributed_press_publishers.id; + + +-- +-- Name: indexed_posts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.indexed_posts ( + id uuid DEFAULT public.gen_random_uuid() NOT NULL, + site_id bigint, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + locale character varying DEFAULT 'simple'::character varying, + layout character varying NOT NULL, + path character varying NOT NULL, + title character varying DEFAULT ''::character varying, + front_matter jsonb DEFAULT '"{}"'::jsonb, + content character varying DEFAULT ''::character varying, + indexed_content tsvector, + "order" integer DEFAULT 0, + dictionary character varying, + post_id uuid +); + + +-- +-- Name: licencias; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.licencias ( + id bigint NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + name character varying, + description text, + deed text, + url character varying, + icons character varying, + short_description character varying +); + + +-- +-- Name: licencias_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.licencias_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: licencias_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.licencias_id_seq OWNED BY public.licencias.id; + + +-- +-- Name: log_entries; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.log_entries ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + site_id bigint, + text text, + sent boolean DEFAULT false +); + + +-- +-- Name: log_entries_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.log_entries_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: log_entries_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.log_entries_id_seq OWNED BY public.log_entries.id; + + +-- +-- Name: maintenances; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.maintenances ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + message text, + estimated_from timestamp without time zone, + estimated_to timestamp without time zone, + are_we_back boolean DEFAULT false +); + + +-- +-- Name: maintenances_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.maintenances_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: maintenances_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.maintenances_id_seq OWNED BY public.maintenances.id; + + +-- +-- Name: mobility_string_translations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.mobility_string_translations ( + id bigint NOT NULL, + locale character varying NOT NULL, + key character varying NOT NULL, + value character varying, + translatable_type character varying, + translatable_id integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: mobility_string_translations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.mobility_string_translations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: mobility_string_translations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.mobility_string_translations_id_seq OWNED BY public.mobility_string_translations.id; + + +-- +-- Name: mobility_text_translations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.mobility_text_translations ( + id bigint NOT NULL, + locale character varying NOT NULL, + key character varying NOT NULL, + value text, + translatable_type character varying, + translatable_id integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: mobility_text_translations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.mobility_text_translations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: mobility_text_translations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.mobility_text_translations_id_seq OWNED BY public.mobility_text_translations.id; + + +-- +-- Name: privacy_policies; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.privacy_policies ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + title character varying, + description text, + content text +); + + +-- +-- Name: privacy_policies_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.privacy_policies_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: privacy_policies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.privacy_policies_id_seq OWNED BY public.privacy_policies.id; + + +-- +-- Name: que_jobs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.que_jobs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: que_jobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.que_jobs_id_seq OWNED BY public.que_jobs.id; + + +-- +-- Name: que_lockers; Type: TABLE; Schema: public; Owner: - +-- + +CREATE UNLOGGED TABLE public.que_lockers ( + pid integer NOT NULL, + worker_count integer NOT NULL, + worker_priorities integer[] NOT NULL, + ruby_pid integer NOT NULL, + ruby_hostname text NOT NULL, + queues text[] NOT NULL, + listening boolean NOT NULL, + job_schema_version integer DEFAULT 1, + CONSTRAINT valid_queues CHECK (((array_ndims(queues) = 1) AND (array_length(queues, 1) IS NOT NULL))), + CONSTRAINT valid_worker_priorities CHECK (((array_ndims(worker_priorities) = 1) AND (array_length(worker_priorities, 1) IS NOT NULL))) +); + + +-- +-- Name: que_values; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.que_values ( + key text NOT NULL, + value jsonb DEFAULT '{}'::jsonb NOT NULL, + CONSTRAINT valid_value CHECK ((jsonb_typeof(value) = 'object'::text)) +) +WITH (fillfactor='90'); + + +-- +-- Name: roles; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.roles ( + id bigint NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + site_id integer, + usuarie_id integer, + rol character varying, + temporal boolean +); + + +-- +-- Name: roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.roles_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.roles_id_seq OWNED BY public.roles.id; + + +-- +-- Name: rollups; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.rollups ( + id bigint NOT NULL, + name character varying NOT NULL, + "interval" character varying NOT NULL, + "time" timestamp without time zone NOT NULL, + dimensions jsonb DEFAULT '{}'::jsonb NOT NULL, + value double precision +); + + +-- +-- Name: rollups_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.rollups_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: rollups_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.rollups_id_seq OWNED BY public.rollups.id; + + +-- +-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.schema_migrations ( + version character varying NOT NULL +); + + +-- +-- Name: sites; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sites ( + id bigint NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + name character varying, + design_id integer, + licencia_id integer, + status character varying DEFAULT 'waiting'::character varying, + description text, + title character varying, + colaboracion_anonima boolean DEFAULT false, + contact boolean DEFAULT false, + private_key_ciphertext character varying, + acepta_invitades boolean DEFAULT false, + tienda_api_key_ciphertext character varying DEFAULT ''::character varying, + tienda_url character varying DEFAULT ''::character varying, + api_key_ciphertext character varying, + slugify_mode character varying DEFAULT 'default'::character varying, + pagination boolean DEFAULT false +); + + +-- +-- Name: sites_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.sites_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: sites_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.sites_id_seq OWNED BY public.sites.id; + + +-- +-- Name: stats; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.stats ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + site_id bigint, + name character varying NOT NULL +); + + +-- +-- Name: stats_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.stats_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: stats_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.stats_id_seq OWNED BY public.stats.id; + + +-- +-- Name: usuaries; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.usuaries ( + id bigint NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + email character varying DEFAULT ''::character varying NOT NULL, + encrypted_password character varying DEFAULT ''::character varying NOT NULL, + reset_password_token character varying, + reset_password_sent_at timestamp without time zone, + remember_created_at timestamp without time zone, + confirmation_token character varying, + confirmed_at timestamp without time zone, + confirmation_sent_at timestamp without time zone, + unconfirmed_email character varying, + failed_attempts integer DEFAULT 0 NOT NULL, + unlock_token character varying, + locked_at timestamp without time zone, + invitation_token character varying, + invitation_created_at timestamp without time zone, + invitation_sent_at timestamp without time zone, + invitation_accepted_at timestamp without time zone, + invitation_limit integer, + invited_by_type character varying, + invited_by_id integer, + invitations_count integer DEFAULT 0, + lang character varying DEFAULT 'es'::character varying, + privacy_policy_accepted_at timestamp without time zone, + terms_of_service_accepted_at timestamp without time zone, + code_of_conduct_accepted_at timestamp without time zone, + available_for_feedback_accepted_at timestamp without time zone +); + + +-- +-- Name: usuaries_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.usuaries_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: usuaries_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.usuaries_id_seq OWNED BY public.usuaries.id; + + +-- +-- Name: action_text_rich_texts id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.action_text_rich_texts ALTER COLUMN id SET DEFAULT nextval('public.action_text_rich_texts_id_seq'::regclass); + + +-- +-- Name: active_storage_attachments id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.active_storage_attachments ALTER COLUMN id SET DEFAULT nextval('public.active_storage_attachments_id_seq'::regclass); + + +-- +-- Name: active_storage_blobs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.active_storage_blobs ALTER COLUMN id SET DEFAULT nextval('public.active_storage_blobs_id_seq'::regclass); + + +-- +-- Name: active_storage_variant_records id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.active_storage_variant_records ALTER COLUMN id SET DEFAULT nextval('public.active_storage_variant_records_id_seq'::regclass); + + +-- +-- Name: blazer_audits id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_audits ALTER COLUMN id SET DEFAULT nextval('public.blazer_audits_id_seq'::regclass); + + +-- +-- Name: blazer_checks id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_checks ALTER COLUMN id SET DEFAULT nextval('public.blazer_checks_id_seq'::regclass); + + +-- +-- Name: blazer_dashboard_queries id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_dashboard_queries ALTER COLUMN id SET DEFAULT nextval('public.blazer_dashboard_queries_id_seq'::regclass); + + +-- +-- Name: blazer_dashboards id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_dashboards ALTER COLUMN id SET DEFAULT nextval('public.blazer_dashboards_id_seq'::regclass); + + +-- +-- Name: blazer_queries id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_queries ALTER COLUMN id SET DEFAULT nextval('public.blazer_queries_id_seq'::regclass); + + +-- +-- Name: build_stats id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.build_stats ALTER COLUMN id SET DEFAULT nextval('public.build_stats_id_seq'::regclass); + + +-- +-- Name: codes_of_conduct id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.codes_of_conduct ALTER COLUMN id SET DEFAULT nextval('public.codes_of_conduct_id_seq'::regclass); + + +-- +-- Name: deploys id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.deploys ALTER COLUMN id SET DEFAULT nextval('public.deploys_id_seq'::regclass); + + +-- +-- Name: designs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.designs ALTER COLUMN id SET DEFAULT nextval('public.designs_id_seq'::regclass); + + +-- +-- Name: distributed_press_publishers id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.distributed_press_publishers ALTER COLUMN id SET DEFAULT nextval('public.distributed_press_publishers_id_seq'::regclass); + + +-- +-- Name: licencias id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.licencias ALTER COLUMN id SET DEFAULT nextval('public.licencias_id_seq'::regclass); + + +-- +-- Name: log_entries id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.log_entries ALTER COLUMN id SET DEFAULT nextval('public.log_entries_id_seq'::regclass); + + +-- +-- Name: maintenances id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.maintenances ALTER COLUMN id SET DEFAULT nextval('public.maintenances_id_seq'::regclass); + + +-- +-- Name: mobility_string_translations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mobility_string_translations ALTER COLUMN id SET DEFAULT nextval('public.mobility_string_translations_id_seq'::regclass); + + +-- +-- Name: mobility_text_translations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mobility_text_translations ALTER COLUMN id SET DEFAULT nextval('public.mobility_text_translations_id_seq'::regclass); + + +-- +-- Name: privacy_policies id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.privacy_policies ALTER COLUMN id SET DEFAULT nextval('public.privacy_policies_id_seq'::regclass); + + +-- +-- Name: que_jobs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.que_jobs ALTER COLUMN id SET DEFAULT nextval('public.que_jobs_id_seq'::regclass); + + +-- +-- Name: roles id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.roles ALTER COLUMN id SET DEFAULT nextval('public.roles_id_seq'::regclass); + + +-- +-- Name: rollups id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.rollups ALTER COLUMN id SET DEFAULT nextval('public.rollups_id_seq'::regclass); + + +-- +-- Name: sites id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sites ALTER COLUMN id SET DEFAULT nextval('public.sites_id_seq'::regclass); + + +-- +-- Name: stats id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.stats ALTER COLUMN id SET DEFAULT nextval('public.stats_id_seq'::regclass); + + +-- +-- Name: usuaries id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.usuaries ALTER COLUMN id SET DEFAULT nextval('public.usuaries_id_seq'::regclass); + + +-- +-- Name: access_logs access_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.access_logs + ADD CONSTRAINT access_logs_pkey PRIMARY KEY (id); + + +-- +-- Name: action_text_rich_texts action_text_rich_texts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.action_text_rich_texts + ADD CONSTRAINT action_text_rich_texts_pkey PRIMARY KEY (id); + + +-- +-- Name: active_storage_attachments active_storage_attachments_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.active_storage_attachments + ADD CONSTRAINT active_storage_attachments_pkey PRIMARY KEY (id); + + +-- +-- Name: active_storage_blobs active_storage_blobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.active_storage_blobs + ADD CONSTRAINT active_storage_blobs_pkey PRIMARY KEY (id); + + +-- +-- Name: active_storage_variant_records active_storage_variant_records_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.active_storage_variant_records + ADD CONSTRAINT active_storage_variant_records_pkey PRIMARY KEY (id); + + +-- +-- Name: blazer_audits blazer_audits_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_audits + ADD CONSTRAINT blazer_audits_pkey PRIMARY KEY (id); + + +-- +-- Name: blazer_checks blazer_checks_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_checks + ADD CONSTRAINT blazer_checks_pkey PRIMARY KEY (id); + + +-- +-- Name: blazer_dashboard_queries blazer_dashboard_queries_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_dashboard_queries + ADD CONSTRAINT blazer_dashboard_queries_pkey PRIMARY KEY (id); + + +-- +-- Name: blazer_dashboards blazer_dashboards_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_dashboards + ADD CONSTRAINT blazer_dashboards_pkey PRIMARY KEY (id); + + +-- +-- Name: blazer_queries blazer_queries_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blazer_queries + ADD CONSTRAINT blazer_queries_pkey PRIMARY KEY (id); + + +-- +-- Name: build_stats build_stats_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.build_stats + ADD CONSTRAINT build_stats_pkey PRIMARY KEY (id); + + +-- +-- Name: codes_of_conduct codes_of_conduct_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.codes_of_conduct + ADD CONSTRAINT codes_of_conduct_pkey PRIMARY KEY (id); + + +-- +-- Name: csp_reports csp_reports_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.csp_reports + ADD CONSTRAINT csp_reports_pkey PRIMARY KEY (id); + + +-- +-- Name: deploys deploys_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.deploys + ADD CONSTRAINT deploys_pkey PRIMARY KEY (id); + + +-- +-- Name: designs designs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.designs + ADD CONSTRAINT designs_pkey PRIMARY KEY (id); + + +-- +-- Name: distributed_press_publishers distributed_press_publishers_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.distributed_press_publishers + ADD CONSTRAINT distributed_press_publishers_pkey PRIMARY KEY (id); + + +-- +-- Name: indexed_posts indexed_posts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.indexed_posts + ADD CONSTRAINT indexed_posts_pkey PRIMARY KEY (id); + + +-- +-- Name: licencias licencias_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.licencias + ADD CONSTRAINT licencias_pkey PRIMARY KEY (id); + + +-- +-- Name: log_entries log_entries_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.log_entries + ADD CONSTRAINT log_entries_pkey PRIMARY KEY (id); + + +-- +-- Name: maintenances maintenances_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.maintenances + ADD CONSTRAINT maintenances_pkey PRIMARY KEY (id); + + +-- +-- Name: mobility_string_translations mobility_string_translations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mobility_string_translations + ADD CONSTRAINT mobility_string_translations_pkey PRIMARY KEY (id); + + +-- +-- Name: mobility_text_translations mobility_text_translations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mobility_text_translations + ADD CONSTRAINT mobility_text_translations_pkey PRIMARY KEY (id); + + +-- +-- Name: privacy_policies privacy_policies_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.privacy_policies + ADD CONSTRAINT privacy_policies_pkey PRIMARY KEY (id); + + +-- +-- Name: que_jobs que_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.que_jobs + ADD CONSTRAINT que_jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: que_lockers que_lockers_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.que_lockers + ADD CONSTRAINT que_lockers_pkey PRIMARY KEY (pid); + + +-- +-- Name: que_values que_values_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.que_values + ADD CONSTRAINT que_values_pkey PRIMARY KEY (key); + + +-- +-- Name: roles roles_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.roles + ADD CONSTRAINT roles_pkey PRIMARY KEY (id); + + +-- +-- Name: rollups rollups_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.rollups + ADD CONSTRAINT rollups_pkey PRIMARY KEY (id); + + +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); + + +-- +-- Name: sites sites_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sites + ADD CONSTRAINT sites_pkey PRIMARY KEY (id); + + +-- +-- Name: stats stats_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.stats + ADD CONSTRAINT stats_pkey PRIMARY KEY (id); + + +-- +-- Name: usuaries usuaries_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.usuaries + ADD CONSTRAINT usuaries_pkey PRIMARY KEY (id); + + +-- +-- Name: index_access_logs_on_geoip2_data_city_name; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_access_logs_on_geoip2_data_city_name ON public.access_logs USING btree (geoip2_data_city_name); + + +-- +-- Name: index_access_logs_on_geoip2_data_country_name; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_access_logs_on_geoip2_data_country_name ON public.access_logs USING btree (geoip2_data_country_name); + + +-- +-- Name: index_access_logs_on_host; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_access_logs_on_host ON public.access_logs USING btree (host); + + +-- +-- Name: index_access_logs_on_http_origin; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_access_logs_on_http_origin ON public.access_logs USING btree (http_origin); + + +-- +-- Name: index_access_logs_on_http_user_agent; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_access_logs_on_http_user_agent ON public.access_logs USING btree (http_user_agent); + + +-- +-- Name: index_access_logs_on_status; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_access_logs_on_status ON public.access_logs USING btree (status); + + +-- +-- Name: index_access_logs_on_uri; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_access_logs_on_uri ON public.access_logs USING btree (uri); + + +-- +-- Name: index_action_text_rich_texts_uniqueness; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_action_text_rich_texts_uniqueness ON public.action_text_rich_texts USING btree (record_type, record_id, name); + + +-- +-- Name: index_active_storage_attachments_on_blob_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_active_storage_attachments_on_blob_id ON public.active_storage_attachments USING btree (blob_id); + + +-- +-- Name: index_active_storage_attachments_uniqueness; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_active_storage_attachments_uniqueness ON public.active_storage_attachments USING btree (record_type, record_id, name, blob_id); + + +-- +-- Name: index_active_storage_blobs_on_key_and_service_name; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_active_storage_blobs_on_key_and_service_name ON public.active_storage_blobs USING btree (key, service_name); + + +-- +-- Name: index_active_storage_variant_records_uniqueness; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_active_storage_variant_records_uniqueness ON public.active_storage_variant_records USING btree (blob_id, variation_digest); + + +-- +-- Name: index_blazer_audits_on_query_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_blazer_audits_on_query_id ON public.blazer_audits USING btree (query_id); + + +-- +-- Name: index_blazer_audits_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_blazer_audits_on_user_id ON public.blazer_audits USING btree (user_id); + + +-- +-- Name: index_blazer_checks_on_creator_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_blazer_checks_on_creator_id ON public.blazer_checks USING btree (creator_id); + + +-- +-- Name: index_blazer_checks_on_query_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_blazer_checks_on_query_id ON public.blazer_checks USING btree (query_id); + + +-- +-- Name: index_blazer_dashboard_queries_on_dashboard_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_blazer_dashboard_queries_on_dashboard_id ON public.blazer_dashboard_queries USING btree (dashboard_id); + + +-- +-- Name: index_blazer_dashboard_queries_on_query_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_blazer_dashboard_queries_on_query_id ON public.blazer_dashboard_queries USING btree (query_id); + + +-- +-- Name: index_blazer_dashboards_on_creator_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_blazer_dashboards_on_creator_id ON public.blazer_dashboards USING btree (creator_id); + + +-- +-- Name: index_blazer_queries_on_creator_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_blazer_queries_on_creator_id ON public.blazer_queries USING btree (creator_id); + + +-- +-- Name: index_build_stats_on_deploy_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_build_stats_on_deploy_id ON public.build_stats USING btree (deploy_id); + + +-- +-- Name: index_deploys_on_site_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_deploys_on_site_id ON public.deploys USING btree (site_id); + + +-- +-- Name: index_deploys_on_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_deploys_on_type ON public.deploys USING btree (type); + + +-- +-- Name: index_designs_on_gem; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_designs_on_gem ON public.designs USING btree (gem); + + +-- +-- Name: index_designs_on_name; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_designs_on_name ON public.designs USING btree (name); + + +-- +-- Name: index_indexed_posts_on_front_matter; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_indexed_posts_on_front_matter ON public.indexed_posts USING gin (front_matter); + + +-- +-- Name: index_indexed_posts_on_indexed_content; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_indexed_posts_on_indexed_content ON public.indexed_posts USING gin (indexed_content); + + +-- +-- Name: index_indexed_posts_on_layout; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_indexed_posts_on_layout ON public.indexed_posts USING btree (layout); + + +-- +-- Name: index_indexed_posts_on_locale; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_indexed_posts_on_locale ON public.indexed_posts USING btree (locale); + + +-- +-- Name: index_indexed_posts_on_site_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_indexed_posts_on_site_id ON public.indexed_posts USING btree (site_id); + + +-- +-- Name: index_licencias_on_name; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_licencias_on_name ON public.licencias USING btree (name); + + +-- +-- Name: index_log_entries_on_site_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_log_entries_on_site_id ON public.log_entries USING btree (site_id); + + +-- +-- Name: index_mobility_string_translations_on_keys; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_mobility_string_translations_on_keys ON public.mobility_string_translations USING btree (translatable_id, translatable_type, locale, key); + + +-- +-- Name: index_mobility_string_translations_on_query_keys; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_mobility_string_translations_on_query_keys ON public.mobility_string_translations USING btree (translatable_type, key, value, locale); + + +-- +-- Name: index_mobility_string_translations_on_translatable_attribute; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_mobility_string_translations_on_translatable_attribute ON public.mobility_string_translations USING btree (translatable_id, translatable_type, key); + + +-- +-- Name: index_mobility_text_translations_on_keys; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_mobility_text_translations_on_keys ON public.mobility_text_translations USING btree (translatable_id, translatable_type, locale, key); + + +-- +-- Name: index_mobility_text_translations_on_translatable_attribute; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_mobility_text_translations_on_translatable_attribute ON public.mobility_text_translations USING btree (translatable_id, translatable_type, key); + + +-- +-- Name: index_roles_on_site_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_roles_on_site_id ON public.roles USING btree (site_id); + + +-- +-- Name: index_roles_on_site_id_and_usuarie_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_roles_on_site_id_and_usuarie_id ON public.roles USING btree (site_id, usuarie_id); + + +-- +-- Name: index_roles_on_usuarie_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_roles_on_usuarie_id ON public.roles USING btree (usuarie_id); + + +-- +-- Name: index_rollups_on_name_and_interval_and_time_and_dimensions; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_rollups_on_name_and_interval_and_time_and_dimensions ON public.rollups USING btree (name, "interval", "time", dimensions); + + +-- +-- Name: index_sites_on_design_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_sites_on_design_id ON public.sites USING btree (design_id); + + +-- +-- Name: index_sites_on_licencia_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_sites_on_licencia_id ON public.sites USING btree (licencia_id); + + +-- +-- Name: index_sites_on_name; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_sites_on_name ON public.sites USING btree (name); + + +-- +-- Name: index_stats_on_name; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_stats_on_name ON public.stats USING hash (name); + + +-- +-- Name: index_stats_on_site_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_stats_on_site_id ON public.stats USING btree (site_id); + + +-- +-- Name: index_usuaries_on_confirmation_token; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_usuaries_on_confirmation_token ON public.usuaries USING btree (confirmation_token); + + +-- +-- Name: index_usuaries_on_email; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_usuaries_on_email ON public.usuaries USING btree (email); + + +-- +-- Name: index_usuaries_on_invitation_token; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_usuaries_on_invitation_token ON public.usuaries USING btree (invitation_token); + + +-- +-- Name: index_usuaries_on_invitations_count; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_usuaries_on_invitations_count ON public.usuaries USING btree (invitations_count); + + +-- +-- Name: index_usuaries_on_invited_by_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_usuaries_on_invited_by_id ON public.usuaries USING btree (invited_by_id); + + +-- +-- Name: index_usuaries_on_invited_by_type_and_invited_by_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_usuaries_on_invited_by_type_and_invited_by_id ON public.usuaries USING btree (invited_by_type, invited_by_id); + + +-- +-- Name: index_usuaries_on_reset_password_token; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_usuaries_on_reset_password_token ON public.usuaries USING btree (reset_password_token); + + +-- +-- Name: index_usuaries_on_unlock_token; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_usuaries_on_unlock_token ON public.usuaries USING btree (unlock_token); + + +-- +-- Name: que_jobs_args_gin_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX que_jobs_args_gin_idx ON public.que_jobs USING gin (args jsonb_path_ops); + + +-- +-- Name: que_jobs_data_gin_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX que_jobs_data_gin_idx ON public.que_jobs USING gin (data jsonb_path_ops); + + +-- +-- Name: que_jobs_kwargs_gin_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX que_jobs_kwargs_gin_idx ON public.que_jobs USING gin (kwargs jsonb_path_ops); + + +-- +-- Name: que_poll_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX que_poll_idx ON public.que_jobs USING btree (job_schema_version, queue, priority, run_at, id) WHERE ((finished_at IS NULL) AND (expired_at IS NULL)); + + +-- +-- Name: access_logs access_logs_before_insert_row_tr; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER access_logs_before_insert_row_tr BEFORE INSERT ON public.access_logs FOR EACH ROW EXECUTE FUNCTION public.access_logs_before_insert_row_tr(); + + +-- +-- Name: indexed_posts indexed_posts_before_insert_update_row_tr; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER indexed_posts_before_insert_update_row_tr BEFORE INSERT OR UPDATE ON public.indexed_posts FOR EACH ROW EXECUTE FUNCTION public.indexed_posts_before_insert_update_row_tr(); + + +-- +-- Name: que_jobs que_job_notify; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER que_job_notify AFTER INSERT ON public.que_jobs FOR EACH ROW WHEN ((NOT (COALESCE(current_setting('que.skip_notify'::text, true), ''::text) = 'true'::text))) EXECUTE FUNCTION public.que_job_notify(); + + +-- +-- Name: que_jobs que_state_notify; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER que_state_notify AFTER INSERT OR DELETE OR UPDATE ON public.que_jobs FOR EACH ROW WHEN ((NOT (COALESCE(current_setting('que.skip_notify'::text, true), ''::text) = 'true'::text))) EXECUTE FUNCTION public.que_state_notify(); + + +-- +-- Name: active_storage_variant_records fk_rails_993965df05; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.active_storage_variant_records + ADD CONSTRAINT fk_rails_993965df05 FOREIGN KEY (blob_id) REFERENCES public.active_storage_blobs(id); + + +-- +-- Name: active_storage_attachments fk_rails_c3b3935057; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.active_storage_attachments + ADD CONSTRAINT fk_rails_c3b3935057 FOREIGN KEY (blob_id) REFERENCES public.active_storage_blobs(id); + + +-- +-- PostgreSQL database dump complete +-- + +SET search_path TO "$user", public; + +INSERT INTO "schema_migrations" (version) VALUES +('20180925183241'), +('20190211184815'), +('20190703190859'), +('20190703200455'), +('20190705195758'), +('20190705215536'), +('20190706000159'), +('20190706002615'), +('20190711183726'), +('20190712165059'), +('20190716195155'), +('20190716195449'), +('20190716195811'), +('20190716195812'), +('20190716202024'), +('20190717214308'), +('20190718185817'), +('20190719221653'), +('20190723220002'), +('20190725185427'), +('20190726003756'), +('20190730211624'), +('20190730211756'), +('20190820225238'), +('20190829163530'), +('20190829180743'), +('20200118155319'), +('20200126175158'), +('20200130193655'), +('20200205173039'), +('20200206151057'), +('20200206163257'), +('20200527221900'), +('20200529154040'), +('20200615171026'), +('20200616133218'), +('20200801230101'), +('20200801233025'), +('20200810230944'), +('20200811210507'), +('20200816003344'), +('20200820165316'), +('20200822204920'), +('20201111203031'), +('20201207152354'), +('20201224162153'), +('20201224162154'), +('20210414152728'), +('20210504224144'), +('20210504224343'), +('20210506212356'), +('20210507221120'), +('20210511211357'), +('20210722191718'), +('20210807003928'), +('20210807004941'), +('20210926205448'), +('20211008201239'), +('20211022224008'), +('20211022225449'), +('20220406211042'), +('20220428135113'), +('20220712135053'), +('20220802153308'), +('20230119165420'), +('20230318183722'), +('20230322214924'), +('20230322231344'), +('20230325163802'), +('20230328200129'), +('20230328213242'), +('20230328231029'), +('20230411185406'), +('20230415153231'), +('20230421182627'), +('20230424174544'), +('20230519143500'), +('20230524190240'); + + diff --git a/lib/tasks/distributed_press.rake b/lib/tasks/distributed_press.rake new file mode 100644 index 00000000..8ba270ec --- /dev/null +++ b/lib/tasks/distributed_press.rake @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +namespace :distributed_press do + namespace :tokens do + desc 'Renew tokens' + task renew: :environment do + RenewDistributedPressTokensJob.perform_now + end + end +end diff --git a/monit.conf b/monit.conf index 39f45d6d..99025968 100644 --- a/monit.conf +++ b/monit.conf @@ -4,6 +4,11 @@ check program cleanup every "0 3 1 * *" if status != 0 then alert +check program distributed_press_tokens_renew + with path "/usr/bin/foreman run -f /srv/Procfile -d /srv distributed_press_tokens_renew" as uid "rails" gid "www-data" + every "0 3 * * *" + if status != 0 then alert + check program access_logs with path "/srv/http/bin/access_logs" as uid "app" and gid "www-data" every "0 0 * * *" @@ -13,3 +18,7 @@ check program stats with path "/usr/bin/foreman run -f /srv/Procfile -d /srv stats" as uid "rails" gid "www-data" every "0 1 * * *" if status != 0 then alert + +check process que with pidfile /srv/tmp/que.pid + start program = "/usr/bin/foreman run -f /srv/Procfile -d /srv que" + stop program = "/bin/sh -c 'cat /srv/tmp/que.pid | xargs -r kill'" diff --git a/package.json b/package.json index d520c8f5..6a5f159f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "sutty", + "author": "Sutty ", "private": true, "dependencies": { "@airbrake/browser": "^1.4.1", diff --git a/public/500.html b/public/500.html index 2d69baf6..9e8ea780 100644 --- a/public/500.html +++ b/public/500.html @@ -19,7 +19,11 @@

Gracias por ayudarnos a encontrar errores :)

-

Volver al panel

+

+ Volver al panel + | + Contáctanos +

@@ -31,7 +35,11 @@

Thanks for helping us in finding errors! :)

-

Go back to panel

+

+ Go back to panel + | + Contact us +

diff --git a/public/assets/.sprockets-manifest-c6294bb290dcb7473076f4de99ce9c00.json b/public/assets/.sprockets-manifest-c6294bb290dcb7473076f4de99ce9c00.json new file mode 100644 index 00000000..862103e6 --- /dev/null +++ b/public/assets/.sprockets-manifest-c6294bb290dcb7473076f4de99ce9c00.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abb17ebe7d32c8d1bc90cc26c95cd99188a8ea066d8c975834b8299b7297f157 +size 11872 diff --git a/public/assets/.sprockets-manifest-c6294bb290dcb7473076f4de99ce9c00.json.br b/public/assets/.sprockets-manifest-c6294bb290dcb7473076f4de99ce9c00.json.br new file mode 100644 index 00000000..98bb90ad --- /dev/null +++ b/public/assets/.sprockets-manifest-c6294bb290dcb7473076f4de99ce9c00.json.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e0106b0b4c59585bab8c4f20fcf9026a22ffcd845ce281a3ff6879b905edfe8 +size 4003 diff --git a/public/assets/activestorage-9f749d32771691cb3104d21c1c9926e52c04c48d8730498df4aaa39b0adfb7ba.js b/public/assets/activestorage-9f749d32771691cb3104d21c1c9926e52c04c48d8730498df4aaa39b0adfb7ba.js new file mode 100644 index 00000000..cbde635a --- /dev/null +++ b/public/assets/activestorage-9f749d32771691cb3104d21c1c9926e52c04c48d8730498df4aaa39b0adfb7ba.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aa76e929b6ee4e3bab1734223a46c0d22a1cf2425d2e028c3075528436171dc +size 33422 diff --git a/public/assets/activestorage-9f749d32771691cb3104d21c1c9926e52c04c48d8730498df4aaa39b0adfb7ba.js.br b/public/assets/activestorage-9f749d32771691cb3104d21c1c9926e52c04c48d8730498df4aaa39b0adfb7ba.js.br new file mode 100644 index 00000000..89860cb3 --- /dev/null +++ b/public/assets/activestorage-9f749d32771691cb3104d21c1c9926e52c04c48d8730498df4aaa39b0adfb7ba.js.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2a65b9d05b5f692ad4199c5aa612e13b165e68b4a1330ee7c60a414fd5a8cbf +size 6541 diff --git a/public/assets/activestorage-9f749d32771691cb3104d21c1c9926e52c04c48d8730498df4aaa39b0adfb7ba.js.gz b/public/assets/activestorage-9f749d32771691cb3104d21c1c9926e52c04c48d8730498df4aaa39b0adfb7ba.js.gz new file mode 100644 index 00000000..00389f1a --- /dev/null +++ b/public/assets/activestorage-9f749d32771691cb3104d21c1c9926e52c04c48d8730498df4aaa39b0adfb7ba.js.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6dcef6c2e458d817696a109d51b14564902cf9c50606b7f8adf522ea6d03bec +size 6899 diff --git a/public/assets/application-dc5234b33b6c06db79ab79114f5b85952f35482ce5b97061ce9402b2a8a4cd19.css b/public/assets/application-dc5234b33b6c06db79ab79114f5b85952f35482ce5b97061ce9402b2a8a4cd19.css new file mode 100644 index 00000000..f3f0e72f --- /dev/null +++ b/public/assets/application-dc5234b33b6c06db79ab79114f5b85952f35482ce5b97061ce9402b2a8a4cd19.css @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45b58cea6de408b3b9e666b149e9cb5aae3f3669d6bd3587def372af51e9ec9c +size 531463 diff --git a/public/assets/application-dc5234b33b6c06db79ab79114f5b85952f35482ce5b97061ce9402b2a8a4cd19.css.br b/public/assets/application-dc5234b33b6c06db79ab79114f5b85952f35482ce5b97061ce9402b2a8a4cd19.css.br new file mode 100644 index 00000000..cfb849ef --- /dev/null +++ b/public/assets/application-dc5234b33b6c06db79ab79114f5b85952f35482ce5b97061ce9402b2a8a4cd19.css.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:587c4692885409e7bf9eec2ae7a47eb1954b86f14d4e25a42403663d4d59b268 +size 35024 diff --git a/public/assets/application-dc5234b33b6c06db79ab79114f5b85952f35482ce5b97061ce9402b2a8a4cd19.css.gz b/public/assets/application-dc5234b33b6c06db79ab79114f5b85952f35482ce5b97061ce9402b2a8a4cd19.css.gz new file mode 100644 index 00000000..6d8cbce0 --- /dev/null +++ b/public/assets/application-dc5234b33b6c06db79ab79114f5b85952f35482ce5b97061ce9402b2a8a4cd19.css.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2997bf3c615d602a40238b3ab474034918f4837d88c7f55fec818fda8ea31f17 +size 42896 diff --git a/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg b/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg new file mode 100644 index 00000000..2a7f2458 --- /dev/null +++ b/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92f2db633d3e9611dc07743d7267204c40de2f8d75b9fa7e0646b195541f2d53 +size 571 diff --git a/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.br b/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.br new file mode 100644 index 00000000..fe5ef2ad --- /dev/null +++ b/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66f6cc7289f2ab65e5c05f470dc61b62d3b67dfc6b02111f7ac608da9f371273 +size 323 diff --git a/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.gz b/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.gz new file mode 100644 index 00000000..f5afff36 --- /dev/null +++ b/public/assets/arrows-alt-v-89a34626be855d3b1c3199cd75f62cf6327678eed1e126f74b7cbc6de3502606.svg.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f690800fb0f0e2ce7b86b2564f2b4521030854c3eb13ccad730911181b906a1e +size 359 diff --git a/public/assets/blazer/application-59c73da0ca1f2fd8dd42765f0a172ae513546920ad9fa0718c15d9b10a4f18dd.css b/public/assets/blazer/application-59c73da0ca1f2fd8dd42765f0a172ae513546920ad9fa0718c15d9b10a4f18dd.css new file mode 100644 index 00000000..d065b1d6 --- /dev/null +++ b/public/assets/blazer/application-59c73da0ca1f2fd8dd42765f0a172ae513546920ad9fa0718c15d9b10a4f18dd.css @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbc16a398c527ae16bd978e4c16f5803989f3025be59e35332a0547a57eae668 +size 161257 diff --git a/public/assets/blazer/application-59c73da0ca1f2fd8dd42765f0a172ae513546920ad9fa0718c15d9b10a4f18dd.css.br b/public/assets/blazer/application-59c73da0ca1f2fd8dd42765f0a172ae513546920ad9fa0718c15d9b10a4f18dd.css.br new file mode 100644 index 00000000..1c55ad41 --- /dev/null +++ b/public/assets/blazer/application-59c73da0ca1f2fd8dd42765f0a172ae513546920ad9fa0718c15d9b10a4f18dd.css.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:669ad50ad737f1d4f996854f4da308c78dee7766e6bbfde1df609fd72502f56b +size 22772 diff --git a/public/assets/blazer/application-59c73da0ca1f2fd8dd42765f0a172ae513546920ad9fa0718c15d9b10a4f18dd.css.gz b/public/assets/blazer/application-59c73da0ca1f2fd8dd42765f0a172ae513546920ad9fa0718c15d9b10a4f18dd.css.gz new file mode 100644 index 00000000..bb2ecb25 --- /dev/null +++ b/public/assets/blazer/application-59c73da0ca1f2fd8dd42765f0a172ae513546920ad9fa0718c15d9b10a4f18dd.css.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab6c28f79b9b9a72deb9c4d8247f2f481e0e7c769f063d63a195d31c1fc40c37 +size 25170 diff --git a/public/assets/blazer/application-f14294068656a14630709493ca1bdd375b16209db5d3307d61c4927dd3eff3c5.js b/public/assets/blazer/application-f14294068656a14630709493ca1bdd375b16209db5d3307d61c4927dd3eff3c5.js new file mode 100644 index 00000000..14b3990b --- /dev/null +++ b/public/assets/blazer/application-f14294068656a14630709493ca1bdd375b16209db5d3307d61c4927dd3eff3c5.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32ff6af566ab140819899b0fea350b80fdf527efbe30aadb14cac437e2bc01fb +size 9 diff --git a/public/assets/blazer/application-f14294068656a14630709493ca1bdd375b16209db5d3307d61c4927dd3eff3c5.js.br b/public/assets/blazer/application-f14294068656a14630709493ca1bdd375b16209db5d3307d61c4927dd3eff3c5.js.br new file mode 100644 index 00000000..9e09ad94 --- /dev/null +++ b/public/assets/blazer/application-f14294068656a14630709493ca1bdd375b16209db5d3307d61c4927dd3eff3c5.js.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b3fb497058b6fbc2560ef56b4e38cd96fa5537289c45fbbfe090f23c8b1161e +size 14 diff --git a/public/assets/blazer/application-f14294068656a14630709493ca1bdd375b16209db5d3307d61c4927dd3eff3c5.js.gz b/public/assets/blazer/application-f14294068656a14630709493ca1bdd375b16209db5d3307d61c4927dd3eff3c5.js.gz new file mode 100644 index 00000000..0a26fc68 --- /dev/null +++ b/public/assets/blazer/application-f14294068656a14630709493ca1bdd375b16209db5d3307d61c4927dd3eff3c5.js.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8cd6192edf216bb14ad01dcf0d7a164e3c8e759376d51528907fe0bcde2b781 +size 29 diff --git a/public/assets/blazer/favicon-ccead91b8853543a3542af03c7dde9963359b1c0b9b725220a7f193e1324ecd8.png b/public/assets/blazer/favicon-ccead91b8853543a3542af03c7dde9963359b1c0b9b725220a7f193e1324ecd8.png new file mode 100644 index 00000000..f275a453 --- /dev/null +++ b/public/assets/blazer/favicon-ccead91b8853543a3542af03c7dde9963359b1c0b9b725220a7f193e1324ecd8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:392743107d0d14fea5699cff1e80d209db7058ef660e643ff9c9f07e990ac084 +size 320 diff --git a/public/assets/blazer/favicon-ccead91b8853543a3542af03c7dde9963359b1c0b9b725220a7f193e1324ecd8.png.br b/public/assets/blazer/favicon-ccead91b8853543a3542af03c7dde9963359b1c0b9b725220a7f193e1324ecd8.png.br new file mode 100644 index 00000000..4290fce9 --- /dev/null +++ b/public/assets/blazer/favicon-ccead91b8853543a3542af03c7dde9963359b1c0b9b725220a7f193e1324ecd8.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42f1101a87e305093c8326571f6eba5e2f647183f15922b68b511f13e172908d +size 248 diff --git a/public/assets/blazer/glyphicons-halflings-regular-0703369a358a012c0011843ae337a8a20270c336948a8668df5cb89a8827299b.woff b/public/assets/blazer/glyphicons-halflings-regular-0703369a358a012c0011843ae337a8a20270c336948a8668df5cb89a8827299b.woff new file mode 100644 index 00000000..334f2da2 --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-0703369a358a012c0011843ae337a8a20270c336948a8668df5cb89a8827299b.woff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a26394f7ede100ca118eff2eda08596275a9839b959c226e15439557a5a80742 +size 23424 diff --git a/public/assets/blazer/glyphicons-halflings-regular-0703369a358a012c0011843ae337a8a20270c336948a8668df5cb89a8827299b.woff.br b/public/assets/blazer/glyphicons-halflings-regular-0703369a358a012c0011843ae337a8a20270c336948a8668df5cb89a8827299b.woff.br new file mode 100644 index 00000000..5acfec3d --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-0703369a358a012c0011843ae337a8a20270c336948a8668df5cb89a8827299b.woff.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ee84e264080ea5f4f7f23ae9c8aa8f138621b4a4dbbb86f3c839da28b0e5a56 +size 23116 diff --git a/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot b/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot new file mode 100644 index 00000000..42ea705f --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13634da87d9e23f8c3ed9108ce1724d183a39ad072e73e1b3d8cbf646d2d0407 +size 20127 diff --git a/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.br b/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.br new file mode 100644 index 00000000..558ceb52 --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1a2ea6dc4d0790cede399727cf57dc55dd126e3886e756559cef13f897c6dd0 +size 20012 diff --git a/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.gz b/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.gz new file mode 100644 index 00000000..44d30965 --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-0805fb1fe24235f70a639f67514990e4bfb6d2cfb00ca563ad4b553c240ddc33.eot.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:232dfba09dd29cfeff9e075a9ba30ed192f678b5946c1a1ec022e9887cc1dc8f +size 20056 diff --git a/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg b/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg new file mode 100644 index 00000000..781641cc --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42f60659d265c1a3c30f9fa42abcbb56bd4a53af4d83d316d6dd7a36903c43e5 +size 108738 diff --git a/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.br b/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.br new file mode 100644 index 00000000..63ae3b6c --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51506970c9012e0474056ff17ff204bf0f1782c547c6fb3dfb584e1572b1b1c5 +size 24800 diff --git a/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.gz b/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.gz new file mode 100644 index 00000000..49a5ccf2 --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-22d0c88a49d7d0ebe45627143a601061a32a46a9b9afd2dc7f457436f5f15f6e.svg.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2879447637c85f780c62c9decfdc9b6847dfddc0c353ecf056a5563afd5d98c0 +size 26508 diff --git a/public/assets/blazer/glyphicons-halflings-regular-403acfcf0cbaebd1c28b404eec442cea53642644b3a73f91c5a4ab46859af772.woff2 b/public/assets/blazer/glyphicons-halflings-regular-403acfcf0cbaebd1c28b404eec442cea53642644b3a73f91c5a4ab46859af772.woff2 new file mode 100644 index 00000000..3336ba11 --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-403acfcf0cbaebd1c28b404eec442cea53642644b3a73f91c5a4ab46859af772.woff2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe185d11a49676890d47bb783312a0cda5a44c4039214094e7957b4c040ef11c +size 18028 diff --git a/public/assets/blazer/glyphicons-halflings-regular-403acfcf0cbaebd1c28b404eec442cea53642644b3a73f91c5a4ab46859af772.woff2.br b/public/assets/blazer/glyphicons-halflings-regular-403acfcf0cbaebd1c28b404eec442cea53642644b3a73f91c5a4ab46859af772.woff2.br new file mode 100644 index 00000000..2968e5d2 --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-403acfcf0cbaebd1c28b404eec442cea53642644b3a73f91c5a4ab46859af772.woff2.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7b1a9608c41733d8a73d73c31348a3ab34df13dd8f69cf5dc6eb918a14f7bd4 +size 18016 diff --git a/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf b/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf new file mode 100644 index 00000000..233386da --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e395044093757d82afcb138957d06a1ea9361bdcf0b442d06a18a8051af57456 +size 45404 diff --git a/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.br b/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.br new file mode 100644 index 00000000..2f238f1e --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83d0181bfdb29d3744dbc122e63263657dcfa359fbd8a4f2cd915b2ee49d3fbe +size 22714 diff --git a/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.gz b/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.gz new file mode 100644 index 00000000..e77b3e37 --- /dev/null +++ b/public/assets/blazer/glyphicons-halflings-regular-7c9caa5f4e16169b0129fdf93c84e85ad14d6c107eb1b0ad60b542daf01ee1f0.ttf.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0668c774700aef315f25dcf8376be7aa88eec8b609b47f37c9ba53bbdb997ee +size 23360 diff --git a/public/assets/editor-6ceecb9d2dc47b39b99575e60bc2c9eec573a495812d521ec5b82dedd4f55807.css b/public/assets/editor-6ceecb9d2dc47b39b99575e60bc2c9eec573a495812d521ec5b82dedd4f55807.css new file mode 100644 index 00000000..377ab667 --- /dev/null +++ b/public/assets/editor-6ceecb9d2dc47b39b99575e60bc2c9eec573a495812d521ec5b82dedd4f55807.css @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0b95902009200f525c963c605b9ffa600ea673860dae9f43f33bc557257182b +size 3562 diff --git a/public/assets/editor-6ceecb9d2dc47b39b99575e60bc2c9eec573a495812d521ec5b82dedd4f55807.css.br b/public/assets/editor-6ceecb9d2dc47b39b99575e60bc2c9eec573a495812d521ec5b82dedd4f55807.css.br new file mode 100644 index 00000000..df162533 --- /dev/null +++ b/public/assets/editor-6ceecb9d2dc47b39b99575e60bc2c9eec573a495812d521ec5b82dedd4f55807.css.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9700c118f5e2df28e708aa79143d9135e0b7e67874ab3c976cd604d8a3beff9e +size 738 diff --git a/public/assets/editor-6ceecb9d2dc47b39b99575e60bc2c9eec573a495812d521ec5b82dedd4f55807.css.gz b/public/assets/editor-6ceecb9d2dc47b39b99575e60bc2c9eec573a495812d521ec5b82dedd4f55807.css.gz new file mode 100644 index 00000000..2b210f57 --- /dev/null +++ b/public/assets/editor-6ceecb9d2dc47b39b99575e60bc2c9eec573a495812d521ec5b82dedd4f55807.css.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0122fcc56f8dc98299e779bd5b3b2960c6d3e3535b0da3265765ca136268a8f +size 846 diff --git a/public/assets/fonts-86ce817a6287b9ff301be232f102b66152dd83f264e13a531098d8dc8b1398da.css b/public/assets/fonts-86ce817a6287b9ff301be232f102b66152dd83f264e13a531098d8dc8b1398da.css new file mode 100644 index 00000000..92436ec6 --- /dev/null +++ b/public/assets/fonts-86ce817a6287b9ff301be232f102b66152dd83f264e13a531098d8dc8b1398da.css @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:053af9006ae009bec81ce6b0403cff779891708d6b446684fb7870ac5bacb462 +size 839 diff --git a/public/assets/fonts-86ce817a6287b9ff301be232f102b66152dd83f264e13a531098d8dc8b1398da.css.br b/public/assets/fonts-86ce817a6287b9ff301be232f102b66152dd83f264e13a531098d8dc8b1398da.css.br new file mode 100644 index 00000000..430e9dd8 --- /dev/null +++ b/public/assets/fonts-86ce817a6287b9ff301be232f102b66152dd83f264e13a531098d8dc8b1398da.css.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fb27aaf2a70e5353771f5b1a3deb39506c97438f74930dbde35137a3370735c +size 340 diff --git a/public/assets/fonts-86ce817a6287b9ff301be232f102b66152dd83f264e13a531098d8dc8b1398da.css.gz b/public/assets/fonts-86ce817a6287b9ff301be232f102b66152dd83f264e13a531098d8dc8b1398da.css.gz new file mode 100644 index 00000000..2e6771be --- /dev/null +++ b/public/assets/fonts-86ce817a6287b9ff301be232f102b66152dd83f264e13a531098d8dc8b1398da.css.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e005f190d04b1bccc984df0b54ea772eb13fd97c813075f4b2e9c39fab2cc35 +size 390 diff --git a/public/assets/icon_external_link-1af8262ac9c00df26e81bc5a33bcf64350729f954b85f82d5e759fffec4e183a.png b/public/assets/icon_external_link-1af8262ac9c00df26e81bc5a33bcf64350729f954b85f82d5e759fffec4e183a.png new file mode 100644 index 00000000..e882b8bc --- /dev/null +++ b/public/assets/icon_external_link-1af8262ac9c00df26e81bc5a33bcf64350729f954b85f82d5e759fffec4e183a.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a0acd631fd5704e940b9f486d3234aa9ab871881733f48d6edd3cb1f1a09ffc +size 144 diff --git a/public/assets/icon_external_link-1af8262ac9c00df26e81bc5a33bcf64350729f954b85f82d5e759fffec4e183a.png.br b/public/assets/icon_external_link-1af8262ac9c00df26e81bc5a33bcf64350729f954b85f82d5e759fffec4e183a.png.br new file mode 100644 index 00000000..ca08d316 --- /dev/null +++ b/public/assets/icon_external_link-1af8262ac9c00df26e81bc5a33bcf64350729f954b85f82d5e759fffec4e183a.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c50779d6d93ea71b10be4972af879e611353812b40f566d864f189218129cf9 +size 149 diff --git a/public/assets/layers-0e356f4d554162eb71f127f50460dbc55d405027189ebe90b20729ef18d13d36.png b/public/assets/layers-0e356f4d554162eb71f127f50460dbc55d405027189ebe90b20729ef18d13d36.png new file mode 100644 index 00000000..7b44754c --- /dev/null +++ b/public/assets/layers-0e356f4d554162eb71f127f50460dbc55d405027189ebe90b20729ef18d13d36.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dbbe9d028e292f36fcba8f8b3a28d5e8932754fc2215b9ac69e4cdecf5107c6 +size 696 diff --git a/public/assets/layers-0e356f4d554162eb71f127f50460dbc55d405027189ebe90b20729ef18d13d36.png.br b/public/assets/layers-0e356f4d554162eb71f127f50460dbc55d405027189ebe90b20729ef18d13d36.png.br new file mode 100644 index 00000000..21812b3e --- /dev/null +++ b/public/assets/layers-0e356f4d554162eb71f127f50460dbc55d405027189ebe90b20729ef18d13d36.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e255d97d12f227f9d2fabd481fa56d89bf33b47a4d108b031e7b96ddc0fb0066 +size 701 diff --git a/public/assets/layers-2x-ba8fa601e413b14db27db07285ade3951721e02244c31523284ab2d1ed53c3dc.png b/public/assets/layers-2x-ba8fa601e413b14db27db07285ade3951721e02244c31523284ab2d1ed53c3dc.png new file mode 100644 index 00000000..d3cf7e52 --- /dev/null +++ b/public/assets/layers-2x-ba8fa601e413b14db27db07285ade3951721e02244c31523284ab2d1ed53c3dc.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:066daca850d8ffbef007af00b06eac0015728dee279c51f3cb6c716df7c42edf +size 1259 diff --git a/public/assets/layers-2x-ba8fa601e413b14db27db07285ade3951721e02244c31523284ab2d1ed53c3dc.png.br b/public/assets/layers-2x-ba8fa601e413b14db27db07285ade3951721e02244c31523284ab2d1ed53c3dc.png.br new file mode 100644 index 00000000..cda7f53d --- /dev/null +++ b/public/assets/layers-2x-ba8fa601e413b14db27db07285ade3951721e02244c31523284ab2d1ed53c3dc.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47119b2b0eb7d15fa47527efc1fa04713d80990ed6a7f2e2f58f91a8cddd1b03 +size 1264 diff --git a/public/assets/logo-e11ab53230eae9497ea201f3ad57549af343ddc1d24ddc78b055627cf14e9d5d.png b/public/assets/logo-e11ab53230eae9497ea201f3ad57549af343ddc1d24ddc78b055627cf14e9d5d.png new file mode 100644 index 00000000..be79dc74 --- /dev/null +++ b/public/assets/logo-e11ab53230eae9497ea201f3ad57549af343ddc1d24ddc78b055627cf14e9d5d.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abab4950f459ebf3f475d376d6fd468e735cab2d86c712222fa586569a273a09 +size 2001 diff --git a/public/assets/logo-e11ab53230eae9497ea201f3ad57549af343ddc1d24ddc78b055627cf14e9d5d.png.br b/public/assets/logo-e11ab53230eae9497ea201f3ad57549af343ddc1d24ddc78b055627cf14e9d5d.png.br new file mode 100644 index 00000000..5fce7f8e --- /dev/null +++ b/public/assets/logo-e11ab53230eae9497ea201f3ad57549af343ddc1d24ddc78b055627cf14e9d5d.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:348d75dc528dbaff2a18adb6f4a4b66d4610e6dca2f99889b52e7f0c629a8be7 +size 2006 diff --git a/public/assets/manifest-dad05bf766af0fe3d79dd746db3c1361c0583026cdf35d6a2921bccaea835331.js b/public/assets/manifest-dad05bf766af0fe3d79dd746db3c1361c0583026cdf35d6a2921bccaea835331.js new file mode 100644 index 00000000..7a77a071 --- /dev/null +++ b/public/assets/manifest-dad05bf766af0fe3d79dd746db3c1361c0583026cdf35d6a2921bccaea835331.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a3cf5192354f71615ac51034b3e97c20eda99643fcaf5bbe6d41ad59bd12167 +size 3 diff --git a/public/assets/manifest-dad05bf766af0fe3d79dd746db3c1361c0583026cdf35d6a2921bccaea835331.js.br b/public/assets/manifest-dad05bf766af0fe3d79dd746db3c1361c0583026cdf35d6a2921bccaea835331.js.br new file mode 100644 index 00000000..40a6d1ac --- /dev/null +++ b/public/assets/manifest-dad05bf766af0fe3d79dd746db3c1361c0583026cdf35d6a2921bccaea835331.js.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad41782cd73052ce3e59ea9b261ffeeeebfbacaa90924c01c7c47f012674bb15 +size 8 diff --git a/public/assets/manifest-dad05bf766af0fe3d79dd746db3c1361c0583026cdf35d6a2921bccaea835331.js.gz b/public/assets/manifest-dad05bf766af0fe3d79dd746db3c1361c0583026cdf35d6a2921bccaea835331.js.gz new file mode 100644 index 00000000..884f1518 --- /dev/null +++ b/public/assets/manifest-dad05bf766af0fe3d79dd746db3c1361c0583026cdf35d6a2921bccaea835331.js.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19a332fca5af4a8c3ee68386360b5212428611709c8463c00631d65141b8f5bb +size 23 diff --git a/public/assets/marker-icon-2x-091245b393c16cdcefe54920aa7d3994a0683317ca9a58d35cbc5ec65996398c.png b/public/assets/marker-icon-2x-091245b393c16cdcefe54920aa7d3994a0683317ca9a58d35cbc5ec65996398c.png new file mode 100644 index 00000000..09e8445f --- /dev/null +++ b/public/assets/marker-icon-2x-091245b393c16cdcefe54920aa7d3994a0683317ca9a58d35cbc5ec65996398c.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00179c4c1ee830d3a108412ae0d294f55776cfeb085c60129a39aa6fc4ae2528 +size 2464 diff --git a/public/assets/marker-icon-2x-091245b393c16cdcefe54920aa7d3994a0683317ca9a58d35cbc5ec65996398c.png.br b/public/assets/marker-icon-2x-091245b393c16cdcefe54920aa7d3994a0683317ca9a58d35cbc5ec65996398c.png.br new file mode 100644 index 00000000..585d2e48 --- /dev/null +++ b/public/assets/marker-icon-2x-091245b393c16cdcefe54920aa7d3994a0683317ca9a58d35cbc5ec65996398c.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4f6ce09c4c3a59cfac5c03a03de02b170d35cfefd7e1247a317dc772c889395 +size 2469 diff --git a/public/assets/marker-icon-3d253116ec4ba0e1f22a01cdf1ff7f120fa4d89a6cd0933d68f12951d19809b4.png b/public/assets/marker-icon-3d253116ec4ba0e1f22a01cdf1ff7f120fa4d89a6cd0933d68f12951d19809b4.png new file mode 100644 index 00000000..b1789c76 --- /dev/null +++ b/public/assets/marker-icon-3d253116ec4ba0e1f22a01cdf1ff7f120fa4d89a6cd0933d68f12951d19809b4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437 +size 1466 diff --git a/public/assets/marker-icon-3d253116ec4ba0e1f22a01cdf1ff7f120fa4d89a6cd0933d68f12951d19809b4.png.br b/public/assets/marker-icon-3d253116ec4ba0e1f22a01cdf1ff7f120fa4d89a6cd0933d68f12951d19809b4.png.br new file mode 100644 index 00000000..62bba2a0 --- /dev/null +++ b/public/assets/marker-icon-3d253116ec4ba0e1f22a01cdf1ff7f120fa4d89a6cd0933d68f12951d19809b4.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d73a1e81d4d7b880342c1fa48caeb6cdb56cc941ad18000d932dfaec8ba1086 +size 1471 diff --git a/public/assets/marker-shadow-a2d94406ba198f61f68a71ed8f9f9c701122c0c33b775d990edceae4aece567f.png b/public/assets/marker-shadow-a2d94406ba198f61f68a71ed8f9f9c701122c0c33b775d990edceae4aece567f.png new file mode 100644 index 00000000..dc111216 --- /dev/null +++ b/public/assets/marker-shadow-a2d94406ba198f61f68a71ed8f9f9c701122c0c33b775d990edceae4aece567f.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da +size 618 diff --git a/public/assets/marker-shadow-a2d94406ba198f61f68a71ed8f9f9c701122c0c33b775d990edceae4aece567f.png.br b/public/assets/marker-shadow-a2d94406ba198f61f68a71ed8f9f9c701122c0c33b775d990edceae4aece567f.png.br new file mode 100644 index 00000000..940c4d9b --- /dev/null +++ b/public/assets/marker-shadow-a2d94406ba198f61f68a71ed8f9f9c701122c0c33b775d990edceae4aece567f.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8758eb0644225f0a3c8e03fb1dbe319f6427b4116a538084f18ba5a6dcd65eb2 +size 623 diff --git a/public/assets/saira/v3/SairaBold-subset-0c1968b6a54ea5684d70cc5b51fb1ae3186386fe9363bf3edaf01663ac641341.woff2 b/public/assets/saira/v3/SairaBold-subset-0c1968b6a54ea5684d70cc5b51fb1ae3186386fe9363bf3edaf01663ac641341.woff2 new file mode 100644 index 00000000..580c899c --- /dev/null +++ b/public/assets/saira/v3/SairaBold-subset-0c1968b6a54ea5684d70cc5b51fb1ae3186386fe9363bf3edaf01663ac641341.woff2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ad13d7b01c301feecfb7f760791100710930be2d232fcdf97fb09a1301e4c3e +size 16264 diff --git a/public/assets/saira/v3/SairaBold-subset-0c1968b6a54ea5684d70cc5b51fb1ae3186386fe9363bf3edaf01663ac641341.woff2.br b/public/assets/saira/v3/SairaBold-subset-0c1968b6a54ea5684d70cc5b51fb1ae3186386fe9363bf3edaf01663ac641341.woff2.br new file mode 100644 index 00000000..ab0b6472 --- /dev/null +++ b/public/assets/saira/v3/SairaBold-subset-0c1968b6a54ea5684d70cc5b51fb1ae3186386fe9363bf3edaf01663ac641341.woff2.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a45bda0673da257af77f89c6d9151c908dd15dae3eeb536911be81383e58e42 +size 16260 diff --git a/public/assets/saira/v3/SairaBold-subset.zopfli-2d3f8769110de8d5709d5162dbf0dcb9ab8df71e55f63cfd986bdb68ee2ade0b.woff b/public/assets/saira/v3/SairaBold-subset.zopfli-2d3f8769110de8d5709d5162dbf0dcb9ab8df71e55f63cfd986bdb68ee2ade0b.woff new file mode 100644 index 00000000..94528fe4 --- /dev/null +++ b/public/assets/saira/v3/SairaBold-subset.zopfli-2d3f8769110de8d5709d5162dbf0dcb9ab8df71e55f63cfd986bdb68ee2ade0b.woff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd722d9eb3a725f4188615c514c5c0e6b1b82de55a00dca194efa84704257dd2 +size 20120 diff --git a/public/assets/saira/v3/SairaBold-subset.zopfli-2d3f8769110de8d5709d5162dbf0dcb9ab8df71e55f63cfd986bdb68ee2ade0b.woff.br b/public/assets/saira/v3/SairaBold-subset.zopfli-2d3f8769110de8d5709d5162dbf0dcb9ab8df71e55f63cfd986bdb68ee2ade0b.woff.br new file mode 100644 index 00000000..38204fe0 --- /dev/null +++ b/public/assets/saira/v3/SairaBold-subset.zopfli-2d3f8769110de8d5709d5162dbf0dcb9ab8df71e55f63cfd986bdb68ee2ade0b.woff.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d58d08fffad848dc745108c011fe0b71bdfa14d11d17d68d514205970726bd5 +size 20055 diff --git a/public/assets/saira/v3/SairaMedium-subset-6d53d976d73b86358489cb76fc34ec0683d57a1b2635c43acbae45be3349d976.woff2 b/public/assets/saira/v3/SairaMedium-subset-6d53d976d73b86358489cb76fc34ec0683d57a1b2635c43acbae45be3349d976.woff2 new file mode 100644 index 00000000..7127bebe --- /dev/null +++ b/public/assets/saira/v3/SairaMedium-subset-6d53d976d73b86358489cb76fc34ec0683d57a1b2635c43acbae45be3349d976.woff2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ac4573ca9ed8e08a742d203171057acc3d8f5f49825d9351bd2a22c2d58e669 +size 16388 diff --git a/public/assets/saira/v3/SairaMedium-subset-6d53d976d73b86358489cb76fc34ec0683d57a1b2635c43acbae45be3349d976.woff2.br b/public/assets/saira/v3/SairaMedium-subset-6d53d976d73b86358489cb76fc34ec0683d57a1b2635c43acbae45be3349d976.woff2.br new file mode 100644 index 00000000..2d4a45ab --- /dev/null +++ b/public/assets/saira/v3/SairaMedium-subset-6d53d976d73b86358489cb76fc34ec0683d57a1b2635c43acbae45be3349d976.woff2.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92f6b5c4fd4dabbbedbc96519088b4896dbe07b393d06bbbb868d82a618d352a +size 16391 diff --git a/public/assets/saira/v3/SairaMedium-subset.zopfli-47d24dd6cc6451cd2fb3803333f4de3e11033437aa0c8d3d06edcb19e4e38ecb.woff b/public/assets/saira/v3/SairaMedium-subset.zopfli-47d24dd6cc6451cd2fb3803333f4de3e11033437aa0c8d3d06edcb19e4e38ecb.woff new file mode 100644 index 00000000..bb8a3825 --- /dev/null +++ b/public/assets/saira/v3/SairaMedium-subset.zopfli-47d24dd6cc6451cd2fb3803333f4de3e11033437aa0c8d3d06edcb19e4e38ecb.woff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dec398a6c292da28349e76f94646d91d425cf4ddc7f91d749075f72209d0dab +size 20232 diff --git a/public/assets/saira/v3/SairaMedium-subset.zopfli-47d24dd6cc6451cd2fb3803333f4de3e11033437aa0c8d3d06edcb19e4e38ecb.woff.br b/public/assets/saira/v3/SairaMedium-subset.zopfli-47d24dd6cc6451cd2fb3803333f4de3e11033437aa0c8d3d06edcb19e4e38ecb.woff.br new file mode 100644 index 00000000..6da047e7 --- /dev/null +++ b/public/assets/saira/v3/SairaMedium-subset.zopfli-47d24dd6cc6451cd2fb3803333f4de3e11033437aa0c8d3d06edcb19e4e38ecb.woff.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2a22be46135bb21d4984271f8dd42e6762a35152fba61f93ce89edd6478df23 +size 20174 diff --git a/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg b/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg new file mode 100644 index 00000000..68fca50c --- /dev/null +++ b/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb86744bcec2d691c94d431627e412113f3648eb02420f2af86de3e3884c5a6d +size 1956 diff --git a/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.br b/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.br new file mode 100644 index 00000000..bbf3b21e --- /dev/null +++ b/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb20639b4ce178eedfc586c73f7086bef65ef70533c995afdc2eedb3d1fa76c4 +size 894 diff --git a/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.gz b/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.gz new file mode 100644 index 00000000..dffee65e --- /dev/null +++ b/public/assets/sutty-08b30df17da83a32911e3bb4fa0a2c967148a2f98020a63b6c171c17c94bf05d.svg.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19973f783284df5608ec79a4fa46dce362f5626112c218266f04329b01049c43 +size 943 diff --git a/public/assets/sutty_cuadrada-547911cb970c82f2bf8fa2d68b3b32e5e2a1c0ed787dafa6ea1b98b52b96328f.png b/public/assets/sutty_cuadrada-547911cb970c82f2bf8fa2d68b3b32e5e2a1c0ed787dafa6ea1b98b52b96328f.png new file mode 100644 index 00000000..1b018802 --- /dev/null +++ b/public/assets/sutty_cuadrada-547911cb970c82f2bf8fa2d68b3b32e5e2a1c0ed787dafa6ea1b98b52b96328f.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56aa311c3c5249e54d70f03b33fc681b593cd93e74b7af5d80316c5b7d1c6a76 +size 6868 diff --git a/public/assets/sutty_cuadrada-547911cb970c82f2bf8fa2d68b3b32e5e2a1c0ed787dafa6ea1b98b52b96328f.png.br b/public/assets/sutty_cuadrada-547911cb970c82f2bf8fa2d68b3b32e5e2a1c0ed787dafa6ea1b98b52b96328f.png.br new file mode 100644 index 00000000..727045b0 --- /dev/null +++ b/public/assets/sutty_cuadrada-547911cb970c82f2bf8fa2d68b3b32e5e2a1c0ed787dafa6ea1b98b52b96328f.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8af682a97967a339d63b8c22a24ab30065e9423e1775e6c6ccdf662e5081a4f9 +size 6530 diff --git a/public/packs/css/application-7d15ae94.css b/public/packs/css/application-7d15ae94.css new file mode 100644 index 00000000..544ddf74 --- /dev/null +++ b/public/packs/css/application-7d15ae94.css @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0179eec59d4881001e3d26ec0e25d8ca1257c6e4956d76f18cc97c3325a9625 +size 48029 diff --git a/public/packs/css/application-7d15ae94.css.br b/public/packs/css/application-7d15ae94.css.br new file mode 100644 index 00000000..c0e44938 --- /dev/null +++ b/public/packs/css/application-7d15ae94.css.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f9a030d083e92a55a9a9211539c894371f200778de283958a318c87a825cb4f +size 10934 diff --git a/public/packs/css/application-7d15ae94.css.br.br b/public/packs/css/application-7d15ae94.css.br.br new file mode 100644 index 00000000..bd3a0882 --- /dev/null +++ b/public/packs/css/application-7d15ae94.css.br.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c61a1f5c9ca9cfc7b2b314046b258a94f707535a34e0c3e7b621d9b7c2cd648 +size 10939 diff --git a/public/packs/css/application-7d15ae94.css.gz b/public/packs/css/application-7d15ae94.css.gz new file mode 100644 index 00000000..306dc931 --- /dev/null +++ b/public/packs/css/application-7d15ae94.css.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfeb6b9af353618bdc475d6c921197da2b8a9ae7685a91772658d41883843c12 +size 12022 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js new file mode 100644 index 00000000..b6dc46a3 --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0322257341b6d5bcbee0153eb5007363aefe26870b63744de6f1cdf6d1a7065e +size 1134731 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.LICENSE.txt b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.LICENSE.txt new file mode 100644 index 00000000..89f0cf4f --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.LICENSE.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a016dd85be9a400040f4440d2ce1a94524f6e885a3d0e1f2422b46c2397df38f +size 629 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.LICENSE.txt.br b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.LICENSE.txt.br new file mode 100644 index 00000000..3b0be40c --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.LICENSE.txt.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46eca40f00a3261e41a1e02d89697deb163a49a56237e72ae2643cb1c28b99c0 +size 307 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.br b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.br new file mode 100644 index 00000000..6ba2a97b --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b88ebe0dd46c4b22fea0ecccb721afc5b4a88ce4b55950bce19654c2d265f72e +size 302355 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.br.br b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.br.br new file mode 100644 index 00000000..93ce3091 --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.br.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ed407a10485f5157d5eb98bc11a158c2565bc417faedaa197bdfcea486ccdcd +size 302363 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.gz b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.gz new file mode 100644 index 00000000..a978f9f3 --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ed777e669bf1e81dd7f43ff8d9c2d5d5b4c9d81c68a1fac7b7fdd3d7d578b30 +size 329294 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map new file mode 100644 index 00000000..ab3aa8ef --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02a1682a2421fd6f340241bbc6b07951aa583f2ef2b74f29fb16cf9aaf3956a0 +size 4658473 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map.br b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map.br new file mode 100644 index 00000000..ca0133f3 --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3ef78d7fb35e3c0547740d68249462805e7cf65eafd3b6f77b787e7746c1f14 +size 1099488 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map.br.br b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map.br.br new file mode 100644 index 00000000..dd2dcd4a --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map.br.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cda688a8dbd865c4720c68f2af7c8e1130cd43a955c737da9f4f38296b2d6d56 +size 1099496 diff --git a/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map.gz b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map.gz new file mode 100644 index 00000000..38ab8f51 --- /dev/null +++ b/public/packs/js/application-fd20cd4c95f90c1a3ecd.js.map.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f18506f9e0cec6f8a84d2a640646b6bae20eeeea081aba5ba26324028ffae8c2 +size 1222748 diff --git a/public/packs/manifest.json b/public/packs/manifest.json new file mode 100644 index 00000000..f3fcbef3 --- /dev/null +++ b/public/packs/manifest.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:880db5bc10cd865a8cb1a2fc56d69bba8464ef28aa59c31913256c7062528289 +size 1426 diff --git a/public/packs/manifest.json.br b/public/packs/manifest.json.br new file mode 100644 index 00000000..82077c62 --- /dev/null +++ b/public/packs/manifest.json.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cfa3712c659f01703314a2a9dd306a55d0f52a3456e20d43e080a5d716c8e45 +size 325 diff --git a/public/packs/manifest.json.br.br b/public/packs/manifest.json.br.br new file mode 100644 index 00000000..b8d746ce --- /dev/null +++ b/public/packs/manifest.json.br.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82c427cf2556c027323f463d5d735bc8dd3b2b22ac1d5eed3e7d77b3f1cc9178 +size 330 diff --git a/public/packs/manifest.json.gz b/public/packs/manifest.json.gz new file mode 100644 index 00000000..dd2dc7c2 --- /dev/null +++ b/public/packs/manifest.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9aee952354edb8ae47a200f1059beaf3ab588f0fef6e60dcd983de8808ed3351 +size 365 diff --git a/public/packs/media/fonts/forkawesome-webfont-2dfb5f36.woff b/public/packs/media/fonts/forkawesome-webfont-2dfb5f36.woff new file mode 100644 index 00000000..8925c5d3 --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-2dfb5f36.woff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53ab31062cf740aa76615d2c98aea80b177845d7ed95de45889b3824b0e1597c +size 115148 diff --git a/public/packs/media/fonts/forkawesome-webfont-2dfb5f36.woff.br b/public/packs/media/fonts/forkawesome-webfont-2dfb5f36.woff.br new file mode 100644 index 00000000..df2c95b5 --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-2dfb5f36.woff.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90b60c42bcbd106f5658710acd84f60307a00f0de10b921eeb5627590b429858 +size 115153 diff --git a/public/packs/media/fonts/forkawesome-webfont-7c20758e.woff2 b/public/packs/media/fonts/forkawesome-webfont-7c20758e.woff2 new file mode 100644 index 00000000..52865bdb --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-7c20758e.woff2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84422de97eb1cf27bcb9bca4f3fbb18f3ebc711647b09c68292f5f43c89d5064 +size 91624 diff --git a/public/packs/media/fonts/forkawesome-webfont-7c20758e.woff2.br b/public/packs/media/fonts/forkawesome-webfont-7c20758e.woff2.br new file mode 100644 index 00000000..1d640da9 --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-7c20758e.woff2.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a76b02ceece82f286d306ed0988f8c220066ecce509241a425245950e4e4c839 +size 91629 diff --git a/public/packs/media/fonts/forkawesome-webfont-86541105.svg b/public/packs/media/fonts/forkawesome-webfont-86541105.svg new file mode 100644 index 00000000..af45aadc --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-86541105.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:505e7f9fe61ba6d17d8514b5d0c3a75166dab1b57527e2d3a3baf2047624ba93 +size 480784 diff --git a/public/packs/media/fonts/forkawesome-webfont-86541105.svg.br b/public/packs/media/fonts/forkawesome-webfont-86541105.svg.br new file mode 100644 index 00000000..bad26711 --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-86541105.svg.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7aee80d4013bd5a42f4035b0dc08a2fc903dae87f04182d9a86db53b95c9add +size 143247 diff --git a/public/packs/media/fonts/forkawesome-webfont-86541105.svg.br.br b/public/packs/media/fonts/forkawesome-webfont-86541105.svg.br.br new file mode 100644 index 00000000..344bb103 --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-86541105.svg.br.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e03d221ca2c49b7099c550f8f33462365d285809e5a0b1e000c6c52e6982996a +size 143252 diff --git a/public/packs/media/fonts/forkawesome-webfont-86541105.svg.gz b/public/packs/media/fonts/forkawesome-webfont-86541105.svg.gz new file mode 100644 index 00000000..0676bab5 --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-86541105.svg.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49664085aa6a37335724f0288531562f352e81d10fdfeadd4a8ed0764eae2f50 +size 160947 diff --git a/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot b/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot new file mode 100644 index 00000000..37e93df5 --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d96fdd7d6854cf875ce3090e017b0078ae2f7e923763bcbb90748a01c6fb7fd +size 188946 diff --git a/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot.br b/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot.br new file mode 100644 index 00000000..22d6e19f --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4283befabbc5c3bfec093dd89b65ad0063a3581e45bbfbaf9b40a4d178cd5e1 +size 110762 diff --git a/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot.br.br b/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot.br.br new file mode 100644 index 00000000..e6ebb6ac --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot.br.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2adacd487b97dcd4da3bc37328482c809a0d910353990de13b228dd1aa4710a +size 110767 diff --git a/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot.gz b/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot.gz new file mode 100644 index 00000000..5bb32cdc --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-e182ad6d.eot.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:413b68fbd4bc71d5aa98410c6f8048f971b55e7d824180d0fab665cc55c3d9a3 +size 115772 diff --git a/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf b/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf new file mode 100644 index 00000000..e6380d0e --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e4beb40f0cc19ec55f2ab741d42e806fc6155ccf6e40e50965196c0bcc6aba4 +size 188756 diff --git a/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf.br b/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf.br new file mode 100644 index 00000000..87ab54e0 --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e01bc03dc20b0d760488b20f4f5a408dddd0554fb8c23d7ace34ab1698c4c807 +size 110703 diff --git a/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf.br.br b/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf.br.br new file mode 100644 index 00000000..e633502b --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf.br.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba3d7e97443577de6e23b5936de2e1a6f35d9e6335b069d8071e6269afc78246 +size 110708 diff --git a/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf.gz b/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf.gz new file mode 100644 index 00000000..44d9de5a --- /dev/null +++ b/public/packs/media/fonts/forkawesome-webfont-ee4d8bfd.ttf.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26c357b09eb784753afcd35a1aa47fe65378a52c95db0496e98dfb999c9ab1b8 +size 115685 diff --git a/public/packs/media/images/layers-2x-8f2c4d11.png b/public/packs/media/images/layers-2x-8f2c4d11.png new file mode 100644 index 00000000..d3cf7e52 --- /dev/null +++ b/public/packs/media/images/layers-2x-8f2c4d11.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:066daca850d8ffbef007af00b06eac0015728dee279c51f3cb6c716df7c42edf +size 1259 diff --git a/public/packs/media/images/layers-2x-8f2c4d11.png.br b/public/packs/media/images/layers-2x-8f2c4d11.png.br new file mode 100644 index 00000000..cda7f53d --- /dev/null +++ b/public/packs/media/images/layers-2x-8f2c4d11.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47119b2b0eb7d15fa47527efc1fa04713d80990ed6a7f2e2f58f91a8cddd1b03 +size 1264 diff --git a/public/packs/media/images/layers-416d9136.png b/public/packs/media/images/layers-416d9136.png new file mode 100644 index 00000000..7b44754c --- /dev/null +++ b/public/packs/media/images/layers-416d9136.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dbbe9d028e292f36fcba8f8b3a28d5e8932754fc2215b9ac69e4cdecf5107c6 +size 696 diff --git a/public/packs/media/images/layers-416d9136.png.br b/public/packs/media/images/layers-416d9136.png.br new file mode 100644 index 00000000..21812b3e --- /dev/null +++ b/public/packs/media/images/layers-416d9136.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e255d97d12f227f9d2fabd481fa56d89bf33b47a4d108b031e7b96ddc0fb0066 +size 701 diff --git a/public/packs/media/images/marker-icon-2b3e1faf.png b/public/packs/media/images/marker-icon-2b3e1faf.png new file mode 100644 index 00000000..b1789c76 --- /dev/null +++ b/public/packs/media/images/marker-icon-2b3e1faf.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437 +size 1466 diff --git a/public/packs/media/images/marker-icon-2b3e1faf.png.br b/public/packs/media/images/marker-icon-2b3e1faf.png.br new file mode 100644 index 00000000..62bba2a0 --- /dev/null +++ b/public/packs/media/images/marker-icon-2b3e1faf.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d73a1e81d4d7b880342c1fa48caeb6cdb56cc941ad18000d932dfaec8ba1086 +size 1471 diff --git a/public/packs/media/images/marker-icon-2x-680f69f3.png b/public/packs/media/images/marker-icon-2x-680f69f3.png new file mode 100644 index 00000000..09e8445f --- /dev/null +++ b/public/packs/media/images/marker-icon-2x-680f69f3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00179c4c1ee830d3a108412ae0d294f55776cfeb085c60129a39aa6fc4ae2528 +size 2464 diff --git a/public/packs/media/images/marker-icon-2x-680f69f3.png.br b/public/packs/media/images/marker-icon-2x-680f69f3.png.br new file mode 100644 index 00000000..585d2e48 --- /dev/null +++ b/public/packs/media/images/marker-icon-2x-680f69f3.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4f6ce09c4c3a59cfac5c03a03de02b170d35cfefd7e1247a317dc772c889395 +size 2469 diff --git a/public/packs/media/images/marker-shadow-a0c6cc14.png b/public/packs/media/images/marker-shadow-a0c6cc14.png new file mode 100644 index 00000000..dc111216 --- /dev/null +++ b/public/packs/media/images/marker-shadow-a0c6cc14.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da +size 618 diff --git a/public/packs/media/images/marker-shadow-a0c6cc14.png.br b/public/packs/media/images/marker-shadow-a0c6cc14.png.br new file mode 100644 index 00000000..940c4d9b --- /dev/null +++ b/public/packs/media/images/marker-shadow-a0c6cc14.png.br @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8758eb0644225f0a3c8e03fb1dbe319f6427b4116a538084f18ba5a6dcd65eb2 +size 623