Add experience level averaging to articles (#1922)
* Add experience level averaging to articles * Adjust copy on /mod ratings page
This commit is contained in:
parent
704b7bce9c
commit
532903c688
22 changed files with 355 additions and 13 deletions
|
|
@ -33,6 +33,7 @@ function insertNewArticles(user){
|
|||
var containsOrganizationID = findOne([article.organization_id], user.followed_organization_ids || [])
|
||||
var intersectedTags = intersect_arrays(user.followed_tag_names, article.cached_tag_list_array)
|
||||
var followedPoints = 1
|
||||
var experienceDifference = Math.abs(article['experience_level_rating'] - user.experience_level)
|
||||
JSON.parse(user.followed_tags).map(function(tag) {
|
||||
if (intersectedTags.includes(tag.name)) {
|
||||
followedPoints = followedPoints + tag.points
|
||||
|
|
@ -51,6 +52,7 @@ function insertNewArticles(user){
|
|||
} else if (rand < 0.6) {
|
||||
articlePoints = articlePoints + 2
|
||||
}
|
||||
articlePoints = articlePoints - (experienceDifference/2);
|
||||
article['points'] = articlePoints
|
||||
});
|
||||
var sortedArticles = articlesJSON.sort(function(a, b) {
|
||||
|
|
@ -70,12 +72,13 @@ function insertTopArticles(user){
|
|||
var articlesJSON = JSON.parse(el.dataset.articles)
|
||||
var insertPlace = document.getElementById("article-index-hidden-div");
|
||||
if (insertPlace) {
|
||||
articlesJSON.forEach(function(article){
|
||||
articlesJSON.forEach(function(article){
|
||||
var articlePoints = 0
|
||||
var containsUserID = findOne([article.user_id], user.followed_user_ids || [])
|
||||
var containsOrganizationID = findOne([article.organization_id], user.followed_organization_ids || [])
|
||||
var intersectedTags = intersect_arrays(user.followed_tag_names, article.cached_tag_list_array)
|
||||
var followedPoints = 1
|
||||
var experienceDifference = Math.abs(article['experience_level_rating'] - user.experience_level)
|
||||
JSON.parse(user.followed_tags).map(function(tag) {
|
||||
if (intersectedTags.includes(tag.name)) {
|
||||
followedPoints = followedPoints + tag.points
|
||||
|
|
@ -94,6 +97,7 @@ function insertTopArticles(user){
|
|||
} else if (rand < 0.6) {
|
||||
articlePoints = articlePoints + 2
|
||||
}
|
||||
articlePoints = articlePoints - (experienceDifference/2);
|
||||
article['points'] = articlePoints
|
||||
});
|
||||
var sortedArticles = articlesJSON.sort(function(a, b) {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ class AsyncInfoController < ApplicationController
|
|||
checked_code_of_conduct: @user.checked_code_of_conduct,
|
||||
number_of_comments: @user.comments.count,
|
||||
display_sponsors: @user.display_sponsors,
|
||||
trusted: @user.trusted
|
||||
trusted: @user.trusted,
|
||||
experience_level: @user.experience_level
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
class ModerationsController < ApplicationController
|
||||
after_action :verify_authorized
|
||||
|
||||
def index
|
||||
authorize(User, :moderation_routes?)
|
||||
@articles = Article.where(published: true).
|
||||
includes(:rating_votes).
|
||||
where("rating_votes_count < 3").
|
||||
where("score > -5").
|
||||
order("featured_number DESC").limit(50)
|
||||
if params[:tag].present?
|
||||
@articles = @articles.
|
||||
cached_tagged_with(params[:tag])
|
||||
end
|
||||
@articles = @articles.decorate
|
||||
end
|
||||
|
||||
def article
|
||||
authorize(User, :moderation_routes?)
|
||||
@moderatable = Article.find_by_slug(params[:slug])
|
||||
|
|
|
|||
24
app/controllers/rating_votes_controller.rb
Normal file
24
app/controllers/rating_votes_controller.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
class RatingVotesController < ApplicationController
|
||||
after_action :verify_authorized
|
||||
|
||||
def create
|
||||
authorize RatingVote
|
||||
rating_vote = RatingVote.where(user_id: current_user.id, article_id: rating_vote_params[:article_id]).first || RatingVote.new
|
||||
rating_vote.user_id = current_user.id
|
||||
rating_vote.article_id = rating_vote_params[:article_id]
|
||||
rating_vote.rating = rating_vote_params[:rating].to_f
|
||||
rating_vote.group = rating_vote_params[:group]
|
||||
if rating_vote.save
|
||||
rating_vote.assign_article_rating
|
||||
redirect_back(fallback_location: "/mod")
|
||||
else
|
||||
render json: { result: "Not Upserted Successfully" }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rating_vote_params
|
||||
params.require(:rating_vote).permit(policy(RatingVote).permitted_attributes)
|
||||
end
|
||||
end
|
||||
|
|
@ -106,7 +106,7 @@ class StoriesController < ApplicationController
|
|||
def handle_base_index
|
||||
@home_page = true
|
||||
@page = (params[:page] || 1).to_i
|
||||
num_articles = 25
|
||||
num_articles = 30
|
||||
@stories = article_finder(num_articles)
|
||||
add_param_context(:page, :timeframe)
|
||||
if ["week", "month", "year", "infinity"].include?(params[:timeframe])
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ class Article < ApplicationRecord
|
|||
belongs_to :collection, optional: true
|
||||
has_many :comments, as: :commentable
|
||||
has_many :buffer_updates
|
||||
has_many :notifications, as: :notifiable
|
||||
has_many :notifications, as: :notifiable
|
||||
has_many :rating_votes
|
||||
|
||||
validates :slug, presence: { if: :published? }, format: /\A[0-9a-z-]*\z/,
|
||||
uniqueness: { scope: :user_id }
|
||||
|
|
@ -76,6 +77,7 @@ class Article < ApplicationRecord
|
|||
:main_image, :main_image_background_hex_color, :updated_at, :slug,
|
||||
:video, :user_id, :organization_id, :video_source_url, :video_code,
|
||||
:video_thumbnail_url, :video_closed_caption_track_url,
|
||||
:experience_level_rating, :experience_level_rating_distribution,
|
||||
:published_at, :crossposted_at, :boost_states, :description, :reading_time, :video_duration_in_seconds)
|
||||
}
|
||||
|
||||
|
|
@ -137,8 +139,8 @@ class Article < ApplicationRecord
|
|||
per_environment: true,
|
||||
enqueue: :trigger_delayed_index do
|
||||
attributes :title, :path, :class_name, :comments_count, :reading_time,
|
||||
:tag_list, :positive_reactions_count, :id, :hotness_score, :score, :readable_publish_date,
|
||||
:flare_tag, :user_id, :organization_id, :cloudinary_video_url, :video_duration_in_minutes
|
||||
:tag_list, :positive_reactions_count, :id, :hotness_score, :score, :readable_publish_date, :flare_tag, :user_id,
|
||||
:organization_id, :cloudinary_video_url, :video_duration_in_minutes, :experience_level_rating, :experience_level_rating_distribution
|
||||
attribute :published_at_int do
|
||||
published_at.to_i
|
||||
end
|
||||
|
|
|
|||
27
app/models/rating_vote.rb
Normal file
27
app/models/rating_vote.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
class RatingVote < ApplicationRecord
|
||||
belongs_to :article
|
||||
belongs_to :user
|
||||
|
||||
validates_uniqueness_of :user_id, scope: :article_id
|
||||
validates :group, inclusion: { in: %w(experience_level) }
|
||||
validates :rating, numericality: { greater_than: 0.0, less_than_or_equal_to: 10.0 }
|
||||
validate :permissions
|
||||
counter_culture :article
|
||||
counter_culture :user
|
||||
|
||||
def assign_article_rating
|
||||
ratings = article.rating_votes.where(group: group).pluck(:rating)
|
||||
average = ratings.sum / ratings.size
|
||||
article.update_column(:experience_level_rating, average)
|
||||
article.update_column(:experience_level_rating_distribution, ratings.sort.max - ratings.sort.min)
|
||||
article.update_column(:last_experience_level_rating_at, Time.current)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permissions
|
||||
unless user&.trusted
|
||||
errors.add(:user_id, "is not permitted to take this action.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -35,6 +35,7 @@ class User < ApplicationRecord
|
|||
has_many :chat_channels, through: :chat_channel_memberships
|
||||
has_many :push_notification_subscriptions, dependent: :destroy
|
||||
has_many :feedback_messages
|
||||
has_many :rating_votes
|
||||
has_many :html_variants, dependent: :destroy
|
||||
has_many :mentor_relationships_as_mentee,
|
||||
class_name: "MentorRelationship", foreign_key: "mentee_id"
|
||||
|
|
@ -65,6 +66,7 @@ class User < ApplicationRecord
|
|||
exclusion: { in: ReservedWords.all, message: "username is reserved" }
|
||||
validates :twitter_username, uniqueness: { allow_blank: true }
|
||||
validates :github_username, uniqueness: { allow_blank: true }
|
||||
validates :experience_level, numericality: { less_than_or_equal_to: 10 }, allow_blank: true
|
||||
validates :text_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
|
||||
validates :bg_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
|
||||
validates :website_url, :employer_url, :mastodon_url,
|
||||
|
|
|
|||
9
app/policies/rating_vote_policy.rb
Normal file
9
app/policies/rating_vote_policy.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class RatingVotePolicy < ApplicationPolicy
|
||||
def create?
|
||||
!user_is_banned?
|
||||
end
|
||||
|
||||
def permitted_attributes
|
||||
%i[rating group article_id]
|
||||
end
|
||||
end
|
||||
|
|
@ -73,6 +73,7 @@ class UserPolicy < ApplicationPolicy
|
|||
employer_name
|
||||
employer_url
|
||||
employment_title
|
||||
experience_level
|
||||
facebook_url
|
||||
feed_admin_publish_permission
|
||||
feed_mark_canonical
|
||||
|
|
|
|||
62
app/views/moderations/index.html.erb
Normal file
62
app/views/moderations/index.html.erb
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<style>
|
||||
.level-rating-button {
|
||||
border-radius: 8px;
|
||||
font-size: 1.1em;
|
||||
padding: 3px;
|
||||
width: 42px;
|
||||
border: 2px solid black;
|
||||
font-weight: bold;
|
||||
}
|
||||
.level-rating-button.selected {
|
||||
background: #9bebff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<br /><br /><br /><br />
|
||||
<center>
|
||||
<h1>Mod Convenience Dashboard</h1>
|
||||
<h2>Add "Experience Level" ratings</h2>
|
||||
<h5>More mod tools will be added as they are created.</h5>
|
||||
</center>
|
||||
<div class="container">
|
||||
<% @articles.each do |article| %>
|
||||
<% @rating_vote = article.rating_votes.where(user_id: current_user.id).first %>
|
||||
<% unless @rating_vote %>
|
||||
<div style="border-bottom: 2px solid black; margin-bottom: 15px;">
|
||||
<center>
|
||||
<h1><%= article.title %></h1>
|
||||
<% cache("main-article-tags-#{article.cached_tag_list}", expires_in: 30.hours) do %>
|
||||
<div class="tags">
|
||||
<% article.cached_tag_list_array.each do |tag| %>
|
||||
<a class="tag" href="/t/<%= tag %>" style="background-color:<%= tag_colors(tag)[:background] %>;color:<%= tag_colors(tag)[:color] %>">#<%= tag %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</center>
|
||||
<div class="body">
|
||||
<%= HTML_Truncator.truncate(article.processed_html,
|
||||
200, ellipsis: '<a class="comment-read-more" href="' + article.path + '">... Read Entire Post</a>').html_safe %>
|
||||
</div>
|
||||
<center>
|
||||
<h2><a href="<%= article.path %>/mod">Moderate</a></h2>
|
||||
<h2>Experience Level Target:</h2>
|
||||
<%= form_for(RatingVote.new) do |f| %>
|
||||
<%= f.hidden_field :article_id, value: article.id %>
|
||||
<%= f.hidden_field :group, value: "experience_level" %>
|
||||
<button value="1" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 1.0 %>">1</button>
|
||||
<button value="2" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 2.0 %>">2</button>
|
||||
<button value="3" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 3.0 %>">3</button>
|
||||
<button value="4" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 4.0 %>">4</button>
|
||||
<button value="5" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 5.0 %>">5</button>
|
||||
<button value="6" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 6.0 %>">6</button>
|
||||
<button value="7" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 7.0 %>">7</button>
|
||||
<button value="8" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 8.0 %>">8</button>
|
||||
<button value="9" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 9.0 %>">9</button>
|
||||
<button value="10" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 10.0 %>">10</button>
|
||||
<% end %>
|
||||
<br /><br />
|
||||
</center>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -55,6 +55,17 @@
|
|||
height: 50px;
|
||||
border: 1px solid #888;
|
||||
}
|
||||
.level-rating-button {
|
||||
border-radius: 8px;
|
||||
font-size: 1.1em;
|
||||
padding: 3px;
|
||||
width: 42px;
|
||||
border: 2px solid black;
|
||||
font-weight: bold;
|
||||
}
|
||||
.level-rating-button.selected {
|
||||
background: #9bebff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container" style="text-align: center">
|
||||
|
|
@ -74,7 +85,7 @@
|
|||
<h1> ADMIN: <a href="/admin/comments/<%= @moderatable.id %>" data-no-instant>Comment Admin</a> | <a href="/admin/users/<%= @moderatable.user_id %>" data-no-instant>User Admin</a></h1>
|
||||
<% else %>
|
||||
<p style="width: 90%;margin:50px auto;text-align:left">
|
||||
<b style="font-size:1.3em">All negative reactions are 100% private.</b>
|
||||
<b style="font-size:1.3em">All negative reactions are private.</b>
|
||||
<br><br>Use the <b>thumbsdown(👎)</b> to move something "down" for any reason (quality, usefulness, code of conduct grey area).
|
||||
<br><br>The <b>vomit(🤢)</b> is for code of conduct violations (harassment, being an asshole, spam, etc.).
|
||||
<br><br>The DEV team will be notified about vomits and take appropriate action, but leaving a level-headed comment addressing the behavior is welcome if you feel up for it.
|
||||
|
|
@ -110,6 +121,31 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @moderatable.class.name == "Article" %>
|
||||
<% @rating_vote = RatingVote.where(article_id: @moderatable.id, user_id: current_user.id).first %>
|
||||
<%= form_for(RatingVote.new) do |f| %>
|
||||
<h2>Experience Level of Post</h2>
|
||||
<%= f.hidden_field :article_id, value: @moderatable.id %>
|
||||
<%= f.hidden_field :group, value: "experience_level" %>
|
||||
<button value="1" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 1.0 %>">1</button>
|
||||
<button value="2" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 2.0 %>">2</button>
|
||||
<button value="3" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 3.0 %>">3</button>
|
||||
<button value="4" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 4.0 %>">4</button>
|
||||
<button value="5" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 5.0 %>">5</button>
|
||||
<button value="6" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 6.0 %>">6</button>
|
||||
<button value="7" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 7.0 %>">7</button>
|
||||
<button value="8" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 8.0 %>">8</button>
|
||||
<button value="9" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 9.0 %>">9</button>
|
||||
<button value="10" name="rating_vote[rating]" class="level-rating-button <%= "selected" if @rating_vote&.rating == 10.0 %>">10</button>
|
||||
<% end %>
|
||||
<p style="width: 90%;margin:50px auto;text-align:left">
|
||||
<b style="font-size:1.1em">All rating votes are private.</b>
|
||||
<br><br>Provide a rating between 1 and 10 based on which audience would find this post most valuable.
|
||||
<br><br>1: for brand new developers, 3: for junior developers, 10: for very advanced developers, etc.
|
||||
<br><br>This will provide <em>approximate and gentle</em> algorithmic adjustments to help deliver relevant content.
|
||||
<br><br>It will be used as one of many indicators. No one rating will have an overly dramatic affect.
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<script defer>
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@
|
|||
decorate %>
|
||||
<div id="new-articles-object" data-articles="
|
||||
<%= @new_stories.to_json(
|
||||
only: %i[title path id user_id comments_count positive_reactions_count
|
||||
organization_id reading_time video_thumbnail_url video],
|
||||
only: %i[title path id user_id comments_count positive_reactions_count organization_id
|
||||
reading_time video_thumbnail_url video video_duration_in_minutes
|
||||
experience_level_rating experience_level_rating_distribution],
|
||||
methods: %i[readable_publish_date cached_tag_list_array flare_tag class_name
|
||||
cloudinary_video_url video_duration_in_minutes],
|
||||
include: {
|
||||
|
|
@ -22,7 +23,8 @@
|
|||
<div id="home-articles-object" data-articles="
|
||||
<%= @stories.to_json(
|
||||
only: %i[title path id user_id comments_count positive_reactions_count organization_id
|
||||
reading_time video_thumbnail_url video video_duration_in_minutes],
|
||||
reading_time video_thumbnail_url video video_duration_in_minutes
|
||||
experience_level_rating experience_level_rating_distribution],
|
||||
methods: %i[readable_publish_date cached_tag_list_array flare_tag class_name
|
||||
cloudinary_video_url video_duration_in_minutes],
|
||||
include: {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,19 @@
|
|||
<%= f.submit "SUBMIT", class: "cta" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2>Feed Customization</h2>
|
||||
<%= form_for(@user) do |f| %>
|
||||
<div class="sub-field">
|
||||
<%= f.label :experience_level, "Enter your coding experience level (1-10)" %>
|
||||
<%= f.text_field :experience_level %>
|
||||
<sub><em>This will not be displayed on your profile or anywhere publicly. It will help determine what content you are shown along with tags you follow etc.</em></sub>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label></label>
|
||||
<%= f.hidden_field :tab, value: @tab %>
|
||||
<%= f.submit "SUBMIT", class: "cta" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2>Languages</h2>
|
||||
<h3>Select which languages you'd prefer to see in your feed <span style="color:#e05252">(beta)</span></h3>
|
||||
<h4 style="font-weight:400">
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ Rails.application.routes.draw do
|
|||
resources :html_variant_successes, only: [:create]
|
||||
resources :push_notification_subscriptions, only: [:create]
|
||||
resources :tag_adjustments, only: [:create]
|
||||
resources :rating_votes, only: [:create]
|
||||
|
||||
get "/notifications/:filter" => "notifications#index"
|
||||
get "/notifications/:filter/:org_id" => "notifications#index"
|
||||
|
|
@ -236,6 +237,7 @@ Rails.application.routes.draw do
|
|||
get "/memberships", to: redirect("/membership")
|
||||
get "/shop", to: redirect("https://shop.dev.to/")
|
||||
get "/tag-moderation" => "pages#tag_moderation"
|
||||
get "/mod" => "moderations#index"
|
||||
|
||||
post "/fallback_activity_recorder" => "ga_events#create"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
class AddExperienceLevelToPosts < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
add_column :articles, :experience_level_rating, :float, default: 5.0
|
||||
add_column :articles, :experience_level_rating_distribution, :float, default: 5.0
|
||||
add_column :articles, :last_experience_level_rating_at, :datetime
|
||||
add_column :articles, :rating_votes_count, :integer, null: false, default: 0
|
||||
add_column :users, :rating_votes_count, :integer, null: false, default: 0
|
||||
end
|
||||
end
|
||||
13
db/migrate/20190227163803_create_rating_votes.rb
Normal file
13
db/migrate/20190227163803_create_rating_votes.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
class CreateRatingVotes < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :rating_votes do |t|
|
||||
t.bigint :user_id
|
||||
t.bigint :article_id
|
||||
t.string :group
|
||||
t.float :rating
|
||||
t.timestamps
|
||||
end
|
||||
add_index :rating_votes, :user_id
|
||||
add_index :rating_votes, :article_id
|
||||
end
|
||||
end
|
||||
18
db/schema.rb
18
db/schema.rb
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20190216185753) do
|
||||
ActiveRecord::Schema.define(version: 20190227163803) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
|
@ -69,6 +69,8 @@ ActiveRecord::Schema.define(version: 20190216185753) do
|
|||
t.string "description"
|
||||
t.datetime "edited_at"
|
||||
t.boolean "email_digest_eligible", default: true
|
||||
t.float "experience_level_rating", default: 5.0
|
||||
t.float "experience_level_rating_distribution", default: 5.0
|
||||
t.datetime "facebook_last_buffered"
|
||||
t.boolean "featured", default: false
|
||||
t.float "featured_clickthrough_rate", default: 0.0
|
||||
|
|
@ -81,6 +83,7 @@ ActiveRecord::Schema.define(version: 20190216185753) do
|
|||
t.string "language"
|
||||
t.datetime "last_buffered"
|
||||
t.datetime "last_comment_at", default: "2017-01-01 05:00:00"
|
||||
t.datetime "last_experience_level_rating_at"
|
||||
t.datetime "last_invoiced_at"
|
||||
t.decimal "lat", precision: 10, scale: 6
|
||||
t.boolean "live_now", default: false
|
||||
|
|
@ -100,6 +103,7 @@ ActiveRecord::Schema.define(version: 20190216185753) do
|
|||
t.boolean "published", default: false
|
||||
t.datetime "published_at"
|
||||
t.boolean "published_from_feed", default: false
|
||||
t.integer "rating_votes_count", default: 0, null: false
|
||||
t.integer "reactions_count", default: 0, null: false
|
||||
t.integer "reading_time", default: 0
|
||||
t.boolean "receive_notifications", default: true
|
||||
|
|
@ -573,6 +577,17 @@ ActiveRecord::Schema.define(version: 20190216185753) do
|
|||
t.index ["user_id"], name: "index_push_notification_subscriptions_on_user_id"
|
||||
end
|
||||
|
||||
create_table "rating_votes", force: :cascade do |t|
|
||||
t.bigint "article_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.string "group"
|
||||
t.float "rating"
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "user_id"
|
||||
t.index ["article_id"], name: "index_rating_votes_on_article_id"
|
||||
t.index ["user_id"], name: "index_rating_votes_on_user_id"
|
||||
end
|
||||
|
||||
create_table "reactions", id: :serial, force: :cascade do |t|
|
||||
t.string "category"
|
||||
t.datetime "created_at", null: false
|
||||
|
|
@ -797,6 +812,7 @@ ActiveRecord::Schema.define(version: 20190216185753) do
|
|||
t.datetime "personal_data_updated_at"
|
||||
t.string "profile_image"
|
||||
t.datetime "profile_updated_at", default: "2017-01-01 05:00:00"
|
||||
t.integer "rating_votes_count", default: 0, null: false
|
||||
t.integer "reactions_count", default: 0, null: false
|
||||
t.datetime "remember_created_at"
|
||||
t.string "remember_token"
|
||||
|
|
|
|||
6
spec/factories/rating_votes.rb
Normal file
6
spec/factories/rating_votes.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :rating_vote do
|
||||
group { "experience_level" }
|
||||
rating { rand(1.0..8.0) }
|
||||
end
|
||||
end
|
||||
56
spec/models/rating_vote_spec.rb
Normal file
56
spec/models/rating_vote_spec.rb
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe RatingVote, type: :model do
|
||||
let(:user) { create(:user, :trusted) }
|
||||
let(:user2) { create(:user, :trusted) }
|
||||
let(:user3) { create(:user, :trusted) }
|
||||
let(:article) { create(:article, user_id: user.id) }
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to validate_numericality_of(:rating).is_greater_than(0.0).is_less_than_or_equal_to(10.0) }
|
||||
it { is_expected.to validate_inclusion_of(:group).in_array(%w(experience_level)) }
|
||||
end
|
||||
|
||||
describe "uniqueness" do
|
||||
it "does allow a user to create one rating for one article" do
|
||||
rating = build(:rating_vote, article_id: article.id, user_id: user.id)
|
||||
expect(rating).to be_valid
|
||||
end
|
||||
it "does not allow a user to create multiple ratings for one article" do
|
||||
create(:rating_vote, article_id: article.id, user_id: user.id)
|
||||
rating = build(:rating_vote, article_id: article.id, user_id: user.id)
|
||||
expect(rating).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "modifies article rating score" do
|
||||
it "assigns article rating" do
|
||||
rating = create(:rating_vote, article_id: article.id, user_id: user.id, rating: 2.0)
|
||||
create(:rating_vote, article_id: article.id, user_id: user2.id, rating: 3.0)
|
||||
rating.assign_article_rating
|
||||
expect(article.reload.experience_level_rating).to eq(2.5)
|
||||
expect(article.reload.experience_level_rating_distribution).to eq(1.0)
|
||||
end
|
||||
|
||||
it "assigns article rating with larger distribution" do
|
||||
rating = create(:rating_vote, article_id: article.id, user_id: user.id, rating: 1.0)
|
||||
create(:rating_vote, article_id: article.id, user_id: user2.id, rating: 7.0)
|
||||
rating.assign_article_rating
|
||||
expect(article.reload.experience_level_rating).to eq(4.0)
|
||||
expect(article.reload.experience_level_rating_distribution).to eq(6.0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "permissions" do
|
||||
it "allows trusted users to make rating" do
|
||||
rating = build(:rating_vote, article_id: article.id, user_id: user.id)
|
||||
expect(rating).to be_valid
|
||||
end
|
||||
|
||||
it "does not allow non-trusted users to make rating" do
|
||||
nontrusted_user = create(:user)
|
||||
rating = build(:rating_vote, article_id: article.id, user_id: nontrusted_user.id)
|
||||
expect(rating).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -37,14 +37,24 @@ RSpec.describe "Moderations", type: :request do
|
|||
context "when user is trusted" do
|
||||
before { sign_in user }
|
||||
|
||||
it "grant acess to comment moderation" do
|
||||
it "grant access to comment moderation" do
|
||||
get comment.path + "/mod"
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it "grant acess to article moderation" do
|
||||
it "grant access to article moderation" do
|
||||
get article.path + "/mod"
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it "grants access to /mod index" do
|
||||
get "/mod"
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
it "grants access to /mod index with articles" do
|
||||
create(:article, published: true)
|
||||
get "/mod"
|
||||
expect(response.body).to include("Experience Level Target")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
33
spec/requests/rating_votes_spec.rb
Normal file
33
spec/requests/rating_votes_spec.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# http://localhost:3000/rating_votes
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "RatingVotes", type: :request do
|
||||
let(:user) { create(:user, :trusted) }
|
||||
let(:article) { create(:article) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
describe "POST /rating_votes" do
|
||||
it "creates a new rating vote" do
|
||||
post "/rating_votes", params: {
|
||||
rating_vote: {
|
||||
article_id: article.id, group: "experience_level", rating: 3.0
|
||||
}
|
||||
}
|
||||
expect(RatingVote.last.rating).to eq(3.0)
|
||||
expect(RatingVote.last.user_id).to eq(user.id)
|
||||
end
|
||||
|
||||
it "does not create new rating vote for non-trusted user" do
|
||||
user.remove_role(:trusted)
|
||||
post "/rating_votes", params: {
|
||||
rating_vote: {
|
||||
article_id: article.id, group: "experience_level", rating: 3.0
|
||||
}
|
||||
}
|
||||
expect(RatingVote.all.size).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue