memorandums

日々のメモです。

Google App ScriptとFusion Tableで簡易クリッカーアプリ

Google App ScriptとFusion Tableを使って簡易クリッカーアプリを作っては試してきました。

担当講義でも非常勤先でも使ってみていい感じになってきました。

バージョン的には以下のような感じで作ってきました。

  1. 学籍番号と4択を4問をHTMLで表示して解答を送信するまで。
  2. 下2桁でいいって伝えているのにフル桁入力する輩がいるので自動修正する機能を付加。
  3. 送信後に「送信中」、そして送信完了時に正常終了かエラーだったのかを表示する機能を付加。
  4. CSSを使ってボタンや選択肢を大きく
  5. Fusion TableでCSVダウンロードしてGoogleスプレッドシートに貼り付け、正解を予め入力しておいて、個人ごとに集計する機能をスプレッドシートで作成。
  6. クイズ解答中以外は解答できないようにロック機構を作った。ロック解除は講師が別ページから行う。

Fusion TableとGoogleスプレッドシートの連携のところが手動ですが、とりあえずは許容範囲という感じです。

きちゃないソースですが。。。もしかすると誰かの参考になるかもしれませんのでソースを貼っておきます。

クリッカーアプリ(学生用)

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="cache-control" content="no-cache" />
    <meta http-equiv="expires" content="0" />
    <base target="_top">
    <style>
    html {
      font-size: calc(112.5% + 4 * (100vw - 600px) / 400)
    }
    body {
      -webkit-text-size-adjust: 100%; 
    }
    .button {
      font-size: 1.4em;
      font-weight: bold;
      padding: 10px 30px;
      background-color: #248;
      color: #fff;
      border-style: none;
    }
    .radio {
      width: 30px;
      height: 30px;
    }
    </style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=yes" />
  </head>
  <body>
    <h3>学籍番号(下2桁)</h3>
    <input type="text" id="id" style="font-size: 1.4em" onchange="validateId()"></p>
    <div id="infoId" style="color:red;"></div>
<hr>
    <form id="no1">
      <h3>第1問</h3>
      <p><input type="radio" class="radio" id="a" name="ans" value="A" checked/><label for="a">A</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="b" name="ans" value="B" /><label for="b">B</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="c" name="ans" value="C" /><label for="c">C</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="d" name="ans" value="D" /><label for="d">D</label>&nbsp;&nbsp;
      <input type="button" class="button" class="button" id="btn1" value="送信" onclick="submitAns(1);"></p>
      <div id="infoNo1" style="color:green;"></div>
    </form>
<hr>
    <form id="no2">
      <h3>第2問</h3>
      <p><input type="radio" class="radio" id="a" name="ans" value="A" checked/><label for="a">A</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="b" name="ans" value="B" /><label for="b">B</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="c" name="ans" value="C" /><label for="c">C</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="d" name="ans" value="D" /><label for="d">D</label>&nbsp;&nbsp;
      <input type="button" class="button" class="button" id="btn2" value="送信" onclick="submitAns(2);"></p>
      <div id="infoNo2" style="color:green;"></div>
    </form>
<hr>
    <form id="no3">
      <h3>第3問</h3>
      <p><input type="radio" class="radio" id="a" name="ans" value="A" checked/><label for="a">A</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="b" name="ans" value="B" /><label for="b">B</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="c" name="ans" value="C" /><label for="c">C</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="d" name="ans" value="D" /><label for="d">D</label>&nbsp;&nbsp;
      <input type="button" class="button" class="button" id="btn2" value="送信" onclick="submitAns(3);"></p>
      <div id="infoNo3" style="color:green;"></div>
    </form>
<hr>
    <form id="no4">
      <h3>第4問</h3>
      <p><input type="radio" class="radio" id="a" name="ans" value="A" checked/><label for="a">A</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="b" name="ans" value="B" /><label for="b">B</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="c" name="ans" value="C" /><label for="c">C</label>&nbsp;&nbsp;
      <input type="radio" class="radio" id="d" name="ans" value="D" /><label for="d">D</label>&nbsp;&nbsp;
      <input type="button" class="button" class="button" id="btn2" value="送信" onclick="submitAns(4);"></p>
      <div id="infoNo4" style="color:green;"></div>
    </form>
    
    <script type="text/javascript">
    function validateId() {
      var id = document.getElementById("id");
      var infoId = document.getElementById("infoId");
      if (isNaN(id.value) || id.value == "") {
        infoId.innerHTML = "学籍番号を数値で入力してください。";
        return false;
      } else {
        infoId.innerHTML = "";
        var no1 = document.getElementById("no1");
        var no2 = document.getElementById("no2");
        var no3 = document.getElementById("no3");
        var no4 = document.getElementById("no4");
        no1.value = no2.value = no3.value = no4.value = id.value;
        return true;
      }
    }

    function getTime() {
      var d = new Date();
      var yy = d.getFullYear();
      var mm = d.getMonth() + 1;
      var dd = d.getDate();
      var H = d.getHours();
      var M = d.getMinutes();
      var S = d.getSeconds();
      return yy + '-' + mm + '-' + dd + ' ' + H + ':' + M + ':' + S;
    }
    
    function submitAns(no) {
      if (validateId() == false) {
        alert("学籍番号を入力してから再度送信してください。");
        return;
      }
      var idElement = document.getElementById("id");
      var id = idElement.value;
      var noElement = document.getElementById("no" + no);
      var radioNodeList = noElement.ans;
      var ans = radioNodeList.value;
      var infoNo = document.getElementById('infoNo' + no);
      infoNo.innerHTML = '送信中です...'
      function onSuccess(res) {
        var infoNo = document.getElementById('infoNo' + no);
        if (res == '0') {
          infoNo.style.color='red';
          infoNo.innerHTML = '時間外のため送信できません。'
        } else {
          infoNo.style.color='green';
          infoNo.innerHTML = '送信完了しました。'
        }
        var btn = document.getElementById('btn' + no);
        btn.disabled = true;
      }
      function onFailure(error) {
        var infoNo = document.getElementById('infoNo' + no);
        infoNo.style.color = 'red';
        infoNo.innerHTML = '再度、送信してください。';
      }
      console.log(id+","+no+","+ans);
      google.script.run
        .withSuccessHandler(onSuccess)
        .withFailureHandler(onFailure)
        .insert(id, no, ans, getTime());
    }
    </script>
  </body>
</html>

コード.gs

function doGet() {
  return HtmlService.createHtmlOutputFromFile('index');
}

function isWritable(no) {
  var tableId = '★ここは書き換えてね★';
  var sql = 'SELECT writable FROM ' + tableId;
  var res = FusionTables.Query.sql(sql);
//  Logger.log(res['rows'][0]);
  return (res['rows'][0]  == no);
}

function insert(id, no, ans, timestamp){
  if (isWritable(no)) {
    var tableId = '★ここは書き換えてね★';
    var sql = 'INSERT INTO ' + tableId + '(ID, NO, ANS, TIMESTAMP) VALUES (' + id + ',' + no + ',' + '\'' + ans + '\'' + ',\'' + timestamp + '\')';
    var res = FusionTables.Query.sql(sql);
    return 1;
  } else {
    return 0; //時間外のため送信不可
  }
}


書込み制御アプリ(講師用)

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="cache-control" content="no-cache" />
    <meta http-equiv="expires" content="0" />
    <base target="_top">
    <style>
    html {
      font-size: calc(112.5% + 4 * (100vw - 600px) / 400)
    }
    body {
      -webkit-text-size-adjust: 100%; 
    }
    .button {
      font-size: 1.4em;
      font-weight: bold;
      padding: 10px 30px;
      background-color: #248;
      color: #fff;
      border-style: none;
    }
    .radio {
      width: 30px;
      height: 30px;
    }
    </style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=yes" />
  </head>
  <body>
    <form>
      <input type="button" class="button" id="btn1" value="NO" onclick="submitAns(1);">
      <input type="button" class="button" id="btn2" value="NO" onclick="submitAns(2);">
      <input type="button" class="button" id="btn3" value="NO" onclick="submitAns(3);">
      <input type="button" class="button" id="btn4" value="NO" onclick="submitAns(4);">
    </form>
    <div id="info"></div>
    
    <script type="text/javascript">
    function submitAns(no) {
      info.innerHTML = '送信中です...'
      var btn = document.getElementById('btn' + no);
      var vv = (btn.value == 'NO') ? no : 'NO';
      google.script.run
        .withSuccessHandler(function(res) {
          var btn = document.getElementById('btn' + no);
          btn.value = vv;
          var info = document.getElementById('info');
          info.innerHTML = '送信完了しました。'
        })
        .withFailureHandler(function(error) {
          var info = document.getElementById('info');
          info.style.color = 'red';
          info.innerHTML = '再度、送信してください エラー:' + error;
        })
        .setWritable(vv);
    }
    
    google.script.run
      .withFailureHandler(function(error) {
        var info = document.getElementById('info');
        info.innerHTML = error;
      })
      .setWritable('NO');
    </script>    
  </body>
</html>

コード.gs

function doGet() {
  return HtmlService.createHtmlOutputFromFile('index');
}

function getWritable() {
  var tableId = '★ここは書き換えてね★';
  var sql = 'SELECT writable FROM ' + tableId;
  var res = FusionTables.Query.sql(sql);
//  Logger.log(res['rows'][0]);
  return res['rows'][0];
}

function setWritable(value) {
  var tableId = '★ここは書き換えてね★';
  var sql = 'UPDATE ' + tableId + " SET writable=\'" + value + "\' WHERE ROWID=1";
//  Logger.log(sql);
  var res = FusionTables.Query.sql(sql);
}

解答用テーブル
f:id:ke_takahashi:20180501144855p:plain

書込制御用テーブル
f:id:ke_takahashi:20180501144902p:plain