diff --git a/app/assets/javascripts/initializers/initializeFetchFollowedArticles.js.erb b/app/assets/javascripts/initializers/initializeFetchFollowedArticles.js.erb
index 2692ac66e..205566a87 100644
--- a/app/assets/javascripts/initializers/initializeFetchFollowedArticles.js.erb
+++ b/app/assets/javascripts/initializers/initializeFetchFollowedArticles.js.erb
@@ -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) {
diff --git a/app/controllers/async_info_controller.rb b/app/controllers/async_info_controller.rb
index e6975e2b0..b26c8c5c7 100644
--- a/app/controllers/async_info_controller.rb
+++ b/app/controllers/async_info_controller.rb
@@ -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
diff --git a/app/controllers/moderations_controller.rb b/app/controllers/moderations_controller.rb
index 222a8b72b..55065eb75 100644
--- a/app/controllers/moderations_controller.rb
+++ b/app/controllers/moderations_controller.rb
@@ -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])
diff --git a/app/controllers/rating_votes_controller.rb b/app/controllers/rating_votes_controller.rb
new file mode 100644
index 000000000..29581fdfd
--- /dev/null
+++ b/app/controllers/rating_votes_controller.rb
@@ -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
diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb
index 052f72c32..90155207b 100644
--- a/app/controllers/stories_controller.rb
+++ b/app/controllers/stories_controller.rb
@@ -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])
diff --git a/app/models/article.rb b/app/models/article.rb
index 16bc48af2..7da085189 100644
--- a/app/models/article.rb
+++ b/app/models/article.rb
@@ -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
diff --git a/app/models/rating_vote.rb b/app/models/rating_vote.rb
new file mode 100644
index 000000000..2d418b98c
--- /dev/null
+++ b/app/models/rating_vote.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 975ed2933..06bac2300 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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,
diff --git a/app/policies/rating_vote_policy.rb b/app/policies/rating_vote_policy.rb
new file mode 100644
index 000000000..53b7d435d
--- /dev/null
+++ b/app/policies/rating_vote_policy.rb
@@ -0,0 +1,9 @@
+class RatingVotePolicy < ApplicationPolicy
+ def create?
+ !user_is_banned?
+ end
+
+ def permitted_attributes
+ %i[rating group article_id]
+ end
+end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 1ed31728f..2c7b6b200 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -73,6 +73,7 @@ class UserPolicy < ApplicationPolicy
employer_name
employer_url
employment_title
+ experience_level
facebook_url
feed_admin_publish_permission
feed_mark_canonical
diff --git a/app/views/moderations/index.html.erb b/app/views/moderations/index.html.erb
new file mode 100644
index 000000000..3394c47c4
--- /dev/null
+++ b/app/views/moderations/index.html.erb
@@ -0,0 +1,62 @@
+
+
+
+
- All negative reactions are 100% private.
+ All negative reactions are private.
Use the thumbsdown(👎) to move something "down" for any reason (quality, usefulness, code of conduct grey area).
The vomit(🤢) is for code of conduct violations (harassment, being an asshole, spam, etc.).
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 %>
+ All rating votes are private.
+
Provide a rating between 1 and 10 based on which audience would find this post most valuable.
+
1: for brand new developers, 3: for junior developers, 10: for very advanced developers, etc.
+
This will provide approximate and gentle algorithmic adjustments to help deliver relevant content.
+
It will be used as one of many indicators. No one rating will have an overly dramatic affect.
+