mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-15 09:01:42 +00:00
Merge branch 'rails' of 0xacab.org:sutty/sutty into issue-12980
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
commit
b96363a59f
45 changed files with 583 additions and 129 deletions
5
Gemfile
5
Gemfile
|
@ -39,7 +39,7 @@ gem 'commonmarker'
|
|||
gem 'devise'
|
||||
gem 'devise-i18n'
|
||||
gem 'devise_invitable'
|
||||
gem 'distributed-press-api-client', '~> 0.2.3'
|
||||
gem 'distributed-press-api-client', '~> 0.3.0rc0'
|
||||
gem 'njalla-api-client', '~> 0.2.0'
|
||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||
gem 'exception_notification'
|
||||
|
@ -69,7 +69,8 @@ gem 'redis', '~> 4.0', require: %w[redis redis/connection/hiredis]
|
|||
gem 'redis-rails'
|
||||
gem 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update'
|
||||
gem 'rubyzip'
|
||||
gem 'rugged'
|
||||
gem 'rugged', '1.5.0.1'
|
||||
gem 'git_clone_url'
|
||||
gem 'concurrent-ruby-ext'
|
||||
gem 'que'
|
||||
gem 'symbol-fstring', require: 'fstring/all'
|
||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -154,7 +154,7 @@ GEM
|
|||
devise_invitable (2.0.8)
|
||||
actionmailer (>= 5.0)
|
||||
devise (>= 4.6)
|
||||
distributed-press-api-client (0.2.4)
|
||||
distributed-press-api-client (0.3.0rc0)
|
||||
addressable (~> 2.3, >= 2.3.0)
|
||||
climate_control
|
||||
dry-schema
|
||||
|
@ -218,6 +218,8 @@ GEM
|
|||
activerecord (>= 4.0.0)
|
||||
get_process_mem (0.2.7)
|
||||
ffi (~> 1.0)
|
||||
git_clone_url (2.0.0)
|
||||
uri-ssh_git (>= 2.0)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
groupdate (6.2.1)
|
||||
|
@ -495,7 +497,7 @@ GEM
|
|||
ruby_parser (3.20.1)
|
||||
sexp_processor (~> 4.16)
|
||||
rubyzip (2.3.2)
|
||||
rugged (1.6.3-x86_64-linux-musl)
|
||||
rugged (1.5.0.1-x86_64-linux-musl)
|
||||
safe_yaml (1.0.6)
|
||||
safely_block (0.3.0)
|
||||
errbase (>= 0.1.1)
|
||||
|
@ -554,6 +556,7 @@ GEM
|
|||
unf_ext
|
||||
unf_ext (0.0.8.2-x86_64-linux-musl)
|
||||
unicode-display_width (1.8.0)
|
||||
uri-ssh_git (2.0.0)
|
||||
validates_hostname (1.0.13)
|
||||
activerecord (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
|
@ -597,7 +600,7 @@ DEPENDENCIES
|
|||
devise
|
||||
devise-i18n
|
||||
devise_invitable
|
||||
distributed-press-api-client (~> 0.2.3)
|
||||
distributed-press-api-client (~> 0.3.0rc0)
|
||||
dotenv-rails
|
||||
down
|
||||
ed25519
|
||||
|
@ -608,6 +611,7 @@ DEPENDENCIES
|
|||
fast_jsonparser (~> 0.5.0)
|
||||
flamegraph
|
||||
friendly_id
|
||||
git_clone_url
|
||||
hairtrigger
|
||||
haml-lint
|
||||
hamlit-rails
|
||||
|
@ -652,7 +656,7 @@ DEPENDENCIES
|
|||
rollups!
|
||||
rubocop-rails
|
||||
rubyzip
|
||||
rugged
|
||||
rugged (= 1.5.0.1)
|
||||
safe_yaml
|
||||
safely_block (~> 0.3.0)
|
||||
sassc-rails
|
||||
|
|
77
app/controllers/api/v1/webhooks_controller.rb
Normal file
77
app/controllers/api/v1/webhooks_controller.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
# Recibe webhooks y lanza un PullJob
|
||||
class WebhooksController < BaseController
|
||||
# responde con forbidden si falla la validación del token
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer
|
||||
|
||||
# Trae los cambios a partir de un post de Webhooks:
|
||||
# (Gitlab, Github, Gitea, etc)
|
||||
#
|
||||
# @return [nil]
|
||||
def pull
|
||||
message = I18n.with_locale(site.default_locale) do
|
||||
I18n.t('webhooks.pull.message')
|
||||
end
|
||||
|
||||
GitPullJob.perform_later(site, usuarie, message)
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# encuentra el sitio a partir de la url
|
||||
def site
|
||||
@site ||= Site.find_by_name!(params[:site_id])
|
||||
end
|
||||
|
||||
# valida el token que envía la plataforma del webhook
|
||||
#
|
||||
# @return [String]
|
||||
def token
|
||||
@token ||=
|
||||
begin
|
||||
# Gitlab
|
||||
if request.headers['X-Gitlab-Token'].present?
|
||||
request.headers['X-Gitlab-Token']
|
||||
# Github
|
||||
elsif request.headers['X-Hub-Signature-256'].present?
|
||||
token_from_signature(request.headers['X-Hub-Signature-256'], 'sha256=')
|
||||
# Gitea
|
||||
elsif request.headers['X-Gitea-Signature'].present?
|
||||
token_from_signature(request.headers['X-Gitea-Signature'])
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound, 'proveedor no soportado'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# valida token a partir de firma de webhook
|
||||
#
|
||||
# @return [String, Boolean]
|
||||
def token_from_signature(signature, prepend = '')
|
||||
payload = request.body.read
|
||||
site.roles.where(temporal: false, rol: 'usuarie').pluck(:token).find do |token|
|
||||
new_signature = prepend + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), token, payload)
|
||||
ActiveSupport::SecurityUtils.secure_compare(new_signature, signature.to_s)
|
||||
end.tap do |t|
|
||||
raise ActiveRecord::RecordNotFound, 'token no encontrado' if t.nil?
|
||||
end
|
||||
end
|
||||
|
||||
# encuentra le usuarie
|
||||
def usuarie
|
||||
@usuarie ||= site.roles.find_by!(temporal: false, rol: 'usuarie', token: token).usuarie
|
||||
end
|
||||
|
||||
# respuesta de error a plataformas
|
||||
def platforms_answer(exception)
|
||||
ExceptionNotifier.notify_exception(exception, data: { headers: request.headers.to_h }
|
||||
|
||||
head :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -59,7 +59,11 @@ class ApplicationController < ActionController::Base
|
|||
#
|
||||
# @return [String,Symbol]
|
||||
def current_locale
|
||||
session[:locale] = params[:change_locale_to] if params[:change_locale_to].present?
|
||||
locale = params[:change_locale_to]
|
||||
|
||||
if locale.present? && I18n.locale_available?(locale)
|
||||
session[:locale] = params[:change_locale_to]
|
||||
end
|
||||
|
||||
session[:locale] || current_usuarie&.lang || I18n.locale
|
||||
end
|
||||
|
|
|
@ -22,7 +22,12 @@ class BuildStatsController < ApplicationController
|
|||
|
||||
@table = site.deployment_list.map do |deploy|
|
||||
type = deploy.class.name.underscore
|
||||
urls = deploy.respond_to?(:urls) ? deploy.urls : [deploy.url].compact
|
||||
urls = deploy.urls.map do |url|
|
||||
URI.parse(url)
|
||||
rescue URI::Error
|
||||
nil
|
||||
end.compact
|
||||
|
||||
urls = [nil] if urls.empty?
|
||||
build_stat = deploy.build_stats.where(status: true).last
|
||||
seconds = build_stat&.seconds || 0
|
||||
|
|
|
@ -24,6 +24,7 @@ class PostsController < ApplicationController
|
|||
|
||||
# Todos los artículos de este sitio para el idioma actual
|
||||
@posts = site.indexed_posts.where(locale: locale)
|
||||
@posts = @posts.page(filter_params.delete(:page)) if site.pagination
|
||||
# De este tipo
|
||||
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
|
||||
# Que estén dentro de la categoría
|
||||
|
@ -154,7 +155,7 @@ class PostsController < ApplicationController
|
|||
#
|
||||
# @return [Hash]
|
||||
def filter_params
|
||||
@filter_params ||= params.permit(:q, :category, :layout).to_hash.select do |_, v|
|
||||
@filter_params ||= params.permit(:q, :category, :layout, :page).to_hash.select do |_, v|
|
||||
v.present?
|
||||
end.transform_keys(&:to_sym)
|
||||
end
|
||||
|
|
|
@ -57,6 +57,7 @@ class SitesController < ApplicationController
|
|||
usuarie: current_usuarie)
|
||||
|
||||
if service.update.valid?
|
||||
flash[:notice] = I18n.t('sites.update.post')
|
||||
redirect_to site_posts_path(site, locale: site.default_locale)
|
||||
else
|
||||
render 'edit'
|
||||
|
|
|
@ -51,7 +51,11 @@ class DeployJob < ApplicationJob
|
|||
status = d.deploy(output: @output)
|
||||
seconds = d.build_stats.last.try(:seconds) || 0
|
||||
size = d.size
|
||||
urls = d.respond_to?(:urls) ? d.urls : [d.url].compact
|
||||
urls = d.urls.map do |url|
|
||||
URI.parse url
|
||||
rescue URI::Error
|
||||
nil
|
||||
end.compact
|
||||
rescue StandardError => e
|
||||
status = false
|
||||
seconds ||= 0
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Permite traer los cambios cada vez que se
|
||||
# hace un push al repositorio
|
||||
# Permite traer los cambios desde webhooks
|
||||
|
||||
class GitPullJob < ApplicationJob
|
||||
# @param :site [Site]
|
||||
# @param :usuarie [Usuarie]
|
||||
|
|
|
@ -52,7 +52,7 @@ class DeployMailer < ApplicationMailer
|
|||
t << (row.map do |k, v|
|
||||
case k
|
||||
when :seconds then v[:human]
|
||||
when :urls then url
|
||||
when :urls then url.to_s
|
||||
else v
|
||||
end
|
||||
end)
|
||||
|
|
|
@ -23,6 +23,11 @@ class Deploy < ApplicationRecord
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# @return [Array]
|
||||
def urls
|
||||
[url].compact
|
||||
end
|
||||
|
||||
def limit
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
@ -55,6 +60,22 @@ class Deploy < ApplicationRecord
|
|||
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
|
||||
end
|
||||
|
||||
# Un entorno que solo tiene lo que necesitamos
|
||||
#
|
||||
# @return [Hash]
|
||||
def env
|
||||
# XXX: This doesn't support Windows paths :B
|
||||
paths = [File.dirname(`which bundle`), '/usr/local/bin', '/usr/bin', '/bin']
|
||||
|
||||
# Las variables de entorno extra no pueden superponerse al local.
|
||||
extra_env.merge({
|
||||
'HOME' => home_dir,
|
||||
'PATH' => paths.join(':'),
|
||||
'JEKYLL_ENV' => Rails.env,
|
||||
'LANG' => ENV['LANG'],
|
||||
})
|
||||
end
|
||||
|
||||
# Corre un comando, lo registra en la base de datos y devuelve el
|
||||
# estado.
|
||||
#
|
||||
|
@ -65,22 +86,20 @@ class Deploy < ApplicationRecord
|
|||
lines = []
|
||||
|
||||
time_start
|
||||
Dir.chdir(site.path) do
|
||||
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, o, t|
|
||||
# TODO: Enviar a un websocket para ver el proceso en vivo?
|
||||
Thread.new do
|
||||
o.each do |line|
|
||||
lines << line
|
||||
Open3.popen2e(env, cmd, unsetenv_others: true, chdir: site.path) do |_, o, t|
|
||||
# TODO: Enviar a un websocket para ver el proceso en vivo?
|
||||
Thread.new do
|
||||
o.each do |line|
|
||||
lines << line
|
||||
|
||||
puts line if output
|
||||
end
|
||||
rescue IOError => e
|
||||
lines << e.message
|
||||
puts e.message if output
|
||||
puts line if output
|
||||
end
|
||||
|
||||
r = t.value
|
||||
rescue IOError => e
|
||||
lines << e.message
|
||||
puts e.message if output
|
||||
end
|
||||
|
||||
r = t.value
|
||||
end
|
||||
time_stop
|
||||
|
||||
|
@ -100,6 +119,11 @@ class Deploy < ApplicationRecord
|
|||
@local_env ||= {}
|
||||
end
|
||||
|
||||
# Devuelve opciones para jekyll build
|
||||
#
|
||||
# @return [String,nil]
|
||||
def flags_for_build(**args); end
|
||||
|
||||
# Trae todas las dependencias
|
||||
#
|
||||
# @return [Array]
|
||||
|
@ -109,6 +133,21 @@ class Deploy < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
# Escribe el contenido en un archivo temporal y ejecuta el bloque
|
||||
# provisto con el archivo como parámetro
|
||||
#
|
||||
# @param :content [String]
|
||||
def with_tempfile(content, &block)
|
||||
Tempfile.create(SecureRandom.hex) do |file|
|
||||
file.write content.to_s
|
||||
file.rewind
|
||||
file.close
|
||||
|
||||
# @yieldparam :file [File]
|
||||
yield file
|
||||
end
|
||||
end
|
||||
|
||||
# @param [String]
|
||||
# @return [String]
|
||||
def readable_cmd(cmd)
|
||||
|
@ -119,7 +158,14 @@ class Deploy < ApplicationRecord
|
|||
@deploy_local ||= site.deploys.find_by(type: 'DeployLocal')
|
||||
end
|
||||
|
||||
def non_local_deploys
|
||||
@non_local_deploys ||= site.deploys.where.not(type: 'DeployLocal')
|
||||
# Consigue todas las variables de entorno configuradas por otros
|
||||
# deploys.
|
||||
#
|
||||
# @return [Hash]
|
||||
def extra_env
|
||||
@extra_env ||=
|
||||
site.deployment_list.reduce({}) do |extra, deploy|
|
||||
extra.merge deploy.local_env
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -52,7 +52,12 @@ class DeployDistributedPress < Deploy
|
|||
end
|
||||
end
|
||||
|
||||
status = c.publish(publishing_site, deploy_local.destination)
|
||||
begin
|
||||
status = c.publish(publishing_site, deploy_local.destination)
|
||||
rescue DistributedPress::V1::Error => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||
status = false
|
||||
end
|
||||
|
||||
if status
|
||||
self.remote_info[:distributed_press] = c.show(publishing_site).to_h
|
||||
|
|
|
@ -62,34 +62,26 @@ class DeployLocal < Deploy
|
|||
FileUtils.rm_rf(File.join(site.path, '.jekyll-cache'))
|
||||
end
|
||||
|
||||
# Opciones necesarias para la compilación del sitio
|
||||
#
|
||||
# @return [Hash]
|
||||
def local_env
|
||||
@local_env ||= {
|
||||
'SPREE_API_KEY' => site.tienda_api_key,
|
||||
'SPREE_URL' => site.tienda_url,
|
||||
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||
'YARN_CACHE_FOLDER' => yarn_cache_dir,
|
||||
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mkdir
|
||||
FileUtils.mkdir_p destination
|
||||
end
|
||||
|
||||
# Un entorno que solo tiene lo que necesitamos
|
||||
#
|
||||
# @return [Hash]
|
||||
def env
|
||||
# XXX: This doesn't support Windows paths :B
|
||||
paths = [File.dirname(`which bundle`), '/usr/local/bin', '/usr/bin', '/bin']
|
||||
|
||||
# Las variables de entorno extra no pueden superponerse al local.
|
||||
extra_env.merge({
|
||||
'HOME' => home_dir,
|
||||
'PATH' => paths.join(':'),
|
||||
'SPREE_API_KEY' => site.tienda_api_key,
|
||||
'SPREE_URL' => site.tienda_url,
|
||||
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||
'JEKYLL_ENV' => Rails.env,
|
||||
'LANG' => ENV['LANG'],
|
||||
'YARN_CACHE_FOLDER' => yarn_cache_dir,
|
||||
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
|
||||
})
|
||||
end
|
||||
|
||||
def yarn_cache_dir
|
||||
Rails.root.join('_yarn_cache').to_s
|
||||
end
|
||||
|
@ -138,11 +130,20 @@ class DeployLocal < Deploy
|
|||
end
|
||||
|
||||
def bundle(output: false)
|
||||
run %(bundle install --deployment --no-cache --path="#{gems_dir}" --clean --without test development), output: output
|
||||
run %(bundle config set --local clean 'true'), output: output
|
||||
run %(bundle config set --local deployment 'true'), output: output
|
||||
run %(bundle config set --local path '#{gems_dir}'), output: output
|
||||
run %(bundle config set --local without 'test development'), output: output
|
||||
run %(bundle config set --local cache_all 'false'), output: output
|
||||
run %(bundle install), output: output
|
||||
end
|
||||
|
||||
def jekyll_build(output: false)
|
||||
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}"), output: output
|
||||
with_tempfile(site.private_key_pem) do |file|
|
||||
flags = extra_flags(private_key: file)
|
||||
|
||||
run %(bundle exec jekyll build --trace --profile #{flags} --destination "#{escaped_destination}"), output: output
|
||||
end
|
||||
end
|
||||
|
||||
# no debería haber espacios ni caracteres especiales, pero por si
|
||||
|
@ -156,17 +157,13 @@ class DeployLocal < Deploy
|
|||
FileUtils.rm_rf destination
|
||||
end
|
||||
|
||||
# Consigue todas las variables de entorno configuradas por otros
|
||||
# deploys.
|
||||
# Genera opciones extra desde los 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
|
||||
# @param :args [Hash]
|
||||
# @return [String]
|
||||
def extra_flags(**args)
|
||||
site.deployment_list.map do |deploy|
|
||||
deploy.flags_for_build(**args)
|
||||
end.compact.join(' ')
|
||||
end
|
||||
end
|
||||
|
|
55
app/models/deploy_social_distributed_press.rb
Normal file
55
app/models/deploy_social_distributed_press.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'distributed_press/v1/social/client'
|
||||
|
||||
# Publicar novedades al Fediverso
|
||||
class DeploySocialDistributedPress < Deploy
|
||||
# Solo luego de publicar remotamente
|
||||
DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync]
|
||||
|
||||
# Envía las notificaciones
|
||||
def deploy(output: false)
|
||||
with_tempfile(site.private_key_pem) do |file|
|
||||
key = Shellwords.escape file.path
|
||||
dest = Shellwords.escape destination
|
||||
|
||||
run %(bundle exec jekyll notify --trace --key #{key} --destination "#{dest}"), output: output
|
||||
end
|
||||
end
|
||||
|
||||
# Igual que DeployLocal
|
||||
#
|
||||
# @return [String]
|
||||
def destination
|
||||
File.join(Rails.root, '_deploy', site.hostname)
|
||||
end
|
||||
|
||||
# Solo uno
|
||||
#
|
||||
# @return [Integer]
|
||||
def limit
|
||||
1
|
||||
end
|
||||
|
||||
# Espacio ocupado, pero no podemos calcularlo
|
||||
#
|
||||
# @return [Integer]
|
||||
def size
|
||||
0
|
||||
end
|
||||
|
||||
# El perfil de actor
|
||||
#
|
||||
# @return [String,nil]
|
||||
def url
|
||||
site.data.dig('activity_pub', 'actor')
|
||||
end
|
||||
|
||||
# Genera la opción de llave privada para jekyll build
|
||||
#
|
||||
# @params :args [Hash]
|
||||
# @return [String]
|
||||
def flags_for_build(**args)
|
||||
"--key #{Shellwords.escape args[:private_key].path}"
|
||||
end
|
||||
end
|
|
@ -4,8 +4,12 @@
|
|||
#
|
||||
# Esto es increíblemente difícil de lograr que salga bien!
|
||||
class MetadataBoolean < MetadataTemplate
|
||||
# El valor por defecto es una versión booleana de lo que diga (o no
|
||||
# diga) el esquema
|
||||
#
|
||||
# @return [Boolean]
|
||||
def default_value
|
||||
false
|
||||
!!super
|
||||
end
|
||||
|
||||
# Los checkboxes son especiales porque la especificación de HTML
|
||||
|
|
19
app/models/metadata_created_at.rb
Normal file
19
app/models/metadata_created_at.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Fecha y hora de creación
|
||||
class MetadataCreatedAt < MetadataTemplate
|
||||
# Por defecto la hora actual, pero por retrocompatibilidad, queremos
|
||||
# la fecha de publicación
|
||||
def default_value
|
||||
if post.date.value.to_date < Time.now.to_date
|
||||
post.date.value
|
||||
else
|
||||
Time.now
|
||||
end
|
||||
end
|
||||
|
||||
# Nunca cambia
|
||||
def value=(new_value)
|
||||
value
|
||||
end
|
||||
end
|
|
@ -34,7 +34,7 @@ class MetadataRelatedPosts < MetadataArray
|
|||
end
|
||||
|
||||
def title(post)
|
||||
"#{post&.title&.value || post&.slug&.value} (#{post.layout.humanized_name})"
|
||||
"#{post&.title&.value || post&.slug&.value} #{post&.date&.value.strftime('%F')} (#{post.layout.humanized_name})"
|
||||
end
|
||||
|
||||
# Encuentra el filtro
|
||||
|
|
|
@ -202,7 +202,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
|||
|
||||
def allowed_attributes
|
||||
@allowed_attributes ||= %w[style href src alt controls data-align data-multimedia data-multimedia-inner id
|
||||
name].freeze
|
||||
name start].freeze
|
||||
end
|
||||
|
||||
def allowed_tags
|
||||
|
|
|
@ -12,7 +12,7 @@ class Post
|
|||
DEFAULT_ATTRIBUTES = %i[site document layout].freeze
|
||||
# Otros atributos que no vienen en los metadatos
|
||||
PRIVATE_ATTRIBUTES = %i[path slug attributes errors].freeze
|
||||
PUBLIC_ATTRIBUTES = %i[lang date uuid].freeze
|
||||
PUBLIC_ATTRIBUTES = %i[lang date uuid created_at].freeze
|
||||
ATTR_SUFFIXES = %w[? =].freeze
|
||||
|
||||
attr_reader :attributes, :errors, :layout, :site, :document
|
||||
|
@ -217,6 +217,11 @@ class Post
|
|||
post: self, required: true)
|
||||
end
|
||||
|
||||
# La fecha de creación inmodificable del post
|
||||
def created_at
|
||||
@metadata[:created_at] ||= MetadataCreatedAt.new(document: document, site: site, layout: layout, name: :created_at, type: :created_at, post: self, required: true)
|
||||
end
|
||||
|
||||
# Detecta si es un atributo válido o no, a partir de la tabla de la
|
||||
# plantilla
|
||||
def attribute?(mid)
|
||||
|
@ -267,6 +272,7 @@ class Post
|
|||
# Y que no se procese liquid
|
||||
yaml['liquid'] = false
|
||||
yaml['usuaries'] = usuaries.map(&:id).uniq
|
||||
yaml['created_at'] = created_at.value
|
||||
yaml['last_modified_at'] = modified_at
|
||||
|
||||
"#{yaml.to_yaml}---\n\n#{body}"
|
||||
|
|
|
@ -14,6 +14,8 @@ class Rol < ApplicationRecord
|
|||
|
||||
validates_inclusion_of :rol, in: ROLES
|
||||
|
||||
before_save :add_token_if_missing!
|
||||
|
||||
def invitade?
|
||||
rol == INVITADE
|
||||
end
|
||||
|
@ -25,4 +27,11 @@ class Rol < ApplicationRecord
|
|||
def self.role?(rol)
|
||||
ROLES.include? rol
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Asegurarse que tenga un token
|
||||
def add_token_if_missing!
|
||||
self.token ||= SecureRandom.hex(64)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ class Site < ApplicationRecord
|
|||
include Site::DeployDependencies
|
||||
include Site::BuildStats
|
||||
include Site::LayoutOrdering
|
||||
include Site::SocialDistributedPress
|
||||
include Tienda
|
||||
|
||||
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
|
||||
|
@ -18,10 +19,6 @@ class Site < ApplicationRecord
|
|||
# protege de acceso al panel de Sutty!
|
||||
encrypts :private_key
|
||||
|
||||
# TODO: Hacer que los diferentes tipos de deploy se auto registren
|
||||
# @see app/services/site_service.rb
|
||||
DEPLOYS = %i[local private www zip hidden_service distributed_press].freeze
|
||||
|
||||
validates :name, uniqueness: true, hostname: {
|
||||
allow_root_label: true
|
||||
}
|
||||
|
@ -447,6 +444,10 @@ class Site < ApplicationRecord
|
|||
find_by(name: "#{Site.domain}.")
|
||||
end
|
||||
|
||||
def self.one_at_a_time
|
||||
@@one_at_a_time ||= Thread::Mutex.new
|
||||
end
|
||||
|
||||
def reset
|
||||
@read = false
|
||||
@layouts = nil
|
||||
|
@ -474,6 +475,9 @@ class Site < ApplicationRecord
|
|||
return if jekyll?
|
||||
|
||||
Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path
|
||||
|
||||
# Necesita un bloque
|
||||
repository.rugged.remotes.rename('origin', 'upstream') {}
|
||||
end
|
||||
|
||||
# Elimina el directorio del sitio
|
||||
|
@ -553,7 +557,9 @@ class Site < ApplicationRecord
|
|||
end
|
||||
|
||||
def run_in_path(&block)
|
||||
Dir.chdir path, &block
|
||||
Site.one_at_a_time.synchronize do
|
||||
Dir.chdir path, &block
|
||||
end
|
||||
end
|
||||
|
||||
# Instala las gemas cuando es necesario:
|
||||
|
|
|
@ -8,8 +8,6 @@ class Site
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# TODO: Debería ser un Job?
|
||||
after_create :index_posts!
|
||||
has_many :indexed_posts, dependent: :destroy
|
||||
|
||||
MODIFIED_STATUSES = %i[added modified].freeze
|
||||
|
@ -108,11 +106,14 @@ class Site
|
|||
end
|
||||
|
||||
# Obtiene el idioma y la ruta del post a partir de la ubicación en
|
||||
# el disco
|
||||
# el disco.
|
||||
#
|
||||
# Las rutas vienen en ASCII-9BIT desde Rugged, pero en realidad
|
||||
# son UTF-8
|
||||
#
|
||||
# @return [Array<String>]
|
||||
def locale_and_path_from(path)
|
||||
locale, path = path.split(File::SEPARATOR, 2)
|
||||
locale, path = path.force_encoding('utf-8').split(File::SEPARATOR, 2)
|
||||
|
||||
[
|
||||
locale.sub(LOCALE_FROM_PATH, ''),
|
||||
|
|
|
@ -45,9 +45,7 @@ class Site
|
|||
# @return [Integer]
|
||||
def fetch
|
||||
if origin.check_connection(:fetch, credentials: credentials)
|
||||
rugged.fetch(origin, credentials: credentials)[:received_objects].tap do |objects|
|
||||
git_sh("git", "lfs", "fetch", "origin", default_branch) if objects&.positive?
|
||||
end
|
||||
rugged.fetch(origin, credentials: credentials)[:received_objects]
|
||||
else
|
||||
0
|
||||
end
|
||||
|
@ -56,7 +54,7 @@ class Site
|
|||
# Incorpora los cambios en el repositorio actual
|
||||
#
|
||||
# @return [Rugged::Commit]
|
||||
def merge(usuarie)
|
||||
def merge(usuarie, message = I18n.t('sites.fetch.merge.message'))
|
||||
merge = rugged.merge_commits(head_commit, remote_head_commit)
|
||||
|
||||
# No hacemos nada si hay conflictos, pero notificarnos
|
||||
|
@ -71,12 +69,14 @@ class Site
|
|||
.create(rugged, update_ref: 'HEAD',
|
||||
parents: [head_commit, remote_head_commit],
|
||||
tree: merge.write_tree(rugged),
|
||||
message: I18n.t('sites.fetch.merge.message'),
|
||||
message: message,
|
||||
author: author(usuarie), committer: committer)
|
||||
|
||||
# Forzamos el checkout para mover el HEAD al último commit y
|
||||
# escribir los cambios
|
||||
rugged.checkout 'HEAD', strategy: :force
|
||||
|
||||
git_sh("git", "lfs", "fetch", "origin", default_branch)
|
||||
# reemplaza los pointers por los archivos correspondientes
|
||||
git_sh("git", "lfs", "checkout")
|
||||
commit
|
||||
|
@ -162,22 +162,49 @@ class Site
|
|||
|
||||
# Pushea cambios al repositorio remoto
|
||||
#
|
||||
# @param :remote [Rugged::Remote]
|
||||
# @return [Boolean, nil]
|
||||
def push
|
||||
origin.push(rugged.head.canonical_name, credentials: credentials)
|
||||
git_sh("git", "lfs", "push", "origin", default_branch)
|
||||
def push(remote = origin)
|
||||
remote.push(rugged.head.canonical_name, credentials: credentials_for(remote))
|
||||
git_sh('git', 'lfs', 'push', remote.name, default_branch)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @deprecated
|
||||
def credentials
|
||||
@credentials ||= credentials_for(origin)
|
||||
end
|
||||
|
||||
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las
|
||||
# credenciales necesarias para trabajar con repositorios remotos.
|
||||
#
|
||||
# @param :remote [Rugged::Remote]
|
||||
# @return [Nil, Rugged::Credentials::SshKey]
|
||||
def credentials
|
||||
def credentials_for(remote)
|
||||
return unless File.exist? private_key
|
||||
|
||||
@credentials ||= Rugged::Credentials::SshKey.new username: 'git', publickey: public_key, privatekey: private_key
|
||||
Rugged::Credentials::SshKey.new username: username_for(remote), publickey: public_key, privatekey: private_key
|
||||
end
|
||||
|
||||
# Obtiene el nombre de usuario para el repositorio remoto, por
|
||||
# defecto git
|
||||
#
|
||||
# @param :remote [Rugged::Remote]
|
||||
# @return [String]
|
||||
def username_for(remote)
|
||||
username = parse_url(remote.url)&.user if remote.respond_to? :url
|
||||
|
||||
username || 'git'
|
||||
end
|
||||
|
||||
# @param :url [String]
|
||||
# @return [URI, nil]
|
||||
def parse_url(url)
|
||||
GitCloneUrl.parse(url)
|
||||
rescue URI::Error => e
|
||||
ExceptionNotifier.notify_exception(e, data: { path: path, url: url })
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
|
|
23
app/models/site/social_distributed_press.rb
Normal file
23
app/models/site/social_distributed_press.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Site
|
||||
# Agrega soporte para Social Distributed Press en los sitios
|
||||
module SocialDistributedPress
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
encrypts :private_key_pem
|
||||
|
||||
before_save :generate_private_key_pem!, unless: :private_key_pem?
|
||||
|
||||
private
|
||||
|
||||
# Genera la llave privada y la almacena
|
||||
#
|
||||
# @return [nil]
|
||||
def generate_private_key_pem!
|
||||
self.private_key_pem ||= DistributedPress::V1::Social::Client.new(public_key_url: nil, key_size: 2048).private_key.export
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,6 +10,7 @@ class Usuarie < ApplicationRecord
|
|||
|
||||
validates_uniqueness_of :email
|
||||
validates_with EmailAddress::ActiveRecordValidator, field: :email
|
||||
validate :locale_available!
|
||||
|
||||
before_create :lang_from_locale!
|
||||
before_update :remove_confirmation_invitation_inconsistencies!
|
||||
|
@ -78,4 +79,15 @@ class Usuarie < ApplicationRecord
|
|||
self.invitation_accepted_at ||= Time.now.utc
|
||||
end
|
||||
end
|
||||
|
||||
# Muestra un error si el idioma no está disponible al cambiar el
|
||||
# idioma de la cuenta.
|
||||
#
|
||||
# @return [nil]
|
||||
def locale_available!
|
||||
return if I18n.locale_available? self.lang
|
||||
|
||||
errors.add(:lang, I18n.t('activerecord.errors.models.usuarie.attributes.lang.not_available'))
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -102,6 +102,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
|||
usuarie: usuarie,
|
||||
message: I18n.t("post_service.#{action}",
|
||||
title: post&.title&.value))
|
||||
|
||||
GitPushJob.perform_later(site)
|
||||
end
|
||||
|
||||
# Solo permitir cambiar estos atributos de cada articulo
|
||||
|
|
|
@ -33,6 +33,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
|||
add_licencias &&
|
||||
add_code_of_conduct &&
|
||||
add_privacy_policy &&
|
||||
site.index_posts! &&
|
||||
deploy
|
||||
end
|
||||
|
||||
|
@ -54,9 +55,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
|||
|
||||
# Genera los Deploy necesarios para el sitio a menos que ya los tenga.
|
||||
def build_deploys
|
||||
Site::DEPLOYS.map { |deploy| "Deploy#{deploy.to_s.camelcase}" }
|
||||
.each do |deploy|
|
||||
next if site.deploys.find_by type: deploy
|
||||
Deploy.subclasses.each do |deploy|
|
||||
next if site.deploys.find_by type: deploy.name
|
||||
|
||||
site.deploys.build type: deploy
|
||||
end
|
||||
|
@ -97,6 +97,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
|||
add: [site.config.path],
|
||||
message: I18n.t("site_service.#{action}",
|
||||
name: site.name))
|
||||
|
||||
GitPushJob.perform_later(site)
|
||||
end
|
||||
|
||||
def add_role(temporal: true, rol: 'invitade')
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
- row[:urls].each do |url|
|
||||
%tr
|
||||
%th{ scope: 'row' }= row[:title]
|
||||
%td= link_to_if url.present?, url, url, class: 'word-break-all'
|
||||
%td= link_to_if (url.present? && url.scheme.present?), url.to_s, url.to_s, class: 'word-break-all'
|
||||
%td
|
||||
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
|
||||
%td= row[:size]
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
%tr
|
||||
%td= row[:title]
|
||||
%td= row[:status]
|
||||
%td= link_to_if url.present?, url, url
|
||||
%td= link_to_if (url.present? && url.scheme.present?), url.to_s, url.to_s
|
||||
%td
|
||||
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
|
||||
%td= row[:size]
|
||||
|
|
21
app/views/deploys/_deploy_social_distributed_press.haml
Normal file
21
app/views/deploys/_deploy_social_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'),
|
||||
tags: %w[p strong em a]
|
||||
|
||||
|
||||
%hr/
|
|
@ -1,4 +1,4 @@
|
|||
- flash.each do |type, message|
|
||||
- unless type == 'js'
|
||||
= render 'bootstrap/alert' do
|
||||
= message
|
||||
= sanitize_markdown message, tags: %w[a strong em]
|
||||
|
|
4
app/views/posts/attribute_ro/_created_at.haml
Normal file
4
app/views/posts/attribute_ro/_created_at.haml
Normal file
|
@ -0,0 +1,4 @@
|
|||
%tr{ id: attribute }
|
||||
%th= post_label_t(attribute, post: post)
|
||||
%td{ dir: dir, lang: locale }
|
||||
%time{ datetime: metadata.value.xmlschema }= l metadata.value
|
1
app/views/posts/attributes/_created_at.haml
Normal file
1
app/views/posts/attributes/_created_at.haml
Normal file
|
@ -0,0 +1 @@
|
|||
-# nada
|
|
@ -84,7 +84,10 @@
|
|||
%button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
|
||||
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
|
||||
|
||||
%div
|
||||
- if @site.pagination
|
||||
%div
|
||||
= link_to_prev_page @posts, t('posts.prev'), class: 'btn'
|
||||
= link_to_next_page @posts, t('posts.next'), class: 'btn'
|
||||
%tbody
|
||||
- dir = @site.data.dig(params[:locale], 'dir')
|
||||
- size = @posts.size
|
||||
|
|
|
@ -46,36 +46,37 @@
|
|||
.invalid-feedback= site.errors.messages[:description].join(', ')
|
||||
%hr/
|
||||
|
||||
.form-group#design_id
|
||||
%h2= t('.design.title')
|
||||
%p.lead= t('.help.design')
|
||||
- if invalid? site, :design_id
|
||||
= render 'bootstrap/alert' do
|
||||
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
|
||||
layouts: site.incompatible_layouts.to_sentence)
|
||||
.row.row-cols-1.row-cols-md-2.designs
|
||||
-# Demasiado complejo para un f.collection_radio_buttons
|
||||
- Design.all.order(priority: :desc).each do |design|
|
||||
.design.col.d-flex.flex-column
|
||||
.custom-control.custom-radio
|
||||
= f.radio_button :design_id, design.id,
|
||||
checked: design.id == site.design_id,
|
||||
disabled: design.disabled,
|
||||
required: true, class: 'custom-control-input'
|
||||
= f.label "design_id_#{design.id}", design.name,
|
||||
class: 'custom-control-label'
|
||||
.flex-fill
|
||||
= sanitize_markdown design.description,
|
||||
tags: %w[p a strong em]
|
||||
- unless site.persisted?
|
||||
.form-group#design_id
|
||||
%h2= t('.design.title')
|
||||
%p.lead= t('.help.design')
|
||||
- if invalid? site, :design_id
|
||||
= render 'bootstrap/alert' do
|
||||
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
|
||||
layouts: site.incompatible_layouts.to_sentence)
|
||||
.row.row-cols-1.row-cols-md-2.designs
|
||||
-# Demasiado complejo para un f.collection_radio_buttons
|
||||
- Design.all.order(priority: :desc).each do |design|
|
||||
.design.col.d-flex.flex-column
|
||||
.custom-control.custom-radio
|
||||
= f.radio_button :design_id, design.id,
|
||||
checked: design.id == site.design_id,
|
||||
disabled: design.disabled,
|
||||
required: true, class: 'custom-control-input'
|
||||
= f.label "design_id_#{design.id}", design.name,
|
||||
class: 'custom-control-label'
|
||||
.flex-fill
|
||||
= sanitize_markdown design.description,
|
||||
tags: %w[p a strong em]
|
||||
|
||||
.btn-group{ role: 'group', 'aria-label': t('.design.actions') }
|
||||
- if design.url
|
||||
= link_to t('.design.url'), design.url,
|
||||
target: '_blank', class: 'btn'
|
||||
- if design.license
|
||||
= link_to t('.design.license'), design.license,
|
||||
target: '_blank', class: 'btn'
|
||||
%hr/
|
||||
.btn-group{ role: 'group', 'aria-label': t('.design.actions') }
|
||||
- if design.url
|
||||
= link_to t('.design.url'), design.url,
|
||||
target: '_blank', class: 'btn'
|
||||
- if design.license
|
||||
= link_to t('.design.license'), design.license,
|
||||
target: '_blank', class: 'btn'
|
||||
%hr/
|
||||
|
||||
.form-group.licenses#license_id
|
||||
%h2= t('.licencia.title')
|
||||
|
|
|
@ -142,7 +142,7 @@ Rails.application.configure do
|
|||
}
|
||||
config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', "noreply@sutty.nl") }
|
||||
|
||||
config.middleware.use ExceptionNotification::Rack, gitlab: {}, ignore_exceptions: (['DeployJob::DeployAlreadyRunningException'] + ExceptionNotifier.ignored_exceptions)
|
||||
config.middleware.use ExceptionNotification::Rack, gitlab: {}, ignore_exceptions: ['DeployJob::DeployAlreadyRunningException']
|
||||
|
||||
Rails.application.routes.default_url_options[:host] = "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}"
|
||||
Rails.application.routes.default_url_options[:protocol] = 'https'
|
||||
|
|
|
@ -91,6 +91,15 @@ module Jekyll
|
|||
spec.name == name
|
||||
end
|
||||
|
||||
unless spec
|
||||
I18n.with_locale(locale) do
|
||||
raise Jekyll::Errors::InvalidThemeName, I18n.t('activerecord.errors.models.site.attributes.design_id.missing_gem', theme: name)
|
||||
rescue Jekyll::Errors::InvalidThemeName => e
|
||||
ExceptionNotifier.notify_exception(e, data: { theme: name, site: File.basename(site.source) })
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
ruby_version = Gem::Version.new(RUBY_VERSION)
|
||||
ruby_version.canonical_segments[2] = 0
|
||||
base_path = Rails.root.join('_storage', 'gems', File.basename(site.source), 'ruby',
|
||||
|
@ -114,6 +123,11 @@ module Jekyll
|
|||
private
|
||||
|
||||
def gemspec; end
|
||||
|
||||
# @return [Symbol]
|
||||
def locale
|
||||
@locale ||= (site.config['locale'] || site.config['lang'] || I18n.locale).to_sym
|
||||
end
|
||||
end
|
||||
|
||||
# No necesitamos los archivos de la plantilla
|
||||
|
|
|
@ -123,6 +123,10 @@ en:
|
|||
title: Distributed Web
|
||||
success: Success!
|
||||
error: Error
|
||||
deploy_social_distributed_press:
|
||||
title: Fediverse
|
||||
success: Success!
|
||||
error: Error
|
||||
deploy_reindex:
|
||||
title: Reindex
|
||||
success: Success!
|
||||
|
@ -192,9 +196,14 @@ en:
|
|||
deploys:
|
||||
deploy_local_presence: 'We need to be build the site!'
|
||||
design_id:
|
||||
missing_gem: "Site is configured to use %{theme} theme, but the corresponding gem is missing from Gemfile"
|
||||
layout_incompatible:
|
||||
error: "Design can't be changed because there are posts with incompatible layouts"
|
||||
help: "Your site has posts with layouts only compatible with the current design. If you change it, the site won't work as you expect. If you're trying out designs, you can delete posts in the following incompatible layouts:: %{layouts}."
|
||||
usuarie:
|
||||
attributes:
|
||||
lang:
|
||||
not_available: "This language is not yet available, would you help us by translating Sutty into it?"
|
||||
errors:
|
||||
argument_error: 'Argument `%{argument}` must be an instance of %{class}'
|
||||
unknown_locale: 'Unknown %{locale} locale'
|
||||
|
@ -308,6 +317,14 @@ en:
|
|||
indefinitely.
|
||||
|
||||
[Learn more](https://sutty.nl/learn-more-about-publish-to-dweb-functionality/)
|
||||
deploy_social_distributed_press:
|
||||
title: 'Publish on the Fediverse'
|
||||
help: |
|
||||
By using the ActivityPub protocol, people on the Fediverse
|
||||
([Mastodon](https://joinmastodon.org/servers),
|
||||
[Pixelfed](https://pixelfed.social/site/about), and
|
||||
[others](https://fediverse.party/)) can follow your site,
|
||||
receive news and interact with them.
|
||||
stats:
|
||||
index:
|
||||
title: Statistics
|
||||
|
@ -408,6 +425,8 @@ en:
|
|||
title: 'Edit %{site}'
|
||||
submit: 'Save changes'
|
||||
btn: 'Configuration'
|
||||
update:
|
||||
post: "Your changes have been saved. **If you enabled a publication method, don't forget to publish changes.**"
|
||||
form:
|
||||
errors:
|
||||
title: There were errors and we couldn't save your changes :(
|
||||
|
@ -416,7 +435,7 @@ en:
|
|||
name: "This will be the host name for your site, ie. **example**.sutty.nl. Choose an expression up to 63 characters. It can contain only lowercase letters, numbers and dashes, **and no spaces**. It can't start or end with a dash, or be entirely composed of numbers."
|
||||
title: 'The title can be anything you want'
|
||||
description: 'You site description that appears in search engines. Between 50 and 160 characters.'
|
||||
design: 'Select the design for your site. You can change it later. We add more designs from time to time!'
|
||||
design: 'Select the design for your site. We add more designs from time to time!'
|
||||
licencia: 'Everything we publish has automatic copyright. This
|
||||
means nobody can use our works without explicit permission. By
|
||||
using licenses, we stablish conditions by which we want to share
|
||||
|
@ -467,6 +486,9 @@ en:
|
|||
success: 'Site upgrade has been completed. Your next build will run this upgrade :)'
|
||||
error: "There was an error when trying to upgrade your site. This could be due to conflicts that couldn't be solved automatically. A report of the issue has already been sent to our admins. Sorry for the inconvenience! :("
|
||||
message: 'Skeleton upgrade'
|
||||
webhooks:
|
||||
pull:
|
||||
message: 'Webhooks pull'
|
||||
footer:
|
||||
powered_by: 'is developed by'
|
||||
i18n:
|
||||
|
@ -513,6 +535,8 @@ en:
|
|||
feedback: 'This field cannot be empty!'
|
||||
uuid:
|
||||
label: 'Unique identifier'
|
||||
created_at:
|
||||
label: 'Created at'
|
||||
geo:
|
||||
uri: 'Open in app'
|
||||
osm: 'Open in web map'
|
||||
|
@ -686,7 +710,7 @@ en:
|
|||
new: 'Create'
|
||||
edit: 'Configure'
|
||||
posts:
|
||||
new: 'New %{layout}'
|
||||
new: 'Add %{layout}'
|
||||
edit: 'Editing'
|
||||
usuaries:
|
||||
index: 'Users'
|
||||
|
|
|
@ -123,6 +123,10 @@ es:
|
|||
title: Web distribuida
|
||||
success: ¡Éxito!
|
||||
error: Hubo un error
|
||||
deploy_social_distributed_press:
|
||||
title: Fediverso
|
||||
success: ¡Éxito!
|
||||
error: Hubo un error
|
||||
deploy_reindex:
|
||||
title: Reindexación
|
||||
success: ¡Éxito!
|
||||
|
@ -192,9 +196,14 @@ es:
|
|||
deploys:
|
||||
deploy_local_presence: '¡Necesitamos poder generar el sitio!'
|
||||
design_id:
|
||||
missing_gem: "El sitio usa la plantilla %{theme} pero la gema correspondiente no se encuentra en el Gemfile"
|
||||
layout_incompatible:
|
||||
error: 'No se puede cambiar la plantilla porque hay artículos con formatos incompatibles'
|
||||
help: 'En tu sitio hay artículos que solo son compatibles con el diseño actual, si cambias la plantilla el sitio no funcionará como esperas. Si estás probando plantillas, puedes eliminar los artículos en los formatos incompatibles: %{layouts}.'
|
||||
usuarie:
|
||||
attributes:
|
||||
lang:
|
||||
not_available: "Este idioma todavía no está disponible, ¿nos ayudas a agregarlo y mantenerlo?"
|
||||
errors:
|
||||
argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}'
|
||||
unknown_locale: 'El idioma %{locale} es desconocido'
|
||||
|
@ -313,6 +322,14 @@ es:
|
|||
copias de tu contenido indefinidamente.
|
||||
|
||||
[Saber más](https://sutty.nl/saber-mas-sobre-publicar-a-la-web-distribuida/)
|
||||
deploy_social_distributed_press:
|
||||
title: 'Publicar al Fediverso'
|
||||
help: |
|
||||
Utilizando el protocolo ActivityPub, otras personas en el
|
||||
Fediverso ([Mastodon](https://joinmastodon.org/servers),
|
||||
[Pixelfed](https://pixelfed.social/site/about) y
|
||||
[otros](https://fediverse.party/)) pueden seguir a tu sitio,
|
||||
recibir novedades e interactuar con ellas.
|
||||
stats:
|
||||
index:
|
||||
title: Estadísticas
|
||||
|
@ -414,6 +431,8 @@ es:
|
|||
title: 'Editar %{site}'
|
||||
submit: 'Guardar cambios'
|
||||
btn: 'Configuración'
|
||||
update:
|
||||
post: "Tus cambios han sido guardados. **Si agregaste un método de publicación, no te olvides de publicar cambios.**"
|
||||
form:
|
||||
errors:
|
||||
title: Hubo errores y no pudimos guardar tus cambios :(
|
||||
|
@ -422,7 +441,7 @@ es:
|
|||
name: 'El nombre de tu sitio que formará parte de la dirección (**ejemplo**.sutty.nl). Solo puede contener hasta 63 letras minúsculas, números y guiones, pero **sin espacios**. No puede empezar ni terminar con guión, ni estar compuesto enteramente por números.'
|
||||
title: 'El título de tu sitio puede ser lo que quieras.'
|
||||
description: 'La descripción del sitio, que saldrá en buscadores. Entre 50 y 160 caracteres.'
|
||||
design: 'Elegí el diseño que va a tener tu sitio aquí. Podés cambiarlo luego. De tanto en tanto vamos sumando diseños nuevos.'
|
||||
design: 'Elegí el diseño que va a tener tu sitio aquí. De tanto en tanto vamos sumando diseños nuevos.'
|
||||
licencia: 'Todo lo que publicamos posee automáticamente derechos
|
||||
de autore. Esto significa que nadie puede hacer uso de nuestras
|
||||
obras sin permiso explícito. Con las licencias establecemos
|
||||
|
@ -475,6 +494,9 @@ es:
|
|||
success: 'Ya se incorporaron los cambios en el sitio, se aplicarán en la próxima compilación que hagas :)'
|
||||
error: 'Hubo un error al incorporar los cambios en el sitio. Esto puede deberse a conflictos entre cambios que no se pueden resolver automáticamente. Hemos enviado un reporte del problema a les administradores de Sutty para que estén al tanto de la situación. ¡Lo sentimos! :('
|
||||
message: 'Actualización del esqueleto'
|
||||
webhooks:
|
||||
pull:
|
||||
message: 'Traer los cambios a partir de un evento remoto'
|
||||
footer:
|
||||
powered_by: 'es desarrollada por'
|
||||
i18n:
|
||||
|
@ -521,6 +543,8 @@ es:
|
|||
feedback: '¡Este campo no puede estar vacío!'
|
||||
uuid:
|
||||
label: 'Identificador único'
|
||||
created_at:
|
||||
label: 'Fecha de creación'
|
||||
geo:
|
||||
uri: 'Abrir en aplicación'
|
||||
osm: 'Abrir en mapa web'
|
||||
|
@ -694,7 +718,7 @@ es:
|
|||
new: 'Crear'
|
||||
edit: 'Configurar'
|
||||
posts:
|
||||
new: 'Nuevo %{layout}'
|
||||
new: 'Agregar %{layout}'
|
||||
edit: 'Editando'
|
||||
usuaries:
|
||||
index: 'Usuaries'
|
||||
|
|
|
@ -17,6 +17,8 @@ Rails.application.routes.draw do
|
|||
|
||||
get :'contact/cookie', to: 'invitades#contact_cookie'
|
||||
post :'contact/:form', to: 'contact#receive', as: :contact
|
||||
|
||||
post :'webhooks/pull', to: 'webhooks#pull'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
8
db/migrate/20230519143500_add_pagination_to_site.rb
Normal file
8
db/migrate/20230519143500_add_pagination_to_site.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Agrega la opción de paginación a los sitios
|
||||
class AddPaginationToSite < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :sites, :pagination, :boolean, default: false
|
||||
end
|
||||
end
|
12
db/migrate/20230731195050_add_token_to_roles.rb
Normal file
12
db/migrate/20230731195050_add_token_to_roles.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class AddTokenToRoles < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
add_column :roles, :token, :string
|
||||
Rol.find_each do |m|
|
||||
m.update_column( :token, SecureRandom.hex(64) )
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :roles, :token
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Almacena las llaves privadas de cada sitio
|
||||
class AddPrivateKeyPemCiphertextToSites < ActiveRecord::Migration[6.1]
|
||||
# Agrega la columna cifrada
|
||||
def change
|
||||
add_column :sites, :private_key_pem_ciphertext, :text
|
||||
end
|
||||
end
|
20
db/migrate/20230921155401_site_rename_origin_to_upstream.rb
Normal file
20
db/migrate/20230921155401_site_rename_origin_to_upstream.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Renombrar todos los repositorios que apunten a skel como su origin
|
||||
class SiteRenameOriginToUpstream < ActiveRecord::Migration[6.1]
|
||||
# Renombrar
|
||||
def up
|
||||
Site.find_each do |site|
|
||||
next unless site.repository.origin&.url == ENV['SKEL_SUTTY']
|
||||
|
||||
site.repository.rugged.remotes.rename('origin', 'upstream') do |_|
|
||||
Rails.logger.info "#{site.name}: renamed origin to upstream"
|
||||
end
|
||||
rescue Rugged::Error, Rugged::OSError => e
|
||||
Rails.logger.warn "#{site.name}: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
# No se puede deshacer
|
||||
def down; end
|
||||
end
|
|
@ -18,7 +18,7 @@
|
|||
- name_en: 'Minima'
|
||||
name_es: 'Mínima'
|
||||
gem: 'sutty-minima'
|
||||
url: 'https://0xacab.org/sutty/jekyll/minima'
|
||||
url: 'https://minima.sutty.nl/'
|
||||
description_en: "Sutty Minima is based on [Minima](https://jekyll.github.io/minima/), a blog-focused theme for Jekyll."
|
||||
description_es: 'Sutty Mínima es una plantilla para blogs basada en [Mínima](https://jekyll.github.io/minima/).'
|
||||
license: 'https://0xacab.org/sutty/jekyll/minima/-/blob/master/LICENSE.txt'
|
||||
|
|
Loading…
Reference in a new issue