diff --git a/.env.example b/.env.example
index fb086224..f3cf48d9 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,9 @@
+# pwgen -1 32
+RAILS_MASTER_KEY=11111111111111111111111111111111
RAILS_GROUPS=assets
DELEGATE=athshe.sutty.nl
HAINISH=../haini.sh/haini.sh
-DATABASE=
+DATABASE_URL=postgres://suttier@postgresql.sutty.local/sutty
RAILS_ENV=development
IMAP_SERVER=
DEFAULT_FROM=
diff --git a/.woodpecker.yml b/.woodpecker.yml
new file mode 100644
index 00000000..2e775624
--- /dev/null
+++ b/.woodpecker.yml
@@ -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"
diff --git a/Dockerfile b/Dockerfile
index 71edc3d5..e0f1dc9f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,9 @@
-FROM registry.nulo.in/sutty/rails:3.13.6-2.7.5
-ARG PANDOC_VERSION=2.17.1.1
+ARG RUBY_VERSION=2.7
+ARG RUBY_PATCH=6
+ARG ALPINE_VERSION=3.13.10
+ARG BASE_IMAGE=registry.nulo.in/sutty/rails
+FROM ${BASE_IMAGE}:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}
+ARG PANDOC_VERSION=2.18
ENV RAILS_ENV production
# Instalar las dependencias, separamos la librería de base de datos para
@@ -10,10 +14,11 @@ ENV RAILS_ENV production
# principal
RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \
rsync git jpegoptim vips tectonic oxipng git-lfs openssh-client \
- yarn daemonize ruby-webrick
+ yarn daemonize ruby-webrick postgresql-client dateutils file
RUN gem install --no-document --no-user-install foreman
RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc
+RUN apk add npm && npm install -g pnpm@~7 && apk del npm
COPY ./monit.conf /etc/monit.d/sutty.conf
diff --git a/Procfile b/Procfile
index 79daa90b..4cc6e5b3 100644
--- a/Procfile
+++ b/Procfile
@@ -1,3 +1,11 @@
+migrate: bundle exec rake db:prepare db:seed
+sutty: bundle exec puma config.ru
+blazer_5m: bundle exec rake blazer:run_checks SCHEDULE="5 minutes"
+blazer_1h: bundle exec rake blazer:run_checks SCHEDULE="1 hour"
+blazer_1d: bundle exec rake blazer:run_checks SCHEDULE="1 day"
+blazer: bundle exec rake blazer:send_failing_checks
+prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
+distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
cleanup: bundle exec rake cleanup:everything
stats: bundle exec rake stats:process_all
distributed_press_renew_tokens: bundle exec rake distributed_press:tokens:renew
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 2f52829b..bba48558 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -158,6 +158,12 @@ ol.breadcrumb {
transition: all 3s;
}
+fieldset {
+ legend {
+ font-size: 1rem;
+ }
+}
+
.mapable,
.taggable {
.input-map,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e80c279d..b4be5a97 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
before_action :prepare_exception_notifier
before_action :configure_permitted_parameters, if: :devise_controller?
+ before_action :notify_unconfirmed_email, unless: :devise_controller?
around_action :set_locale
rescue_from Pundit::NilPolicyError, with: :page_not_found
@@ -27,6 +28,15 @@ class ApplicationController < ActionController::Base
private
+ def notify_unconfirmed_email
+ return unless current_usuarie
+ return if current_usuarie.confirmed?
+
+ I18n.with_locale(current_usuarie.lang) do
+ flash[:notice] ||= I18n.t('devise.registrations.signed_up')
+ end
+ end
+
def uuid?(string)
/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string
end
@@ -84,6 +94,7 @@ class ApplicationController < ActionController::Base
protected
def configure_permitted_parameters
+ devise_parameter_sanitizer.permit(:sign_up, keys: Usuarie::CONSENT_FIELDS)
devise_parameter_sanitizer.permit(:account_update, keys: %i[lang])
end
diff --git a/app/lib/active_storage/service/jekyll_service.rb b/app/lib/active_storage/service/jekyll_service.rb
index 88ffa83c..e6c5fda6 100644
--- a/app/lib/active_storage/service/jekyll_service.rb
+++ b/app/lib/active_storage/service/jekyll_service.rb
@@ -4,11 +4,6 @@ module ActiveStorage
class Service
# Sube los archivos a cada repositorio y los agrega al LFS de su
# repositorio git.
- #
- # @todo: Implementar LFS. No nos gusta mucho la idea porque duplica
- # el espacio en disco, pero es la única forma que tenemos (hasta que
- # implementemos IPFS) para poder transferir los archivos junto con el
- # sitio.
class JekyllService < Service::DiskService
# Genera un servicio para un sitio determinado
#
@@ -27,7 +22,10 @@ module ActiveStorage
# @param :checksum [String]
def upload(key, io, checksum: nil, **)
instrument :upload, key: key, checksum: checksum do
- IO.copy_stream(io, make_path_for(key)) unless exist?(key)
+ unless exist?(key)
+ IO.copy_stream(io, make_path_for(key))
+ LfsObjectService.new(site: site, blob: blob_for(key)).process
+ end
ensure_integrity_of(key, checksum) if checksum
end
end
@@ -79,7 +77,7 @@ module ActiveStorage
# @param :key [String]
# @return [String]
def filename_for(key)
- ActiveStorage::Blob.where(key: key).limit(1).pluck(:filename).first.tap do |filename|
+ blob_for(key).filename.to_s.tap do |filename|
raise ArgumentError, "Filename for key #{key} is blank" if filename.blank?
end
end
@@ -91,6 +89,15 @@ module ActiveStorage
def path_for(key)
File.join root, folder_for(key), filename_for(key)
end
+
+ # @return [Site]
+ def site
+ @site ||= Site.find_by_name(name)
+ end
+
+ def blob_for(key)
+ ActiveStorage::Blob.find_by(key: key, service_name: name)
+ end
end
end
end
diff --git a/app/lib/devise/failure_app_decorator.rb b/app/lib/devise/failure_app_decorator.rb
new file mode 100644
index 00000000..f17cb482
--- /dev/null
+++ b/app/lib/devise/failure_app_decorator.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Devise
+ module FailureAppDecorator
+ extend ActiveSupport::Concern
+
+ included do
+ include AbstractController::Callbacks
+
+ around_action :set_locale
+
+ private
+
+ def set_locale(&action)
+ I18n.with_locale(session[:locale] || I18n.locale, &action)
+ end
+ end
+ end
+end
+
+Devise::FailureApp.include Devise::FailureAppDecorator
diff --git a/app/models/code_of_conduct.rb b/app/models/code_of_conduct.rb
new file mode 100644
index 00000000..87c24c7f
--- /dev/null
+++ b/app/models/code_of_conduct.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Códigos de conducta
+class CodeOfConduct < ApplicationRecord
+ extend Mobility
+
+ translates :title, type: :string, locale_accessors: true
+ translates :description, type: :text, locale_accessors: true
+ translates :content, type: :text, locale_accessors: true
+
+ validates :title, presence: true, uniqueness: true
+ validates :description, presence: true
+ validates :content, presence: true
+end
diff --git a/app/models/deploy_local.rb b/app/models/deploy_local.rb
index 39cd9622..71c23b36 100644
--- a/app/models/deploy_local.rb
+++ b/app/models/deploy_local.rb
@@ -105,6 +105,14 @@ class DeployLocal < Deploy
File.exist? yarn_lock
end
+ def pnpm_lock
+ File.join(site.path, 'pnpm-lock.yaml')
+ end
+
+ def pnpm_lock?
+ File.exist? pnpm_lock
+ end
+
def gem(output: false)
run %(gem install bundler --no-document), output: output
end
diff --git a/app/models/licencia.rb b/app/models/licencia.rb
index c0eb1c80..65009f46 100644
--- a/app/models/licencia.rb
+++ b/app/models/licencia.rb
@@ -7,6 +7,7 @@ class Licencia < ApplicationRecord
translates :name, type: :string, locale_accessors: true
translates :url, type: :string, locale_accessors: true
translates :description, type: :text, locale_accessors: true
+ translates :short_description, type: :string, locale_accessors: true
translates :deed, type: :text, locale_accessors: true
has_many :sites
@@ -14,5 +15,10 @@ class Licencia < ApplicationRecord
validates :name, presence: true, uniqueness: true
validates :url, presence: true
validates :description, presence: true
+ validates :short_description, presence: true
validates :deed, presence: true
+
+ def custom?
+ icons == 'custom'
+ end
end
diff --git a/app/models/metadata_locales.rb b/app/models/metadata_locales.rb
index 4d540efc..37b50286 100644
--- a/app/models/metadata_locales.rb
+++ b/app/models/metadata_locales.rb
@@ -1,22 +1,49 @@
# frozen_string_literal: true
# Los valores de este metadato son artículos en otros idiomas
-class MetadataLocales < MetadataTemplate
- def default_value
- super || []
- end
-
+class MetadataLocales < MetadataHasAndBelongsToMany
# Todos los valores posibles para cada idioma disponible
#
- # TODO: Optimizar?
- # TODO: Mantener sincronizados
- #
# @return { lang: { title: uuid } }
def values
@values ||= site.locales.map do |locale|
- [locale, site.posts(lang: locale).map do |post|
- [post.title.value, post.uuid.value]
+ [locale, posts.where(lang: locale).map do |post|
+ [title(post), post.uuid.value]
end.to_h]
end.to_h
end
+
+ # Siempre hay una relación inversa
+ #
+ # @return [True]
+ def inverse?
+ true
+ end
+
+ # El campo inverso se llama igual en el otro post
+ #
+ # @return [Symbol]
+ def inverse
+ :locales
+ end
+
+ private
+
+ # Obtiene todos los locales distintos a este post
+ #
+ # @return [Array]
+ def other_locales
+ site.locales.reject do |locale|
+ locale == post.lang.value.to_sym
+ end
+ end
+
+ # Obtiene todos los posts de los otros locales con el mismo layout
+ #
+ # @return [PostRelation]
+ def posts
+ other_locales.map do |locale|
+ site.posts(lang: locale).where(layout: post.layout.value)
+ end.reduce(&:concat) || PostRelation.new(site: site, lang: 'any')
+ end
end
diff --git a/app/models/privacy_policy.rb b/app/models/privacy_policy.rb
new file mode 100644
index 00000000..8805daa9
--- /dev/null
+++ b/app/models/privacy_policy.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Políticas de privacidad
+class PrivacyPolicy < ApplicationRecord
+ extend Mobility
+
+ translates :title, type: :string, locale_accessors: true
+ translates :description, type: :text, locale_accessors: true
+ translates :content, type: :text, locale_accessors: true
+
+ validates :title, presence: true, uniqueness: true
+ validates :description, presence: true
+ validates :content, presence: true
+end
diff --git a/app/models/site.rb b/app/models/site.rb
index af0b2c53..3f2aa34e 100644
--- a/app/models/site.rb
+++ b/app/models/site.rb
@@ -8,6 +8,7 @@ class Site < ApplicationRecord
include Site::FindAndReplace
include Site::Api
include Site::DeployDependencies
+ include Site::BuildStats
include Tienda
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
@@ -554,10 +555,33 @@ class Site < ApplicationRecord
Dir.chdir path, &block
end
+ # Instala las gemas cuando es necesario:
+ #
+ # * El sitio existe
+ # * No están instaladas
+ # * El archivo Gemfile se modificó
+ # * El archivo Gemfile.lock se modificó
def install_gems
return unless persisted?
- return if Rails.root.join('_storage', 'gems', name).directory?
- deploys.find_by_type('DeployLocal').send(:bundle)
+ if !gem_dir? || gemfile_updated? || gemfile_lock_updated?
+ deploys.find_by_type('DeployLocal').send(:bundle)
+ touch
+ end
+ end
+
+ # Detecta si el repositorio de gemas existe
+ def gem_dir?
+ Rails.root.join('_storage', 'gems', name).directory?
+ end
+
+ # Detecta si el Gemfile fue modificado
+ def gemfile_updated?
+ updated_at < File.mtime(File.join(path, 'Gemfile'))
+ end
+
+ # Detecta si el Gemfile.lock fue modificado
+ def gemfile_lock_updated?
+ updated_at < File.mtime(File.join(path, 'Gemfile.lock'))
end
end
diff --git a/app/models/site/build_stats.rb b/app/models/site/build_stats.rb
new file mode 100644
index 00000000..6eebcc84
--- /dev/null
+++ b/app/models/site/build_stats.rb
@@ -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
diff --git a/app/models/site/config.rb b/app/models/site/config.rb
index d2e78d98..fb9175c1 100644
--- a/app/models/site/config.rb
+++ b/app/models/site/config.rb
@@ -31,7 +31,7 @@ class Site
# Escribe los cambios en el repositorio
def write
- return if persisted?
+ return true if persisted?
@saved = Site::Writer.new(site: site, file: path, content: content.to_yaml).save.tap do |result|
# Actualizar el hash para no escribir dos veces
diff --git a/app/models/site/repository.rb b/app/models/site/repository.rb
index f63288d4..62e4c45e 100644
--- a/app/models/site/repository.rb
+++ b/app/models/site/repository.rb
@@ -117,6 +117,9 @@ class Site
def commit(file:, usuarie:, message:, remove: false)
file = [file] unless file.respond_to? :each
+ # Cargar el árbol actual
+ rugged.index.read_tree rugged.head.target.tree
+
file.each do |f|
remove ? rm(f) : add(f)
end
diff --git a/app/models/usuarie.rb b/app/models/usuarie.rb
index c87d82f9..7b83ee75 100644
--- a/app/models/usuarie.rb
+++ b/app/models/usuarie.rb
@@ -41,6 +41,12 @@ class Usuarie < ApplicationRecord
lock_access! if attempts_exceeded? && !access_locked?
end
+ def send_devise_notification(notification, *args)
+ I18n.with_locale(lang) do
+ devise_mailer.send(notification, self, *args).deliver_later
+ end
+ end
+
private
def lang_from_locale!
diff --git a/app/services/lfs_object_service.rb b/app/services/lfs_object_service.rb
new file mode 100644
index 00000000..bb62301d
--- /dev/null
+++ b/app/services/lfs_object_service.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+# Representa un objeto git LFS
+class LfsObjectService
+ attr_reader :site, :blob
+
+ # @param :site [Site]
+ # @param :blob [ActiveStorage::Blob]
+ def initialize(site:, blob:)
+ @site = site
+ @blob = blob
+ end
+
+ def process
+ # Crear el directorio
+ FileUtils.mkdir_p(File.dirname(object_path))
+
+ # Mover el archivo
+ FileUtils.mv(path, object_path) unless File.exist? object_path
+
+ # Crear el pointer
+ Site::Writer.new(site: site, file: path, content: pointer).save
+
+ # Commitear el pointer
+ site.repository.commit(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
diff --git a/app/services/post_service.rb b/app/services/post_service.rb
index e448bb4c..7b31867d 100644
--- a/app/services/post_service.rb
+++ b/app/services/post_service.rb
@@ -12,8 +12,14 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
post.usuaries << 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)
+ update_site_license!
+
# Devolver el post aunque no se haya salvado para poder rescatar los
# errores
post
@@ -40,6 +46,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
# relacionados.
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
# errores
post
@@ -133,4 +141,12 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
p.path.absolute if p.save(validate: false)
end.compact << post.path.absolute
end
+
+ # Si les usuaries modifican o crean una licencia, considerarla
+ # personalizada en el panel.
+ def update_site_license!
+ if site.usuarie?(usuarie) && post.layout.name == :license && !site.licencia.custom?
+ site.update licencia: Licencia.find_by_icons('custom')
+ end
+ end
end
diff --git a/app/services/site_service.rb b/app/services/site_service.rb
index bb76c6ac..848f3cfc 100644
--- a/app/services/site_service.rb
+++ b/app/services/site_service.rb
@@ -27,13 +27,14 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
site.save &&
site.config.write &&
- commit_config(action: :create)
+ commit_config(action: :create) &&
+ site.reset.nil? &&
+ add_licencias &&
+ add_code_of_conduct &&
+ add_privacy_policy &&
+ deploy
end
- add_licencias
-
- deploy
-
site
end
@@ -42,11 +43,11 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
site.update(params) &&
site.config.write &&
- commit_config(action: :update)
+ commit_config(action: :update) &&
+ site.reset.nil? &&
+ change_licencias
end
- change_licencias
-
site
end
@@ -106,24 +107,28 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
end
# Crea la licencia del sitio para cada locale disponible en el sitio
+ #
+ # @return [Boolean]
def add_licencias
- site.locales.each do |locale|
- next unless I18n.available_locales.include? locale
+ return true unless site.layout? :license
+ return true if site.licencia.custom?
- Mobility.with_locale(locale) do
- add_licencia lang: locale
- end
- end
+ with_all_locales do |locale|
+ add_licencia lang: locale
+ end.compact.map(&:valid?).all?
end
+ # Crea una licencia
+ #
+ # @return [Post]
def add_licencia(lang:)
params = ActionController::Parameters.new(
post: {
+ layout: 'license',
+ slug: Jekyll::Utils.slugify(I18n.t('activerecord.models.licencia')),
lang: lang,
title: site.licencia.name,
- description: I18n.t('sites.form.licencia.title'),
- author: %w[Sutty],
- permalink: "#{I18n.t('activerecord.models.licencia').downcase}/",
+ description: site.licencia.short_description,
content: CommonMarker.render_html(site.licencia.deed)
}
)
@@ -134,25 +139,27 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
# Encuentra la licencia a partir de su enlace permanente y le cambia
# el contenido
#
- # TODO: Crear un layout específico para licencias así es más certera
- # la búsqueda.
+ # @return [Boolean]
def change_licencias
- site.locales.each do |locale|
- next unless I18n.available_locales.include? locale
+ return true unless site.layout? :license
+ return true if site.licencia.custom?
- Mobility.with_locale(locale) do
- permalink = "#{I18n.t('activerecord.models.licencia').downcase}/"
- post = site.posts(lang: locale).find_by(permalink: permalink)
+ with_all_locales do |locale|
+ post = site.posts(lang: locale).find_by(layout: 'license')
- post ? change_licencia(post: post) : add_licencia(lang: locale)
- end
- end
+ change_licencia(post: post) if post
+ end.compact.map(&:valid?).all?
end
+ # Cambia una licencia
+ #
+ # @param :post [Post]
+ # @return [Post]
def change_licencia(post:)
params = ActionController::Parameters.new(
post: {
title: site.licencia.name,
+ description: site.licencia.short_description,
content: CommonMarker.render_html(site.licencia.deed)
}
)
@@ -161,10 +168,69 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
params: params).update
end
+ # Agrega un código de conducta
+ #
+ # @return [Boolean]
+ def add_code_of_conduct
+ return true unless site.layout?(:code_of_conduct) || site.layout?(:page)
+
+ # TODO: soportar más códigos de conducta
+ coc = CodeOfConduct.first
+
+ with_all_locales do |locale|
+ params = ActionController::Parameters.new(
+ post: {
+ layout: site.layout?(:code_of_conduct) ? 'code_of_conduct' : 'page',
+ lang: locale.to_s,
+ title: coc.title,
+ description: coc.description,
+ content: CommonMarker.render_html(coc.content)
+ }
+ )
+
+ PostService.new(site: site, usuarie: usuarie, params: params).create
+ end.compact.map(&:valid?).all?
+ end
+
+ # Agrega política de privacidad
+ #
+ # @return [Boolean]
+ def add_privacy_policy
+ return true unless site.layout?(:privacy_policy) || site.layout?(:page)
+
+ pp = PrivacyPolicy.first
+
+ with_all_locales do |locale|
+ params = ActionController::Parameters.new(
+ post: {
+ layout: site.layout?(:privacy_policy) ? 'privacy_policy' : 'page',
+ lang: locale.to_s,
+ title: pp.title,
+ description: pp.description,
+ content: CommonMarker.render_html(pp.content)
+ }
+ )
+
+ PostService.new(site: site, usuarie: usuarie, params: params).create
+ end.compact.map(&:valid?).all?
+ end
+
# Crea los deploys necesarios para sincronizar a otros nodos de Sutty
def sync_nodes
Rails.application.nodes.each do |node|
site.deploys.build(type: '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
diff --git a/app/views/posts/attribute_ro/_locales.haml b/app/views/posts/attribute_ro/_locales.haml
index 3ac22933..16ecb532 100644
--- a/app/views/posts/attribute_ro/_locales.haml
+++ b/app/views/posts/attribute_ro/_locales.haml
@@ -1,9 +1,10 @@
-%tr{ id: attribute }
- %th= post_label_t(attribute, post: post)
- %td
- %ul
- - metadata.value.each do |uuid|
- - p = site.docs.find(uuid, uuid: true)
- %li{ dir: t("locales.#{p.lang.value}.dir"), lang: p.lang.value }
- = link_to p.title.value,
- site_post_path(site, p.id, locale: p.lang.value)
+- if site.locales.count > 1
+ %tr{ id: attribute }
+ %th= post_label_t(attribute, post: post)
+ %td
+ %ul
+ - metadata.value.each do |uuid|
+ - p = site.docs.find(uuid, uuid: true)
+ %li{ dir: t("locales.#{p.lang.value}.dir"), lang: p.lang.value }
+ = link_to p.title.value,
+ site_post_path(site, p.id, locale: p.lang.value)
diff --git a/app/views/posts/attributes/_locales.haml b/app/views/posts/attributes/_locales.haml
index 8dd7adf6..4978f6b4 100644
--- a/app/views/posts/attributes/_locales.haml
+++ b/app/views/posts/attributes/_locales.haml
@@ -1,39 +1,19 @@
--#
-
- Crea un input-map para cada idioma por separado. Podríamos hacer uno
- solo que tenga todos los idiomas pero puede ser una interfaz confusa.
-
- TODO: Esto permite seleccionar más de una traducción por idioma...
-
-- site.locales.each do |locale|
- -# Ignorar el idioma actual
- - next if post.lang.value == locale
- - locale_t = t("locales.#{locale}.name")
- - values = metadata.value.select do |x|
- - metadata.values[locale].values.include? x
-
- .form-group
- = label_tag "#{base}_#{attribute}_#{locale}", locale_t
-
- .mapable{ dir: t("locales.#{locale}.dir"), lang: locale,
- data: { values: values.to_json,
- 'default-values': metadata.values[locale].to_json,
- name: "#{base}[#{attribute}][]",
- list: id_for_datalist(attribute, locale),
- button: t('posts.attributes.add'),
- remove: 'false', legend: locale_t,
- described: id_for_help(attribute, locale) } }
-
- = text_field(*field_name_for(base, attribute, '[]'),
- value: values.join(', '),
- dir: t("locales.#{locale}.dir"), lang: locale,
- **field_options(attribute, metadata))
+- if site.locales.count > 1
+ %fieldset
+ %legend= post_label_t(attribute, post: post)
= render 'posts/attribute_feedback',
- post: post,
- attribute: [attribute, 'mapable'].flatten,
- metadata: metadata
+ post: post, attribute: attribute, metadata: metadata
- %datalist{ id: id_for_datalist(attribute, locale) }
- - metadata.values[locale].keys.each do |value|
- %option{ value: value }
+ - site.locales.each do |locale|
+ - next if post.lang.value == locale
+ - locale_t = t("locales.#{locale}.name", default: locale.to_s.humanize)
+ - value = metadata.value.find do |v|
+ - metadata.values[locale].values.include? v
+
+ .form-group
+ = label_tag "#{base}_#{attribute}_#{locale}", locale_t
+
+ = select_tag("#{plain_field_name_for(base, attribute)}[]",
+ options_for_select(metadata.values[locale], value),
+ **field_options(attribute, metadata), include_blank: t('.empty'))
diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml
index dbeddaad..4f814cda 100644
--- a/app/views/posts/index.haml
+++ b/app/views/posts/index.haml
@@ -1,11 +1,13 @@
%main.row
%aside.menu.col-md-3
- %h1= link_to @site.title, @site.url
+ %h1= @site.title
%p.lead= @site.description
+ - cache_if @usuarie, [@site, I18n.locale] do
+ = render 'sites/status', site: @site
%h3= t('posts.new')
%table.mb-3
- - @site.layouts.each do |layout|
+ - @site.layouts.sort_by(&:humanized_name).each do |layout|
- next if layout.hidden?
%tr
%th= layout.humanized_name
diff --git a/app/views/sites/_form.haml b/app/views/sites/_form.haml
index 48e30e0c..001f542e 100644
--- a/app/views/sites/_form.haml
+++ b/app/views/sites/_form.haml
@@ -53,10 +53,10 @@
= render 'bootstrap/alert' do
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
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
- Design.all.find_each do |design|
- .design.col-md-4.d-flex.flex-column
+ .design.col.d-flex.flex-column
.custom-control.custom-radio
= f.radio_button :design_id, design.id,
checked: design.id == site.design_id,
@@ -81,10 +81,12 @@
%h2= t('.licencia.title')
%p.lead= t('.help.licencia')
- Licencia.all.find_each do |licencia|
+ - next if licencia.custom? && site.licencia != licencia
.row.license
.col
.media.mt-1
- = image_tag licencia.icons, alt: licencia.name, class: 'mr-3 mt-4'
+ - unless licencia.custom?
+ = image_tag licencia.icons, alt: licencia.name, class: 'mr-3 mt-4'
.media-body
.custom-control.custom-radio
= f.radio_button :licencia_id, licencia.id,
@@ -95,8 +97,8 @@
= sanitize_markdown licencia.description,
tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6]
- = link_to t('.licencia.url'), licencia.url,
- target: '_blank', class: 'btn'
+ - unless licencia.custom?
+ = link_to t('.licencia.url'), licencia.url, target: '_blank', class: 'btn', rel: 'noopener'
%hr/
diff --git a/app/views/sites/_status.haml b/app/views/sites/_status.haml
new file mode 100644
index 00000000..a731aa7d
--- /dev/null
+++ b/app/views/sites/_status.haml
@@ -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'
diff --git a/config/credentials.yml.enc.ci b/config/credentials.yml.enc.ci
new file mode 100644
index 00000000..4add450d
--- /dev/null
+++ b/config/credentials.yml.enc.ci
@@ -0,0 +1 @@
+1jEfzfldP9tT4+HWfhP48I9hw31gYCnnxHWpYjPrcTm/pgkFdiG+mDa6y31EOxzs50w6FEw2GO127BnyBSUIPIxuWY0cR96xL5pVrS3vjyzM84QN4lJF9ER0Tz1AQ9S7NJ54CelSkMfFt/rf+O4YM8cLtdSVsVC/HlGbp16p3D1pm4MFo5cQb0hEmlyyYlzEn4oJtsp/MCIwI4+z8oFhxKdMIxdbiw+KS/7PBRfMm1h5rdGORCnD69iVmnXseMvVtZn9A7N7uR6+gFlhxlD5yyEW0pwTj3tbu9NeIOVbtmYOL5ZhLW9REXtGTqR5Op/LN+ukIXbDNEScKltJXUdWfa9Pd/QjVT8IMURZ04POEMDgs1cw363yz4f+WQForhSco9oYLDOd5hTGRXoZ9fnjnfJSTjINM62hkfDY3w3+s844nNbjbj+lPTJHU/QjRhcuNqBDDxWUfwTmRIqm5zrelnHnZnuFmFwCNet6NChC6EFUAFjrals6kTSQllyMt4xImqA+HL7DnjWj6VURSH+nGQTA4tQvDdfbDwTzg/PvRkJcsy2dRd135RQdmRZ+8KXBviLabwdR256vaCqSO1j+jyeUPGLll35ghyLxncyBkkAKt1zaDRPDWgVafg0gJ3v7hVV5TYgToPzlv4w88KPCY7cBhkb1qGoXAhtO6iAuZYK9eyZd1gNQJKyqbcLqA5aTTX/ylfdbptWhaZ8ibB8KBgVyn2RmrOHEhB38rDSMHHNfK3Xs4/hhqMFIGHGTGCUYVmjCzhVFd15yRurU32d3YtP8W4L77H7qkFsF1gnvsZx+R084LcJqknwY94dmjtUE4x2u+Qh3ElFj--lr8JoUq1WH9xXNsB--mE8hxHADL7SbDWabAPY1+Q==
\ No newline at end of file
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 0e18b987..6002ee65 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -13,6 +13,10 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.singular 'roles', 'rol'
inflect.plural 'rollup', 'rollups'
inflect.singular 'rollups', 'rollup'
+ inflect.plural 'code_of_conduct', 'codes_of_conduct'
+ inflect.singular 'codes_of_conduct', 'code_of_conduct'
+ inflect.plural 'privacy_policy', 'privacy_policies'
+ inflect.singular 'privacy_policies', 'privacy_policy'
end
ActiveSupport::Inflector.inflections(:es) do |inflect|
@@ -28,4 +32,8 @@ ActiveSupport::Inflector.inflections(:es) do |inflect|
inflect.singular 'licencias', 'licencia'
inflect.plural 'rollup', 'rollups'
inflect.singular 'rollups', 'rollup'
+ inflect.plural 'code_of_conduct', 'codes_of_conduct'
+ inflect.singular 'codes_of_conduct', 'code_of_conduct'
+ inflect.plural 'privacy_policy', 'privacy_policies'
+ inflect.singular 'privacy_policies', 'privacy_policy'
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 8ecdd6d3..3ddf681d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -13,6 +13,9 @@ en:
ar:
name: Arabic
dir: rtl
+ ur:
+ name: Urdu
+ dir: rtl
zh:
name: Chinese
dir: ltr
@@ -114,6 +117,10 @@ en:
title: Alternative domain name
success: Success!
error: Error
+ deploy_distributed_press:
+ title: Distributed Web
+ success: Success!
+ error: Error
deploy_reindex:
title: Reindex
success: Success!
@@ -354,6 +361,10 @@ en:
designer_url: 'Support the designer'
static_file_migration: 'File migration'
find_and_replace: 'Search and replace'
+ status:
+ building: "Your site is building, please wait 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:
title: 'My Sites'
pull: 'Upgrade'
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 90c07e32..01f1085c 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -13,6 +13,9 @@ es:
ar:
name: Árabe
dir: rtl
+ ur:
+ name: Urdu
+ dir: rtl
zh:
name: Chino
dir: ltr
@@ -114,6 +117,10 @@ es:
title: Dominio alternativo
success: ¡Éxito!
error: Hubo un error
+ deploy_distributed_press:
+ title: Web distribuida
+ success: ¡Éxito!
+ error: Hubo un error
deploy_reindex:
title: Reindexación
success: ¡Éxito!
@@ -359,6 +366,10 @@ es:
designer_url: 'Apoyá a le(s) diseñadore(s)'
static_file_migration: 'Migración de archivos'
find_and_replace: 'Búsqueda y reemplazo'
+ status:
+ building: "Tu sitio se está publicando, por favor espera 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:
title: 'Mis sitios'
pull: 'Actualizar'
diff --git a/db/migrate/20230322214924_add_code_of_conduct.rb b/db/migrate/20230322214924_add_code_of_conduct.rb
new file mode 100644
index 00000000..f859b08c
--- /dev/null
+++ b/db/migrate/20230322214924_add_code_of_conduct.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# Crea códigos de conducta
+class AddCodeOfConduct < ActiveRecord::Migration[6.1]
+ def up
+ create_table :codes_of_conduct do |t|
+ t.timestamps
+ t.string :title
+ t.text :description
+ t.text :content
+ end
+
+ # XXX: En lugar de ponerlo en las seeds
+ YAML.safe_load(File.read('db/seeds/codes_of_conduct.yml')).each do |coc|
+ CodeOfConduct.new(**coc).save!
+ end
+ end
+
+ def down
+ drop_table :codes_of_conduct
+ end
+end
diff --git a/db/migrate/20230322231344_add_privacy_policy.rb b/db/migrate/20230322231344_add_privacy_policy.rb
new file mode 100644
index 00000000..e0d7ae59
--- /dev/null
+++ b/db/migrate/20230322231344_add_privacy_policy.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# Agrega políticas de privacidad
+class AddPrivacyPolicy < ActiveRecord::Migration[6.1]
+ def up
+ create_table :privacy_policies do |t|
+ t.timestamps
+ t.string :title
+ t.text :description
+ t.text :content
+ end
+
+ # XXX: En lugar de ponerlo en las seeds
+ YAML.safe_load(File.read('db/seeds/privacy_policies.yml')).each do |pp|
+ PrivacyPolicy.new(**pp).save!
+ end
+ end
+
+ def down
+ drop_table :privacy_policies
+ end
+end
diff --git a/db/migrate/20230325163802_add_short_description_to_licencias.rb b/db/migrate/20230325163802_add_short_description_to_licencias.rb
new file mode 100644
index 00000000..efcc01e4
--- /dev/null
+++ b/db/migrate/20230325163802_add_short_description_to_licencias.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# Agrega descripciones cortas a las licencias
+class AddShortDescriptionToLicencias < ActiveRecord::Migration[6.1]
+ def up
+ add_column :licencias, :short_description, :string
+
+ YAML.safe_load_file('db/seeds/licencias.yml').each do |licencia|
+ Licencia.find_by_icons(licencia['icons']).update licencia
+ end
+ end
+
+ def down
+ remove_column :licencias, :short_description
+ end
+end
diff --git a/db/seeds/codes_of_conduct.yml b/db/seeds/codes_of_conduct.yml
new file mode 100644
index 00000000..64582072
--- /dev/null
+++ b/db/seeds/codes_of_conduct.yml
@@ -0,0 +1,613 @@
+---
+- title_en: "Codes for sharing"
+ title_es: "Códigos para compartir"
+ description_en: "Codes of conduct allow inclusive communities."
+ description_es: "Los códigos de convivencia nos permiten alojar comunidades inclusivas."
+ content_en: |
+ # Code for sharing
+
+ > This code of conduct is based in "[Códigos para compartir, hackear,
+ > piratear en
+ > libertad](https://utopia.partidopirata.com.ar/zines/codigos_para_compartir.html)"
+ > published by [Partido Interdimensional
+ > Pirata](https://partidopirata.com.ar/).
+
+ > We use gender neutral pronouns to include all peoples. In this sense,
+ > we encourage different forms, strategies and tools used to embody
+ > practices that aren't anthropocentric, sexist, cis-sexist in our
+ > language.
+
+ ## Introduction
+
+ This is an example code that strives to give a consensual frame to
+ enable asistance, permanence and confortable stay to everyone using and
+ inhabiting [Sutty](https://sutty.nl/), and to welcome new users and
+ potential allies as well. It sets the floor for desirable and
+ acceptable, and undesirable and intolerable conducts for its community.
+ You can use it with or without changes, adapting it to your activities.
+ This code is in permanent and collective mutation and feeds, copies and
+ inspires on the following sources:
+
+ *
+
+ *
+
+ *
+
+ *
+
+ We strive to sustain and foment an open community, that invites and
+ attains participation from more people, in all their diversity. We know
+ that spaces related to computing and free software are mostly inhabited
+ by middle class cis white males, even when there's an acknowledgement of
+ the need to close the gender gap. In this sense, this is our little
+ contribution, made from collective practices on multiple dimentions,
+ reflections, readings, and experiences that grow every day.
+
+ ## That everyone needs to be well treated
+
+ Every being that we share space with deserves good treatment, respect
+ and compassion. Here we share basic criteria for introduction and care.
+
+ ### Towards humans
+
+ Everyone is deserving of care and greetings and we have a right to
+ assume good intentions from others.
+
+ When we refer to other humans, we try to be careful and respectul
+ towards their gender identity. For this, these principles are useful:
+
+ * Don't assume, judge or try to "interpret" the gender of others.
+
+ * Don't use gender beforehand. It's related to the previous item, but
+ puts emphasis towards naturalized gendering behaviour (ie. assuming
+ someone wearing a dress uses female pronouns...). The proposal is to
+ discard them.
+
+ * If a person explicits their pronouns and mode in which they want to be
+ referenced, we respect them, by listening and trying to use their
+ prefered pronouns.
+
+ * When presentations don't include pronouns, we can ask respectfully
+ for prefered pronouns. But be careful! This question must be asked
+ to everyone, otherwise the "suspicion" is loaded towards a person, and
+ it can become a form of harrassment.
+
+ * Do we need to know the gender of a person to relate with them? Maybe
+ a better practice is to evade gendering others. But if this means to
+ use "them" compulsively, some people may be made to feel bad. (For
+ instance, trans\* people who use female or male pronouns may feel
+ upset or outed when refering to them as "them", specially if they're
+ the only ones to be gendered like this in a group!
+
+ * When in doubt, asking and apologizing respectfully is a good way of
+ being careful towards each other.
+
+ ## Important points to guarantee our space from being expulsive
+
+ **Listening to and between everyone in a caring climate**
+
+ * Listen to what everyone has to say, being mindful that everyone has
+ something valuable to communicate.
+
+ * For active listening, we prefer to ask first, before making judgement.
+
+ * Sometimes being silent is a condition for others to be able to talk.
+ To listen is an exercise that requires practice. Also talking.
+
+ * We're interested in what everyone has to say. If you're more trained
+ in participating, talking, and having opinions, take into account that
+ not everyone does. Give them space if they want to take it. But
+ remember that encouraging is not the same as pressuring!
+
+ * We try to check and stop offensive practices to add to the respectful
+ climate. This doesn't mean to be submissive or to agree to
+ everything. At the least, it sets a floor of respect towards enabling
+ a dialogue when necessary.
+
+ * It's at the very least disrepectful to repeat damaging behaviour when
+ it was already identified as such. It can make others unconfortable,
+ or hurt and expel them. We'll make this point every time that is
+ needed and tolerable.
+
+ * We avoid this behaviour ourselves and we help others to notice their
+ own.
+
+ * When raising attention becomes insufficient, we need to review this
+ agreements to keep the coexistence. This implies to act in accord to
+ them, and that this code can be revised and updated when deemed
+ necessary (there's no consensus).
+
+ * One of the ways in which free software spaces can be and are expulsive
+ is with attitudes that don't contemplate diversity in knowledges and
+ interlocutors. By appealing to technicisms, many comrades are kept
+ out of what's happening, and no one verifies if everyone is keeping up
+ with the conversation.
+
+ Our fervent recommendation is to be attentive to this dynamic so we
+ can avoid or revert them.
+
+ * The counter to the previous situation is "mansplaining": a cis-male
+ person assuming the authoritative place of knowledge to (over-)explain
+ everything to others, in a patronizing way and without taking into
+ account what others want to listen or not, to say, what already know
+ or do, etc.
+
+ * We believe there's no "authoritative voice" to have an opinion and to
+ participate. Free culture is for everyone to share.
+
+ * "Sharing is caring" v. "Google is your friend". Meritocracy and other
+ traditional codes in cyber-communities work against free culture. We
+ support the pirate culture that onboards ever more pirates to their
+ ships. We believe that culture is for everyone and we defy elitisms.
+
+ * We don't assume that other people shares our likings, beliefs, class
+ position, sexuality, etc. We can be violent when we misread others.
+ We recommend to ask respectfully and to avoid comments or jokes that
+ can be hurtful to others.
+
+ * We speak and act gently and inclusively.
+
+ * We respect different points of view, experiences, beliefs, etc. and we
+ take them into account when we act collectively so it reflects in our
+ attitudes.
+
+ * We welcome criticism, specially the constructive kind ;)
+
+ * We focus in what's best for the community, without losing warmth,
+ respect and diversity amongst ourselves.
+
+ * We show empathy toward others. We want to share and communicate.
+
+ * It's useful to think everyone has different abilities, stories,
+ experiences... It's possible to not understand some comments. We try
+ to avoid acting in bad faith and to use every accessibility tool we
+ can.
+
+ * The last item includes neurodiverse people and those that have
+ experienced trauma. Sometimes, sarcasm or irony is not well received
+ or understood by others. We take this into our strategies to include
+ everyone in our communications. Even more, if we think some topics
+ could be sensitive to others (memory-triggering, phobias, untolerable,
+ explicit violence or body images, etc.), we use content warnings (cw)
+ before what we wanted to share. For instance: "cw: comments about
+ sexual and physical violence". This allows everyone to opt in to the
+ content instead of being taken by surprise.
+
+ * We're respectful of limits established by others (personal space,
+ physical contact, interaction mood, privacy, being photographed, etc.)
+
+ * We want to and believe in welcoming more pirates!
+
+ ## Consent for documenting and sharing in media
+
+ * If you're going to take pictures or record video, ask consent from
+ people involved.
+
+ * If there're minors, ask their responsible families.
+
+ ## Our commitment against harassment
+
+ In the interest of fomenting an open, diverse and welcoming community,
+ we contributors and admins make a commitment against harassment in our
+ projects and community for everyone, without regard of age, body
+ diversity, capacity, neuro-diversity, ethnicity, gender identity and
+ expression, experience level, nationality, physical appearance,
+ religion, sexual identity or orientation.
+
+ Examples of unacceptable behaviour from participants:
+
+ * Offensive comments about gender/s, gender identity and expression,
+ sexual orientation, capacity, mental sickness, neuro-(a)tipicality,
+ physical appearance, body size, ethnicity or religion.
+
+ * Unwelcomed comments related to personal and life choices, including
+ amongst others, those related to food, health, children upbringing,
+ drug use and employment.
+
+ * Insulting or despective comments and personal or political attacks.
+ **Trolling**.
+
+ * Assuming others' gender. If you're in doubt, ask politely about
+ pronouns. Don't use the name(s) that people don't use anymore, use
+ the name, _nickname_ or pseudonym that they prefer. Do you really
+ need the name, ID number, biometric data, birth certificate of others?
+
+ * Sexual comments, images or behaviour, unneeded or in spaces where they
+ weren't appropiate.
+
+ * Unconsented physical contact or repeated after being asked to stop.
+
+ * Threatening others.
+
+ * Inciting violence towards others, including self-damage.
+
+ * Deliberate intimidation.
+
+ * Stalking.
+
+ * To harass by photographing or recording without consent, including
+ uploading personal information to the Internet.
+
+ * Interrupting a conversation constantly.
+
+ * Making unwanted sexual comments.
+
+ * Unappropiate patterns of social contact, like asking/assuming
+ inappropiate intimacy levels with others.
+
+ * Trying to interact with a person after being asked not to.
+
+ * Exposing deliberately any aspect of a person identity without consent,
+ except when necessary for protecting others against intentional abuse.
+
+ * Making public any kind of private conversation.
+
+ * Other kinds of conduct that can be considered inappropiate in an
+ environment of camaraderie.
+
+ * Repeating attitudes that others find offensive or violatory of this
+ code.
+
+ ## Consequences
+
+ * Any person that has been asked to stop offensive behaviour is expected
+ to respond immediately, even when in disagreement.
+
+ * Admins can take any action deemed necessary and adequate, including
+ expelling the person or removing their site without advertence. This
+ decision is taken by consensus between admins and is reserved for
+ extreme cases that compromise the community or the permanence of
+ others without feeling wronged or threatened.
+
+ * Admins reserve the right to forbid participation to any future
+ activity or publication.
+
+ As we mentioned before, this code is in permanent collective mutation.
+ It's main objective is to generate an inclusive and non-expulsive
+ environment that is also transparent and open without [missing
+ stairs](https://en.wikipedia.org/wiki/Missing_stair) ("the missing stair
+ from a house that everyone knows about but no one wants to take
+ responsibility for"). It's important to adapt it to different
+ activities and that it nurtures from contributions from its users.
+ Receiving your comments and input will help us to achieve this
+ objective.
+
+ ## Let's keep in contact!
+
+ Si pasaste por alguna situación que quieras compartir --te hayas animado
+ o no a decirlo en el momento--, podés ponerte en contacto con nosotres.
+
+ Con respecto a quejas o avisos acerca de situaciones de violencia,
+ acoso, abuso o repetición de conductas que se advirtieron como
+ intolerables, tomamos la responsabilidad de tenerlas en cuenta y
+ trabajar en ellas para que el resultado sea el favorable al espíritu de
+ colectiva que elegimos y describimos aquí. Si bien consideramos que las
+ prácticas punitivistas no van con nosotres, nuestra decisión explícita
+ es escuchar a la persona que se manifiesta como violentada o víctima y
+ acompañarla.
+
+ You can contact us if you were part of a situation you want to share
+ --even if you didn't pointed it in the moment.
+
+ In regards to complaints or notices about violence, harassment, abuse or
+ repeated untolerable conducts, we take the responsibility of working on
+ them for a favorable result towards the collective spirit we defined
+ here. Even when we don't condone punitivist practices, our explicit
+ decision is for the victim to be listened and accompanied.
+ content_es: |
+ # Códigos para compartir
+
+ > Este código de convivencia está basado en los "[Códigos para
+ > compartir, hackear, piratear en
+ > libertad](https://utopia.partidopirata.com.ar/zines/codigos_para_compartir.html)"
+ > publicados por el [Partido Interdimensional
+ > Pirata](https://partidopirata.com.ar/).
+
+ > Utilizamos preferentemente la 'e' para referirnos a las personas en
+ > general. En ese sentido, alentamos las diferentes formas, estrategias
+ > y herramientas para incorporar prácticas no antropocéntricas,
+ > sexistas, ni cisexistas en la lengua. Otras alternativas que apoyamos
+ > --y eventualmente usamos-- son el uso del femenino, la letra e, arrobas,
+ > equis, asteriscos, etc.
+
+ ## Introducción
+
+ Este es un ejemplo de código que busca aportar un marco de consenso para
+ garantizar la asistencia, permanencia y cómoda estadía de todas las
+ personas que habitan y utilizan Sutty, así como para bienvenir a nueves
+ usuaries y potenciales aliades. Para esto, fija un piso de conductas
+ deseables, aceptables, indeseables y/o intolerables para la comunidad.
+ Podés usarlo sin cambios o modificarlo para adaptarlo a tus actividades.
+ Este código está en permanente mutación colectiva y se alimenta, copia e
+ inspira de las siguientes fuentes:
+
+ *
+
+ *
+
+ *
+
+ *
+
+ Procuramos mantener y fomentar una comunidad abierta, que invite y logre
+ la participación de cada vez más personas, en toda su diversidad.
+ Sabemos que los espacios de Software Libre, informática, sistemas, etc.
+ son habitados mayormente por varones cis, blancos y de clase media, pese
+ al reconocimiento de la necesidad de eliminar la brecha de géneros. En
+ este sentido, este es nuestro pequeño aporte, hecho de prácticas
+ colectivas de múltiples dimensiones, reflexiones, lecturas, experiencias
+ que crecen día a día.
+
+ ## Que todes les seres sean bien tratades
+
+ Cada ser con el que compartamos el espacio es merecedore de buen trato,
+ respeto y compasión. En otras palabras, compartimos a continuación los
+ criterios básicos de presentación y cuidados.
+
+ Para les humanes
+
+ Todes somos dignes de cuidados y de saludos y tenemos derecho a suponer
+ las buenas intenciones de le otre.
+
+ Para referirnos a otres humanes, trataremos de ser cuidadoses y
+ respuestuoses de su identidad de género. Para ello, son útiles los
+ siguientes principios:
+
+ * No presuponer, juzgar o "interpretar" el género de le otre.
+
+ * No generizar de antemano. Se desprende del punto anterior, pero hace
+ especial énfasis en comportamientos naturalizados de generización (EJ:
+ presuponer que porque una persona usa un vestido se nombra en
+ femenino...). La propuesta es desecharlos.
+
+ * Si la persona explicita sus pronombres y modos en que quiere ser
+ referenciade, lo respetamos, escuchando y procurando referirnos a elle
+ usando sus pronombres elegidos.
+
+ * Si no se incluye en la presentación los pronombres preferidos, podemos
+ preguntar respetuosamente qué pronombres se usan. ¡Pero atención! Es
+ una pregunta que debe dirigirse a todes por igual, de lo contrario,
+ carga la "sospecha" sobre la persona señalada y puede resultar en una
+ forma de hostigamiento.
+
+ * ¿Es necesario conocer el género de una persona para relacionarnos o
+ referirnos a ella? Quizás una buena práctica es evitar generizar para
+ todas las personas. Pero si esto implica el uso compulsivo de la "e"
+ para todes, puede ser que alguna persona se sienta molesta. (Por
+ ejemplo, las personas trans\* que se identifican en femenino o
+ masculino suelen sentirse molestas y "sacadas del clóset" u *outeadas*
+ si se refieren a ellas con la "e", ¡en especial si son las únicas en
+ ser generizadas de esta forma en un grupo!).
+
+ * Ante cualquier duda, preguntar respetuosamente y disculparse
+ respetuosamente es una buena idea para ayudar a cuidarnos.
+
+ ## Puntos importantes para garantizar que nuestro espacio no resulte expulsivo
+
+ **Escucharnos a todas y entre todas en un clima de cuidados**
+
+ * Escuchar lo que cada quien tiene para decir, conscientes de que todes
+ tenemos algo valioso para comunicar(nos).
+
+ * Para la escucha activa, preferimos preguntar primero, en lugar de
+ hacer juicios.
+
+ * Hacer silencio a veces es la condición para que otres puedan animarse
+ a hablar. Escuchar es un ejercicio que requiere práctica. También lo
+ es hablar.
+
+ * Nos interesa lo que todes tengan para decir. Por lo tanto, si estás
+ más entrenade en el ejercicio de participar, hablar, opinar, tené en
+ cuenta que quizás haya otres que no lo estén tanto: darles el espacio
+ si quieren tomarlo. ¡Pero recordá que incentivar no es lo mismo que
+ presionar!
+
+ * Tratamos de revisar y discontinuar alguna práctica que pueda haber
+ resultado ofensiva, para sumar al clima de respeto. Sin embargo, esto
+ no significa "bajar la cabeza" o estar necesariamente de acuerdo. Al
+ menos, fija un piso de respeto para comenzar un diálogo en el caso en
+ que sea necesario.
+
+ * Es --al menos-- una falta de respeto repetir un comportamiento dañino
+ que ya se identificó como tal. Puede incomodar, lastimar y expulsar a
+ otres, por lo que preferimos llamar la atención sobre este punto todas
+ las veces que sea necesario y tolerable.
+
+ * Evitamos esto nosotres y ayudamos a otres a darse cuenta cuando lo
+ están haciendo.
+
+ * En los casos en los que los llamados de atención resulten
+ insuficientes, hemos de revisar estos acuerdos para sostener la
+ convivencia. Eso implica actuar de acuerdo a ellos. Y también que
+ estos códigos pueden ser revisados y actualizados en caso de que se
+ considere necesario (deje de haber consenso).
+
+ * Una manera en la que los espacios de Software Libre y tecnologías
+ pueden y suelen ser expulsivos es mediante actitudes que no contemplan
+ la diversidad de saberes e interlocutor\*s. So pretexto de incluir
+ tecnicismos, muches compañeres quedan al margen de lo que está
+ sucediendo, muchas veces, sin que nadie tenga la mínima delicadeza de
+ verificar que todes estén siguiendo la conversación.
+
+ Recomendamos fervientemente estar atentes a estas dinámicas para poder
+ evitarlas y/o revertirlas.
+
+ * La otra cara de la situación anterior es el famoso _mansplaining_: un
+ tipo cis poniéndose en el lugar de la autoridad del saber para
+ (sobre-)explicar todo a le otre, de manera paternalista y sin tener en
+ cuenta lo que le otre quiere o no escuchar, decir, lo que sabe o hace,
+ etc.
+
+ * Creemos que no hace falta ser "una voz autorizada" para opinar y
+ participar. La cultura libre se comparte entre todes.
+
+ * "Compartir es bueno" vs. "Google es tu amigo". La meritocracia y
+ ciertos códigos tradicionales de ciertas ciber-comunidades suelen
+ operar de manera contraria a la propuesta de la cultura libre de
+ compartir. Apoyamos la cultura piratil que suma más piratas a los
+ barcos. Creemos que la cultura es para todes y desafiamos las
+ prácticas elitistas.
+
+ * No damos por sentado que la persona con la que estamos interactuando
+ comparte gustos, creencias, pertenencias de clase, sexualidad, etc.
+ Podemos ser violentes si hacemos una lectura equivocada de le otre.
+ Recomendamos siempre preguntar de manera respetuosa y evitar
+ comentarios o chistes que puedan herir a les otres.
+
+ * Usamos lenguaje amable e inclusivo y mostramos conductas amables e
+ inclusivas.
+
+ * Respetamos los diferentes puntos de vista, experiencias, creencias,
+ etc. y lo tenemos en cuenta cuando estamos en grupo para verlo
+ reflejado en nuestras actitudes.
+
+ * Aceptamos las críticas. En especial las constructivas ;)
+
+ * Nos enfocamos en lo que es mejor para la comunidad, sin por ello
+ perder de vista la calidez, el respeto y la diversidad entre cada une
+ de nosotres.
+
+ * Mostramos empatía con les otres. Queremos comunicarnos y compartir.
+
+ * Es útil tener en cuenta que las personas tenemos capacidades,
+ historias, recorridos... diferentes. Es posible que algunos
+ comentarios no sean comprendidos. Trataremos de evitar la mala fe y
+ sumar todas las herramientas de accesibilidad para todas las personas.
+
+ * El punto anterior incluye a personas neurodiversas y con experiencias
+ de trauma. A veces el sarcasmo o la ironía no es bien recibido o
+ comprendido por todes. Será útil tenerlo en cuenta para buscar
+ estrategias que no excluyan a las personas de nuestros intercambios.
+ Por otro lado, si creemos que determinados temas pueden ser sensibles
+ (desencadenantes de recuerdos, fobias, difíciles de tolerar o cargados
+ de violencia o imágenes corporales muy explícitas, por ejemplo) para
+ algunas personas y nos valemos de las advertencias de contenido o
+ _content warning_ (cw) (ej: "cw: comentarios de violencia sexual y
+ violencia física") antes del contenido a introducir. Esto permite que
+ cada cual pueda elegir si acceder o no a esos contenidos y que no le
+ tomen por sorpresa.
+
+ * Respetamos los límites que establecen otras personas (espacio
+ personal, contacto físico, ganas de interactuar, no querer dar datos
+ de contacto o ser fotografiades, etc.)
+
+ * ¡Queremos y (creemos) en sumar piratas!
+
+ ## Consentimiento para documentar o compartir en medios
+
+ * Si vas a publicar video o fotos, obtené el consentimiento de las
+ personas.
+
+ * Si hay menores, consultalo con su familia responsable.
+
+ ## Nuestro compromiso contra el acoso
+
+ En el interés de fomentar una comunidad abierta, diversa y hospitalaria,
+ nosotres como contribuyentes y administradores nos comprometemos a hacer
+ de la participación en nuestro proyecto y nuestra comunidad una
+ experiencia libre de acoso para todes, independientemente de la edad,
+ diversidad corporal, capacidades, neuro-diversidad, etnia, identidad y
+ expresión de género, nivel de experiencia, nacionalidad, apariencia
+ física, raza, religión, identidad u orientación sexual y otras.
+
+ Ejemplos de comportamiento inaceptable por parte de participantes:
+
+ * Comentarios ofensivos relacionados con el/los género/s, la identidad
+ y expresión de género, la orientación sexual, las capacidades, las
+ enfermedades mentales, la neuro(a)tipicalidad, la apariencia física,
+ el tamaño corporal, la raza o la religión.
+
+ * Comentarios indeseados relacionados con las elecciones y las prácticas
+ de estilo de vida de una persona, incluidas, entre otras, las
+ relacionadas con alimentos, salud, crianza de les hijes, drogas y
+ empleo.
+
+ * Comentarios insultantes o despectivos (_trolling_) y ataques
+ personales o políticos.
+
+ * Dar por sentado el género de las demás personas. En caso de duda,
+ preguntá educadamente por los pronombres. No uses nombres con los que
+ las personas no se identifican, usá el nombre, _nickname_ o apodo que
+ hayan elegido (¿Realmente necesitás el nombre y el número de DNI,
+ datos biométricos, carta natal, etc.?).
+
+ * Comentarios, imágenes o comportamientos sexuales innecesarios o fuera
+ de lugar en espacios en los que no son apropiados.
+
+ * Contacto físico sin consentimiento o reiterado tras un pedido de cese.
+ En el mismo sentido, invasión del espacio corporal (y espacios en
+ general).
+
+ * Amenazas contra otras personas.
+
+ * Incitación a la violencia contra otra persona, que también incluye
+ alentar a una persona a autolesionarse.
+
+ * Intimidación deliberada.
+
+ * Acechar (_stalkear_) o perseguir.
+
+ * Acosar fotografiando o grabando sin consentimiento, incluyendo también
+ subir información personal a Internet sobre alguien para acosarle.
+
+ * Interrumpir constantemente en una conversación.
+
+ * Hacer comentarios sexuales indeseados.
+
+ * Patrones de contacto social inapropiados, como por ejemplo
+ pedir/suponer niveles de intimidad inapropiados con les demás.
+
+ * Seguir tratando de entablar conversación con una persona cuando se te
+ pidió que no lo hagas.
+
+ * Divulgar deliberadamente cualquier aspecto de la identidad de una
+ persona sin su consentimiento, excepto que sea necesario para proteger
+ a otras personas de abuso intencional.
+
+ * Hacer pública una conversación privada de cualquier tipo.
+
+ * Otros tipos de conducta que pudieran considerarse inapropiadas en un
+ entorno de camaradería.
+
+ * Reiteración de actitudes que les participantes señalen como ofensivas
+ o violatorias de este código.
+
+ ## Consecuencias
+
+ * Se espera que la persona a la que se la haya pedido que cese un
+ comportamiento que infringe este código acate el pedido de forma
+ inmediata, incluso si no está de acuerdo con este.
+
+ * Les administradores pueden tomar cualquier acción que juzguen
+ necesaria y adecuada, incluyendo expulsar a la persona o dar de baja
+ sus sitios sin advertencia. Esta decisión la toman les administradores
+ en consenso y se reserva para casos extremos que comprometan la
+ continuidad de la comunidad o bien la posibilidad de permanencia en
+ ella de otres participantes sin sentirse agraviades o amenazades.
+
+ * Les administradores se reservan el derecho a prohibir la asistencia a
+ cualquier actividad futura o publicación de sitios.
+
+ Como mencionamos antes, este código está en permanente mutación
+ colectiva. El objetivo principal es generar un ambiente inclusivo y no
+ expulsivo, un ambiente transparente y abierto en el que no haya
+ escalones faltantes ("el escalón que falta en la escalera y todo el
+ mundo sabe y avisa pero nadie se quiere hacer cargo"). Es importante que
+ se adapte a las actividades y se nutra de las contribuciones de les
+ usuaries. Recibir tus comentarios y aportes nos ayudará a cumplir con
+ su objetivo principal.
+
+ ## ¡Sigamos en contacto!
+
+ Si pasaste por alguna situación que quieras compartir --te hayas animado
+ o no a decirlo en el momento--, podés ponerte en contacto con nosotres.
+
+ Con respecto a quejas o avisos acerca de situaciones de violencia,
+ acoso, abuso o repetición de conductas que se advirtieron como
+ intolerables, tomamos la responsabilidad de tenerlas en cuenta y
+ trabajar en ellas para que el resultado sea el favorable al espíritu de
+ colectiva que elegimos y describimos aquí. Si bien consideramos que las
+ prácticas punitivistas no van con nosotres, nuestra decisión explícita
+ es escuchar a la persona que se manifiesta como violentada o víctima y
+ acompañarla.
diff --git a/db/seeds/licencias.yml b/db/seeds/licencias.yml
index cbe3bace..f6b76296 100644
--- a/db/seeds/licencias.yml
+++ b/db/seeds/licencias.yml
@@ -1,6 +1,19 @@
---
+- name_en: "Custom license"
+ name_es: "Licencia personalizada"
+ url_en: ""
+ url_es: ""
+ icons: "custom"
+ short_description_en: ""
+ short_description_es: ""
+ description_en: "The license terms are provided by you."
+ description_es: "Los términos de la licencia fueron provistos por vos."
+ deed_en: ""
+ deed_es: ""
- name_en: 'Peer Production License'
name_es: 'Licencia de Producción de Pares'
+ short_description_en: "This work is licensed under a Peer Production License"
+ short_description_es: "Esta obra está bajo una Licencia de Producción de Pares"
icons: "/images/ppl.png"
url_en: 'https://wiki.p2pfoundation.net/Peer_Production_License'
url_es: 'https://endefensadelsl.org/ppl_es.html'
@@ -100,6 +113,8 @@
hacerlo es enlazar a esta página.
- icons: "/images/by.png"
+ short_description_en: "This work is licensed under a Creative Commons Attribution 4.0 International License."
+ short_description_es: "Esta obra está bajo una Licencia Creative Commons Atribución 4.0 Internacional."
name_en: 'Creative Commons Attribution 4.0 International (CC BY 4.0)'
description_en: "This license gives everyone the freedom to use,
adapt, and redistribute the contents of your site by requiring
@@ -194,6 +209,8 @@
- icons: "/images/sa.png"
name_en: "Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)"
name_es: "Creative Commons Atribución-CompartirIgual 4.0 Internacional (CC BY-SA 4.0)"
+ short_description_en: "This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License."
+ short_description_es: "Esta obra está bajo una Licencia Creative Commons Atribución-CompartirIgual 4.0 Internacional."
url_en: 'https://creativecommons.org/licenses/by-sa/4.0/'
url_es: 'https://creativecommons.org/licenses/by-sa/4.0/deed.es'
description_en: "This license is the same as the CC-BY 4.0 but it adds
diff --git a/db/seeds/privacy_policies.yml b/db/seeds/privacy_policies.yml
new file mode 100644
index 00000000..98ce8379
--- /dev/null
+++ b/db/seeds/privacy_policies.yml
@@ -0,0 +1,113 @@
+---
+- title_en: "Privacy Policy"
+ title_es: "Políticas de privacidad"
+ description_en: "With what care does this site handles personal data of its users and visitors?"
+ description_es: "¿Cuáles son los cuidados de este sitio con respecto a sus usuaries y visitantes?"
+ content_en: |
+ > We use "them" as neutral pronoun to refer to people regardless of
+ > gender identity.
+
+ This document details Sutty's privacy policy, including web site,
+ platform, other infrastructure (support channels, etc.) and web sites
+ generated by users.
+
+ ## This is too long!
+
+ * Sutty doesn't collect any kind of personal data.
+
+ * Sutty may only collect statistical data that doesn't identify
+ individuals.
+
+ ## Analytic data
+
+ Sutty may only collect data for analytics (number of visits, duration,
+ etc.), not associated to personal data.
+
+ Analytical data collected for every web site can only be used internally
+ by Sutty. Sutty doesn't share any data privately with any third
+ parties. Selected analytical data could be used publicly.
+
+ Sutty doesn't recommend personal data collection in any way, but it
+ doesn't monitor if its users use third party services with their own
+ privacy policies. We recommend users and visitors to inform themselves
+ before using third parties analytics services.
+
+ ## No personal data collection
+
+ Sutty doesn't collect IP addresses from users nor visitors in any way.
+
+ Sutty doesn't ask for personal data for registering user accounts in its
+ platform.
+
+ Sutty only uses session "cookies" to identify users during their use of
+ the platform. It doesn't use "cookies" to identify visitors of web
+ sites hosted by Sutty.
+
+ The only exception where Sutty could collect personal data is during
+ service payment. Digital safety measures will be taken to keep this
+ information and to discard it if possible after needed.
+
+ Users will be notified when their personal data is removed.
+
+ If users decide to host their web sites with third parties, they must
+ inform themselves about the corresponding privacy policies. Sutty only
+ recommends third parties with privacy policies compatible with these.
+ content_es: |
+ > Utilizamos la e como pronombre neutro para referirnos a personas
+ > independientemente de su identidad de género, por ejemplo “usuarie”.
+
+ Este documento detalla la política de privacidad de Sutty, incluyendo
+ sitio web, plataforma de edición, infraestructura relacionada (salas de
+ chat, etc.) y sitios creados por sus usuaries a través de la plataforma,
+ en adelante "Sutty".
+
+ ## ¡Esto es demasiado largo!
+
+ Un resumen:
+
+ * Sutty no recolecta datos personales de ningún tipo
+
+ * Sutty solo recolectaría datos analíticos que no identifican a
+ personas
+
+ ## Datos analíticos
+
+ La única recolección de datos realizada por Sutty es con fines
+ analíticos (cantidad de visitas, duración, etc.), no asociados a datos
+ personales.
+
+ Los datos analíticos recolectados por cada sitio podrán ser utilizados
+ internamente por Sutty. Sutty no comparte datos analíticos con
+ terceros en forma privada. Datos analíticos seleccionados podrán ser
+ utilizados públicamente.
+
+ Sutty no recomienda la recolección de datos personales de ninguna forma,
+ pero no monitorea que les usuaries utilicen servicios de terceros con
+ sus propias políticas de privacidad. Recomendamos a les usuaries y
+ visitantes informarse antes de utilizar servicios de estadísticas de
+ terceros.
+
+ ## No registro de datos personales
+
+ Sutty no registra direcciones IP de usuaries ni de visitantes de ninguna
+ forma.
+
+ Sutty no solicita datos personales para el registro de cuentas de
+ usuarie en su plataforma.
+
+ Sutty solo utiliza “cookies” de sesión para identificar usuaries
+ mientras utilicen la plataforma. No se utilizan “cookies” para
+ identificar visitantes a los sitios alojados por Sutty.
+
+ El único caso en el que Sutty podría solicitar datos personales es
+ durante el pago de servicios. Se tomarán medidas de seguridad digital
+ para salvaguardar esta información y descartar lo que sea posible una
+ vez que ya no sea necesaria.
+
+ Se notificará a les usuaries cuando su información personal sea
+ eliminada.
+
+ Si les usuaries deciden alojar sus sitios con terceros, deberán
+ informarse de las políticas de privacidad correspondientes. Sutty
+ recomienda servicios de terceros con políticas de privacidad coherentes
+ con estas.
diff --git a/monit.conf b/monit.conf
index 1d6bbae1..0bd18907 100644
--- a/monit.conf
+++ b/monit.conf
@@ -4,6 +4,11 @@ check program cleanup
every "0 3 1 * *"
if status != 0 then alert
+check program distributed_press_tokens_renew
+ with path "/usr/bin/foreman run -f /srv/Procfile -d /srv distributed_press_tokens_renew" as uid "rails" gid "www-data"
+ every "0 3 * * *"
+ if status != 0 then alert
+
check program access_logs
with path "/srv/http/bin/access_logs" as uid "app" and gid "www-data"
every "0 0 * * *"
diff --git a/package.json b/package.json
index d520c8f5..6a5f159f 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,6 @@
{
"name": "sutty",
+ "author": "Sutty ",
"private": true,
"dependencies": {
"@airbrake/browser": "^1.4.1",