commit
bca3825c17
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { shortNumberFormat } from 'mastodon/utils/numbers';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class AutosuggestHashtag extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
tag: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
history: PropTypes.array.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { tag } = this.props;
|
||||
const weeklyUses = shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
|
||||
|
||||
return (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div>
|
||||
<div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SearchQueryParser < Parslet::Parser
|
||||
rule(:term) { match('[^\s":]').repeat(1).as(:term) }
|
||||
rule(:quote) { str('"') }
|
||||
rule(:colon) { str(':') }
|
||||
rule(:space) { match('\s').repeat(1) }
|
||||
rule(:operator) { (str('+') | str('-')).as(:operator) }
|
||||
rule(:prefix) { (term >> colon).as(:prefix) }
|
||||
rule(:phrase) { (quote >> (term >> space.maybe).repeat >> quote).as(:phrase) }
|
||||
rule(:clause) { (prefix.maybe >> operator.maybe >> (phrase | term)).as(:clause) }
|
||||
rule(:query) { (clause >> space.maybe).repeat.as(:query) }
|
||||
root(:query)
|
||||
end
|
@ -0,0 +1,86 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SearchQueryTransformer < Parslet::Transform
|
||||
class Query
|
||||
attr_reader :should_clauses, :must_not_clauses, :must_clauses
|
||||
|
||||
def initialize(clauses)
|
||||
grouped = clauses.chunk(&:operator).to_h
|
||||
@should_clauses = grouped.fetch(:should, [])
|
||||
@must_not_clauses = grouped.fetch(:must_not, [])
|
||||
@must_clauses = grouped.fetch(:must, [])
|
||||
end
|
||||
|
||||
def apply(search)
|
||||
should_clauses.each { |clause| search = search.query.should(clause_to_query(clause)) }
|
||||
must_clauses.each { |clause| search = search.query.must(clause_to_query(clause)) }
|
||||
must_not_clauses.each { |clause| search = search.query.must_not(clause_to_query(clause)) }
|
||||
search.query.minimum_should_match(1)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clause_to_query(clause)
|
||||
case clause
|
||||
when TermClause
|
||||
{ multi_match: { type: 'most_fields', query: clause.term, fields: ['text', 'text.stemmed'] } }
|
||||
when PhraseClause
|
||||
{ match_phrase: { text: { query: clause.phrase } } }
|
||||
else
|
||||
raise "Unexpected clause type: #{clause}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Operator
|
||||
class << self
|
||||
def symbol(str)
|
||||
case str
|
||||
when '+'
|
||||
:must
|
||||
when '-'
|
||||
:must_not
|
||||
when nil
|
||||
:should
|
||||
else
|
||||
raise "Unknown operator: #{str}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TermClause
|
||||
attr_reader :prefix, :operator, :term
|
||||
|
||||
def initialize(prefix, operator, term)
|
||||
@prefix = prefix
|
||||
@operator = Operator.symbol(operator)
|
||||
@term = term
|
||||
end
|
||||
end
|
||||
|
||||
class PhraseClause
|
||||
attr_reader :prefix, :operator, :phrase
|
||||
|
||||
def initialize(prefix, operator, phrase)
|
||||
@prefix = prefix
|
||||
@operator = Operator.symbol(operator)
|
||||
@phrase = phrase
|
||||
end
|
||||
end
|
||||
|
||||
rule(clause: subtree(:clause)) do
|
||||
prefix = clause[:prefix][:term].to_s if clause[:prefix]
|
||||
operator = clause[:operator]&.to_s
|
||||
|
||||
if clause[:term]
|
||||
TermClause.new(prefix, operator, clause[:term].to_s)
|
||||
elsif clause[:phrase]
|
||||
PhraseClause.new(prefix, operator, clause[:phrase].map { |p| p[:term].to_s }.join(' '))
|
||||
else
|
||||
raise "Unexpected clause type: #{clause}"
|
||||
end
|
||||
end
|
||||
|
||||
rule(query: sequence(:clauses)) { Query.new(clauses) }
|
||||
end
|
@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Scheduler::PreviewCardsCleanupScheduler
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options unique: :until_executed, retry: 0
|
||||
|
||||
def perform
|
||||
Maintenance::UncachePreviewWorker.push_bulk(recent_link_preview_cards.pluck(:id))
|
||||
Maintenance::UncachePreviewWorker.push_bulk(older_preview_cards.pluck(:id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def recent_link_preview_cards
|
||||
PreviewCard.where(type: :link).where('updated_at < ?', 1.month.ago)
|
||||
end
|
||||
|
||||
def older_preview_cards
|
||||
PreviewCard.where('updated_at < ?', 6.months.ago)
|
||||
end
|
||||
end
|
@ -0,0 +1,15 @@
|
||||
class AddCaseInsensitiveIndexToTags < ActiveRecord::Migration[5.2]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
safety_assured { execute 'CREATE UNIQUE INDEX CONCURRENTLY index_tags_on_name_lower ON tags (lower(name))' }
|
||||
remove_index :tags, name: 'index_tags_on_name'
|
||||
remove_index :tags, name: 'hashtag_search_index'
|
||||
end
|
||||
|
||||
def down
|
||||
add_index :tags, :name, unique: true, algorithm: :concurrently
|
||||
safety_assured { execute 'CREATE INDEX CONCURRENTLY hashtag_search_index ON tags (name text_pattern_ops)' }
|
||||
remove_index :tags, name: 'index_tags_on_name_lower'
|
||||
end
|
||||
end
|
@ -0,0 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'tty-prompt'
|
||||
require_relative '../../config/boot'
|
||||
require_relative '../../config/environment'
|
||||
require_relative 'cli_helper'
|
||||
|
||||
module Mastodon
|
||||
class PreviewCardsCLI < Thor
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
def self.exit_on_failure?
|
||||
true
|
||||
end
|
||||
|
||||
option :days, type: :numeric, default: 180
|
||||
option :background, type: :boolean, default: false
|
||||
option :verbose, type: :boolean, default: false
|
||||
option :dry_run, type: :boolean, default: false
|
||||
option :link, type: :boolean, default: false
|
||||
desc 'remove', 'Remove preview cards'
|
||||
long_desc <<-DESC
|
||||
Removes locally thumbnails for previews.
|
||||
|
||||
The --days option specifies how old preview cards have to be before
|
||||
they are removed. It defaults to 180 days.
|
||||
|
||||
With the --background option, instead of deleting the files sequentially,
|
||||
they will be queued into Sidekiq and the command will exit as soon as
|
||||
possible. In Sidekiq they will be processed with higher concurrency, but
|
||||
it may impact other operations of the Mastodon server, and it may overload
|
||||
the underlying file storage.
|
||||
|
||||
With the --dry-run option, no work will be done.
|
||||
|
||||
With the --verbose option, when preview cards are processed sequentially in the
|
||||
foreground, the IDs of the preview cards will be printed.
|
||||
|
||||
With the --link option, delete only link-type preview cards.
|
||||
DESC
|
||||
def remove
|
||||
prompt = TTY::Prompt.new
|
||||
time_ago = options[:days].days.ago
|
||||
queued = 0
|
||||
processed = 0
|
||||
size = 0
|
||||
dry_run = options[:dry_run] ? '(DRY RUN)' : ''
|
||||
link = options[:link] ? 'link-type ' : ''
|
||||
scope = PreviewCard.where.not(image_file_name: nil)
|
||||
scope = scope.where.not(image_file_name: '')
|
||||
scope = scope.where(type: :link) if options[:link]
|
||||
scope = scope.where('updated_at < ?', time_ago)
|
||||
|
||||
if time_ago > 2.weeks.ago
|
||||
prompt.say "\n"
|
||||
prompt.say('The preview cards less than the past two weeks will not be re-acquired even when needed.')
|
||||
prompt.say "\n"
|
||||
|
||||
unless prompt.yes?('Are you sure you want to delete the preview cards?', default: false)
|
||||
prompt.say "\n"
|
||||
prompt.warn 'Nothing execute. Bye!'
|
||||
prompt.say "\n"
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
if options[:background]
|
||||
scope.select(:id, :image_file_size).reorder(nil).find_in_batches do |preview_cards|
|
||||
queued += preview_cards.size
|
||||
size += preview_cards.reduce(0) { |sum, p| sum + (p.image_file_size || 0) }
|
||||
Maintenance::UncachePreviewWorker.push_bulk(preview_cards.map(&:id)) unless options[:dry_run]
|
||||
end
|
||||
|
||||
else
|
||||
scope.select(:id, :image_file_size).reorder(nil).find_in_batches do |preview_cards|
|
||||
preview_cards.each do |p|
|
||||
size += p.image_file_size || 0
|
||||
Maintenance::UncachePreviewWorker.new.perform(p.id) unless options[:dry_run]
|
||||
options[:verbose] ? say(p.id) : say('.', :green, false)
|
||||
processed += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
say
|
||||
|
||||
if options[:background]
|
||||
say("Scheduled the deletion of #{queued} #{link}preview cards (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true)
|
||||
else
|
||||
say("Removed #{processed} #{link}preview cards (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,5 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PollVote, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
describe '#object_type' do
|
||||
let(:poll_vote) { Fabricate.build(:poll_vote) }
|
||||
|
||||
it 'returns :vote' do
|
||||
expect(poll_vote.object_type).to eq :vote
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in new issue