Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Countable and order hot user by yearly replies count #1317

Merged
merged 5 commits into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/api/v3/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def index

limit = params[:limit].to_i
limit = 100 if limit > 100
@users = User.fields_for_list.hot.limit(limit)
@users = @active_users = Counter.where(countable_type: "User", key: "yearly_replies_count").includes(:countable).order("value desc").limit(limit).map(&:countable)
end

# Get full detail of current user, for account setting.
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class UsersController < ApplicationController

def index
@total_user_count = User.count
@active_users = User.without_team.fields_for_list.hot.limit(100)
key = params[:type] = "monthly" ? :monthly_replies_count : :yearly_replies_count
@active_users = Counter.where(countable_type: "User", key: key).includes(:countable).order("value desc").limit(100).map(&:countable)
end

def feed
Expand Down
20 changes: 20 additions & 0 deletions app/jobs/scheduler/total_user_stats_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Scheduler
# cleanup spam topic at 1 month ago
class TotalUserStatsJob < ApplicationJob
def perform(limit: 2000)
users = User.where("replies_count > 0")
users = if limit.to_i > 0
users.order("updated_at desc").limit(limit)
else
users.where("replies_count > 0").order("updated_at desc").find_each
end

users.each do |user|
user.monthly_replies_count.update(value: user.replies.where("created_at > ?", 1.month.ago).count)
user.yearly_replies_count.update(value: user.replies.where("created_at > ?", 1.year.ago).count)
end
end
end
end
3 changes: 2 additions & 1 deletion app/models/application_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true

include Counterable
include RedisCountable
include Countable

scope :recent, -> { order(id: :desc) }
scope :exclude_ids, ->(ids) { where.not(id: ids.map(&:to_i)) }
Expand Down
25 changes: 25 additions & 0 deletions app/models/concerns/countable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Countable for storage by use ActiveRecord
module Countable
extend ActiveSupport::Concern

included do
scope :with_counters, -> { includes(@@countable_names) }
end

class_methods do
def countable(*names)
class_eval do
@@countable_names ||= []

names.each do |name|
@@countable_names << "#{name}_counter".to_sym
has_one :"#{name}_counter", -> { where(key: name) }, as: :countable, class_name: "Counter"

define_method :"#{name}" do
send(:"#{name}_counter") || send(:"create_#{name}_counter")
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# 增加访问量的功能
module Counterable
module RedisCountable
extend ActiveSupport::Concern

class Counter
Expand Down
22 changes: 22 additions & 0 deletions app/models/counter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class Counter < ApplicationRecord
belongs_to :countable, polymorphic: true
validates :countable, presence: true

delegate :to_i, :to_s, :inspect, to: :value

def incr(by = 1)
increment!(:value, by).value
end

def decr(by = 1)
decrement!(:value, by).value
end

def method_missing(method, *args, &block)
if value.respond_to?(method)
value.send(method, *args, &block)
else
super
end
end
end
6 changes: 4 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class User < ApplicationRecord
has_many :teams, through: :team_users
has_one :sso, class_name: "UserSSO", dependent: :destroy

countable :monthly_replies_count, :yearly_replies_count

attr_accessor :password_confirmation

validates :login, format: {with: ALLOW_LOGIN_FORMAT_REGEXP, message: I18n.t("users.username_allows_format")},
Expand All @@ -62,11 +64,11 @@ def self.find_for_database_authentication(warden_conditions)
end

def self.find_by_email(email)
fetch_by_uniq_keys(email: email)
fetch_by_uniq_keys(email:)
end

def self.find_by_login!(slug)
find_by_login(slug) || raise(ActiveRecord::RecordNotFound.new(slug: slug))
find_by_login(slug) || raise(ActiveRecord::RecordNotFound.new(slug:))
end

def self.find_by_login(slug)
Expand Down
10 changes: 5 additions & 5 deletions app/views/users/_sidebar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<div class="item number">
<%= t("users.number_of_user", no: @user.id)%> / <span title="<%= t("users.join_time") %>"><%= @user.created_at.to_date %></span>
</div>
</ul>
</div>
</div>
<hr>
Expand All @@ -42,11 +43,10 @@
<%= icon_tag("location") %> <%= location_name_tag(@user.location) %>
</div>
<% end %>
<div class="item counts">
<%= t("users.topics_count_html", count: @user.topics_count) %>
<span class="divider">/</span>
<%= t("users.replies_count_html", count: @user.replies_count) %>
</div>
<hr>
<div class="item number"><%= t("users.topics_count_html", count: @user.topics_count) %></div>
<div class="item number"><%= t("users.replies_count_html", count: @user.replies_count, yearly_count: @user.yearly_replies_count) %></div>
<hr>
<div class="item social">
<% if !@user.twitter.blank? %>
<%= link_to icon_bold_tag("twitter"), @user.twitter_url, class: "twitter", rel: "nofollow" %>
Expand Down
12 changes: 9 additions & 3 deletions app/views/users/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
<div id="users">
<div id="hot_users" class="user-list card card-lg">
<div class="card-body">
<div class="card-title">
<div><%= t("users.hot_users") %></div>
<div class="counter">Total <%= @total_user_count %> users</div>
<div class="flex jcsb items-center">
<div class="card-title">
<div><%= t("users.hot_users") %></div>
<div class="counter">Total <%= @total_user_count %> users</div>
</div>
<div class="tabs">
<a href="<%= users_path %>" class="<%= "active" if params[:type].blank? %>">Yearly</a> <span class="divider">/</span>
<a href="<%= users_path(type: "monthly") %>" class="<%= "active" if params[:type] == "monthly" %>">Monthly</a>
</div>
</div>

<%= render "user_list", users: @active_users, columns: 4 %>
Expand Down
2 changes: 1 addition & 1 deletion config/locales/views.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@
join_time: "Joined at"
signature: "Status"
topics_count_html: <span>%{count}</span> Topics
replies_count_html: <span>%{count}</span> Replies
replies_count_html: "<span>%{count}</span> Replies (Yearly: %{yearly_count})"
personal_website: "Blog"
recent_publish_topic: "Recent Topics"
recent_reply_topic: "Recent Replies"
Expand Down
6 changes: 3 additions & 3 deletions config/locales/views.zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@
join_time: "Since"
signature: "签名"
personal_website: "博客"
topics_count_html: <span>%{count}</span> 篇帖子
replies_count_html: <span>%{count}</span> 条回帖
topics_count_html: 累计 <span>%{count}</span> 篇帖子
replies_count_html: 累计 <span>%{count}</span> 条回帖 (最近一年 %{yearly_count} 条)
recent_publish_topic: "最近发布的帖子"
recent_reply_topic: "最近回复过的帖子"
unbind_warning: "至少要保留一个关联账号,现在不能解绑。"
Expand All @@ -317,7 +317,7 @@
already_have_account: "已经有账号了?"
forget_password: "忘记了密码?"
find_password: "找回密码"
find_password_help_text: "此功能将会发送一个找回密码的特别链接到你的邮箱,通过该链接可以进入重置密码的页面。"
find_password_help_text: "此功能将会发送一个找回密码的特别链接到你的邮箱,通过该链接可以进入重置密码的页面。"
not_recieve_confirm_mail: "未收到确认邮件?"
not_recieve_unlock_mail: "未收到解锁邮件?"
resend_unlock_mail: "重新发送解锁邮件"
Expand Down
6 changes: 6 additions & 0 deletions config/schedule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ spam_cleanup:
class: "Scheduler::SpamCleanupJob"
queue: cleanup
description: "Cleanup spam contents before 1 month ago"

update_user_stats:
cron: "0 0 * * 6"
class: "Scheduler::TotalUserStatsJob"
queue: default
description: "Update user stats"
13 changes: 13 additions & 0 deletions db/migrate/20220113130722_create_counters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CreateCounters < ActiveRecord::Migration[7.0]
def change
create_table :counters do |t|
t.references :countable, polymorphic: true
t.string :key, null: false
t.integer :value, null: false, default: 0
t.timestamps
end

add_index :counters, [:countable_type, :countable_id, :key], unique: true
add_index :counters, [:countable_type, :key, :value]
end
end
14 changes: 13 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2021_04_09_032709) do
ActiveRecord::Schema.define(version: 2022_01_13_130722) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -50,6 +50,18 @@
t.index ["user_id"], name: "index_comments_on_user_id"
end

create_table "counters", force: :cascade do |t|
t.string "countable_type"
t.bigint "countable_id"
t.string "key", null: false
t.integer "value", default: 0, null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["countable_type", "countable_id", "key"], name: "index_counters_on_countable_type_and_countable_id_and_key", unique: true
t.index ["countable_type", "countable_id"], name: "index_counters_on_countable"
t.index ["countable_type", "key", "value"], name: "index_counters_on_countable_type_and_key_and_value"
end

create_table "devices", id: :serial, force: :cascade do |t|
t.integer "platform", null: false
t.integer "user_id", null: false
Expand Down
23 changes: 14 additions & 9 deletions test/controllers/api/users_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
describe Api::V3::UsersController do
describe "GET /api/v3/users.json" do
before do
create_list(:user, 10)
@users = create_list(:user, 10)

@users.each do |user|
user.yearly_replies_count.to_i
user.monthly_replies_count.to_i
end
end

it "should work" do
get "/api/v3/users.json"
assert_equal 200, response.status
assert_equal User.count, json["users"].size
assert_equal @users.count, json["users"].size
assert_has_keys json["users"][0], "id", "name", "login", "avatar_url"
end

Expand Down Expand Up @@ -90,7 +95,7 @@

describe "recent order" do
it "should work" do
@topics = create_list(:topic, 3, user: user)
@topics = create_list(:topic, 3, user:)
get "/api/v3/users/#{user.login}/topics.json", offset: 0, limit: 2
assert_equal 200, response.status
assert_equal 2, json["topics"].size
Expand All @@ -103,8 +108,8 @@

describe "hot order" do
it "should work" do
@hot_topic = create(:topic, user: user, likes_count: 4)
@topics = create_list(:topic, 3, user: user)
@hot_topic = create(:topic, user:, likes_count: 4)
@topics = create_list(:topic, 3, user:)

get "/api/v3/users/#{user.login}/topics.json", order: "likes", offset: 0, limit: 3
assert_equal 200, response.status
Expand All @@ -115,8 +120,8 @@

describe "hot order" do
it "should work" do
@hot_topic = create(:topic, user: user, replies_count: 4)
@topics = create_list(:topic, 3, user: user)
@hot_topic = create(:topic, user:, replies_count: 4)
@topics = create_list(:topic, 3, user:)

get "/api/v3/users/#{user.login}/topics.json", order: "replies", offset: 0, limit: 3
assert_equal 200, response.status
Expand All @@ -132,7 +137,7 @@

describe "recent order" do
it "should work" do
@replies = create_list(:reply, 3, user: user, topic: topic)
@replies = create_list(:reply, 3, user:, topic:)
get "/api/v3/users/#{user.login}/replies.json", offset: 0, limit: 2
assert_equal 2, json["replies"].size
fields = %w[id user body_html topic_id topic_title]
Expand All @@ -148,7 +153,7 @@
let(:user) { create(:user) }

it "should work" do
@topics = create_list(:topic, 4, user: user)
@topics = create_list(:topic, 4, user:)
user.favorite_topic(@topics[0].id)
user.favorite_topic(@topics[1].id)
user.favorite_topic(@topics[3].id)
Expand Down
9 changes: 9 additions & 0 deletions test/factories/counters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

FactoryBot.define do
factory :counter do
association :countable, factory: :user
key { "foo_count" }
value { 0 }
end
end
42 changes: 42 additions & 0 deletions test/models/counter_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require "test_helper"

class CounterTest < ActiveSupport::TestCase
test "base" do
counter = create :counter
assert_equal 0, counter.value
assert_equal 0, counter.to_i

counter.incr
assert_equal 1, counter.value

counter.incr
assert_equal 2, counter.value

counter.incr(3)
assert_equal 5, counter.value

counter.decr
assert_equal 4, counter.value

counter.decr(2)
assert_equal 2, counter.value
end

test "User counter" do
user = create(:user)

assert_equal 0, user.yearly_replies_count.to_i
user.yearly_replies_count.incr
assert_equal 1, user.yearly_replies_count.to_i
user.yearly_replies_count.decr
assert_equal 0, user.yearly_replies_count.to_i

assert_equal 0, user.monthly_replies_count.to_i
user.monthly_replies_count.incr
assert_equal 1, user.monthly_replies_count.to_i
user.monthly_replies_count.decr
assert_equal 0, user.monthly_replies_count.to_i
end
end