# frozen_string_literal: true # Un sitio es un directorio dentro del directorio de sitios de cada # Usuaria class Site < ApplicationRecord include FriendlyId friendly_id :name, use: %i[finders] has_many :roles has_and_belongs_to_many :usuaries, class_name: 'Usuarie' has_and_belongs_to_many :invitades, class_name: 'Usuarie', join_table: 'invitades_sites' # Carga el sitio Jekyll una vez que se inicializa el modelo after_initialize :load_jekyll! attr_accessor :jekyll, :collections def invitade?(usuarie) invitades.pluck(:id).include? usuarie.id end def usuarie?(usuarie) usuaries.pluck(:id).include? usuarie.id end # Traer la ruta del sitio # # Equivale a _sites + nombre def path @path ||= File.join(Site.site_path, name) end # Este sitio acepta invitadxs? def invitadxs? jekyll.config.fetch('invitadxs', false) end def cover "/covers/#{name}.png" end # Determina si el sitio está en varios idiomas def i18n? !translations.empty? end # Define si el sitio tiene un glosario def glossary? jekyll.config.fetch('glossary', false) end # Obtiene la lista de traducciones actuales def translations @jekyll.config.dig('i18n') || [] end # Devuelve el idioma por defecto del sitio # # TODO: volver elegante def default_lang # Si está traducido, intentamos saber si podemos trabajar en el # idioma actual de la plataforma. if i18n? i18n = I18n.locale.to_s if translations.include? i18n # Podemos trabajar en el idioma actual i18n else # Sino, trabajamos con el primer idioma translations.first end else # Si el sitio no está traducido, estamos trabajando con posts en # cualquier idioma # # XXX: no será un dirty hack? 'posts' end end def layouts @layouts ||= @jekyll.layouts.keys.sort end def name_with_i18n(lang) [name, lang].join('/') end def read @jekyll.read end # Fuerza relectura del sitio, eliminando el sitio actual en favor de # un sitio nuevo, leído desde cero def read! @jekyll = Site.load_jekyll(@jekyll.path) @jekyll.read end def data if @jekyll.data.empty? read Rails.logger.info 'Leyendo data' end @jekyll.data end def config if @jekyll.config.empty? read Rails.logger.info 'Leyendo config' end @jekyll.config end def collections_names @jekyll.config['collections'].keys end # Los posts de este sitio, si el sitio está traducido, trae los posts # del idioma actual, porque _posts va a estar vacío def posts @posts ||= posts_for('posts') end # Obtiene todas las plantillas de artículos def templates @templates ||= posts_for('templates') || [] end # Obtiene todos los posts de una colección determinada # # Devuelve nil si la colección no existe def posts_for(collection) return unless collections_names.include? collection # Si pedimos 'posts' pero estamos en un sitio traducido, traemos el # idioma actual collection = default_lang if collection == 'posts' && i18n? @collections ||= {} c = @collections[collection] return c if c Rails.logger.info "Procesando #{collection}" col = @jekyll.collections[collection].docs if col.empty? @jekyll.read # Queremos saber cuantas veces releemos los articulos Rails.logger.info 'Leyendo articulos' end # Los convertimos a una clase intermedia capaz de acceder a sus # datos y modificarlos @collections[collection] = col.map do |post| Post.new(site: self, post: post, lang: collection) end end def categories(lang: nil) everything_of :categories, lang: lang end def tags(lang: nil) everything_of :tags, lang: lang end def everything_of(attr, lang: nil) collection = lang || 'posts' return [] unless collections_names.include? collection posts_for(collection).map do |p| p.get_front_matter attr end.flatten.uniq.compact end def failed_file File.join(path, '.failed') end def failed? File.exist? failed_file end def defail FileUtils.rm failed_file if failed? end alias defail! defail def build_log File.join(path, 'build.log') end def build_log? File.exist? build_log end def queue_file File.join(path, '.generate') end def enqueued? File.exist? queue_file end alias queued? enqueued? # El sitio se genera cuando se coloca en una cola de generación, para # que luego lo construya un cronjob def enqueue defail! # TODO: ya van tres métodos donde usamos esta idea, convertir en un # helper o algo File.open(queue_file, File::RDWR | File::CREAT, 0o640) do |f| # Bloquear el archivo para que no sea accedido por otro # proceso u otra editora f.flock(File::LOCK_EX) # Empezar por el principio f.rewind # Escribir la fecha de creación f.write(Time.now.to_i.to_s) # Eliminar el resto f.flush f.truncate(f.pos) end end alias enqueue! enqueue # Eliminar de la cola def dequeue FileUtils.rm(queue_file) if enqueued? end alias dequeue! dequeue # Verifica si los posts están ordenados def ordered?(collection = 'posts') posts_for(collection).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 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 def get_url_for_sutty(path) # Remover los puntos para que no nos envíen a ../../ File.join('/', 'sites', id, path.gsub('..', '')) end def get_url_from_site(path) "https://#{name}#{path}" end # El directorio donde se almacenan los sitios def self.site_path File.join(Rails.root, '_sites') end # El directorio de los sitios de una usuaria # # Los sitios se organizan por usuaria, entonces los sitios que # administra pueden encontrarse directamente en su directorio. # # Si comparten gestión con otras usuarias, se hacen links simbólicos # entre sí. def self.site_path_for(site) File.join(Site.site_path, site) end # Comprueba que el directorio parezca ser de jekyll def self.jekyll?(dir) File.directory?(dir) && File.exist?(File.join(dir, '_config.yml')) end def self.load_jekyll(path) # Pasamos destination porque configuration() toma el directorio # actual y se mezclan :/ # # Especificamos `safe` para no cargar los _plugins, que interfieren # entre sitios incluso config = ::Jekyll.configuration('source' => path, 'destination' => File.join(path, '_site'), 'safe' => true, 'watch' => false, 'quiet' => true) # No necesitamos cargar plugins en este momento %w[plugins gems theme].each do |unneeded| config[unneeded] = [] if config.key? unneeded end # Si estamos usando nuestro propio plugin de i18n, los posts están # en "colecciones" i18n = config.dig('i18n') i18n&.each do |i| config['collections'][i] = {} end Jekyll::Site.new(config) end private # Carga el sitio Jekyll def load_jekyll! Dir.chdir(path) do @jekyll ||= Site.load_jekyll(Dir.pwd) end end end