Merge pull request #2206 from consul/related-contents-score

Related contents score
This commit is contained in:
BertoCQ
2017-12-20 15:13:12 +01:00
committed by GitHub
25 changed files with 267 additions and 123 deletions

View File

@@ -2424,11 +2424,28 @@ table {
li {
border-bottom: 1px solid $border;
padding: $line-height / 4;
margin-bottom: 0 !important;
padding: $line-height / 2;
&:first-child {
border-top: 1px solid $border;
}
@include breakpoint(medium) {
.score-actions {
display: none;
float: right;
}
}
&:hover {
background: #f9f9f9;
.score-actions {
display: block;
}
}
}
h3 {
@@ -2436,7 +2453,7 @@ table {
font-weight: normal;
}
span:not(.icon-flag) {
.related-content-title {
color: #4f4f4f;
font-size: rem-calc(12);
text-transform: uppercase;
@@ -2447,6 +2464,47 @@ table {
}
}
.relate-content-score {
display: block;
@include breakpoint(medium) {
text-align: center;
}
a {
font-weight: bold;
margin-right: $line-height;
padding-left: rem-calc(20);
position: relative;
text-decoration: none;
&.score-positive:before,
&.score-negative:before {
font-family: 'icons';
left: 0;
position: absolute;
}
&.score-positive {
color: $color-success;
&:before {
color: $color-success;
content: '\6c';
}
}
&.score-negative {
color: $color-alert;
&:before {
color: $color-alert;
content: '\76';
}
}
}
}
// 22. Images
// -----------------

View File

@@ -1,13 +1,11 @@
class RelatedContentsController < ApplicationController
VALID_URL = /#{Setting['url']}\/.*\/.*/
skip_authorization_check
respond_to :html, :js
def create
if relationable_object && related_object
@relationable.relate_content(@related)
RelatedContent.create(parent_relationable: @relationable, child_relationable: @related, author: current_user)
flash[:success] = t('related_content.success')
else
@@ -17,28 +15,25 @@ class RelatedContentsController < ApplicationController
redirect_to @relationable
end
def flag
@related = RelatedContent.find_by(id: params[:id])
Flag.flag(current_user, @related)
Flag.flag(current_user, @related.opposite_related_content)
render template: 'relationable/_refresh_flag_actions'
def score_positive
score(:positive)
end
def unflag
@related = RelatedContent.find_by(id: params[:id])
Flag.unflag(current_user, @related)
Flag.unflag(current_user, @related.opposite_related_content)
render template: 'relationable/_refresh_flag_actions'
def score_negative
score(:negative)
end
private
def score(action)
@related = RelatedContent.find_by(id: params[:id])
@related.send("score_#{action}", current_user)
render template: 'relationable/_refresh_score_actions'
end
def valid_url?
params[:url].match(VALID_URL)
params[:url].start_with?(Setting['url'])
end
def relationable_object

View File

@@ -21,8 +21,6 @@ module FlagsHelper
def own_flaggable?(flaggable)
if flaggable.is_a? Comment
flaggable.user_id == current_user.id
elsif flaggable.is_a? RelatedContent
false
else
flaggable.author_id == current_user.id
end

View File

@@ -5,19 +5,11 @@ module Relationable
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)
def find_related_content(relationable)
RelatedContent.where(parent_relationable: self, child_relationable: relationable).first
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!(:flags_count)
related_content.opposite_related_content.increment!(:flags_count)
end
end
end

View File

@@ -1,12 +1,15 @@
class RelatedContent < ActiveRecord::Base
include Flaggable
RELATED_CONTENTS_REPORT_THRESHOLD = Setting['related_contents_report_threshold'].to_i
RELATED_CONTENT_SCORE_THRESHOLD = Setting['related_content_score_threshold'].to_f
RELATIONABLE_MODELS = %w{proposals debates}.freeze
acts_as_paranoid column: :hidden_at
include ActsAsParanoidAliases
belongs_to :author, class_name: 'User', foreign_key: 'author_id'
belongs_to :parent_relationable, polymorphic: true, touch: true
belongs_to :child_relationable, polymorphic: true, touch: true
has_one :opposite_related_content, class_name: 'RelatedContent', foreign_key: :related_content_id
has_many :related_content_scores
validates :parent_relationable_id, presence: true
validates :parent_relationable_type, presence: true
@@ -15,22 +18,46 @@ class RelatedContent < ActiveRecord::Base
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? }
after_create :create_author_score
scope :not_hidden, -> { where('flags_count <= ?', RELATED_CONTENTS_REPORT_THRESHOLD) }
scope :not_hidden, -> { where(hidden_at: nil) }
def hidden_by_reports?
flags_count > RELATED_CONTENTS_REPORT_THRESHOLD
def score_positive(user)
score(RelatedContentScore::SCORES[:POSITIVE], user)
end
def score_negative(user)
score(RelatedContentScore::SCORES[:NEGATIVE], user)
end
def scored_by_user?(user)
related_content_scores.exists?(user: user)
end
private
def create_opposite_related_content
related_content = RelatedContent.create!(opposite_related_content: self, parent_relationable: child_relationable, child_relationable: parent_relationable)
related_content = RelatedContent.create!(opposite_related_content: self, parent_relationable: child_relationable,
child_relationable: parent_relationable, author: author)
self.opposite_related_content = related_content
end
def destroy_opposite_related_content
opposite_related_content.destroy
def score(value, user)
score_with_opposite(value, user)
hide_with_opposite if (related_content_scores.sum(:value) / related_content_scores_count) < RELATED_CONTENT_SCORE_THRESHOLD
end
def hide_with_opposite
hide
opposite_related_content.hide
end
def create_author_score
score_positive(author)
end
def score_with_opposite(value, user)
RelatedContentScore.create(user: user, related_content: self, value: value)
RelatedContentScore.create(user: user, related_content: opposite_related_content, value: value)
end
end

View File

@@ -0,0 +1,13 @@
class RelatedContentScore < ActiveRecord::Base
SCORES = {
POSITIVE: 1,
NEGATIVE: -1
}.freeze
belongs_to :related_content, touch: true, counter_cache: :related_content_scores_count
belongs_to :user
validates :user, presence: true
validates :related_content, presence: true
validates :related_content_id, uniqueness: { scope: [:user_id] }
end

View File

@@ -1,19 +0,0 @@
<span class="flag-content">
<% if show_flag_action? related %>
<a id="flag-expand-related-<%= related.id %>" data-toggle="flag-drop-related-<%= related.id %>" title="<%= t('shared.flag') %>" class="float-right flag">
<span class="icon-flag flag-disable"></span>
</a>
<span class="dropdown-pane" id="flag-drop-related-<%= related.id %>" data-dropdown data-auto-focus="true">
<%= link_to t('shared.flag'), flag_related_content_path(related), method: :put, remote: true, id: "flag-related-#{ related.id }" %>
</span>
<% end %>
<% if show_unflag_action? related %>
<a id="unflag-expand-related-<%= related.id %>" data-toggle="unflag-drop-related-<%= related.id %>" title="<%= t('shared.unflag') %>" class="float-right flag">
<span class="icon-flag flag-active"></span>
</a>
<span class="dropdown-pane" id="unflag-drop-related-<%= related.id %>" data-dropdown data-auto-focus="true">
<%= link_to t('shared.unflag'), unflag_related_content_path(related), method: :put, remote: true, id: "unflag-related-#{ related.id }" %>
</span>
<% end %>
</span>

View File

@@ -1 +0,0 @@
$("#<%= dom_id(@related) %>.js-flag-actions").html('<%= j render("relationable/flag_actions", related: @related) %>');

View File

@@ -0,0 +1 @@
$("#<%= dom_id(@related) %>.js-score-actions").html('');

View File

@@ -4,11 +4,13 @@
<h2 class="inline-block">
<%= t("related_content.title") %>&nbsp;<span>(<%= relationable.relationed_contents.count %>)</span>
</h2>
<a>
<button type="button" data-toggle="related_content" class="add-related-content" id="add-related-content">
<%= t("related_content.add") %>
</button>
</a>
<% if current_user %>
<a>
<button type="button" data-toggle="related_content" class="add-related-content" id="add-related-content">
<%= t("related_content.add") %>
</button>
</a>
<% end %>
</div>
<%= render 'relationable/form', relationable: relationable %>

View File

@@ -1,11 +1,14 @@
<ul class="related-content-list" id="related-content-list">
<% @related_contents.each do |related| %>
<li>
<span id="<%= dom_id(related.relate_content(relationable)) %>" class="js-flag-actions">
<%= render 'relationable/flag_actions', related: related.relate_content(relationable) %>
</span>
<li id="related-content-<%= related.find_related_content(relationable).id %>">
<% related_content = related.find_related_content(relationable) %>
<% if current_user && related_content.author != current_user && !related_content.scored_by_user?(current_user)%>
<span id="<%= dom_id(related.find_related_content(relationable)) %>" class="js-score-actions score-actions">
<%= render 'relationable/score', related: related_content %>
</span>
<% end %>
<span><%= t("related_content.content_title.#{related.class.name.downcase}") %></span><br>
<span class="related-content-title"><%= t("related_content.content_title.#{related.class.name.downcase}") %></span><br>
<h3 class="inline-block">
<%= link_to related.title, eval("#{related.class.name.downcase}_path(related)") %>
</h3>

View File

@@ -0,0 +1,17 @@
<small><%= t("related_content.is_related") %></small>
<span class="relate-content-score">
<%= link_to t("related_content.score_positive"),
score_positive_related_content_path(related),
method: :put,
remote: true,
id: "score-positive-related-#{ related.id }",
class: "score-positive" %>
<%= link_to t("related_content.score_negative"),
score_negative_related_content_path(related),
method: :put,
remote: true,
id: "score-negative-related-#{ related.id }",
class: "score-negative" %>
</span>

View File

@@ -820,6 +820,9 @@ en:
submit: "Add"
error: "Link not valid. Remember to start with %{url}."
success: "You added a new related content"
is_related: "¿Is it related content?"
score_positive: "Yes"
score_negative: "No"
content_title:
proposal: "Proposal"
debate: "Debate"

View File

@@ -819,6 +819,9 @@ es:
submit: "Añadir"
error: "Enlace no válido. Recuerda que debe empezar por %{url}."
success: "Has añadido un nuevo contenido relacionado"
is_related: "¿Es contenido relacionado?"
score_positive: "Sí"
score_negative: "No"
content_title:
proposal: "Propuesta"
debate: "Debate"

View File

@@ -464,8 +464,8 @@ Rails.application.routes.draw do
resources :related_contents, only: [:create] do
member do
put :flag
put :unflag
put :score_positive
put :score_negative
end
end

View File

@@ -67,7 +67,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)
Setting.create(key: 'related_content_score_threshold', value: -0.3)
end
section "Creating Geozones" do

View File

@@ -0,0 +1,5 @@
class RemoveRelatedContentsFlagsCount < ActiveRecord::Migration
def change
remove_column :related_contents, :flags_count
end
end

View File

@@ -0,0 +1,11 @@
class CreateRelatedContentScores < ActiveRecord::Migration
def change
create_table :related_content_scores do |t|
t.references :user, index: true, foreign_key: true
t.references :related_content, index: true, foreign_key: true
t.integer :value
end
add_index :related_content_scores, [:user_id, :related_content_id], name: "unique_user_related_content_scoring", unique: true, using: :btree
end
end

View File

@@ -0,0 +1,6 @@
class AddHiddenAtToRelatedContents < ActiveRecord::Migration
def change
add_column :related_contents, :hidden_at, :datetime
add_index :related_contents, :hidden_at
end
end

View File

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

View File

@@ -0,0 +1,5 @@
class AddAuthorToRelatedContent < ActiveRecord::Migration
def change
add_column :related_contents, :author_id, :integer
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: 20171215152244) do
ActiveRecord::Schema.define(version: 20171220010000) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -854,6 +854,16 @@ ActiveRecord::Schema.define(version: 20171215152244) 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_content_scores", force: :cascade do |t|
t.integer "user_id"
t.integer "related_content_id"
t.integer "value"
end
add_index "related_content_scores", ["related_content_id"], name: "index_related_content_scores_on_related_content_id", using: :btree
add_index "related_content_scores", ["user_id", "related_content_id"], name: "unique_user_related_content_scoring", unique: true, using: :btree
add_index "related_content_scores", ["user_id"], name: "index_related_content_scores_on_user_id", using: :btree
create_table "related_contents", force: :cascade do |t|
t.integer "parent_relationable_id"
t.string "parent_relationable_type"
@@ -862,10 +872,13 @@ ActiveRecord::Schema.define(version: 20171215152244) do
t.integer "related_content_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "flags_count", default: 0
t.datetime "hidden_at"
t.integer "related_content_scores_count", default: 0
t.integer "author_id"
end
add_index "related_contents", ["child_relationable_type", "child_relationable_id"], name: "index_related_contents_on_child_relationable", using: :btree
add_index "related_contents", ["hidden_at"], name: "index_related_contents_on_hidden_at", 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
@@ -1189,6 +1202,8 @@ ActiveRecord::Schema.define(version: 20171215152244) do
add_foreign_key "poll_recounts", "poll_officer_assignments", column: "officer_assignment_id"
add_foreign_key "poll_voters", "polls"
add_foreign_key "proposals", "communities"
add_foreign_key "related_content_scores", "related_contents"
add_foreign_key "related_content_scores", "users"
add_foreign_key "users", "geozones"
add_foreign_key "valuators", "users"
end

View File

@@ -120,4 +120,4 @@ Setting['map_longitude'] = 0.0
Setting['map_zoom'] = 10
# Related content
Setting['related_contents_report_threshold'] = 5
Setting['related_content_score_threshold'] = -0.3

View File

@@ -18,7 +18,7 @@ describe RelatedContent do
end
it "should not allow repeated related contents" do
related_content = create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)
related_content = create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable, author: build(:user))
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
@@ -26,7 +26,7 @@ describe RelatedContent do
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) }
let(:related_content) { build(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable, author: build(:user)) }
it 'creates an opposite related_content' do
expect { related_content.save }.to change { RelatedContent.count }.by(2)
@@ -38,32 +38,18 @@ describe RelatedContent do
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 flags_count 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.flags_count).to eq(1)
expect(related_content.reload.opposite_related_content.flags_count).to eq(1)
end
end
describe '#relationed_contents' do
before do
create(:related_content, parent_relationable: parent_relationable, child_relationable: create(:proposal), flags_count: 6)
create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)
related_content = create(:related_content, parent_relationable: parent_relationable, child_relationable: create(:proposal), author: build(:user))
create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable, author: build(:user))
2.times do
related_content.send("score_positive", build(:user))
end
6.times do
related_content.send("score_negative", build(:user))
end
end
it 'returns not hidden by reports related contents' do

View File

@@ -6,7 +6,7 @@ shared_examples "relationable" do |relationable_model_name|
let(:user) { create(:user) }
scenario 'related contents are listed' do
related_content = create(:related_content, parent_relationable: relationable, child_relationable: related1)
related_content = create(:related_content, parent_relationable: relationable, child_relationable: related1, author: build(:user))
visit eval("#{relationable.class.name.downcase}_path(relationable)")
within("#related-content-list") do
@@ -25,6 +25,7 @@ shared_examples "relationable" do |relationable_model_name|
end
scenario 'related contents can be added' do
login_as(user)
visit eval("#{relationable.class.name.downcase}_path(relationable)")
expect(page).to have_selector('#related_content', visible: false)
@@ -57,6 +58,7 @@ shared_examples "relationable" do |relationable_model_name|
end
scenario 'if related content URL is invalid returns error' do
login_as(user)
visit eval("#{relationable.class.name.downcase}_path(relationable)")
click_on("Add related content")
@@ -69,37 +71,54 @@ shared_examples "relationable" do |relationable_model_name|
expect(page).to have_content("Link not valid. Remember to start with #{Setting[:url]}.")
end
scenario 'related content can be flagged', :js do
related_content = create(:related_content, parent_relationable: relationable, child_relationable: related1)
scenario 'related content can be scored positively', :js do
related_content = create(:related_content, parent_relationable: relationable, child_relationable: related1, author: build(:user))
login_as(user)
visit eval("#{relationable.class.name.downcase}_path(relationable)")
within("#related-content-list") do
expect(page).to have_css("#flag-expand-related-#{related_content.opposite_related_content.id}")
find("#flag-expand-related-#{related_content.opposite_related_content.id}").click
expect(page).to have_css("#flag-drop-related-#{related_content.opposite_related_content.id}", visible: true)
click_link("flag-related-#{related_content.opposite_related_content.id}")
expect(page).to have_css("#unflag-expand-related-#{related_content.opposite_related_content.id}")
find("#related-content-#{related_content.opposite_related_content.id}").hover
find("#score-positive-related-#{related_content.opposite_related_content.id}").click
expect(page).to_not have_css("#score-positive-related-#{related_content.opposite_related_content.id}")
end
expect(related_content.reload.flags_count).to eq(1)
expect(related_content.opposite_related_content.flags_count).to eq(1)
expect(related_content.related_content_scores.find_by(user_id: user.id, related_content_id: related_content.id).value).to eq(1)
expect(related_content.opposite_related_content.related_content_scores.find_by(user_id: user.id, related_content_id: related_content.opposite_related_content.id).value).to eq(1)
end
scenario 'if related content has been flagged more than 5 times it will be hidden', :js do
related_content = create(:related_content, parent_relationable: relationable, child_relationable: related1)
related_content.flags_count = Setting['related_contents_report_threshold'].to_i + 1
related_content.opposite_related_content.flags_count = related_content.flags_count
related_content.save
related_content.opposite_related_content.save
scenario 'related content can be scored negatively', :js do
related_content = create(:related_content, parent_relationable: relationable, child_relationable: related1, author: build(:user))
login_as(user)
visit eval("#{relationable.class.name.downcase}_path(relationable)")
within("#related-content-list") do
find("#related-content-#{related_content.opposite_related_content.id}").hover
find("#score-negative-related-#{related_content.opposite_related_content.id}").click
expect(page).to_not have_css("#score-negative-related-#{related_content.opposite_related_content.id}")
end
expect(related_content.related_content_scores.find_by(user_id: user.id, related_content_id: related_content.id).value).to eq(-1)
expect(related_content.opposite_related_content.related_content_scores.find_by(user_id: user.id, related_content_id: related_content.opposite_related_content.id).value).to eq(-1)
end
scenario 'if related content has negative score it will be hidden' do
related_content = create(:related_content, parent_relationable: relationable, child_relationable: related1, author: build(:user))
2.times do
related_content.send("score_positive", build(:user))
end
6.times do
related_content.send("score_negative", build(:user))
end
login_as(user)
visit eval("#{relationable.class.name.downcase}_path(relationable)")
expect(page).to_not have_css("#related-content-list")
end
end