簡易なアンケートシステムを簡単に設置する話

たまに調査票形式のフォームをつくってアンケートをやりたいという話があるので、汎用的に使えるものを作っておいて楽しようと思った。

(執筆中)

フォーム作成

ここで使うCGIのために、専用の仕様を満たすフォームを書く必要がある。これを自動で作るスクリプトを書いたので、設定スクリプトを読み込ませて簡単にフォームの原型を作ってしまって、あとで適当に飾り付けを足すのが早い。

form_maker.py
# -*- coding:cp932 -*-
 
# チェック用のjavascriptをつくるか選択
config_need_client_check = True
 
# hiddenタグ書きやすくする
def hidden_tag(name, value):
  return '<input type="hidden" name="%s" value="%s">' % (name, value)
 
# アンケートフォームをつくるクラス
# 特定の仕様のCGIに最適化したやつだけど
class FormRenderer(object):
 
  # いろいろ初期設定
  def __init__(self, id="no_name", action="", after="thankyou.html"):
    self.id = id
    self.action = action
    self.after = after
 
  # テキストボックスつくるよ(size - maxlength)
  def render_textbox(self, elem, lines):
    (size, limit) = lines[0][3:].split("-")
    return '<input type="text" name="%s" size="%s" maxlength="%s">' % (elem, size, limit)
 
  # テキストエリアつくるよ(cols x rows)
  def render_textarea(self, elem, lines):
    (cols, rows) = lines[0][3:].split("x")
    return '<textarea name="%s" cols="%s" rows="%s"></textarea>' % (elem, cols, rows)
 
  # チェックボックスつくるよ
  def render_checkbox(self, elem, lines):
    value = lines[0][3:]
    return '<input type="checkbox" name="%s" value="%s">' % (elem, value)
 
  # リストボックスつくるよ
  def render_listbox(self, elem, lines):
    t = ['<select name="%s">' % elem]
    for i in lines:
      if i.startswith(" - "):
        (opt, value) = i[3:].split(":")
        if opt.startswith("*"):
          selected = "selected"
          opt = opt[1:]
        else:
          selected = ""
        if value == "-": value = opt
        t.append('<option value="%s" %s>%s</option>' % (value, selected, opt))
    t.append('</select>')
    return "\n".join(t)
 
  # ラジオボタン群つくるよ
  def render_radio_buttons(self, elem, lines):
    t = []
    ord = 0
    for i in lines:
      if i.startswith(" - "):
        ord = ord + 1
        (opt, value) = i[3:].split(":")
        if opt.startswith("*"):
          checked = "checked"
          opt = opt[1:]
        else:
          checked = ""
        order = "%s_%d" % (elem, ord)
        if value == "-": value = opt
        t.append(
           '<input type="radio" name="%s" id="%s" value="%s" %s><label for="%s">%s</label>' %
           (elem, order, value, checked, order, opt))
    return "\n".join(t)
 
  # サブミットボタンつくるよ
  def render_submit_button(self, elem, lines):
    label = lines[0][3:]
    return '<input type="submit" value="%s">' % label
 
  # エレメント登録(各種フォーム部品の総合窓口)
  def register_from_markup(self, lines):
    g = lines.pop(0)
    (kind, elem, short_q, long_qw) = g.split(":")
    if kind.startswith("*"):
      mand = 1
      kind = kind[1:]
    else:
      mand = 0
    if self.renderer_table.has_key(kind):
      if kind != "submit":
        self.questions.append((elem, short_q, kind))
      if mand:
        self.mand_list.append((elem, short_q, kind))
      self.form_elements.append((long_qw, self.renderer_table[kind](elem, lines)))
 
 
  # 複数項目の記述をいっぺんに処理するのはここに専用テキストを投げる
  def parse_markup(self, text):
    self.questions = []
    self.mand_list = []
    self.form_elements = []
    self.renderer_table = {
      "text": self.render_textbox,
      "textarea": self.render_textarea,
      "check": self.render_checkbox,
      "list": self.render_listbox,
      "radio": self.render_radio_buttons,
      "submit": self.render_submit_button,
    }
    buf = []
    for line in text.split("\n"):
      if not line:
        if buf:
          self.register_from_markup(buf)
          buf = []
          continue
      else:
        buf.append(line)
    if buf:
      self.register_from_markup(buf)
 
  # 提出前の必須項目チェックのためのJavascript
  def checker_javascript(self):
    b = []
    b.append("""function radio_checked(obj) {
var i;
for (i=0; i<obj.length; i++) { if (obj[i].checked) { return true; } }
return false;
} """)
    b.append("function check_form() { var mes = ''; ")
    for i in self.mand_list:
      if i[2] == 'radio':
        condition = "! radio_checked(document.enq.%s)" % i[0]
      else:
        condition = "document.enq.%s.value == ''" % i[0]
      b.append("if (%s) { mes = mes + '%s を入力してください\\n'; }" % (condition, i[1]))
    b.append("if (mes != '') { alert(mes); return false; } ")
    b.append("return true; }")
    return "\n".join(b)
 
  # HTMLヘッダ描写。必要に応じてjavascriptも
  def html_header_and_maybe_script(self):
    b = []
    b.append("""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title></title>
<META http-equiv="Content-Type" content="text/html; charset=Shift-JIS">
<style>
</style>
<script>""")
    if config_need_client_check:
        b.append(self.checker_javascript())
    b.append("</script></head><body>")
    return "\n".join(b)
 
  # フォームの前半のhidden項目など
  def render_form_header(self):
    b = []
    if config_need_client_check:
      extra_handler = 'onSubmit="return check_form()"'
    else:
      extra_handler = ''
    b.append('<form name="enq" action="%s" method="post" %s>' % (self.action, extra_handler))
    b.append(hidden_tag('id', self.id))
    b.append(hidden_tag('after', self.after))
    b.append(hidden_tag('mode', 'preview'))
    q_st = "|".join([i[0] for i in self.questions])
    b.append(hidden_tag('questions', q_st))
    for i in self.questions:
      b.append(hidden_tag("%s_title" % i[0], i[1]))
    return "\n".join(b)
 
  # フォームの描写。ここのフォーマットをいじって、table型や
  # ただの改行型などに表示を調整できる
  def render_form_body(self):
    b = []
    #for i in self.form_elements:
    #  b.append('<b>%s</b><br>\n%s<br>' % (i[0], i[1]))
    b.append('<table>')
    for i in self.form_elements:
      b.append('<tr><th>%s</th><td>%s</td></tr>' % (i[0], i[1]))
    b.append('</table>')
    return "\n".join(b)
 
  # フォームやjavascriptなどを含んだ全HTMLの描写
  def output_form(self):
    outs = open("%s.html" % self.id, "w")
    outs.write(self.html_header_and_maybe_script())
    outs.write(self.render_form_header())
    outs.write(self.render_form_body())
    outs.write("</form></body></html>")
    outs.close()
 
#
# こんな風に実行。
# フォームidと、cgiの場所(action)と、回答後の表示URLと、フォーム定義ファイルの場所。
#
r = FormRenderer(id="testform", action="../cgi-bin/enq3.cgi")
r.parse_markup(open("form_def1.txt").read())
r.output_form()

CGI設置

CGIスクリプトの設定と設置

enq3.cgi
#!/path/to/python
 
import sys
sys.path.append('/lib/to/system/library')
#import cgitb; cgitb.enable()  # for debug
 
import enquete_main

システム用ライブラリの設定と設置

enquete_main.py
# -*- coding:cp932 -*-
 
# ウェブ調査票のようなものを作るための基本CGIスクリプト
# 2010.2.16 / 2010.3.3 Yamamoto.T
#
# このスクリプトに対応したフォームの作成仕様:
#  <form>要素のmethodはどちらでもよいが、普通はPOST
#              actionは、このスクリプトをインポートするcgiスクリプトに。cgiの名前は何でもよい
#
#    hidden要素として、id, quetions, *_title, mode, after を入れる。
#
#           id は、アンケートを区別するためのID。英数字で適当に。
#              これにあわせて form_config の内容も適宜編集すること。
#
#           questionsは、回答として回収する要素名をすべて | でつなげて表記しておく。
#              例:q1|q2|q3
#
#           *_titleは、上のすべての回答要素の略名を設定するのに使う。
#              例:q1_title = 名前
#                  q2_title = 職名 ...
#
#           modeは、preview と決め打ち。
#
#           afterは、入力完了後に表示させるページ。相対or絶対URL
#
#    実際の入力要素として、上のquestionsの各要素名を使って任意のタイプのフォームを書く。
#           textタイプ、checkboxタイプ、radioタイプ、textareaタイプ、等々
#
# 調査結果のcsvをExcelで開く人って多いので、今回はShiftJISバージョン
#
 
import sys
import cgi
 
# データ格納場所。絶対or相対パス
store_path = "../data"
 
# CGIからのパラメータ取得
f_store = cgi.FieldStorage()
def _fm(key):
    return  f_store.getfirst(key, '')
form_id = _fm('id')
 
# 稼動中のアンケートにあわせて適宜修正のこと
#   'アンケートID': 'パスワード'
#   パスワードのハッシュ化をしようと思ったが、スクリプト見られる時点で
#   データファイルに接触されているのと同然だし、それ以上抵抗しないことにした。
#   そのかわり、このスクリプトはインポート経由でのみ実行して、Webから直接アクセスさせない。
password = {
    'test': 'password',
    'test1': 'password',
    'test2': 'password',
}.get(form_id, 'defaultpassword')
 
def header_and_title(title):
    return "Content-Type: text/html" + "\x0d\x0a\x0d\x0a" + """\
<html><head>
<meta http-equiv="Content-Type" content="text/html charset=Shift_JIS">
<title>%s</title>
</head><body>""" % title
 
# エラー表示(具体的なメッセージを出さないのは手抜き…)
def print_error():
    print header_and_title('ERROR') + "ERROR!</body></html>"
 
# 入力内容テスト表示(mode=previewのかわりにmode=testにしてみると、ここに)
def test_form():
    print header_and_title('TEST')
    #cgi.print_environ_usage()
    #cgi.print_environ()
    for i in f_store.keys():
      print "<dl><dt>%s</dt><dd>%s</dd></dl>" % (i, _fm(i))
 
# 入力内容確認画面
def form_preview():
    print header_and_title('入力確認')
    print """\
<form action="" method=post>
<input type=hidden name="mode" value="confirm">
<input type=hidden name="id" value="%s">
<p>この入力でよいですか?OKなら確定ボタンを押してください。<br>
修正が必要なら、ブラウザの[戻る]ボタンなどで前画面に戻ってください。</p>
""" % form_id
    qs = _fm('questions').split("|")
    after = _fm('after')
    print "<table>"
    for q in qs:
        print "<tr><td align='right'><b>%s</b></td>" % _fm('%s_title' % q)
        print "<td>&nbsp;%s</td></tr>" % cgi.escape(_fm(q), True)
    print "</table>"
    for q in qs:
        print "<input type='hidden' name='%s' value='%s'>" % (q, cgi.escape(_fm(q), True))
    print "<input type='hidden' name='questions' value='%s'>" % _fm('questions')
    print "<input type='hidden' name='after' value='%s'>" % after
    print """\
<input type="submit" value="確定">
</form></body></html>
"""
 
# 入力確定
def store_data():
    from datetime import datetime
    t = [datetime.now().strftime('%Y/%m/%d %H:%M:%S')]
    for q in _fm('questions').split("|"):
        t.append('"%s"' % cgi.escape(_fm(q), True).replace("\n", " ").replace("\r", " "))
    o = open("%s/%s.csv" % (store_path, form_id), "a")
    o.write(",".join(t) + "\n")
    o.close()
    print "Location: %s\n" % _fm('after')
 
# 回答データのダウンロード
def download_data():
    if (password == _fm('pass')):
        # データストアから結果の蓄積を読んで、そのままクライアントに渡す
        print "Content-Type: application/octet-stream"
        print "Content-Disposition: attachment filename=data_%s.csv" % form_id
        print
        for line in open("%s/%s.csv" % (store_path, form_id)):
            sys.stdout.write(line)
    else:
        # パスワード未入力、または不一致
        print header_and_title('認証')
        print """\
<form action="" method="post">
<input type="hidden" name="mode" value="download">
<input type="hidden" name="id" value="%s">
PASSWORD:<input type="password" name="pass">
<input type="submit">
</form></body></html>
""" % form_id
 
{'test': test_form,
 'preview': form_preview,
 'confirm': store_data,
 'download': download_data,
}.get(_fm('mode'), print_error)()

データ格納ディレクトリの準備

 
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki