見出し画像

【GAS】電子チケット自動生成を実現した話

さて、ダブルダッチの大会の運営メンバーをしている私。
今回はとうとう面倒すぎて頭に来ていた「電子チケットの画像作成」をGAS(Google apps Script)を用いて自動化しました!

function ticket() 
  {
  let startTime = new Date(); // ①実行開始時点の日時
  //開いているスプレッドシートファイルを取得
  var spreadsheet = SpreadsheetApp.openById("******"); //IDを差し替え
  //シートを取得  
  var sheet = spreadsheet.getSheetByName('シート1'); //※利用時はシート名を差し替え
  //保存先のフォルダを指定
  var folder1 = DriveApp.getFolderById('*******'); //フォルダIDを差し替え
  //チケットを格納するための新規フォルダを作成します
  var folder2 = folder1.createFolder("チケットデータ"); //※利用時はフォルダ名を差し替え
  Logger.log('folder2=チケットデータ');
  Logger.log('作業時間計測開始');

    //ここからチケットの作成に入ります。
    //定義したもの以外のテキストボックスや図形が編集可能な状態にあるとエラーが出るため、
    //テーマ作成ツールでテンプレートとして保存しておいてください。

   //テキスト置換のための下準備です。(★)
   //置換を行いたいテキストボックスの数だけ以下のように定義します。
   //var id_***** = '{******}';「''」内をテキストボックスのオリジナルテキスト(置換前のテキスト)に差し替えてください。
   var id_program = '{program}';
   //申し込み者リストを全行取得します。
  var sheetRows = sheet.getDataRange().getValues();
 
    let startIndex = Number(PropertiesService.getScriptProperties().getProperty('nextIndex'));
  if (!startIndex) startIndex = 1; // もしstartIndexがnullの場合は1を代入
    for(let row of sheetRows) 
  {
      let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)

    if(seconds > 300){

      setTrigger();
      return;
    }
      //氏名が入った列を指定(A列=0)
      let program = `${row[0]}`; //列を差し替え
      let name = `${row[1]}`; //列を差し替え
      let dantai = `${row[3]}`; //列を差し替え
      //ファイル名を指定(複製したスライドおよび画像出力した際のファイル名として使います。違う名前にすることも可能です)
      let proof_name = program+'_'+name+'_'+dantai; 
              Logger.log(proof_name);
      //テンプレートのスライドファイルを複製
      let templatefile1 = DriveApp.getFileById('*******'); //※利用時ファイルIDを差し替え
      let copied_proof = templatefile1.makeCopy(proof_name); //ファイル名、保存フォルダを指定
        //複製したファイルを取得し、テキスト置換を行います。
        //コピーしたファイルのID取得
        let cp_id = copied_proof.getId();
        //コピーしたファイルを編集する準備
      let slides_file = SlidesApp.openById(cp_id);
      let slides =slides_file.getSlides();

      //今回は1ページ目(=0)のみ編集します。
      let page_no = 0
      let slide = slides[page_no];
      let pageElements = slide.getPageElements();

      //スライドのテキストボックス(シェイプ)を取得
      for(let shape_no=0;shape_no<pageElements.length;shape_no++)
      {
        //指定のフォーマットの文字列を置換(★で指定したID,置換後テキスト)
        pageElements[shape_no].asShape().getText().replaceAllText(id_program, program);  //テキスト置換2
      }
      //編集したスライドを保存(これがないと最初に取得したスライドの状態のまま画像変換されるので注意)
      slides_file.saveAndClose();

      //修了証をスライドから画像に変換します
      //変換url(エクスポート)
      let export_url = 'https://docs.google.com/presentation/d/'+ cp_id +'/export/png';  //ファイル形式を指定
      //オプションを指定
      let options = 
      {
        method: "get",
        headers: {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
        muteHttpExceptions: true
      }
      let res = UrlFetchApp.fetch(export_url, options);
      if (res.getResponseCode() === 200) 
      {
      folder2.createFile(res.getBlob()).setName(proof_name + ".png");  //ファイル名、保存フォルダを指定
            Logger.log(proof_name + '画像作成完了');      
      }

      //複製したファイルを取得し、削除を行います。
        let fileData = DriveApp.getFileById(cp_id);
      //IDから取得したファイルをゴミ箱のフラグをtrueにする
      //ファイルから処理済みのデータを削除します。
      let getData = fileData.setTrashed(true);
      let spreadsheet = SpreadsheetApp.openById("******"); //IDを差し替え
      //シートを取得
      let sheet = spreadsheet.getSheetByName('シート1'); //※利用時はシート名を差し替え  

      Logger.log('複製ファイルを削除済み');

  //スクリプトに紐づいたスプレッドシートのアクティブなシートを読み込み
  //deleteRowメソッドで引数指定した1行を削除
  sheet.deleteRow(1);//ここでは1行目指定
        Logger.log('処理済みの行を削除');

            // 処理し終えたらトリガーを削除
  let triggers = ScriptApp.getProjectTriggers();
  for(let trigger of triggers){
    if(trigger.getHandlerFunction() == 'ticket'){
      ScriptApp.deleteTrigger(trigger);
    }
  }
}
}


function setTrigger() {
  let triggers = ScriptApp.getProjectTriggers();
  for(let trigger of triggers)
    {
      if(trigger.getHandlerFunction() == 'ticket')
      {
        ScriptApp.deleteTrigger(trigger);
      }
    }
  // 1分後にトリガーをセット(1分 = 60秒 = 1秒*60 = 1000ミリ秒 * 60)
  ScriptApp.newTrigger('ticket').timeBased().after(1000 * 60).create();
          Logger.log('トリガーをセット');
  }

無駄が多いコードですが色々と試行錯誤して頑張って組み立てました。
それでは一箇所ずつ解説していきたいと思います〜!

処理開始時間の計測→シート取得→フォルダの作成まで

GASには6分間の壁というものがあるので全体の処理が5分で一度止まるようにプログラムを組みました。

開始時間の計測

なので当function(function ticket)の稼働開始時間を記録するため
let startTime = new Date(); // ①実行開始時点の日時
を仕込みました。
これで任意の時間で止める準備が出来ました。

シート取得

ちょっと面倒ですがアクティブなものでなく、IDを元にプログラムを動かしています。
シート選択は本当はシート名ではない方が良いかもしれないですが、GASに詳しくないので。。。

フォルダ作成

チケットデータを作成し、それを格納するフォルダを作成します。
保存先のフォルダを指定してからCreatefolderでフォルダを作成。

  function ticket() 
  {
  let startTime = new Date(); // ①実行開始時点の日時
  //開いているスプレッドシートファイルを取得
  var spreadsheet = SpreadsheetApp.openById("******"); //IDを差し替え
  //シートを取得  
  var sheet = spreadsheet.getSheetByName('シート1'); //※利用時はシート名を差し替え
  //保存先のフォルダを指定
  var folder1 = DriveApp.getFolderById('*******'); //フォルダIDを差し替え
  //チケットを格納するための新規フォルダを作成します
  var folder2 = folder1.createFolder("チケットデータ"); //※利用時はフォルダ名を差し替

ticket作成

さてここからはメインの部分。
チケット画像の作成です。

斜線部(//)はプログラムでは反映しないコメントの部分ですので無視してもらって大丈夫です。

参照元はこちらです(https://note.com/mochi_assistant/n/nb543a4ee2143
下準備の部分などはコチラの記事を参考にしてください。

リストを全取得

GetDateRangeで申込者のリストまたは配席したチケットの座席表を読み込みます。

    //ここからチケットの作成に入ります。
    //定義したもの以外のテキストボックスや図形が編集可能な状態にあるとエラーが出るため、
    //テーマ作成ツールでテンプレートとして保存しておいてください。

   //テキスト置換のための下準備です。(★)
   //置換を行いたいテキストボックスの数だけ以下のように定義します。
   //var id_***** = '{******}';「''」内をテキストボックスのオリジナルテキスト(置換前のテキスト)に差し替えてください。
   var id_program = '{program}';
   //申し込み者リストを全行取得します。
  var sheetRows = sheet.getDataRange().getValues();
    for(let row of sheetRows) //くりかえし処理For...of
  {
      let currentTime = new Date(); // ②ループx周目時点の日時
    let seconds = (currentTime - startTime)/1000; // 経過秒数を計算(①と②の差分)

    if(seconds > 300){
      setTrigger();
      return;
    }

ループ開始

6分間の壁を超えるために繰り返し関数を参照元で利用していた「forEach」から「for(…of…)」へと変更しています。(ここを乗り越えるのが大変だった(;´・ω・))

ここから繰り返し処理が始まるので現在時点での時間を取得し、開始時との差分が5分(300秒)になったら停止するように指定しています。

if関数でもしも現在の経過時間が300秒を超えていればトリガーをセットしてループを強制終了します。
トリガーセットは別のfunctionでまとめています。

繰り返し処理(列の指定)

列の内容をそれぞれの変数ボックスに格納していく準備をします。
生成後の画像ファイルのファイル名はわかりやすいようにここで格納した変数を利用させて名付けていきます。

  //氏名が入った列を指定(A列=0)
   let program = `${row[0]}`; //列を差し替え
      let name = `${row[1]}`; //列を差し替え
      let dantai = `${row[3]}`; //列を差し替え

      //ファイル名を指定(複製したスライドおよび画像出力した際のファイル名として使います。違う名前にすることも可能です)
      let proof_name = program+'_'+name+'_'+dantai;

繰り返し処理(画像作成)

テンプレートファイルを複製してから内容を変更し、スライドを保存します。

参考元をそのまま使って1ページ目のみ編集する形のものにしました。

//テンプレートのスライドファイルを複製
let templatefile1 = DriveApp.getFileById('1X9wIqz_2u8ACqUZCWzl20SUQzue7V63l0vhb-on5ZB8'); //※利用時ファイルIDを差し替え
let copied_proof = templatefile1.makeCopy(proof_name); //ファイル名、保存フォルダを指定
//複製したファイルを取得し、テキスト置換を行います。
//コピーしたファイルのID取得
let cp_id = copied_proof.getId();
 //コピーしたファイルを編集する準備
 let slides_file = SlidesApp.openById(cp_id);
 let slides =slides_file.getSlides();

      //今回は1ページ目(=0)のみ編集します。
      let page_no = 0
      let slide = slides[page_no];
      let pageElements = slide.getPageElements();

      //スライドのテキストボックス(シェイプ)を取得
      for(let shape_no=0;shape_no<pageElements.length;shape_no++)
      {
        //指定のフォーマットの文字列を置換(★で指定したID,置換後テキスト)
        pageElements[shape_no].asShape().getText().replaceAllText(id_program, program);  //テキスト置換2
      }
      //編集したスライドを保存(これがないと最初に取得したスライドの状態のまま画像変換されるので注意)
      slides_file.saveAndClose();

ここから先は結構ブラックボックスで私もあまり理解していない領域です(;^ω^)
そして参考元との変更点で、生成したスライドファイルを削除するパートも組み込んだのでファイルIDも取得しておきます。

      //修了証をスライドから画像に変換します
      //変換url(エクスポート)
      let export_url = 'https://docs.google.com/presentation/d/'+ cp_id +'/export/png';  //ファイル形式を指定
      //オプションを指定
      let options = 
      {
        method: "get",
        headers: {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
        muteHttpExceptions: true
      }
      let res = UrlFetchApp.fetch(export_url, options);
      if (res.getResponseCode() === 200) 
        {
        folder2.createFile(res.getBlob()).setName(proof_name + ".png");  //ファイル名、保存フォルダを指定
              Logger.log(proof_name + '画像作成完了');      
        let fileId = file.getId(); // ファイルID
        Logger.log(fileId)
        let fileName = file.getName(); // ファイルID
        Logger.log(fileName)

        }

複製したスライドファイルを削除

それと同時に6分間の壁対策で私が導入したのは「処理済みの行を削除しちゃえ」ってことでもう一度シートを確認しております。(必要か不明w)

      //複製したファイルを取得し、削除を行います。
        let fileData = DriveApp.getFileById(cp_id);
      //IDから取得したファイルをゴミ箱のフラグをtrueにする
      //ファイルから処理済みのデータを削除します。
      let getData = fileData.setTrashed(true);
      let spreadsheet = SpreadsheetApp.openById("1926JZg2V6tQfAklcI1FNBlojrUEqh-VDq9WbkGeBzGg");
      //シートを取得
      let sheet = spreadsheet.getSheetByName('シート1'); //※利用時はシート名を差し替え  

      Logger.log('複製ファイルを削除済み');

処理済みのデータを削除

処理した行を削除していく脳筋プレイ。
同時にGASのトリガー上限20個対策も行っちゃいます。


  //スクリプトに紐づいたスプレッドシートのアクティブなシートを読み込み
  //deleteRowメソッドで引数指定した1行を削除
  sheet.deleteRow(1);//ここでは1行目指定
        Logger.log('処理済みの行を削除');

            // 処理し終えたらトリガーを削除
  let triggers = ScriptApp.getProjectTriggers();
  for(let trigger of triggers){
    if(trigger.getHandlerFunction() == 'ticket'){
      ScriptApp.deleteTrigger(trigger);
    }
   }
  }
}

トリガーセット

300秒経過した時、IF関数でトリガーの設置を設定します。
参考元はこちら(https://coporilife.com/116/

function setTrigger() {
  let triggers = ScriptApp.getProjectTriggers();
  for(let trigger of triggers)
    {
      if(trigger.getHandlerFunction() == 'ticket')
      {
        ScriptApp.deleteTrigger(trigger);
      }
    }
  // 1分後にトリガーをセット(1分 = 60秒 = 1秒*60 = 1000ミリ秒 * 60)
  ScriptApp.newTrigger('ticket').timeBased().after(1000 * 60).create();
          Logger.log('トリガーをセット');
  }

これもあんまりわかってないですw
どうにかこうにかこねくり回してこの形に落ち着けました。

まとめ

さて今回はGASとトリガーを用いて電子チケットを作成するプログラムを書きました。
今まで手作業で行っていたチケット制作がノンストレスで行えると思うと一つ肩の荷が下りた気分です。

他にも小規模イベントに使えるGASを組んでいったら紹介したいと思いますのでどうぞよろしくお願いいたします。

応援したいと思ってくれた方、いらっしゃいましたらサポートお願いします!