Merge pull request #573 from coopdevs/develop

Release v3.0
This commit is contained in:
Pau Pérez Fabregat
2020-12-15 10:08:48 +01:00
committed by GitHub
15 changed files with 1307 additions and 377 deletions

View File

@ -3,7 +3,6 @@ dist: bionic
cache: bundler
services:
- postgresql
- elasticsearch
addons:
postgresql: "9.4"
chrome: stable
@ -19,7 +18,5 @@ before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
# allow elasticsearch to be ready - https://docs.travis-ci.com/user/database-setup/#ElasticSearch
- sleep 10
after_script:
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT

View File

@ -16,10 +16,9 @@ gem 'unicorn'
gem 'kaminari', '~> 1.1.1'
gem "simple_form", ">= 3.0.0"
gem 'rollbar', '2.8.3'
gem 'pg_search', '2.1.4'
gem 'prawn', '~> 2.2.0'
gem 'prawn-table', '~> 0.2.2'
gem 'elasticsearch-model'
gem 'elasticsearch-rails'
gem 'skylight'
gem 'sidekiq', '5.1.3'
gem 'sidekiq-cron', '~> 1.1.0'

View File

@ -118,29 +118,14 @@ GEM
dotenv-rails (2.7.1)
dotenv (= 2.7.1)
railties (>= 3.2, < 6.1)
elasticsearch (1.0.8)
elasticsearch-api (= 1.0.7)
elasticsearch-transport (= 1.0.7)
elasticsearch-api (1.0.7)
multi_json
elasticsearch-model (0.1.7)
activesupport (> 3)
elasticsearch (> 0.4)
hashie
elasticsearch-rails (0.1.7)
elasticsearch-transport (1.0.7)
faraday
multi_json
erubi (1.7.1)
erubi (1.9.0)
erubis (2.7.0)
et-orbi (1.1.7)
tzinfo
execjs (2.7.0)
fabrication (2.20.1)
faker (1.9.3)
faker (1.9.6)
i18n (>= 0.7)
faraday (0.9.1)
multipart-post (>= 1.2, < 3)
ffi (1.12.2)
formtastic (3.1.5)
actionpack (>= 3.2.13)
@ -154,7 +139,6 @@ GEM
has_scope (0.6.0)
actionpack (>= 3.2, < 5)
activesupport (>= 3.2, < 5)
hashie (3.4.1)
hstore_translate (1.0.0)
activerecord (>= 3.1.0)
http-cookie (1.0.3)
@ -207,7 +191,6 @@ GEM
mini_portile2 (2.4.0)
minitest (5.14.0)
multi_json (1.11.2)
multipart-post (2.0.0)
net-scp (2.0.0)
net-ssh (>= 2.6.5, < 6.0.0)
net-ssh (5.2.0)
@ -220,8 +203,9 @@ GEM
ast (~> 2.4.0)
pdf-core (0.7.0)
pg (0.21.0)
polyamorous (1.3.3)
activerecord (>= 3.0)
pg_search (2.1.4)
activerecord (>= 4.2)
activesupport (>= 4.2)
prawn (2.2.2)
pdf-core (~> 0.7.0)
ttfunk (~> 1.5)
@ -266,12 +250,11 @@ GEM
rainbow (3.0.0)
raindrops (0.16.0)
rake (13.0.1)
ransack (1.8.6)
actionpack (>= 3.0)
activerecord (>= 3.0)
activesupport (>= 3.0)
ransack (1.8.10)
actionpack (>= 3.0, < 5.2)
activerecord (>= 3.0, < 5.2)
activesupport (>= 3.0, < 5.2)
i18n
polyamorous (~> 1.3.2)
rb-fsevent (0.10.3)
rb-inotify (0.10.1)
ffi (~> 1.0)
@ -290,13 +273,13 @@ GEM
multi_json
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.0)
rspec-expectations (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (3.9.0)
rspec-rails (3.9.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
@ -333,7 +316,7 @@ GEM
selenium-webdriver (3.142.7)
childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2)
shoulda-matchers (3.1.2)
shoulda-matchers (3.1.3)
activesupport (>= 4.0.0)
sidekiq (5.1.3)
concurrent-ruby (~> 1.0)
@ -414,8 +397,6 @@ DEPENDENCIES
database_cleaner (= 1.6.2)
devise (~> 4.7.1)
dotenv-rails (~> 2.7.1)
elasticsearch-model
elasticsearch-rails
fabrication (~> 2.20)
faker (~> 1.9)
has_scope
@ -426,6 +407,7 @@ DEPENDENCIES
letter_opener (= 1.4.1)
localeapp (= 2.1.1)
pg (= 0.21.0)
pg_search (= 2.1.4)
prawn (~> 2.2.0)
prawn-table (~> 0.2.2)
pundit (~> 2.0.0)

View File

@ -20,11 +20,6 @@ class MembersController < ApplicationController
def toggle_active
find_member
@member.toggle(:active).save!
if @member.active
@member.add_all_posts_to_index
else
@member.remove_all_posts_from_index
end
respond_to do |format|
format.json { head :ok }
format.html { redirect_to :back }

View File

@ -4,31 +4,22 @@ class PostsController < ApplicationController
has_scope :by_organization, as: :org
def index
if (query = params[:q]).present?
# match query term on fields
must = [ { multi_match: {
query: query.to_s,
type: "phrase_prefix",
fields: ["title^2", "description", "tags^2"]
} } ]
if current_organization.present?
# filter by organization
must << { term: { organization_id: { value: current_organization.id } } }
end
posts = model.__elasticsearch__.search(
query: {
bool: {
must: must
}
}
).page(params[:page]).per(25).records
else
posts = model.active.of_active_members
if current_organization.present?
posts = posts.merge(current_organization.posts)
end
posts = apply_scopes(posts).page(params[:page]).per(25)
context = model.active.of_active_members
if current_organization.present?
context = context.where(
organization_id: current_organization.id
)
end
posts = if (query = params[:q]).present?
context.
search_by_query(query).
page(params[:page]).
per(25)
else
apply_scopes(context).page(params[:page]).per(25)
end
instance_variable_set("@#{resources}", posts)
end

View File

@ -44,18 +44,6 @@ class Member < ActiveRecord::Base
member_uid
end
def remove_all_posts_from_index
Post.with_member.where("members.id = ?", self.id).find_each do |post|
post.delete_document
end
end
def add_all_posts_to_index
Post.with_member.where("members.id = ?", self.id).find_each do |post|
post.update_or_delete_document(self)
end
end
def assign_registration_number
self.member_uid ||= organization.next_reg_number_seq
end

View File

@ -1,40 +1,16 @@
require 'elasticsearch/model'
class Post < ActiveRecord::Base
include Taggable
include PgSearch
# Elasticsearch::Model doesn't work well with STI, so
# include it in subclasses directly.
def self.inherited(child)
super
child.instance_eval do
include Elasticsearch::Model
after_commit :index_document, on: :create
after_commit :update_or_delete_document, on: :update
after_commit :delete_document, on: :destroy
settings(
analysis: {
analyzer: {
normal: {
tokenizer: "standard",
# lowercase, unaccent
filter: %w[lowercase asciifolding]
}
}
}
) do
mapping do
indexes :title, analyzer: "normal"
indexes :description, analyzer: "normal"
indexes :tags
indexes :organization_id, type: :integer
end
end
end
end
pg_search_scope :search_by_query,
against: [:title, :description, :tags],
ignoring: :accents,
using: {
tsearch: {
prefix: true,
tsvector_column: 'tsv'
}
}
attr_reader :member_id
@ -72,30 +48,6 @@ class Post < ActiveRecord::Base
validates :category, presence: true
validates :title, presence: true
def index_document
__elasticsearch__.index_document
end
# pass member when doing bulk things
def update_or_delete_document(member = nil)
member ||= self.member
if active && member.try(:active)
begin
__elasticsearch__.update_document
rescue # document was not in the index. TODO: more specifi exception class
__elasticsearch__.index_document
end
else
__elasticsearch__.delete_document
end
rescue # document was not in the index. TODO: more specifi exception class
end
def delete_document
__elasticsearch__.delete_document
rescue # document was not in the index. TODO: more specifi exception class
end
def as_indexed_json(*)
as_json(only: [:title, :description, :tags, :organization_id])
end

View File

@ -35,5 +35,9 @@ module Timeoverflow
# ActiveJob configuration
config.active_job.queue_adapter = :sidekiq
# Use db/structure.sql with SQL as schema format
# This is needed to store in the schema SQL statements not covered by the ORM
config.active_record.schema_format = :sql
end
end

View File

@ -1,3 +0,0 @@
if ENV["ELASTICSEARCH_URL"].present?
Elasticsearch::Model.client = Elasticsearch::Client.new host: ENV["ELASTICSEARCH_URL"]
end

View File

@ -0,0 +1,32 @@
class AddTsvectorColumnToPost < ActiveRecord::Migration
def up
execute <<-SQL
ALTER TABLE posts ADD COLUMN tsv tsvector;
CREATE FUNCTION posts_trigger() RETURNS trigger AS $$
begin
new.tsv :=
to_tsvector('simple', unaccent(coalesce(new.title::text, ''))) ||
to_tsvector('simple', unaccent(coalesce(new.description::text, ''))) ||
to_tsvector('simple', unaccent(coalesce(new.tags::text, '')));
return new;
end
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON posts FOR EACH ROW EXECUTE PROCEDURE posts_trigger();
SQL
add_index :posts, :tsv, using: "gin"
end
def down
execute <<-SQL
DROP TRIGGER tsvectorupdate ON posts;
DROP FUNCTION posts_trigger();
SQL
remove_index :posts, :tsv
remove_column :posts, :tsv
end
end

View File

@ -0,0 +1,9 @@
class EnableUnaccentExtension < ActiveRecord::Migration
def up
enable_extension "unaccent"
end
def down
disable_extension "unaccent"
end
end

View File

@ -1,221 +0,0 @@
# encoding: UTF-8
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190412163011) do
# These are extensions that must be enabled in order to support this database
enable_extension "hstore"
create_table "accounts", force: :cascade do |t|
t.integer "accountable_id"
t.string "accountable_type"
t.integer "balance", default: 0
t.integer "max_allowed_balance"
t.integer "min_allowed_balance"
t.boolean "flagged"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "organization_id"
end
add_index "accounts", ["accountable_type", "accountable_id"], name: "index_accounts_on_accountable_type_and_accountable_id", using: :btree
add_index "accounts", ["organization_id"], name: "index_accounts_on_organization_id", using: :btree
create_table "active_admin_comments", force: :cascade do |t|
t.string "namespace"
t.text "body"
t.string "resource_id", null: false
t.string "resource_type", null: false
t.integer "author_id"
t.string "author_type"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "active_admin_comments", ["author_type", "author_id"], name: "index_active_admin_comments_on_author_type_and_author_id", using: :btree
add_index "active_admin_comments", ["namespace"], name: "index_active_admin_comments_on_namespace", using: :btree
add_index "active_admin_comments", ["resource_type", "resource_id"], name: "index_active_admin_comments_on_resource_type_and_resource_id", using: :btree
create_table "categories", force: :cascade do |t|
t.datetime "created_at"
t.datetime "updated_at"
t.hstore "name_translations"
end
create_table "device_tokens", force: :cascade do |t|
t.integer "user_id", null: false
t.string "token", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "device_tokens", ["user_id", "token"], name: "index_device_tokens_on_user_id_and_token", unique: true, using: :btree
create_table "documents", force: :cascade do |t|
t.integer "documentable_id"
t.string "documentable_type"
t.text "title"
t.text "content"
t.string "label"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "documents", ["documentable_id", "documentable_type"], name: "index_documents_on_documentable_id_and_documentable_type", using: :btree
add_index "documents", ["label"], name: "index_documents_on_label", using: :btree
create_table "events", force: :cascade do |t|
t.integer "action", null: false
t.integer "post_id"
t.integer "member_id"
t.integer "transfer_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "events", ["member_id"], name: "index_events_on_member_id", where: "(member_id IS NOT NULL)", using: :btree
add_index "events", ["post_id"], name: "index_events_on_post_id", where: "(post_id IS NOT NULL)", using: :btree
add_index "events", ["transfer_id"], name: "index_events_on_transfer_id", where: "(transfer_id IS NOT NULL)", using: :btree
create_table "members", force: :cascade do |t|
t.integer "user_id"
t.integer "organization_id"
t.boolean "manager"
t.datetime "created_at"
t.datetime "updated_at"
t.date "entry_date"
t.integer "member_uid"
t.boolean "active", default: true
end
add_index "members", ["organization_id"], name: "index_members_on_organization_id", using: :btree
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
create_table "movements", force: :cascade do |t|
t.integer "account_id"
t.integer "transfer_id"
t.integer "amount"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "movements", ["account_id"], name: "index_movements_on_account_id", using: :btree
add_index "movements", ["transfer_id"], name: "index_movements_on_transfer_id", using: :btree
create_table "organizations", force: :cascade do |t|
t.string "name", limit: 255, null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "reg_number_seq"
t.string "theme"
t.string "email"
t.string "phone"
t.string "web"
t.text "public_opening_times"
t.text "description"
t.text "address"
t.string "neighborhood"
t.string "city"
t.string "domain"
end
add_index "organizations", ["name"], name: "index_organizations_on_name", unique: true, using: :btree
create_table "posts", force: :cascade do |t|
t.string "title"
t.string "type"
t.integer "category_id"
t.integer "user_id"
t.text "description"
t.date "start_on"
t.date "end_on"
t.datetime "created_at"
t.datetime "updated_at"
t.text "tags", array: true
t.integer "organization_id"
t.boolean "active", default: true
t.boolean "is_group", default: false
end
add_index "posts", ["category_id"], name: "index_posts_on_category_id", using: :btree
add_index "posts", ["organization_id"], name: "index_posts_on_organization_id", using: :btree
add_index "posts", ["tags"], name: "index_posts_on_tags", using: :gin
add_index "posts", ["user_id"], name: "index_posts_on_user_id", using: :btree
create_table "push_notifications", force: :cascade do |t|
t.integer "event_id", null: false
t.integer "device_token_id", null: false
t.datetime "processed_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "title", default: "", null: false
t.string "body", default: "", null: false
t.json "data", default: {}, null: false
end
create_table "transfers", force: :cascade do |t|
t.integer "post_id"
t.text "reason"
t.integer "operator_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "transfers", ["operator_id"], name: "index_transfers_on_operator_id", using: :btree
add_index "transfers", ["post_id"], name: "index_transfers_on_post_id", using: :btree
create_table "users", force: :cascade do |t|
t.string "username", null: false
t.string "email", null: false
t.date "date_of_birth"
t.string "identity_document"
t.string "phone"
t.string "alt_phone"
t.text "address"
t.datetime "created_at"
t.datetime "updated_at"
t.datetime "deleted_at"
t.string "gender"
t.text "description"
t.boolean "active", default: true
t.datetime "terms_accepted_at"
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.integer "failed_attempts", default: 0
t.string "unlock_token"
t.datetime "locked_at"
t.string "locale", default: "es"
t.boolean "notifications", default: true
t.boolean "push_notifications", default: true, null: false
end
add_index "users", ["email"], name: "index_users_on_email", using: :btree
add_foreign_key "accounts", "organizations"
add_foreign_key "events", "members", name: "events_member_id_fkey"
add_foreign_key "events", "posts", name: "events_post_id_fkey"
add_foreign_key "events", "transfers", name: "events_transfer_id_fkey"
add_foreign_key "push_notifications", "device_tokens"
add_foreign_key "push_notifications", "events"
end

1141
db/structure.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
require 'elasticsearch/rails/tasks/import'

View File

@ -12,46 +12,111 @@ RSpec.describe OffersController, type: :controller do
organization: organization,
category: test_category)
end
let!(:other_offer) do
Fabricate(:offer,
user: another_member.user,
organization: organization,
category: test_category)
end
describe "GET #index" do
context "with a logged user" do
it "populates an array of offers" do
login(another_member.user)
get "index"
expect(assigns(:offers)).to eq([offer])
get :index
expect(assigns(:offers)).to eq([other_offer, offer])
end
context "when one offer is not active" do
before do
other_offer.active = false
other_offer.save!
end
it "only returns active offers" do
login(another_member.user)
get :index
expect(assigns(:offers)).to eq([offer])
end
end
context "when one offer's user is not active" do
before do
member.active = false
member.save!
end
it "only returns offers from active users" do
login(another_member.user)
get :index
expect(assigns(:offers)).to eq([other_offer])
end
end
end
context "with another organization" do
it "skips the original org's offers" do
login(yet_another_member.user)
get "index"
get :index
expect(assigns(:offers)).to eq([])
end
end
end
describe "GET #index (search)" do
before { login(another_member.user) }
before do
Offer.__elasticsearch__.create_index!(force: true)
# Import any already existing model into the index
# for instance the ones that have been created in upper
# `let!` or `before` blocks
Offer.__elasticsearch__.import(force: true, refresh: true)
offer.title = "Queridos compañeros"
offer.save!
end
it "populates an array of offers" do
login(another_member.user)
get :index, q: 'compañeros'
get "index", q: offer.title.split(/\s/).first
expect(assigns(:offers)).to eq([offer])
end
# @offers is a wrapper from Elasticsearch. It's iterator-equivalent to
# the underlying query from the database.
expect(assigns(:offers)).to be_a Elasticsearch::Model::Response::Records
expect(assigns(:offers).size).to eq 1
expect(assigns(:offers)[0]).to eq offer
expect(assigns(:offers).to_a).to eq([offer])
it "allows to search by partial word" do
get :index, q: 'compañ'
expect(assigns(:offers)).to eq([offer])
end
context "when one offer is not active" do
before do
other_offer.active = false
other_offer.save!
end
it "only returns active offers" do
login(another_member.user)
get :index
expect(assigns(:offers)).to eq([offer])
end
end
context "when one offer's user is not active" do
before do
member.active = false
member.save!
end
it "only returns offers from active users" do
login(another_member.user)
get :index
expect(assigns(:offers)).to eq([other_offer])
end
end
end