Djangoのバッチをつくってcronで日次実行する

## はじめに
`vietnam_research` には、バッチ処理があった。

https://qiita.com/YoshitakaOkada/items/d1e14776040e64cd1434

最近このアプリケーションをナマSQLベースからQuerySetベースに変えるリファクタリングを行っていた。そしたらこんなエラーが出るようになった。
```text:こんなエラー
RuntimeError: Model class mysite.vietnam_research.models.Market 
doesn't declare an explicit app_label and isn't in an application 
in INSTALLED_APPS.

訳：
RuntimeError: モデルクラス mysite.vietnam_research.models.Market
は、明示的な app_label を宣言しておらず、INSTALLED_APPS の
アプリケーションに含まれていません。
```

しばらく原因がわからなかったんだけど、どうやら「Modelを使うならDjangoドメイン（＝ディレクトリの中）でつくれよ」っていう話らしい。そうかー、生SQLだから気が付かなかったのか

## 参考
https://docs.djangoproject.com/en/4.2/howto/custom-management-commands/

## 手順
### バッチファイル新規作成
`vietnam_research` アプリケーション配下に `management/commands/daily_chart.py` ファイルを新規追加します（※daily_chartはもちろん任意の名前で）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/989ad1dd-825c-fa26-2309-cbf65e83d74f.png)

```python:mysite/vietnam_research/management/commands/daily_chart.py
from django.core.management.base import BaseCommand, CommandError
from vietnam_research.models import Industry


class Command(BaseCommand):
    help = 'plot industry stacked bar_chart'

    def add_arguments(self, parser):
        parser.add_argument('industry_ids', nargs='+', type=int)

    def handle(self, *args, **options):
        for industry_id in options['industry_ids']:
            try:
                industry = Industry.objects.get(pk=industry_id)
            except Industry.DoesNotExist:
                raise CommandError('Industry "%s" does not exist' % industry_id)

            industry.open_price = 99.9
            industry.save()

            self.stdout.write(self.style.SUCCESS('Successfully updated industry "%s"' % industry_id))
```

## 確認
- `plot industry stacked bar_chart` という文字が確認できたということは `daily_chart` がバッチコマンドとして認識されたということだ

### バッチコマンドが認識されたことを確認
```console:console
dev\portfolio\mysite> python manage.py daily_chart -h
usage: manage.py daily_chart [-h] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks] industry_ids [industry_ids ...]

plot industry stacked bar_chart

positional arguments:
                        A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".
  --traceback           Raise on CommandError exceptions.
  --no-color            Don't colorize the command output.
  --force-color         Force colorization of the command output.
  --skip-checks         Skip system checks.
```

### 更新前の確認
```python:console
dev\portfolio> python manage.py shell
```
```python:shell
# Industry テーブルの1レコードめの open_price の確認
In [1]: from vietnam_research.models import Industry
In [2]: query = Industry.objects.get(pk=1)
In [3]: query.open_price
Out[3]: Decimal('12.40')
In [4]: exit
```

### バッチ実行
- スペース区切りにすると list で送ったことになるらしい（loop処理される）
```console:console
dev\portfolio\mysite> python manage.py daily_chart 1 2
Successfully updated industry "1"
Successfully updated industry "2"
```

### 更新前の確認
```python:console
dev\portfolio> python manage.py shell
```
```python:shell
# Industry テーブルの1レコードめの open_price の確認
In [1]: from vietnam_research.models import Industry
In [2]: query = Industry.objects.get(pk=1)
In [3]: query.open_price
Out[3]: Decimal('99.90')
In [4]: exit
```

## cronでセット
https://zerofromlight.com/blogs/detail/37/#crontab

```console:console(ubuntu)
# cp /etc/crontab /etc/cron.d/cron_for_portfolio
# vi /etc/cron.d/cron_for_portfolio
```
```vim:/home/ubuntu/etc/cron.d/cron_for_portfolio
51  *  *  *  * /var/www/html/venv/bin/python /var/www/html/portfolio/mysite/manage.py daily_chart
```
```console:console(ubuntu)
# service cron restart
```