DjangoとReact redux TypeScriptを使ってオリジナルアプリを作ってみました（TwitterAPI）その2

##前回までのあらすじ
Djangoでアプリのデータベースの作成と、settings.pyを編集した。

[DjangoとReact redux TypeScriptを使ってオリジナルアプリを作ってみました（TwitterAPI）その1]
(https://qiita.com/kenshow-blog/items/8d2768b9f4b53cdd4dfd)

今回はviewの部分を構築し、djangoを使ったAPIを完成させる

##この記事でやること

・ユーザー周りのAPIをdjango-rest-frameworkで完成させる
・TwitterAPIを叩いて画像を収集するAPI(media_api)を完成させる

この記事で外部APIを使ってどのようにバックエンドを構築していくのかを参考にできると思います

##このオリジナルアプリ開発記事
[DjangoとReact redux TypeScriptを使ってオリジナルアプリを作ってみました（TwitterAPI）その1](https://qiita.com/kenshow-blog/items/8d2768b9f4b53cdd4dfd)

[DjangoとReact redux TypeScriptを使ってオリジナルアプリを作ってみました（TwitterAPI）その2](https://qiita.com/kenshow-blog/items/2e1501c3d86d66b16f97)

[DjangoとReact redux TypeScriptを使ってオリジナルアプリを作ってみました（TwitterAPI）その3]
(https://qiita.com/kenshow-blog/items/7ecbd85e00a7e1f0bc75)

[DjangoとReact redux TypeScriptを使ってオリジナルアプリを作ってみました（TwitterAPI）その4]
(https://qiita.com/kenshow-blog/items/5606d29b0b2bfa4a5c1d)

#アプリ構造

git↓
https://github.com/kenshow-blog/twitter

バックエンドディレクトリ構造図（django)


````````
twitter
├── auth_api #ユーザーのアカウントを管理するところ
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   ├── models.py
│   ├── serializers.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── db.sqlite3
├── manage.py
├── media
│   └── media
│       ├── 収集した画像が入る
│
├── media_api #TwitterAPIを叩いて画像を収集し、保存をしてくれるところ
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── config.py #APIキーを格納しておくファイル
│   ├── migrations
│   ├── models.py 
│   ├── serializers.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
└── twitter
    ├── __init__.py
    ├── __pycache__
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

```

##ユーザーモデルのserializer,viewの作成

シリアライザーとは、client側とデータベース側のやり取りの中間に立つもので、主にvalidationやpasswordをwrite-onlyにするといった制約等をかけることができる

早速コードを見てみる、、、

```python:twitter/auth_api/serializers.py
from django.contrib.auth import update_session_auth_hash
from rest_framework import serializers

from .models import Account, AccountManager


class AccountSerializer(serializers.ModelSerializer):

    class Meta:
        model = Account
        fields = ('id', 'username', 'email', 'password')
        extra_kwargs = {'password': {'write_only': True, 'required': True}}

    def create(self, validated_data):
        return Account.objects.create_user(request_data=validated_data)
```

passwordは入力された情報の認証にしか使わないのと、セキュリティ上公開できないようにしたいので、write_onlyをTrueに設定することで書き込みだけできるようにした。


続いて、ユーザーモデルのviewを作成する

```python:twitter/auth_api/views.py
from django.shortcuts import render
from django.db import transaction
from django.http import HttpResponse, Http404
from rest_framework import authentication, permissions, generics, status, viewsets, filters
import django
import os
import sys

sys.path.append('/Users/tanakakenshou/Desktop/Twitter/twitter')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'twitter.settings')
django.setup()
from rest_framework_jwt.settings import api_settings
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import AccountSerializer
from .models import Account, AccountManager
# Create your views here.

class AuthRegister(generics.CreateAPIView):
    permission_classes = (permissions.AllowAny,)
    #登録時はpermissionを許可して、クライアント側からの登録するユーザー情報を受け取れるようにしている
    serializer_class = AccountSerializer

    @transaction.atomic
    def post(self, request, format=None):
        serializer = AccountSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class AuthInfoGetView(generics.RetrieveAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    queryset = Account.objects.all()
    serializer_class = AccountSerializer

    def get(self, request, format=None):
        return Response(data={
            'id': request.user.id,
            'username': request.user.username,
            'email': request.user.email,
        },
        status=status.HTTP_200_OK)
```

とりあえず、ユーザーのviews.pyも完成。

次にこれらのpathを通して実際にブラウザ上でも処理ができるか確認できるようにしてみる。

```python:twitter/auth_api/urls.py
from django.conf.urls import include, url
from rest_framework import routers
from .views import AuthRegister, AuthInfoGetView

urlpatterns = [
    url(r'^register/$', AuthRegister.as_view()),
    url(r'^mypage/$', AuthInfoGetView.as_view()),
]

```


ここまでできたら
python manage.py runserver
を実行して、

http:127.0.0.1:8000/api/register
でユーザーを作成した後、

http:127.0.0.1:8000/login/
でJWTトークンを作成

http:127.0.0.1:8000/api/mypage
でユーザーを確認できる

##メディアモデルのserializer,viewを作成
ここで初めてTwitterAPIを実際に叩いて
データ保存をできるようにコードを書いていく

```python:twitter/media_api/serializers.py
from django.contrib.auth import update_session_auth_hash
from rest_framework import serializers

from .models import Images,ImagePost


class ImagePostSerializer(serializers.ModelSerializer):
    created_on = serializers.DateTimeField(format="%Y/%m/%d", read_only=True)

    class Meta:
        model = ImagePost
        fields = ('id', 'scrName', 'userPost', 'created_on')
        extra_kwargs = {'userPost': {'read_only': True}}

class ImagesSerializer(serializers.ModelSerializer):
    
    class Meta:
        model = Images
        fields = ('id', 'userImg', 'imgs', 'imgPost')
        extra_kwargs = {'userImg': {'read_only': True}}
```

ここでuserPost,userImgはどちらもユーザーモデルから外部キーとして取ってきているため、これらのviewページでユーザー情報を編集できてしまってはいけないので、read_onlyをTrueに設定しておく。

続いて、メディアモデルのviewを作成するのだが、その前にTwitterAPIのキーを保管しておくページを作っておく。

```python:twitter/media_auth/config.py
# API の秘密鍵
CK =   # APIキー
CS =  # APIシークレットキー
AT =   # アクセストークン
AS = # アクセストークンシークレット
```

これをviews.pyでimportしてTwitterAPIを扱えるようにしておく

そして、views.pyを作成

```python:twitter/media_auth/views.py
from django.shortcuts import render
from django.http import HttpResponse, JsonResponse
from rest_framework import generics, status
from django.views.generic import DeleteView
from rest_framework import viewsets
from . import serializers
from django.views.decorators.csrf import csrf_exempt
from .models import Images, ImagePost
from . import config
import requests
import tweepy
import datetime
import urllib.request
from .models import Images
from rest_framework.response import Response
import pprint
import os
from auth_api.models import Account
import random
import string
import json
import glob
import re
# Create your views here.

CK = config.CK
CS = config.CS
AT = config.AT
AS = config.AS
auth = tweepy.OAuthHandler(CK,CS)
auth.set_access_token(AT, AS)
api = tweepy.API(auth, wait_on_rate_limit=True)

class ImagePostViewSet(viewsets.ModelViewSet):
    queryset = ImagePost.objects.all()
    serializer_class = serializers.ImagePostSerializer
    def create(self, request, *args, **kwargs):
        
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        try:
            Account = self.request.data['scrName']
            tweets = api.user_timeline(Account, count=1, page=1)
        except:
            return Response(serializer.data, status=status.HTTP_404_NOT_FOUND)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def get_queryset(self):
        return self.queryset.filter(userPost=self.request.user)

    def perform_create(self, serializer):
        
        serializer.save(userPost=self.request.user)

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        imgs = Images.objects.filter(imgPost=instance.id)

        imgsdata = []
        for img in imgs:
            imgsdata.append(str(img.id))
        images_list = glob.glob("./media/media/*")
        url_items = "http://127.0.0.1:8000/api/mypage/"
        headers = {'Authorization': request.headers['Authorization']}
        r_get = requests.get(url_items, headers=headers)
        user = r_get.json()['username']
        for image in images_list:
            split_image = re.split('[_.]', str(image))
            check = split_image[-2]
            if check in imgsdata and user in image:
                os.remove(image)

        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)
        


class ImagePostListView(generics.ListAPIView):
    queryset = ImagePost.objects.all()
    serializer_class = serializers.ImagePostSerializer

    def get_queryset(self):
        return self.queryset.filter(userPost=self.request.user)


class ImagesListView(generics.ListAPIView):
    queryset = Images.objects.all()
    serializer_class = serializers.ImagesSerializer

    def get_queryset(self):
        return self.queryset.filter(userImg=self.request.user)

class ImagesDestroy(generics.DestroyAPIView):
    queryset = Images.objects.all()
    serializer_class = serializers.ImagesSerializer
    def delete(self, request, *args, **kwargs):
        pk = self.kwargs['pk']
        images_list = glob.glob("./media/media/*")
        url_items = "http://127.0.0.1:8000/api/mypage/"
        headers = {'Authorization': request.headers['Authorization']}
        r_get = requests.get(url_items, headers=headers)
        user = r_get.json()['username']
        for image in images_list:
            if str(pk) in image and user in image:
                os.remove(image)
        return self.destroy(request, *args, **kwargs)

def upload_post_path(user,instance, filename):
    ext = filename.split('.')[-1]
    return "".join(["media/media/",str(user)+'_'+str(instance.imgPost.scrName)+'_'+str(instance.id)+str(".")+str(ext)])
    
@csrf_exempt
def images(request):
    json_scrName = json.loads(request.body)
    screen_name = json_scrName["scrName"]
    search_results = (tweepy.Cursor(api.user_timeline,
                        screen_name=screen_name,
                        tweet_mode="extended",
                        include_entities=True,
                        exclude_replies=True,
                        include_rts=False).items())
    quantity = 0
    try:
        for result in search_results:
            contents = result._json
            if quantity > 20:
                break
            if "extended_entities" in contents:
                content_check = contents["extended_entities"]["media"][0]
                if len(content_check) > 0:
                    if not "video_info" in content_check:
                        
                        for photo in contents['extended_entities']['media']:
                            if ".jpg" in photo['media_url']:
                                image_url = photo['media_url'][:-4] + \
                                                "?format=jpg&name=orig"
                                print(image_url)
                                save_name = 'test.jpg'
                            elif ".png" in photo['media_url']:
                                image_url = photo['media_url'][:-4] + \
                                        "?format=png&name=orig"
                                print(image_url)
                                save_name = 'test.png'
                            else:
                                image_url = photo['media_url']
                                print(image_url)
                                save_name = 'test.jpg'

                            
                            # response = request.GET.get(image_url)
                            tgt = urllib.request.urlopen(image_url).read()
                            
                            
                            url_items = "http://127.0.0.1:8000/api/mypage/"

                            
                            headers = {'Authorization': request.headers['Authorization']}
                            r_get = requests.get(url_items, headers=headers)
                            userId = r_get.json()['id']
                            user = r_get.json()['username']
                            userImg = Account.objects.get(id=userId)
                            imgPost = ImagePost.objects.get(scrName=screen_name)
                            data = Images()
                            data.id = int("".join([str(random.randint(1, 10)) for i in range(5)]))
                            data.userImg = userImg
                            data.imgPost = imgPost
                            save_name = upload_post_path(user, data, save_name)
                            with open(save_name, mode='wb') as f:
                                f.write(tgt)
                            data.imgs = save_name.replace('media/','',1)

                            data.save()
                            quantity += 1
    except:
        return HttpResponse(status=404)

    return HttpResponse(status=201)
```


かなり複雑になってしまったので一つずつ解説していく



```python
from . import config
CK = config.CK
CS = config.CS
AT = config.AT
AS = config.AS
auth = tweepy.OAuthHandler(CK,CS)
auth.set_access_token(AT, AS)
api = tweepy.API(auth, wait_on_rate_limit=True)
```

上記のコードは、TwitterAPIを叩く際の最初の準備的なもの、、、

これらを利用して、ユーザーのツイート情報の収集や画像収集ができるようになる。


```python
class ImagePostViewSet(viewsets.ModelViewSet):
    queryset = ImagePost.objects.all()
    serializer_class = serializers.ImagePostSerializer
    def create(self, request, *args, **kwargs):

        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        
        try:
            Account = self.request.data['scrName']
            tweets = api.user_timeline(Account, count=1, page=1)
        except:
            return Response(serializer.data, status=status.HTTP_404_NOT_FOUND)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)

        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def get_queryset(self):
        return self.queryset.filter(userPost=self.request.user)

    def perform_create(self, serializer):

        serializer.save(userPost=self.request.user)

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        imgs = Images.objects.filter(imgPost=instance.id)

        imgsdata = []
        for img in imgs:
            imgsdata.append(str(img.id))
        images_list = glob.glob("./media/media/*")
        url_items = "http://127.0.0.1:8000/api/mypage/"
        headers = {'Authorization': request.headers['Authorization']}
        r_get = requests.get(url_items, headers=headers)
        user = r_get.json()['username']
        for image in images_list:
            split_image = re.split('[_.]', str(image))
            check = split_image[-2]
            if check in imgsdata and user in image:
                os.remove(image)

        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)
```

ImagePostViewSetは、フロントで入力されたスクリーンネームをDBに保存、削除する処理を実装してくれている。


```python
try:
   Account = self.request.data['scrName']
   tweets = api.user_timeline(Account, count=1, page=1)
except:
   return Response(serializer.data, status=status.HTTP_404_NOT_FOUND)

```

は、入力されたスクリーンネームが実際に存在するかをTwitterAPIで、そのユーザーのタイムライン情報を取得する実装をすることで確認している。
存在しなかったらtryの中身の処理の途中に、エラーが発生するので、その時のエラーメッセージを返してDBには保存できないようにしている。


```python
def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        imgs = Images.objects.filter(imgPost=instance.id)

        imgsdata = []
        for img in imgs:
            imgsdata.append(str(img.id))
        images_list = glob.glob("./media/media/*")
        url_items = "http://127.0.0.1:8000/api/mypage/"
        headers = {'Authorization': request.headers['Authorization']}
        r_get = requests.get(url_items, headers=headers)
        user = r_get.json()['username']
        for image in images_list:
            split_image = re.split('[_.]', str(image))
            check = split_image[-2]
            if check in imgsdata and user in image:
                os.remove(image)

        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)
```

続いて削除機能の実装について、
一言で表すと、取得したユーザーの画像も名前も全て削除する実装をしている。
twitter/media_api/models.py
の中身を参照するとわかるのだが、取得した画像の名前には、保存する際にその画像に設定したidと、取得を試みたユーザーの名前（username）が入っている。

```python
imgs = Images.objects.filter(imgPost=instance.id)
```

で削除したいImagePostのidが、instance.idに入っているので、それをもとにImagesモデルにフィルターをかけて削除したImagePostに紐づけられた画像だけを取得してきている

これらの情報をゲットした後、それらの条件を満たす画像をすべて削除する実装をしている。
あとは、viewsetsのdestroyを実装してあげて、ImagePostからも削除したユーザー（画像収集されたTwitterのユーザー）情報を削除している


ImagePostListViewは、ログインしたユーザが入力しcollectしたTwitterのスクリーンネーム一覧を確認することができる

ImagesListViewは、取得した画像すべてを確認できる

ここからがTwitterAPIを叩いて画像を取得するアルゴリズムが書かれている

```python
@csrf_exempt
def images(request):
    json_scrName = json.loads(request.body)
    screen_name = json_scrName["scrName"]
    search_results = (tweepy.Cursor(api.user_timeline,
                        screen_name=screen_name,
                        tweet_mode="extended",
                        include_entities=True,
                        exclude_replies=True,
                        include_rts=False).items())
    quantity = 0
    try:
        for result in search_results:
            contents = result._json
            if quantity > 20:
                #ここで取得したい画像の枚数を指定できる。すべて取得したい場合はコメントアウトしておく
                break
            if "extended_entities" in contents:
                content_check = contents["extended_entities"]["media"][0]
                if len(content_check) > 0:
                    if not "video_info" in content_check:

                        for photo in contents['extended_entities']['media']:
                            if ".jpg" in photo['media_url']:
                                image_url = photo['media_url'][:-4] + \
                                                "?format=jpg&name=orig"
                                print(image_url)
                                save_name = 'test.jpg'
                            elif ".png" in photo['media_url']:
                                image_url = photo['media_url'][:-4] + \
                                        "?format=png&name=orig"
                                print(image_url)
                                save_name = 'test.png'
                            else:
                                image_url = photo['media_url']
                                print(image_url)
                                save_name = 'test.jpg'


                            # response = request.GET.get(image_url)
                            tgt = urllib.request.urlopen(image_url).read()


                            url_items = "http://127.0.0.1:8000/api/mypage/"


                            headers = {'Authorization': request.headers['Authorization']}
                            r_get = requests.get(url_items, headers=headers)
                            userId = r_get.json()['id']
                            user = r_get.json()['username']
                            userImg = Account.objects.get(id=userId)
                            imgPost = ImagePost.objects.get(scrName=screen_name)
                            data = Images()
                            data.id = int("".join([str(random.randint(1, 10)) for i in range(5)]))
                            data.userImg = userImg
                            data.imgPost = imgPost
                            save_name = upload_post_path(user, data, save_name)
                            with open(save_name, mode='wb') as f:
                                f.write(tgt)
                            data.imgs = save_name.replace('media/','',1)

                            data.save()
                            quantity += 1
    except:
        return HttpResponse(status=404)

    return HttpResponse(status=201)

```

まず、@csrf_exemptを関数の直前に書かないとcsrfのセキュリティ上のエラーが発生してしまう。
search_results には入力されたスクリーンネームから取ってきたツイートデータが全て入っている。

for result in search_results
でそのユーザーの過去のツイートを一軒ずつ見に行っていて、
そのツイートに画像や動画が含まれているかは
contents["extended_entities"]["media"][0]が存在するかで確かめることができる。

if not "video_info" in content_check
で含まれているデータが動画ではない場合の条件を書いている。

あとは、取得した画像の拡張子に合わせて画像の名前を作っている感じ。

```python
tgt = urllib.request.urlopen(image_url).read()


with open(save_name, mode='wb') as f:
          f.write(tgt)
```

で取得した画像を読み込んで、画像の読み書きが行えるwbモードでsave_nameのファイルに書き込んでいっている。


これでmedia_apiのviewsが完成した。

あとは、完成したviewsとpathの紐づけを行ってバックエンドの処理はすべて完了したことになる


```python:twitter/media_api/urls.py
from django.urls import include, path
from . import views
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('post', views.ImagePostViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('images/', views.images),
    path('imageslist/', views.ImagesListView.as_view()),
    path('imageslist/<str:pk>/', views.ImagesDestroy.as_view(), name='detail'),
]
```

djangoのviewsetsを使う場合は、上記のコードのようにrouterを使ってpathを通さなければならない



バックエンド
終了:ok_hand:


##ここまでの感想
TwitterAPIを叩いてDBに保存する処理はめちゃくちゃ苦戦した。。。。。
ここが一番難しかったと思う。
絶対に通常のやり方とは違うが、試行錯誤して何とか動いたので達成感半端なかった（笑）:sob:

次回からreactとtypescriptでフロント開発を発信していきます！

ここまで読んでくださりありがとうございました！:raised_hands_tone2::raised_hands_tone2: