From 18affeb4e9a3efe914f65c9f4aeb87fbfe7f147c Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 23 Jan 2020 18:14:30 -0600 Subject: [PATCH] Add new upload limit system (fix #4234). --- app/logical/upload_limit.rb | 99 +++++++++++++++++++ app/models/post.rb | 3 + app/models/post_approval.rb | 5 +- app/models/user.rb | 4 + app/views/users/_statistics.html.erb | 9 ++ ...200123184743_add_upload_points_to_users.rb | 5 + db/structure.sql | 6 +- script/fixes/062_initialize_upload_points.rb | 14 +++ test/unit/upload_limit_test.rb | 54 ++++++++++ 9 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 app/logical/upload_limit.rb create mode 100644 db/migrate/20200123184743_add_upload_points_to_users.rb create mode 100755 script/fixes/062_initialize_upload_points.rb create mode 100644 test/unit/upload_limit_test.rb diff --git a/app/logical/upload_limit.rb b/app/logical/upload_limit.rb new file mode 100644 index 000000000..ecc4a4ac9 --- /dev/null +++ b/app/logical/upload_limit.rb @@ -0,0 +1,99 @@ +class UploadLimit + extend Memoist + + INITIAL_POINTS = 1000 + MAXIMUM_POINTS = 10000 + + attr_reader :user + + def initialize(user) + @user = user + end + + def limited? + used_upload_slots >= upload_slots + end + + def used_upload_slots + pending = user.posts.pending + early_deleted = user.posts.deleted.where("created_at >= ?", 3.days.ago) + + pending.or(early_deleted).count + end + + def upload_slots + upload_level + 5 + end + + def upload_level + UploadLimit.points_to_level(user.upload_points) + end + + def approvals_on_current_level + (user.upload_points - UploadLimit.level_to_points(upload_level)) / 10 + end + + def approvals_for_next_level + UploadLimit.points_for_next_level(upload_level) / 10 + end + + def update_limit!(post, incremental: true) + return if user.can_upload_free? + + user.with_lock do + if incremental + user.upload_points += UploadLimit.upload_value(user.upload_points, post.is_deleted) + user.save! + else + user.update!(upload_points: UploadLimit.points_for_user(user)) + end + end + end + + def self.points_for_user(user) + points = INITIAL_POINTS + + uploads = user.posts.where(is_pending: false).order(id: :asc).pluck(:is_deleted) + uploads.each do |is_deleted| + points += upload_value(points, is_deleted) + points = points.clamp(0, MAXIMUM_POINTS) + + #warn "slots: %2d, points: %3d, value: %2d" % [UploadLimit.points_to_level(points) + 5, points, UploadLimit.upload_value(level, is_deleted)] + end + + points + end + + def self.upload_value(current_points, is_deleted) + if is_deleted + level = points_to_level(current_points) + -1 * (points_for_next_level(level) / 3.0).round.to_i + else + 10 + end + end + + def self.points_for_next_level(level) + 100 + 20 * [level - 10, 0].max + end + + def self.points_to_level(points) + level = 0 + + loop do + points -= points_for_next_level(level) + break if points < 0 + level += 1 + end + + level + end + + def self.level_to_points(level) + (1..level).map do |n| + points_for_next_level(n - 1) + end.sum + end + + memoize :used_upload_slots +end diff --git a/app/models/post.rb b/app/models/post.rb index c54ee6501..55d3f1d24 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1279,6 +1279,9 @@ class Post < ApplicationRecord # XXX This must happen *after* the `is_deleted` flag is set to true (issue #3419). give_favorites_to_parent(options) if options[:move_favorites] + is_automatic = (reason == "Unapproved in three days") + uploader.new_upload_limit.update_limit!(self, incremental: is_automatic) + unless options[:without_mod_action] ModAction.log("deleted post ##{id}, reason: #{reason}", :post_delete) end diff --git a/app/models/post_approval.rb b/app/models/post_approval.rb index 1cb2d4654..df65375f5 100644 --- a/app/models/post_approval.rb +++ b/app/models/post_approval.rb @@ -26,10 +26,13 @@ class PostApproval < ApplicationRecord end def approve_post - ModAction.log("undeleted post ##{post_id}", :post_undelete) if post.is_deleted + is_undeletion = post.is_deleted post.flags.each(&:resolve!) post.update(approver: user, is_flagged: false, is_pending: false, is_deleted: false) + ModAction.log("undeleted post ##{post_id}", :post_undelete) if is_undeletion + + post.uploader.new_upload_limit.update_limit!(post, incremental: !is_undeletion) end def self.search(params) diff --git a/app/models/user.rb b/app/models/user.rb index c0e96e982..5bd0e8986 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -468,6 +468,10 @@ class User < ApplicationRecord (is_moderator? && flag.not_uploaded_by?(id)) || flag.creator_id == id end + def new_upload_limit + @new_upload_limit ||= UploadLimit.new(self) + end + def upload_limit [max_upload_limit - used_upload_slots, 0].max end diff --git a/app/views/users/_statistics.html.erb b/app/views/users/_statistics.html.erb index fae5374ab..5831f5dfb 100644 --- a/app/views/users/_statistics.html.erb +++ b/app/views/users/_statistics.html.erb @@ -66,6 +66,15 @@ <%= presenter.upload_limit(self) %> (<%= link_to_wiki "help", "about:upload_limits" %>) + + New Upload Limit + + <%= link_to user.new_upload_limit.used_upload_slots, posts_path(tags: "user:#{user.name} status:pending") %> + / + <%= tag.abbr user.new_upload_limit.upload_slots, title: "Next level: #{user.new_upload_limit.approvals_on_current_level} / #{user.new_upload_limit.approvals_for_next_level} approvals" %> + + + Uploads diff --git a/db/migrate/20200123184743_add_upload_points_to_users.rb b/db/migrate/20200123184743_add_upload_points_to_users.rb new file mode 100644 index 000000000..047df2d9a --- /dev/null +++ b/db/migrate/20200123184743_add_upload_points_to_users.rb @@ -0,0 +1,5 @@ +class AddUploadPointsToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :upload_points, :integer, null: false, default: 1000 + end +end diff --git a/db/structure.sql b/db/structure.sql index 31bec31a9..17fa28460 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2192,7 +2192,8 @@ furry -rating:s'::text, bit_prefs bigint DEFAULT 0 NOT NULL, last_ip_addr inet, unread_dmail_count integer DEFAULT 0 NOT NULL, - theme integer DEFAULT 0 NOT NULL + theme integer DEFAULT 0 NOT NULL, + upload_points integer DEFAULT 1000 NOT NULL ); @@ -7366,6 +7367,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200117220602'), ('20200118015014'), ('20200119184442'), -('20200119193110'); +('20200119193110'), +('20200123184743'); diff --git a/script/fixes/062_initialize_upload_points.rb b/script/fixes/062_initialize_upload_points.rb new file mode 100755 index 000000000..f439f7fc2 --- /dev/null +++ b/script/fixes/062_initialize_upload_points.rb @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require_relative "../../config/environment" + +uploaders = User.where(id: Post.select(:uploader_id)).bit_prefs_match(:can_upload_free, false) + +warn "uploaders=#{uploaders.count}" +uploaders.find_each.with_index do |uploader, n| + uploader.new_upload_limit.update_limit!(nil, incremental: false) + warn "n=#{n} id=#{uploader.id} name=#{uploader.name} points=#{uploader.upload_points}" +end + +contributors = User.bit_prefs_match(:can_upload_free, true) +contributors.update_all(upload_points: UploadLimit::MAXIMUM_POINTS) diff --git a/test/unit/upload_limit_test.rb b/test/unit/upload_limit_test.rb new file mode 100644 index 000000000..0f4673552 --- /dev/null +++ b/test/unit/upload_limit_test.rb @@ -0,0 +1,54 @@ +require 'test_helper' + +class UploadLimitTest < ActiveSupport::TestCase + context "Upload limits:" do + setup do + @user = create(:user, upload_points: 1000) + @approver = create(:moderator_user) + end + + context "a pending post that is deleted" do + should "decrease the uploader's upload points" do + @post = create(:post, uploader: @user, is_pending: true, created_at: 7.days.ago) + assert_equal(1000, @user.reload.upload_points) + + PostPruner.new.prune! + assert_equal(967, @user.reload.upload_points) + end + end + + context "a pending post that is approved" do + should "increase the uploader's upload points" do + @post = create(:post, uploader: @user, is_pending: true, created_at: 7.days.ago) + assert_equal(1000, @user.reload.upload_points) + + @post.approve!(@approver) + assert_equal(1010, @user.reload.upload_points) + end + end + + context "an approved post that is deleted" do + should "decrease the uploader's upload points" do + @post = create(:post, uploader: @user, is_pending: true) + assert_equal(1000, @user.reload.upload_points) + + @post.approve!(@approver) + assert_equal(1010, @user.reload.upload_points) + + as(@approver) { @post.delete!("bad") } + assert_equal(967, @user.reload.upload_points) + end + end + + context "a deleted post that is undeleted" do + should "increase the uploader's upload points" do + @post = create(:post, uploader: @user) + as(@approver) { @post.delete!("bad") } + assert_equal(967, @user.reload.upload_points) + + @post.approve!(@approver) + assert_equal(1010, @user.reload.upload_points) + end + end + end +end