diff --git a/.gitignore b/.gitignore index 57416c89..ba1f9bab 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ vendor/ .history/ .env node_modules -.clasp.json +.clasp.* .clasprc.json clasp/**/*.js diff --git a/app/models/direct_message.rb b/app/models/direct_message.rb index d37c15f3..d7df08be 100644 --- a/app/models/direct_message.rb +++ b/app/models/direct_message.rb @@ -7,6 +7,12 @@ class DirectMessage < ApplicationRecord validates :id_number, uniqueness: true + def self.for_spreadsheet + to_gensosenkyo + .order(messaged_at: :asc) + .order(id_number: :asc) + end + # self.user と同義 def sender User.find_by(id_number: sender_id_number) diff --git a/app/services/sheets/write_and_update/all_characters.rb b/app/services/sheets/write_and_update/all_characters.rb new file mode 100644 index 00000000..d4b0c289 --- /dev/null +++ b/app/services/sheets/write_and_update/all_characters.rb @@ -0,0 +1,80 @@ +module Sheets + module WriteAndUpdate + class AllCharacters + # 列情報は clasp/gensosenkyo/ZzzColumnNames.ts を参考にする + def self.exec + # NOTE: for_api で Tweet を指定すると is_public の値によって冪等性が保証されないのでダメ + tweets = Tweet.gensosenkyo_2022_votes.valid_term_votes + + tweets.each_slice(100).with_index do |tweets_100, index_on_hundred| + prepared_written_data_by_array_in_hash = [] + + tweets_100.each_with_index do |tweet, i| + inserted_hash = {} + + user = tweet.user + by_user_other_tweets = tweets.where(user: user).where.not(id: tweet.id) + # カンマ区切りにすると to_s しても数値に変換されてしまう + by_user_other_tweets_for_sheet = by_user_other_tweets.map { |t| t.id_number.to_s }.join(' | ').to_s + + inserted_hash['screen_name'] = tweet.user.screen_name + inserted_hash['tweet_id'] = tweet.id_number.to_s + inserted_hash['日時'] = tweet.tweeted_at.strftime('%Y/%m/%d %H:%M:%S').to_s + inserted_hash['URL'] = tweet.url + inserted_hash['ツイ見られない?'] = !tweet.is_public + inserted_hash['別ツイ'] = by_user_other_tweets_for_sheet.to_s || '' + inserted_hash['ふぁぼ済?'] = true # to_s がいるかも + inserted_hash['内容'] = tweet.full_text + # この行のコストが高い + inserted_hash['suggested_names'] = NaturalLanguage::SuggestCharacterNames.exec(tweet) # Array + + prepared_written_data_by_array_in_hash << inserted_hash + end + + two_digit_number = format('%02d', number: index_on_hundred + 1) + sheet_name = "集計_#{two_digit_number}" + + written_data = [] + + prepared_written_data_by_array_in_hash.each_with_index do |written_data_hash, index| + row = [] + + # TODO: 取得漏れには 10001 始まりを付与したい + id_on_sheet = (index_on_hundred * 100) + (index + 1) + + # TODO: ハードコーディングをしたくない + row[0] = id_on_sheet + row[1] = written_data_hash['screen_name'] + row[2] = written_data_hash['tweet_id'] + row[3] = written_data_hash['日時'] + row[4] = written_data_hash['URL'] + row[5] = written_data_hash['別ツイ'] + row[7] = written_data_hash['ツイ見られない?'] + row[9] = written_data_hash['ふぁぼ済?'] + row[10] = written_data_hash['二次チェック済?'] + row[11] = written_data_hash['内容'] + row[49] = written_data_hash['suggested_names'] # 50列目 (AX) + row[199] = '' # 200列目 (GR) を表示させるために空文字を入れる + + row.flatten! # suggested_names は長さが不定なので flatten する + + written_data << row + end + + # suggested_names を最初に全削除する + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_ALL_CHARACTERS_SHEET_ID', nil), + range: "#{sheet_name}!AX2:GR101", + values: [[''] * 50] * 100 # 100行分の空文字を入れる + ) + + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_ALL_CHARACTERS_SHEET_ID', nil), + range: "#{sheet_name}!A2", # 始点 + values: written_data + ) + 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 new file mode 100644 index 00000000..eccc4f29 --- /dev/null +++ b/app/services/sheets/write_and_update/direct_messages.rb @@ -0,0 +1,64 @@ +module Sheets + module WriteAndUpdate + class DirectMessages + def self.exec + direct_messages = DirectMessage.for_spreadsheet + + 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| + inserted_hash = {} + + inserted_hash['screen_name'] = dm.user.screen_name + inserted_hash['dm_id'] = dm.id_number.to_s + inserted_hash['日時'] = dm.messaged_at.strftime('%Y/%m/%d %H:%M:%S').to_s + inserted_hash['内容'] = dm.content_text + # この行のコストが高い + inserted_hash['suggested_names'] = NaturalLanguage::SuggestCharacterNames.exec(dm) # Array + + prepared_written_data_by_array_in_hash << inserted_hash + end + + two_digit_number = format('%02d', number: index_on_hundred + 1) + sheet_name = "集計_#{two_digit_number}" + + written_data = [] + + prepared_written_data_by_array_in_hash.each_with_index do |written_data_hash, index| + row = [] + + # TODO: 取得漏れには 10001 始まりを付与したい + id_on_sheet = (index_on_hundred * 100) + (index + 1) + + # TODO: ハードコーディングをしたくない + row[0] = id_on_sheet + row[1] = written_data_hash['screen_name'] + row[2] = written_data_hash['dm_id'] + row[3] = written_data_hash['日時'] + row[10] = written_data_hash['内容'] + row[49] = written_data_hash['suggested_names'] # 50列目 (AX) + row[199] = '' # 200列目 (GR) を表示させるために空文字を入れる + + row.flatten! # suggested_names は長さが不定なので flatten する + + written_data << row + end + + # suggested_names を最初に全削除する + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_DIRECT_MESSAGES_SHEET_ID', nil), + range: "#{sheet_name}!AX2:GR101", + values: [[''] * 50] * 100 # 100行分の空文字を入れる + ) + + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_DIRECT_MESSAGES_SHEET_ID', nil), + range: "#{sheet_name}!A2", # 始点 + values: written_data + ) + end + end + end + end +end diff --git a/app/services/sheets/write_and_update/fav_quotes.rb b/app/services/sheets/write_and_update/fav_quotes.rb new file mode 100644 index 00000000..7e13eb74 --- /dev/null +++ b/app/services/sheets/write_and_update/fav_quotes.rb @@ -0,0 +1,80 @@ +# ボーナス票・推し台詞 +module Sheets + module WriteAndUpdate + class FavQuotes + def self.exec + # NOTE: for_api は中身が動的になってしまうので使ってはいけない + tweets = Tweet.fav_quotes + + tweets.each_slice(100).with_index do |tweets_100, index_on_hundred| + prepared_written_data_by_array_in_hash = [] + + tweets_100.each_with_index do |tweet, i| + inserted_hash = {} + + user = tweet.user + by_user_other_tweets = tweets.where(user: user).where.not(id: tweet.id) + # カンマ区切りにすると to_s しても数値に変換されてしまう + by_user_other_tweets_for_sheet = by_user_other_tweets.map { |t| t.id_number.to_s }.join(' | ').to_s + + inserted_hash['screen_name'] = tweet.user.screen_name + inserted_hash['tweet_id'] = tweet.id_number.to_s + inserted_hash['日時'] = tweet.tweeted_at.strftime('%Y/%m/%d %H:%M:%S').to_s + inserted_hash['URL'] = tweet.url + inserted_hash['ツイ見られない?'] = !tweet.is_public + inserted_hash['別ツイ'] = by_user_other_tweets_for_sheet.to_s || '' + inserted_hash['ふぁぼ済?'] = false + inserted_hash['内容'] = tweet.full_text + # この行のコストが高い + inserted_hash['suggested_names'] = NaturalLanguage::SuggestCharacterNames.exec(tweet) # Array + + prepared_written_data_by_array_in_hash << inserted_hash + end + + two_digit_number = format('%02d', number: index_on_hundred + 1) + sheet_name = "集計_#{two_digit_number}" + + written_data = [] + + prepared_written_data_by_array_in_hash.each_with_index do |written_data_hash, index| + row = [] + + # TODO: 取得漏れには 10001 始まりを付与したい + id_on_sheet = (index_on_hundred * 100) + (index + 1) + + # TODO: ハードコーディングをしたくない + row[0] = id_on_sheet + row[1] = written_data_hash['screen_name'] + row[2] = written_data_hash['tweet_id'] + row[3] = written_data_hash['日時'] + row[4] = written_data_hash['URL'] + row[5] = written_data_hash['別ツイ'] + row[7] = written_data_hash['ツイ見られない?'] + row[9] = written_data_hash['ふぁぼ済?'] + row[10] = written_data_hash['二次チェック済?'] + row[11] = written_data_hash['内容'] + row[49] = written_data_hash['suggested_names'] # 50列目 (AX) + row[199] = '' # 200列目 (GR) を表示させるために空文字を入れる + + row.flatten! # suggested_names は長さが不定なので flatten する + + written_data << row + end + + # suggested_names を最初に全削除する + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_BONUS_FAV_QUOTES_SHEET_ID', nil), + range: "#{sheet_name}!AX2:GR101", + values: [[''] * 50] * 100 # 100行分の空文字を入れる + ) + + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_BONUS_FAV_QUOTES_SHEET_ID', nil), + range: "#{sheet_name}!A2", # 始点 + values: written_data + ) + end + end + end + end +end diff --git a/app/services/sheets/write_and_update/short_stories.rb b/app/services/sheets/write_and_update/short_stories.rb new file mode 100644 index 00000000..0effeb45 --- /dev/null +++ b/app/services/sheets/write_and_update/short_stories.rb @@ -0,0 +1,80 @@ +# ボーナス票・お題小説 +module Sheets + module WriteAndUpdate + class ShortStories + def self.exec + # NOTE: for_api は中身が動的になってしまうので使ってはいけない + tweets = Tweet.short_stories + + tweets.each_slice(100).with_index do |tweets_100, index_on_hundred| + prepared_written_data_by_array_in_hash = [] + + tweets_100.each_with_index do |tweet, i| + inserted_hash = {} + + user = tweet.user + by_user_other_tweets = tweets.where(user: user).where.not(id: tweet.id) + # カンマ区切りにすると to_s しても数値に変換されてしまう + by_user_other_tweets_for_sheet = by_user_other_tweets.map { |t| t.id_number.to_s }.join(' | ').to_s + + inserted_hash['screen_name'] = tweet.user.screen_name + inserted_hash['tweet_id'] = tweet.id_number.to_s + inserted_hash['日時'] = tweet.tweeted_at.strftime('%Y/%m/%d %H:%M:%S').to_s + inserted_hash['URL'] = tweet.url + inserted_hash['ツイ見られない?'] = !tweet.is_public + inserted_hash['別ツイ'] = by_user_other_tweets_for_sheet.to_s || '' + inserted_hash['ふぁぼ済?'] = false + inserted_hash['内容'] = tweet.full_text + # この行のコストが高い + inserted_hash['suggested_names'] = NaturalLanguage::SuggestCharacterNames.exec(tweet) # Array + + prepared_written_data_by_array_in_hash << inserted_hash + end + + two_digit_number = format('%02d', number: index_on_hundred + 1) + sheet_name = "集計_#{two_digit_number}" + + written_data = [] + + prepared_written_data_by_array_in_hash.each_with_index do |written_data_hash, index| + row = [] + + # TODO: 取得漏れには 10001 始まりを付与したい + id_on_sheet = (index_on_hundred * 100) + (index + 1) + + # TODO: ハードコーディングをしたくない + row[0] = id_on_sheet + row[1] = written_data_hash['screen_name'] + row[2] = written_data_hash['tweet_id'] + row[3] = written_data_hash['日時'] + row[4] = written_data_hash['URL'] + row[5] = written_data_hash['別ツイ'] + row[7] = written_data_hash['ツイ見られない?'] + row[9] = written_data_hash['ふぁぼ済?'] + row[10] = written_data_hash['二次チェック済?'] + row[11] = written_data_hash['内容'] + row[49] = written_data_hash['suggested_names'] # 50列目 (AX) + row[199] = '' # 200列目 (GR) を表示させるために空文字を入れる + + row.flatten! # suggested_names は長さが不定なので flatten する + + written_data << row + end + + # suggested_names を最初に全削除する + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_BONUS_SHORT_STORIES_SHEET_ID', nil), + range: "#{sheet_name}!AX2:GR101", + values: [[''] * 50] * 100 # 100行分の空文字を入れる + ) + + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_BONUS_SHORT_STORIES_SHEET_ID', nil), + range: "#{sheet_name}!A2", # 始点 + values: written_data + ) + end + end + end + end +end diff --git a/app/services/sheets/write_and_update/sosenkyo_campaigns.rb b/app/services/sheets/write_and_update/sosenkyo_campaigns.rb new file mode 100644 index 00000000..e7507a4a --- /dev/null +++ b/app/services/sheets/write_and_update/sosenkyo_campaigns.rb @@ -0,0 +1,80 @@ +# ボーナス票・選挙運動 +module Sheets + module WriteAndUpdate + class SosenkyoCampaigns + def self.exec + # NOTE: for_api は中身が動的になってしまうので使ってはいけない + tweets = Tweet.sosenkyo_campaigns + + tweets.each_slice(100).with_index do |tweets_100, index_on_hundred| + prepared_written_data_by_array_in_hash = [] + + tweets_100.each_with_index do |tweet, i| + inserted_hash = {} + + user = tweet.user + by_user_other_tweets = tweets.where(user: user).where.not(id: tweet.id) + # カンマ区切りにすると to_s しても数値に変換されてしまう + by_user_other_tweets_for_sheet = by_user_other_tweets.map { |t| t.id_number.to_s }.join(' | ').to_s + + inserted_hash['screen_name'] = tweet.user.screen_name + inserted_hash['tweet_id'] = tweet.id_number.to_s + inserted_hash['日時'] = tweet.tweeted_at.strftime('%Y/%m/%d %H:%M:%S').to_s + inserted_hash['URL'] = tweet.url + inserted_hash['ツイ見られない?'] = !tweet.is_public + inserted_hash['別ツイ'] = by_user_other_tweets_for_sheet.to_s || '' + inserted_hash['ふぁぼ済?'] = false + inserted_hash['内容'] = tweet.full_text + # この行のコストが高い + inserted_hash['suggested_names'] = NaturalLanguage::SuggestCharacterNames.exec(tweet) # Array + + prepared_written_data_by_array_in_hash << inserted_hash + end + + two_digit_number = format('%02d', number: index_on_hundred + 1) + sheet_name = "集計_#{two_digit_number}" + + written_data = [] + + prepared_written_data_by_array_in_hash.each_with_index do |written_data_hash, index| + row = [] + + # TODO: 取得漏れには 10001 始まりを付与したい + id_on_sheet = (index_on_hundred * 100) + (index + 1) + + # TODO: ハードコーディングをしたくない + row[0] = id_on_sheet + row[1] = written_data_hash['screen_name'] + row[2] = written_data_hash['tweet_id'] + row[3] = written_data_hash['日時'] + row[4] = written_data_hash['URL'] + row[5] = written_data_hash['別ツイ'] + row[7] = written_data_hash['ツイ見られない?'] + row[9] = written_data_hash['ふぁぼ済?'] + row[10] = written_data_hash['二次チェック済?'] + row[11] = written_data_hash['内容'] + row[49] = written_data_hash['suggested_names'] # 50列目 (AX) + row[199] = '' # 200列目 (GR) を表示させるために空文字を入れる + + row.flatten! # suggested_names は長さが不定なので flatten する + + written_data << row + end + + # suggested_names を最初に全削除する + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_BONUS_SOSENKYO_CAMPAIGNS_SHEET_ID', nil), + range: "#{sheet_name}!AX2:GR101", + values: [[''] * 50] * 100 # 100行分の空文字を入れる + ) + + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_BONUS_SOSENKYO_CAMPAIGNS_SHEET_ID', nil), + range: "#{sheet_name}!A2", # 始点 + values: written_data + ) + end + end + end + end +end diff --git a/app/services/sheets/write_and_update/unite_attacks.rb b/app/services/sheets/write_and_update/unite_attacks.rb new file mode 100644 index 00000000..f9dc2b51 --- /dev/null +++ b/app/services/sheets/write_and_update/unite_attacks.rb @@ -0,0 +1,85 @@ +module Sheets + module WriteAndUpdate + class UniteAttacks + # 列情報は clasp/gensosenkyo/ZzzColumnNames.ts を参考にする + def self.exec + # NOTE: for_api で Tweet を指定すると is_public の値によって冪等性が保証されないのでダメ + tweets = Tweet.unite_attacks_votes.valid_term_votes + + tweets.each_slice(100).with_index do |tweets_100, index_on_hundred| + prepared_written_data_by_array_in_hash = [] + + tweets_100.each_with_index do |tweet, i| + inserted_hash = {} + + user = tweet.user + by_user_other_tweets = tweets.where(user: user).where.not(id: tweet.id) + # カンマ区切りにすると to_s しても数値に変換されてしまう + by_user_other_tweets_for_sheet = by_user_other_tweets.map { |t| t.id_number.to_s }.join(' | ').to_s + + inserted_hash['screen_name'] = tweet.user.screen_name + inserted_hash['tweet_id'] = tweet.id_number.to_s + inserted_hash['日時'] = tweet.tweeted_at.strftime('%Y/%m/%d %H:%M:%S').to_s + inserted_hash['URL'] = tweet.url + inserted_hash['ツイ見られない?'] = !tweet.is_public + inserted_hash['別ツイ'] = by_user_other_tweets_for_sheet.to_s || '' + inserted_hash['ふぁぼ済?'] = true # to_s がいるかも + inserted_hash['内容'] = tweet.full_text + + # 例: {:title=>"幻想水滸伝II", :attack_name=>"美青年攻撃"} + unite_attack_title_and_name_ = NaturalLanguage::SuggestUniteAttackNames.exec(tweet) + inserted_hash['suggested_names'] = [ + unite_attack_title_and_name_[:title], + unite_attack_title_and_name_[:attack_name], + ] + + prepared_written_data_by_array_in_hash << inserted_hash + end + + two_digit_number = format('%02d', number: index_on_hundred + 1) + sheet_name = "集計_#{two_digit_number}" + + written_data = [] + + prepared_written_data_by_array_in_hash.each_with_index do |written_data_hash, index| + row = [] + + # TODO: 取得漏れには 10001 始まりを付与したい + id_on_sheet = (index_on_hundred * 100) + (index + 1) + + # TODO: ハードコーディングをしたくない + row[0] = id_on_sheet + row[1] = written_data_hash['screen_name'] + row[2] = written_data_hash['tweet_id'] + row[3] = written_data_hash['日時'] + row[4] = written_data_hash['URL'] + row[5] = written_data_hash['別ツイ'] + row[7] = written_data_hash['ツイ見られない?'] + row[9] = written_data_hash['ふぁぼ済?'] + row[10] = written_data_hash['二次チェック済?'] + row[11] = written_data_hash['内容'] + row[49] = written_data_hash['suggested_names'] # 50列目 (AX) + row[199] = '' # 200列目 (GR) を表示させるために空文字を入れる + + row.flatten! # suggested_names は長さが不定なので flatten する + + written_data << row + end + + # suggested_names を最初に全削除する + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_UNITE_ATTACKS_SHEET_ID', nil), + range: "#{sheet_name}!AX2:GR101", + values: [[''] * 50] * 100 # 100行分の空文字を入れる + ) + + SheetData.write_rows( + sheet_id: ENV.fetch('COUNTING_UNITE_ATTACKS_SHEET_ID', nil), + range: "#{sheet_name}!A2", # 始点 + values: written_data + ) + end + end + end + end +end diff --git a/clasp/README.md b/clasp/README.md index 57e92236..81403015 100644 --- a/clasp/README.md +++ b/clasp/README.md @@ -1,11 +1,19 @@ # まずは何はともあれ公式ドキュメント - https://github.com/google/clasp/tree/master/docs +# エクセル列番号対応表 +- https://takamin.github.io/techtips/xlsColNumMap + # 注意点 - サブディレクトリ内で閉じるために `$ npx clasp` でコマンドを実行する - `.ts` のみ push される - ただし pull すると `.js` が降ってくる - コマンドの実行はすべてプロジェクトルートで行う +- プロジェクト外からのファイルをインポートした上でよしなにビルドすることはできない(と思っておいた方がいい) + - 共通のよくある処理ライブラリなんかは単純にコピペしまくる + - どこかのプロジェクトで修正があったらいっせい上書きするシェルスクリプトがあるとよさそう + - 原則として単一ディレクトリ内にグローバルな関数やらが並ぶということ + - ファイル名ごとに命名規則で名前空間もどきなことをしてバッティングを防ぐ # 導入 diff --git a/clasp/gensosenkyo/ZzzCellOperations.ts b/clasp/gensosenkyo/ZzzCellOperations.ts new file mode 100644 index 00000000..5c474970 --- /dev/null +++ b/clasp/gensosenkyo/ZzzCellOperations.ts @@ -0,0 +1,112 @@ +// setValue"s" メソッドの場合には引数は二次元配列になることに注意 +namespace ZzzCellOperations { + export const getRangeSpecificColumnRow2ToRow101 = (columnNumber: number, sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + const startRowNumber = 2 + const endRowNumber = 101 + + return sheet.getRange( + startRowNumber, + columnNumber, + endRowNumber - 1, // 自分自身を含めた数になる(何個のセルを埋めるか、ということ) + 1 + ) + } + + // 表示 -> 固定 + export const freezeFirstRow = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + sheet.setFrozenRows(1) + + return + } + + export const unFreezeFirstRow = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + sheet.setFrozenRows(0) + + return + } + + export const freezeFirstColumn = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + sheet.setFrozenColumns(1) + + return + } + + export const unFreezeFirstColumn = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + sheet.setFrozenColumns(0) + + return + } + + // 例外的に Apps Script からデータをシートに書き込んでいる(Ruby に寄せるところを) + // 既存データを上書きする破壊的メソッドなので注意する + export const setFirstRowNames = ( + sheet: GoogleAppsScript.Spreadsheet.Sheet, + category = 'mainDivision' + ) => { + let names: string[] + + if (category === 'mainDivision') { + names = ZzzColumnNames.columnNamesOnCountingSheet + } else if (category === 'bonusVote') { + names = ZzzColumnNames.columnNamesOnBonusVotesSheet() + } else if (category === 'directMessage') { + names = ZzzColumnNames.columnNamesOnDirectMessageSheet + } else { + throw new Error('unknown category') + } + + for (let i = 0 ; i < names.length ; i++) { + sheet.getRange(1, i + 1).setValue(names[i]) + } + } + + // カラム名の削除(例外的に Apps Script での対応) + // 既存データを上書きする破壊的メソッドなので注意する + export const unSetFirstRowNames = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + const names = ZzzColumnNames.columnNamesOnCountingSheet + + for (let i = 0 ; i < names.length ; i++) { + sheet.getRange(1, i + 1).setValue('') + } + } + + // 102行目 に '@' を番兵として立たせる(既存データを上書きする破壊的メソッド) + export const setLastRowSymbols = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + // 列の末尾は 200列目 (GR) とする + const range = sheet.getRange(102, 1, 1, 200) + + range.setValue('@') + } + + // 既存データを上書きする破壊的メソッド + export const removeLastRowSymbols = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + const range = sheet.getRange(102, 1, 1, 200) + + range.setValue('') + } + + // 表示形式 -> ラッピング -> 「折り返す」 + export const rappingOrikaesu = (range: GoogleAppsScript.Spreadsheet.Range) => { + range.setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP) + } + + // 表示形式 -> ラッピング -> 「はみ出す」(デフォルト) + export const rappingHamidasu = (range: GoogleAppsScript.Spreadsheet.Range) => { + range.setWrapStrategy(SpreadsheetApp.WrapStrategy.OVERFLOW) + } + + // 表示形式 -> ラッピング -> 「切り詰める」(はみ出した部分は見えない) + export const rappingKiritsumeru = (range: GoogleAppsScript.Spreadsheet.Range) => { + range.setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP) + } + + // 挿入 -> チェックボックス(既存データを上書きする破壊的メソッド) + export const createCheckBoxes = (range: GoogleAppsScript.Spreadsheet.Range) => { + range.insertCheckboxes(); + } + + // チェックボックスを削除する(既存データを上書きする破壊的メソッド) + export const removeCheckBoxes = (range: GoogleAppsScript.Spreadsheet.Range) => { + range.removeCheckboxes(); + } +} diff --git a/clasp/gensosenkyo/ZzzColumnNames.ts b/clasp/gensosenkyo/ZzzColumnNames.ts new file mode 100644 index 00000000..8cea929b --- /dev/null +++ b/clasp/gensosenkyo/ZzzColumnNames.ts @@ -0,0 +1,81 @@ +namespace ZzzColumnNames { + // ここの命名は変更することがしばしばあり得るから、固定した英数命名に紐づけたい + export const columnNamesOnCountingSheet: string[] = [ + 'ID', + 'screen_name', + 'tweet_id', + '日時', + 'URL', + '別ツイ', + '全終了?', + 'ツイ見られない?', + '集計対象外?', + 'ふぁぼ済?', // OnCounting では不要だが、冪等性維持のため含んでいる + '二次チェック済?', + '内容', + '備考', + '要レビュー?', + 'キャラ1 or 作品名', // 協力攻撃の場合は「作品名」 + 'キャラ2 or 協力攻撃名', // 協力攻撃の場合は「協力攻撃名」 + 'キャラ3', // 協力攻撃の場合は空文字 + ] + + export const columnNamesOnBonusVotesSheet = (): string[] => { + const columnNamesOnCountingSheet = ZzzColumnNames.columnNamesOnCountingSheet + + return columnNamesOnCountingSheet.concat([ + 'キャラ4', + 'キャラ5', + 'キャラ6', + 'キャラ7', + 'キャラ8', + 'キャラ9', + 'キャラ10', + ]) + } + + export const columnNamesOnDirectMessageSheet: string[] = [ + 'ID', + 'screen_name', + 'dm_id', + '日時', + '全終了?', + 'DMが見られない?', + '集計対象外?', + '対応済み?', + '二次チェック済?', + '種類', // ZzzDataValidation.setDataValidationDirectMessageTypes + '内容', + '備考', + '要レビュー?', + 'キャラ1 or 作品名', + 'キャラ2 or 協力攻撃名', + 'キャラ3', + 'キャラ4', // 「種類」に総選挙運動が来た場合などを考えるとキャラ数は不定になる + 'キャラ5', + 'キャラ6', + 'キャラ7', + 'キャラ8', + 'キャラ9', + 'キャラ10', + ] + + export const colNameToNumber = (category = 'mainDivision') => { + if (category === 'mainDivision') { + return ZzzSheetOperations + .correspondenceObjectAboutColumnNameToColumnNumber( + ZzzColumnNames.columnNamesOnCountingSheet + ) + } else if (category === 'bonusVote') { + return ZzzSheetOperations + .correspondenceObjectAboutColumnNameToColumnNumber( + ZzzColumnNames.columnNamesOnBonusVotesSheet() + ) + } else if (category === 'directMessage') { + return ZzzSheetOperations + .correspondenceObjectAboutColumnNameToColumnNumber( + ZzzColumnNames.columnNamesOnDirectMessageSheet + ) + } + } +} diff --git a/clasp/gensosenkyo/ZzzCommonScripts.ts b/clasp/gensosenkyo/ZzzCommonScripts.ts new file mode 100644 index 00000000..5ba722f4 --- /dev/null +++ b/clasp/gensosenkyo/ZzzCommonScripts.ts @@ -0,0 +1,16 @@ +namespace ZzzCommonScripts { + export const RemoveAtMarkAndBlank = (text: string) => { + let removedAtmarkAndBlankText: string + + removedAtmarkAndBlankText = text.replace('@', '') + removedAtmarkAndBlankText = removedAtmarkAndBlankText.replace(' ', '') + + return removedAtmarkAndBlankText + } + + export const showStartAndEndLogger = (fn, displayLogText = 'showStartAndEndLogger') => { + console.log(`[START] ${displayLogText}`) + fn() + console.log(`[END] ${displayLogText}`) + } +} diff --git a/clasp/gensosenkyo/ZzzConditionalFormats.ts b/clasp/gensosenkyo/ZzzConditionalFormats.ts new file mode 100644 index 00000000..eb43d077 --- /dev/null +++ b/clasp/gensosenkyo/ZzzConditionalFormats.ts @@ -0,0 +1,161 @@ +// 条件付き書式 +namespace ZzzConditionalFormats { + export const setColorToRangeInSpecificCondition = ( + range: GoogleAppsScript.Spreadsheet.Range, + sheet: GoogleAppsScript.Spreadsheet.Sheet, + conditionValue: string, + colorCode: string + ) => { + const newRule = SpreadsheetApp + .newConditionalFormatRule() + .whenTextEqualTo(conditionValue) // この条件が満たされる時に(セル無指定の場合は条件が判定されるセルは range に等しい)、 + .setBackground(colorCode) // このメソッドが適用されて、 + .setRanges([range]) // 適用範囲は range になる(getRange した値を配列にくるんで渡す) + .build() + + // 既存のルールに追加する + const rules = sheet.getConditionalFormatRules() + rules.push(newRule) + + sheet.setConditionalFormatRules(rules) + } + + // 「カスタム数式」における、判定対象セルと適用される範囲の関係は分かりづらいので、以下などを参照 + // https://bamka.info/docs-google-gyo-irokae/ + export const getRuleToSetGrayBackgroundToAllRowCellsInSpecificCondition = ( + rowNumber: number, + sheet: GoogleAppsScript.Spreadsheet.Sheet, + formulaSatisfied: string, + range: GoogleAppsScript.Spreadsheet.Range + ) => { + // formulaSatisfied は '=$F$2=FALSE' などのような形の条件文になる + const newRule = SpreadsheetApp + .newConditionalFormatRule() + .whenFormulaSatisfied(formulaSatisfied) // この条件が満たされる時に、 + .setBackground('#a9a9a9') // このメソッドが適用されて、 + // .setRanges([sheet.getRange(rowNumber, 1, 1, 50)]) // 適用範囲は range になる(getRange した値を配列にくるんで渡す) + .setRanges([range]) // 適用範囲は range になる(getRange した値を配列にくるんで渡す) + .build() + + return newRule + } + + export const clearConditionalFormatsOnAllSheets = () => { + ZzzSheetOperations.applyFunctionToAllCountingSheets((sheet) => { + sheet.clearConditionalFormatRules(); + }, '「条件付き書式」をクリアしました') + } + + export const setInitToIsAllCompletedColumn = (sheet: GoogleAppsScript.Spreadsheet.Sheet, category = '') => { + const colNameToNumber = ZzzColumnNames.colNameToNumber(category) + + const requiredReviewColumnNumber = colNameToNumber['要レビュー?'] + const requiredReviewColumnAlphabet = ZzzConverters.convertColumnNumberToAlphabet(requiredReviewColumnNumber) + + const completedSecondCheckColumnNumber = colNameToNumber['二次チェック済?'] + const completedSecondCheckAlphabet = ZzzConverters.convertColumnNumberToAlphabet(completedSecondCheckColumnNumber) + + const formula = `=IF(AND(${requiredReviewColumnAlphabet}2=FALSE,${completedSecondCheckAlphabet}2=TRUE),"🌞","☔")` + + // 「全終了?」列 + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101( + colNameToNumber['全終了?'], + sheet + ) + + // '☔' という初期値を設定する + range.setValue(formula) + range.setHorizontalAlignment('center'); + + ZzzConditionalFormats.setColorToRangeInSpecificCondition( + range, + sheet, + '🌞', + '#ccffcc' // Green + ) + ZzzConditionalFormats.setColorToRangeInSpecificCondition( + range, + sheet, + '☔', + '#ffc0cb' // Red + ) + } + + export const setInitToIsRequiredReview = (sheet: GoogleAppsScript.Spreadsheet.Sheet, category = '') => { + const colNameToNumber = ZzzColumnNames.colNameToNumber(category) + + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101( + colNameToNumber['要レビュー?'], + sheet + ) + ZzzConditionalFormats.setColorToRangeInSpecificCondition( + range, + sheet, + 'TRUE', + '#ffc0cb' // Red + ) + } + + export const setInitToIsCompletedSecondCheck = (sheet: GoogleAppsScript.Spreadsheet.Sheet, category = '') => { + const colNameToNumber = ZzzColumnNames.colNameToNumber(category) + + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101( + colNameToNumber['二次チェック済?'], + sheet + ) + ZzzConditionalFormats.setColorToRangeInSpecificCondition( + range, + sheet, + 'TRUE', + '#ccffcc' // Green + ) + ZzzConditionalFormats.setColorToRangeInSpecificCondition( + range, + sheet, + 'FALSE', + '#ffc0cb' // Red + ) + } + + export const setInitToIsCompletedFavorite = (sheet: GoogleAppsScript.Spreadsheet.Sheet, category = '') => { + const colNameToNumber = ZzzColumnNames.colNameToNumber(category) + + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101( + colNameToNumber['ふぁぼ済?'], // DMでは'対応済み?' + sheet + ) + ZzzConditionalFormats.setColorToRangeInSpecificCondition( + range, + sheet, + 'TRUE', + '#ccffcc' // Red + ) + ZzzConditionalFormats.setColorToRangeInSpecificCondition( + range, + sheet, + 'FALSE', + '#ffc0cb' // Red + ) + } + + export const setInitToIsCompletedDMResponse = (sheet: GoogleAppsScript.Spreadsheet.Sheet, category = 'directMessage') => { + const colNameToNumber = ZzzColumnNames.colNameToNumber(category) + + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101( + colNameToNumber['対応済み?'], + sheet + ) + ZzzConditionalFormats.setColorToRangeInSpecificCondition( + range, + sheet, + 'TRUE', + '#ccffcc' // Red + ) + ZzzConditionalFormats.setColorToRangeInSpecificCondition( + range, + sheet, + 'FALSE', + '#ffc0cb' // Red + ) + } +} diff --git a/clasp/gensosenkyo/ZzzConverters.ts b/clasp/gensosenkyo/ZzzConverters.ts new file mode 100644 index 00000000..f4d96452 --- /dev/null +++ b/clasp/gensosenkyo/ZzzConverters.ts @@ -0,0 +1,70 @@ +// これデフォルトで用意されてないのか +namespace ZzzConverters { + export const convertColumnAlphabetToNumber = (columnAlphabet: string) => { + return ZzzConverters.convertListAboutColumnAlphabetToNumber[columnAlphabet] + } + + export const convertColumnNumberToAlphabet = (number: number) => { + const convertList = ZzzConverters.convertListAboutColumnAlphabetToNumber + + const result = Object.keys(convertList).find(key => { + return convertList[key] === number + }) + + return result + } + + export const convertListAboutColumnAlphabetToNumber = { + A: 1, + B: 2, + C: 3, + D: 4, + E: 5, + F: 6, + G: 7, + H: 8, + I: 9, + J: 10, + K: 11, + L: 12, + M: 13, + N: 14, + O: 15, + P: 16, + Q: 17, + R: 18, + S: 19, + T: 20, + U: 21, + V: 22, + W: 23, + X: 24, + Y: 25, + Z: 26, + AA: 27, + AB: 28, + AC: 29, + AD: 30, + AE: 31, + AF: 32, + AG: 33, + AH: 34, + AI: 35, + AJ: 36, + AK: 37, + AL: 38, + AM: 39, + AN: 40, + AO: 41, + AP: 42, + AQ: 43, + AR: 44, + AS: 45, + AT: 46, + AU: 47, + AV: 48, + AW: 49, + AX: 50, + AY: 51, // とりあえずここまで + } +} diff --git a/clasp/gensosenkyo/ZzzDataValidation.ts b/clasp/gensosenkyo/ZzzDataValidation.ts new file mode 100644 index 00000000..0ea1378b --- /dev/null +++ b/clasp/gensosenkyo/ZzzDataValidation.ts @@ -0,0 +1,58 @@ +// 「入力規則」 +namespace ZzzDataValidation { + export const setDataValidationToCell = (sheet: GoogleAppsScript.Spreadsheet.Sheet, columnNumber: number, destroyWord: string = '') => { + const startRowNumber = 2 + const endRowNumber = 101 + + for (let i = startRowNumber ; i <= endRowNumber ; i++) { + const dataValidationTargetCell = sheet.getRange(i, columnNumber, 1, 1) + + if (destroyWord === 'DESTROY') { // この引数が与えられていると入力規則全削除 + dataValidationTargetCell.setDataValidation(null) + } else { + const pullDownValuesRange = ZzzDataValidation.rangeWhichContainsDataList(i, sheet) + const rule = SpreadsheetApp.newDataValidation().requireValueInRange(pullDownValuesRange).build() + + dataValidationTargetCell.setDataValidation(rule) + } + } + } + + export const rangeWhichContainsDataList = (rowNumber: number, sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + // AXn (50列目) を始点として、GRn (200列目)までの列で、行は n行目 である範囲 + // 第3引数 および 第4引数 には注意が必要で、自分自身を含めた個数を指定する + // したがって 第4引数 の値は 151 になる + // cf. https://takamin.github.io/techtips/xlsColNumMap + + return sheet.getRange(rowNumber, 50, 1, 151) + } + + export const setDataValidationDirectMessageTypes = (sheet: GoogleAppsScript.Spreadsheet.Sheet, columnNumber: number, destroyWord: string = '') => { + const startRowNumber = 2 + const endRowNumber = 101 + + for (let i = startRowNumber ; i <= endRowNumber ; i++) { + const dataValidationTargetCell = sheet.getRange(i, columnNumber, 1, 1) + + if (destroyWord === 'DESTROY') { // この引数が与えられていると入力規則全削除 + dataValidationTargetCell.setDataValidation(null) + } else { + const requiredValues = ZzzDataValidation.directMessageTypes + const rule = SpreadsheetApp.newDataValidation().requireValueInList(requiredValues).build() + + dataValidationTargetCell.setDataValidation(rule) + } + } + } + + export const directMessageTypes = [ + '①オールキャラ部門', + '②協力攻撃部門', + 'ボ・OP・CLイラスト', + 'ボ・お題小説', + 'ボ・開票イラスト', + 'ボ・推し台詞', + 'ボ・選挙運動', + '票に関係ない', + ] +} diff --git a/clasp/gensosenkyo/ZzzDebugFunctions.ts b/clasp/gensosenkyo/ZzzDebugFunctions.ts new file mode 100644 index 00000000..45f32721 --- /dev/null +++ b/clasp/gensosenkyo/ZzzDebugFunctions.ts @@ -0,0 +1,2 @@ +namespace ZzzDebugFunctions { +} diff --git a/clasp/gensosenkyo/ZzzSheetNames.ts b/clasp/gensosenkyo/ZzzSheetNames.ts new file mode 100644 index 00000000..a4f942ca --- /dev/null +++ b/clasp/gensosenkyo/ZzzSheetNames.ts @@ -0,0 +1,44 @@ +namespace ZzzSheetNames { + // TODO: 添字は 00 から始めた方が id_on_sheet の番号の直感と一致して良いかも + export const allSheetNames = [ + '説明', + '集計状況', + '集計_01', + '集計_02', + '集計_03', + '集計_04', + '集計_05', + '集計_06', + '集計_07', + '集計_08', + '集計_09', + '集計_10', + '集計_11', + '集計_12', + '集計_13', + '集計_14', + '集計_15', + '集計_16', + '集計_17', + '集計_18', + '集計_19', + '集計_20', + '取得漏れ等' + ] + + export const forCountingSheetNames = ZzzSheetNames.allSheetNames.filter((sheetName) => { + return !(['説明', '集計状況'].includes(sheetName)) + }) + + export const sheetFilenames: string[] = [ + '投票まとめ', + '①オールキャラ部門', + '②協力攻撃部門', + 'DM(ダイレクトメッセージ)', + 'ボーナス票・OP・CLイラスト', + 'ボーナス票・お題小説', + 'ボーナス票・開票イラスト', + 'ボーナス票・推し台詞', + 'ボーナス票・選挙運動', + ] +} diff --git a/clasp/gensosenkyo/ZzzSheetOperations.ts b/clasp/gensosenkyo/ZzzSheetOperations.ts new file mode 100644 index 00000000..5c0cd15f --- /dev/null +++ b/clasp/gensosenkyo/ZzzSheetOperations.ts @@ -0,0 +1,156 @@ +namespace ZzzSheetOperations { + // アクティブシートを変更 + export const changeActiveSheetTo = (sheetName: string) => { + const allSheets = SpreadsheetApp.getActiveSpreadsheet().getSheets() + + for (let i = 0 ; i < allSheets.length ; i++) { + if (allSheets[i].getName() === sheetName) { + SpreadsheetApp.setActiveSheet(allSheets[i]) + + return allSheets[i] + } + } + + console.log('[LOG] アクティブ化したいシートが見つかりませんでした') + + return + } + + export const applyFunctionToAllCountingSheets = (fn: any, displayLogText = null) => { + const sheetNames = ZzzSheetNames.forCountingSheetNames + + ZzzSheetOperations.applyFunctionToSpecificSheetNames( + fn, + sheetNames, + displayLogText + ) + + // 使い方 + // ZzzSheetOperations.applyFunctionToAllCountingSheets( + // (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + // // 全シートに対してやりたいこと + // }, + // 'シートごとの実行完了時に表示されるログのテキスト' + // ) + } + + export const applyFunctionToSpecificSheetNames = ( + fn: any, + sheetNames: string[], + displayLogText: string + ) => { + sheetNames.forEach(sheetName => { + const sheet = ZzzSheetOperations.changeActiveSheetTo(sheetName) + + fn(sheet) + + if (displayLogText) { + console.log(`[DONE] ${sheetName} : ${displayLogText}`) + } + }) + } + + // シートの並び順序も保証される + export const getAllSheetNames = (): string[] => { + const sheetNames = new Array() + + const allSheets = SpreadsheetApp.getActiveSpreadsheet().getSheets(); + for (let i = 0 ; i < allSheets.length ; i++) { + sheetNames.push(allSheets[i].getName()) + } + + return sheetNames + } + + // activeSheetName の後ろに sheetName のシートを作成する(指定がない場合は一番最後に作成する) + export const createSheet = ({ activeSheetName = null, newSheetName = '' }) => { + let sheet: GoogleAppsScript.Spreadsheet.Sheet + const allSheetNames = ZzzSheetOperations.getAllSheetNames() + + if (allSheetNames.includes(newSheetName)) { + console.log(`[LOG] ${newSheetName} : すでに存在するシート名です`) + return + } + + if (activeSheetName === null) { + sheet = ZzzSheetOperations.changeActiveSheetTo(allSheetNames[allSheetNames.length - 1]) + } else { + sheet = ZzzSheetOperations.changeActiveSheetTo(activeSheetName) + } + + const newSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(newSheetName) + + console.log(`[END] ${newSheetName} : シートを作成しました`) + return newSheet + } + + // シートの作成および削除については、引数をシートの「名前」にしている + export const removeSheet = (sheetName: string) => { + const allSheetNames = ZzzSheetOperations.getAllSheetNames() + + if (!allSheetNames.includes(sheetName)) { + console.log(`[LOG] ${sheetName} : 存在しないシート名です`) + + return + } + + const spreadSheet = SpreadsheetApp.getActive(); + const sheet = ZzzSheetOperations.changeActiveSheetTo(sheetName) + spreadSheet.deleteSheet(sheet); + + console.log(`[END] ${sheetName} : シートを削除しました`) + return sheetName + } + + export const correspondenceObjectAboutColumnNameToColumnNumber = (columnNames: string[]) => { + let correspondenceObject = {} + + // 「columnNames の配列の並びはカラムの並びになっている」という前提である + columnNames.forEach((columnName, index) => { + correspondenceObject[columnName] = index + 1 + }) + + return correspondenceObject + } + + export const removeAllProtectedCellsOnAllSheets = () => { + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + ZzzSheetOperations.removeAllProtectedCells(sheet) + }, + '保護設定の解除' + ) + } + + export const removeAllProtectedCells = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + const protectionsRange = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE); + + for (let i = 0; i < protectionsRange.length; i++) { + let protection = protectionsRange[i]; + + if (protection.canEdit()) { + protection.remove(); + } + } + } + + // ヘッダ行は含まない + export const setBackgroundColorToSpecificColumnNumberOnSheet = (colNum: number, sheet: GoogleAppsScript.Spreadsheet.Sheet, color: string) => { + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101( + colNum, + sheet + ) + + range.setBackground(color) + } + + // ヘッダ行は含まない + export const setValueToSpecificColumnNumberOnSheet = (colNum: number, sheet: GoogleAppsScript.Spreadsheet.Sheet, value: string) => { + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101( + colNum, + sheet + ) + + range.setValue(value) + } +} diff --git a/clasp/gensosenkyo/app.ts b/clasp/gensosenkyo/app.ts new file mode 100644 index 00000000..538c2e67 --- /dev/null +++ b/clasp/gensosenkyo/app.ts @@ -0,0 +1,60 @@ +const main = () => { + // createTweetCountingSheets.destroyAllSheets() + + // NOTE: 約6分かかる + createTweetCountingSheets.createAllSheets() + + ZzzCommonScripts.showStartAndEndLogger(() => { + // createTweetCountingSheets.setColumnNames('mainDivision') + createTweetCountingSheets.setColumnNames('bonusVote') + // createTweetCountingSheets.setColumnNames('directMessage') + }, '列名を入力する') + + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.freezeFirstRowAndFirstColumn() + }, '一行目 および 一列目 を固定をする') + + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setColumnWidths() + }, '列幅を調整する') + + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setBanpeis() + }, '102行目の各セルに "@" を入れる') + + // 保護設定を全削除(やや重い処理) + ZzzSheetOperations.removeAllProtectedCellsOnAllSheets() + + // やや重い処理 + // NOTE: 冪等ではない(追記となる) + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setProtectedCells() + }, 'シートの保護機能を適用する') + + // やや重い処理 + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.createCheckBoxes() + }, 'チェックボックスを作成する') + + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setRappings() + }, '「ラッピング」の形式を設定する') + + // 「条件付き書式」を全削除 + ZzzConditionalFormats.clearConditionalFormatsOnAllSheets() + + // NOTE: 冪等ではない(追記となる) + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setDefaultConditionalFormats() + }, '「条件付き書式」を設定する') + + // NOTE: 冪等ではない(追記となる) + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setGrayBackGroundInSpecificCondition() + }, '(条件付き書式)特定のセルが条件を満たしたら行を灰色に塗る') + + // サジェスト用に「入力規則」を設定する(重い) + // createTweetCountingSheets.setDataValidationsForSuggestions('mainDivision') + createTweetCountingSheets.setDataValidationsForSuggestions('bonusVote') + // createTweetCountingSheets.setDataValidationsForSuggestions('directMessage') +} diff --git a/clasp/gensosenkyo/appForDirectMessages.ts b/clasp/gensosenkyo/appForDirectMessages.ts new file mode 100644 index 00000000..858dfc87 --- /dev/null +++ b/clasp/gensosenkyo/appForDirectMessages.ts @@ -0,0 +1,64 @@ +const mainForDirectMessages = () => { + // createTweetCountingSheets.destroyAllSheets() + + // NOTE: 約6分かかる + createTweetCountingSheets.createAllSheets() + + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setColumnNames('directMessage') + }, '列名を入力する') + + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.freezeFirstRowAndFirstColumn() + }, '一行目 および 一列目 を固定をする') + + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setColumnWidths('directMessage') + }, '列幅を調整する') + + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setBanpeis() + }, '102行目の各セルに "@" を入れる') + + // 保護設定を全削除(やや重い処理) + ZzzSheetOperations.removeAllProtectedCellsOnAllSheets() + + // やや重い処理 + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.createCheckBoxes('directMessage') + }, 'チェックボックスを作成する') + + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setRappings('directMessage') + }, '「ラッピング」の形式を設定する') + + // 「条件付き書式」を全削除 + ZzzConditionalFormats.clearConditionalFormatsOnAllSheets() + + // NOTE: 冪等ではない(追記となる) + ZzzCommonScripts.showStartAndEndLogger(() => { + // FIXME: 列がズレている + createTweetCountingSheets.setDefaultConditionalFormats('directMessage') + }, '「条件付き書式」を設定する') + + // NOTE: 冪等ではない(追記となる) + ZzzCommonScripts.showStartAndEndLogger(() => { + createTweetCountingSheets.setGrayBackGroundInSpecificCondition('directMessage') + }, '(条件付き書式)特定のセルが条件を満たしたら行を灰色に塗る') + + // サジェスト用に「入力規則」を設定する(重い) + createTweetCountingSheets.setDataValidationsForSuggestions('directMessage') + + // 「種類」に「入力規則」を与える + const colNameToNumber = ZzzColumnNames.colNameToNumber('directMessage') + + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + ZzzDataValidation.setDataValidationDirectMessageTypes( + sheet, + colNameToNumber['種類'] + ) + }, + '「種類」の「入力規則」を設定する' + ) +} diff --git a/clasp/gensosenkyo/appsscript.json b/clasp/gensosenkyo/appsscript.json new file mode 100644 index 00000000..d54a72f3 --- /dev/null +++ b/clasp/gensosenkyo/appsscript.json @@ -0,0 +1,10 @@ +{ + "timeZone": "Asia/Tokyo", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "webapp": { + "executeAs": "USER_DEPLOYING", + "access": "ANYONE_ANONYMOUS" + } +} \ No newline at end of file diff --git a/clasp/gensosenkyo/createDirectMessageCountingSheets.ts b/clasp/gensosenkyo/createDirectMessageCountingSheets.ts new file mode 100644 index 00000000..7dacc7d0 --- /dev/null +++ b/clasp/gensosenkyo/createDirectMessageCountingSheets.ts @@ -0,0 +1,3 @@ +const createDirectMessageCountingSheets = () => { + // console.log(directMessageTypes) +} diff --git a/clasp/gensosenkyo/createTweetCountingSheets.ts b/clasp/gensosenkyo/createTweetCountingSheets.ts new file mode 100644 index 00000000..78cc0770 --- /dev/null +++ b/clasp/gensosenkyo/createTweetCountingSheets.ts @@ -0,0 +1,292 @@ +namespace createTweetCountingSheets { + // NOTE: 約6分かかる + export const createAllSheets = () => { + const sheetNames = ZzzSheetNames.allSheetNames + + sheetNames.forEach(sheetName => { + ZzzSheetOperations.createSheet({newSheetName: sheetName}) + }) + + return sheetNames + } + + export const destroyAllSheets = () => { + const sheetNames = ZzzSheetNames.allSheetNames + + sheetNames.forEach(removeSheet => { + ZzzSheetOperations.removeSheet(removeSheet) + }) + + return sheetNames + } + + export const setColumnNames = (category = 'mainDivision') => { + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + ZzzCellOperations.setFirstRowNames(sheet, category) + }, '列名を設定しました' + ) + } + + export const freezeFirstRowAndFirstColumn = () => { + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + ZzzCellOperations.freezeFirstRow(sheet) + ZzzCellOperations.freezeFirstColumn(sheet) + }, '一行目 および 一列目 を固定をする' + ) + } + + export const setColumnWidths = (category = '') => { + const colNameToNumber = ZzzColumnNames.colNameToNumber(category) + + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + sheet.setColumnWidth(colNameToNumber['ID'], 40) + sheet.setColumnWidth(colNameToNumber['screen_name'], 30) + sheet.setColumnWidth(colNameToNumber['日時'], 30) + sheet.setColumnWidth(colNameToNumber['全終了?'], 40) + sheet.setColumnWidth(colNameToNumber['集計対象外?'], 90) + sheet.setColumnWidth(colNameToNumber['二次チェック済?'], 130) + sheet.setColumnWidth(colNameToNumber['備考'], 100) + sheet.setColumnWidth(colNameToNumber['要レビュー?'], 90) + sheet.setColumnWidth(colNameToNumber['キャラ1 or 作品名'], 140) + sheet.setColumnWidth(colNameToNumber['キャラ2 or 協力攻撃名'], 140) + sheet.setColumnWidth(colNameToNumber['キャラ3'], 140) + + if (category === 'directMessage') { + sheet.setColumnWidth(colNameToNumber['内容'], 300) + sheet.setColumnWidth(colNameToNumber['dm_id'], 50) + sheet.setColumnWidth(colNameToNumber['DMが見られない?'], 120) + sheet.setColumnWidth(colNameToNumber['対応済み?'], 90) + } else { + sheet.setColumnWidth(colNameToNumber['内容'], 200) + sheet.setColumnWidth(colNameToNumber['tweet_id'], 50) + sheet.setColumnWidth(colNameToNumber['URL'], 30) + sheet.setColumnWidth(colNameToNumber['別ツイ'], 40) + sheet.setColumnWidth(colNameToNumber['ツイ見られない?'], 120) + sheet.setColumnWidth(colNameToNumber['ふぁぼ済?'], 90) + } + }, + '列幅を指定しました' + ) + } + + // 既存データを上書きする破壊的メソッドなので注意する + export const setBanpeis = () => { + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + ZzzCellOperations.setLastRowSymbols(sheet) + }, + '102行目の各セルに "@" を入れました' + ) + } + + export const setProtectedCells = () => { + const colNameToNumber = ZzzColumnNames.colNameToNumber() + + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + const protectedColumnNumbers = [ + colNameToNumber['ID'], + colNameToNumber['screen_name'], + colNameToNumber['tweet_id'], + colNameToNumber['日時'], + colNameToNumber['URL'], + colNameToNumber['全終了?'], + colNameToNumber['別ツイ'], + ] + + protectedColumnNumbers.forEach(protectedColumnNumber => { + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101(protectedColumnNumber, sheet) + + range.protect() // デフォルトでは自分と自分のグループのみが編集可 + }) + }, + 'シートの保護設定をしました' + ) + } + + // 既存データを上書きする破壊的メソッドなので注意する + export const createCheckBoxes = (category = '') => { + const colNameToNumber = ZzzColumnNames.colNameToNumber(category) + + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + const requiredCheckboxColumnNumbers = [ + colNameToNumber['集計対象外?'], + colNameToNumber['二次チェック済?'], + colNameToNumber['要レビュー?'], + ] + + if (category === 'directMessage') { + requiredCheckboxColumnNumbers.push(colNameToNumber['対応済み?']) + requiredCheckboxColumnNumbers.push(colNameToNumber['DMが見られない?']) + } else { + requiredCheckboxColumnNumbers.push(colNameToNumber['ふぁぼ済?']) + requiredCheckboxColumnNumbers.push(colNameToNumber['ツイ見られない?']) + } + + requiredCheckboxColumnNumbers.forEach(requiredCheckboxColumnNumber => { + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101(requiredCheckboxColumnNumber, sheet) + + ZzzCellOperations.createCheckBoxes(range) + }) + }, + 'チェックボックスを追加しました' + ) + } + + // 表示形式 -> ラッピング -> はみ出す | 折り返す | 切り詰める + export const setRappings = (category = '') => { + const colNameToNumber = ZzzColumnNames.colNameToNumber('directMessage') + + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + const kiritsumeruColumnNumbers = [ + colNameToNumber['screen_name'], + colNameToNumber['日時'], + ] + + if (category !== 'directMessage') { + kiritsumeruColumnNumbers.push(colNameToNumber['別ツイ']) // DMでは存在しないカラム + kiritsumeruColumnNumbers.push(colNameToNumber['tweet_id']) // DMでは存在しないカラム + kiritsumeruColumnNumbers.push(colNameToNumber['URL']) // DMでは存在しないカラム + } + + kiritsumeruColumnNumbers.forEach(requiredColumnNumber => { + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101(requiredColumnNumber, sheet) + + ZzzCellOperations.rappingKiritsumeru(range) + }) + + const orikaesuColumnNumbers = [ + colNameToNumber['内容'], + colNameToNumber['備考'], + ] + + orikaesuColumnNumbers.forEach(requiredColumnNumber => { + const range = ZzzCellOperations.getRangeSpecificColumnRow2ToRow101(requiredColumnNumber, sheet) + + ZzzCellOperations.rappingOrikaesu(range) + }) + }, + '「表示形式 -> ラッピング」の設定が完了' + ) + } + + export const setDefaultConditionalFormats = (category = '') => { + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + // 「全終了?」列 + // FIXME: + ZzzConditionalFormats.setInitToIsAllCompletedColumn(sheet, category) + + // 「要レビュー?」列 + ZzzConditionalFormats.setInitToIsRequiredReview(sheet, category) + + // 「二次チェック済?」列 + ZzzConditionalFormats.setInitToIsCompletedSecondCheck(sheet, category) + + if (category === 'directMessage') { + // 「対応済み?」列 + ZzzConditionalFormats.setInitToIsCompletedDMResponse(sheet, category) + } else { + // 「ふぁぼ済?」列 + ZzzConditionalFormats.setInitToIsCompletedFavorite(sheet, category) + } + }, + '「条件付き書式」の設定' + ) + } + + // 特定条件において行全体を灰色の背景にする「条件付き書式」 + export const setGrayBackGroundInSpecificCondition = (category = '') => { + const colNameToNumber = ZzzColumnNames.colNameToNumber(category) + let columnAlphabet: string + let newRule: GoogleAppsScript.Spreadsheet.ConditionalFormatRule + + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + const rules = sheet.getConditionalFormatRules() + + for (let i = 2; i <= 101; i++) { + columnAlphabet = ZzzConverters.convertColumnNumberToAlphabet(colNameToNumber['集計対象外?']) + + newRule = ZzzConditionalFormats.getRuleToSetGrayBackgroundToAllRowCellsInSpecificCondition( + i, + sheet, + `=$${columnAlphabet}$${i}=TRUE`, + sheet.getRange(i, 1, 1, 100) + ) + rules.push(newRule) + + if (category === 'directMessage') { + columnAlphabet = ZzzConverters.convertColumnNumberToAlphabet(colNameToNumber['DMが見られない?']) + } else { + columnAlphabet = ZzzConverters.convertColumnNumberToAlphabet(colNameToNumber['ツイ見られない?']) + } + + newRule = ZzzConditionalFormats.getRuleToSetGrayBackgroundToAllRowCellsInSpecificCondition( + i, + sheet, + `=$${columnAlphabet}$${i}=TRUE`, + sheet.getRange(i, 1, 1, 100) + ) + rules.push(newRule) + } + + sheet.setConditionalFormatRules(rules) + }, + '特定条件において行全体を灰色の背景にする「条件付き書式」' + ) + } + + // サジェスト用に「入力規則」を設定する(重い) + export const setDataValidationsForSuggestions = ( + category: 'mainDivision' | 'bonusVote' | 'directMessage' + ) => { + const colNameToNumber = ZzzColumnNames.colNameToNumber(category) + + const targetColumnNumbers = [ + colNameToNumber['キャラ1 or 作品名'], + colNameToNumber['キャラ2 or 協力攻撃名'], + colNameToNumber['キャラ3'], + colNameToNumber['キャラ4'], + colNameToNumber['キャラ5'], + colNameToNumber['キャラ6'], + colNameToNumber['キャラ7'], + colNameToNumber['キャラ8'], + colNameToNumber['キャラ9'], + colNameToNumber['キャラ10'], + ] + const colNumbersWithoutFalsy = targetColumnNumbers.filter(v => v) + + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + colNumbersWithoutFalsy.forEach(targetColumnNumber => { + ZzzDataValidation.setDataValidationToCell( + sheet, + targetColumnNumber + ) + }) + }, + '「入力規則」を設定する(サジェスト用)' + ) + } + + // ここは Ruby 側でデータを入れるときにやればいいので、未使用メソッド + export const setAllTrueValuesToIsFavoriteColumn = () => { + const colNameToNumber = ZzzColumnNames.colNameToNumber() + + ZzzSheetOperations.applyFunctionToAllCountingSheets( + (sheet: GoogleAppsScript.Spreadsheet.Sheet) => { + ZzzSheetOperations.setValueToSpecificColumnNumberOnSheet( + colNameToNumber['ふぁぼ済?'], + sheet, + 'TRUE' + ) + } + ) + } +} diff --git a/clasp/gensosenkyo/debugger.ts b/clasp/gensosenkyo/debugger.ts new file mode 100644 index 00000000..62e8fcf9 --- /dev/null +++ b/clasp/gensosenkyo/debugger.ts @@ -0,0 +1,17 @@ +const debugFunction = () => { + const colNameToNumber = ZzzColumnNames.colNameToNumber() + + const sheet = ZzzSheetOperations.changeActiveSheetTo('シート123') + + ZzzSheetOperations.setBackgroundColorToSpecificColumnNumberOnSheet( + colNameToNumber['ふぁぼ済?'], + sheet, + 'gray' + ) + + ZzzSheetOperations.setValueToSpecificColumnNumberOnSheet( + colNameToNumber['ふぁぼ済?'], + sheet, + 'TRUE' + ) +} diff --git a/clasp/gensosenkyo/package.json b/clasp/gensosenkyo/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/clasp/gensosenkyo/package.json @@ -0,0 +1 @@ +{} diff --git a/config/sheet_names.yml b/config/counting_sheet_names.yml similarity index 62% rename from config/sheet_names.yml rename to config/counting_sheet_names.yml index 9dd417de..20273d6f 100644 --- a/config/sheet_names.yml +++ b/config/counting_sheet_names.yml @@ -1,12 +1,10 @@ -sheet_names: +names: + - 投票まとめ - ①オールキャラ部門 - ②協力攻撃部門 - - DM・①オールキャラ部門 - - DM・②協力攻撃部門 + - DM(ダイレクトメッセージ) - ボーナス票・OP・CLイラスト - ボーナス票・お題小説 - ボーナス票・開票イラスト - ボーナス票・推し台詞 - ボーナス票・選挙運動 - - 開発用スプレッドシート - - 統合用(予定) diff --git a/config/sheet_headers_and_column_names_relations/counting_all_characters.yml b/config/sheet_headers_and_column_names_relations/counting_all_characters.yml new file mode 100644 index 00000000..565613f0 --- /dev/null +++ b/config/sheet_headers_and_column_names_relations/counting_all_characters.yml @@ -0,0 +1,51 @@ +- # UNIQUE + sheet_header: ID + column_name: id_on_sheet +- + sheet_header: screen_name + column_name: screen_name +- + sheet_header: tweet_id # name + column_name: tweet_id_number +- + sheet_header: 日時 + column_name: tweeted_at +- + sheet_header: URL + column_name: url +- + sheet_header: 別ツイ # カンマ区切りテキスト + column_name: other_tweets +- + sheet_header: 全終了? + column_name: is_all_completed +- + sheet_header: ツイ見られない? + column_name: is_tweet_invisible +- + sheet_header: 集計対象外? + column_name: is_not_included_counting +- + sheet_header: ふぁぼ済? + column_name: is_already_favorite +- + sheet_header: 二次チェック済? + column_name: is_already_second_check +- + sheet_header: 内容 + column_name: full_text +- + sheet_header: 備考 + column_name: memo +- + sheet_header: 要レビュー? + column_name: is_review_required +- + sheet_header: キャラ1 # キャラ1 or 作品名 + column_name: character_01 +- + sheet_header: キャラ2 # キャラ2 or 協力攻撃名 + column_name: character_02 +- + sheet_header: キャラ3 + column_name: character_03 diff --git a/config/sheet_headers_and_column_names_relations/on_raw_sheet_unite_attacks.yml b/config/sheet_headers_and_column_names_relations/on_raw_sheet_unite_attacks.yml index 5e96d9b2..2e19d3a7 100644 --- a/config/sheet_headers_and_column_names_relations/on_raw_sheet_unite_attacks.yml +++ b/config/sheet_headers_and_column_names_relations/on_raw_sheet_unite_attacks.yml @@ -8,10 +8,10 @@ sheet_header: 英語表記 column_name: name_en - - sheet_header: キャラ1 + sheet_header: キャラ1 # キャラ1 or 作品名 column_name: chara_1 - - sheet_header: キャラ2 + sheet_header: キャラ2 # キャラ2 or 協力攻撃名 column_name: chara_2 - sheet_header: キャラ3 diff --git a/lib/tasks/change_is_public_attributes.rake b/lib/tasks/change_is_public_attributes.rake index a8f109f4..675aef65 100644 --- a/lib/tasks/change_is_public_attributes.rake +++ b/lib/tasks/change_is_public_attributes.rake @@ -4,7 +4,7 @@ namespace :change_is_public_attributes do tweets = Tweet.all tweet_id_numbers = tweets.map(&:id_number) - # app/lib/check_visible_and_invisible_tweet_id_numbers.rb を参照のこと + # Twitter の REST API が消費されるので注意する visible_and_invisible_tweet_id_numbers = CheckVisibleAndInvisibleTweetIdNumbers.exec(tweet_id_numbers) visible_tweet_id_numbers = visible_and_invisible_tweet_id_numbers[:visible] diff --git a/lib/tasks/write_db_data_to_sheet.rake b/lib/tasks/write_db_data_to_sheet.rake new file mode 100644 index 00000000..b87ab9b0 --- /dev/null +++ b/lib/tasks/write_db_data_to_sheet.rake @@ -0,0 +1,31 @@ +namespace :write_db_data_to_sheet do + desc '[オールキャラ部門] DBのデータをシートに書き込む' + task all_characters: :environment do + Sheets::WriteAndUpdate::AllCharacters.exec + end + + desc '[協力攻撃部門] DBのデータをシートに書き込む' + task unite_attacks: :environment do + Sheets::WriteAndUpdate::UniteAttacks.exec + end + + desc '[ボーナス票・お題小説] DBのデータをシートに書き込む' + task short_stories: :environment do + Sheets::WriteAndUpdate::ShortStories.exec + end + + desc '[ボーナス票・推し台詞] DBのデータをシートに書き込む' + task fav_quotes: :environment do + Sheets::WriteAndUpdate::FavQuotes.exec + end + + desc '[ボーナス票・選挙運動] DBのデータをシートに書き込む' + task sosenkyo_campaigns: :environment do + Sheets::WriteAndUpdate::SosenkyoCampaigns.exec + end + + desc '[ダイレクトメッセージ] DBのダイレクトメッセージのデータをシートに書き込む' + task direct_messages: :environment do + Sheets::WriteAndUpdate::DirectMessages.exec + end +end