DjangoRestFrameWork + SendGridでお手軽お問い合わせフォームを爆速で作る

# はじめに
私が個人Webサービスを作っている上での作業メモ。技術書典用のネタの下書きでもある。
フロントはVue.jsです。そのうちNuxtの方に移行したいですが・・・関係ないので省略します。

## お問い合わせフォームって?
まずこんな画面があって・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/e02d9e8c-9176-c3b4-02d1-0e1593e90322.png)

で、これです。これの事を言ってます。割といろんなサイトで見かけますよね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/0a9fbd61-c5b8-b762-4c2f-e20a8c946df1.png)

このお問い合わせフォームからメール配信サービスであるSendGridを経由して、このフォームの内容を管理人にメールで発信しよう、というのがこの記事の主題です。フロント側の方のコードは特に書く必要が無いと考えていますので、今回の記事では省略いたします。

また、SPFやDKIM等の送信ドメイン認証周りの設定方法も省略とさせてください。書いてる人もその辺り勉強中なので・・・。

##今回したい事の図
雑な図で恐縮です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/6670e17d-ccc2-cf9b-b502-786a0cc1bfa4.png)
流れとしては
1.フロント側でDjangoで設定したエンドポイントに向かってフォームデータをPost
2.Django側はPostされたデータをシリアライザで整形、及び検証した後にDBにInsertして、SendGrid側にパラメータ指定でPostする
3.無事管理人に届く
という感じになります。サーバーの責務多いです。

##SendGridを使う
###Sendgridって?
クラウド型メール配信サービスです。SMTPサーバーを立てる必要もなくAPIベースでのメール送信が可能なので、ITインフラ赤ちゃんでも簡単に自プロダクトにメール配信機能を盛り込む事ができます。料金体系も安くて個人開発者もにっこり。

###利用について
SendGridはAzureから経由して使うパターン、Herokuのアドオンから経由して使うパターン、自分で申請して登録するパターン等々・・・色々ありますよね。

余談ですがFreeプランの利用枠や料金体系が結構パターン別に違ったりするので、個人開発者の方々は留意した方がいいかもしれません。本当に結構違いますよ。


SendGridの契約先による、機能面やサポート面での違い
https://sendgrid.kke.co.jp/blog/?p=10084

###DynamicTemplatesについて
簡単に言うと、SendGrid側でHTMLメールのテンプレートを用意してくれる機能です。そのまんまですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/e4c4826d-a7f0-9c45-fb7d-55d0d2761407.png)
バージョニング機能、パラメータ設定機能などなど・・便利な機能盛りだくさんです。後段で説明します。

個人的には開発者のコーディング品質向上の観点上、非常に重要な機能だと考えています。それも後で説明しますね。

##DjangoRestFrameWorkについて
流石に省略します。RestAPIマンです。

###コード群
シリアライザ部分は特殊な部分が特にないので省略します。

####モデル
フォームのオプション部分はIntEnumで対応させてます。また、emailフィールドではユーザーの主キーを外部参照しています。

```python
class InfromOption(IntEnum):
  UIのクソさについて = 1
  機能追加要望 = 2
  不具合全般 = 3
  規約等 = 4
  その他 = 5

  @classmethod
  def pick_v(cls):
     return [(n.name,n.value) for n in cls]

  @classmethod
  def get_enum_name(cls,key): return cls(key).name.title()

class Inform(models.Model):
    class Meta:
        db_table='inform'

    inform_id =models.AutoField(primary_key=True,null=False)
    
    inform_option=models.IntegerField(
        verbose_name='オプション',
        choices=InfromOption.pick_v(),
        null=True
        )
    inform_rating=models.CharField(verbose_name='評価',max_length=50,null=True)
    inform_subtitle=models.CharField(verbose_name='件名',max_length=50,null=True)
    inform_body=models.TextField(verbose_name='内容',blank=True,null=True,max_length=1000)
    email =models.ForeignKey(
        get_user_model(),
        related_name='informuser',
        on_delete=models.PROTECT,
        null=True
    )

```

####ビュー
匿名ユーザーは空にする、ぐらいですかね。

```python

class InformApiView(views.APIView):
    def post(self,request,*args,**kwargs):
        serializer = InformSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        #anonymous判定
        req_user = request.user if request.user is not None else None
        serializer.save(email = req_user)
        
        posttingObj =serializer.validated_data
        posttingObj['email'] = req_user.email if req_user is not None else None

        UnitWorkService.informPostMail(posttingObj)

        return Response(status.HTTP_201_CREATED)
```

####サービスロジック
型チェック等マジで何もやってないですが動きます。

自分でリクエストパラメータ用のpersonalizationsオブジェクト整形処理したりするの結構めんどくさいです。
ので、サードパーティのSendGridのpython用ライブラリが折角用意されていますので、ありがたく使わせてもらいましょう(´・ω・｀）
https://github.com/sendgrid/sendgrid-python

環境変数等はよしなに設定するとよいでしょう。

```python
import environ
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import (
    Mail, From, To, TemplateId ,DynamicTemplateData
    )
class UnitWorkService:
    """ 
   他にはConohaオブジェクトストレージに画像投げたりパスワードの暗号化するクラスへの橋渡し用のクラスです。
　 主題に関係ない部分は省略します。
   """
    @staticmethod
    def informPostMail(inform_data):
        InformPosttingMail.sendMail(
            info_option= InfromOption.get_enum_name(inform_data['inform_option']) if 'inform_option' in inform_data else "",
            info_rating=inform_data['inform_rating']  if 'inform_rating' in inform_data else "",
            info_subject=inform_data['inform_subtitle']  if 'inform_subtitle' in inform_data else "",
            info_body=inform_data['inform_body']  if 'inform_body' in inform_data else "",
            info_user=inform_data['email']  if 'email' in inform_data else ""
            )

env=environ.Env()

class BaseSendMail(object):
    '''
    Sendgridの基底クラス
    '''

    def __init__(self):
        self.__message = Mail()
    
    @property
    def message(self):
        return self.__message
    
    @message.setter
    def message(self,value):
        name, data ,*_ = value
        setattr (self.__message, name, data)

    @staticmethod
    def lissttingParam(cls,**param):
　　　　　"""
        複数入れる場合に使う
        """
        return [cls(v,*_) for v,*_ in param['data']]
    
    def posttingMail(self):
        try:
            sg = SendGridAPIClient(env('SENDGRID_API_KEY'))
            response = sg.send(self.message)

        except Exception as e:
            print(e)
        
        finally:
            return response.status_code

class GlobalPosttingMail(BaseSendMail):
    def __init__(self):
        super().__init__()
        self.message = 'from_email',From('hogehogehoge','ほげほげほげ')

class InformPosttingMail(GlobalPosttingMail):
    def __init__(self):
        super().__init__()

        self.message = 'template_id',TemplateId(template_id='hogehogehogehoegheoghoeghoge')
        self.message = 'to', self.lissttingParam(To,data=[(env('APP_MANAGE_EMAIL'),'ほげほげほげ')])

    @property
    def dyna_param(self):
        '''
        dynamic_template定義のパラメータ
        '''
        return self.__dyna_param

    @dyna_param.setter
    def dyna_param(self,value):
        self.__dyna_param = {
                'info_option':value['info_option'],
                'info_rating':value['info_rating'],
                'info_subject':value['info_subject'],
                'info_body':value['info_body'],
                'info_user':value['info_user']
            }

    @classmethod
    def sendMail(cls,**param):
        infoS = cls()
        infoS.dyna_param = param
                
        infoS.message = 'dynamic_template_data', DynamicTemplateData(dynamic_template_data= infoS.dyna_param)

        return infoS.posttingMail()
```

###SendGridのDynamicTemplatesを設定する
上記のコードで以下のようなパラメータを成型してますが、このデータ群はDynamicTemplatesのレンダリングに利用します。プレースホルダーにも適用されます。

```python
    @property
    def dyna_param(self):
        '''
        dynamic_template定義のパラメータ
        '''
        return self.__dyna_param

    @dyna_param.setter
    def dyna_param(self,value):
        self.__dyna_param = {
                'info_option':value['info_option'],
                'info_rating':value['info_rating'],
                'info_subject':value['info_subject'],
                'info_body':value['info_body'],
                'info_user':value['info_user']
            }
```
ご覧の通りマスタッシュ記法で指定するパラメータを書けばよいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/a089e89d-2fa2-5ad6-cd91-3fbdb9dfad77.png)

###動作検証
####フロントでフォームで問い合わせを発射する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/cddfd207-2138-1773-fee0-6c54c019fd53.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/6a68e991-43cb-3d19-04dc-f2204057310e.png)
はい、届いてますね～
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302014/e4dfd2bf-bc94-e2d2-f305-659bb55330c5.png)


最後の方説明が超雑になりましたが、何か聞きたい事等があればお知らせください。

よろしくお願いいたします。