sutty/app/models/site.rb

393 lines
10 KiB
Ruby
Raw Normal View History

2019-03-26 15:32:20 +00:00
# frozen_string_literal: true
2018-01-29 22:19:10 +00:00
# Un sitio es un directorio dentro del directorio de sitios de cada
# Usuaria
2019-07-03 22:04:50 +00:00
class Site < ApplicationRecord
2019-07-03 23:40:24 +00:00
include FriendlyId
2019-10-01 19:41:33 +00:00
# TODO: Hacer que los diferentes tipos de deploy se auto registren
# @see app/services/site_service.rb
DEPLOYS = %i[local www zip].freeze
2019-11-18 17:08:30 +00:00
validates :name, uniqueness: true, hostname: {
allow_root_label: true
}
2019-07-17 22:18:48 +00:00
validates :design_id, presence: true
validate :deploy_local_presence
2019-07-26 01:11:01 +00:00
validates_inclusion_of :status, in: %w[waiting enqueued building]
2019-07-31 20:55:34 +00:00
validates_presence_of :title
validates :description, length: { in: 50..160 }
2019-07-03 23:40:24 +00:00
friendly_id :name, use: %i[finders]
2019-07-17 22:18:48 +00:00
belongs_to :design
2019-07-19 22:37:53 +00:00
belongs_to :licencia
2019-07-17 22:18:48 +00:00
has_many :deploys
2019-08-02 00:20:42 +00:00
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
2019-07-03 22:04:50 +00:00
2019-08-22 01:09:29 +00:00
# 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!
2019-07-12 18:34:16 +00:00
# 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
2019-11-14 17:27:24 +00:00
after_create :load_jekyll, :static_file_migration!
2019-07-13 00:20:36 +00:00
# Cambiar el nombre del directorio
before_update :update_name!
2019-07-31 20:55:34 +00:00
# Guardar la configuración si hubo cambios
after_save :sync_attributes_with_config!
2019-07-03 23:25:23 +00:00
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
return @hostname unless name_changed? || @hostname.blank?
2020-03-23 20:46:19 +00:00
sub = name || I18n.t('deploys.deploy_local.ejemplo')
2020-03-23 20:46:19 +00:00
@hostname = if sub.ends_with? '.'
sub.gsub(/\.\Z/, '')
else
"#{sub}.#{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
2019-07-03 23:25:23 +00:00
end
# Este sitio acepta invitades?
def invitades?
config.fetch('invitades', false)
end
2019-07-03 23:25:23 +00:00
# Traer la ruta del sitio
def path
2019-07-13 00:20:36 +00:00
File.join(Site.site_path, name)
end
# La ruta anterior
def path_was
2019-07-13 00:20:36 +00:00
File.join(Site.site_path, name_was)
2019-07-03 23:25:23 +00:00
end
2018-01-29 22:19:10 +00:00
2018-09-28 14:34:37 +00:00
def cover
"/covers/#{name}.png"
end
2018-09-05 20:25:47 +00:00
# Define si el sitio tiene un glosario
def glossary?
config.fetch('glossary', false)
2018-09-05 20:25:47 +00:00
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.
2019-09-20 13:19:04 +00:00
def locales
config.fetch('locales', I18n.available_locales.map(&:to_s))
2018-05-11 16:24:30 +00:00
end
# Devuelve el idioma por defecto del sitio, el primero de la lista.
2019-09-20 13:19:04 +00:00
def default_locale
locales.first
2018-02-22 19:01:11 +00:00
end
2019-09-20 13:19:04 +00:00
alias default_lang default_locale
2018-02-22 19:01:11 +00:00
def read?
@read ||= false
end
# Lee el sitio y todos los artículos
2018-02-19 19:33:28 +00:00
def read
# No hacer nada si ya se leyó antes
return if read?
2018-02-19 19:33:28 +00:00
@jekyll.read
@read = true
2018-02-19 19:33:28 +00:00
end
# Trae los datos del directorio _data dentro del sitio
#
# XXX: Leer directamente sin pasar por Jekyll
2018-02-09 21:28:27 +00:00
def data
read
2018-02-09 21:28:27 +00:00
@jekyll.data
end
# Traer las colecciones. Todos los artículos van a estar dentro de
# colecciones.
def collections
read
@jekyll.collections
2018-02-22 19:01:11 +00:00
end
2018-01-29 22:19:10 +00:00
# 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
2018-02-22 19:01:11 +00:00
#
# @param lang: [String|Symbol] traer los artículos de este idioma
def posts(lang: nil)
read
2019-03-26 15:32:20 +00:00
# Traemos los posts del idioma actual por defecto
lang ||= I18n.locale
# Crea un Struct dinámico con los valores de los locales, si
# llegamos a pasar un idioma que no existe vamos a tener una
# excepción NoMethodError
@posts ||= Struct.new(*locales.map(&:to_sym)).new
return @posts[lang] unless @posts[lang].blank?
2018-04-27 18:48:26 +00:00
@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[Post.find_layout(doc)]
2018-01-29 22:19:10 +00:00
@posts[lang].build(document: doc, layout: layout, lang: lang)
2018-02-23 19:20:51 +00:00
end
2018-01-29 22:19:10 +00:00
@posts[lang]
end
# Elimina un artículo de la colección
def delete_post(post)
lang = post.lang.value
collections[lang.to_s].docs.delete(post.document) &&
posts(lang: lang).delete(post)
post
end
# Obtiene todas las plantillas de artículos
#
# @return { post: Layout }
def layouts
# Crea un Struct dinámico cuyas llaves son los nombres de todos los
# layouts. Si pasamos un layout que no existe, obtenemos un
# NoMethodError
@layouts_struct ||= Struct.new(*data.fetch('layouts', {}).keys.map(&:to_sym), keyword_init: true)
@layouts ||= @layouts_struct.new(**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))
2018-02-08 14:05:05 +00:00
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|
2019-10-31 20:34:23 +00:00
p.send(attr).try(:value) if p.attribute? attr
2018-09-27 18:17:34 +00:00
end.flatten.uniq.compact
end
# Poner en la cola de compilación
2019-07-26 01:11:01 +00:00
def enqueue!
!enqueued? && update_attribute(:status, 'enqueued')
2018-02-20 17:47:11 +00:00
end
# Está en la cola de compilación?
2018-02-20 17:47:11 +00:00
def enqueued?
2019-07-26 01:11:01 +00:00
status == 'enqueued'
2018-02-20 17:47:11 +00:00
end
2018-07-02 22:07:06 +00:00
# Obtener una ruta disponible para Sutty
#
# TODO: Refactorizar y testear
2018-07-02 22:07:06 +00:00
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)
reload_jekyll!
end
def reload_jekyll!
reset
Dir.chdir(path) do
@jekyll = Jekyll::Site.new(jekyll_config)
end
end
def reload
super
reload_jekyll!
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' => '')
2018-02-19 19:33:28 +00:00
# No necesitamos cargar plugins en este momento
%w[plugins gems].each do |unneeded|
configuration[unneeded] = [] if configuration.key? unneeded
2018-02-19 19:33:28 +00:00
end
# Eliminar el theme si no es una gema válida
configuration.delete 'theme' unless theme_available?
2018-02-22 19:01:11 +00:00
# Si estamos usando nuestro propio plugin de i18n, los posts están
# en "colecciones"
2019-09-20 13:19:04 +00:00
locales.each do |i|
configuration['collections'][i] = {}
2018-02-22 19:01:11 +00:00
end
configuration
2018-02-19 19:33:28 +00:00
end
# Lista los nombres de las plantillas disponibles como gemas,
# tomándolas dinámicamente de las que agreguemos en el grupo :themes
# del Gemfile.
def available_themes
@available_themes ||= Bundler.load.current_dependencies.select do |gem|
gem.groups.include? :themes
end.map(&:name)
end
# Detecta si el tema actual es una gema
def theme_available?
available_themes.include? design.gem
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
def reset
@read = false
@layouts = nil
@layouts_struct = nil
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
2019-07-12 19:11:07 +00:00
# Elimina el directorio del sitio
2019-07-12 18:34:16 +00:00
def remove_directories!
FileUtils.rm_rf path
end
2019-07-13 00:20:36 +00:00
def update_name!
return unless name_changed?
FileUtils.mv path_was, path
reload_jekyll!
2019-07-13 00:20:36 +00:00
end
2019-07-31 20:55:34 +00:00
# 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.
2019-07-31 20:55:34 +00:00
def sync_attributes_with_config!
2019-08-09 19:49:25 +00:00
config.theme = design.gem
config.description = description
config.title = title
config.url = url
2020-03-23 20:46:19 +00:00
config.hostname = hostname
2019-07-31 20:55:34 +00:00
end
2019-11-14 17:27:24 +00:00
# Migra los archivos a Sutty
def static_file_migration!
Site::StaticFileMigration.new(site: self).migrate!
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
if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal')
return
end
errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence'))
end
2018-01-29 22:19:10 +00:00
end