mirror of
https://0xacab.org/sutty/sutty
synced 2025-01-19 16:43:39 +00:00
Merge branch 'rails' into issue-2123
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
commit
63ee0ec8f7
95 changed files with 2414 additions and 294 deletions
|
@ -1,7 +1,9 @@
|
||||||
|
# pwgen -1 32
|
||||||
|
RAILS_MASTER_KEY=11111111111111111111111111111111
|
||||||
RAILS_GROUPS=assets
|
RAILS_GROUPS=assets
|
||||||
DELEGATE=athshe.sutty.nl
|
DELEGATE=athshe.sutty.nl
|
||||||
HAINISH=../haini.sh/haini.sh
|
HAINISH=../haini.sh/haini.sh
|
||||||
DATABASE=
|
DATABASE_URL=postgres://suttier@postgresql.sutty.local/sutty
|
||||||
RAILS_ENV=development
|
RAILS_ENV=development
|
||||||
IMAP_SERVER=
|
IMAP_SERVER=
|
||||||
DEFAULT_FROM=
|
DEFAULT_FROM=
|
||||||
|
|
71
.woodpecker.yml
Normal file
71
.woodpecker.yml
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
pipeline:
|
||||||
|
publish:
|
||||||
|
image: "docker.io/woodpeckerci/plugin-docker-buildx"
|
||||||
|
settings:
|
||||||
|
registry: "gitea.nulo.in"
|
||||||
|
username: "sutty"
|
||||||
|
repo: "gitea.nulo.in/sutty/panel"
|
||||||
|
tags:
|
||||||
|
- "${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}"
|
||||||
|
- "latest"
|
||||||
|
build_args:
|
||||||
|
- "RUBY_VERSION=${RUBY_VERSION}"
|
||||||
|
- "RUBY_PATCH=${RUBY_PATCH}"
|
||||||
|
- "ALPINE_VERSION=${ALPINE_VERSION}"
|
||||||
|
- "BASE_IMAGE=gitea.nulo.in/sutty/rails"
|
||||||
|
purge: false
|
||||||
|
secrets:
|
||||||
|
- "DOCKER_PASSWORD"
|
||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- "rails"
|
||||||
|
- "panel.sutty.nl"
|
||||||
|
event: "push"
|
||||||
|
path:
|
||||||
|
include:
|
||||||
|
- "Dockerfile"
|
||||||
|
- ".dockerignore"
|
||||||
|
assets:
|
||||||
|
image: "gitea.nulo.in/sutty/panel:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}"
|
||||||
|
commands:
|
||||||
|
- "apk add python2 dotenv openssh-client brotli"
|
||||||
|
- "install -d -m 700 ~/.ssh/"
|
||||||
|
- "echo \"$${KNOWN_HOSTS}\" | base64 -d >> ~/.ssh/known_hosts"
|
||||||
|
- "chmod 600 ~/.ssh/known_hosts"
|
||||||
|
- "eval $(ssh-agent -s)"
|
||||||
|
- "echo \"$${SSH_KEY}\" | base64 -d | ssh-add -"
|
||||||
|
- "ssh $${ORIGIN%:*}"
|
||||||
|
- "git config user.name Woodpecker"
|
||||||
|
- "git config user.email ci@sutty.coop.ar"
|
||||||
|
- "git remote add upstream $${ORIGIN}"
|
||||||
|
- "git checkout -B ${CI_COMMIT_BRANCH}"
|
||||||
|
- "mv config/credentials.yml.enc.ci config/credentials.yml.enc"
|
||||||
|
- "yarn"
|
||||||
|
- "cp .env.example .env"
|
||||||
|
- "dotenv bundle install --path=vendor"
|
||||||
|
- "dotenv RAILS_ENV=production bundle exec rails webpacker:clobber"
|
||||||
|
- "dotenv RAILS_ENV=production bundle exec rails assets:precompile"
|
||||||
|
- "dotenv RAILS_ENV=production bundle exec rails assets:clean"
|
||||||
|
- "find public -type f -print0 | xargs -r0 brotli -k9f"
|
||||||
|
- "git add public && git commit -m \"ci: assets [skip ci]\""
|
||||||
|
- "git pull upstream ${CI_COMMIT_BRANCH}"
|
||||||
|
- "git push upstream ${CI_COMMIT_BRANCH}"
|
||||||
|
secrets:
|
||||||
|
- "SSH_KEY"
|
||||||
|
- "KNOWN_HOSTS"
|
||||||
|
- "ORIGIN"
|
||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- "rails"
|
||||||
|
- "panel.sutty.nl"
|
||||||
|
path:
|
||||||
|
include:
|
||||||
|
- "app/assets/**/*"
|
||||||
|
- "app/javascript/**/*"
|
||||||
|
- "package.json"
|
||||||
|
- "yarn.lock"
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- ALPINE_VERSION: "3.14.10"
|
||||||
|
RUBY_VERSION: "2.7"
|
||||||
|
RUBY_PATCH: "8"
|
13
Dockerfile
13
Dockerfile
|
@ -1,5 +1,9 @@
|
||||||
FROM registry.nulo.in/sutty/rails:3.13.6-2.7.5
|
ARG RUBY_VERSION=2.7
|
||||||
ARG PANDOC_VERSION=2.17.1.1
|
ARG RUBY_PATCH=6
|
||||||
|
ARG ALPINE_VERSION=3.13.10
|
||||||
|
ARG BASE_IMAGE=registry.nulo.in/sutty/rails
|
||||||
|
FROM ${BASE_IMAGE}:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}
|
||||||
|
ARG PANDOC_VERSION=2.18
|
||||||
ENV RAILS_ENV production
|
ENV RAILS_ENV production
|
||||||
|
|
||||||
# Instalar las dependencias, separamos la librería de base de datos para
|
# Instalar las dependencias, separamos la librería de base de datos para
|
||||||
|
@ -10,13 +14,16 @@ ENV RAILS_ENV production
|
||||||
# principal
|
# principal
|
||||||
RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \
|
RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \
|
||||||
rsync git jpegoptim vips tectonic oxipng git-lfs openssh-client \
|
rsync git jpegoptim vips tectonic oxipng git-lfs openssh-client \
|
||||||
yarn daemonize ruby-webrick
|
yarn daemonize ruby-webrick postgresql-client dateutils file
|
||||||
|
|
||||||
RUN gem install --no-document --no-user-install foreman
|
RUN gem install --no-document --no-user-install foreman
|
||||||
RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc
|
RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc
|
||||||
|
RUN apk add npm && npm install -g pnpm@~7 && apk del npm
|
||||||
|
|
||||||
COPY ./monit.conf /etc/monit.d/sutty.conf
|
COPY ./monit.conf /etc/monit.d/sutty.conf
|
||||||
|
|
||||||
|
RUN apk add npm && npm install -g pnpm && apk del npm
|
||||||
|
|
||||||
VOLUME "/srv"
|
VOLUME "/srv"
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -23,6 +23,7 @@ if ENV['RAILS_GROUPS']&.split(',')&.include? 'assets'
|
||||||
end
|
end
|
||||||
|
|
||||||
gem 'nokogiri'
|
gem 'nokogiri'
|
||||||
|
gem 'rgl'
|
||||||
|
|
||||||
# Turbolinks makes navigating your web application faster. Read more:
|
# Turbolinks makes navigating your web application faster. Read more:
|
||||||
# https://github.com/turbolinks/turbolinks
|
# https://github.com/turbolinks/turbolinks
|
||||||
|
@ -38,6 +39,8 @@ gem 'commonmarker'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-i18n'
|
gem 'devise-i18n'
|
||||||
gem 'devise_invitable'
|
gem 'devise_invitable'
|
||||||
|
gem 'distributed-press-api-client', '~> 0.2.3'
|
||||||
|
gem 'njalla-api-client', '~> 0.2.0'
|
||||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||||
gem 'exception_notification'
|
gem 'exception_notification'
|
||||||
gem 'fast_blank'
|
gem 'fast_blank'
|
||||||
|
|
51
Gemfile.lock
51
Gemfile.lock
|
@ -115,6 +115,7 @@ GEM
|
||||||
xpath (>= 2.0, < 4.0)
|
xpath (>= 2.0, < 4.0)
|
||||||
chartkick (4.1.2)
|
chartkick (4.1.2)
|
||||||
childprocess (4.1.0)
|
childprocess (4.1.0)
|
||||||
|
climate_control (1.2.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
colorator (1.1.0)
|
colorator (1.1.0)
|
||||||
commonmarker (0.21.2-x86_64-linux-musl)
|
commonmarker (0.21.2-x86_64-linux-musl)
|
||||||
|
@ -153,12 +154,45 @@ GEM
|
||||||
devise_invitable (2.0.5)
|
devise_invitable (2.0.5)
|
||||||
actionmailer (>= 5.0)
|
actionmailer (>= 5.0)
|
||||||
devise (>= 4.6)
|
devise (>= 4.6)
|
||||||
|
distributed-press-api-client (0.2.2)
|
||||||
|
addressable (~> 2.3, >= 2.3.0)
|
||||||
|
climate_control
|
||||||
|
dry-schema
|
||||||
|
httparty (~> 0.18)
|
||||||
|
json (~> 2.1, >= 2.1.0)
|
||||||
|
jwt (~> 2.6.0)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
dotenv-rails (2.7.6)
|
dotenv-rails (2.7.6)
|
||||||
dotenv (= 2.7.6)
|
dotenv (= 2.7.6)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
down (5.2.4)
|
down (5.2.4)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
|
dry-configurable (1.0.1)
|
||||||
|
dry-core (~> 1.0, < 2)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-core (1.0.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-inflector (1.0.0)
|
||||||
|
dry-initializer (3.1.1)
|
||||||
|
dry-logic (1.5.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.0, < 2)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-schema (1.13.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-configurable (~> 1.0, >= 1.0.1)
|
||||||
|
dry-core (~> 1.0, < 2)
|
||||||
|
dry-initializer (~> 3.0)
|
||||||
|
dry-logic (>= 1.5, < 2)
|
||||||
|
dry-types (>= 1.7, < 2)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-types (1.7.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.0, < 2)
|
||||||
|
dry-inflector (~> 1.0, < 2)
|
||||||
|
dry-logic (>= 1.4, < 2)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
ed25519 (1.2.4-x86_64-linux-musl)
|
ed25519 (1.2.4-x86_64-linux-musl)
|
||||||
em-websocket (0.5.3)
|
em-websocket (0.5.3)
|
||||||
eventmachine (>= 0.12.9)
|
eventmachine (>= 0.12.9)
|
||||||
|
@ -216,8 +250,8 @@ GEM
|
||||||
thor
|
thor
|
||||||
hiredis (0.6.3-x86_64-linux-musl)
|
hiredis (0.6.3-x86_64-linux-musl)
|
||||||
http_parser.rb (0.8.0-x86_64-linux-musl)
|
http_parser.rb (0.8.0-x86_64-linux-musl)
|
||||||
httparty (0.18.1)
|
httparty (0.21.0)
|
||||||
mime-types (~> 3.0)
|
mini_mime (>= 1.0.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
i18n (1.8.11)
|
i18n (1.8.11)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
@ -292,6 +326,7 @@ GEM
|
||||||
jekyll-write-and-commit-changes (0.2.1)
|
jekyll-write-and-commit-changes (0.2.1)
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
rugged (~> 1)
|
rugged (~> 1)
|
||||||
|
jwt (2.6.0)
|
||||||
kaminari (1.2.1)
|
kaminari (1.2.1)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.1)
|
kaminari-actionview (= 1.2.1)
|
||||||
|
@ -352,7 +387,11 @@ GEM
|
||||||
nokogiri (1.12.5-x86_64-linux-musl)
|
nokogiri (1.12.5-x86_64-linux-musl)
|
||||||
mini_portile2 (~> 2.6.1)
|
mini_portile2 (~> 2.6.1)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
|
njalla-api-client (0.2.0)
|
||||||
|
dry-schema
|
||||||
|
httparty (~> 0.18)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
pairing_heap (3.0.0)
|
||||||
parallel (1.21.0)
|
parallel (1.21.0)
|
||||||
parser (3.0.2.0)
|
parser (3.0.2.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
|
@ -443,6 +482,10 @@ GEM
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
|
rgl (0.6.2)
|
||||||
|
pairing_heap (>= 0.3.0)
|
||||||
|
rexml (~> 3.2, >= 3.2.4)
|
||||||
|
stream (~> 0.5.3)
|
||||||
rouge (3.26.1)
|
rouge (3.26.1)
|
||||||
rubocop (1.23.0)
|
rubocop (1.23.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
|
@ -510,6 +553,7 @@ GEM
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sqlite3 (1.4.2-x86_64-linux-musl)
|
sqlite3 (1.4.2-x86_64-linux-musl)
|
||||||
stackprof (0.2.17-x86_64-linux-musl)
|
stackprof (0.2.17-x86_64-linux-musl)
|
||||||
|
stream (0.5.5)
|
||||||
sucker_punch (3.0.1)
|
sucker_punch (3.0.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
sutty-archives (2.5.4)
|
sutty-archives (2.5.4)
|
||||||
|
@ -578,6 +622,7 @@ DEPENDENCIES
|
||||||
devise
|
devise
|
||||||
devise-i18n
|
devise-i18n
|
||||||
devise_invitable
|
devise_invitable
|
||||||
|
distributed-press-api-client (~> 0.2.3)
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
down
|
down
|
||||||
ed25519
|
ed25519
|
||||||
|
@ -612,6 +657,7 @@ DEPENDENCIES
|
||||||
mini_magick
|
mini_magick
|
||||||
mobility
|
mobility
|
||||||
net-ssh
|
net-ssh
|
||||||
|
njalla-api-client
|
||||||
nokogiri
|
nokogiri
|
||||||
pg
|
pg
|
||||||
pg_search
|
pg_search
|
||||||
|
@ -626,6 +672,7 @@ DEPENDENCIES
|
||||||
rails_warden
|
rails_warden
|
||||||
redis
|
redis
|
||||||
redis-rails
|
redis-rails
|
||||||
|
rgl
|
||||||
rollups!
|
rollups!
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubyzip
|
rubyzip
|
||||||
|
|
9
Procfile
9
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
|
cleanup: bundle exec rake cleanup:everything
|
||||||
stats: bundle exec rake stats:process_all
|
stats: bundle exec rake stats:process_all
|
||||||
|
distributed_press_renew_tokens: bundle exec rake distributed_press:tokens:renew
|
||||||
|
|
|
@ -25,6 +25,10 @@ $spacers: (
|
||||||
2-plus: 0.75rem
|
2-plus: 0.75rem
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$sizes: (
|
||||||
|
"70ch": 70ch,
|
||||||
|
);
|
||||||
|
|
||||||
@import "bootstrap";
|
@import "bootstrap";
|
||||||
@import "editor";
|
@import "editor";
|
||||||
|
|
||||||
|
@ -154,6 +158,12 @@ ol.breadcrumb {
|
||||||
transition: all 3s;
|
transition: all 3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
legend {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mapable,
|
.mapable,
|
||||||
.taggable {
|
.taggable {
|
||||||
.input-map,
|
.input-map,
|
||||||
|
@ -404,6 +414,8 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
|
||||||
@each $prop, $abbrev in (width: w, height: h) {
|
@each $prop, $abbrev in (width: w, height: h) {
|
||||||
@each $size, $length in $sizes {
|
@each $size, $length in $sizes {
|
||||||
.#{$abbrev}-#{$grid-breakpoint}-#{$size} { #{$prop}: $length !important; }
|
.#{$abbrev}-#{$grid-breakpoint}-#{$size} { #{$prop}: $length !important; }
|
||||||
|
.min-#{$abbrev}-#{$grid-breakpoint}-#{$size} { min-#{$prop}: $length !important; }
|
||||||
|
.max-#{$abbrev}-#{$grid-breakpoint}-#{$size} { max-#{$prop}: $length !important; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,31 +12,6 @@ module Api
|
||||||
render json: sites_names + alternative_names + api_names + www_names
|
render json: sites_names + alternative_names + api_names + www_names
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def canonicalize(name)
|
def canonicalize(name)
|
||||||
|
|
|
@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
before_action :prepare_exception_notifier
|
before_action :prepare_exception_notifier
|
||||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||||
|
before_action :notify_unconfirmed_email, unless: :devise_controller?
|
||||||
around_action :set_locale
|
around_action :set_locale
|
||||||
|
|
||||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||||
|
@ -27,6 +28,15 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def notify_unconfirmed_email
|
||||||
|
return unless current_usuarie
|
||||||
|
return if current_usuarie.confirmed?
|
||||||
|
|
||||||
|
I18n.with_locale(current_usuarie.lang) do
|
||||||
|
flash[:notice] ||= I18n.t('devise.registrations.signed_up')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def uuid?(string)
|
def uuid?(string)
|
||||||
/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string
|
/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string
|
||||||
end
|
end
|
||||||
|
@ -84,6 +94,7 @@ class ApplicationController < ActionController::Base
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def configure_permitted_parameters
|
def configure_permitted_parameters
|
||||||
|
devise_parameter_sanitizer.permit(:sign_up, keys: Usuarie::CONSENT_FIELDS)
|
||||||
devise_parameter_sanitizer.permit(:account_update, keys: %i[lang])
|
devise_parameter_sanitizer.permit(:account_update, keys: %i[lang])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ class SitesController < ApplicationController
|
||||||
# Ver un listado de sitios
|
# Ver un listado de sitios
|
||||||
def index
|
def index
|
||||||
authorize Site
|
authorize Site
|
||||||
@sites = current_usuarie.sites.order(:title)
|
@sites = current_usuarie.sites.order(updated_at: :desc)
|
||||||
|
|
||||||
fresh_when @sites
|
fresh_when @sites
|
||||||
end
|
end
|
||||||
|
@ -28,8 +28,6 @@ class SitesController < ApplicationController
|
||||||
|
|
||||||
@site = Site.new
|
@site = Site.new
|
||||||
authorize @site
|
authorize @site
|
||||||
|
|
||||||
@site.deploys.build type: 'DeployLocal'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
|
@ -47,7 +47,7 @@ class UsuariesController < ApplicationController
|
||||||
@usuarie = Usuarie.find(params[:usuarie_id])
|
@usuarie = Usuarie.find(params[:usuarie_id])
|
||||||
|
|
||||||
if @site.usuaries.count > 1
|
if @site.usuaries.count > 1
|
||||||
@usuarie.rol_for_site(@site).update_attribute :rol, 'invitade'
|
@usuarie.rol_for_site(@site).update_attribute :rol, Rol::INVITADE
|
||||||
else
|
else
|
||||||
flash[:warning] = I18n.t('usuaries.index.demote.denied')
|
flash[:warning] = I18n.t('usuaries.index.demote.denied')
|
||||||
end
|
end
|
||||||
|
@ -61,7 +61,7 @@ class UsuariesController < ApplicationController
|
||||||
authorize SiteUsuarie.new(@site, current_usuarie)
|
authorize SiteUsuarie.new(@site, current_usuarie)
|
||||||
|
|
||||||
@usuarie = Usuarie.find(params[:usuarie_id])
|
@usuarie = Usuarie.find(params[:usuarie_id])
|
||||||
@usuarie.rol_for_site(@site).update_attribute :rol, 'usuarie'
|
@usuarie.rol_for_site(@site).update_attribute :rol, Rol::USUARIE
|
||||||
|
|
||||||
redirect_to site_usuaries_path
|
redirect_to site_usuaries_path
|
||||||
end
|
end
|
||||||
|
@ -72,6 +72,8 @@ class UsuariesController < ApplicationController
|
||||||
site_usuarie = SiteUsuarie.new(@site, current_usuarie)
|
site_usuarie = SiteUsuarie.new(@site, current_usuarie)
|
||||||
authorize site_usuarie
|
authorize site_usuarie
|
||||||
|
|
||||||
|
params[:invite_as] = invite_as
|
||||||
|
|
||||||
@policy = policy(site_usuarie)
|
@policy = policy(site_usuarie)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -156,12 +158,14 @@ class UsuariesController < ApplicationController
|
||||||
|
|
||||||
# El tipo de invitación que tenemos que enviar, si alguien mandó
|
# El tipo de invitación que tenemos que enviar, si alguien mandó
|
||||||
# cualquier cosa, usamos el privilegio menor.
|
# cualquier cosa, usamos el privilegio menor.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
def invited_as
|
def invited_as
|
||||||
if Rol::ROLES.include?(params[:invited_as])
|
Rol.role?(params[:invited_as]) ? params[:invited_as] : Rol::INVITADE
|
||||||
params[:invited_as]
|
|
||||||
else
|
|
||||||
'invitade'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def invite_as
|
||||||
|
Rol.role?(params[:invite_as]&.singularize) ? params[:invite_as] : Rol::INVITADE.pluralize
|
||||||
end
|
end
|
||||||
|
|
||||||
def site
|
def site
|
||||||
|
|
|
@ -30,79 +30,90 @@ class DeployJob < ApplicationJob
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@deployed = {}
|
||||||
@site.update status: 'building'
|
@site.update status: 'building'
|
||||||
# Asegurarse que DeployLocal sea el primero!
|
@site.deployment_list.each do |d|
|
||||||
@deployed = {
|
begin
|
||||||
deploy_local: {
|
raise DeployException, 'Una dependencia falló' if failed_dependencies? d
|
||||||
status: deploy_locally,
|
|
||||||
seconds: deploy_local.build_stats.last.seconds,
|
|
||||||
size: deploy_local.size,
|
|
||||||
urls: [deploy_local.url]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# No es opcional
|
status = d.deploy(output: @output)
|
||||||
unless @deployed[:deploy_local][:status]
|
seconds = d.build_stats.last.try(:seconds) || 0
|
||||||
# Hacer fallar la tarea
|
size = d.size
|
||||||
raise DeployException, "#{@site.name}: Falló la compilación"
|
urls = d.respond_to?(:urls) ? d.urls : [d.url].compact
|
||||||
|
rescue StandardError => e
|
||||||
|
status = false
|
||||||
|
seconds ||= 0
|
||||||
|
size ||= 0
|
||||||
|
# XXX: Hace que se vea la tabla
|
||||||
|
urls ||= [nil]
|
||||||
|
|
||||||
|
notify_exception e, d
|
||||||
end
|
end
|
||||||
|
|
||||||
deploy_others
|
@deployed[d.type.underscore.to_sym] = {
|
||||||
|
status: status,
|
||||||
|
seconds: seconds,
|
||||||
|
size: size,
|
||||||
|
urls: urls
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless @output
|
||||||
|
|
||||||
|
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)
|
||||||
rescue DeployTimedOutException => e
|
rescue DeployTimedOutException => e
|
||||||
notify_exception e
|
notify_exception e
|
||||||
rescue DeployException => e
|
|
||||||
notify_exception e, deploy_local
|
|
||||||
ensure
|
ensure
|
||||||
@site&.update status: 'waiting'
|
if @site.present?
|
||||||
|
@site.update status: 'waiting'
|
||||||
|
|
||||||
notify_usuaries if notify
|
notify_usuaries if notify
|
||||||
|
|
||||||
|
puts "\a" if @output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
private
|
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 :exception [StandardError]
|
||||||
# @param :deploy [Deploy]
|
# @param :deploy [Deploy]
|
||||||
def notify_exception(exception, deploy = nil)
|
def notify_exception(exception, deploy = nil)
|
||||||
data = {
|
data = {
|
||||||
site: @site.id,
|
site: @site.id,
|
||||||
deploy: deploy&.type,
|
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)
|
ExceptionNotifier.notify_exception(exception, data: data)
|
||||||
end
|
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
|
def notify_usuaries
|
||||||
@site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
|
@site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
|
||||||
DeployMailer.with(usuarie: usuarie, site: @site.id)
|
DeployMailer.with(usuarie: usuarie, site: @site.id)
|
||||||
|
|
|
@ -16,7 +16,7 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
# @param [Hash] opciones de ExceptionNotifier
|
# @param [Hash] opciones de ExceptionNotifier
|
||||||
def perform(exception, **options)
|
def perform(exception, **options)
|
||||||
@exception = exception
|
@exception = exception
|
||||||
@options = options
|
@options = fix_options options
|
||||||
@issue_data = { count: 1 }
|
@issue_data = { count: 1 }
|
||||||
# Necesitamos saber si el issue ya existía
|
# Necesitamos saber si el issue ya existía
|
||||||
@cached = false
|
@cached = false
|
||||||
|
@ -37,7 +37,7 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
unless @issue['iid']
|
if @issue['iid'].blank? && issue_data[:issue].blank?
|
||||||
Rails.cache.delete(cache_key)
|
Rails.cache.delete(cache_key)
|
||||||
raise GitlabNotifierError, @issue.dig('message', 'title')&.join(', ')
|
raise GitlabNotifierError, @issue.dig('message', 'title')&.join(', ')
|
||||||
end
|
end
|
||||||
|
@ -61,9 +61,9 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
Rails.cache.write(cache_key, issue_data)
|
Rails.cache.write(cache_key, issue_data)
|
||||||
# Si este trabajo genera una excepción va a entrar en un loop, así que
|
# Si este trabajo genera una excepción va a entrar en un loop, así que
|
||||||
# la notificamos por correo
|
# la notificamos por correo
|
||||||
rescue Exception => e
|
rescue StandardError => e
|
||||||
email_notification.call(e)
|
email_notification.call(e, data: @issue)
|
||||||
email_notification.call(exception, options)
|
email_notification.call(exception, data: @options)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -84,10 +84,15 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
exception.class.name,
|
exception.class.name,
|
||||||
Digest::SHA1.hexdigest(exception.message),
|
Digest::SHA1.hexdigest(exception.message),
|
||||||
Digest::SHA1.hexdigest(backtrace&.first.to_s),
|
Digest::SHA1.hexdigest(backtrace&.first.to_s),
|
||||||
Digest::SHA1.hexdigest(options.dig(:data, :params, 'errors').to_s)
|
Digest::SHA1.hexdigest(errors.to_s)
|
||||||
].join('/')
|
].join('/')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Array]
|
||||||
|
def errors
|
||||||
|
options.dig(:data, :params, 'errors') || []
|
||||||
|
end
|
||||||
|
|
||||||
# Define si es una excepción de javascript o local
|
# Define si es una excepción de javascript o local
|
||||||
#
|
#
|
||||||
# @see BacktraceJob
|
# @see BacktraceJob
|
||||||
|
@ -126,6 +131,7 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def body
|
def body
|
||||||
@body ||= ''.dup.tap do |b|
|
@body ||= ''.dup.tap do |b|
|
||||||
|
b << log_section
|
||||||
b << request_section
|
b << request_section
|
||||||
b << javascript_footer
|
b << javascript_footer
|
||||||
b << data_section
|
b << data_section
|
||||||
|
@ -162,14 +168,16 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
|
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def log_section
|
def log_section
|
||||||
return '' unless options[:log]
|
return '' unless options.dig(:data, :log)
|
||||||
|
|
||||||
<<~LOG
|
<<~LOG
|
||||||
# Log
|
|
||||||
|
# Build log
|
||||||
|
|
||||||
```
|
```
|
||||||
#{options[:log]}
|
#{options[:data].delete(:log)}
|
||||||
```
|
```
|
||||||
|
|
||||||
LOG
|
LOG
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -257,8 +265,8 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
|
|
||||||
## Data
|
## Data
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
#{pp options[:data]}
|
#{options[:data].to_yaml}
|
||||||
```
|
```
|
||||||
|
|
||||||
DATA
|
DATA
|
||||||
|
@ -279,4 +287,16 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
def url
|
def url
|
||||||
@url ||= request&.url || options.dig(:data, :params, 'context', 'url')
|
@url ||= request&.url || options.dig(:data, :params, 'context', 'url')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Define llaves necesarias
|
||||||
|
#
|
||||||
|
# @param :options [Hash]
|
||||||
|
# @return [Hash]
|
||||||
|
def fix_options(options)
|
||||||
|
options = { data: options } unless options.is_a? Hash
|
||||||
|
options[:data] ||= {}
|
||||||
|
options[:data][:params] ||= {}
|
||||||
|
|
||||||
|
options
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
17
app/jobs/renew_distributed_press_tokens_job.rb
Normal file
17
app/jobs/renew_distributed_press_tokens_job.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Renueva los tokens de Distributed Press antes que se venzan,
|
||||||
|
# activando los callbacks que hacen que se refresque el token.
|
||||||
|
class RenewDistributedPressTokensJob < ApplicationJob
|
||||||
|
# Renueva todos los tokens a punto de vencer o informa el error sin
|
||||||
|
# detener la tarea si algo pasa.
|
||||||
|
def perform
|
||||||
|
DistributedPressPublisher.with_about_to_expire_tokens.find_each do |publisher|
|
||||||
|
publisher.touch
|
||||||
|
rescue DistributedPress::V1::Error => e
|
||||||
|
data = { instance: publisher.instance, expires_at: publisher.client.token.expires_at }
|
||||||
|
|
||||||
|
ExceptionNotifier.notify_exception(e, data: data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,11 +4,6 @@ module ActiveStorage
|
||||||
class Service
|
class Service
|
||||||
# Sube los archivos a cada repositorio y los agrega al LFS de su
|
# Sube los archivos a cada repositorio y los agrega al LFS de su
|
||||||
# repositorio git.
|
# repositorio git.
|
||||||
#
|
|
||||||
# @todo: Implementar LFS. No nos gusta mucho la idea porque duplica
|
|
||||||
# el espacio en disco, pero es la única forma que tenemos (hasta que
|
|
||||||
# implementemos IPFS) para poder transferir los archivos junto con el
|
|
||||||
# sitio.
|
|
||||||
class JekyllService < Service::DiskService
|
class JekyllService < Service::DiskService
|
||||||
# Genera un servicio para un sitio determinado
|
# Genera un servicio para un sitio determinado
|
||||||
#
|
#
|
||||||
|
@ -27,7 +22,10 @@ module ActiveStorage
|
||||||
# @param :checksum [String]
|
# @param :checksum [String]
|
||||||
def upload(key, io, checksum: nil, **)
|
def upload(key, io, checksum: nil, **)
|
||||||
instrument :upload, key: key, checksum: checksum do
|
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
|
ensure_integrity_of(key, checksum) if checksum
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -79,7 +77,7 @@ module ActiveStorage
|
||||||
# @param :key [String]
|
# @param :key [String]
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def filename_for(key)
|
def filename_for(key)
|
||||||
ActiveStorage::Blob.where(key: key).limit(1).pluck(:filename).first.tap do |filename|
|
blob_for(key).filename.to_s.tap do |filename|
|
||||||
raise ArgumentError, "Filename for key #{key} is blank" if filename.blank?
|
raise ArgumentError, "Filename for key #{key} is blank" if filename.blank?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -91,6 +89,15 @@ module ActiveStorage
|
||||||
def path_for(key)
|
def path_for(key)
|
||||||
File.join root, folder_for(key), filename_for(key)
|
File.join root, folder_for(key), filename_for(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Site]
|
||||||
|
def site
|
||||||
|
@site ||= Site.find_by_name(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def blob_for(key)
|
||||||
|
ActiveStorage::Blob.find_by(key: key, service_name: name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
21
app/lib/devise/failure_app_decorator.rb
Normal file
21
app/lib/devise/failure_app_decorator.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Devise
|
||||||
|
module FailureAppDecorator
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
include AbstractController::Callbacks
|
||||||
|
|
||||||
|
around_action :set_locale
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_locale(&action)
|
||||||
|
I18n.with_locale(session[:locale] || I18n.locale, &action)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Devise::FailureApp.include Devise::FailureAppDecorator
|
13
app/lib/hidden_service_client.rb
Normal file
13
app/lib/hidden_service_client.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'httparty'
|
||||||
|
|
||||||
|
class HiddenServiceClient
|
||||||
|
include HTTParty
|
||||||
|
|
||||||
|
base_uri ENV.fetch('HIDDEN_SERVICE', 'http://tor:3000')
|
||||||
|
|
||||||
|
def create(name)
|
||||||
|
self.class.get("/#{name}").body
|
||||||
|
end
|
||||||
|
end
|
14
app/models/code_of_conduct.rb
Normal file
14
app/models/code_of_conduct.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Códigos de conducta
|
||||||
|
class CodeOfConduct < ApplicationRecord
|
||||||
|
extend Mobility
|
||||||
|
|
||||||
|
translates :title, type: :string, locale_accessors: true
|
||||||
|
translates :description, type: :text, locale_accessors: true
|
||||||
|
translates :content, type: :text, locale_accessors: true
|
||||||
|
|
||||||
|
validates :title, presence: true, uniqueness: true
|
||||||
|
validates :description, presence: true
|
||||||
|
validates :content, presence: true
|
||||||
|
end
|
26
app/models/concerns/usuarie/consent.rb
Normal file
26
app/models/concerns/usuarie/consent.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Usuarie
|
||||||
|
# Gestiona los campos de consentimiento
|
||||||
|
module Consent
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
CONSENT_FIELDS = %i[privacy_policy_accepted terms_of_service_accepted code_of_conduct_accepted available_for_feedback_accepted]
|
||||||
|
|
||||||
|
CONSENT_FIELDS.each do |field|
|
||||||
|
attribute field, :boolean
|
||||||
|
end
|
||||||
|
|
||||||
|
before_save :update_consent_fields!
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def update_consent_fields!
|
||||||
|
CONSENT_FIELDS.each do |field|
|
||||||
|
send(:"#{field}_at=", Time.now) if send(field).present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'open3'
|
require 'open3'
|
||||||
|
|
||||||
# Este modelo implementa los distintos tipos de alojamiento que provee
|
# Este modelo implementa los distintos tipos de alojamiento que provee
|
||||||
# Sutty.
|
# Sutty.
|
||||||
#
|
#
|
||||||
|
@ -11,6 +12,9 @@ class Deploy < ApplicationRecord
|
||||||
belongs_to :site
|
belongs_to :site
|
||||||
has_many :build_stats, dependent: :destroy
|
has_many :build_stats, dependent: :destroy
|
||||||
|
|
||||||
|
DEPENDENCIES = []
|
||||||
|
SOFT_DEPENDENCIES = []
|
||||||
|
|
||||||
def deploy(**)
|
def deploy(**)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
@ -89,6 +93,20 @@ class Deploy < ApplicationRecord
|
||||||
r&.success?
|
r&.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Variables de entorno
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def local_env
|
||||||
|
@local_env ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Trae todas las dependencias
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def self.all_dependencies
|
||||||
|
self::DEPENDENCIES | self::SOFT_DEPENDENCIES
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# @param [String]
|
# @param [String]
|
||||||
|
@ -96,4 +114,12 @@ class Deploy < ApplicationRecord
|
||||||
def readable_cmd(cmd)
|
def readable_cmd(cmd)
|
||||||
cmd.split(' -', 2).first.tr(' ', '_')
|
cmd.split(' -', 2).first.tr(' ', '_')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def deploy_local
|
||||||
|
@deploy_local ||= site.deploys.find_by(type: 'DeployLocal')
|
||||||
|
end
|
||||||
|
|
||||||
|
def non_local_deploys
|
||||||
|
@non_local_deploys ||= site.deploys.where.not(type: 'DeployLocal')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
class DeployAlternativeDomain < Deploy
|
class DeployAlternativeDomain < Deploy
|
||||||
store :values, accessors: %i[hostname], coder: JSON
|
store :values, accessors: %i[hostname], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# Generar un link simbólico del sitio principal al alternativo
|
# Generar un link simbólico del sitio principal al alternativo
|
||||||
def deploy(**)
|
def deploy(**)
|
||||||
File.symlink?(destination) ||
|
File.symlink?(destination) ||
|
||||||
|
@ -18,7 +20,11 @@ class DeployAlternativeDomain < Deploy
|
||||||
end
|
end
|
||||||
|
|
||||||
def destination
|
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
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
|
|
217
app/models/deploy_distributed_press.rb
Normal file
217
app/models/deploy_distributed_press.rb
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'distributed_press/v1/client/site'
|
||||||
|
require 'njalla/v1'
|
||||||
|
|
||||||
|
# Soportar Distributed Press APIv1
|
||||||
|
#
|
||||||
|
# Usa tokens de publicación efímeros para todas las acciones.
|
||||||
|
#
|
||||||
|
# Al ser creado, genera el sitio en la instancia de Distributed Press
|
||||||
|
# configurada y almacena el ID.
|
||||||
|
#
|
||||||
|
# Al ser publicado, envía los archivos en un tarball y actualiza la
|
||||||
|
# información.
|
||||||
|
class DeployDistributedPress < Deploy
|
||||||
|
store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON
|
||||||
|
|
||||||
|
before_create :create_remote_site!, :create_njalla_records!
|
||||||
|
before_destroy :delete_remote_site!, :delete_njalla_records!
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
|
# Actualiza la información y luego envía los cambios
|
||||||
|
#
|
||||||
|
# @param :output [Bool]
|
||||||
|
# @return [Bool]
|
||||||
|
def deploy
|
||||||
|
status = false
|
||||||
|
log = []
|
||||||
|
|
||||||
|
time_start
|
||||||
|
|
||||||
|
create_remote_site! if remote_site_id.blank?
|
||||||
|
create_njalla_records!
|
||||||
|
save
|
||||||
|
|
||||||
|
if remote_site_id.blank?
|
||||||
|
raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press'
|
||||||
|
end
|
||||||
|
|
||||||
|
if create_njalla_records? && remote_info[:njalla].blank?
|
||||||
|
raise DeployJob::DeployException, 'No se pudieron crear los registros necesarios en Njalla'
|
||||||
|
end
|
||||||
|
|
||||||
|
site_client.tap do |c|
|
||||||
|
stdout = Thread.new(publisher.logger_out) do |io|
|
||||||
|
until io.eof?
|
||||||
|
line = io.gets
|
||||||
|
|
||||||
|
puts line if output
|
||||||
|
log << line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
status = c.publish(publishing_site, deploy_local.destination)
|
||||||
|
|
||||||
|
if status
|
||||||
|
self.remote_info[:distributed_press] = c.show(publishing_site).to_h
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
publisher.logger.close
|
||||||
|
stdout.join
|
||||||
|
end
|
||||||
|
|
||||||
|
time_stop
|
||||||
|
|
||||||
|
create_stat! status, log.join
|
||||||
|
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
def limit; end
|
||||||
|
|
||||||
|
def size
|
||||||
|
deploy_local.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def destination; end
|
||||||
|
|
||||||
|
# Devuelve las URLs de todos los protocolos
|
||||||
|
def urls
|
||||||
|
protocol_urls + gateway_urls
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def protocol_urls
|
||||||
|
remote_info.dig(:distributed_press, :protocols).select do |_, enabled|
|
||||||
|
enabled
|
||||||
|
end.map do |protocol, _|
|
||||||
|
"#{protocol}://#{site.hostname}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# El cliente de la API
|
||||||
|
#
|
||||||
|
# TODO: cuando soportemos más, tiene que haber una relación entre
|
||||||
|
# DeployDistributedPress y DistributedPressPublisher.
|
||||||
|
#
|
||||||
|
# @return [DistributedPressPublisher]
|
||||||
|
def publisher
|
||||||
|
@publisher ||= DistributedPressPublisher.last
|
||||||
|
end
|
||||||
|
|
||||||
|
# El cliente para actualizar el sitio
|
||||||
|
#
|
||||||
|
# @return [DistributedPress::V1::Client::Site]
|
||||||
|
def site_client
|
||||||
|
DistributedPress::V1::Client::Site.new(publisher.client)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera el esquema de datos para poder publicar el sitio
|
||||||
|
#
|
||||||
|
# @return [DistributedPress::V1::Schemas::PublishingSite]
|
||||||
|
def publishing_site
|
||||||
|
DistributedPress::V1::Schemas::PublishingSite.new.call(id: remote_site_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera el esquema de datos para crear el sitio
|
||||||
|
#
|
||||||
|
# @return [DistributedPressPublisher::V1::Schemas::NewSite]
|
||||||
|
def create_site
|
||||||
|
DistributedPress::V1::Schemas::NewSite.new.call(domain: hostname, protocols: { http: true, ipfs: true, hyper: true })
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crea el sitio en la instancia con el hostname especificado
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def create_remote_site!
|
||||||
|
created_site = site_client.create(create_site)
|
||||||
|
|
||||||
|
self.remote_site_id = created_site[:id]
|
||||||
|
self.remote_info ||= {}
|
||||||
|
self.remote_info[:distributed_press] = created_site.to_h
|
||||||
|
nil
|
||||||
|
rescue DistributedPress::V1::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crea los registros en Njalla
|
||||||
|
#
|
||||||
|
# XXX: Esto depende de nuestro DNS actual, cuando lo migremos hay
|
||||||
|
# que eliminarlo.
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def create_njalla_records!
|
||||||
|
return unless create_njalla_records?
|
||||||
|
|
||||||
|
self.remote_info ||= {}
|
||||||
|
self.remote_info[:njalla] ||= {}
|
||||||
|
self.remote_info[:njalla][:a] ||= njalla.add_record(name: site.name, type: 'CNAME', content: "#{Site.domain}.").to_h
|
||||||
|
self.remote_info[:njalla][:cname] ||= njalla.add_record(name: "www.#{site.name}", type: 'CNAME', content: "#{Site.domain}.").to_h
|
||||||
|
self.remote_info[:njalla][:ns] ||= njalla.add_record(name: "_dnslink.#{site.name}", type: 'NS', content: "#{publisher.hostname}.").to_h
|
||||||
|
|
||||||
|
nil
|
||||||
|
rescue HTTParty::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
self.remote_info.delete :njalla
|
||||||
|
ensure
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Registra lo que sucedió
|
||||||
|
#
|
||||||
|
# @param status [Bool]
|
||||||
|
# @param log [String]
|
||||||
|
# @return [nil]
|
||||||
|
def create_stat!(status, log)
|
||||||
|
build_stats.create action: publisher.to_s,log: log, seconds: time_spent_in_seconds, bytes: size, status: status
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_remote_site!
|
||||||
|
site_client.delete(publishing_site)
|
||||||
|
nil
|
||||||
|
rescue DistributedPress::V1::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_njalla_records!
|
||||||
|
return unless create_njalla_records?
|
||||||
|
|
||||||
|
%w[a ns cname].each do |type|
|
||||||
|
next if (id = remote_info.dig('njalla', type, 'id')).blank?
|
||||||
|
|
||||||
|
njalla.remove_record(id: id.to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Actualizar registros en Njalla
|
||||||
|
#
|
||||||
|
# @return [Njalla::V1::Domain]
|
||||||
|
def njalla
|
||||||
|
@njalla ||=
|
||||||
|
begin
|
||||||
|
client = Njalla::V1::Client.new(token: Rails.application.credentials.njalla)
|
||||||
|
|
||||||
|
Njalla::V1::Domain.new(domain: Site.domain, client: client)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si tenemos que crear registros en Njalla
|
||||||
|
def create_njalla_records?
|
||||||
|
!site.name.end_with?('.')
|
||||||
|
end
|
||||||
|
end
|
34
app/models/deploy_full_rsync.rb
Normal file
34
app/models/deploy_full_rsync.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def url
|
||||||
|
"https://#{user_host.last}/"
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,17 +2,36 @@
|
||||||
|
|
||||||
# Genera una versión onion
|
# Genera una versión onion
|
||||||
class DeployHiddenService < DeployWww
|
class DeployHiddenService < DeployWww
|
||||||
def deploy(**)
|
store :values, accessors: %i[onion], coder: JSON
|
||||||
return true if fqdn.blank?
|
|
||||||
|
|
||||||
super
|
before_create :create_hidden_service!
|
||||||
end
|
|
||||||
|
ONION_RE = /\A[a-z0-9]{56}\.onion\z/.freeze
|
||||||
|
|
||||||
def fqdn
|
def fqdn
|
||||||
values[:onion]
|
create_hidden_service! if onion.blank?
|
||||||
|
|
||||||
|
onion.tap do |onion|
|
||||||
|
raise ArgumentError, 'Aun no se generó la dirección .onion' if onion.blank?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
"http://#{fqdn}"
|
"http://#{fqdn}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_hidden_service!
|
||||||
|
onion_address = HiddenServiceClient.new.create(site.name)
|
||||||
|
|
||||||
|
if ONION_RE =~ onion_address
|
||||||
|
self.onion = onion_address
|
||||||
|
|
||||||
|
usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor'
|
||||||
|
params = { onion: onion_address, deploy: self }
|
||||||
|
|
||||||
|
SiteService.new(site: site, usuarie: usuarie, params: params).add_onion
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,7 @@ class DeployLocal < Deploy
|
||||||
def deploy(output: false)
|
def deploy(output: false)
|
||||||
return false unless mkdir
|
return false unless mkdir
|
||||||
return false unless yarn(output: output)
|
return false unless yarn(output: output)
|
||||||
|
return false unless pnpm(output: output)
|
||||||
return false unless bundle(output: output)
|
return false unless bundle(output: output)
|
||||||
|
|
||||||
jekyll_build(output: output)
|
jekyll_build(output: output)
|
||||||
|
@ -67,11 +68,14 @@ class DeployLocal < Deploy
|
||||||
end
|
end
|
||||||
|
|
||||||
# Un entorno que solo tiene lo que necesitamos
|
# Un entorno que solo tiene lo que necesitamos
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
def env
|
def env
|
||||||
# XXX: This doesn't support Windows paths :B
|
# XXX: This doesn't support Windows paths :B
|
||||||
paths = [File.dirname(`which bundle`), '/usr/bin', '/bin']
|
paths = [File.dirname(`which bundle`), '/usr/local/bin', '/usr/bin', '/bin']
|
||||||
|
|
||||||
{
|
# Las variables de entorno extra no pueden superponerse al local.
|
||||||
|
extra_env.merge({
|
||||||
'HOME' => home_dir,
|
'HOME' => home_dir,
|
||||||
'PATH' => paths.join(':'),
|
'PATH' => paths.join(':'),
|
||||||
'SPREE_API_KEY' => site.tienda_api_key,
|
'SPREE_API_KEY' => site.tienda_api_key,
|
||||||
|
@ -80,14 +84,19 @@ class DeployLocal < Deploy
|
||||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||||
'JEKYLL_ENV' => Rails.env,
|
'JEKYLL_ENV' => Rails.env,
|
||||||
'LANG' => ENV['LANG'],
|
'LANG' => ENV['LANG'],
|
||||||
'YARN_CACHE_FOLDER' => yarn_cache_dir
|
'YARN_CACHE_FOLDER' => yarn_cache_dir,
|
||||||
}
|
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def yarn_cache_dir
|
def yarn_cache_dir
|
||||||
Rails.root.join('_yarn_cache').to_s
|
Rails.root.join('_yarn_cache').to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pnpm_cache_dir
|
||||||
|
Rails.root.join('_pnpm_cache').to_s
|
||||||
|
end
|
||||||
|
|
||||||
def yarn_lock
|
def yarn_lock
|
||||||
File.join(site.path, 'yarn.lock')
|
File.join(site.path, 'yarn.lock')
|
||||||
end
|
end
|
||||||
|
@ -96,6 +105,14 @@ class DeployLocal < Deploy
|
||||||
File.exist? yarn_lock
|
File.exist? yarn_lock
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pnpm_lock
|
||||||
|
File.join(site.path, 'pnpm-lock.yaml')
|
||||||
|
end
|
||||||
|
|
||||||
|
def pnpm_lock?
|
||||||
|
File.exist? pnpm_lock
|
||||||
|
end
|
||||||
|
|
||||||
def gem(output: false)
|
def gem(output: false)
|
||||||
run %(gem install bundler --no-document), output: output
|
run %(gem install bundler --no-document), output: output
|
||||||
end
|
end
|
||||||
|
@ -107,6 +124,13 @@ class DeployLocal < Deploy
|
||||||
run 'yarn install --production', output: output
|
run 'yarn install --production', output: output
|
||||||
end
|
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)
|
def bundle(output: false)
|
||||||
run %(bundle install --no-cache --path="#{gems_dir}" --clean --without test development), output: output
|
run %(bundle install --no-cache --path="#{gems_dir}" --clean --without test development), output: output
|
||||||
end
|
end
|
||||||
|
@ -125,4 +149,18 @@ class DeployLocal < Deploy
|
||||||
def remove_destination!
|
def remove_destination!
|
||||||
FileUtils.rm_rf destination
|
FileUtils.rm_rf destination
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Consigue todas las variables de entorno configuradas por otros
|
||||||
|
# deploys.
|
||||||
|
#
|
||||||
|
# @deprecated Solo tenía sentido para Distributed Press v0
|
||||||
|
# @return [Hash]
|
||||||
|
def extra_env
|
||||||
|
@extra_env ||=
|
||||||
|
non_local_deploys.reduce({}) do |extra_env, deploy|
|
||||||
|
extra_env.tap do |e|
|
||||||
|
e.merge! deploy.local_env
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
# XXX: La plantilla tiene que soportar esto con el plugin
|
# XXX: La plantilla tiene que soportar esto con el plugin
|
||||||
# jekyll-private-data
|
# jekyll-private-data
|
||||||
class DeployPrivate < DeployLocal
|
class DeployPrivate < DeployLocal
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# No es necesario volver a instalar dependencias
|
# No es necesario volver a instalar dependencias
|
||||||
def deploy(output: false)
|
def deploy(output: false)
|
||||||
jekyll_build(output: output)
|
jekyll_build(output: output)
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
# Sincroniza sitios a servidores remotos usando Rsync. El servidor
|
# Sincroniza sitios a servidores remotos usando Rsync. El servidor
|
||||||
# remoto tiene que tener rsync instalado.
|
# remoto tiene que tener rsync instalado.
|
||||||
class DeployRsync < Deploy
|
class DeployRsync < Deploy
|
||||||
store :values, accessors: %i[destination host_keys], coder: JSON
|
store :values, accessors: %i[hostname destination host_keys], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local deploy_zip]
|
||||||
|
|
||||||
def deploy(output: false)
|
def deploy(output: false)
|
||||||
ssh? && rsync(output: output)
|
ssh? && rsync(output: output)
|
||||||
|
@ -23,6 +25,11 @@ class DeployRsync < Deploy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def url
|
||||||
|
"https://#{hostname}/"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Verificar la conexión SSH implementando Trust On First Use
|
# Verificar la conexión SSH implementando Trust On First Use
|
||||||
|
@ -83,8 +90,8 @@ class DeployRsync < Deploy
|
||||||
# Sincroniza hacia el directorio remoto
|
# Sincroniza hacia el directorio remoto
|
||||||
#
|
#
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def rsync(output: output)
|
def rsync(output: false)
|
||||||
run %(rsync -aviH --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/), output: output
|
run %(rsync -aviH --delete-after --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/), output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
# El origen es el destino de la compilación
|
# El origen es el destino de la compilación
|
||||||
|
|
|
@ -4,9 +4,13 @@
|
||||||
class DeployWww < Deploy
|
class DeployWww < Deploy
|
||||||
store :values, accessors: %i[], coder: JSON
|
store :values, accessors: %i[], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
before_destroy :remove_destination!
|
before_destroy :remove_destination!
|
||||||
|
|
||||||
def deploy(**)
|
def deploy(output: false)
|
||||||
|
puts "Creando symlink #{site.hostname} => #{destination}" if output
|
||||||
|
|
||||||
File.symlink?(destination) ||
|
File.symlink?(destination) ||
|
||||||
File.symlink(site.hostname, destination).zero?
|
File.symlink(site.hostname, destination).zero?
|
||||||
end
|
end
|
||||||
|
@ -28,7 +32,7 @@ class DeployWww < Deploy
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
"https://www.#{site.hostname}/"
|
"https://#{fqdn}/"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -8,28 +8,49 @@ require 'zip'
|
||||||
class DeployZip < Deploy
|
class DeployZip < Deploy
|
||||||
store :values, accessors: %i[], coder: JSON
|
store :values, accessors: %i[], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# Una vez que el sitio está generado, tomar todos los archivos y
|
# Una vez que el sitio está generado, tomar todos los archivos y
|
||||||
# y generar un zip accesible públicamente.
|
# y generar un zip accesible públicamente.
|
||||||
#
|
#
|
||||||
# rubocop:disable Metrics/MethodLength
|
# rubocop:disable Metrics/MethodLength
|
||||||
def deploy(**)
|
def deploy(output: false)
|
||||||
FileUtils.rm_f path
|
FileUtils.rm_f path
|
||||||
|
|
||||||
time_start
|
time_start
|
||||||
Dir.chdir(destination) do
|
Zip::File.open(path, Zip::File::CREATE) do |zip|
|
||||||
Zip::File.open(path, Zip::File::CREATE) do |z|
|
Dir.glob(File.join(destination, '**', '**')).each do |file|
|
||||||
Dir.glob('./**/**').each do |f|
|
entry = Pathname.new(file).relative_path_from(destination).to_s
|
||||||
File.directory?(f) ? z.mkdir(f) : z.add(f, f)
|
|
||||||
|
if File.directory? file
|
||||||
|
log "Creando directorio #{entry}", output
|
||||||
|
|
||||||
|
zip.mkdir(entry)
|
||||||
|
else
|
||||||
|
log "Comprimiendo #{entry}", output
|
||||||
|
zip.add(entry, file)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
time_stop
|
time_stop
|
||||||
|
|
||||||
|
File.exist?(path).tap do |status|
|
||||||
build_stats.create action: 'zip',
|
build_stats.create action: 'zip',
|
||||||
seconds: time_spent_in_seconds,
|
seconds: time_spent_in_seconds,
|
||||||
bytes: size
|
bytes: size,
|
||||||
|
log: @log.join("\n"),
|
||||||
|
status: status
|
||||||
|
end
|
||||||
|
rescue Zip::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
|
||||||
File.exist? path
|
build_stats.create action: 'zip',
|
||||||
|
seconds: 0,
|
||||||
|
bytes: 0,
|
||||||
|
log: @log.join("\n"),
|
||||||
|
status: false
|
||||||
|
|
||||||
|
false
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
|
@ -41,8 +62,11 @@ class DeployZip < Deploy
|
||||||
File.size path
|
File.size path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
def destination
|
def destination
|
||||||
File.join(Rails.root, '_deploy', site.hostname)
|
Rails.root.join('_deploy', site.hostname).realpath.to_s
|
||||||
|
rescue Errno::ENOENT
|
||||||
|
Rails.root.join('_deploy', site.hostname).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def file
|
def file
|
||||||
|
@ -56,4 +80,15 @@ class DeployZip < Deploy
|
||||||
def path
|
def path
|
||||||
File.join(destination, file)
|
File.join(destination, file)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @param :line [String]
|
||||||
|
# @param :output [Boolean]
|
||||||
|
def log(line, output)
|
||||||
|
@log ||= []
|
||||||
|
@log << line
|
||||||
|
|
||||||
|
puts line if output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
84
app/models/distributed_press_publisher.rb
Normal file
84
app/models/distributed_press_publisher.rb
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'distributed_press/v1'
|
||||||
|
|
||||||
|
# Almacena el token de autenticación y la URL, por ahora solo vamos
|
||||||
|
# a tener uno, pero queda abierta la posibilidad de agregar más.
|
||||||
|
class DistributedPressPublisher < ApplicationRecord
|
||||||
|
# Cifrar la información del token en la base de datos
|
||||||
|
has_encrypted :token
|
||||||
|
|
||||||
|
# La salida del log
|
||||||
|
#
|
||||||
|
# @return [IO]
|
||||||
|
attr_reader :logger_out
|
||||||
|
|
||||||
|
# La instancia es única
|
||||||
|
validates_uniqueness_of :instance
|
||||||
|
|
||||||
|
# El token es necesario
|
||||||
|
validates_presence_of :token
|
||||||
|
|
||||||
|
# Mantener la fecha de vencimiento actualizada
|
||||||
|
before_save :update_expires_at_from_token!, :update_token_from_client!
|
||||||
|
|
||||||
|
# Devuelve todos los tokens que vencen en una hora
|
||||||
|
scope :with_about_to_expire_tokens, lambda {
|
||||||
|
where('expires_at > ? and expires_at < ?', Time.now, Time.now + 1.hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Instancia un cliente de Distributed Press a partir del token. Al
|
||||||
|
# cargar un token a punto de vencer se renueva automáticamente.
|
||||||
|
#
|
||||||
|
# @return [DistributedPress::V1::Client]
|
||||||
|
def client
|
||||||
|
@client ||= DistributedPress::V1::Client.new(url: instance, token: token, logger: logger)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def to_s
|
||||||
|
"Distributed Press <#{instance}>"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devuelve el hostname de la instancia
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def hostname
|
||||||
|
@hostname ||= URI.parse(instance).hostname
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Logger]
|
||||||
|
def logger
|
||||||
|
@logger ||=
|
||||||
|
begin
|
||||||
|
@logger_out, @logger_in = IO.pipe
|
||||||
|
::Logger.new @logger_in, formatter: formatter
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def formatter
|
||||||
|
@formatter ||= lambda do |_, _, _, msg|
|
||||||
|
"#{msg}\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Actualiza o desactiva la fecha de vencimiento a partir de la
|
||||||
|
# información del token.
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def update_expires_at_from_token!
|
||||||
|
self.expires_at = client.token.forever? ? nil : client.token.expires_at
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Actualiza el token a partir del cliente, que ya actualiza el token
|
||||||
|
# automáticamente.
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def update_token_from_client!
|
||||||
|
self.token = client.token.to_s
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,6 +7,7 @@ class Licencia < ApplicationRecord
|
||||||
translates :name, type: :string, locale_accessors: true
|
translates :name, type: :string, locale_accessors: true
|
||||||
translates :url, type: :string, locale_accessors: true
|
translates :url, type: :string, locale_accessors: true
|
||||||
translates :description, type: :text, locale_accessors: true
|
translates :description, type: :text, locale_accessors: true
|
||||||
|
translates :short_description, type: :string, locale_accessors: true
|
||||||
translates :deed, type: :text, locale_accessors: true
|
translates :deed, type: :text, locale_accessors: true
|
||||||
|
|
||||||
has_many :sites
|
has_many :sites
|
||||||
|
@ -14,5 +15,10 @@ class Licencia < ApplicationRecord
|
||||||
validates :name, presence: true, uniqueness: true
|
validates :name, presence: true, uniqueness: true
|
||||||
validates :url, presence: true
|
validates :url, presence: true
|
||||||
validates :description, presence: true
|
validates :description, presence: true
|
||||||
|
validates :short_description, presence: true
|
||||||
validates :deed, presence: true
|
validates :deed, presence: true
|
||||||
|
|
||||||
|
def custom?
|
||||||
|
icons == 'custom'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,22 +1,49 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Los valores de este metadato son artículos en otros idiomas
|
# Los valores de este metadato son artículos en otros idiomas
|
||||||
class MetadataLocales < MetadataTemplate
|
class MetadataLocales < MetadataHasAndBelongsToMany
|
||||||
def default_value
|
|
||||||
super || []
|
|
||||||
end
|
|
||||||
|
|
||||||
# Todos los valores posibles para cada idioma disponible
|
# Todos los valores posibles para cada idioma disponible
|
||||||
#
|
#
|
||||||
# TODO: Optimizar?
|
|
||||||
# TODO: Mantener sincronizados
|
|
||||||
#
|
|
||||||
# @return { lang: { title: uuid } }
|
# @return { lang: { title: uuid } }
|
||||||
def values
|
def values
|
||||||
@values ||= site.locales.map do |locale|
|
@values ||= site.locales.map do |locale|
|
||||||
[locale, site.posts(lang: locale).map do |post|
|
[locale, posts.where(lang: locale).map do |post|
|
||||||
[post.title.value, post.uuid.value]
|
[title(post), post.uuid.value]
|
||||||
end.to_h]
|
end.to_h]
|
||||||
end.to_h
|
end.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Siempre hay una relación inversa
|
||||||
|
#
|
||||||
|
# @return [True]
|
||||||
|
def inverse?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# El campo inverso se llama igual en el otro post
|
||||||
|
#
|
||||||
|
# @return [Symbol]
|
||||||
|
def inverse
|
||||||
|
:locales
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Obtiene todos los locales distintos a este post
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def other_locales
|
||||||
|
site.locales.reject do |locale|
|
||||||
|
locale == post.lang.value.to_sym
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene todos los posts de los otros locales con el mismo layout
|
||||||
|
#
|
||||||
|
# @return [PostRelation]
|
||||||
|
def posts
|
||||||
|
other_locales.map do |locale|
|
||||||
|
site.posts(lang: locale).where(layout: post.layout.value)
|
||||||
|
end.reduce(&:concat) || PostRelation.new(site: site, lang: 'any')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
14
app/models/privacy_policy.rb
Normal file
14
app/models/privacy_policy.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Políticas de privacidad
|
||||||
|
class PrivacyPolicy < ApplicationRecord
|
||||||
|
extend Mobility
|
||||||
|
|
||||||
|
translates :title, type: :string, locale_accessors: true
|
||||||
|
translates :description, type: :text, locale_accessors: true
|
||||||
|
translates :content, type: :text, locale_accessors: true
|
||||||
|
|
||||||
|
validates :title, presence: true, uniqueness: true
|
||||||
|
validates :description, presence: true
|
||||||
|
validates :content, presence: true
|
||||||
|
end
|
|
@ -21,4 +21,8 @@ class Rol < ApplicationRecord
|
||||||
def usuarie?
|
def usuarie?
|
||||||
rol == USUARIE
|
rol == USUARIE
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.role?(rol)
|
||||||
|
ROLES.include? rol
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,8 @@ class Site < ApplicationRecord
|
||||||
include Site::Forms
|
include Site::Forms
|
||||||
include Site::FindAndReplace
|
include Site::FindAndReplace
|
||||||
include Site::Api
|
include Site::Api
|
||||||
|
include Site::DeployDependencies
|
||||||
|
include Site::BuildStats
|
||||||
include Tienda
|
include Tienda
|
||||||
|
|
||||||
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
|
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
|
||||||
|
@ -17,7 +19,7 @@ class Site < ApplicationRecord
|
||||||
|
|
||||||
# TODO: Hacer que los diferentes tipos de deploy se auto registren
|
# TODO: Hacer que los diferentes tipos de deploy se auto registren
|
||||||
# @see app/services/site_service.rb
|
# @see app/services/site_service.rb
|
||||||
DEPLOYS = %i[local private www zip hidden_service].freeze
|
DEPLOYS = %i[local private www zip hidden_service distributed_press].freeze
|
||||||
|
|
||||||
validates :name, uniqueness: true, hostname: {
|
validates :name, uniqueness: true, hostname: {
|
||||||
allow_root_label: true
|
allow_root_label: true
|
||||||
|
@ -553,10 +555,33 @@ class Site < ApplicationRecord
|
||||||
Dir.chdir path, &block
|
Dir.chdir path, &block
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Instala las gemas cuando es necesario:
|
||||||
|
#
|
||||||
|
# * El sitio existe
|
||||||
|
# * No están instaladas
|
||||||
|
# * El archivo Gemfile se modificó
|
||||||
|
# * El archivo Gemfile.lock se modificó
|
||||||
def install_gems
|
def install_gems
|
||||||
return unless persisted?
|
return unless persisted?
|
||||||
return if Rails.root.join('_storage', 'gems', name).directory?
|
|
||||||
|
|
||||||
|
if !gem_dir? || gemfile_updated? || gemfile_lock_updated?
|
||||||
deploys.find_by_type('DeployLocal').send(:bundle)
|
deploys.find_by_type('DeployLocal').send(:bundle)
|
||||||
|
touch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el repositorio de gemas existe
|
||||||
|
def gem_dir?
|
||||||
|
Rails.root.join('_storage', 'gems', name).directory?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el Gemfile fue modificado
|
||||||
|
def gemfile_updated?
|
||||||
|
updated_at < File.mtime(File.join(path, 'Gemfile'))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el Gemfile.lock fue modificado
|
||||||
|
def gemfile_lock_updated?
|
||||||
|
updated_at < File.mtime(File.join(path, 'Gemfile.lock'))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
45
app/models/site/build_stats.rb
Normal file
45
app/models/site/build_stats.rb
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# 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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -31,7 +31,7 @@ class Site
|
||||||
|
|
||||||
# Escribe los cambios en el repositorio
|
# Escribe los cambios en el repositorio
|
||||||
def write
|
def write
|
||||||
return if persisted?
|
return true if persisted?
|
||||||
|
|
||||||
@saved = Site::Writer.new(site: site, file: path, content: content.to_yaml).save.tap do |result|
|
@saved = Site::Writer.new(site: site, file: path, content: content.to_yaml).save.tap do |result|
|
||||||
# Actualizar el hash para no escribir dos veces
|
# Actualizar el hash para no escribir dos veces
|
||||||
|
|
38
app/models/site/deploy_dependencies.rb
Normal file
38
app/models/site/deploy_dependencies.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rgl/adjacency'
|
||||||
|
require 'rgl/topsort'
|
||||||
|
|
||||||
|
class Site
|
||||||
|
module DeployDependencies
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Genera un grafo dirigido de todos los métodos de publicación
|
||||||
|
#
|
||||||
|
# @return [RGL::DirectedAdjacencyGraph]
|
||||||
|
def deployment_graph
|
||||||
|
@deployment_graph ||= RGL::DirectedAdjacencyGraph.new.tap do |graph|
|
||||||
|
deploys.each do |deploy|
|
||||||
|
graph.add_vertex deploy
|
||||||
|
end
|
||||||
|
|
||||||
|
deploys.each do |deploy|
|
||||||
|
deploy.class.all_dependencies.each do |dependency|
|
||||||
|
deploys.where(type: dependency.to_s.classify).each do |deploy_dependency|
|
||||||
|
graph.add_edge deploy_dependency, deploy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devuelve una lista ordenada de todos los métodos de publicación
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def deployment_list
|
||||||
|
@deployment_list ||= deployment_graph.topsort_iterator.to_a
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -117,6 +117,9 @@ class Site
|
||||||
def commit(file:, usuarie:, message:, remove: false)
|
def commit(file:, usuarie:, message:, remove: false)
|
||||||
file = [file] unless file.respond_to? :each
|
file = [file] unless file.respond_to? :each
|
||||||
|
|
||||||
|
# Cargar el árbol actual
|
||||||
|
rugged.index.read_tree rugged.head.target.tree
|
||||||
|
|
||||||
file.each do |f|
|
file.each do |f|
|
||||||
remove ? rm(f) : add(f)
|
remove ? rm(f) : add(f)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
# Usuarie de la plataforma
|
# Usuarie de la plataforma
|
||||||
class Usuarie < ApplicationRecord
|
class Usuarie < ApplicationRecord
|
||||||
|
include Usuarie::Consent
|
||||||
|
|
||||||
devise :invitable, :database_authenticatable,
|
devise :invitable, :database_authenticatable,
|
||||||
:recoverable, :rememberable, :validatable,
|
:recoverable, :rememberable, :validatable,
|
||||||
:confirmable, :lockable, :registerable
|
:confirmable, :lockable, :registerable
|
||||||
|
@ -41,6 +43,12 @@ class Usuarie < ApplicationRecord
|
||||||
lock_access! if attempts_exceeded? && !access_locked?
|
lock_access! if attempts_exceeded? && !access_locked?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_devise_notification(notification, *args)
|
||||||
|
I18n.with_locale(lang) do
|
||||||
|
devise_mailer.send(notification, self, *args).deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def lang_from_locale!
|
def lang_from_locale!
|
||||||
|
|
67
app/services/lfs_object_service.rb
Normal file
67
app/services/lfs_object_service.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Representa un objeto git LFS
|
||||||
|
class LfsObjectService
|
||||||
|
attr_reader :site, :blob
|
||||||
|
|
||||||
|
# @param :site [Site]
|
||||||
|
# @param :blob [ActiveStorage::Blob]
|
||||||
|
def initialize(site:, blob:)
|
||||||
|
@site = site
|
||||||
|
@blob = blob
|
||||||
|
end
|
||||||
|
|
||||||
|
def process
|
||||||
|
# Crear el directorio
|
||||||
|
FileUtils.mkdir_p(File.dirname(object_path))
|
||||||
|
|
||||||
|
# Mover el archivo
|
||||||
|
FileUtils.mv(path, object_path) unless File.exist? object_path
|
||||||
|
|
||||||
|
# Crear el pointer
|
||||||
|
Site::Writer.new(site: site, file: path, content: pointer).save
|
||||||
|
|
||||||
|
# Commitear el pointer
|
||||||
|
site.repository.commit(file: path, usuarie: author, message: File.basename(path))
|
||||||
|
|
||||||
|
# Eliminar el pointer
|
||||||
|
FileUtils.rm(path)
|
||||||
|
|
||||||
|
# Hacer link duro del objeto al archivo
|
||||||
|
FileUtils.ln(object_path, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def path
|
||||||
|
@path ||= blob.service.path_for(blob.key)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def digest
|
||||||
|
@digest ||= Digest::SHA256.file(path).hexdigest
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def object_path
|
||||||
|
@object_path ||= File.join(site.path, '.git', 'lfs', 'objects', digest[0..1], digest[2..3], digest)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Integer]
|
||||||
|
def size
|
||||||
|
@size ||= File.size(File.exist?(object_path) ? object_path : path)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def pointer
|
||||||
|
@pointer ||=
|
||||||
|
<<~POINTER
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:#{digest}
|
||||||
|
size #{size}
|
||||||
|
POINTER
|
||||||
|
end
|
||||||
|
|
||||||
|
def author
|
||||||
|
@author ||= GitAuthor.new email: "disk_service@#{Site.domain}", name: 'DiskService'
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,8 +12,14 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
post.usuaries << usuarie
|
post.usuaries << usuarie
|
||||||
params[:post][:draft] = true if site.invitade? usuarie
|
params[:post][:draft] = true if site.invitade? usuarie
|
||||||
|
|
||||||
|
params.require(:post).permit(:slug).tap do |p|
|
||||||
|
post.slug.value = p[:slug] if p[:slug].present?
|
||||||
|
end
|
||||||
|
|
||||||
commit(action: :created, file: update_related_posts) if post.update(post_params)
|
commit(action: :created, file: update_related_posts) if post.update(post_params)
|
||||||
|
|
||||||
|
update_site_license!
|
||||||
|
|
||||||
# Devolver el post aunque no se haya salvado para poder rescatar los
|
# Devolver el post aunque no se haya salvado para poder rescatar los
|
||||||
# errores
|
# errores
|
||||||
post
|
post
|
||||||
|
@ -40,6 +46,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
# relacionados.
|
# relacionados.
|
||||||
commit(action: :updated, file: update_related_posts) if post.update(post_params)
|
commit(action: :updated, file: update_related_posts) if post.update(post_params)
|
||||||
|
|
||||||
|
update_site_license!
|
||||||
|
|
||||||
# Devolver el post aunque no se haya salvado para poder rescatar los
|
# Devolver el post aunque no se haya salvado para poder rescatar los
|
||||||
# errores
|
# errores
|
||||||
post
|
post
|
||||||
|
@ -133,4 +141,12 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
p.path.absolute if p.save(validate: false)
|
p.path.absolute if p.save(validate: false)
|
||||||
end.compact << post.path.absolute
|
end.compact << post.path.absolute
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Si les usuaries modifican o crean una licencia, considerarla
|
||||||
|
# personalizada en el panel.
|
||||||
|
def update_site_license!
|
||||||
|
if site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom?
|
||||||
|
site.update licencia: Licencia.find_by_icons('custom')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
self.site = Site.new params
|
self.site = Site.new params
|
||||||
|
|
||||||
add_role temporal: false, rol: 'usuarie'
|
add_role temporal: false, rol: 'usuarie'
|
||||||
|
site.deploys.build type: 'DeployLocal'
|
||||||
sync_nodes
|
sync_nodes
|
||||||
|
|
||||||
I18n.with_locale(usuarie.lang.to_sym || I18n.default_locale) do
|
I18n.with_locale(usuarie.lang.to_sym || I18n.default_locale) do
|
||||||
|
@ -26,12 +27,13 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
|
|
||||||
site.save &&
|
site.save &&
|
||||||
site.config.write &&
|
site.config.write &&
|
||||||
commit_config(action: :create)
|
commit_config(action: :create) &&
|
||||||
end
|
site.reset.nil? &&
|
||||||
|
add_licencias &&
|
||||||
add_licencias
|
add_code_of_conduct &&
|
||||||
|
add_privacy_policy &&
|
||||||
deploy
|
deploy
|
||||||
|
end
|
||||||
|
|
||||||
site
|
site
|
||||||
end
|
end
|
||||||
|
@ -41,10 +43,10 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
|
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
|
||||||
site.update(params) &&
|
site.update(params) &&
|
||||||
site.config.write &&
|
site.config.write &&
|
||||||
commit_config(action: :update)
|
commit_config(action: :update) &&
|
||||||
end
|
site.reset.nil? &&
|
||||||
|
|
||||||
change_licencias
|
change_licencias
|
||||||
|
end
|
||||||
|
|
||||||
site
|
site
|
||||||
end
|
end
|
||||||
|
@ -62,14 +64,11 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
# Agregar una dirección oculta de Tor al DeployHiddenService y a la
|
# Agregar una dirección oculta de Tor al DeployHiddenService y a la
|
||||||
# configuración del Site.
|
# configuración del Site.
|
||||||
def add_onion
|
def add_onion
|
||||||
onion = params[:onion].strip
|
onion = params[:onion]
|
||||||
deploy = DeployHiddenService.find_by(site: site)
|
deploy = params[:deploy]
|
||||||
|
|
||||||
return false unless !onion.blank? && deploy
|
return false unless !onion.blank? && deploy
|
||||||
|
|
||||||
deploy.values[:onion] = onion
|
|
||||||
deploy.save
|
|
||||||
|
|
||||||
site.config['onion-location'] = onion
|
site.config['onion-location'] = onion
|
||||||
site.config.write
|
site.config.write
|
||||||
|
|
||||||
|
@ -105,24 +104,28 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Crea la licencia del sitio para cada locale disponible en el sitio
|
# Crea la licencia del sitio para cada locale disponible en el sitio
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
def add_licencias
|
def add_licencias
|
||||||
site.locales.each do |locale|
|
return true unless site.layout? :license
|
||||||
next unless I18n.available_locales.include? locale
|
return true if site.licencia.custom?
|
||||||
|
|
||||||
Mobility.with_locale(locale) do
|
with_all_locales do |locale|
|
||||||
add_licencia lang: locale
|
add_licencia lang: locale
|
||||||
end
|
end.compact.map(&:valid?).all?
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Crea una licencia
|
||||||
|
#
|
||||||
|
# @return [Post]
|
||||||
def add_licencia(lang:)
|
def add_licencia(lang:)
|
||||||
params = ActionController::Parameters.new(
|
params = ActionController::Parameters.new(
|
||||||
post: {
|
post: {
|
||||||
|
layout: 'license',
|
||||||
|
slug: Jekyll::Utils.slugify(I18n.t('activerecord.models.licencia')),
|
||||||
lang: lang,
|
lang: lang,
|
||||||
title: site.licencia.name,
|
title: site.licencia.name,
|
||||||
description: I18n.t('sites.form.licencia.title'),
|
description: site.licencia.short_description,
|
||||||
author: %w[Sutty],
|
|
||||||
permalink: "#{I18n.t('activerecord.models.licencia').downcase}/",
|
|
||||||
content: CommonMarker.render_html(site.licencia.deed)
|
content: CommonMarker.render_html(site.licencia.deed)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -133,25 +136,27 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
# Encuentra la licencia a partir de su enlace permanente y le cambia
|
# Encuentra la licencia a partir de su enlace permanente y le cambia
|
||||||
# el contenido
|
# el contenido
|
||||||
#
|
#
|
||||||
# TODO: Crear un layout específico para licencias así es más certera
|
# @return [Boolean]
|
||||||
# la búsqueda.
|
|
||||||
def change_licencias
|
def change_licencias
|
||||||
site.locales.each do |locale|
|
return true unless site.layout? :license
|
||||||
next unless I18n.available_locales.include? locale
|
return true if site.licencia.custom?
|
||||||
|
|
||||||
Mobility.with_locale(locale) do
|
with_all_locales do |locale|
|
||||||
permalink = "#{I18n.t('activerecord.models.licencia').downcase}/"
|
post = site.posts(lang: locale).find_by(layout: 'license')
|
||||||
post = site.posts(lang: locale).find_by(permalink: permalink)
|
|
||||||
|
|
||||||
post ? change_licencia(post: post) : add_licencia(lang: locale)
|
change_licencia(post: post) if post
|
||||||
end
|
end.compact.map(&:valid?).all?
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Cambia una licencia
|
||||||
|
#
|
||||||
|
# @param :post [Post]
|
||||||
|
# @return [Post]
|
||||||
def change_licencia(post:)
|
def change_licencia(post:)
|
||||||
params = ActionController::Parameters.new(
|
params = ActionController::Parameters.new(
|
||||||
post: {
|
post: {
|
||||||
title: site.licencia.name,
|
title: site.licencia.name,
|
||||||
|
description: site.licencia.short_description,
|
||||||
content: CommonMarker.render_html(site.licencia.deed)
|
content: CommonMarker.render_html(site.licencia.deed)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -160,10 +165,69 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
params: params).update
|
params: params).update
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Agrega un código de conducta
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def add_code_of_conduct
|
||||||
|
return true unless site.layout?(:code_of_conduct) || site.layout?(:page)
|
||||||
|
|
||||||
|
# TODO: soportar más códigos de conducta
|
||||||
|
coc = CodeOfConduct.first
|
||||||
|
|
||||||
|
with_all_locales do |locale|
|
||||||
|
params = ActionController::Parameters.new(
|
||||||
|
post: {
|
||||||
|
layout: site.layout?(:code_of_conduct) ? 'code_of_conduct' : 'page',
|
||||||
|
lang: locale.to_s,
|
||||||
|
title: coc.title,
|
||||||
|
description: coc.description,
|
||||||
|
content: CommonMarker.render_html(coc.content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
PostService.new(site: site, usuarie: usuarie, params: params).create
|
||||||
|
end.compact.map(&:valid?).all?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Agrega política de privacidad
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def add_privacy_policy
|
||||||
|
return true unless site.layout?(:privacy_policy) || site.layout?(:page)
|
||||||
|
|
||||||
|
pp = PrivacyPolicy.first
|
||||||
|
|
||||||
|
with_all_locales do |locale|
|
||||||
|
params = ActionController::Parameters.new(
|
||||||
|
post: {
|
||||||
|
layout: site.layout?(:privacy_policy) ? 'privacy_policy' : 'page',
|
||||||
|
lang: locale.to_s,
|
||||||
|
title: pp.title,
|
||||||
|
description: pp.description,
|
||||||
|
content: CommonMarker.render_html(pp.content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
PostService.new(site: site, usuarie: usuarie, params: params).create
|
||||||
|
end.compact.map(&:valid?).all?
|
||||||
|
end
|
||||||
|
|
||||||
# Crea los deploys necesarios para sincronizar a otros nodos de Sutty
|
# Crea los deploys necesarios para sincronizar a otros nodos de Sutty
|
||||||
def sync_nodes
|
def sync_nodes
|
||||||
Rails.application.nodes.each do |node|
|
Rails.application.nodes.each do |node|
|
||||||
site.deploys.build(type: 'DeployRsync', destination: "sutty@#{node}:#{site.hostname}")
|
site.deploys.build(type: 'DeployFullRsync', destination: "sutty@#{node}:")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def with_all_locales(&block)
|
||||||
|
site.locales.map do |locale|
|
||||||
|
next unless I18n.available_locales.include? locale
|
||||||
|
|
||||||
|
Mobility.with_locale(locale) do
|
||||||
|
yield locale
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
2
app/views/bootstrap/_alert.haml
Normal file
2
app/views/bootstrap/_alert.haml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.alert.alert-primary.mx-auto.content.max-w-md-70ch{ role: 'alert', class: local_assigns[:class] }
|
||||||
|
= yield
|
6
app/views/bootstrap/_custom_checkbox.haml
Normal file
6
app/views/bootstrap/_custom_checkbox.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
- help_id = "#{id}_help"
|
||||||
|
|
||||||
|
.custom-control.custom-checkbox
|
||||||
|
%input.custom-control-input{ id: id, type: 'checkbox', name: name, value: value, required: required }
|
||||||
|
%label.custom-control-label{ for: id, aria: { describedby: help_id } }= content
|
||||||
|
%small.form-text.text-muted{ id: help_id }= yield
|
|
@ -11,7 +11,6 @@
|
||||||
url: site_collaborate_path(@site),
|
url: site_collaborate_path(@site),
|
||||||
method: :post) do |f|
|
method: :post) do |f|
|
||||||
- unless current_usuarie
|
- unless current_usuarie
|
||||||
= render 'layouts/flash'
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :email
|
= f.label :email
|
||||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||||
|
|
21
app/views/deploys/_deploy_distributed_press.haml
Normal file
21
app/views/deploys/_deploy_distributed_press.haml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
-# Publicar a la web distribuida
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col
|
||||||
|
= deploy.hidden_field :id
|
||||||
|
= deploy.hidden_field :type
|
||||||
|
.custom-control.custom-switch
|
||||||
|
-#
|
||||||
|
El checkbox invierte la lógica de destrucción porque queremos
|
||||||
|
crear el deploy si está activado y destruirlo si está
|
||||||
|
desactivado.
|
||||||
|
= deploy.check_box :_destroy,
|
||||||
|
{ checked: deploy.object.persisted?, class: 'custom-control-input' },
|
||||||
|
'0', '1'
|
||||||
|
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||||
|
%h3= t('.title')
|
||||||
|
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
||||||
|
tags: %w[p strong em a]
|
||||||
|
|
||||||
|
|
||||||
|
%hr/
|
1
app/views/deploys/_deploy_full_rsync.haml
Normal file
1
app/views/deploys/_deploy_full_rsync.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
-# nada
|
|
@ -17,7 +17,8 @@
|
||||||
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
|
|
||||||
- if deploy.object.fqdn
|
- begin
|
||||||
= sanitize_markdown t('.help_2', url: deploy.object.url),
|
= sanitize_markdown t('.help_2', url: deploy.object.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
|
- rescue ArgumentError
|
||||||
%hr/
|
%hr/
|
||||||
|
|
1
app/views/deploys/_deploy_localized_domain.haml
Normal file
1
app/views/deploys/_deploy_localized_domain.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
-# nada
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-4.align-self-center
|
.col-md-4.align-self-center
|
||||||
.sr-only
|
.sr-only
|
||||||
|
@ -11,8 +13,6 @@
|
||||||
url: confirmation_path(resource_name),
|
url: confirmation_path(resource_name),
|
||||||
html: { method: :post }) do |f|
|
html: { method: :post }) do |f|
|
||||||
|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
|
|
||||||
:ruby
|
:ruby
|
||||||
value = if resource.pending_reconfirmation?
|
value = if resource.pending_reconfirmation?
|
||||||
resource.unconfirmed_email
|
resource.unconfirmed_email
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
%h2= t 'devise.invitations.edit.header'
|
%h2= t 'devise.invitations.edit.header'
|
||||||
|
@ -8,7 +10,6 @@
|
||||||
as: resource_name,
|
as: resource_name,
|
||||||
url: invitation_path(resource_name),
|
url: invitation_path(resource_name),
|
||||||
html: { method: :put }) do |f|
|
html: { method: :put }) do |f|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
= f.hidden_field :invitation_token, readonly: true
|
= f.hidden_field :invitation_token, readonly: true
|
||||||
- if f.object.class.require_password_on_accepting
|
- if f.object.class.require_password_on_accepting
|
||||||
.form-group
|
.form-group
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
%h2= t 'devise.invitations.new.header'
|
%h2= t 'devise.invitations.new.header'
|
||||||
|
@ -8,7 +10,6 @@
|
||||||
as: resource_name,
|
as: resource_name,
|
||||||
url: invitation_path(resource_name),
|
url: invitation_path(resource_name),
|
||||||
html: { method: :post }) do |f|
|
html: { method: :post }) do |f|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
- resource.class.invite_key_fields.each do |field|
|
- resource.class.invite_key_fields.each do |field|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label field
|
= f.label field
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
.sr-only
|
.sr-only
|
||||||
|
@ -10,7 +12,6 @@
|
||||||
= form_for(resource, as: resource_name,
|
= form_for(resource, as: resource_name,
|
||||||
url: password_path(resource_name),
|
url: password_path(resource_name),
|
||||||
html: { method: :put }) do |f|
|
html: { method: :put }) do |f|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
|
|
||||||
= f.hidden_field :reset_password_token
|
= f.hidden_field :reset_password_token
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
.sr-only
|
.sr-only
|
||||||
|
@ -11,7 +13,6 @@
|
||||||
as: resource_name,
|
as: resource_name,
|
||||||
url: password_path(resource_name),
|
url: password_path(resource_name),
|
||||||
html: { method: :post }) do |f|
|
html: { method: :post }) do |f|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :email, class: 'sr-only'
|
= f.label :email, class: 'sr-only'
|
||||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-6.align-self-center
|
.col-md-6.align-self-center
|
||||||
%h2= t('.title')
|
%h2= t('.title')
|
||||||
|
@ -11,8 +13,6 @@
|
||||||
url: registration_path(resource_name),
|
url: registration_path(resource_name),
|
||||||
html: { method: :put }) do |f|
|
html: { method: :put }) do |f|
|
||||||
|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :email
|
= f.label :email
|
||||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-6.align-self-center
|
||||||
%h2= t('.sign_up')
|
%h2= t('.sign_up')
|
||||||
%p= t('.help')
|
%p= t('.help')
|
||||||
|
|
||||||
|
@ -10,8 +12,6 @@
|
||||||
as: resource_name,
|
as: resource_name,
|
||||||
url: registration_path(resource_name, params: { locale: params[:locale] })) do |f|
|
url: registration_path(resource_name, params: { locale: params[:locale] })) do |f|
|
||||||
|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :email, class: 'sr-only'
|
= f.label :email, class: 'sr-only'
|
||||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||||
|
@ -39,6 +39,21 @@
|
||||||
min: @minimum_password_length,
|
min: @minimum_password_length,
|
||||||
aria: { describedby: 'minimum-password-length' },
|
aria: { describedby: 'minimum-password-length' },
|
||||||
placeholder: t("#{password}_confirmation")
|
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
|
.actions
|
||||||
= f.submit t('.sign_up'),
|
= f.submit t('.sign_up'),
|
||||||
class: 'btn btn-lg btn-block'
|
class: 'btn btn-lg btn-block'
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
= render 'layouts/flash'
|
|
||||||
|
|
||||||
.sr-only
|
.sr-only
|
||||||
%h2= t('.sign_in')
|
%h2= t('.sign_in')
|
||||||
%p= t('.help')
|
%p= t('.help')
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
- if resource.errors.any?
|
- if resource.errors.any?
|
||||||
#error_explanation
|
= render 'bootstrap/alert' do
|
||||||
%h2
|
|
||||||
= I18n.t("errors.messages.not_saved", |
|
|
||||||
count: resource.errors.count, |
|
|
||||||
resource: resource.class.model_name.human.downcase) |
|
|
||||||
%ul
|
|
||||||
- resource.errors.full_messages.each do |message|
|
- resource.errors.full_messages.each do |message|
|
||||||
%li= message
|
%p= message
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
= content_for :body do
|
= content_for :body do
|
||||||
- 'black-bg'
|
- 'black-bg'
|
||||||
|
|
||||||
|
= render 'devise/shared/error_messages', resource: resource
|
||||||
|
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-5.align-self-center
|
.col-md-5.align-self-center
|
||||||
.sr-only
|
.sr-only
|
||||||
|
@ -11,7 +13,6 @@
|
||||||
as: resource_name,
|
as: resource_name,
|
||||||
url: unlock_path(resource_name),
|
url: unlock_path(resource_name),
|
||||||
html: { method: :post }) do |f|
|
html: { method: :post }) do |f|
|
||||||
= render 'devise/shared/error_messages', resource: resource
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :email, class: 'sr-only'
|
= f.label :email, class: 'sr-only'
|
||||||
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
= f.email_field :email, autofocus: true, autocomplete: 'email',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.row.align-items-center.justify-content-center.full-height
|
.row.align-items-center.justify-content-center.full-height
|
||||||
.col-md-6.align-self-center
|
.col-md-6.align-self-center
|
||||||
.alert{role: 'alert', class: "alert-success"}
|
= render 'bootstrap/alert' do
|
||||||
= t('.confirmation_sent')
|
= t('.confirmation_sent')
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
= link_to t('.logout'), main_app.destroy_usuarie_session_path,
|
= link_to t('.logout'), main_app.destroy_usuarie_session_path,
|
||||||
method: :delete, role: 'button', class: 'btn'
|
method: :delete, role: 'button', class: 'btn'
|
||||||
- else
|
- else
|
||||||
|
- params.permit!
|
||||||
- I18n.available_locales.each do |locale|
|
- I18n.available_locales.each do |locale|
|
||||||
- next if locale == I18n.locale
|
- next if locale == I18n.locale
|
||||||
= link_to t(locale), "?change_locale_to=#{locale}"
|
= link_to t(locale), params.to_h.merge(change_locale_to: locale)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
- flash.each do |type, message|
|
- flash.each do |type, message|
|
||||||
- unless type == 'js'
|
- unless type == 'js'
|
||||||
.alert{ role: 'alert', class: "alert-#{type}" }= message
|
= render 'bootstrap/alert' do
|
||||||
|
= message
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
-# DEPRECADO
|
||||||
.alert.alert-info.alert-dismissible.fade.show{role: 'alert'}
|
.alert.alert-info.alert-dismissible.fade.show{role: 'alert'}
|
||||||
- if help.respond_to? :each
|
- if help.respond_to? :each
|
||||||
%ul
|
%ul
|
||||||
|
|
|
@ -21,6 +21,9 @@
|
||||||
%body{ class: yield(:body) }
|
%body{ class: yield(:body) }
|
||||||
.container-fluid#sutty
|
.container-fluid#sutty
|
||||||
= render 'layouts/breadcrumb'
|
= render 'layouts/breadcrumb'
|
||||||
|
= render 'layouts/flash'
|
||||||
|
|
||||||
= yield
|
= yield
|
||||||
|
|
||||||
- if flash[:js]
|
- if flash[:js]
|
||||||
.js-flash.d-none{ data: flash[:js] }
|
.js-flash.d-none{ data: flash[:js] }
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
- unless post.errors.empty?
|
- unless post.errors.empty?
|
||||||
.alert.alert-danger
|
- title = t('.errors.title')
|
||||||
%h4= t('.errors.title')
|
- help = t('.errors.help')
|
||||||
%p= t('.errors.help')
|
= render 'bootstrap/alert' do
|
||||||
|
%h4= title
|
||||||
|
%p= help
|
||||||
|
|
||||||
%ul
|
%ul
|
||||||
- post.errors.each do |attribute, errors|
|
- post.errors.each do |attribute, errors|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.form-group
|
.form-group
|
||||||
= submit_tag t('.save'), class: 'btn submit-post'
|
= submit_tag t('.save'), class: 'btn submit-post'
|
||||||
.invalid-help.alert.alert-danger.d-none
|
= render 'bootstrap/alert', class: 'invalid-help d-none' do
|
||||||
= site.config.fetch('invalid_help', t('.invalid_help'))
|
= site.config.fetch('invalid_help', t('.invalid_help'))
|
||||||
.sending-help.alert.alert-success.d-none
|
= render 'bootstrap/alert', class: 'sending-help d-none' do
|
||||||
= site.config.fetch('sending_help', t('.sending_help'))
|
= site.config.fetch('sending_help', t('.sending_help'))
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
%tr{ id: attribute }
|
- if site.locales.count > 1
|
||||||
|
%tr{ id: attribute }
|
||||||
%th= post_label_t(attribute, post: post)
|
%th= post_label_t(attribute, post: post)
|
||||||
%td
|
%td
|
||||||
%ul
|
%ul
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
.editor{ id: attribute, data: { editor: '' } }
|
.editor{ id: attribute, data: { editor: '' } }
|
||||||
-# Esto es para luego decirle al navegador que se olvide estas cosas.
|
-# Esto es para luego decirle al navegador que se olvide estas cosas.
|
||||||
= hidden_field_tag 'storage_keys[]', "#{request.original_url}##{attribute}", data: { target: 'storage-key' }
|
= hidden_field_tag 'storage_keys[]', "#{request.original_url}##{attribute}", data: { target: 'storage-key' }
|
||||||
.alert.alert-info
|
= render 'bootstrap/alert' do
|
||||||
:markdown
|
:markdown
|
||||||
#{t('editor.alert')}
|
#{t('editor.alert')}
|
||||||
= text_area_tag "#{base}[#{attribute}]", '',
|
= text_area_tag "#{base}[#{attribute}]", '',
|
||||||
|
@ -123,7 +123,7 @@
|
||||||
%label{ for: 'link-url' }= t('editor.url')
|
%label{ for: 'link-url' }= t('editor.url')
|
||||||
%input.form-control{ type: 'url', id: 'link-url', name: 'link-url' }/
|
%input.form-control{ type: 'url', id: 'link-url', name: 'link-url' }/
|
||||||
|
|
||||||
.editor-aviso-word.alert.alert-info
|
= render 'bootstrap/alert', class: 'editor-aviso-word' do
|
||||||
%p= t('editor.word')
|
%p= t('editor.word')
|
||||||
|
|
||||||
.editor-content.form-control.h-auto.mt-1{ contenteditable: 'true' }
|
.editor-content.form-control.h-auto.mt-1{ contenteditable: 'true' }
|
||||||
|
|
|
@ -1,39 +1,19 @@
|
||||||
-#
|
- if site.locales.count > 1
|
||||||
|
%fieldset
|
||||||
|
%legend= post_label_t(attribute, post: post)
|
||||||
|
|
||||||
Crea un input-map para cada idioma por separado. Podríamos hacer uno
|
= render 'posts/attribute_feedback',
|
||||||
solo que tenga todos los idiomas pero puede ser una interfaz confusa.
|
post: post, attribute: attribute, metadata: metadata
|
||||||
|
|
||||||
TODO: Esto permite seleccionar más de una traducción por idioma...
|
- site.locales.each do |locale|
|
||||||
|
|
||||||
- site.locales.each do |locale|
|
|
||||||
-# Ignorar el idioma actual
|
|
||||||
- next if post.lang.value == locale
|
- next if post.lang.value == locale
|
||||||
- locale_t = t("locales.#{locale}.name")
|
- locale_t = t("locales.#{locale}.name", default: locale.to_s.humanize)
|
||||||
- values = metadata.value.select do |x|
|
- value = metadata.value.find do |v|
|
||||||
- metadata.values[locale].values.include? x
|
- metadata.values[locale].values.include? v
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
= label_tag "#{base}_#{attribute}_#{locale}", locale_t
|
= label_tag "#{base}_#{attribute}_#{locale}", locale_t
|
||||||
|
|
||||||
.mapable{ dir: t("locales.#{locale}.dir"), lang: locale,
|
= select_tag("#{plain_field_name_for(base, attribute)}[]",
|
||||||
data: { values: values.to_json,
|
options_for_select(metadata.values[locale], value),
|
||||||
'default-values': metadata.values[locale].to_json,
|
**field_options(attribute, metadata), include_blank: t('.empty'))
|
||||||
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))
|
|
||||||
|
|
||||||
= render 'posts/attribute_feedback',
|
|
||||||
post: post,
|
|
||||||
attribute: [attribute, 'mapable'].flatten,
|
|
||||||
metadata: metadata
|
|
||||||
|
|
||||||
%datalist{ id: id_for_datalist(attribute, locale) }
|
|
||||||
- metadata.values[locale].keys.each do |value|
|
|
||||||
%option{ value: value }
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
%main.row
|
%main.row
|
||||||
%aside.menu.col-md-3
|
%aside.menu.col-md-3
|
||||||
%h1= link_to @site.title, @site.url
|
%h1= @site.title
|
||||||
%p.lead= @site.description
|
%p.lead= @site.description
|
||||||
|
- cache_if @usuarie, [@site, I18n.locale] do
|
||||||
|
= render 'sites/status', site: @site
|
||||||
|
|
||||||
%h3= t('posts.new')
|
%h3= t('posts.new')
|
||||||
%table.mb-3
|
%table.mb-3
|
||||||
- @site.layouts.each do |layout|
|
- @site.layouts.sort_by(&:humanized_name).each do |layout|
|
||||||
- next if layout.hidden?
|
- next if layout.hidden?
|
||||||
%tr
|
%tr
|
||||||
%th= layout.humanized_name
|
%th= layout.humanized_name
|
||||||
|
@ -34,14 +36,13 @@
|
||||||
= render 'sites/build', site: @site
|
= render 'sites/build', site: @site
|
||||||
|
|
||||||
- if @site.design.credits
|
- if @site.design.credits
|
||||||
.alert.alert-primary{ role: 'alert' }
|
= render 'bootstrap/alert' do
|
||||||
= sanitize_markdown @site.design.credits
|
= sanitize_markdown @site.design.credits
|
||||||
= link_to t('sites.donations.text'), t('sites.donations.url'), class: 'btn'
|
= link_to t('sites.donations.text'), t('sites.donations.url'), class: 'btn'
|
||||||
- if @site.design.designer_url
|
- if @site.design.designer_url
|
||||||
= link_to t('sites.designer_url'), @site.design.designer_url, class: 'btn'
|
= link_to t('sites.designer_url'), @site.design.designer_url, class: 'btn'
|
||||||
|
|
||||||
%section.col
|
%section.col
|
||||||
= render 'layouts/flash'
|
|
||||||
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
|
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
|
||||||
%form{ action: site_posts_path }
|
%form{ action: site_posts_path }
|
||||||
- @filter_params.each do |param, value|
|
- @filter_params.each do |param, value|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
- unless site.errors.empty?
|
- unless site.errors.empty?
|
||||||
.alert.alert-info
|
- title = t('.errors.title')
|
||||||
%h4= t('.errors.title')
|
- help = t('.errors.help')
|
||||||
%p.lead= t('.errors.help')
|
= render 'bootstrap/alert' do
|
||||||
|
%h4= title
|
||||||
|
%p.lead= help
|
||||||
%ul
|
%ul
|
||||||
- site.errors.messages.each_pair do |attr, error|
|
- site.errors.messages.each_pair do |attr, error|
|
||||||
- attr = attr.to_s
|
- attr = attr.to_s
|
||||||
|
@ -48,13 +50,13 @@
|
||||||
%h2= t('.design.title')
|
%h2= t('.design.title')
|
||||||
%p.lead= t('.help.design')
|
%p.lead= t('.help.design')
|
||||||
- if invalid? site, :design_id
|
- if invalid? site, :design_id
|
||||||
.alert.alert-info
|
= render 'bootstrap/alert' do
|
||||||
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
|
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
|
||||||
layouts: site.incompatible_layouts.to_sentence)
|
layouts: site.incompatible_layouts.to_sentence)
|
||||||
.row.designs
|
.row.row-cols-1.row-cols-md-2.designs
|
||||||
-# Demasiado complejo para un f.collection_radio_buttons
|
-# Demasiado complejo para un f.collection_radio_buttons
|
||||||
- Design.all.find_each do |design|
|
- Design.all.find_each do |design|
|
||||||
.design.col-md-4.d-flex.flex-column
|
.design.col.d-flex.flex-column
|
||||||
.custom-control.custom-radio
|
.custom-control.custom-radio
|
||||||
= f.radio_button :design_id, design.id,
|
= f.radio_button :design_id, design.id,
|
||||||
checked: design.id == site.design_id,
|
checked: design.id == site.design_id,
|
||||||
|
@ -79,9 +81,11 @@
|
||||||
%h2= t('.licencia.title')
|
%h2= t('.licencia.title')
|
||||||
%p.lead= t('.help.licencia')
|
%p.lead= t('.help.licencia')
|
||||||
- Licencia.all.find_each do |licencia|
|
- Licencia.all.find_each do |licencia|
|
||||||
|
- next if licencia.custom? && site.licencia != licencia
|
||||||
.row.license
|
.row.license
|
||||||
.col
|
.col
|
||||||
.media.mt-1
|
.media.mt-1
|
||||||
|
- unless licencia.custom?
|
||||||
= image_tag licencia.icons, alt: licencia.name, class: 'mr-3 mt-4'
|
= image_tag licencia.icons, alt: licencia.name, class: 'mr-3 mt-4'
|
||||||
.media-body
|
.media-body
|
||||||
.custom-control.custom-radio
|
.custom-control.custom-radio
|
||||||
|
@ -93,8 +97,8 @@
|
||||||
= sanitize_markdown licencia.description,
|
= sanitize_markdown licencia.description,
|
||||||
tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6]
|
tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6]
|
||||||
|
|
||||||
= link_to t('.licencia.url'), licencia.url,
|
- unless licencia.custom?
|
||||||
target: '_blank', class: 'btn'
|
= link_to t('.licencia.url'), licencia.url, target: '_blank', class: 'btn', rel: 'noopener'
|
||||||
|
|
||||||
%hr/
|
%hr/
|
||||||
|
|
||||||
|
@ -156,9 +160,6 @@
|
||||||
= f.fields_for :deploys do |deploy|
|
= f.fields_for :deploys do |deploy|
|
||||||
= render "deploys/#{deploy.object.type.underscore}",
|
= render "deploys/#{deploy.object.type.underscore}",
|
||||||
deploy: deploy, site: site
|
deploy: deploy, site: site
|
||||||
- else
|
|
||||||
= f.fields_for :deploys do |deploy|
|
|
||||||
= deploy.hidden_field :type
|
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.submit submit, class: 'btn btn-lg btn-block'
|
= f.submit submit, class: 'btn btn-lg btn-block'
|
||||||
|
|
19
app/views/sites/_status.haml
Normal file
19
app/views/sites/_status.haml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
- link = nil
|
||||||
|
- if site.not_published_yet?
|
||||||
|
- message = t('.not_published_yet')
|
||||||
|
- if 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.url, class: 'alert-link'
|
1
config/credentials.yml.enc.ci
Normal file
1
config/credentials.yml.enc.ci
Normal file
|
@ -0,0 +1 @@
|
||||||
|
1jEfzfldP9tT4+HWfhP48I9hw31gYCnnxHWpYjPrcTm/pgkFdiG+mDa6y31EOxzs50w6FEw2GO127BnyBSUIPIxuWY0cR96xL5pVrS3vjyzM84QN4lJF9ER0Tz1AQ9S7NJ54CelSkMfFt/rf+O4YM8cLtdSVsVC/HlGbp16p3D1pm4MFo5cQb0hEmlyyYlzEn4oJtsp/MCIwI4+z8oFhxKdMIxdbiw+KS/7PBRfMm1h5rdGORCnD69iVmnXseMvVtZn9A7N7uR6+gFlhxlD5yyEW0pwTj3tbu9NeIOVbtmYOL5ZhLW9REXtGTqR5Op/LN+ukIXbDNEScKltJXUdWfa9Pd/QjVT8IMURZ04POEMDgs1cw363yz4f+WQForhSco9oYLDOd5hTGRXoZ9fnjnfJSTjINM62hkfDY3w3+s844nNbjbj+lPTJHU/QjRhcuNqBDDxWUfwTmRIqm5zrelnHnZnuFmFwCNet6NChC6EFUAFjrals6kTSQllyMt4xImqA+HL7DnjWj6VURSH+nGQTA4tQvDdfbDwTzg/PvRkJcsy2dRd135RQdmRZ+8KXBviLabwdR256vaCqSO1j+jyeUPGLll35ghyLxncyBkkAKt1zaDRPDWgVafg0gJ3v7hVV5TYgToPzlv4w88KPCY7cBhkb1qGoXAhtO6iAuZYK9eyZd1gNQJKyqbcLqA5aTTX/ylfdbptWhaZ8ibB8KBgVyn2RmrOHEhB38rDSMHHNfK3Xs4/hhqMFIGHGTGCUYVmjCzhVFd15yRurU32d3YtP8W4L77H7qkFsF1gnvsZx+R084LcJqknwY94dmjtUE4x2u+Qh3ElFj--lr8JoUq1WH9xXNsB--mE8hxHADL7SbDWabAPY1+Q==
|
|
@ -13,6 +13,10 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||||
inflect.singular 'roles', 'rol'
|
inflect.singular 'roles', 'rol'
|
||||||
inflect.plural 'rollup', 'rollups'
|
inflect.plural 'rollup', 'rollups'
|
||||||
inflect.singular 'rollups', 'rollup'
|
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
|
end
|
||||||
|
|
||||||
ActiveSupport::Inflector.inflections(:es) do |inflect|
|
ActiveSupport::Inflector.inflections(:es) do |inflect|
|
||||||
|
@ -28,4 +32,8 @@ ActiveSupport::Inflector.inflections(:es) do |inflect|
|
||||||
inflect.singular 'licencias', 'licencia'
|
inflect.singular 'licencias', 'licencia'
|
||||||
inflect.plural 'rollup', 'rollups'
|
inflect.plural 'rollup', 'rollups'
|
||||||
inflect.singular 'rollups', 'rollup'
|
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
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Enviar una notificación cuando falla una tarea
|
# Enviar una notificación cuando falla una tarea
|
||||||
SuckerPunch.exception_handler = lambda { |ex, _klass, _args|
|
SuckerPunch.exception_handler = lambda { |ex, _, args|
|
||||||
ExceptionNotifier.notify_exception(ex)
|
ExceptionNotifier.notify_exception(ex, data: args.last)
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,26 @@ en:
|
||||||
new:
|
new:
|
||||||
sign_up: Sign up
|
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.
|
help: We only ask for an e-mail address and a password. The password is safely stored, no one else besides you knows it! You'll also receive an e-mail to confirm your account.
|
||||||
signed_up: Welcome! You have signed up successfully.
|
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_inactive: You have signed up successfully. However, we could not sign you in because your account is not yet activated.
|
||||||
signed_up_but_locked: You have signed up successfully. However, we could not sign you in because your account is locked.
|
signed_up_but_locked: You have signed up successfully. However, we could not sign you in because your account is locked.
|
||||||
signed_up_but_unconfirmed: A message with a confirmation link has been sent to your email address. Please follow the link to activate your account.
|
signed_up_but_unconfirmed: A message with a confirmation link has been sent to your email address. Please follow the link to activate your account.
|
||||||
|
@ -138,7 +157,7 @@ en:
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
already_confirmed: was already confirmed, please try signing in
|
already_confirmed: was already confirmed, please try signing in
|
||||||
confirmation_period_expired: needs to be confirmed within %{period}, please request a new one
|
confirmation_period_expired: "wasn't confirmed within %{period}. Please request a new confirmation link by using the \"Resend confirmation instructions\" button below and find it in your inbox."
|
||||||
expired: has expired, please request a new one
|
expired: has expired, please request a new one
|
||||||
not_found: not found
|
not_found: not found
|
||||||
not_locked: was not locked
|
not_locked: was not locked
|
||||||
|
|
|
@ -104,7 +104,25 @@ es:
|
||||||
new:
|
new:
|
||||||
sign_up: Registrarme
|
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.
|
help: Para registrarte solo pedimos una dirección de correo y una contraseña. La contraseña se almacena de forma segura, ¡nadie más que vos la sabe! Recibirás un correo de confirmación de cuenta.
|
||||||
signed_up: Bienvenide. Tu cuenta fue creada.
|
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_inactive: Tu cuenta ha sido creada correctamente. Sin embargo, no hemos podido iniciar la sesión porque tu cuenta aún no está activada.
|
||||||
signed_up_but_locked: Tu cuenta ha sido creada correctamente. Sin embargo, no hemos podido iniciar la sesión porque que tu cuenta está bloqueada.
|
signed_up_but_locked: Tu cuenta ha sido creada correctamente. Sin embargo, no hemos podido iniciar la sesión porque que tu cuenta está bloqueada.
|
||||||
signed_up_but_unconfirmed: Para recibir actualizaciones, se ha enviado un mensaje con un enlace de confirmación a tu correo electrónico. Abre el enlace para activar tu cuenta.
|
signed_up_but_unconfirmed: Para recibir actualizaciones, se ha enviado un mensaje con un enlace de confirmación a tu correo electrónico. Abre el enlace para activar tu cuenta.
|
||||||
|
@ -138,7 +156,7 @@ es:
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
already_confirmed: ya ha sido confirmada, por favor intenta iniciar sesión
|
already_confirmed: ya ha sido confirmada, por favor intenta iniciar sesión
|
||||||
confirmation_period_expired: necesita confirmarse dentro de %{period}, por favor solicita una nueva
|
confirmation_period_expired: "quedó sin confirmar luego de %{period}. Por favor, usa el botón \"Reenviar instrucciones de confirmación\" y busca el nuevo link en tu casilla."
|
||||||
expired: ha expirado, por favor solicita una nueva
|
expired: ha expirado, por favor solicita una nueva
|
||||||
not_found: no se ha encontrado
|
not_found: no se ha encontrado
|
||||||
not_locked: no estaba bloqueada
|
not_locked: no estaba bloqueada
|
||||||
|
|
|
@ -13,6 +13,9 @@ en:
|
||||||
ar:
|
ar:
|
||||||
name: Arabic
|
name: Arabic
|
||||||
dir: rtl
|
dir: rtl
|
||||||
|
ur:
|
||||||
|
name: Urdu
|
||||||
|
dir: rtl
|
||||||
zh:
|
zh:
|
||||||
name: Chinese
|
name: Chinese
|
||||||
dir: ltr
|
dir: ltr
|
||||||
|
@ -114,6 +117,10 @@ en:
|
||||||
title: Alternative domain name
|
title: Alternative domain name
|
||||||
success: Success!
|
success: Success!
|
||||||
error: Error
|
error: Error
|
||||||
|
deploy_distributed_press:
|
||||||
|
title: Distributed Web
|
||||||
|
success: Success!
|
||||||
|
error: Error
|
||||||
deploy_reindex:
|
deploy_reindex:
|
||||||
title: Reindex
|
title: Reindex
|
||||||
success: Success!
|
success: Success!
|
||||||
|
@ -126,6 +133,14 @@ en:
|
||||||
title: Synchronize to backup server
|
title: Synchronize to backup server
|
||||||
success: Success!
|
success: Success!
|
||||||
error: Error
|
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
|
help: You can contact us by replying to this e-mail
|
||||||
maintenance_mailer:
|
maintenance_mailer:
|
||||||
notice:
|
notice:
|
||||||
|
@ -272,6 +287,22 @@ en:
|
||||||
|
|
||||||
Only accessible through [Tor
|
Only accessible through [Tor
|
||||||
Browser](https://www.torproject.org/download/)
|
Browser](https://www.torproject.org/download/)
|
||||||
|
deploy_distributed_press:
|
||||||
|
title: 'Publish to the distributed Web'
|
||||||
|
help: |
|
||||||
|
Make your site available through peer-to-peer protocols,
|
||||||
|
Inter-Planetary File System (IPFS), Hypercore, and via
|
||||||
|
BitTorrent, so your site is more resilient and can be available
|
||||||
|
offline, including in community mesh networks.
|
||||||
|
|
||||||
|
**Important:** Only use this option if you would like your data
|
||||||
|
to be permanently available. If you decide to undo this
|
||||||
|
selection, a cleared version of the site will be shared in its
|
||||||
|
place. However, it is possible that nodes on the distributed
|
||||||
|
storage network may continue retaining copies of the data
|
||||||
|
indefinitely.
|
||||||
|
|
||||||
|
[Learn more](https://ffdweb.org/building-distributed-press-a-publishing-tool-for-the-decentralized-web/)
|
||||||
stats:
|
stats:
|
||||||
index:
|
index:
|
||||||
title: Statistics
|
title: Statistics
|
||||||
|
@ -330,6 +361,10 @@ en:
|
||||||
designer_url: 'Support the designer'
|
designer_url: 'Support the designer'
|
||||||
static_file_migration: 'File migration'
|
static_file_migration: 'File migration'
|
||||||
find_and_replace: 'Search and replace'
|
find_and_replace: 'Search and replace'
|
||||||
|
status:
|
||||||
|
building: "Your site is building, please wait <time datetime=\"PT%{seconds}S\">%{average_time}</time> to refresh this page..."
|
||||||
|
not_published_yet: "Your site is being published for the first time, please wait up to 1 minute..."
|
||||||
|
available: "Your site is available! Click here to visit it."
|
||||||
index:
|
index:
|
||||||
title: 'My Sites'
|
title: 'My Sites'
|
||||||
pull: 'Upgrade'
|
pull: 'Upgrade'
|
||||||
|
|
|
@ -13,6 +13,9 @@ es:
|
||||||
ar:
|
ar:
|
||||||
name: Árabe
|
name: Árabe
|
||||||
dir: rtl
|
dir: rtl
|
||||||
|
ur:
|
||||||
|
name: Urdu
|
||||||
|
dir: rtl
|
||||||
zh:
|
zh:
|
||||||
name: Chino
|
name: Chino
|
||||||
dir: ltr
|
dir: ltr
|
||||||
|
@ -114,6 +117,10 @@ es:
|
||||||
title: Dominio alternativo
|
title: Dominio alternativo
|
||||||
success: ¡Éxito!
|
success: ¡Éxito!
|
||||||
error: Hubo un error
|
error: Hubo un error
|
||||||
|
deploy_distributed_press:
|
||||||
|
title: Web distribuida
|
||||||
|
success: ¡Éxito!
|
||||||
|
error: Hubo un error
|
||||||
deploy_reindex:
|
deploy_reindex:
|
||||||
title: Reindexación
|
title: Reindexación
|
||||||
success: ¡Éxito!
|
success: ¡Éxito!
|
||||||
|
@ -126,6 +133,14 @@ es:
|
||||||
title: Sincronizar al servidor alternativo
|
title: Sincronizar al servidor alternativo
|
||||||
success: ¡Éxito!
|
success: ¡Éxito!
|
||||||
error: Hubo un error
|
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.
|
help: Por cualquier duda, responde este correo para contactarte con nosotres.
|
||||||
maintenance_mailer:
|
maintenance_mailer:
|
||||||
notice:
|
notice:
|
||||||
|
@ -277,6 +292,22 @@ es:
|
||||||
|
|
||||||
Sólo será accesible a través del [Navegador
|
Sólo será accesible a través del [Navegador
|
||||||
Tor](https://www.torproject.org/es/download/).
|
Tor](https://www.torproject.org/es/download/).
|
||||||
|
deploy_distributed_press:
|
||||||
|
title: 'Publicar a la Web distribuida'
|
||||||
|
help: |
|
||||||
|
Utiliza protocolos de pares, Inter-Planetary File System (IPFS),
|
||||||
|
Hypercore y torrents, para que tu sitio sea más resiliente y
|
||||||
|
esté disponible _offline_, inclusive en redes _mesh_
|
||||||
|
comunitarias.
|
||||||
|
|
||||||
|
**Importante:** Sólo usa esta opción si te parece correcto que
|
||||||
|
tu contenido esté disponible permanentemente. Cuando elijas
|
||||||
|
des-hacer esta acción, una versión "vacía" del sitio será
|
||||||
|
compartida en su lugar. Sin embargo, es posible que algunos
|
||||||
|
nodos en la red de almacenamiento distribuida puedan retener
|
||||||
|
copias de tu contenido indefinidamente.
|
||||||
|
|
||||||
|
[Saber más (en inglés)](https://ffdweb.org/building-distributed-press-a-publishing-tool-for-the-decentralized-web/)
|
||||||
stats:
|
stats:
|
||||||
index:
|
index:
|
||||||
title: Estadísticas
|
title: Estadísticas
|
||||||
|
@ -335,6 +366,10 @@ es:
|
||||||
designer_url: 'Apoyá a le(s) diseñadore(s)'
|
designer_url: 'Apoyá a le(s) diseñadore(s)'
|
||||||
static_file_migration: 'Migración de archivos'
|
static_file_migration: 'Migración de archivos'
|
||||||
find_and_replace: 'Búsqueda y reemplazo'
|
find_and_replace: 'Búsqueda y reemplazo'
|
||||||
|
status:
|
||||||
|
building: "Tu sitio se está publicando, por favor espera <time datetime=\"PT%{seconds}S\">%{average_time}</time> para recargar esta página..."
|
||||||
|
not_published_yet: "Tu sitio se está publicando por primera vez, por favor espera hasta un minuto..."
|
||||||
|
available: "¡Tu sitio está disponible! Cliquea aquí para visitarlo."
|
||||||
index:
|
index:
|
||||||
title: 'Mis sitios'
|
title: 'Mis sitios'
|
||||||
pull: 'Actualizar'
|
pull: 'Actualizar'
|
||||||
|
|
|
@ -11,8 +11,6 @@ Rails.application.routes.draw do
|
||||||
namespace :v1 do
|
namespace :v1 do
|
||||||
resources :csp_reports, only: %i[create]
|
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
|
resources :sites, only: %i[index], constraints: { site_id: /[a-z0-9\-.]+/, id: /[a-z0-9\-.]+/ } do
|
||||||
get :'invitades/cookie', to: 'invitades#cookie'
|
get :'invitades/cookie', to: 'invitades#cookie'
|
||||||
post :'posts/:layout', to: 'posts#create', as: :posts
|
post :'posts/:layout', to: 'posts#create', as: :posts
|
||||||
|
|
|
@ -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
|
|
@ -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
|
22
db/migrate/20230322214924_add_code_of_conduct.rb
Normal file
22
db/migrate/20230322214924_add_code_of_conduct.rb
Normal file
|
@ -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
|
22
db/migrate/20230322231344_add_privacy_policy.rb
Normal file
22
db/migrate/20230322231344_add_privacy_policy.rb
Normal file
|
@ -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
|
|
@ -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
|
12
db/migrate/20230328200129_add_consent_to_usuaries.rb
Normal file
12
db/migrate/20230328200129_add_consent_to_usuaries.rb
Normal file
|
@ -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
|
|
@ -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
|
613
db/seeds/codes_of_conduct.yml
Normal file
613
db/seeds/codes_of_conduct.yml
Normal file
|
@ -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:
|
||||||
|
|
||||||
|
* <https://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy>
|
||||||
|
|
||||||
|
* <https://trans-code.org/code-of-conduct/>
|
||||||
|
|
||||||
|
* <https://openhardware.science/logistics/gosh-code-of-conduct/>
|
||||||
|
|
||||||
|
* <https://hypatiasoftware.org/code-of-conduct/>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
* <https://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy>
|
||||||
|
|
||||||
|
* <https://trans-code.org/code-of-conduct/>
|
||||||
|
|
||||||
|
* <https://openhardware.science/logistics/gosh-code-of-conduct/>
|
||||||
|
|
||||||
|
* <https://hypatiasoftware.org/code-of-conduct/>
|
||||||
|
|
||||||
|
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.
|
|
@ -23,7 +23,7 @@
|
||||||
- name_en: 'Sutty'
|
- name_en: 'Sutty'
|
||||||
name_es: 'Sutty'
|
name_es: 'Sutty'
|
||||||
gem: 'sutty-jekyll-theme'
|
gem: 'sutty-jekyll-theme'
|
||||||
url: 'https://rubygems.org/gems/sutty-jekyll-theme/'
|
url: "https://anarres.sutty.nl"
|
||||||
description_en: "The Sutty design"
|
description_en: "The Sutty design"
|
||||||
description_es: 'El diseño de Sutty'
|
description_es: 'El diseño de Sutty'
|
||||||
license: 'https://0xacab.org/sutty/jekyll/sutty-jekyll-theme/-/blob/master/LICENSE.txt'
|
license: 'https://0xacab.org/sutty/jekyll/sutty-jekyll-theme/-/blob/master/LICENSE.txt'
|
||||||
|
|
|
@ -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_en: 'Peer Production License'
|
||||||
name_es: 'Licencia de Producción de Pares'
|
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"
|
icons: "/images/ppl.png"
|
||||||
url_en: 'https://wiki.p2pfoundation.net/Peer_Production_License'
|
url_en: 'https://wiki.p2pfoundation.net/Peer_Production_License'
|
||||||
url_es: 'https://endefensadelsl.org/ppl_es.html'
|
url_es: 'https://endefensadelsl.org/ppl_es.html'
|
||||||
|
@ -100,6 +113,8 @@
|
||||||
hacerlo es enlazar a esta página.
|
hacerlo es enlazar a esta página.
|
||||||
|
|
||||||
- icons: "/images/by.png"
|
- 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)'
|
name_en: 'Creative Commons Attribution 4.0 International (CC BY 4.0)'
|
||||||
description_en: "This license gives everyone the freedom to use,
|
description_en: "This license gives everyone the freedom to use,
|
||||||
adapt, and redistribute the contents of your site by requiring
|
adapt, and redistribute the contents of your site by requiring
|
||||||
|
@ -194,6 +209,8 @@
|
||||||
- icons: "/images/sa.png"
|
- icons: "/images/sa.png"
|
||||||
name_en: "Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)"
|
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)"
|
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_en: 'https://creativecommons.org/licenses/by-sa/4.0/'
|
||||||
url_es: 'https://creativecommons.org/licenses/by-sa/4.0/deed.es'
|
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
|
description_en: "This license is the same as the CC-BY 4.0 but it adds
|
||||||
|
|
113
db/seeds/privacy_policies.yml
Normal file
113
db/seeds/privacy_policies.yml
Normal file
|
@ -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.
|
10
lib/tasks/distributed_press.rake
Normal file
10
lib/tasks/distributed_press.rake
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
namespace :distributed_press do
|
||||||
|
namespace :tokens do
|
||||||
|
desc 'Renew tokens'
|
||||||
|
task renew: :environment do
|
||||||
|
RenewDistributedPressTokensJob.perform_now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
monit.conf
10
monit.conf
|
@ -4,6 +4,11 @@ check program cleanup
|
||||||
every "0 3 1 * *"
|
every "0 3 1 * *"
|
||||||
if status != 0 then alert
|
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
|
check program access_logs
|
||||||
with path "/srv/http/bin/access_logs" as uid "app" and gid "www-data"
|
with path "/srv/http/bin/access_logs" as uid "app" and gid "www-data"
|
||||||
every "0 0 * * *"
|
every "0 0 * * *"
|
||||||
|
@ -13,3 +18,8 @@ check program stats
|
||||||
with path "/usr/bin/foreman run -f /srv/Procfile -d /srv stats" as uid "rails" gid "www-data"
|
with path "/usr/bin/foreman run -f /srv/Procfile -d /srv stats" as uid "rails" gid "www-data"
|
||||||
every "0 1 * * *"
|
every "0 1 * * *"
|
||||||
if status != 0 then alert
|
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
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sutty",
|
"name": "sutty",
|
||||||
|
"author": "Sutty <hi@sutty.nl>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@airbrake/browser": "^1.4.1",
|
"@airbrake/browser": "^1.4.1",
|
||||||
|
|
Loading…
Reference in a new issue