FlaskとHTMLでシンプルな小学生向け算数クイズアプリを作成する方法

## はじめに
この記事では、FlaskとHTMLを使用してシンプルなクイズアプリを作成する方法を紹介します。このアプリケーションでは、サーバーサイドでFlaskを利用して問題を生成し、クライアントサイドではHTMLとJavaScriptを使って問題を表示し、ユーザーからの回答を受け付けます。クイズの問題は算数の基本的な計算問題をランダムに生成し、ユーザーが選択肢をクリックして回答できるインターフェースを提供します。さらに、ユーザーがクイズを提出すると、正答率を計算して結果を表示します。

このアプリを作成することで、Flaskの基本的な使い方や、HTML、CSS、JavaScriptの連携方法を学ぶことができます。以下に、ディレクトリ構成やプログラムの詳細な説明を示しますので、ぜひ参考にしてみてください。


### ディレクトリ構成

まず、アプリケーションのディレクトリ構成は以下のようになります。

```
quiz_app/
├── app.py
├── static/
│   └── style.css
└── templates/
    └── index.html
```

- `app.py`: Flaskアプリケーションのメインファイル
- `static/style.css`: アプリケーションのスタイルを定義するCSSファイル
- `templates/index.html`: アプリケーションのHTMLテンプレートファイル

### プログラムの説明

#### `app.py`

Flaskを使ってサーバーサイドのロジックを処理します。

```python
from flask import Flask, render_template, request, jsonify, session
import random

app = Flask(__name__)
app.secret_key = 'your_secret_key'  # セッションを使用するために必要

def generate_math_question():
    operators = ['+', '-', '*']
    num1 = random.randint(1, 10)
    num2 = random.randint(1, 10)
    operator = random.choice(operators)
    question = f"{num1} {operator} {num2}"
    answer = eval(question)
    
    # 正解を含む4つの選択肢を生成
    options = [answer,
               random.randint(answer - 10, answer + 10),
               random.randint(answer - 10, answer + 10),
               random.randint(answer - 10, answer + 10)]
    random.shuffle(options)
    
    return {
        "question": f"What is {question}?",
        "options": options,
        "answer": answer
    }

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/get_question', methods=['POST'])
def get_question():
    quiz = [generate_math_question() for _ in range(10)]  # 10問ランダムに生成
    session['questions'] = quiz  # セッションに質問を保存
    return jsonify(quiz)

@app.route('/submit_answers', methods=['POST'])
def submit_answers():
    user_answers = request.json.get('answers', [])
    questions = session.get('questions', [])
    
    if not questions:
        return jsonify({'error': 'No questions found in session'}), 400
    
    score = 0
    for user_answer, question in zip(user_answers, questions):
        if user_answer is not None and int(user_answer) == question['answer']:
            score += 1

    percentage = (score / len(questions)) * 100
    return jsonify({'score': percentage})

if __name__ == '__main__':
    app.run(debug=True)
```

- `generate_math_question()`: ランダムな数学の問題を生成し、正解と選択肢を含む辞書を返します。
- `/` (ルート): `index.html`テンプレートをレンダリングして返します。
- `/get_question`: クイズの10問を生成し、セッションに保存してJSON形式で返します。
- `/submit_answers`: ユーザーが送信した回答をセッションに保存された質問と比較し、正解率を計算して返します。

#### `templates/index.html`

HTMLファイルで、フロントエンドのUIを提供します。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Quiz App</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <h1>Quiz App</h1>
    <div id="quiz-container"></div>
    <button onclick="submitQuiz()">Submit Quiz</button>
    <div id="result"></div>

    <script>
        let questions = [];
        let userAnswers = [];

        window.onload = async function() {
            const response = await fetch('/get_question', { method: 'POST' });
            questions = await response.json();
            displayQuiz();
        };

        function displayQuiz() {
            const container = document.getElementById('quiz-container');
            container.innerHTML = '';
            questions.forEach((q, index) => {
                const questionDiv = document.createElement('div');
                questionDiv.classList.add('question');
                questionDiv.innerHTML = `<p>${q.question}</p>`;
                q.options.forEach(option => {
                    const optionButton = document.createElement('button');
                    optionButton.textContent = option;
                    optionButton.classList.add('option-button');
                    optionButton.onclick = () => selectAnswer(index, option, optionButton);
                    questionDiv.appendChild(optionButton);
                });
                container.appendChild(questionDiv);
            });
        }

        function selectAnswer(questionIndex, answer, button) {
            userAnswers[questionIndex] = answer;

            // 選択されたボタンのスタイルを変更
            const questionDiv = button.parentElement;
            const optionButtons = questionDiv.getElementsByClassName('option-button');
            for (let btn of optionButtons) {
                btn.classList.remove('selected');
            }
            button.classList.add('selected');
        }

        async function submitQuiz() {
            const response = await fetch('/submit_answers', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ answers: userAnswers })
            });
            const result = await response.json();
            if (result.error) {
                document.getElementById('result').textContent = result.error;
            } else {
                document.getElementById('result').textContent = `Your score: ${result.score}%`;
            }
        }
    </script>
</body>
</html>
```

- `window.onload`: ページが読み込まれたときにサーバーから問題を取得し、表示します。
- `displayQuiz()`: 取得した問題をHTMLに表示します。
- `selectAnswer()`: ユーザーが選択肢をクリックしたときの処理を行い、選択された回答を記録します。
- `submitQuiz()`: ユーザーの回答をサーバーに送信し、結果を表示します。

#### `static/style.css`

アプリケーションのスタイルを定義します。

```css
body {
    font-family: Arial, sans-serif;
    background-color: #f0f0f0;
    margin: 0;
    padding: 0;
}

h1 {
    text-align: center;
    margin-top: 20px;
}

#quiz-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 20px;
}

.question {
    margin-bottom: 20px;
}

.option-button {
    display: block;
    margin: 5px 0;
    padding: 10px;
    background-color: #ffffff;
    border: 1px solid #cccccc;
    border-radius: 5px;
    cursor: pointer;
}

.option-button.selected {
    background-color: #d3ffd3;
}

button {
    display: block;
    margin: 20px auto;
    padding: 10px 20px;
    font-size: 16px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

button:hover {
    background-color: #45a049;
}

#result {
    text-align: center;
    font-size: 18px;
    font-weight: bold;
}
```

- スタイルの設定で、ボタンのデザインやページ全体の見た目を整えています。

### 実行

以下のコマンドで Flask アプリケーションを実行します。

```bash
python app.py
```

ブラウザで `http://127.0.0.1:5000/` にアクセスすると以下の画面が出ます

![screencapture-localhost-5000-2024-08-21-16_23_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75802/4dc82b38-2b27-30e5-a69e-56b77f3f5095.png)



## おわりに
この記事では、FlaskとHTMLを使ったシンプルなクイズアプリの作成方法を説明しました。サーバーサイドで問題を生成し、クライアントサイドでユーザーインターフェースを構築することで、Webアプリケーションの基本的なフローを学ぶことができたかと思います。

このクイズアプリをもとに、さらに複雑な機能を追加することもできます。例えば、ユーザー認証機能や問題の難易度選択、ユーザーごとの成績管理などの機能を追加して、自分だけのオリジナルアプリを作成してみてください。

質問やコメントがあれば、お気軽にお知らせください。最後までお読みいただき、ありがとうございました。