forked from e621ng/e621ng
Expire unused aliases and implications after 2 years of inactivity
This commit is contained in:
parent
cdb0a2cac7
commit
ea4e7f1970
67
app/logical/tag_relationship_retirement_service.rb
Normal file
67
app/logical/tag_relationship_retirement_service.rb
Normal file
@ -0,0 +1,67 @@
|
||||
module TagRelationshipRetirementService
|
||||
THRESHOLD = 2.year
|
||||
|
||||
extend self
|
||||
|
||||
def forum_topic_title
|
||||
return "Retired tag aliases & implications"
|
||||
end
|
||||
|
||||
def forum_topic_body
|
||||
return "This topic deals with tag relationships created two or more years ago that have not been used since. They will be retired. This topic will be updated as an automated system retires expired relationships."
|
||||
end
|
||||
|
||||
def dry_run
|
||||
[TagAlias, TagImplication].each do |model|
|
||||
each_candidate(model) do |rel|
|
||||
puts "#{rel.relationship} #{rel.antecedent_name} -> #{rel.consequent_name} retired"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def forum_topic
|
||||
topic = ForumTopic.where(title: forum_topic_title).first
|
||||
if topic.nil?
|
||||
topic = CurrentUser.as_system do
|
||||
ForumTopic.create(title: forum_topic_title, category_id: 1, original_post_attributes: {body: forum_topic_body})
|
||||
end
|
||||
end
|
||||
return topic
|
||||
end
|
||||
|
||||
def find_and_retire!
|
||||
messages = []
|
||||
|
||||
[TagAlias, TagImplication].each do |model|
|
||||
each_candidate(model) do |rel|
|
||||
rel.update(status: "retired")
|
||||
messages << rel.retirement_message
|
||||
end
|
||||
end
|
||||
|
||||
updater = ForumUpdater.new(forum_topic)
|
||||
updater.update(messages.sort.join("\n"))
|
||||
end
|
||||
|
||||
def each_candidate(model)
|
||||
model.active.where("created_at < ?", THRESHOLD.ago).find_each do |rel|
|
||||
if is_unused?(rel.consequent_name)
|
||||
yield(rel)
|
||||
end
|
||||
end
|
||||
|
||||
# model.active.where("created_at < ?", SMALL_THRESHOLD.ago).find_each do |rel|
|
||||
# if is_underused?(rel.consequent_name)
|
||||
# yield(rel)
|
||||
# end
|
||||
# end
|
||||
end
|
||||
|
||||
def is_underused?(name)
|
||||
(Tag.find_by_name(name).try(:post_count) || 0) < COUNT_THRESHOLD
|
||||
end
|
||||
|
||||
def is_unused?(name)
|
||||
return !Post.tag_match("status:any #{name}").where("created_at > ?", THRESHOLD.ago).exists?
|
||||
end
|
||||
end
|
@ -2,6 +2,7 @@ class TagAlias < TagRelationship
|
||||
before_save :ensure_tags_exist
|
||||
after_save :clear_all_cache
|
||||
after_destroy :clear_all_cache
|
||||
after_save :clear_all_cache, if: ->(rec) {rec.is_retired?}
|
||||
after_save :create_mod_action
|
||||
validates_uniqueness_of :antecedent_name
|
||||
validate :absence_of_transitive_relation
|
||||
|
@ -2,6 +2,7 @@ class TagImplication < TagRelationship
|
||||
before_save :update_descendant_names
|
||||
after_save :update_descendant_names_for_parents
|
||||
after_destroy :update_descendant_names_for_parents
|
||||
after_save :update_descendant_names_for_parents, if: ->(rec) { rec.is_retired? }
|
||||
after_save :create_mod_action
|
||||
validates_uniqueness_of :antecedent_name, :scope => :consequent_name
|
||||
validate :absence_of_circular_relation
|
||||
|
@ -13,13 +13,15 @@ class TagRelationship < ApplicationRecord
|
||||
has_one :antecedent_tag, :class_name => "Tag", :foreign_key => "name", :primary_key => "antecedent_name"
|
||||
has_one :consequent_tag, :class_name => "Tag", :foreign_key => "name", :primary_key => "consequent_name"
|
||||
|
||||
scope :active, ->{where(status: "active")}
|
||||
scope :expired, ->{where("created_at < ?", EXPIRY.days.ago)}
|
||||
scope :old, ->{where("created_at >= ? and created_at < ?", EXPIRY.days.ago, EXPIRY_WARNING.days.ago)}
|
||||
scope :pending, ->{where(status: "pending")}
|
||||
scope :retired, ->{where(status: "retired")}
|
||||
|
||||
before_validation :initialize_creator, :on => :create
|
||||
before_validation :normalize_names
|
||||
validates_format_of :status, :with => /\A(active|deleted|pending|processing|queued|error: .*)\Z/
|
||||
validates_format_of :status, :with => /\A(active|deleted|pending|processing|queued|retired|error: .*)\Z/
|
||||
validates_presence_of :creator_id, :antecedent_name, :consequent_name
|
||||
validates :creator, presence: { message: "must exist" }, if: -> { creator_id.present? }
|
||||
validates :approver, presence: { message: "must exist" }, if: -> { approver_id.present? }
|
||||
@ -35,6 +37,10 @@ class TagRelationship < ApplicationRecord
|
||||
self.consequent_name = consequent_name.mb_chars.downcase.tr(" ", "_")
|
||||
end
|
||||
|
||||
def is_retired?
|
||||
status == "retired"
|
||||
end
|
||||
|
||||
def is_pending?
|
||||
status == "pending"
|
||||
end
|
||||
@ -98,6 +104,8 @@ class TagRelationship < ApplicationRecord
|
||||
|
||||
if params[:status].present?
|
||||
q = q.status_matches(params[:status])
|
||||
else
|
||||
q = q.active
|
||||
end
|
||||
|
||||
if params[:category].present?
|
||||
@ -140,6 +148,10 @@ class TagRelationship < ApplicationRecord
|
||||
"The #{relationship} [[#{antecedent_name}]] -> [[#{consequent_name}]] #{forum_link} has been rejected by @#{rejector.name}."
|
||||
end
|
||||
|
||||
def retirement_message
|
||||
"The #{relationship} [[#{antecedent_name}]] -> [[#{consequent_name}]] #{forum_link} has been retired."
|
||||
end
|
||||
|
||||
def conflict_message
|
||||
"The tag alias [[#{antecedent_name}]] -> [[#{consequent_name}]] #{forum_link} has conflicting wiki pages. [[#{consequent_name}]] should be updated to include information from [[#{antecedent_name}]] if necessary."
|
||||
end
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div id="a-index">
|
||||
<%= simple_form_for(:search, method: :get, url: tag_aliases_path, defaults: { required: false }, html: { class: "inline-form" }) do |f| %>
|
||||
<%= f.input :name_matches, label: "Name", input_html: { value: params[:search][:name_matches], data: { autocomplete: "tag" } } %>
|
||||
<%= f.input :status, label: "Status", collection: ["", "Approved", "Pending"], selected: params[:search][:status] %>
|
||||
<%= f.input :status, label: "Status", collection: ["", "Approved", "Pending", "Retired"], selected: params[:search][:status] %>
|
||||
<%= f.input :category, label: "Category", collection: TagCategory.canonical_mapping.to_a, include_blank: true, selected: params[:search][:category] %>
|
||||
<%= f.input :order, label: "Order", collection: [%w[Status status], %w[Recently\ created created_at], %w[Recently\ updated updated_at], %w[Name name], %w[Tag\ count tag_count]], selected: params[:search][:order] %>
|
||||
<%= f.submit "Search" %>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div id="a-index">
|
||||
<%= simple_form_for(:search, method: :get, url: tag_implications_path, defaults: { required: false }, html: { class: "inline-form" }) do |f| %>
|
||||
<%= f.input :name_matches, label: "Name", input_html: { value: params[:search][:name_matches], data: { autocomplete: "tag" } } %>
|
||||
<%= f.input :status, label: "Status", collection: ["", "Approved", "Pending"], selected: params[:search][:status] %>
|
||||
<%= f.input :status, label: "Status", collection: ["", "Approved", "Pending", "Retired"], selected: params[:search][:status] %>
|
||||
<%= f.input :category, label: "Category", collection: TagCategory.canonical_mapping.to_a, include_blank: true, selected: params[:search][:category] %>
|
||||
<%= f.input :order, label: "Order", collection: [%w[Status status], %w[Recently\ created created_at], %w[Recently\ updated updated_at], %w[Name name], %w[Tag\ count tag_count]], selected: params[:search][:order] %>
|
||||
<%= f.submit "Search" %>
|
||||
|
106
bin/bundle
106
bin/bundle
@ -1,105 +1,3 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# This file was generated by Bundler.
|
||||
#
|
||||
# The application 'bundle' is installed as part of a gem, and
|
||||
# this file is here to facilitate running it.
|
||||
#
|
||||
|
||||
require "rubygems"
|
||||
|
||||
m = Module.new do
|
||||
module_function
|
||||
|
||||
def invoked_as_script?
|
||||
File.expand_path($0) == File.expand_path(__FILE__)
|
||||
end
|
||||
|
||||
def env_var_version
|
||||
ENV["BUNDLER_VERSION"]
|
||||
end
|
||||
|
||||
def cli_arg_version
|
||||
return unless invoked_as_script? # don't want to hijack other binstubs
|
||||
return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
|
||||
bundler_version = nil
|
||||
update_index = nil
|
||||
ARGV.each_with_index do |a, i|
|
||||
if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
|
||||
bundler_version = a
|
||||
end
|
||||
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
|
||||
bundler_version = $1 || ">= 0.a"
|
||||
update_index = i
|
||||
end
|
||||
bundler_version
|
||||
end
|
||||
|
||||
def gemfile
|
||||
gemfile = ENV["BUNDLE_GEMFILE"]
|
||||
return gemfile if gemfile && !gemfile.empty?
|
||||
|
||||
File.expand_path("../../Gemfile", __FILE__)
|
||||
end
|
||||
|
||||
def lockfile
|
||||
lockfile =
|
||||
case File.basename(gemfile)
|
||||
when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
|
||||
else "#{gemfile}.lock"
|
||||
end
|
||||
File.expand_path(lockfile)
|
||||
end
|
||||
|
||||
def lockfile_version
|
||||
return unless File.file?(lockfile)
|
||||
lockfile_contents = File.read(lockfile)
|
||||
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
|
||||
Regexp.last_match(1)
|
||||
end
|
||||
|
||||
def bundler_version
|
||||
@bundler_version ||= begin
|
||||
env_var_version || cli_arg_version ||
|
||||
lockfile_version || "#{Gem::Requirement.default}.a"
|
||||
end
|
||||
end
|
||||
|
||||
def load_bundler!
|
||||
ENV["BUNDLE_GEMFILE"] ||= gemfile
|
||||
|
||||
# must dup string for RG < 1.8 compatibility
|
||||
activate_bundler(bundler_version.dup)
|
||||
end
|
||||
|
||||
def activate_bundler(bundler_version)
|
||||
if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0")
|
||||
bundler_version = "< 2"
|
||||
end
|
||||
gem_error = activation_error_handling do
|
||||
gem "bundler", bundler_version
|
||||
end
|
||||
return if gem_error.nil?
|
||||
require_error = activation_error_handling do
|
||||
require "bundler/version"
|
||||
end
|
||||
return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION))
|
||||
warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`"
|
||||
exit 42
|
||||
end
|
||||
|
||||
def activation_error_handling
|
||||
yield
|
||||
nil
|
||||
rescue StandardError, LoadError => e
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
m.load_bundler!
|
||||
|
||||
if m.invoked_as_script?
|
||||
load Gem.bin_path("bundler", "bundle")
|
||||
end
|
||||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
||||
load Gem.bin_path('bundler', 'bundle')
|
||||
|
31
bin/rails
31
bin/rails
@ -1,29 +1,4 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# This file was generated by Bundler.
|
||||
#
|
||||
# The application 'rails' is installed as part of a gem, and
|
||||
# this file is here to facilitate running it.
|
||||
#
|
||||
|
||||
require "pathname"
|
||||
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
||||
Pathname.new(__FILE__).realpath)
|
||||
|
||||
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
||||
|
||||
if File.file?(bundle_binstub)
|
||||
if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
|
||||
load(bundle_binstub)
|
||||
else
|
||||
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
||||
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
||||
end
|
||||
end
|
||||
|
||||
require "rubygems"
|
||||
require "bundler/setup"
|
||||
|
||||
load Gem.bin_path("railties", "rails")
|
||||
APP_PATH = File.expand_path('../config/application', __dir__)
|
||||
require_relative '../config/boot'
|
||||
require 'rails/commands'
|
||||
|
31
bin/rake
31
bin/rake
@ -1,29 +1,4 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# This file was generated by Bundler.
|
||||
#
|
||||
# The application 'rake' is installed as part of a gem, and
|
||||
# this file is here to facilitate running it.
|
||||
#
|
||||
|
||||
require "pathname"
|
||||
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
||||
Pathname.new(__FILE__).realpath)
|
||||
|
||||
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
||||
|
||||
if File.file?(bundle_binstub)
|
||||
if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
|
||||
load(bundle_binstub)
|
||||
else
|
||||
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
||||
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
||||
end
|
||||
end
|
||||
|
||||
require "rubygems"
|
||||
require "bundler/setup"
|
||||
|
||||
load Gem.bin_path("rake", "rake")
|
||||
require_relative '../config/boot'
|
||||
require 'rake'
|
||||
Rake.application.run
|
||||
|
@ -1,7 +0,0 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ForumPostVoteTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
75
test/models/tag_relationship_retirement_service_test.rb
Normal file
75
test/models/tag_relationship_retirement_service_test.rb
Normal file
@ -0,0 +1,75 @@
|
||||
require 'test_helper'
|
||||
|
||||
class TagRelationshipRetirementServiceTest < ActiveSupport::TestCase
|
||||
context ".forum_topic" do
|
||||
subject { TagRelationshipRetirementService }
|
||||
|
||||
should "create a new topic if one doesn't already exist" do
|
||||
assert_difference(-> { ForumTopic.count }) do
|
||||
subject.forum_topic
|
||||
end
|
||||
end
|
||||
|
||||
should "create a new post if one doesn't already exist" do
|
||||
assert_difference(-> { ForumPost.count }) do
|
||||
subject.forum_topic
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context ".each_candidate" do
|
||||
subject { TagRelationshipRetirementService }
|
||||
|
||||
setup do
|
||||
subject.stubs(:is_unused?).returns(true)
|
||||
|
||||
@user = FactoryBot.create(:user)
|
||||
as_user do
|
||||
@new_alias = FactoryBot.create(:tag_alias, antecedent_name: "aaa", consequent_name: "bbb")
|
||||
|
||||
travel_to(3.years.ago) do
|
||||
@old_alias = FactoryBot.create(:tag_alias, antecedent_name: "ccc", consequent_name: "ddd")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should "find old tag relationships" do
|
||||
subject.each_candidate(TagAlias) do |rel|
|
||||
assert_equal(@old_alias, rel)
|
||||
end
|
||||
end
|
||||
|
||||
should "not find new tag relationships" do
|
||||
subject.each_candidate(TagAlias) do |rel|
|
||||
assert_not_equal(@new_alias, rel)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context ".is_unused?" do
|
||||
subject { TagRelationshipRetirementService }
|
||||
|
||||
setup do
|
||||
@user = FactoryBot.create(:user)
|
||||
|
||||
as_user do
|
||||
@new_alias = FactoryBot.create(:tag_alias, antecedent_name: "aaa", consequent_name: "bbb")
|
||||
@new_post = FactoryBot.create(:post, tag_string: "bbb")
|
||||
|
||||
travel_to(3.years.ago) do
|
||||
@old_alias = FactoryBot.create(:tag_alias, antecedent_name: "ccc", consequent_name: "ddd")
|
||||
@old_post = FactoryBot.create(:post, tag_string: "ddd")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should "return true if no recent post exists" do
|
||||
assert(subject.is_unused?("ddd"))
|
||||
end
|
||||
|
||||
should "return false if a recent post exists" do
|
||||
refute(subject.is_unused?("bbb"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user