Merge branch 'search-engine' into rails

This commit is contained in:
f 2021-06-26 20:33:45 -03:00
commit 411728648a
36 changed files with 788 additions and 175 deletions

View file

@ -53,7 +53,6 @@ gem 'loaf'
gem 'lockbox' gem 'lockbox'
gem 'mini_magick' gem 'mini_magick'
gem 'mobility' gem 'mobility'
gem 'pg'
gem 'pundit' gem 'pundit'
gem 'rails-i18n' gem 'rails-i18n'
gem 'rails_warden' gem 'rails_warden'
@ -69,6 +68,11 @@ gem 'validates_hostname'
gem 'webpacker' gem 'webpacker'
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
# database
gem 'hairtrigger'
gem 'pg'
gem 'pg_search'
# performance # performance
gem 'flamegraph' gem 'flamegraph'
gem 'memory_profiler' gem 'memory_profiler'

View file

@ -205,6 +205,10 @@ GEM
ffi (~> 1.0) ffi (~> 1.0)
globalid (0.4.2) globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
hairtrigger (0.2.24)
activerecord (>= 5.0, < 7)
ruby2ruby (~> 2.4)
ruby_parser (~> 3.10)
haml (5.2.1) haml (5.2.1)
temple (>= 0.8.0) temple (>= 0.8.0)
tilt tilt
@ -366,6 +370,9 @@ GEM
pathutil (0.16.2) pathutil (0.16.2)
forwardable-extended (~> 2.6) forwardable-extended (~> 2.6)
pg (1.2.3-x86_64-linux-musl) pg (1.2.3-x86_64-linux-musl)
pg_search (2.3.5)
activerecord (>= 5.2)
activesupport (>= 5.2)
popper_js (1.16.0) popper_js (1.16.0)
prometheus_exporter (0.7.0) prometheus_exporter (0.7.0)
webrick webrick
@ -498,7 +505,12 @@ GEM
ruby-statistics (2.1.3) ruby-statistics (2.1.3)
ruby-vips (2.1.2) ruby-vips (2.1.2)
ffi (~> 1.12) ffi (~> 1.12)
ruby2ruby (2.4.4)
ruby_parser (~> 3.1)
sexp_processor (~> 4.6)
ruby_dep (1.5.0) ruby_dep (1.5.0)
ruby_parser (3.15.1)
sexp_processor (~> 4.9)
rubyzip (2.3.0) rubyzip (2.3.0)
rugged (1.1.0-x86_64-linux-musl) rugged (1.1.0-x86_64-linux-musl)
safe_yaml (1.0.6) safe_yaml (1.0.6)
@ -516,6 +528,7 @@ GEM
childprocess (>= 0.5, < 4.0) childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2) rubyzip (>= 1.2.2)
semantic_range (3.0.0) semantic_range (3.0.0)
sexp_processor (4.15.2)
share-to-fediverse-jekyll-theme (0.1.4) share-to-fediverse-jekyll-theme (0.1.4)
jekyll (~> 4.0) jekyll (~> 4.0)
jekyll-data (~> 1.1) jekyll-data (~> 1.1)
@ -643,6 +656,7 @@ DEPENDENCIES
fast_jsonparser fast_jsonparser
flamegraph flamegraph
friendly_id friendly_id
hairtrigger
haml-lint haml-lint
hamlit-rails hamlit-rails
hiredis hiredis
@ -667,6 +681,7 @@ DEPENDENCIES
mobility mobility
net-ssh net-ssh
pg pg
pg_search
prometheus_exporter prometheus_exporter
pry pry
puma puma

View file

@ -21,6 +21,10 @@ $form-feedback-invalid-color: $magenta;
$form-feedback-icon-valid-color: $black; $form-feedback-icon-valid-color: $black;
$component-active-bg: $magenta; $component-active-bg: $magenta;
$spacers: (
2-plus: 0.75rem
);
@import "bootstrap"; @import "bootstrap";
@import "editor"; @import "editor";
@ -210,6 +214,10 @@ svg {
} }
} }
.btn-sm {
@extend .badge
}
.black-bg { .black-bg {
color: $white; color: $white;
background-color: $black; background-color: $black;

View file

@ -20,30 +20,22 @@ class PostsController < ApplicationController
def index def index
authorize Post authorize Post
@site = find_site
@category = params.dig(:category)
@layout = params.dig(:layout)
@locale = locale
# XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es # XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es
# más simple saber si hubo cambios. # más simple saber si hubo cambios.
if @category || @layout || stale?([current_usuarie, @site]) if stale?([current_usuarie, site, filter_params])
@posts = @site.posts(lang: locale) # Todos los artículos de este sitio para el idioma actual
@posts = @posts.where(categories: @category) if @category @posts = site.indexed_posts.where(locale: locale)
@posts = @posts.where(layout: @layout) if @layout # De este tipo
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
# Que estén dentro de la categoría
@posts = @posts.in_category(filter_params[:category]) if filter_params[:category]
# Aplicar los parámetros de búsqueda
@posts = @posts.search(locale, filter_params[:q]) if filter_params[:q].present?
# A los que este usuarie tiene acceso
@posts = PostPolicy::Scope.new(current_usuarie, @posts).resolve @posts = PostPolicy::Scope.new(current_usuarie, @posts).resolve
@category_name = if uuid?(@category)
@site.posts(lang: locale).find(@category, uuid: true)&.title&.value
else
@category
end
# Filtrar los posts que les invitades no pueden ver # Filtrar los posts que les invitades no pueden ver
@usuarie = @site.usuarie? current_usuarie @usuarie = site.usuarie? current_usuarie
# Orden descendiente por número y luego por fecha
@posts.sort_by!(:order, :date).reverse!
end end
end end
@ -157,6 +149,14 @@ class PostsController < ApplicationController
private private
# Los parámetros de filtros que vamos a mantener en todas las URLs,
# solo los que no estén vacíos.
#
# @return [Hash]
def filter_params
@filter_params ||= params.permit(:q, :category, :layout).to_h.select { |_, v| v.present? }
end
def site def site
@site ||= find_site @site ||= find_site
end end

View file

@ -107,7 +107,7 @@ class SitesController < ApplicationController
def merge def merge
authorize site authorize site
if site.repository.merge(current_usuarie) if SiteService.new(site: site, usuarie: current_usuarie).merge
flash[:success] = I18n.t('sites.fetch.merge.success') flash[:success] = I18n.t('sites.fetch.merge.success')
else else
flash[:error] = I18n.t('sites.fetch.merge.error') flash[:error] = I18n.t('sites.fetch.merge.error')

View file

@ -0,0 +1,46 @@
# frozen_string_literal: true
# La representación indexable de un artículo
class IndexedPost < ApplicationRecord
include PgSearch::Model
# La traducción del locale según Sutty al locale según PostgreSQL
DICTIONARIES = {
es: 'spanish',
en: 'english'
}.freeze
# TODO: Los indexed posts tienen que estar scopeados al idioma actual,
# no buscar sobre todos
pg_search_scope :search,
lambda { |locale, query|
{
against: :content,
query: query,
using: {
tsearch: {
dictionary: IndexedPost.to_dictionary(locale: locale),
tsvector_column: 'indexed_content'
},
trigram: {
word_similarity: true
}
}
}
}
# Trae los IndexedPost en el orden en que van a terminar en el sitio.
default_scope -> { order(order: :desc, created_at: :desc) }
scope :in_category, ->(category) { where("front_matter->'categories' ? :category", category: category.to_s) }
scope :by_usuarie, ->(usuarie) { where("front_matter->'usuaries' @> :usuarie::jsonb", usuarie: usuarie.to_s) }
belongs_to :site
# Convertir locale a direccionario de PG
#
# @param [String,Symbol]
# @return [String]
def self.to_dictionary(locale:)
DICTIONARIES[locale.to_sym] || 'simple'
end
end

View file

@ -13,6 +13,26 @@ class MetadataArray < MetadataTemplate
false false
end end
# Solo los datos públicos se indexan, aunque MetadataArray no se cifra
# aun, dejamos esto preparado para la posteridad.
def indexable?
true && !private?
end
def to_s
value.join(', ')
end
# Obtiene el valor desde el documento, convirtiéndolo a Array si no lo
# era ya, por retrocompabilidad.
#
# @return [Array]
def document_value
[super].flatten(1)
end
alias indexable_values value
private private
# TODO: Sanitizar otros valores # TODO: Sanitizar otros valores

View file

@ -77,6 +77,10 @@ class MetadataBelongsTo < MetadataRelatedPosts
@related_methods ||= %i[belongs_to belonged_to].freeze @related_methods ||= %i[belongs_to belonged_to].freeze
end end
def indexable_values
belongs_to&.title&.value
end
private private
def post_exists? def post_exists?

View file

@ -19,6 +19,14 @@ class MetadataContent < MetadataTemplate
document.content document.content
end end
def indexable?
true && !private?
end
def to_s
sanitizer.sanitize value, tags: [], attributes: []
end
private private
# Detectar si el contenido estaba en Markdown y pasarlo a HTML # Detectar si el contenido estaba en Markdown y pasarlo a HTML

View file

@ -7,12 +7,17 @@ class MetadataDocumentDate < MetadataTemplate
Date.today.to_time Date.today.to_time
end end
def value_from_document # @return [Time]
def document_value
return nil if post.new? return nil if post.new?
document.date document.date
end end
def indexable?
true && !private?
end
# Siempre es obligatorio # Siempre es obligatorio
def required def required
true true
@ -41,10 +46,10 @@ class MetadataDocumentDate < MetadataTemplate
begin begin
Date.iso8601(self[:value]).to_time Date.iso8601(self[:value]).to_time
rescue Date::Error rescue Date::Error
value_from_document || default_value document_value || default_value
end end
else else
self[:value] || value_from_document || default_value self[:value] || document_value || default_value
end end
end end

View file

@ -6,12 +6,13 @@ class MetadataLang < MetadataTemplate
super || I18n.locale super || I18n.locale
end end
def value_from_document # @return [Symbol]
def document_value
document.collection.label.to_sym document.collection.label.to_sym
end end
def value def value
self[:value] ||= value_from_document || default_value self[:value] ||= document_value || default_value
end end
def values def values

View file

@ -9,14 +9,15 @@ class MetadataMarkdownContent < MetadataText
end end
def value def value
self[:value] || value_from_document || default_value self[:value] || document_value || default_value
end end
def front_matter? def front_matter?
false false
end end
def value_from_document # @return [String]
def document_value
document.content document.content
end end

View file

@ -3,12 +3,16 @@
# Este campo representa el archivo donde se almacenan los datos # Este campo representa el archivo donde se almacenan los datos
class MetadataPath < MetadataTemplate class MetadataPath < MetadataTemplate
# :label en este caso es el idioma/colección # :label en este caso es el idioma/colección
#
# @return [String]
def default_value def default_value
File.join(site.path, "_#{lang}", "#{date}-#{slug}#{ext}") File.join(site.path, "_#{lang}", "#{date}-#{slug}#{ext}")
end end
# El valor no vuelve desde el documento # La ruta del archivo según Jekyll
def value_from_document #
# @return [String]
def document_value
document.path document.path
end end

View file

@ -18,6 +18,14 @@ class MetadataRelatedPosts < MetadataArray
false false
end end
def indexable?
false
end
def indexable_values
posts.where(uuid: value).map(&:title).map(&:value)
end
private private
# Obtiene todos los posts y opcionalmente los filtra # Obtiene todos los posts y opcionalmente los filtra

View file

@ -7,6 +7,10 @@ class MetadataString < MetadataTemplate
super || '' super || ''
end end
def indexable?
true && !private?
end
private private
# No se permite HTML en las strings # No se permite HTML en las strings

View file

@ -7,6 +7,11 @@
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
:value, :help, :required, :errors, :post, :value, :help, :required, :errors, :post,
:layout, keyword_init: true) do :layout, keyword_init: true) do
# Determina si el campo es indexable
def indexable?
false
end
def inspect def inspect
"#<#{self.class} site=#{site.name.inspect} post=#{post.id.inspect} value=#{value.inspect}>" "#<#{self.class} site=#{site.name.inspect} post=#{post.id.inspect} value=#{value.inspect}>"
end end
@ -44,11 +49,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
def value_was def value_was
return @value_was if instance_variable_defined? '@value_was' return @value_was if instance_variable_defined? '@value_was'
@value_was = value_from_document @value_was = document_value
end
def value_from_document
@value_from_document ||= document.data[name.to_s]
end end
def changed? def changed?
@ -80,7 +81,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
# Valor actual o por defecto. Al memoizarlo podemos modificarlo # Valor actual o por defecto. Al memoizarlo podemos modificarlo
# usando otros métodos que el de asignación. # usando otros métodos que el de asignación.
def value def value
self[:value] ||= if (data = value_from_document).present? self[:value] ||= if (data = document_value).present?
private? ? decrypt(data) : data private? ? decrypt(data) : data
else else
default_value default_value

View file

@ -17,6 +17,12 @@ class Post
attr_reader :attributes, :errors, :layout, :site, :document attr_reader :attributes, :errors, :layout, :site, :document
# TODO: Modificar el historial de Git con callbacks en lugar de
# services. De esta forma podríamos agregar soporte para distintos
# backends.
include ActiveRecord::Callbacks
include Post::Indexable
class << self class << self
# Obtiene el layout sin leer el Document # Obtiene el layout sin leer el Document
# #
@ -194,6 +200,8 @@ class Post
post: self, required: true) post: self, required: true)
end end
alias locale lang
# TODO: Mover a method_missing # TODO: Mover a method_missing
def uuid def uuid
@metadata[:uuid] ||= MetadataUuid.new(document: document, site: site, layout: layout, name: :uuid, type: :uuid, @metadata[:uuid] ||= MetadataUuid.new(document: document, site: site, layout: layout, name: :uuid, type: :uuid,
@ -256,11 +264,17 @@ class Post
end end
# Eliminar el artículo del repositorio y de la lista de artículos del # Eliminar el artículo del repositorio y de la lista de artículos del
# sitio # sitio.
#
# TODO: Si el callback falla deberíamos recuperar el archivo.
#
# @return [Post]
def destroy def destroy
FileUtils.rm_f path.absolute run_callbacks :destroy do
FileUtils.rm_f path.absolute
site.delete_post self site.delete_post self
end
end end
alias destroy! destroy alias destroy! destroy
@ -284,8 +298,10 @@ class Post
end end
end end
return false unless save_attributes! run_callbacks :save do
return false unless write return false unless save_attributes!
return false unless write
end
# Vuelve a leer el post para tomar los cambios # Vuelve a leer el post para tomar los cambios
document.reset document.reset

View file

@ -0,0 +1,76 @@
# frozen_string_literal: true
class Post
# Vuelve indexables a los Posts
module Indexable
extend ActiveSupport::Concern
included do
# Indexa o reindexa el Post
after_save :index!
after_destroy :remove_from_index!
# Devuelve una versión indexable del Post
#
# @return [IndexedPost]
def to_index
IndexedPost.find_or_create_by(id: uuid.value).tap do |indexed_post|
indexed_post.layout = layout.name
indexed_post.site_id = site.id
indexed_post.path = path.basename
indexed_post.locale = locale.value
indexed_post.dictionary = IndexedPost.to_dictionary(locale: locale.value)
indexed_post.title = title.value
indexed_post.front_matter = indexable_front_matter
indexed_post.content = indexable_content
indexed_post.created_at = date.value
indexed_post.order = attribute?(:order) ? order.value : 0
end
end
private
# Indexa o reindexa el Post
#
# @return [Boolean]
def index!
to_index.save
end
def remove_from_index!
to_index.destroy.destroyed?
end
# Los metadatos que se almacenan como objetos JSON. Empezamos con
# las categorías porque se usan para filtrar en el listado de
# artículos.
#
# @return [Hash]
def indexable_front_matter
{}.tap do |ifm|
ifm[:usuaries] = usuaries.map(&:id)
ifm[:draft] = attribute?(:draft) ? draft.value : false
ifm[:categories] = categories.indexable_values if attribute? :categories
end
end
# Devuelve un documento indexable en texto plano
#
# XXX: No memoizamos para permitir actualizaciones, aunque
# probablemente se indexe una sola vez.
#
# @return [String]
def indexable_content
indexable_attributes.map do |attr|
self[attr].to_s.tr("\n", ' ')
end.join("\n").squeeze("\n")
end
def indexable_attributes
@indexable_attributes ||= attributes.select do |attr|
self[attr].indexable?
end
end
end
end
end

View file

@ -68,6 +68,9 @@ class Site < ApplicationRecord
# El sitio en Jekyll # El sitio en Jekyll
attr_reader :jekyll attr_reader :jekyll
# XXX: Es importante incluir luego de los callbacks de :load_jekyll
include Site::Index
# No permitir HTML en estos atributos # No permitir HTML en estos atributos
def title=(title) def title=(title)
super(title.strip_tags) super(title.strip_tags)

24
app/models/site/index.rb Normal file
View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
# Indexa todos los artículos de un sitio
#
# TODO: Hacer opcional
class Site
module Index
extend ActiveSupport::Concern
included do
# TODO: Debería ser un Job?
after_create :index_posts!
has_many :indexed_posts, dependent: :destroy
def index_posts!
Site.transaction do
docs.each do |post|
post.to_index.save
end
end
end
end
end
end

View file

@ -59,9 +59,7 @@ class PostPolicy
def resolve def resolve
return scope if scope&.first&.site&.usuarie? usuarie return scope if scope&.first&.site&.usuarie? usuarie
scope.select do |post| scope.by_usuarie(usuarie.id)
post.usuaries.include? usuarie
end
end end
end end
end end

View file

@ -61,6 +61,18 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
commit_config(action: :tor) commit_config(action: :tor)
end end
# Trae cambios desde la rama remota y reindexa los artículos.
#
# @return [Boolean]
def merge
result = site.repository.merge(usuarie)
# TODO: Implementar callbacks
site.try(:index_posts!) if result
result.present?
end
private private
# Guarda los cambios de la configuración en el repositorio git # Guarda los cambios de la configuración en el repositorio git

View file

@ -7,15 +7,13 @@
%table.mb-3 %table.mb-3
- @site.layouts.each do |layout| - @site.layouts.each do |layout|
- next if layout.hidden? - next if layout.hidden?
- filter = params[:layout] == layout.value
%tr %tr
%th= layout.humanized_name %th= layout.humanized_name
%td.pl-3= link_to t('posts.add'), %td.pl-3= link_to t('posts.add'), new_site_post_path(@site, layout: layout.value), class: 'btn btn-secondary btn-sm'
new_site_post_path(@site, layout: layout.name), - if @filter_params[:layout] == layout.name.to_s
class: 'badge badge-secondary' %td= link_to t('posts.remove_filter'), site_posts_path(@site, **@filter_params.merge(layout: nil)), class: 'btn btn-primary btn-sm'
%td= link_to t(filter ? 'posts.remove_filter' : 'posts.filter'), - else
site_posts_path(@site, layout: (filter ? nil : layout.value)), %td= link_to t('posts.filter'), site_posts_path(@site, **@filter_params.merge(layout: layout.value)), class: 'btn btn-secondary btn-sm'
class: 'badge badge-' + (filter ? 'primary' : 'secondary')
- if policy(@site).edit? - if policy(@site).edit?
= link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn' = link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn'
@ -41,19 +39,34 @@
%section.col %section.col
= render 'layouts/flash' = render 'layouts/flash'
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
%form
- @filter_params.each do |param, value|
- next if param == 'q'
%input{ type: 'hidden', name: param, value: value }
.form-group.flex-grow-0.m-0
%input.form-control.border.border-magenta{ type: 'search', placeholder: 'Buscar', name: 'q', value: @filter_params[:q] }
%input.sr-only{ type: 'submit' }
- if @site.locales.size > 1
%nav#locales
- @site.locales.each do |locale|
= link_to t("locales.#{locale}.name"), site_posts_path(@site, **@filter_params.merge(locale: locale)),
class: "mr-2 mt-2 mb-2 #{locale == @locale ? 'active font-weight-bold' : ''}"
.pl-2-plus
- @filter_params.each do |param, value|
- if param == 'layout'
- value = @site.layouts[value.to_sym].humanized_name
= link_to site_posts_path(@site, **@filter_params.reject { |k, _| k == param }),
class: 'btn btn-secondary btn-sm',
title: t('posts.remove_filter_help', filter: value),
aria: { labelledby: "help-filter-#{param}" } do
= value
&times;
- if @posts.empty? - if @posts.empty?
%h2= t('posts.none') %h2= t('posts.empty')
- else - else
= form_tag site_posts_reorder_path, method: :post do = form_tag site_posts_reorder_path, method: :post do
.d-flex.justify-content-between.align-items-center
-#
TODO: Pensar una interfaz mejor para cuando haya más de tres
idiomas
- unless @site.locales.length == 1
.locales
- @site.locales.each do |locale|
= link_to t("locales.#{locale}.name"), site_posts_path(@site, locale: locale),
class: "mr-2 mt-2 mb-2#{locale == @locale ? 'active font-weight-bold' : ''}"
%table.table{ data: { controller: 'reorder' } } %table.table{ data: { controller: 'reorder' } }
%caption.sr-only= t('posts.caption') %caption.sr-only= t('posts.caption')
%thead %thead
@ -69,54 +82,48 @@
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom') %button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
%tbody %tbody
- dir = t("locales.#{@locale}.dir") - dir = t("locales.#{@locale}.dir")
- size = @posts.size
- @posts.each_with_index do |post, i| - @posts.each_with_index do |post, i|
-# -#
TODO: Solo les usuaries cachean porque tenemos que separar TODO: Solo les usuaries cachean porque tenemos que separar
les botones por permisos. les botones por permisos.
- cache_if @usuarie, [post, I18n.locale] do - cache_if @usuarie, [post, I18n.locale] do
- checkbox_id = "checkbox-#{post.uuid.value}" - checkbox_id = "checkbox-#{post.id}"
%tr{ id: post.uuid.value, data: { target: 'reorder.row' } } %tr{ id: post.id, data: { target: 'reorder.row' } }
%td %td
.custom-control.custom-checkbox .custom-control.custom-checkbox
%input.custom-control-input{ id: checkbox_id, type: 'checkbox', autocomplete: 'off', data: { action: 'reorder#select' } } %input.custom-control-input{ id: checkbox_id, type: 'checkbox', autocomplete: 'off', data: { action: 'reorder#select' } }
%label.custom-control-label{ for: checkbox_id } %label.custom-control-label{ for: checkbox_id }
%span.sr-only= t('posts.reorder.select') %span.sr-only= t('posts.reorder.select')
-# Orden más alto es mayor prioridad -# Orden más alto es mayor prioridad
= hidden_field 'post[reorder]', post.uuid.value, = hidden_field 'post[reorder]', post.id,
value: @posts.length - i, value: size - i,
data: { reorder: true } data: { reorder: true }
%td.w-100{ class: dir } %td.w-100{ class: dir }
= link_to site_post_path(@site, post.id) do = link_to site_post_path(@site, post.path) do
%span{ lang: post.lang.value, dir: dir }= post.title.value %span{ lang: post.locale, dir: dir }= post.title
- if post.attributes.include? :draft - if post.front_matter['draft'].present?
- if post.draft.value %span.badge.badge-primary= I18n.t('posts.attributes.draft.label')
%span.badge.badge-primary - if post.front_matter['categories'].present?
= post_label_t(:draft, post: post) %br
- if post.attributes.include? :categories %small
- unless post.categories.value.empty? - post.front_matter['categories'].each do |category|
%br = link_to site_posts_path(@site, **@filter_params.merge(category: category)) do
%small %span{ lang: post.locale, dir: dir }= category
- categories = post.categories.respond_to?(:has_many) ? post.categories.has_many : post.categories.value = '/' unless post.front_matter['categories'].last == category
- categories.each do |c|
- c.read if c.respond_to? :read
= link_to site_posts_path(@site, category: (c.respond_to?(:uuid) ? c.uuid.value : c)) do
%span{ lang: post.lang.value, dir: dir }= (c.respond_to?(:title) ? c.title.value : c)
- unless categories.last == c
= '/'
%td %td
= post.date.value.strftime('%F') = post.created_at.strftime('%F')
%br/ %br/
- if post.attribute? :order = post.order
= post.order.value
%td %td
- if @usuarie || policy(post).edit? - if @usuarie || policy(post).edit?
= link_to t('posts.edit'), = link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block'
edit_site_post_path(@site, post.id),
class: 'btn btn-block'
- if @usuarie || policy(post).destroy? - if @usuarie || policy(post).destroy?
= link_to t('posts.destroy'), = link_to t('posts.destroy'), site_post_path(@site, post.path), class: 'btn btn-block', method: :delete, data: { confirm: t('posts.confirm_destroy') }
site_post_path(@site, post.id),
class: 'btn btn-block', #footnotes{ hidden: true }
method: :delete, - @filter_params.each do |param, value|
data: { confirm: t('posts.confirm_destroy') } - if param == 'layout'
- value = @site.layouts[value.to_sym].humanized_name
%label{ id: "help-filter-#{param}" }= t('posts.remove_filter_help', filter: value)

View file

@ -69,8 +69,8 @@ Rails.application.configure do
error_grouping: true, error_grouping: true,
email: { email: {
email_prefix: '', email_prefix: '',
sender_address: ENV.fetch('DEFAULT_FROM', "noreply@sutty.nl"), sender_address: ENV.fetch('DEFAULT_FROM', 'noreply@sutty.nl'),
exception_recipients: ENV.fetch('EXCEPTION_TO', "errors@sutty.nl"), exception_recipients: ENV.fetch('EXCEPTION_TO', 'errors@sutty.nl'),
normalize_subject: true normalize_subject: true
} }
end end

View file

@ -102,6 +102,19 @@ module Jekyll
end end
end end
# No aplicar el orden por ranking para poder obtener los artículos en el
# orden que tendrían en el sitio final.
module PgSearch
ScopeOptions.class_eval do
def apply(scope)
scope = include_table_aliasing_for_rank(scope)
rank_table_alias = scope.pg_search_rank_table_alias(include_counter: true)
scope.joins(rank_join(rank_table_alias))
end
end
end
# JekyllData::Reader del plugin jekyll-data modifica Jekyll::Site#reader # JekyllData::Reader del plugin jekyll-data modifica Jekyll::Site#reader
# para también leer los datos que vienen en el theme. # para también leer los datos que vienen en el theme.
module JekyllData module JekyllData

View file

@ -376,6 +376,7 @@ en:
en: 'English' en: 'English'
ar: 'Arabic' ar: 'Arabic'
posts: posts:
empty: "There are no results for those search parameters."
caption: Post list caption: Post list
attribute_ro: attribute_ro:
file: file:
@ -412,6 +413,8 @@ en:
destroy: Remove image destroy: Remove image
belongs_to: belongs_to:
empty: "(Empty)" empty: "(Empty)"
draft:
label: Draft
reorder: reorder:
submit: 'Save order' submit: 'Save order'
select: 'Select this post' select: 'Select this post'
@ -430,6 +433,7 @@ en:
add: 'Add' add: 'Add'
filter: 'Filter' filter: 'Filter'
remove_filter: 'Back' remove_filter: 'Back'
remove_filter_help: 'Remove the filter: %{filter}'
categories: 'Everything' categories: 'Everything'
index: 'Posts' index: 'Posts'
edit: 'Edit' edit: 'Edit'

View file

@ -383,6 +383,7 @@ es:
en: 'inglés' en: 'inglés'
ar: 'árabe' ar: 'árabe'
posts: posts:
empty: No hay artículos con estos parámetros de búsqueda.
caption: Lista de artículos caption: Lista de artículos
attribute_ro: attribute_ro:
file: file:
@ -419,6 +420,8 @@ es:
destroy: 'Eliminar imagen' destroy: 'Eliminar imagen'
belongs_to: belongs_to:
empty: "(Vacío)" empty: "(Vacío)"
draft:
label: Borrador
reorder: reorder:
submit: 'Guardar orden' submit: 'Guardar orden'
select: 'Seleccionar este artículo' select: 'Seleccionar este artículo'
@ -438,6 +441,7 @@ es:
add: 'Agregar' add: 'Agregar'
filter: 'Filtrar' filter: 'Filtrar'
remove_filter: 'Volver' remove_filter: 'Volver'
remove_filter_help: 'Quitar este filtro: %{filter}'
index: 'Artículos' index: 'Artículos'
edit: 'Editar' edit: 'Editar'
preview: preview:

View file

@ -31,7 +31,8 @@ Rails.application.routes.draw do
# detectar el nombre del sitio. # detectar el nombre del sitio.
get '/sites/private/:site_id(*file)', to: 'private#show', constraints: { site_id: %r{[^/]+} } get '/sites/private/:site_id(*file)', to: 'private#show', constraints: { site_id: %r{[^/]+} }
# Obtener archivos estáticos desde el directorio público # Obtener archivos estáticos desde el directorio público
get '/sites/:site_id/static_file/(*file)', to: 'sites#static_file', as: 'site_static_file', constraints: { site_id: %r{[^/]+} } get '/sites/:site_id/static_file/(*file)', to: 'sites#static_file', as: 'site_static_file',
constraints: { site_id: %r{[^/]+} }
get '/env.js', to: 'env#index' get '/env.js', to: 'env#index'
match '/api/v3/projects/:site_id/notices' => 'api/v1/notices#create', via: %i[post] match '/api/v3/projects/:site_id/notices' => 'api/v1/notices#create', via: %i[post]

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
# Habilitar las extensiones de búsqueda de texto libre
class CreatePgSearchExtensions < ActiveRecord::Migration[6.1]
def change
enable_extension 'plpgsql'
enable_extension 'pg_trgm'
end
end

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
# Crea la tabla donde se indexa el contenido de los artículos, los
# IndexedPosts van a estar relacionados con un Post del mismo UUID.
#
# Solo contienen la información mínima necesaria para mostrar los
# resultados de búsqueda.
class CreateIndexedPosts < ActiveRecord::Migration[6.1]
def change
# Necesario para gen_random_uuid()
#
# XXX: En realidad no lo necesitamos porque cada IndexedPost va a
# tener el UUID del Post correspondiente.
enable_extension 'pgcrypto'
create_table :indexed_posts, id: false do |t|
t.primary_key :id, :uuid, default: 'public.gen_random_uuid()'
t.belongs_to :site, index: true
t.timestamps
# Filtramos por idioma
t.string :locale, default: 'simple', index: true
# Vamos a querer filtrar por layout
t.string :layout, null: false, index: true
# Esta es la ruta al artículo
t.string :path, null: false
# Queremos mostrar el título por separado
t.string :title, default: ''
# También vamos a mostrar las categorías
t.jsonb :front_matter, default: '{}'
t.string :content, default: ''
t.tsvector :indexed_content
t.index :indexed_content, using: 'gin'
t.index :front_matter, using: 'gin'
end
# Crea un trigger que actualiza el índice tsvector con el título y
# contenido del artículo y su idioma.
create_trigger(compatibility: 1).on(:indexed_posts).before(:insert, :update) do
"new.indexed_content := to_tsvector(('pg_catalog.' || new.locale)::regconfig, coalesce(new.title, '') || '\n' || coalesce(new.content,''));"
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
# Agrega el orden y fecha del post en el post indexado, de forma que los
# resultados se puedan obtener en el mismo orden.
class AddOrderToIndexedPosts < ActiveRecord::Migration[6.1]
def change
add_column :indexed_posts, :order, :integer, default: 0
end
end

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
# Para no estar calculando todo el tiempo el diccionario del idioma,
# agregamos una columna más.
class AddDictionaryToIndexedPosts < ActiveRecord::Migration[6.1]
LOCALES = {
'english' => 'en',
'spanish' => 'es'
}
def up
add_column :indexed_posts, :dictionary, :string
create_trigger(compatibility: 1).on(:indexed_posts).before(:insert, :update) do
"new.indexed_content := to_tsvector(('pg_catalog.' || new.dictionary)::regconfig, coalesce(new.title, '') || '\n' || coalesce(new.content,''));"
end
IndexedPost.find_each do |post|
locale = post.locale
post.update dictionary: locale, locale: LOCALES[locale]
end
end
def down
remove_column :indexed_posts, :locale
rename_column :indexed_posts, :dictionary, :locale
end
end

View file

@ -10,16 +10,74 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_05_11_211357) do ActiveRecord::Schema.define(version: 2021_05_14_165639) do
# Could not dump table "access_logs" because of following StandardError # These are extensions that must be enabled in order to support this database
# Unknown type '' for column 'id' enable_extension "pg_trgm"
enable_extension "pgcrypto"
enable_extension "plpgsql"
create_table "access_logs", id: :uuid, default: nil, force: :cascade do |t|
t.string "host"
t.float "msec"
t.string "server_protocol"
t.string "request_method"
t.string "request_completion"
t.string "uri"
t.string "query_string"
t.integer "status"
t.string "sent_http_content_type"
t.string "sent_http_content_encoding"
t.string "sent_http_etag"
t.string "sent_http_last_modified"
t.string "http_accept"
t.string "http_accept_encoding"
t.string "http_accept_language"
t.string "http_pragma"
t.string "http_cache_control"
t.string "http_if_none_match"
t.string "http_dnt"
t.string "http_user_agent"
t.string "http_origin"
t.float "request_time"
t.integer "bytes_sent"
t.integer "body_bytes_sent"
t.integer "request_length"
t.string "http_connection"
t.string "pipe"
t.integer "connection_requests"
t.string "geoip2_data_country_name"
t.string "geoip2_data_city_name"
t.string "ssl_server_name"
t.string "ssl_protocol"
t.string "ssl_early_data"
t.string "ssl_session_reused"
t.string "ssl_curves"
t.string "ssl_ciphers"
t.string "ssl_cipher"
t.string "sent_http_x_xss_protection"
t.string "sent_http_x_frame_options"
t.string "sent_http_x_content_type_options"
t.string "sent_http_strict_transport_security"
t.string "nginx_version"
t.integer "pid"
t.string "remote_user"
t.boolean "crawler", default: false
t.string "http_referer"
t.index ["geoip2_data_city_name"], name: "index_access_logs_on_geoip2_data_city_name"
t.index ["geoip2_data_country_name"], name: "index_access_logs_on_geoip2_data_country_name"
t.index ["host"], name: "index_access_logs_on_host"
t.index ["http_origin"], name: "index_access_logs_on_http_origin"
t.index ["http_user_agent"], name: "index_access_logs_on_http_user_agent"
t.index ["status"], name: "index_access_logs_on_status"
t.index ["uri"], name: "index_access_logs_on_uri"
end
create_table "action_text_rich_texts", force: :cascade do |t| create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.text "body" t.text "body"
t.string "record_type", null: false t.string "record_type", null: false
t.integer "record_id", null: false t.bigint "record_id", null: false
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
@ -28,8 +86,8 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
create_table "active_storage_attachments", force: :cascade do |t| create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
t.integer "record_id", null: false t.bigint "record_id", null: false
t.integer "blob_id", null: false t.bigint "blob_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
@ -40,7 +98,7 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
t.string "filename", null: false t.string "filename", null: false
t.string "content_type" t.string "content_type"
t.text "metadata" t.text "metadata"
t.integer "byte_size", null: false t.bigint "byte_size", null: false
t.string "checksum", null: false t.string "checksum", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.string "service_name", null: false t.string "service_name", null: false
@ -53,10 +111,65 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end end
create_table "blazer_audits", force: :cascade do |t|
t.bigint "user_id"
t.bigint "query_id"
t.text "statement"
t.string "data_source"
t.datetime "created_at"
t.index ["query_id"], name: "index_blazer_audits_on_query_id"
t.index ["user_id"], name: "index_blazer_audits_on_user_id"
end
create_table "blazer_checks", force: :cascade do |t|
t.bigint "creator_id"
t.bigint "query_id"
t.string "state"
t.string "schedule"
t.text "emails"
t.text "slack_channels"
t.string "check_type"
t.text "message"
t.datetime "last_run_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["creator_id"], name: "index_blazer_checks_on_creator_id"
t.index ["query_id"], name: "index_blazer_checks_on_query_id"
end
create_table "blazer_dashboard_queries", force: :cascade do |t|
t.bigint "dashboard_id"
t.bigint "query_id"
t.integer "position"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["dashboard_id"], name: "index_blazer_dashboard_queries_on_dashboard_id"
t.index ["query_id"], name: "index_blazer_dashboard_queries_on_query_id"
end
create_table "blazer_dashboards", force: :cascade do |t|
t.bigint "creator_id"
t.text "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["creator_id"], name: "index_blazer_dashboards_on_creator_id"
end
create_table "blazer_queries", force: :cascade do |t|
t.bigint "creator_id"
t.string "name"
t.text "description"
t.text "statement"
t.string "data_source"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["creator_id"], name: "index_blazer_queries_on_creator_id"
end
create_table "build_stats", force: :cascade do |t| create_table "build_stats", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "deploy_id" t.bigint "deploy_id"
t.bigint "bytes" t.bigint "bytes"
t.float "seconds" t.float "seconds"
t.string "action", null: false t.string "action", null: false
@ -65,13 +178,27 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
t.index ["deploy_id"], name: "index_build_stats_on_deploy_id" t.index ["deploy_id"], name: "index_build_stats_on_deploy_id"
end end
# Could not dump table "csp_reports" because of following StandardError create_table "csp_reports", id: :uuid, default: nil, force: :cascade do |t|
# Unknown type 'uuid' for column 'id' t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "disposition"
t.string "referrer"
t.string "blocked_uri"
t.string "document_uri"
t.string "effective_directive"
t.string "original_policy"
t.string "script_sample"
t.string "status_code"
t.string "violated_directive"
t.integer "column_number"
t.integer "line_number"
t.string "source_file"
end
create_table "deploys", force: :cascade do |t| create_table "deploys", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "site_id" t.bigint "site_id"
t.string "type" t.string "type"
t.text "values" t.text "values"
t.index ["site_id"], name: "index_deploys_on_site_id" t.index ["site_id"], name: "index_deploys_on_site_id"
@ -91,6 +218,26 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
t.string "designer_url" t.string "designer_url"
end end
create_table "indexed_posts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.bigint "site_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "locale", default: "simple"
t.string "layout", null: false
t.string "path", null: false
t.string "title", default: ""
t.jsonb "front_matter", default: "{}"
t.string "content", default: ""
t.tsvector "indexed_content"
t.integer "order", default: 0
t.string "dictionary"
t.index ["front_matter"], name: "index_indexed_posts_on_front_matter", using: :gin
t.index ["indexed_content"], name: "index_indexed_posts_on_indexed_content", using: :gin
t.index ["layout"], name: "index_indexed_posts_on_layout"
t.index ["locale"], name: "index_indexed_posts_on_locale"
t.index ["site_id"], name: "index_indexed_posts_on_site_id"
end
create_table "licencias", force: :cascade do |t| create_table "licencias", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
@ -104,7 +251,7 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
create_table "log_entries", force: :cascade do |t| create_table "log_entries", force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.integer "site_id" t.bigint "site_id"
t.text "text" t.text "text"
t.boolean "sent", default: false t.boolean "sent", default: false
t.index ["site_id"], name: "index_log_entries_on_site_id" t.index ["site_id"], name: "index_log_entries_on_site_id"
@ -124,7 +271,7 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
t.string "key", null: false t.string "key", null: false
t.string "value" t.string "value"
t.string "translatable_type" t.string "translatable_type"
t.integer "translatable_id" t.bigint "translatable_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_string_translations_on_translatable_attribute" t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_string_translations_on_translatable_attribute"
@ -137,7 +284,7 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
t.string "key", null: false t.string "key", null: false
t.text "value" t.text "value"
t.string "translatable_type" t.string "translatable_type"
t.integer "translatable_id" t.bigint "translatable_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_text_translations_on_translatable_attribute" t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_text_translations_on_translatable_attribute"
@ -147,8 +294,8 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
create_table "roles", force: :cascade do |t| create_table "roles", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "site_id" t.bigint "site_id"
t.integer "usuarie_id" t.bigint "usuarie_id"
t.string "rol" t.string "rol"
t.boolean "temporal" t.boolean "temporal"
t.index ["site_id", "usuarie_id"], name: "index_roles_on_site_id_and_usuarie_id", unique: true t.index ["site_id", "usuarie_id"], name: "index_roles_on_site_id_and_usuarie_id", unique: true
@ -160,15 +307,14 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "name" t.string "name"
t.integer "design_id" t.bigint "design_id"
t.integer "licencia_id" t.bigint "licencia_id"
t.string "status", default: "waiting" t.string "status", default: "waiting"
t.text "description" t.text "description"
t.string "title" t.string "title"
t.boolean "colaboracion_anonima", default: false t.boolean "colaboracion_anonima", default: false
t.boolean "contact", default: false t.boolean "contact", default: false
t.string "private_key_ciphertext" t.string "private_key_ciphertext"
t.boolean "invitades", default: false
t.boolean "acepta_invitades", default: false t.boolean "acepta_invitades", default: false
t.string "tienda_api_key_ciphertext", default: "" t.string "tienda_api_key_ciphertext", default: ""
t.string "tienda_url", default: "" t.string "tienda_url", default: ""
@ -200,7 +346,7 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
t.datetime "invitation_accepted_at" t.datetime "invitation_accepted_at"
t.integer "invitation_limit" t.integer "invitation_limit"
t.string "invited_by_type" t.string "invited_by_type"
t.integer "invited_by_id" t.bigint "invited_by_id"
t.integer "invitations_count", default: 0 t.integer "invitations_count", default: 0
t.string "lang", default: "es" t.string "lang", default: "es"
t.index ["confirmation_token"], name: "index_usuaries_on_confirmation_token", unique: true t.index ["confirmation_token"], name: "index_usuaries_on_confirmation_token", unique: true
@ -208,11 +354,20 @@ ActiveRecord::Schema.define(version: 2021_05_11_211357) do
t.index ["invitation_token"], name: "index_usuaries_on_invitation_token", unique: true t.index ["invitation_token"], name: "index_usuaries_on_invitation_token", unique: true
t.index ["invitations_count"], name: "index_usuaries_on_invitations_count" t.index ["invitations_count"], name: "index_usuaries_on_invitations_count"
t.index ["invited_by_id"], name: "index_usuaries_on_invited_by_id" t.index ["invited_by_id"], name: "index_usuaries_on_invited_by_id"
t.index ["invited_by_type", "invited_by_id"], name: "index_usuaries_on_invited_by_type_and_invited_by_id" t.index ["invited_by_type", "invited_by_id"], name: "index_usuaries_on_invited_by"
t.index ["reset_password_token"], name: "index_usuaries_on_reset_password_token", unique: true t.index ["reset_password_token"], name: "index_usuaries_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_usuaries_on_unlock_token", unique: true t.index ["unlock_token"], name: "index_usuaries_on_unlock_token", unique: true
end end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
create_trigger("indexed_posts_before_insert_update_row_tr", :compatibility => 1).
on("indexed_posts").
before(:insert, :update) do
<<-SQL_ACTIONS
new.indexed_content := to_tsvector(('pg_catalog.' || new.dictionary)::regconfig, coalesce(new.title, '') || '
' || coalesce(new.content,''));
SQL_ACTIONS
end
end end

View file

@ -19,87 +19,87 @@ class DeployJobTest < ActiveSupport::TestCase
# setTimeout(() => { throw('Prueba') }, 1000) # setTimeout(() => { throw('Prueba') }, 1000)
def notice def notice
@notice ||= { @notice ||= {
"errors" => [ 'errors' => [
{ {
"type" => "", 'type' => '',
"message" => "Prueba", 'message' => 'Prueba',
"backtrace" => [ 'backtrace' => [
{ {
"function" => "pt</e.prototype.notify", 'function' => 'pt</e.prototype.notify',
"file" => "https://tintalimon.com.ar/assets/js/pack.js", 'file' => 'https://tintalimon.com.ar/assets/js/pack.js',
"line" => 89, 'line' => 89,
"column" => 74094 'column' => 74_094
}, },
{ {
"function" => "pt</e.prototype.onerror", 'function' => 'pt</e.prototype.onerror',
"file" => "https://tintalimon.com.ar/assets/js/pack.js", 'file' => 'https://tintalimon.com.ar/assets/js/pack.js',
"line" => 89, 'line' => 89,
"column" => 74731 'column' => 74_731
}, },
{ {
"function" => "pt</e.prototype._instrument/window.onerror", 'function' => 'pt</e.prototype._instrument/window.onerror',
"file" => "https://tintalimon.com.ar/assets/js/pack.js", 'file' => 'https://tintalimon.com.ar/assets/js/pack.js',
"line" => 89, 'line' => 89,
"column" => 71925 'column' => 71_925
}, },
{ {
"function" => "setTimeout handler*", 'function' => 'setTimeout handler*',
"file" => "debugger eval code", 'file' => 'debugger eval code',
"line" => 1, 'line' => 1,
"column" => 11 'column' => 11
} }
] ]
} }
], ],
"context" => { 'context' => {
"severity" => "error", 'severity' => 'error',
"history" => [ 'history' => [
{ {
"type" => "error", 'type' => 'error',
"target" => "html. > head. > script.[type=\"text/javascript\"][src=\"//stats.habitapp.org/piwik.js\"]", 'target' => 'html. > head. > script.[type="text/javascript"][src="//stats.habitapp.org/piwik.js"]',
"date" => "2021-04-26T22:06:58.390Z" 'date' => '2021-04-26T22:06:58.390Z'
}, },
{ {
"type" => "DOMContentLoaded", 'type' => 'DOMContentLoaded',
"target" => "[object HTMLDocument]", 'target' => '[object HTMLDocument]',
"date" => "2021-04-26T22:06:58.510Z" 'date' => '2021-04-26T22:06:58.510Z'
}, },
{ {
"type" => "load", 'type' => 'load',
"target" => "[object HTMLDocument]", 'target' => '[object HTMLDocument]',
"date" => "2021-04-26T22:06:58.845Z" 'date' => '2021-04-26T22:06:58.845Z'
}, },
{ {
"type" => "xhr", 'type' => 'xhr',
"date" => "2021-04-26T22:06:58.343Z", 'date' => '2021-04-26T22:06:58.343Z',
"method" => "GET", 'method' => 'GET',
"url" => "assets/data/site.json", 'url' => 'assets/data/site.json',
"statusCode" => 200, 'statusCode' => 200,
"duration" => 506 'duration' => 506
}, },
{ {
"type" => "xhr", 'type' => 'xhr',
"date" => "2021-04-26T22:06:58.886Z", 'date' => '2021-04-26T22:06:58.886Z',
"method" => "GET", 'method' => 'GET',
"url" => "assets/templates/cart.html", 'url' => 'assets/templates/cart.html',
"statusCode" => 200, 'statusCode' => 200,
"duration" => 591 'duration' => 591
} }
], ],
"windowError" => true, 'windowError' => true,
"notifier" => { 'notifier' => {
"name" => "airbrake-js/browser", 'name' => 'airbrake-js/browser',
"version" => "1.4.2", 'version' => '1.4.2',
"url" => "https://github.com/airbrake/airbrake-js/tree/master/packages/browser" 'url' => 'https://github.com/airbrake/airbrake-js/tree/master/packages/browser'
}, },
"userAgent" => "Mozilla/5.0 (Windows NT 6.1; rv:85.0) Gecko/20100101 Firefox/85.0", 'userAgent' => 'Mozilla/5.0 (Windows NT 6.1; rv:85.0) Gecko/20100101 Firefox/85.0',
"url" => "https://tintalimon.com.ar/carrito/", 'url' => 'https://tintalimon.com.ar/carrito/',
"rootDirectory" => "https://tintalimon.com.ar", 'rootDirectory' => 'https://tintalimon.com.ar',
"language" => "JavaScript" 'language' => 'JavaScript'
}, },
"params" => {}, 'params' => {},
"environment" => {}, 'environment' => {},
"session" => {} 'session' => {}
} }
# XXX: Siempre devolvemos un duplicado porque BacktraceJob lo # XXX: Siempre devolvemos un duplicado porque BacktraceJob lo
@ -120,8 +120,8 @@ class DeployJobTest < ActiveSupport::TestCase
email = ActionMailer::Base.deliveries.first email = ActionMailer::Base.deliveries.first
assert email assert email
assert_equal " (BacktraceJob::BacktraceException) \"tintalimon.com.ar: Prueba\"", email.subject assert_equal ' (BacktraceJob::BacktraceException) "tintalimon.com.ar: Prueba"', email.subject
assert (%r{webpack://} =~ email.body.to_s) assert(%r{webpack://} =~ email.body.to_s)
end end
test 'los errores se basan en un sitio' do test 'los errores se basan en un sitio' do

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'test_helper'
class IndexedPostTest < ActiveSupport::TestCase
def site
@site ||= create :site
end
teardown do
@site&.destroy
end
test 'se pueden convertir los diccionarios' do
IndexedPost::DICTIONARIES.each do |locale, dict|
assert_equal dict, IndexedPost.to_dictionary(locale: locale)
end
end
test 'se pueden buscar por categoría' do
assert(post = site.posts.create(title: SecureRandom.hex, description: SecureRandom.hex,
categories: [SecureRandom.hex, SecureRandom.hex]))
assert_not_empty site.indexed_posts.in_category(post.categories.value.sample)
end
test 'se pueden encontrar por usuarie' do
usuarie = create :usuarie
assert(post = site.posts.create(title: SecureRandom.hex, description: SecureRandom.hex))
post.usuaries << usuarie
post.save
assert_not_empty site.indexed_posts.by_usuarie(usuarie.id)
end
end

View file

@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'test_helper'
class Post::IndexableTest < ActiveSupport::TestCase
setup do
@site = create :site
end
teardown do
@site&.destroy
end
test 'los posts se indexan apenas se crean' do
post = @site.posts.create(title: SecureRandom.hex, description: SecureRandom.hex)
indexed_post = @site.indexed_posts.find_by_title post.title.value
assert indexed_post
assert_equal post.locale.value.to_s, indexed_post.locale
assert_equal post.order.value, indexed_post.order
assert_equal post.path.basename, indexed_post.path
assert_equal post.layout.name.to_s, indexed_post.layout
end
test 'se pueden encontrar posts' do
post = @site.posts.sample
assert @site.indexed_posts.where(locale: post.lang.value).search(post.lang.value, post.title.value)
assert @site.indexed_posts.where(locale: post.lang.value).search(post.lang.value, post.description.value)
end
test 'se pueden actualizar posts' do
post = @site.posts.sample
post.description.value = SecureRandom.hex
assert post.save
assert @site.indexed_posts.where(locale: post.lang.value).search(post.lang.value, post.description.value)
end
test 'al borrar el post se borra el indice' do
post = @site.posts.sample
assert post.destroy
assert_not @site.indexed_posts.find_by_id(post.uuid.value)
end
end