Django × Reactでハイブリッド株式管理アプリを構築する【更新/削除編】

前回株式情報の登録処理を作成したので、本記事では最新の株式情報への更新と登録済みの株式情報の削除処理を作成します。
前回と同じく更新処理にスクレイピングを行っていますが、一部コードを省略して実装します。
スクレイピング処理は登録処理で使用した内容と同じもの(戻り値)を使用します。


## URLパターン設定
プロジェクトの urls.py は変更せずにequity_hub.urlsへ飛ぶようにしておきます。
```py:config/urls.py
from django.contrib import admin
from django.urls import path
from django.urls.conf import include

urlpatterns = [
    path('', include('equity_hub.urls')),
]
```

equity_hub の urls.py にupdaste と delete を追加して、delete には削除対象がわかるように<int:pk>(プライマリーキー)を渡すようにしておきます。
```py:equity_hub/urls.py
from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('home/api/register/', views.EquityHubRegisterViewAPI.as_view(), name='register'),
    path('home/api/update/', views.EquityHubUpdateViewAPI.as_view(), name='update'),
    path('home/api/delete/<int:pk>/', views.EquityHubDeleteViewAPI.as_view(), name='delete'),
]
```

## view設定
update処理(EquityHubUpdateViewAPI) と delete処理(EquityHubUpdateViewAPI)を作成します。
#### 更新処理
```py:equity_hub/views.py
@method_decorator(login_required, name='dispatch')
class EquityHubUpdateViewAPI(APIView):
    def put(self, request):
        get_equity_hub = GetEquityHubView()
        try:
            equity_hub = EquityHub.objects.filter(user=self.request.user)
            for item in equity_hub:
                stock_name, dividend_yield, current_price, industry = get_equity_hub.search_stock(item.symbol)
                if stock_name is not None:
                    item.dividend_yield = dividend_yield
                    item.price = current_price
                    item.industry = industry
                    item.save()
                    time.sleep(1)
            return Response(status=204)
        except Exception as e:
            return Response(status=400)
```
EquityHub.objects.filter()でログインしているユーザーの所有株を取得してひとつずつ行使にしています。
また、連続での処理となるためtime.sleep(1)を入れて1秒待たせています。

#### 削除処理
```py:equity_hub/views.py
@method_decorator(login_required, name='dispatch')
class EquityHubDeleteViewAPI(APIView):
    def delete(self, request, *args, **kwargs):
        try:
            EquityHub.objects.filter(stock_No=kwargs['pk']).delete()
            return Response(status=204)
        except Exception as e:
            return Response(status=400)
```
削除処理は受け取った[ pk ] に対して delete() で削除しています。

## 更新処理のテスト
```py:equity_hub/tests.py
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
from django.contrib.auth import get_user_model
from equity_hub.models import EquityHub
from unittest.mock import patch

User = get_user_model()

class EquityHubUpdateViewAPITest(APITestCase):
    def setUp(self):
        # テスト用ユーザーを作成
        self.user = User.objects.create_user(username='testuser', password='password')
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)
        self.url = reverse('update')  # URLの名前を逆引きで取得
        self.equity_hub_entry = EquityHub.objects.create(
            user=self.user,
            symbol="****", # 適当な証券コード
            dividend_yield=9999,
            price=9999,
            industry="****", # 適当な業種
            shares_owned=0
        )

    def test_update_equity_hub_success(self):
        with patch('equity_hub.views.GetEquityHubView.search_stock') as mock_search_stock:
            # ログイン状態をシミュレート
            self.client.login(username='testuser', password='password')
            
            mock_search_stock.return_value = ("****", 1000, 1000, "Technology") # 戻り値設定

            # PUTリクエストを送信
            response = self.client.put(self.url)

            # ステータスコード204を確認
            self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

            # EquityHubエントリが正しく更新されたか確認
            self.equity_hub_entry.refresh_from_db()
            self.assertEqual(self.equity_hub_entry.dividend_yield, 1000)
            self.assertEqual(self.equity_hub_entry.price, 1000)
            self.assertEqual(self.equity_hub_entry.industry, "Technology")

    def test_update_equity_hub_unauthorized(self):
        # 認証せずにPUTリクエストを送信
        self.client.logout()
        response = self.client.put(self.url)

        # ステータスコード302を確認(認証必須)
        self.assertEqual(response.status_code, status.HTTP_302_FOUND)
```
setUp で equity_hub_entry を作成しておき、patchを使用してsearch_stockのモックを設定しておきます。
self.client.put() でモックの値が equity_hub_entry に登録されているかを確認しています。

## 削除処理のテスト
```py:equity_hub/tests.py
class EquityHubDeleteViewAPITest(APITestCase):
    def setUp(self):
        # テスト用ユーザーを作成
        self.user = User.objects.create_user(username='testuser', password='password')
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)
        self.equity_hub_entry = EquityHub.objects.create(
            user=self.user,
            stock_No=1, # 適当なNo.
            symbol="9999",  # 適当な証券コード
            dividend_yield=9999,
            price=9999,
            industry="****",
            shares_owned=0
        )

    def test_delete_equity_hub_success(self):
        # ログイン状態をシミュレート
        self.client.login(username='testuser', password='password')
        url = reverse('delete', kwargs={'pk': self.equity_hub_entry.pk})
        response = self.client.delete(url)

        # ステータスコードが204 (No Content) であることを確認
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

        # データベースにエントリが削除されていることを確認
        self.assertEqual(EquityHub.objects.count(), 0)

    def test_delete_equity_hub_unauthorized(self):
        # 認証無しでdeleteリクエスト
        url = reverse('delete', kwargs={'pk': self.equity_hub_entry.pk})  # 存在しないIDを指定
        response = self.client.delete(url)

        # ステータスコードが400 (Bad Request) であることを確認
        self.assertEqual(response.status_code, status.HTTP_302_FOUND)

        # データベースにエントリが削除されていないことを確認
        self.assertEqual(EquityHub.objects.count(), 1)
```

#### 実行結果
```sh
> python manage.py test
Found 4 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....
----------------------------------------------------------------------
Ran 4 tests in 2.211s

OK
Destroying test database for alias 'default'...
```
Ran 4 tests に対して OK となっていれば成功です。

## 終わりに
以上で登録, 更新, 削除処理が作成できました。
次回は、登録している株の一覧(ホーム)と各処理の関連付けを行います。