Anvil の CRUD チュートリアルを改造した時のメモ

# 概要

[Anvil](https://anvil.works/) は Python でコーディングする、Web ベースの統合開発環境です。
1個のプロジェクトが以下のような構成になっています。

- Anvil プロジェクト
    - Client Code (UI のデザインとコード)
    - Server Code (サーバサイドのコード)
    - Services (連携するサービス (DB など))

この構成を理解するには、DB の基本操作 (CRUD) を実装するチュートリアル [News Aggregator Tutorial](https://anvil.works/learn/tutorials/database-backed-apps) を学ぶのが手っ取り早いです。

このチュートリアルのプロジェクトを少し改造して、クイズ集アプリのようにしましたので、その時のメモをご紹介します。

# 前提

[News Aggregator Tutorial](https://anvil.works/learn/tutorials/database-backed-apps) をやった前提で説明します (プロジェクトを作るには Anvil へのサインアップが必要です)。
News Aggregator Tutorial ではニュースデータを扱いますが、この構造をそのまま使ってクイズ集アプリとしました。

# 変更点

## リネーム

扱うデータが (構造は同じですが) ニュースからクイズに変わりますので、分かりやすくリネームしました。

- プロジェクト名
    - 何でも良いですが、'Quiz' としました。
- フォーム名
    - 'ArticleEdit' から 'QuestionEdit' に変更
    - 'ArticleView' から 'QuestionView' に変更

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/79cf1bb8-ac39-4ceb-6497-21513536c2d3.png" width="400">

- データテーブル名
    - 'articles' から 'questions' に変更
- カラム名 (および関連するデータバインド)
    - 'title' を 'question' に変更
    - 'content' を 'answer' に変更

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/1adf2134-7121-aaa1-e182-6600cfd2cef9.png" width="400">

- 他にもフォームのコンポーネントやイベントハンドラ名などを以下のように変更
    - 'articles' → 'questions'
    - 'title' → 'question'
    - 'content' → 'answer'

身も蓋もないことを言うと、あとは入力するデータの内容を「質問」と「答え」にしさえすれば、クイズ集アプリになります。
とはいえ、気になるところもあるので、もう少し改造してみました。

## QuestionView

QuestionView フォームに、以下の変更をしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/90fd0e66-1f92-5cee-e8ed-c05ee3a53591.png" width="400">

### 「問題」「答え」ラベルを追加

「タイトル」と「記事本文」を「問題」と「答え」として使うので、それぞれ「問題」と「答え」であることを示した方が良いと思い、ラベルを追加しました。

やり方は Toolbox から Label をドラッグ & ドロップするだけです。
Column Panel や Flow Panel を使ってレイアウトの調整もします (なかなか思い通りに行きませんが…)。

### 答えの表示/非表示を切り替える

クイズなのに答えが丸見えだったので、答えの表示/非表示を切り替えるようにしました。

Toolbox から CheckBox をドラッグ & ドロップし、名前を `answer_check` としました。
初期状態はチェックがはずれた状態とし、`change` イベントを追加しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/f4a8f767-0709-c9f1-3a7e-dc2da26f1786.png" width="400">

イベントハンドラを以下のように書いて、クリックするたびに答えの表示/非表示が切り替わるようにしました。

```python:QuestionView
  def answer_check_change(self, **event_args):
    """This method is called when this checkbox is checked or unchecked"""
    self.answer_label.visible = self.answer_check.checked
```

答えの方も初期状態を非表示にしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/b5dad567-ff31-1343-2f59-c524f673ccdd.png" width="400">

## カテゴリの初期値

チュートリアルをなぞっただけでは、新規レコード作成時に何もせずに 'SAVE' を押すとエラーになります。
これは、カテゴリが外部参照なのに値が入っていないからです。

None を許容する方法や、フォームを閉じる前にバリデーションする方法が分からなかったので、初期値を与えることで (強引に) 解決しました。
初期値を与えるには、問題を作成するボタンのイベントハンドラ (`Homepage` フォーム) を以下のようにしました。

**変更前**

```python:Homepage
  def add_question_button_click(self, **event_args):
    """This method is called when the button is clicked"""
    new_question = {}
    
    save_clicked = alert(
      ...
```

**変更後**

```python:Homepage
  def add_question_button_click(self, **event_args):
    """This method is called when the button is clicked"""
    default_category = anvil.server.call('default_category')  # 追加
    new_question = {'category': default_category}             # 変更
    
    save_clicked = alert(
      ...
```

サーバ側のコードにカテゴリの初期値を取得する関数を追加 ('Python' というカテゴリが存在する場合)

```python:ServerModule1
@anvil.server.callable
def default_category():
  return app_tables.categories.get(name='Python')
```

## シャッフルボタン

できれば、問題が1個出て答えると次の問題が出る、というフローにしたいところですが、改造の域を超えそうなので、シャッフルボタンをつけてお茶を濁しました。

仕様としては、以下のようにしました。

- シャッフルボタンを押すとカードの並びがランダムになる。
    - 同時にシャッフル解除ボタンが有効になる。
- シャッフル解除ボタンを押すと、DB から取得したままの順になる。
    - 同時にシャッフル解除ボタンが無効になる。
- 編集をした時などは、DB から取得した順になる。
    - シャッフル解除ボタンは無効になる。

### ボタンを追加

シャッフルボタンとシャッフル解除ボタンを追加して、それぞれ `shuffle_button`、`unshuffle_button` としました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/c866f9dc-90fc-1f89-48f4-688727e190f2.png" width="400">

### コードの追加・変更

シャッフルボタンのクリックハンドラ

```python:Homepage
  def shuffle_button_click(self, **event_args):
    questions = self.questions_panel.items
    self.questions_panel.items = random.sample(questions, len(questions))
    self.unshuffle_button.enabled = True
```

シャッフル解除ボタンのクリックハンドラ

```python:Homepage
  def unshuffle_button_click(self, **event_args):
    self.refresh_questions()
```

`refresh_questions()` メソッド (元々 refresh_articles() だったもの) も変更

```python:Homepage
  def refresh_questions(self):
    self.questions_panel.items = anvil.server.call('get_questions')
    self.unshuffle_button.enabled = False  # 追加
```

# デモ

以上の変更によりできたクイズ集アプリを、以下の URL で公開しています。
https://Z7NSR2ZWKFYP6CJK.anvil.app/OGKX5IPZ2SLK3TKVBPVJLZGH

# 所感

まだなかなか、思ったことがすぐ出来る (すぐやり方が見つかる) という感じではないです。
Anvil の UI を思い通りにデザインしようとすると道は険しそうです。

UI を極めるよりは、次は Anvil Uplink という仕組みを試してみたいです。
Anvil Uplink は、ローカルで動いているモデル等を Web アプリから (API サーバみたいな感じに？) 使える仕組みのようです。

- [Uplink: Code outside Anvil](https://anvil.works/docs/uplink)
- [Turning a Jupyter notebook into a web app](https://anvil.works/learn/tutorials/jupyter-notebook-to-web-app)