講義の出席は、紙を回して名前を書かせて確認しています。しかし50人を超える受講者の場合、いちいち講義毎に紙から名前を拾って出席をカウントするのが面倒です。同じような筆跡もよく見かけますし。最近では、スマホを持っていない学生もほとんど見かけないので、web上で出席をとってみることにしました。具体的にはQRコードをスクリーンに映してURLを配布し、学生のスマホで名前と学生証番号を登録させます。
環境変数を記録しといて1つの端末で複数登録(代返)していないか、後で確認することとします。QRコードをばらまかれたら、自宅にいても出席登録できてしまいます。教室で登録しているか確認するため、スマホのGPS情報も一緒に記録しておいて確認します。ブラウザに位置情報へのアクセスを求められるので、許可してもらう必要があります。正直、これでも抜け穴はありますが。
このコードに出席登録できる日時と時間の制限、登録後の確認メールの送信、二重登録防止の機能を加えて、4月から3つの講義で実際に使ってみたいと思います。
apache2.4+python3.7で動作確認しています。また、保存する*.logは、外部からアクセスできないよう.htaccessでアクセス制限しておく必要があります。

jscode.js
緯度と経度を取得するjavascript。Pythonで今いる位置情報やユーザーエージェントなんかを取得を参考にしました。入力した学生証の確認、位置情報取得の可否をチェックしています。
// 現在の位置情報取得を実施
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
// 位置情報取得成功時
function (pos) {
var location = pos.coords.latitude;
location += "," + pos.coords.longitude;
document.getElementById("location").value = location;
},
// 位置情報取得失敗時
function (pos) {
var location ="位置情報が取得できません。<br>";
location = location + "ブラウザを再起動してやり直してみる。<br>";
document.getElementById("infotext").innerHTML = location;
var submit = document.getElementById("submit");
submit.disabled = "true"
});
}
else {
window.alert("このブラウザはGeolocationに対応していません。");
}
// inputのチェック
function check(){
if(document.form.name.value == ""){
window.alert('名前が入力されていません');
return false;
}
if(document.form.name.value == ""){
window.alert('位置情報が取得できていません');
return false;
}
if(! document.form.gakuseki.value.match(/^[1-3][0-9][a-zA-Z]{1,2}\d{4}[a-zA-Z]{0,1}$/)){
window.alert('学生証番号が正しくありません');
return false;
}
return true;
}
check.cgi
出欠登録のためにアクセスさせるページを生成する。名前と学生証番号を入力させ、session-IDを付与してsubmitするとaction.cgiに移動する。
import secrets
INPUT_HTML = """Content-Type: text/html
Set-Cookie: session={session}
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link href="style.css" rel="stylesheet" type="text/css">
<title>出欠確認</title>
</head>
<body>
<script src="jscode.js" charset="utf-8"></script>
<form method="POST" action="./action.cgi" name="form" onSubmit="return check()">
<h1>出席確認</h1>
<h2>名前</h2>
<p>フルネームを記入する。</p>
<p><input type="text" name="name" value=""></p>
<h2>学生証番号</h2>
<p>大文字小文字は区別しません。例: 10t0123x</p>
<p><input type="text" name="gakuseki" value="" style="ime-mode:disabled;"></p>
<p id="infotext"></p>
<input type="hidden" name="geo" id="location" value="">
<input type="hidden" name="session" value="{session}">
<br>
<p>フォームに入力したら送信ボタンを押す。</p>
<p style="text-align: center">
<input type="submit" value="送信" id="submit">
</submit>
</form>
</body>
</html>
"""
# HTMLの生成
token = secrets.token_hex(6)
print(INPUT_HTML.format(session=token))
action.cgi
cookieからsession-IDを拾ってページ遷移を確認します。位置情報や入力データ、環境変数一式をlogs/22:07:20:408746_10T0000T.logのようなファイル名で保存します。1登録につき1ファイルで保存します。logs/は自前で作成してchmod 777しときます。
import os
from datetime import datetime
from http import cookies
import cgi
BODY = """Content-Type: text/html\n
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link href="style.css" rel="stylesheet" type="text/css">
</head>
<body>
<p>{}</p>
</body>
</html>
"""
def CheckSession(dic):
form = cgi.FieldStorage()
form_keys = form.keys()
cookie = cookies.SimpleCookie(os.environ.get('HTTP_COOKIE', None))
if ("name" in form_keys) is False or\
("geo" in form_keys) is False or\
("gakuseki" in form_keys) is False or\
("session" in form_keys) is False or\
cookie is None or\
("session" in cookie.keys()) is False:
print(BODY.format("不正遷移"))
exit()
dic["name"] = form.getvalue("name", "")
dic["geo"] = form.getvalue("geo", "")
dic["id"] = form.getvalue("gakuseki", "").upper()
dic["session"] = form.getvalue("session", "")
dic["session_cookie"] = cookie["session"].value
if dic["session_cookie"] != dic["session"]:
print(BODY.format("不正遷移"))
exit()
def WriteLog(now, dic, body):
time = now.strftime("%Y-%m-%d_%H:%M:%S:%f")
outfile = "logs/{}_{}.log".format(time, dic["id"])
o = open(outfile, "w")
for k, v in dic.items():
o.write("{}: {}\n".format(k, v))
for k, v in os.environ.items():
o.write("{}: {}\n".format(k, v))
o.close()
body = "出席登録しました。\n" + body
print(BODY.format(body.replace("\n", "<br>\n")))
now = datetime.now()
dic = {}
CheckSession(dic)
body = ""
body += "氏名: {}\n".format(dic["name"])
body += "学生証番号: {}\n".format(dic["id"])
gmap = "https://www.google.com/maps?q={}".format(dic["geo"])
body += "位置情報: <a href={}>{}</a>\n".format(gmap, dic["geo"])
WriteLog(now, dic, body)
