# 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 sub = name || I18n.t('deploys.deploy_local.ejemplo') 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 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 locales config.fetch('locales', I18n.available_locales.map(&:to_s)) end # Devuelve el idioma por defecto del sitio, el primero de la lista. def default_locale locales.first end alias default_lang default_locale def read? @read ||= false end # Lee el sitio y todos los artículos def read # No hacer nada si ya se leyó antes return if read? @jekyll.read @read = true 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) read @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) reload_jekyll! end def reload_jekyll! 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' => '') # 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" locales.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 reload_jekyll! 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