Merge pull request #2160 from consul/relationables

Related Content model and Relatable concern
This commit is contained in:
María Checa
2017-12-05 15:19:42 +01:00
committed by GitHub
12 changed files with 176 additions and 1 deletions

View File

@@ -17,6 +17,7 @@ class Budget
acts_as_votable
acts_as_paranoid column: :hidden_at
include ActsAsParanoidAliases
include Relationable
belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
belongs_to :heading

View File

@@ -0,0 +1,23 @@
module Relationable
extend ActiveSupport::Concern
included do
has_many :related_contents, as: :parent_relationable, dependent: :destroy
end
def relate_content(relationable)
RelatedContent.find_or_create_by(parent_relationable: self, child_relationable: relationable)
end
def relationed_contents
related_contents.not_hidden.map { |related_content| related_content.child_relationable }
end
def report_related_content(relationable)
related_content = related_contents.find_by(child_relationable: relationable)
if related_content.present?
related_content.increment!(:times_reported)
related_content.opposite_related_content.increment!(:times_reported)
end
end
end

View File

@@ -9,6 +9,7 @@ class Debate < ActiveRecord::Base
include Filterable
include HasPublicAuthor
include Graphqlable
include Relationable
acts_as_votable
acts_as_paranoid column: :hidden_at

View File

@@ -17,6 +17,7 @@ class Proposal < ActiveRecord::Base
max_file_size: 3.megabytes,
accepted_content_types: [ "application/pdf" ]
include EmbedVideosHelper
include Relationable
acts_as_votable
acts_as_paranoid column: :hidden_at

View File

@@ -0,0 +1,33 @@
class RelatedContent < ActiveRecord::Base
RELATED_CONTENTS_REPORT_THRESHOLD = Setting['related_contents_report_threshold'].to_i
belongs_to :parent_relationable, polymorphic: true
belongs_to :child_relationable, polymorphic: true
has_one :opposite_related_content, class_name: 'RelatedContent', foreign_key: :related_content_id
validates :parent_relationable_id, presence: true
validates :parent_relationable_type, presence: true
validates :child_relationable_id, presence: true
validates :child_relationable_type, presence: true
validates :parent_relationable_id, uniqueness: { scope: [:parent_relationable_type, :child_relationable_id, :child_relationable_type] }
after_create :create_opposite_related_content, unless: proc { opposite_related_content.present? }
after_destroy :destroy_opposite_related_content, if: proc { opposite_related_content.present? }
scope :not_hidden, -> { where('times_reported <= ?', RELATED_CONTENTS_REPORT_THRESHOLD) }
def hidden_by_reports?
times_reported > RELATED_CONTENTS_REPORT_THRESHOLD
end
private
def create_opposite_related_content
related_content = RelatedContent.create!(opposite_related_content: self, parent_relationable: child_relationable, child_relationable: parent_relationable)
self.opposite_related_content = related_content
end
def destroy_opposite_related_content
opposite_related_content.destroy
end
end

View File

@@ -65,6 +65,7 @@ section "Creating Settings" do
Setting.create(key: 'map_latitude', value: 51.48)
Setting.create(key: 'map_longitude', value: 0.0)
Setting.create(key: 'map_zoom', value: 10)
Setting.create(key: 'related_contents_report_threshold', value: 2)
end
section "Creating Geozones" do

View File

@@ -0,0 +1,12 @@
class CreateRelatedContent < ActiveRecord::Migration
def change
create_table :related_contents do |t|
t.references :parent_relationable, polymorphic: true, index: { name: 'index_related_contents_on_parent_relationable' }
t.references :child_relationable, polymorphic: true, index: { name: 'index_related_contents_on_child_relationable' }
t.references :related_content, index: { name: 'opposite_related_content' }
t.timestamps
end
add_index :related_contents, [:parent_relationable_id, :parent_relationable_type, :child_relationable_id, :child_relationable_type], name: "unique_parent_child_related_content", unique: true, using: :btree
end
end

View File

@@ -0,0 +1,5 @@
class AddTimeReportedToRelatedContent < ActiveRecord::Migration
def change
add_column :related_contents, :times_reported, :integer, default: 0
end
end

View File

@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171115164152) do
ActiveRecord::Schema.define(version: 20171127230716) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -851,6 +851,22 @@ ActiveRecord::Schema.define(version: 20171115164152) do
add_index "proposals", ["title"], name: "index_proposals_on_title", using: :btree
add_index "proposals", ["tsv"], name: "index_proposals_on_tsv", using: :gin
create_table "related_contents", force: :cascade do |t|
t.integer "parent_relationable_id"
t.string "parent_relationable_type"
t.integer "child_relationable_id"
t.string "child_relationable_type"
t.integer "related_content_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "times_reported", default: 0
end
add_index "related_contents", ["child_relationable_type", "child_relationable_id"], name: "index_related_contents_on_child_relationable", using: :btree
add_index "related_contents", ["parent_relationable_id", "parent_relationable_type", "child_relationable_id", "child_relationable_type"], name: "unique_parent_child_related_content", unique: true, using: :btree
add_index "related_contents", ["parent_relationable_type", "parent_relationable_id"], name: "index_related_contents_on_parent_relationable", using: :btree
add_index "related_contents", ["related_content_id"], name: "opposite_related_content", using: :btree
create_table "settings", force: :cascade do |t|
t.string "key"
t.string "value"

View File

@@ -116,3 +116,6 @@ Setting['proposal_improvement_path'] = nil
Setting['map_latitude'] = 51.48
Setting['map_longitude'] = 0.0
Setting['map_zoom'] = 10
# Related content
Setting['related_contents_report_threshold'] = 5

View File

@@ -882,4 +882,7 @@ LOREM_IPSUM
end
end
factory :related_content do
end
end

View File

@@ -0,0 +1,76 @@
require 'rails_helper'
describe RelatedContent do
let(:parent_relationable) { create([:proposal, :debate, :budget_investment].sample) }
let(:child_relationable) { create([:proposal, :debate, :budget_investment].sample) }
it "should allow relationables from various classes" do
expect(build(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)).to be_valid
expect(build(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)).to be_valid
expect(build(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)).to be_valid
end
it "should not allow empty relationables" do
expect(build(:related_content)).not_to be_valid
expect(build(:related_content, parent_relationable: parent_relationable)).not_to be_valid
expect(build(:related_content, child_relationable: child_relationable)).not_to be_valid
end
it "should not allow repeated related contents" do
related_content = create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)
new_related_content = build(:related_content, parent_relationable: related_content.parent_relationable, child_relationable: related_content.child_relationable)
expect(new_related_content).not_to be_valid
end
describe 'create_opposite_related_content' do
let(:parent_relationable) { create(:proposal) }
let(:child_relationable) { create(:debate) }
let(:related_content) { build(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable) }
it 'creates an opposite related_content' do
expect { related_content.save }.to change { RelatedContent.count }.by(2)
expect(related_content.opposite_related_content.child_relationable_id).to eq(parent_relationable.id)
expect(related_content.opposite_related_content.child_relationable_type).to eq(parent_relationable.class.name)
expect(related_content.opposite_related_content.parent_relationable_id).to eq(child_relationable.id)
expect(related_content.opposite_related_content.parent_relationable_type).to eq(child_relationable.class.name)
expect(related_content.opposite_related_content.opposite_related_content.id).to eq(related_content.id)
end
end
describe 'relationable destroy' do
let(:parent_relationable) { create(:proposal) }
let(:child_relationable) { create(:debate) }
it 'destroys both related contents involved' do
related_content = create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)
expect { related_content.parent_relationable.destroy }.to change { RelatedContent.all.count }.by(-2)
expect(child_relationable.related_contents).to be_empty
end
end
# TODO: Move this into a Relationable shared context
describe '#report_related_content' do
it 'increments both relation and opposite relation times_reported counters' do
related_content = create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)
parent_relationable.report_related_content(child_relationable)
expect(related_content.reload.times_reported).to eq(1)
expect(related_content.reload.opposite_related_content.times_reported).to eq(1)
end
end
describe '#relationed_contents' do
before do
create(:related_content, parent_relationable: parent_relationable, child_relationable: create(:proposal), times_reported: 6)
create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)
end
it 'returns not hidden by reports related contents' do
expect(parent_relationable.relationed_contents.count).to eq(1)
expect(parent_relationable.relationed_contents.first.class.name).to eq(child_relationable.class.name)
expect(parent_relationable.relationed_contents.first.id).to eq(child_relationable.id)
end
end
end