【Python】【Django】お天気予報機能2(xmlから取得したデータをDBに登録)

##はじめに
前回の続き(【Python】【Django】お天気予報機能1(要件整理))
https://qiita.com/oshiro-pooh/items/4c48e3ca7a39e4f137bf

アプリケーション名は直球で「weather」です。

今回は市町村IDを管理しているxmlを取り込み、テーブルとして管理する為の画面(地域情報一括更新画面)をざっくり作りました。

##modelについて
xmlから取得したデータを「都道府県テーブル(親)」、「市町村テーブル(子)」で管理するために以下のようなmodelを作成しました。

```python:models.py
from django.db import models

class Pref(models.Model):
  """
  都道府県テーブル(親)のmodel
  """
  
  # 都道府県名
  name = models.CharField(max_length=255)

  def __str__(self):
    return self.name

class City(models.Model):
  """
  市町村テーブル(子)のmodel
  """
  
  # 都道府県テーブル(親)の主キー
  pref = models.ForeignKey(Pref, on_delete=models.CASCADE)
  # 市町村名
  name = models.CharField(max_length=255)
  # 市町村ID(この値をキーとして市町村ごとの天気を取得します)
  city_id = models.CharField(max_length=6)

  def __str__(self):
    return self.name
```

上記をマイグレーションした結果、以下のようなテーブルができました。

```
mysql> describe weather_pref;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | NO   |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

mysql> describe weather_city;
+---------+--------------+------+-----+---------+----------------+
| Field   | Type         | Null | Key | Default | Extra          |
+---------+--------------+------+-----+---------+----------------+
| id      | int(11)      | NO   | PRI | NULL    | auto_increment |
| name    | varchar(255) | NO   |     | NULL    |                |
| city_id | varchar(6)   | NO   |     | NULL    |                |
| pref_id | int(11)      | NO   | MUL | NULL    |                |
+---------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)
```

いいですね。ちゃんと親子関係ができてます。

##viewについて
updateボタン押下によりテーブルを全書き換えする仕様のため、update関数を定義しました。
(index関数、next関数はここでは触れません。)

```python:views.py
from django.shortcuts import render
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from .models import Pref,City
import urllib.request
import urllib.parse
import xml.etree.ElementTree as ET
from django.contrib import messages

@login_required
def index(request):
  return render(request, 'weather/index.html')

@login_required
def next(request):
  return render(request, 'weather/update.html')

@login_required
def update(request):
  # テーブルを初期化
  prefs = Pref.objects.all()
  prefs.delete();
  citys = City.objects.all()
  citys.delete()
  
  try:
    # livedoor提供のcityIDが定義されたxmlファイルを抽出
    url = 'http://weather.livedoor.com/forecast/rss/primary_area.xml'
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
      xml_string = response.read()
    root = ET.fromstring(xml_string)
  except urllib.error.HTTPError:
    messages.error(request, '元データを取得できませんでした。システム管理者へご連絡下さい。')
    return render(request, 'weather/update.html')

  # タグを抽出し、テーブルに登録する
  for pref in root.iter('pref'):
    pref_entity = Pref(name=pref.attrib['title'])
    pref_entity.save()
    for city in pref.iter('city'):
      ciyte_entity = City(pref_id=pref_entity.id ,name=city.attrib['title'], city_id=city.attrib['id'])
      ciyte_entity.save()
      
  messages.success(request, '更新処理に成功しました。')
  return render(request, 'weather/update.html')
```

xmlの構成は都道府県ごとにprefタグが定義されており、prefタグの子要素としてcityタグが定義されてます。

以下、印象に残った点。

* iter()メソッド(指定されたタグ名をルートからまとめて取り出し)がとにかく便利
    * pythonはxmlの解析処理が直感的に実装できて良いですね。
実装時、参考にさせていただきました。→
https://www.sejuku.net/blog/74013#iter

* 保存済みの親テーブルの主キー(外部キーとして使用)取得の実装に悪戦苦闘
    * もはやview実装の9割の記憶はここにある。saveしたタイミングでテーブルインスタンスにidフィールド(自動追加される主キー)が自動追加されるのですね。。idカラム値の取得の実装方式が全く見えず、2時間弱くらいトライ&エラーを繰り返しました。。

* 完了メッセージの実装が容易
  *messages(django.contrib)を利用することにより意図も簡単にメッセージを画面出力できる。このページを参考にさせていただきました。→https://qiita.com/sapuri/items/9ee0523af2a82f6de753 

一つ納得いっていないのはfor文のネスト。もっとカッコよく効率良い形式で書けそうな。。
これはPythonの基礎を要学習ですね。。

##templateについて
超シンプル。特に苦労してません。
(今回はサンプルアプリと平仄を合わせるためにmilligramフレームワークを利用してます。OSHARE。)

こっちが継承元。

```html:base.html
{% load staticfiles %}
<!DOCTYPE html>
<html>
    <head>
        <title>Weather</title>
        <!-- CSS And JavaScript -->
        <link rel="stylesheet"
            href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
        <link rel="stylesheet"
            href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
        <link rel="stylesheet"
            href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
        <link rel="stylesheet"
            href="{% static 'css/todo.css' %}">
    </head>
    <body>
        <div class="container">
            {% block content %}
            {% endblock %}
        </div>
    </body>
</html>
```

こっちが本体。

```html:update.html
{% extends 'weather/base.html' %}

{% block content %}
<h1>Update</h1>
{% if messages %}
  <ul class="messages">
    {% for message in messages %}
      <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
  </ul>
{% endif %}

<form action="{% url 'weather:update' %}" method="post">
    {% csrf_token %}
    <button>UPDATE</button>
</form>

<form action="{% url 'weather:index' %}" method="post">
    {% csrf_token %}
    <button>◀︎Back</button>
</form>
   
{% endblock %}
```

##URLConfについて
特筆点なし。(プロジェクト側本体のURLConfについては割愛。)

```python:urls.py
from django.urls import path
from . import views

app_name = 'weather'

urlpatterns = [
    path('index', views.index, name='index'),
    path('next', views.next, name='next'),
    path('update', views.update, name='update'), # ◀︎今回の実装に関連する制御文
    path('', views.index, name='index'),
]
```

##完成！！！
こんな感じの画面イメージです。
![スクリーンショット 2019-05-08 23.22.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407364/d9dfce2a-1355-b37c-0a2b-3c72d030f710.png)

UPDATEボタンを押下すると・・
![スクリーンショット 2019-05-08 23.23.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407364/bb6adc35-0d07-5ec1-3ffc-e5295d35d223.png)

テーブルにデータが登録されます!!良い感じ！！
![スクリーンショット 2019-05-08 23.24.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407364/a6a8f70f-a0c0-1836-393b-784e9a96eac0.png)
![スクリーンショット 2019-05-08 23.25.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407364/c43b6144-6786-80ee-0e55-76c36bdb27bf.png)

ちなみに今回は例外処理も実装してみました。

views.pyを以下のように変更。
(参照するxmlのパスを嘘のものに変更。)

```python:views.py
from django.shortcuts import render
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from .models import Pref,City
import urllib.request
import urllib.parse
import xml.etree.ElementTree as ET
from django.contrib import messages

@login_required
def index(request):
  return render(request, 'weather/index.html')

@login_required
def next(request):
  return render(request, 'weather/update.html')

@login_required
def update(request):
  # テーブルを初期化
  prefs = Pref.objects.all()
  prefs.delete();
  citys = City.objects.all()
  citys.delete()
  
  try:
    # livedoor提供のcityIDが定義されたxmlファイルを抽出
    url = 'http://weather.livedoor.com/test/primary_area.xml' # ここを変更。
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as response:
      xml_string = response.read()
    root = ET.fromstring(xml_string)
  except urllib.error.HTTPError:
    messages.error(request, '元データを取得できませんでした。システム管理者へご連絡下さい。')
    return render(request, 'weather/update.html')

  # タグを抽出し、テーブルに登録する
  for pref in root.iter('pref'):
    pref_entity = Pref(name=pref.attrib['title'])
    pref_entity.save()
    for city in pref.iter('city'):
      ciyte_entity = City(pref_id=pref_entity.id ,name=city.attrib['title'], city_id=city.attrib['id'])
      ciyte_entity.save()
      
  messages.success(request, '更新処理に成功しました。')
  return render(request, 'weather/update.html')
```

この状態でupdateボタンを押下するとエラーメッセージが表示される。
(エラーの場合、フォントを変更したいのですが、それはまたおいおい。)
![スクリーンショット 2019-05-08 23.34.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407364/04e1fe53-97da-1ed5-ff79-1960e14c24b5.png)

##NEXT
データ取り込みの画面が完成したため、次回は肝心のお天気検索画面の実装に入ります。
プルダウンの変更処理をAjaxで表現するか悩ましい。。