Merge remote-tracking branch 'origin/main' into feature/351
このコミットが含まれているのは:
@@ -0,0 +1,21 @@
|
||||
class GekanatorAiRun < ApplicationRecord
|
||||
STATUSES = ['pending', 'running', 'succeeded', 'failed', 'blocked_budget'].freeze
|
||||
|
||||
belongs_to :gekanator_question_suggestion
|
||||
|
||||
validates :model, presence: true, length: { maximum: 255 }
|
||||
validates :status, presence: true, inclusion: { in: STATUSES }
|
||||
validates :input_tokens,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
validates :output_tokens,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
validates :estimated_cost_jpy,
|
||||
presence: true,
|
||||
numericality: { greater_than_or_equal_to: 0 }
|
||||
|
||||
scope :this_month, lambda {
|
||||
where(created_at: Time.current.beginning_of_month..Time.current.end_of_month)
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
class GekanatorGame < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :guessed_post, class_name: 'Post'
|
||||
belongs_to :correct_post, class_name: 'Post'
|
||||
has_many :question_suggestions,
|
||||
class_name: 'GekanatorQuestionSuggestion',
|
||||
dependent: :delete_all
|
||||
has_many :question_examples,
|
||||
class_name: 'GekanatorQuestionExample',
|
||||
dependent: :delete_all
|
||||
|
||||
validates :answers, presence: true
|
||||
validates :question_count, numericality: { greater_than_or_equal_to: 0 }
|
||||
validates :won, inclusion: { in: [true, false] }
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
class GekanatorQuestion < ApplicationRecord
|
||||
KINDS = ['tag', 'source', 'title', 'original_date', 'post_similarity'].freeze
|
||||
SOURCES = ['user_suggested', 'ai_generated', 'admin_curated'].freeze
|
||||
STATUSES = ['pending', 'accepted', 'rejected', 'disabled'].freeze
|
||||
|
||||
belongs_to :gekanator_question_suggestion, optional: true
|
||||
belongs_to :created_by, class_name: 'User', optional: true
|
||||
has_many :gekanator_question_examples, dependent: :delete_all
|
||||
|
||||
validates :kind, presence: true, inclusion: { in: KINDS }
|
||||
validates :source, presence: true, inclusion: { in: SOURCES }
|
||||
validates :status, presence: true, inclusion: { in: STATUSES }
|
||||
validates :text, presence: true, length: { maximum: 1000 }
|
||||
validates :condition, presence: true
|
||||
validates :priority_weight,
|
||||
presence: true,
|
||||
numericality: {
|
||||
greater_than: 0,
|
||||
less_than_or_equal_to: 3
|
||||
}
|
||||
|
||||
scope :accepted, -> { where(status: 'accepted') }
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
class GekanatorQuestionExample < ApplicationRecord
|
||||
ANSWERS = GekanatorQuestionSuggestion::ANSWERS
|
||||
NON_UNKNOWN_ANSWERS = ANSWERS - ['unknown']
|
||||
SOURCES = ['initial_suggestion', 'post_game_answer', 'post_game_extra'].freeze
|
||||
|
||||
belongs_to :gekanator_question
|
||||
belongs_to :post
|
||||
belongs_to :user
|
||||
belongs_to :gekanator_game, optional: true
|
||||
|
||||
validates :answer, presence: true, inclusion: { in: ANSWERS }
|
||||
validates :answer_counts, presence: true
|
||||
validates :sample_count,
|
||||
presence: true,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than: 0
|
||||
}
|
||||
validates :source, presence: true, inclusion: { in: SOURCES }
|
||||
validates :weight,
|
||||
presence: true,
|
||||
numericality: {
|
||||
greater_than: 0
|
||||
}
|
||||
|
||||
before_validation :normalize_learning_state
|
||||
|
||||
def record_answer!(answer:, source:, gekanator_game: nil)
|
||||
answer = answer.to_s
|
||||
raise ArgumentError, 'invalid answer' unless ANSWERS.include?(answer)
|
||||
|
||||
counts = normalized_answer_counts
|
||||
counts[answer] += 1
|
||||
|
||||
self.answer_counts = counts
|
||||
self.sample_count = counts.values.sum
|
||||
self.gekanator_game = gekanator_game if gekanator_game.present?
|
||||
self.source = source
|
||||
|
||||
apply_aggregated_answer!(preferred_answer: answer)
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_learning_state
|
||||
counts = normalized_answer_counts
|
||||
|
||||
if counts.values.sum.zero? && answer.present?
|
||||
counts[answer] = 1
|
||||
end
|
||||
|
||||
self.answer_counts = counts
|
||||
self.sample_count = counts.values.sum
|
||||
|
||||
apply_aggregated_answer!
|
||||
end
|
||||
|
||||
def apply_aggregated_answer!(preferred_answer: nil)
|
||||
counts = normalized_answer_counts
|
||||
known_counts = counts.slice(*NON_UNKNOWN_ANSWERS)
|
||||
known_total = known_counts.values.sum
|
||||
|
||||
if known_total.zero?
|
||||
self.answer = 'unknown'
|
||||
self.weight = 0.1
|
||||
return
|
||||
else
|
||||
max_count = known_counts.values.max
|
||||
candidates = known_counts.select { |_answer, count| count == max_count }.keys
|
||||
self.answer =
|
||||
if preferred_answer.present? && candidates.include?(preferred_answer)
|
||||
preferred_answer
|
||||
elsif answer.present? && candidates.include?(answer)
|
||||
answer
|
||||
else
|
||||
candidates.first
|
||||
end
|
||||
end
|
||||
|
||||
consensus = max_count.to_f / known_total
|
||||
self.weight = Math.sqrt(known_total) * consensus
|
||||
end
|
||||
|
||||
def normalized_answer_counts
|
||||
base = ANSWERS.index_with(0)
|
||||
|
||||
answer_counts.to_h.each do |key, value|
|
||||
answer_key = key.to_s
|
||||
next unless ANSWERS.include?(answer_key)
|
||||
|
||||
base[answer_key] = value.to_i
|
||||
end
|
||||
|
||||
base
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class GekanatorQuestionSuggestion < ApplicationRecord
|
||||
ANSWERS = ['yes', 'no', 'partial', 'probably_no', 'unknown'].freeze
|
||||
|
||||
belongs_to :gekanator_game
|
||||
belongs_to :user
|
||||
has_many :gekanator_questions, dependent: :nullify
|
||||
has_many :gekanator_ai_runs, dependent: :destroy
|
||||
|
||||
validates :question_text, presence: true, length: { maximum: 1000 }
|
||||
validates :answer, presence: true, inclusion: { in: ANSWERS }
|
||||
validates :processed, inclusion: { in: [true, false] }
|
||||
end
|
||||
@@ -7,10 +7,23 @@ class Post < ApplicationRecord
|
||||
has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :post
|
||||
has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
|
||||
has_many :tags, through: :active_post_tags
|
||||
has_many :active_tags, -> { where(tags: { deprecated_at: nil }) },
|
||||
through: :active_post_tags, source: :tag
|
||||
|
||||
has_many :user_post_views, dependent: :delete_all
|
||||
has_many :post_similarities, dependent: :delete_all
|
||||
has_many :post_versions
|
||||
has_many :gekanator_guessed_games,
|
||||
class_name: 'GekanatorGame',
|
||||
foreign_key: :guessed_post_id,
|
||||
dependent: :delete_all,
|
||||
inverse_of: :guessed_post
|
||||
has_many :gekanator_correct_games,
|
||||
class_name: 'GekanatorGame',
|
||||
foreign_key: :correct_post_id,
|
||||
dependent: :delete_all,
|
||||
inverse_of: :correct_post
|
||||
has_many :gekanator_question_examples, dependent: :delete_all
|
||||
|
||||
has_many :parent_post_implications,
|
||||
class_name: 'PostImplication',
|
||||
@@ -125,7 +138,7 @@ class Post < ApplicationRecord
|
||||
return if !(f) || !(b)
|
||||
|
||||
if f >= b
|
||||
errors.add :original_created_before, 'オリジナルの作成日時の順番がをかしぃです.'
|
||||
errors.add :original_created_at, 'オリジナルの作成日時の順番がをかしぃです.'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+24
-1
@@ -8,6 +8,15 @@ class Tag < ApplicationRecord
|
||||
;
|
||||
end
|
||||
|
||||
class DeprecatedTagNormalisationError < ArgumentError
|
||||
attr_reader :tag_names
|
||||
|
||||
def initialize tag_names
|
||||
@tag_names = Array(tag_names)
|
||||
super('deprecated tags are not allowed')
|
||||
end
|
||||
end
|
||||
|
||||
has_many :post_tags, inverse_of: :tag
|
||||
has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag
|
||||
has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
|
||||
@@ -58,6 +67,7 @@ class Tag < ApplicationRecord
|
||||
validate :nico_tag_name_must_start_with_nico
|
||||
validate :tag_name_must_be_canonical
|
||||
validate :category_must_be_deerjikist_with_deerjikists
|
||||
validate :nico_tags_cannot_be_deprecated
|
||||
|
||||
scope :nico_tags, -> { nico }
|
||||
|
||||
@@ -77,11 +87,13 @@ class Tag < ApplicationRecord
|
||||
(self.tag_name ||= build_tag_name).name = val
|
||||
end
|
||||
|
||||
def deprecated? = deprecated_at?
|
||||
|
||||
def has_wiki = wiki_page.present?
|
||||
|
||||
def material_id = materials.first&.id
|
||||
|
||||
def has_deerjikists = deerjikists.present?
|
||||
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
||||
|
||||
def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
|
||||
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
|
||||
@@ -93,6 +105,7 @@ class Tag < ApplicationRecord
|
||||
def self.normalise_tags! tag_names, with_tagme: true,
|
||||
with_no_deerjikist: true,
|
||||
deny_nico: true,
|
||||
deny_deprecated: false,
|
||||
with_sections: false
|
||||
if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
raise NicoTagNormalisationError
|
||||
@@ -119,6 +132,10 @@ class Tag < ApplicationRecord
|
||||
name = TagName.canonicalise(name).first
|
||||
|
||||
find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
|
||||
if deny_deprecated && tag.deprecated?
|
||||
raise DeprecatedTagNormalisationError, [tag.name]
|
||||
end
|
||||
|
||||
tag.update!(category: cat) if cat && tag.category != cat
|
||||
next if sections_by_tag.empty?
|
||||
|
||||
@@ -275,4 +292,10 @@ class Tag < ApplicationRecord
|
||||
|
||||
total_s * 1_000 + ms.to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
|
||||
def nico_tags_cannot_be_deprecated
|
||||
if nico? && deprecated_at.present?
|
||||
errors.add :deprecated_at, 'ニコタグは廃止できません.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,10 @@ class Theatre < ApplicationRecord
|
||||
class_name: 'TheatreWatchingUser', inverse_of: :theatre
|
||||
has_many :watching_users, through: :active_theatre_watching_users, source: :user
|
||||
|
||||
has_many :programmes, class_name: 'TheatreProgramme'
|
||||
has_many :skip_votes, class_name: 'TheatreSkipVote', dependent: :delete_all
|
||||
has_many :skip_events, class_name: 'TheatreSkipEvent', dependent: :delete_all
|
||||
|
||||
belongs_to :host_user, class_name: 'User', optional: true
|
||||
belongs_to :current_post, class_name: 'Post', optional: true
|
||||
belongs_to :created_by_user, class_name: 'User'
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class TheatreProgramme < ApplicationRecord
|
||||
self.primary_key = :theatre_id, :position
|
||||
|
||||
belongs_to :theatre
|
||||
belongs_to :post
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
class TheatreSkipEvent < ApplicationRecord
|
||||
belongs_to :theatre
|
||||
belongs_to :post
|
||||
belongs_to :skipped_by_user, class_name: 'User'
|
||||
|
||||
has_many :voters, class_name: 'TheatreSkipEventVoter', dependent: :delete_all
|
||||
has_many :event_tags, class_name: 'TheatreSkipEventTag', dependent: :delete_all
|
||||
has_many :users, through: :voters
|
||||
has_many :tags, through: :event_tags
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class TheatreSkipEventTag < ApplicationRecord
|
||||
self.primary_key = :theatre_skip_event_id, :tag_id
|
||||
|
||||
belongs_to :theatre_skip_event
|
||||
belongs_to :tag
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class TheatreSkipEventVoter < ApplicationRecord
|
||||
self.primary_key = :theatre_skip_event_id, :user_id
|
||||
|
||||
belongs_to :theatre_skip_event
|
||||
belongs_to :user
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class TheatreSkipVote < ApplicationRecord
|
||||
self.primary_key = :theatre_id, :post_id, :user_id
|
||||
|
||||
belongs_to :theatre
|
||||
belongs_to :post
|
||||
belongs_to :user
|
||||
end
|
||||
@@ -22,6 +22,7 @@ class WikiPage < ApplicationRecord
|
||||
validates :body, presence: true
|
||||
|
||||
def title = tag_name.name
|
||||
def deprecated_at = tag_name.tag&.deprecated_at
|
||||
|
||||
def title= val
|
||||
(self.tag_name ||= build_tag_name).name = val
|
||||
|
||||
新しい課題から参照
ユーザをブロックする