diff --git a/app/lib/presenter.rb b/app/lib/presenter.rb index a5a1dfb5..bce12048 100644 --- a/app/lib/presenter.rb +++ b/app/lib/presenter.rb @@ -43,5 +43,53 @@ def self.normalized_screen_name(screen_name) screen_name.gsub!(' ', '') screen_name.gsub('@', '') end + + def self.formatted_product_names_for_tweet(character_name) + products = Character.find_by(name: character_name).products + + product_name_long_to_short = { + '幻想水滸伝' => 'I', + '幻想水滸伝II' => 'II', + '幻想水滸外伝Vol.1' => '外1', + '幻想水滸外伝Vol.2' => '外2', + '幻想水滸伝III' => 'III', + '幻想水滸伝IV' => 'IV', + 'Rhapsodia' => 'R', + '幻想水滸伝V' => 'V', + '幻想水滸伝ティアクライス' => 'TK', + '幻想水滸伝 紡がれし百年の時' => '紡時' + } + + "(#{products.map { |product| product_name_long_to_short[product.name] }.join(',')})" + end + end + + class Counting + # { "key1" => 100, "key2" => 99, "key3" => 99, "key4" => 98 } の入力に対し、 + # { "key1" => 1, "key2" => 2, "key3" => 2, "key4" => 3 } を返す + # 票数の降順でソートしてあることが前提となるので注意する + def self.key_to_rank_number_by_sosenkyo_style(hash_records) + current_rank = 1 + key_to_rank_number = {} + hash_records_keys = hash_records.keys + + hash_records_keys.each_with_index do |key, index| + if index == 0 + key_to_rank_number[key] = current_rank + + next + end + + if hash_records[key] == hash_records[hash_records_keys[index - 1]] + key_to_rank_number[key] = key_to_rank_number[hash_records_keys[index - 1]] + else + current_rank += 1 + + key_to_rank_number[key] = current_rank + end + end + + key_to_rank_number + end end end diff --git a/app/models/counting_all_character.rb b/app/models/counting_all_character.rb index c2f8f5e4..2a7ccdaf 100644 --- a/app/models/counting_all_character.rb +++ b/app/models/counting_all_character.rb @@ -8,8 +8,10 @@ class CountingAllCharacter < ApplicationRecord scope :invisible, -> { where(is_invisible: true) } scope :out_of_counting, -> { where(is_out_of_counting: true) } scope :valid_records, -> { where(is_out_of_counting: false).where(is_invisible: false) } + scope :by_tweet, -> { where(vote_method: :by_tweet) } + scope :by_dm, -> { where(vote_method: :by_direct_message) } - enum vote_method: { by_tweet: 0, by_direct_message: 1, by_others: 99 } + enum vote_method: { by_tweet: 0, by_direct_message: 1, by_others: 99 }, _prefix: true def self.tweets_whose_invisible_status_is_different_between_sheet_and_database sheet_invisible_tweet_ids = CountingAllCharacter.invisible.pluck(:tweet_id) @@ -24,31 +26,60 @@ def self.tweets_whose_invisible_status_is_different_between_sheet_and_database result end - # キャラ1〜3は、AIがsuggestしたものに含まれるか否か? - # キャラ名はキャラデータベースにあるものと一致しているか? - # 同一人物の複数ツイートで複数計上していないか - def self.tweeted_all_characters - chara_1_column_characters = CountingAllCharacter.pluck(:chara_1) - chara_2_column_characters = CountingAllCharacter.pluck(:chara_2) - chara_3_column_characters = CountingAllCharacter.pluck(:chara_3) + def self.all_character_names_including_duplicated + chara_1_column_characters = CountingAllCharacter.valid_records.pluck(:chara_1) + chara_2_column_characters = CountingAllCharacter.valid_records.pluck(:chara_2) + chara_3_column_characters = CountingAllCharacter.valid_records.pluck(:chara_3) - (chara_1_column_characters + chara_2_column_characters + chara_3_column_characters).uniq.compact.sort + # 空文字は削除する + (chara_1_column_characters + chara_2_column_characters + chara_3_column_characters).compact.reject(&:empty?).sort end - def self.character_db_diff + def self.character_name_to_number_of_votes + all_character_names_including_duplicated.tally.sort_by { |_, v| v }.reverse.to_h + end + + def self.character_names_which_not_exist_in_character_db result = [] - tweeted_all_characters.each do |character| + all_character_names_including_duplicated.uniq.each do |character| result << character if Character.where(name: character).blank? end result end - # もとの行の内容が知りたい - # どうだったらOKでどうだったらNGなのか? - # キャラ計上数が4以上だとNG - # other_tweetがvalid_recordsでないならスルー + # character_names のうちのどれか一つのキャラ名が含まれているかどうか + def includes_either_character_name?(*character_names) + character_names_on_record = [chara_1, chara_2, chara_3].compact.reject(&:empty?) + + return false if character_names_on_record.blank? + + character_names.any? do |character_name| + character_name.in?(character_names_on_record) + end + end + + # character_names の全てのキャラ名が含まれているかどうか + def includes_all_character_names?(*character_names) + character_names_on_record = [chara_1, chara_2, chara_3].compact.reject(&:empty?) + + return false if character_names_on_record.blank? + + character_names.all? do |character_name| + character_name.in?(character_names_on_record) + end + end + + def other_tweets + return if vote_method_by_direct_message? + + CountingAllCharacter.includes(:tweet).where( + tweet: { id_number: other_tweet_ids_text.split(',') } + ) + end + + # FIXME: このメソッドは使用していない def self.check_other_tweets result = [] @@ -64,14 +95,4 @@ def self.check_other_tweets result end - - def other_tweets - # CountingAllCharacter.where('other_tweet_ids_text like ?', '%|%') - CountingAllCharacter.includes(:tweet).where( - tweet: - { - id_number: other_tweet_ids_text.split(',') - } - ) - end end diff --git a/app/models/counting_bonus_vote.rb b/app/models/counting_bonus_vote.rb new file mode 100644 index 00000000..43cef553 --- /dev/null +++ b/app/models/counting_bonus_vote.rb @@ -0,0 +1,45 @@ +class CountingBonusVote < ApplicationRecord + include CountingTools + + belongs_to :tweet, optional: true + belongs_to :direct_message, optional: true + belongs_to :user # 鍵でも User のレコードは取得できるので optional 指定は不要 + + scope :invisible, -> { where(is_invisible: true) } + scope :out_of_counting, -> { where(is_out_of_counting: true) } + scope :valid_records, -> { where(is_out_of_counting: false).where(is_invisible: false) } + scope :by_tweet, -> { where(vote_method: :by_tweet) } + scope :by_dm, -> { where(vote_method: :by_direct_message) } + + enum vote_method: { by_tweet: 0, by_direct_message: 1, by_others: 99 }, _prefix: true + enum bonus_category: { + op_cl_illustrations: 0, + short_stories: 1, + result_illustrations: 2, + fav_quotes: 3, + sosenkyo_campaigns: 4 + }, _prefix: true + + def theme_on_short_story + # TODO: contents に「お題」が含まれている場合は、そのお題を返す + end + + def self.all_fav_quote_character_names_including_duplicated + chara_columns = %i[chara_01 chara_02 chara_03 chara_04 chara_05 chara_06 chara_07 chara_08 chara_09 chara_10] + character_names = [] + + chara_columns.each do |column| + character_names += CountingBonusVote.valid_records.where(bonus_category: :fav_quotes).pluck(column) + end + + character_names.compact.reject(&:empty?).sort + end + + def self.fav_quote_character_name_to_number_of_votes + all_fav_quote_character_names_including_duplicated.tally.sort_by { |_, v| v }.reverse.to_h + end + + def self.fav_quote_ranking_style + Presenter::Counting.key_to_rank_number_by_sosenkyo_style(fav_quote_character_name_to_number_of_votes) + end +end diff --git a/app/models/counting_unite_attack.rb b/app/models/counting_unite_attack.rb index b2ca99f3..3bf8a284 100644 --- a/app/models/counting_unite_attack.rb +++ b/app/models/counting_unite_attack.rb @@ -7,7 +7,54 @@ class CountingUniteAttack < ApplicationRecord scope :invisible, -> { where(is_invisible: true) } scope :out_of_counting, -> { where(is_out_of_counting: true) } - scope :valid_records, -> { where(is_out_of_counting: false).where(is_invisible: false) } + scope :no_data, -> { where(product_name: [nil, '']).where(unite_attack_name: [nil, '']) } + scope :valid_records, lambda { + where(is_invisible: false) + .where(is_out_of_counting: false) + .where.not(product_name: [nil, '']) + .where.not(unite_attack_name: [nil, '']) + } + scope :by_tweet, -> { where(vote_method: :by_tweet) } + scope :by_dm, -> { where(vote_method: :by_direct_message) } - enum vote_method: { by_tweet: 0, by_direct_message: 1, by_others: 99 } + enum vote_method: { by_tweet: 0, by_direct_message: 1, by_others: 99 }, _prefix: true + + def self.full_ranking + group(:product_name, :unite_attack_name).having('unite_attack_name is not null').order('count_all desc').count + end + + def self.product_name_ranking + group(:product_name).having('product_name is not null').order('count_all desc').count + end + + # 不正レコードのチェッカ + def self.invalid_records_whose_product_name_is_incorrect + correct_product_names = NaturalLanguage::SuggestUniteAttackNames.title_names + + invalid_records = [] + + valid_records.each do |record| + invalid_records << record unless record.product_name.in?(correct_product_names) + end + + invalid_records + end + + # 不正レコードのチェッカ + def self.invalid_records_whose_attack_name_is_incorrect + correct_attack_names = OnRawSheetUniteAttack.pluck(:name, :name_en).flatten.reject(&:empty?) + + invalid_records = [] + + valid_records.each do |record| + invalid_records << record unless record.unite_attack_name.in?(correct_attack_names) + end + + invalid_records + end + + # 'English' というかは 'Not Japanese' であるのだが、便宜上 'English' とする + def self.english_records + includes(:tweet).valid_records.where.not(tweet: { language: 'ja' }) + end end diff --git a/app/models/direct_message.rb b/app/models/direct_message.rb index 62b66f35..a63de61b 100644 --- a/app/models/direct_message.rb +++ b/app/models/direct_message.rb @@ -17,6 +17,10 @@ def self.missing_records where(id_number: 1540436647376031755..1540790299618066435) end + def self.remove_missing_records + where.not(id_number: 1540436647376031755..1540790299618066435) + end + def is_missing_record? id_number.in?(1540436647376031755..1540790299618066435) end diff --git a/app/models/on_raw_sheet_result_illustration_totalling.rb b/app/models/on_raw_sheet_result_illustration_totalling.rb index ca2a6c21..f95c09bf 100644 --- a/app/models/on_raw_sheet_result_illustration_totalling.rb +++ b/app/models/on_raw_sheet_result_illustration_totalling.rb @@ -1,3 +1,13 @@ class OnRawSheetResultIllustrationTotalling < ApplicationRecord # on_raw_sheet_result_illustration_totalling + def convert_name_to_gensosenkyo_style(name) + { + "ほげ" => "ほげげげ" + }[name] || name + end end + +# irb(main):003:0> x = { a: 1, b: 2, c: 3 } +# irb(main):004:0> y = { a: 4, c: 5, d: 10 } +# irb(main):007:0> x.merge(y) { |_, oldval, newval| oldval + newval } +# => {:a=>5, :b=>2, :c=>8, :d=>10} diff --git a/app/models/user.rb b/app/models/user.rb index 0405cbf4..22e15156 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -38,4 +38,8 @@ def self.who_vote_two_or_more_without_not_public def self.did_vote_without_not_public self.select { |user| user.tweets.gensosenkyo_2021_votes.is_public.count > 0 } end + + def all_counting_records + # TODO: 集計対象となっている全てのレコードを引っ張ってこられる + end end diff --git a/app/services/sheets/counting/all_characters.rb b/app/services/sheets/counting/all_characters.rb index 801a634d..91eaeb8c 100644 --- a/app/services/sheets/counting/all_characters.rb +++ b/app/services/sheets/counting/all_characters.rb @@ -9,9 +9,9 @@ def self.import_via_tweet rows = SheetData.get_rows(sheet_id: ENV.fetch('COUNTING_ALL_CHARACTERS_SHEET_ID', nil), range: "#{sheet_names[i]}!A2:Q101") rows.each do |row| - # TODO: 設定ファイルを用いてよりスマートに定義したい column_vs_value = { id_on_sheet: row[0], + screen_name: row[1], tweet_id_number: row[2], other_tweet_ids_text: row[5], is_invisible: row[7], # "FALSE" のような文字列なので注意 @@ -26,8 +26,10 @@ def self.import_via_tweet next if column_vs_value[:id_on_sheet].blank? || column_vs_value[:tweet_id_number].blank? || column_vs_value[:contents].blank? tweet = Tweet.find_by(id_number: column_vs_value[:tweet_id_number]) - tweet_id = tweet.id - user_id = tweet.user.id + tweet_id = tweet&.id + + user = tweet&.user || create_or_find_by_user(column_vs_value) + user_id = user&.id unique_attrs = { id_on_sheet: column_vs_value[:id_on_sheet], @@ -50,6 +52,8 @@ def self.import_via_tweet CountingAllCharacter.find_or_initialize_by(unique_attrs).update!(mutable_attrs) end + + puts "#{sheet_name} is Done." # rubocop:disable Rails/Output end end @@ -68,6 +72,7 @@ def self.import_via_dm # TODO: 設定ファイルを用いてスマートに定義したい column_vs_value = { id_on_sheet: row[0], + screen_name: row[1], dm_id_number: row[2], is_invisible: row[5], # "FALSE" のような文字列なので注意 is_out_of_counting: row[6], # "FALSE" のような文字列なので注意 @@ -88,12 +93,15 @@ def self.import_via_dm # DMの書式が自由すぎるので、こちらで条件を吸収する next if column_vs_value[:category] != '①オールキャラ部門' && column_vs_value[:category] != '両部門' - + next if column_vs_value[:is_invisible] == 'TRUE' || column_vs_value[:is_out_of_counting] == 'TRUE' next if column_vs_value[:id_on_sheet].blank? || column_vs_value[:dm_id_number].blank? || column_vs_value[:contents].blank? dm = DirectMessage.find_by(id_number: column_vs_value[:dm_id_number]) - dm_id = dm.id - user_id = dm.user.id + dm_id = dm&.id + + # NOTE: dm&.user は不要としてもいい + user = dm&.user || create_or_find_by_user(column_vs_value) + user_id = user&.id unique_attrs = { id_on_sheet: column_vs_value[:id_on_sheet], @@ -135,6 +143,34 @@ def self.import_via_dm '[DONE] Sheets::Counting::AllCharacters.import_via_dm' end + + # screen_name でユーザーを一意に特定するのははあまり良くない + def self.create_or_find_by_user(column_vs_value) + existing_user = User.find_by(screen_name: column_vs_value[:screen_name]) + + if existing_user.blank? + client = TwitterRestApi.client + # ここで API を消費する + user = client.user(column_vs_value[:screen_name]) + + # NOTE: ユーザーが削除されていると User not found. (Twitter::Error::NotFound) が発生する + # NOTE: その場合はシートのデータを修正する必要がある + + new_user = User.new( + id_number: user.id, + name: user.name, + screen_name: user.screen_name, + profile_image_url_https: user.profile_image_url_https.to_s, + is_protected: user.protected?, + born_at: user.created_at + ) + new_user.save! + + new_user + else + existing_user + end + end end end end diff --git a/app/services/sheets/counting/bonus_votes.rb b/app/services/sheets/counting/bonus_votes.rb new file mode 100644 index 00000000..ecb326f6 --- /dev/null +++ b/app/services/sheets/counting/bonus_votes.rb @@ -0,0 +1,269 @@ +module Sheets + module Counting + class BonusVotes + def self.import_via_tweet + ActiveRecord::Base.transaction do + sheet_names.each_with_index do |sheet_name, i| + bonus_category_to_sheet_id.each do |bonus_category, this_sheet_id| + next if this_sheet_id.blank? + + rows = SheetData.get_rows(sheet_id: this_sheet_id, range: "#{sheet_names[i]}!A2:Q101") + + rows.each do |row| + column_vs_value = column_vs_value(row) + + next if column_vs_value[:id_on_sheet].blank? || column_vs_value[:tweet_id_number].blank? || column_vs_value[:screen_name].blank? || column_vs_value[:contents].blank? + + tweet_id, user_id, this_is_recovered = define_or_create_tweet_and_user_and_is_recovered(sheet_name, column_vs_value) + + unique_attrs = { + id_on_sheet: column_vs_value[:id_on_sheet], + user_id: user_id, + vote_method: :by_tweet, + bonus_category: bonus_category, + is_recovered: this_is_recovered, + tweet_id: tweet_id, + contents: column_vs_value[:contents], + memo: column_vs_value[:memo] + } + + mutable_attrs = { + # 123456,654321,777777 のような文字列になる + other_tweet_ids_text: column_vs_value[:other_tweet_ids_text].split('|').map(&:strip).join(','), + is_invisible: column_vs_value[:is_invisible].to_boolean, + is_out_of_counting: column_vs_value[:is_out_of_counting].to_boolean, + chara_01: column_vs_value[:input_01], + chara_02: column_vs_value[:input_02], + chara_03: column_vs_value[:input_03], + chara_04: column_vs_value[:input_04], + chara_05: column_vs_value[:input_05], + chara_06: column_vs_value[:input_06], + chara_07: column_vs_value[:input_07], + chara_08: column_vs_value[:input_08], + chara_09: column_vs_value[:input_09], + chara_10: column_vs_value[:input_10] + } + + CountingBonusVote.find_or_initialize_by(unique_attrs).update!(mutable_attrs) + end + end + end + end + + '[DONE] Sheets::Counting::BonusVotes.import_via_tweet' + end + + def self.import_via_dm + ActiveRecord::Base.transaction do + sheet_names.each_with_index do |sheet_name, i| + rows = SheetData.get_rows(sheet_id: ENV.fetch('COUNTING_DIRECT_MESSAGES_SHEET_ID', nil), range: "#{sheet_names[i]}!A2:Q101") + + rows.each do |row| + dm_column_vs_value = dm_column_vs_value(row) + + next unless dm_column_vs_value[:category].in?(dm_target_categories) + + next if dm_column_vs_value[:id_on_sheet].blank? || dm_column_vs_value[:dm_id_number].blank? || dm_column_vs_value[:screen_name].blank? || dm_column_vs_value[:contents].blank? + + dm_id, user_id, this_is_recovered = define_or_create_dm_and_user_and_is_recovered(sheet_name, dm_column_vs_value) + + unique_attrs = { + id_on_sheet: dm_column_vs_value[:id_on_sheet], + user_id: user_id, + vote_method: :by_direct_message, + bonus_category: dm_sheet_category_to_bonus_category[dm_column_vs_value[:category]], + direct_message_id: dm_id, + is_recovered: this_is_recovered, + other_tweet_ids_text: nil, + contents: dm_column_vs_value[:contents], + memo: dm_column_vs_value[:memo] + } + + mutable_attrs = { + is_invisible: dm_column_vs_value[:is_invisible].to_boolean, + is_out_of_counting: dm_column_vs_value[:is_out_of_counting].to_boolean, + chara_01: dm_column_vs_value[:input_01], + chara_02: dm_column_vs_value[:input_02], + chara_03: dm_column_vs_value[:input_03], + chara_04: dm_column_vs_value[:input_04], + chara_05: dm_column_vs_value[:input_05], + chara_06: dm_column_vs_value[:input_06], + chara_07: dm_column_vs_value[:input_07], + chara_08: dm_column_vs_value[:input_08], + chara_09: dm_column_vs_value[:input_09], + chara_10: dm_column_vs_value[:input_10] + } + + CountingBonusVote.find_or_initialize_by(unique_attrs).update!(mutable_attrs) + end + + puts "#{sheet_names[i]} is Done." # rubocop:disable Rails/Output + end + end + + '[DONE] Sheets::Counting::BonusVotes.import_via_dm' + end + + def self.define_or_create_tweet_and_user_and_is_recovered(sheet_name, column_vs_value) + if sheet_name == '取得漏れ等' + # Tweetを作ってしまうとシートに流し込んだときにズレるので絶対に駄目 + tweet_id = nil + this_is_recovered = true + + existing_user = User.find_by(screen_name: column_vs_value[:screen_name]) + + if existing_user.blank? + client = TwitterRestApi.client + # ここで API を消費する + user = client.user(column_vs_value[:screen_name]) + + new_user = User.new( + id_number: user.id, + name: user.name, + screen_name: user.screen_name, + profile_image_url_https: user.profile_image_url_https.to_s, + is_protected: user.protected?, + born_at: user.created_at + ) + new_user.save! + + user_id = user.id + else + user_id = existing_user.id + end + else + tweet = Tweet.find_by(id_number: column_vs_value[:tweet_id_number]) + + tweet_id = tweet.id + user_id = tweet.user.id + this_is_recovered = false + end + + [tweet_id, user_id, this_is_recovered] + end + + def self.dm_sheet_category_to_bonus_category + { + 'ボ・OP・CLイラスト' => :op_cl_illustrations, + 'ボ・お題小説' => :short_stories, + 'ボ・開票イラスト' => :result_illustrations, + 'ボ・推し台詞' => :fav_quotes, + 'ボ・選挙運動' => :sosenkyo_campaigns + } + end + + def self.bonus_category_to_sheet_id + { + op_cl_illustrations: nil, # これは DM で受け取る + short_stories: ENV.fetch('COUNTING_BONUS_SHORT_STORIES_SHEET_ID', nil), + result_illustrations: nil, # シートの設計が根本的に違う + fav_quotes: ENV.fetch('COUNTING_BONUS_FAV_QUOTES_SHEET_ID', nil), + sosenkyo_campaigns: ENV.fetch('COUNTING_BONUS_SOSENKYO_CAMPAIGNS_SHEET_ID', nil) + } + end + + def self.sheet_names + YAML.load_file(Rails.root.join('config/counting_sheet_names.yml'))['names'] + end + + def self.column_vs_value(row) + { + id_on_sheet: row[0], + screen_name: row[1], + tweet_id_number: row[2], + other_tweet_ids_text: row[5], + is_invisible: row[7], # "FALSE" のような文字列なので注意 + is_out_of_counting: row[8], # "FALSE" のような文字列なので注意 + contents: row[11], + memo: row[12], + input_01: row[14], + input_02: row[15], + input_03: row[16], + input_04: row[17], + input_05: row[18], + input_06: row[19], + input_07: row[20], + input_08: row[21], + input_09: row[22], + input_10: row[23] + } + end + + def self.dm_column_vs_value(row) + { + id_on_sheet: row[0], + screen_name: row[1], + dm_id_number: row[2], + is_invisible: row[5], # "FALSE" のような文字列なので注意 + is_out_of_counting: row[6], # "FALSE" のような文字列なので注意 + category: row[9], + contents: row[10], + memo: row[11], + input_01: row[13], + input_02: row[14], + input_03: row[15], + input_04: row[16], + input_05: row[17], + input_06: row[18], + input_07: row[19], + input_08: row[20], + input_09: row[21], + input_10: row[22] + } + end + + def self.dm_target_categories + [ + 'ボ・OP・CLイラスト', + 'ボ・お題小説', + 'ボ・開票イラスト', + 'ボ・推し台詞', + 'ボ・選挙運動', + ] + end + + def self.define_or_create_dm_and_user_and_is_recovered(sheet_name, dm_column_vs_value) + user = create_or_find_by_user(dm_column_vs_value) + + if sheet_name == '取得漏れ等' + dm_id = nil + user_id = user.id + this_is_recovered = true + else + dm = DirectMessage.find_by(id_number: dm_column_vs_value[:dm_id_number]) + + dm_id = dm&.id + user_id = dm&.user&.id || user.id + this_is_recovered = false + end + + [dm_id, user_id, this_is_recovered] + end + + # screen_name はあまり良くない + def self.create_or_find_by_user(dm_column_vs_value) + existing_user = User.find_by(screen_name: dm_column_vs_value[:screen_name]) + + if existing_user.blank? + client = TwitterRestApi.client + # ここで API を消費する + user = client.user(dm_column_vs_value[:screen_name]) + + new_user = User.new( + id_number: user.id, + name: user.name, + screen_name: user.screen_name, + profile_image_url_https: user.profile_image_url_https.to_s, + is_protected: user.protected?, + born_at: user.created_at + ) + new_user.save! + + new_user + else + existing_user + end + end + end + end +end diff --git a/app/services/sheets/counting/unite_attacks.rb b/app/services/sheets/counting/unite_attacks.rb index f8d00113..40cd06e8 100644 --- a/app/services/sheets/counting/unite_attacks.rb +++ b/app/services/sheets/counting/unite_attacks.rb @@ -12,6 +12,7 @@ def self.import_via_tweet # TODO: 設定ファイルを用いてスマートに定義したい column_vs_value = { id_on_sheet: row[0], + screen_name: row[1], tweet_id_number: row[2], other_tweet_ids_text: row[5], is_invisible: row[7], # "FALSE" のような文字列なので注意 @@ -24,9 +25,16 @@ def self.import_via_tweet next if column_vs_value[:id_on_sheet].blank? || column_vs_value[:tweet_id_number].blank? || column_vs_value[:contents].blank? - tweet = Tweet.find_by(id_number: column_vs_value[:tweet_id_number]) - tweet_id = tweet.id - user_id = tweet.user.id + # TODO: 綺麗じゃないので直したい + if sheet_name == '取得漏れ等' + tweet_id = nil + user = create_or_find_by_user(column_vs_value) + user_id = user.id + else + tweet = Tweet.find_by(id_number: column_vs_value[:tweet_id_number]) + tweet_id = tweet.id + user_id = tweet.user.id + end unique_attrs = { id_on_sheet: column_vs_value[:id_on_sheet], @@ -48,6 +56,8 @@ def self.import_via_tweet CountingUniteAttack.find_or_initialize_by(unique_attrs).update!(mutable_attrs) end + + puts "#{sheet_name} is Done." # rubocop:disable Rails/Output end end @@ -65,6 +75,7 @@ def self.import_via_dm # TODO: 設定ファイルを用いてスマートに定義したい column_vs_value = { id_on_sheet: row[0], + screen_name: row[1], dm_id_number: row[2], is_invisible: row[5], # "FALSE" のような文字列なので注意 is_out_of_counting: row[6], # "FALSE" のような文字列なので注意 @@ -118,6 +129,34 @@ def self.import_via_dm '[DONE] Sheets::Counting::UniteAttacks.import_via_dm' end + + # screen_name でユーザーを一意に特定するのははあまり良くない + def self.create_or_find_by_user(column_vs_value) + existing_user = User.find_by(screen_name: column_vs_value[:screen_name]) + + if existing_user.blank? + client = TwitterRestApi.client + # ここで API を消費する + user = client.user(column_vs_value[:screen_name]) + + # NOTE: ユーザーが削除されていると User not found. (Twitter::Error::NotFound) が発生する + # NOTE: その場合はシートのデータを修正する必要がある + + new_user = User.new( + id_number: user.id, + name: user.name, + screen_name: user.screen_name, + profile_image_url_https: user.profile_image_url_https.to_s, + is_protected: user.protected?, + born_at: user.created_at + ) + new_user.save! + + new_user + else + existing_user + end + end end end end diff --git a/app/services/sheets/write_and_update/direct_messages.rb b/app/services/sheets/write_and_update/direct_messages.rb index 9337607f..1c33e6cc 100644 --- a/app/services/sheets/write_and_update/direct_messages.rb +++ b/app/services/sheets/write_and_update/direct_messages.rb @@ -6,15 +6,13 @@ def self.exec(complement_missing_messages: false) direct_messages = if complement_missing_messages DirectMessage.missing_records.for_spreadsheet else - DirectMessage.for_spreadsheet + DirectMessage.remove_missing_records.for_spreadsheet end direct_messages.each_slice(100).with_index do |dm_100, index_on_hundred| prepared_written_data_by_array_in_hash = [] - dm_100.each_with_index do |dm, i| - next if complement_missing_messages == false && dm.is_missing_record? - + dm_100.each do |dm| inserted_hash = {} inserted_hash['screen_name'] = dm.user.screen_name diff --git a/app/services/sheets/write_and_update/final_results/all_characters_stand_alone.rb b/app/services/sheets/write_and_update/final_results/all_characters_stand_alone.rb new file mode 100644 index 00000000..d1cf60df --- /dev/null +++ b/app/services/sheets/write_and_update/final_results/all_characters_stand_alone.rb @@ -0,0 +1,75 @@ +# ボーナス票・推し台詞 +module Sheets + module WriteAndUpdate + module FinalResults + class AllCharactersStandAlone + def initialize + @sheet_name = '単体・①オールキャラ部門' + @column_name_to_index_hash = { + id: 0, + 順位: 1, + キャラ名: 2, + 得票数: 3, + 開票イラストがある?: 5, + 推しセリフがある?: 6, + 登場作品名: 7, + キャラDBに存在する?: 8 + } + end + + def exec + hash_records = CountingAllCharacter.character_name_to_number_of_votes + key_to_rank_number = Presenter::Counting.key_to_rank_number_by_sosenkyo_style(hash_records) + written_data = [] + + hash_records.each_with_index do |(character_name, number_of_votes), index| + row = [] + + is_exists_in_character_db = Character.where(name: character_name).present? + product_names = is_exists_in_character_db ? Presenter::Common.formatted_product_names_for_tweet(character_name) : '' + + # シートのキャラ名表記がキャラDBのキャラ名表記とは異なるので、対応表を作成して対処している + on_sheet_name_to_on_db_name = YAML.load_file( + Rails.root.join('config/character_names_on_result_illustrations_sheet.yml') + )['on_database_character_name_to_on_sheet_character_name'] + fixed_character_name = on_sheet_name_to_on_db_name[character_name] || character_name + + result_illustaration_characters = OnRawSheetResultIllustrationTotalling.pluck(:character_name_for_public) + is_fav_quotes_exists = character_name.in?(CountingBonusVote.all_fav_quote_character_names_including_duplicated) + + row[@column_name_to_index_hash[:id]] = index + 1 + row[@column_name_to_index_hash[:順位]] = key_to_rank_number[character_name] + row[@column_name_to_index_hash[:キャラ名]] = character_name + row[@column_name_to_index_hash[:得票数]] = number_of_votes + row[@column_name_to_index_hash[:開票イラストがある?]] = fixed_character_name.in?(result_illustaration_characters) + row[@column_name_to_index_hash[:推しセリフがある?]] = is_fav_quotes_exists + row[@column_name_to_index_hash[:登場作品名]] = product_names + row[@column_name_to_index_hash[:キャラDBに存在する?]] = is_exists_in_character_db + + written_data << row + end + + write(written_data) + end + + # TODO: 切り出せそう + def write(written_data) + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_FINAL_RESULTS_SHEET_ID', nil), + range: "#{@sheet_name}!A2", # 始点 + values: written_data + ) + end + + # TODO: 切り出せそう + def delete + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_FINAL_RESULTS_SHEET_ID', nil), + range: "#{@sheet_name}!A2:T500", + values: [[''] * 20] * 500 + ) + end + end + end + end +end diff --git a/app/services/sheets/write_and_update/final_results/unite_attacks.rb b/app/services/sheets/write_and_update/final_results/unite_attacks.rb new file mode 100644 index 00000000..2af80cc6 --- /dev/null +++ b/app/services/sheets/write_and_update/final_results/unite_attacks.rb @@ -0,0 +1,62 @@ +# ボーナス票・推し台詞 +module Sheets + module WriteAndUpdate + module FinalResults + class UniteAttacks + def initialize + @sheet_name = '②協力攻撃部門' + @column_name_to_index_hash = { + id: 0, + 順位: 1, + 作品名: 2, + 協力攻撃名: 3, + 全得票数: 4, + 投票方法内訳・ツイート: 8, + 投票方法内訳・DM: 9 + } + end + + def exec + hash_records = CountingUniteAttack.valid_records.full_ranking + key_to_rank_number = Presenter::Counting.key_to_rank_number_by_sosenkyo_style(hash_records) + written_data = [] + + hash_records.each_with_index do |(product_name_and_attack_name, number_of_votes), index| + row = [] + product_name = product_name_and_attack_name[0] + attack_name = product_name_and_attack_name[1] + + row[@column_name_to_index_hash[:id]] = index + 1 + row[@column_name_to_index_hash[:順位]] = key_to_rank_number[product_name_and_attack_name] + row[@column_name_to_index_hash[:作品名]] = product_name + row[@column_name_to_index_hash[:協力攻撃名]] = attack_name + row[@column_name_to_index_hash[:全得票数]] = number_of_votes + + # row[@column_name_to_index_hash[:投票方法内訳・ツイート]] = '' + # row[@column_name_to_index_hash[:投票方法内訳・DM]] = '' + + written_data << row + end + + write(written_data) + end + + def write(written_data) + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_FINAL_RESULTS_SHEET_ID', nil), + range: "#{@sheet_name}!A2", # 始点 + values: written_data + ) + end + + def delete + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_FINAL_RESULTS_SHEET_ID', nil), + range: "#{@sheet_name}!A2:T500", + values: [[''] * 20] * 500 + ) + end + end + end + end +end diff --git a/config/character_names_on_result_illustrations_sheet.yml b/config/character_names_on_result_illustrations_sheet.yml new file mode 100644 index 00000000..9ba80a62 --- /dev/null +++ b/config/character_names_on_result_illustrations_sheet.yml @@ -0,0 +1,38 @@ +on_database_character_name_to_on_sheet_character_name: + シーザー・シルバーバーグ: シーザー + アルベルト・シルバーバーグ: アルベルト + ワイアット・ライトフェロー(ジンバ): ジンバ + ジョウイ・アトレイド(ブライト): ジョウイ + ジル・ブライト: ジル + リムスレーア・ファレナス: リムスレーア + 幻水1主人公(坊ちゃん): 1主人公 + イリア・バルカイ: イリア + ルセリナ・バロウズ: ルセリナ + シエラ・ミケーネ: シエラ + フレッド・マクシミリアン: フレッド + 幻水4主人公(4様): 4主人公 + ルカ・ブライト: ルカ + フリード・Y: フリード・Y + ゲオルグ・プライム: ゲオルグ + ナッシュ・ラトキエ(クロービス): ナッシュ + 幻水5主人公(王子): 5主人公 + アルシュタート・ファレナス: アルシュタート + クリス・ライトフェロー: クリス + ザジ・キュイロス: ザジ + 幻水2主人公(2主): 2主人公 + ソロン・ジー: ソロン + ペック(暗器使い): ペック + ボルス・レッドラム: ボルス + サロメ・ハラス: サロメ + パーシヴァル・フロイライン: パーシヴァル + ルイス・キファーソン: ルイス + クラウス・ウィンダミア: クラウス + ティアクライス主人公(団長): TK主人公 + カーン・マリィ: カーン + ヨシュア・レーベンハイト: ヨシュア + ユーラム・バロウズ: ユーラム + サイアリーズ・ファレナス: サイアリーズ + オデッサ・シルバーバーグ: オデッサ + リウ・シエン: リウ + スノウ・フィンガーフート: スノウ + キルキス・シャナ・クエス・ラビアンカーナ: キルキス diff --git a/db/migrate/20220703105847_create_counting_bonus_votes.rb b/db/migrate/20220703105847_create_counting_bonus_votes.rb new file mode 100644 index 00000000..f4aa920a --- /dev/null +++ b/db/migrate/20220703105847_create_counting_bonus_votes.rb @@ -0,0 +1,30 @@ +class CreateCountingBonusVotes < ActiveRecord::Migration[7.0] + def change + create_table :counting_bonus_votes do |t| + t.integer :id_on_sheet # tweet の id と dm の id とで重複があり得る + t.integer :user_id, null: false + t.integer :bonus_category, null: false + t.integer :vote_method, null: false + t.integer :tweet_id # dm の場合は NULL があり得る + t.integer :direct_message_id + t.string :other_tweet_ids_text # "|" で区切った文字列 + t.boolean :is_invisible + t.boolean :is_out_of_counting + t.boolean :is_recovered + t.string :contents + t.string :memo + t.string :chara_01 + t.string :chara_02 + t.string :chara_03 + t.string :chara_04 + t.string :chara_05 + t.string :chara_06 + t.string :chara_07 + t.string :chara_08 + t.string :chara_09 + t.string :chara_10 + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 86f47a97..5cad100e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_07_03_055608) do +ActiveRecord::Schema[7.0].define(version: 2022_07_03_105847) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -80,6 +80,33 @@ t.datetime "updated_at", null: false end + create_table "counting_bonus_votes", force: :cascade do |t| + t.integer "id_on_sheet" + t.integer "user_id", null: false + t.integer "bonus_category", null: false + t.integer "vote_method", null: false + t.integer "tweet_id" + t.integer "direct_message_id" + t.string "other_tweet_ids_text" + t.boolean "is_invisible" + t.boolean "is_out_of_counting" + t.boolean "is_recovered" + t.string "contents" + t.string "memo" + t.string "chara_01" + t.string "chara_02" + t.string "chara_03" + t.string "chara_04" + t.string "chara_05" + t.string "chara_06" + t.string "chara_07" + t.string "chara_08" + t.string "chara_09" + t.string "chara_10" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "counting_unite_attacks", force: :cascade do |t| t.integer "id_on_sheet" t.integer "user_id", null: false diff --git a/import_all_counting_data.sh b/import_all_counting_data.sh new file mode 100755 index 00000000..b1b66b4d --- /dev/null +++ b/import_all_counting_data.sh @@ -0,0 +1,7 @@ +#!/bin/bash -xe + +bin/rails import_counting_all_characters:exec_all +bin/rails import_counting_bonus_votes:exec_all +bin/rails import_counting_unite_attacks:exec_all + +exit 0 diff --git a/lib/tasks/import_counting_all_characters.rake b/lib/tasks/import_counting_all_characters.rake index d778eb3b..29a18060 100644 --- a/lib/tasks/import_counting_all_characters.rake +++ b/lib/tasks/import_counting_all_characters.rake @@ -6,6 +6,12 @@ namespace :import_counting_all_characters do desc '「オールキャラ部門」のDM投票の開票データをスプレッドシートからインポートする' task exec_via_dm: :environment do - # Sheets::Counting::AllCharacters.import_via_dm + Sheets::Counting::AllCharacters.import_via_dm + end + + desc '「オールキャラ部門」のツイートとDMの両方の開票データをスプレッドシートからインポートする' + task exec_all: :environment do + Sheets::Counting::AllCharacters.import_via_tweet + Sheets::Counting::AllCharacters.import_via_dm end end diff --git a/lib/tasks/import_counting_bonus_votes.rake b/lib/tasks/import_counting_bonus_votes.rake new file mode 100644 index 00000000..ced8a4fa --- /dev/null +++ b/lib/tasks/import_counting_bonus_votes.rake @@ -0,0 +1,17 @@ +namespace :import_counting_bonus_votes do + desc '「ボーナス票」の各対象ツイートの開票データをスプレッドシートからインポートする' + task exec_via_tweet: :environment do + Sheets::Counting::BonusVotes.import_via_tweet + end + + desc '「ボーナス票」の各対象DM投票の開票データをスプレッドシートからインポートする' + task exec_via_dm: :environment do + Sheets::Counting::BonusVotes.import_via_dm + end + + desc '「ボーナス票」の各対象ツイートおよびDM投票の開票データをスプレッドシートからインポートする' + task exec_all: :environment do + Sheets::Counting::BonusVotes.import_via_tweet + Sheets::Counting::BonusVotes.import_via_dm + end +end diff --git a/lib/tasks/import_counting_unite_attacks.rake b/lib/tasks/import_counting_unite_attacks.rake index 0191d788..5e4fff84 100644 --- a/lib/tasks/import_counting_unite_attacks.rake +++ b/lib/tasks/import_counting_unite_attacks.rake @@ -8,4 +8,10 @@ namespace :import_counting_unite_attacks do task exec_via_dm: :environment do Sheets::Counting::UniteAttacks.import_via_dm end + + desc '「協力攻撃部門」のツイートおよびDM投票の両方の開票データをスプレッドシートからインポートする' + task exec_all: :environment do + Sheets::Counting::UniteAttacks.import_via_tweet + Sheets::Counting::UniteAttacks.import_via_dm + end end diff --git a/write_all_final_result_sheets.sh b/write_all_final_result_sheets.sh new file mode 100755 index 00000000..e95ecc18 --- /dev/null +++ b/write_all_final_result_sheets.sh @@ -0,0 +1,7 @@ +#!/bin/bash -xe + +bin/rails runner "Sheets::WriteAndUpdate::FinalResults::AllCharactersStandAlone.new.exec" +bin/rails runner "Sheets::WriteAndUpdate::FinalResults::UniteAttacks.new.exec" +# TODO: ボーナス票 + +exit 0