# Un sitio es un directorio dentro del directorio de sitios de cada # Usuaria class Site attr_accessor :jekyll, :collections attr_reader :path def initialize(jekyll:, path: nil) @jekyll = jekyll @path = path || @jekyll.config['source'] @collections = {} end # Determina si el sitio está en varios idiomas def i18n? !translations.empty? 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 # Obtener el nombre del sitio def name @name ||= @jekyll.config['source'].split('/').last end def name_with_i18n(lang) [name, lang].join('/') end # El id es el sitio sin puntos, para no confundir al routeador de # rails def id @id ||= name.tr('.', '-') end alias :to_s :id 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 @collections 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 @jekyll.config['collections'].key? collection # Si pedimos 'posts' pero estamos en un sitio traducido, traemos el # idioma actual collection = I18n.locale.to_s if collection == 'posts' && i18n? _collection = @collections[collection] return _collection if _collection 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' posts_for(collection).map do |p| p.get_front_matter attr end.flatten.uniq end # Las usuarias que tienen acceso a este sitio se guardan en un archivo # `.usuarias` que tiene la dirección de correo de cada una def usuarias_file File.join(path, '.usuarias') end # Obtiene las usuarias que gestionan este sitio def usuarias @usuarias ||= File.read(usuarias_file).split("\n").map do |u| Usuaria.find(u) end 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 r = 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 do |p| p.order end.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 = 'posts', 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! # 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(usuaria) File.join(Site.site_path, usuaria.username) 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) # No necesitamos cargar plugins en este momento %w[plugins gems].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') if i18n i18n.each do |i| config['collections'][i] = {} end end Jekyll::Site.new(config) end # Obtener todos los directorios de sitios asociados a esta usuaria def self.all_for(usuaria) @sites ||= Pathname.new(Site.site_path_for(usuaria)) .children.map(&:expand_path).map(&:to_s).map do |j| next unless Site.jekyll? j Dir.chdir(j) do jekyll = Site.load_jekyll(Dir.pwd) Site.new(jekyll: jekyll, path: j) end end.compact end end