mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 16:26:21 +00:00
354 lines
9.3 KiB
Ruby
354 lines
9.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Un sitio es un directorio dentro del directorio de sitios de cada
|
|
# Usuaria
|
|
class Site < ApplicationRecord
|
|
include FriendlyId
|
|
|
|
validates :name, uniqueness: true, hostname: true
|
|
validates :design_id, presence: true
|
|
validate :deploy_local_presence
|
|
validates_inclusion_of :status, in: %w[waiting enqueued building]
|
|
validates_presence_of :title
|
|
validates :description, length: { in: 50..160 }
|
|
|
|
friendly_id :name, use: %i[finders]
|
|
|
|
belongs_to :design
|
|
belongs_to :licencia
|
|
|
|
has_many :deploys
|
|
has_many :build_stats, through: :deploys
|
|
has_many :roles
|
|
has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') },
|
|
through: :roles
|
|
has_many :invitades, -> { where('roles.rol = ?', 'invitade') },
|
|
through: :roles, source: :usuarie
|
|
|
|
# Mantenemos el nombre que les da Jekyll
|
|
has_many_attached :static_files
|
|
|
|
# Clonar el directorio de esqueleto antes de crear el sitio
|
|
before_create :clone_skel!
|
|
# Elimina el directorio al destruir un sitio
|
|
before_destroy :remove_directories!
|
|
# Carga el sitio Jekyll una vez que se inicializa el modelo o después
|
|
# de crearlo
|
|
after_initialize :load_jekyll
|
|
after_create :load_jekyll
|
|
# Cambiar el nombre del directorio
|
|
before_update :update_name!
|
|
# Guardar la configuración si hubo cambios
|
|
after_save :sync_attributes_with_config!
|
|
|
|
accepts_nested_attributes_for :deploys, allow_destroy: true
|
|
|
|
# El sitio en Jekyll
|
|
attr_accessor :jekyll
|
|
|
|
# No permitir HTML en estos atributos
|
|
def title=(title)
|
|
super(title.strip_tags)
|
|
end
|
|
|
|
def description=(description)
|
|
super(description.strip_tags)
|
|
end
|
|
|
|
# El repositorio git para este sitio
|
|
def repository
|
|
@repository ||= Site::Repository.new path
|
|
end
|
|
|
|
def hostname
|
|
if name.ends_with? '.'
|
|
name.gsub(/\.\Z/, '')
|
|
else
|
|
"#{name}.#{Site.domain}"
|
|
end
|
|
end
|
|
|
|
def url
|
|
"https://#{hostname}/"
|
|
end
|
|
|
|
# TODO: Mover esta consulta a la base de datos para no traer un montón
|
|
# de cosas a la memoria
|
|
def invitade?(usuarie)
|
|
invitades.pluck(:id).include? usuarie.id
|
|
end
|
|
|
|
def usuarie?(usuarie)
|
|
usuaries.pluck(:id).include? usuarie.id
|
|
end
|
|
|
|
# Este sitio acepta invitades?
|
|
def invitades?
|
|
config.fetch('invitades', false)
|
|
end
|
|
|
|
# Traer la ruta del sitio
|
|
def path
|
|
File.join(Site.site_path, name)
|
|
end
|
|
|
|
# La ruta anterior
|
|
def path_was
|
|
File.join(Site.site_path, name_was)
|
|
end
|
|
|
|
def cover
|
|
"/covers/#{name}.png"
|
|
end
|
|
|
|
# Define si el sitio tiene un glosario
|
|
def glossary?
|
|
config.fetch('glossary', false)
|
|
end
|
|
|
|
# Obtiene la lista de traducciones actuales
|
|
#
|
|
# Siempre tiene que tener algo porque las traducciones están
|
|
# incorporadas a los sitios de Sutty, aunque les usuaries no traduzcan
|
|
# sus sitios.
|
|
def translations
|
|
config.fetch('translations', I18n.available_locales.map(&:to_s))
|
|
end
|
|
|
|
# Devuelve el idioma por defecto del sitio, el primero de la lista.
|
|
def default_language
|
|
translations.first
|
|
end
|
|
alias default_lang default_language
|
|
|
|
# Lee el sitio y todos los artículos
|
|
def read
|
|
# No hacer nada si ya se leyó antes
|
|
return unless @jekyll.layouts.empty?
|
|
|
|
@jekyll.read
|
|
end
|
|
|
|
# Trae los datos del directorio _data dentro del sitio
|
|
#
|
|
# XXX: Leer directamente sin pasar por Jekyll
|
|
def data
|
|
read
|
|
|
|
@jekyll.data
|
|
end
|
|
|
|
# Traer las colecciones. Todos los artículos van a estar dentro de
|
|
# colecciones.
|
|
def collections
|
|
read
|
|
|
|
@jekyll.collections
|
|
end
|
|
|
|
# Traer la configuración de forma modificable
|
|
def config
|
|
@config ||= Site::Config.new(self)
|
|
end
|
|
|
|
# Los posts en el idioma actual o en uno en particular
|
|
#
|
|
# @param lang: [String|Symbol] traer los artículos de este idioma
|
|
def posts(lang: nil)
|
|
@posts ||= {}
|
|
lang ||= I18n.locale
|
|
|
|
return @posts[lang] if @posts.key? lang
|
|
|
|
@posts[lang] = PostRelation.new site: self
|
|
|
|
# No fallar si no existe colección para este idioma
|
|
# XXX: queremos fallar silenciosamente?
|
|
(collections[lang.to_s].try(:docs) || []).each do |doc|
|
|
layout = layouts[doc.data['layout'].to_sym]
|
|
|
|
@posts[lang].build(document: doc, layout: layout, lang: lang)
|
|
end
|
|
|
|
@posts[lang]
|
|
end
|
|
|
|
# Obtiene todas las plantillas de artículos
|
|
#
|
|
# @return { post: Layout }
|
|
def layouts
|
|
@layouts ||= data.fetch('layouts', {}).map do |name, metadata|
|
|
{ name.to_sym => Layout.new(site: self,
|
|
name: name.to_sym,
|
|
metadata: metadata.with_indifferent_access) }
|
|
end.inject(:merge)
|
|
end
|
|
|
|
# Trae todos los valores disponibles para un campo
|
|
#
|
|
# TODO: Traer recursivamente, si el campo contiene Hash
|
|
#
|
|
# @return Array
|
|
def everything_of(attr, lang: nil)
|
|
posts(lang: lang).map do |p|
|
|
# XXX: Tener cuidado con los métodos que no existan
|
|
p.send(attr).try :value
|
|
end.flatten.uniq.compact
|
|
end
|
|
|
|
# Poner en la cola de compilación
|
|
def enqueue!
|
|
!enqueued? && update_attribute(:status, 'enqueued')
|
|
end
|
|
|
|
# Está en la cola de compilación?
|
|
def enqueued?
|
|
status == 'enqueued'
|
|
end
|
|
|
|
# Verifica si los posts están ordenados
|
|
def ordered?(lang: nil)
|
|
posts(lang: lang).map(&:order).all?
|
|
end
|
|
|
|
# Reordena la colección usando la posición informada
|
|
#
|
|
# new_order es un hash cuya key es la posición actual del post y el
|
|
# valor la posición nueva
|
|
#
|
|
# TODO: Refactorizar y testear
|
|
def reorder_collection(collection, new_order)
|
|
# Tenemos que pasar el mismo orden
|
|
return if new_order.values.map(&:to_i).sort != new_order.keys.map(&:to_i).sort
|
|
|
|
# Solo traer los posts que vamos a modificar
|
|
posts_to_order = posts_for(collection).values_at(*new_order.keys.map(&:to_i))
|
|
|
|
# Recorre todos los posts y asigna el nuevo orden
|
|
posts_to_order.each_with_index do |p, i|
|
|
# Usar el index si el artículo no estaba ordenado, para tener una
|
|
# ruta de adopción
|
|
oo = (p.order || i).to_s
|
|
no = new_order[oo].to_i
|
|
# No modificar nada si no hace falta
|
|
next if p.order == no
|
|
|
|
p.update_attributes order: no
|
|
p.save
|
|
end
|
|
|
|
posts_to_order.map(&:ordered?).all?
|
|
end
|
|
|
|
# Reordena la colección usando la posición actual de los artículos
|
|
def reorder_collection!(collection = 'posts')
|
|
order = Hash[posts_for(collection).count.times.map { |i| [i.to_s, i.to_s] }]
|
|
reorder_collection collection, order
|
|
end
|
|
alias reorder_posts! reorder_collection!
|
|
|
|
# Obtener una ruta disponible para Sutty
|
|
#
|
|
# TODO: Refactorizar y testear
|
|
def get_url_for_sutty(path)
|
|
# Remover los puntos para que no nos envíen a ../../
|
|
File.join('/', 'sites', id, path.gsub('..', ''))
|
|
end
|
|
|
|
# Cargar el sitio Jekyll
|
|
#
|
|
# TODO: En lugar de leer todo junto de una vez, extraer la carga de
|
|
# documentos de Jekyll hacia Sutty para que podamos leer los datos que
|
|
# necesitamos.
|
|
def load_jekyll
|
|
return unless name.present? && File.directory?(path)
|
|
|
|
Dir.chdir(path) do
|
|
@jekyll = Jekyll::Site.new(jekyll_config)
|
|
end
|
|
end
|
|
|
|
def jekyll_config
|
|
# Pasamos destination porque configuration() toma el directorio
|
|
# actual
|
|
#
|
|
# Especificamos `safe` para no cargar los _plugins, que interfieren
|
|
# entre sitios incluso.
|
|
#
|
|
# excerpt_separator está vacío para no incorporar el Excerpt en los
|
|
# metadatos de Document
|
|
configuration =
|
|
::Jekyll.configuration('source' => path,
|
|
'destination' => File.join(path, '_site'),
|
|
'safe' => true, 'watch' => false,
|
|
'quiet' => true, 'excerpt_separator' => '')
|
|
|
|
# No necesitamos cargar plugins en este momento
|
|
%w[plugins gems theme].each do |unneeded|
|
|
configuration[unneeded] = [] if configuration.key? unneeded
|
|
end
|
|
|
|
# Si estamos usando nuestro propio plugin de i18n, los posts están
|
|
# en "colecciones"
|
|
translations.each do |i|
|
|
configuration['collections'][i] = {}
|
|
end
|
|
|
|
configuration
|
|
end
|
|
|
|
# Devuelve el dominio actual
|
|
def self.domain
|
|
ENV.fetch('SUTTY', 'sutty.nl')
|
|
end
|
|
|
|
# El directorio donde se almacenan los sitios
|
|
def self.site_path
|
|
File.join(Rails.root, '_sites')
|
|
end
|
|
|
|
private
|
|
|
|
# Clona el esqueleto de Sutty para crear el sitio nuevo, no pasa nada
|
|
# si el sitio ya existe
|
|
def clone_skel!
|
|
return if File.directory? path
|
|
|
|
Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path
|
|
end
|
|
|
|
# Elimina el directorio del sitio
|
|
def remove_directories!
|
|
FileUtils.rm_rf path
|
|
end
|
|
|
|
def update_name!
|
|
return unless name_changed?
|
|
|
|
FileUtils.mv path_was, path
|
|
end
|
|
|
|
# Sincroniza algunos atributos del sitio con su configuración y
|
|
# guarda los cambios
|
|
#
|
|
# TODO: Guardar la configuración también, quizás aprovechando algún
|
|
# método de ActiveRecord para que lance un salvado recursivo.
|
|
def sync_attributes_with_config!
|
|
config.theme = design.gem
|
|
config.description = description
|
|
config.title = title
|
|
end
|
|
|
|
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
|
# y es la local
|
|
#
|
|
# TODO: Volver opcional el alojamiento local, pero ahora mismo está
|
|
# atado a la generación del sitio así que no puede faltar
|
|
def deploy_local_presence
|
|
# Usamos size porque queremos saber la cantidad de deploys sin
|
|
# guardar también
|
|
return if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal')
|
|
|
|
errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence'))
|
|
end
|
|
end
|