小村のポートフォリオサイト開発(18) ページネーションを追加する(1)
こんばんは、小村だよ!
今日も下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
前回storeを作成したので、今回はその機能を増やしていくよ!
よろしくね!!!
目次
- Entry取得時にページネーションを追加する
- ページ切替処理を追加する
記録
Entry取得時にページネーションを追加する
現状はシンプルに、
api/entries
にアクセスするとDBの全件を取得しますこのままだと今後記事が増えた時に読み込みが大変ですね!
というわけでページネーションを追加していこうと思います。
-
- この参考サイトもシンプルでいいですね
では早速実装していきましょう!
ページネーション用のクラスを作成し、デフォルト値を設定します。
あとは
view
にpagination_class = EntryViewPagination
を加えれば完成UIからAPIを呼び出すと下記のようになります
responseに
count
,next
,previous
,results
が加わりました!返り値が変わったためデータを正しく取得できず、UIがエラーとなってますね
正しく取得してあげましょう!
データの格納場所が
request.data
からrequest.data.results
に移動しました。これで取得するレコード数がPaginationで定義した数になりましたね。
- 3件だけだと上に寄ってる感がすごいな……
ページ切替処理を追加する
ではページを切り替えられるようにしていきましょう
まず、storeのentry.jsに
page
を用意して、pageの値を取得、セットするgettars
,mutation
,actions
を用意しますそれから
fetchEntries
ではurlを引数に取るようにしましょうか
次はコンポーネント側でstoreの
page
を読み書きできるようにしますcomputedに
page:{get(){},set(){}}
を記述することで実現できますあとはこのpageの値に応じてURLを変更する
getFetchUrl
を用意して<v-pagenation>
を操作時に再読み込みする処理を追加すれば完成!
わーい!
ちゃんとページの切替できてますね!
おわりに
まだページ遷移処理は少し残ってます
具体的には、ページネーション数をレコード数に応じて変化させたり
ソート順も降順にしたいですね。最新の記事が手前に欲しい
そのあたりを次回にやりたいとおもいます!
それでは!ちゃおちゃお~~~!
小村のポートフォリオサイト開発(17) API呼び出しをstoreに記述する
こんばんは、小村だよ!
今日も下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
まずはデデン!
サクサクっとサムネイル画像を表示できるようにUIを修正しました。
- 修正箇所は割愛。
画像入るとだいぶそれっぽく見えますねー
今日は何をしようかなーーー
ひとまず今は単純にAPI呼び出ししてますが、今後にそなえてstoreに移動しましょうか。
そうしないと他の箇所で使うときに困ることになりますからねー
目次
- 現状整理
- storeを記述
記録
現状整理
APIの呼び出しなどを1ヵ所で行い、このBrogCardはそこから値を呼び出すのが好ましいです
そこで出てくるのが
store
になりますstoreはvuexの仕組みを適用したNuxt.jsのフォルダになります
storeを記述
では早速書いていきましょうか!
storeでは
state``getter``mutation``action
の4つの要素があります詳しい説明は割愛!基礎なので書籍読もう!
書いた結果がこちら
export const state = () => ({ itemsloading: false, items: [], }) export const getters = { getEntryItemsLoading(state) { return state.itemsloading }, getEntryItems(state) { return state.items }, } export const mutations = { SET_ITEMS_LOADING(state, bool) { state.itemsloading = bool }, SET_ITEMS(state, items) { state.items = items }, } export const actions = { async fetchEntries({ commit }, params) { commit('SET_ITEMS_LOADING', true) const response = await this.$axios.get('/api/entries/') const items = response.data commit('SET_ITEMS', items) commit('SET_ITEMS_LOADING', false) }, }
- そしてBlogCard.vueを下記のように修正
このようにして、API呼び出し部分をstoreに記述し、それを呼び出すように変更できました
画面表示は変わっておりません
注意点として、async awaitでデータを取得しに行っている間にその格納予定の変数を画面から覗くとエラー吐くので、ロード中はアクセスしないようにしましょう!
上図の
<div v-if="!getEntryItemsLoading">
がその役割を担ってます
おわりに
基礎と言いながらエラーと戦って数時間かかりました!
業務ではコピペで済ましてた所だったので、改めて自分で調べながら書くと変なところでつまづいたりしますね。
今後は、このstoreを拡張しつつ、ページネーションを作ったりカテゴリをちゃんと表示したりします!
ではでは、ちゃお~~~!
小村のポートフォリオサイト開発(16) データ加工 MD形式に図のURLを埋め込む
こんばんは、小村だよ!
今日も下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
前回APIを利用してUI側にデータ取得するところまで完了しました!
色々やることあるけど、今日はデータ加工を主にやっていきます!
目次
- HTML形式とMD形式の図の共通点を見つける
- 方針決め
- 実装
記録
HTML形式とMD形式の図の共通点を見つける
現状、取得したデータをそのままWebサイトに表示するのに問題があります。
まずWeb内で使用するデータは
内容_MarkDown
を使用する予定です。これをちゃんとしたURLにしたものが、HTML形式のものに載ってますね。
というわけで、このURLを抜き出してMarkDown形式側を差替えする必要があります
まずは共通点を探すように、見比べてみましょうか
[f:id:kom314_prog:20210526210606p:plain]|
<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kom314_prog/20210526/20210526210606.png" alt="f:id:kom314_prog:20210526210606p:plain" title="" class="hatena-fotolife" itemprop="image">
ふむふむ!!!
何個か比べてみてみましたが、ほとんど共通形式として下記が見つかりました
markdown
頭は右記から始まる
[f:id:kom314_prog:
中間の数字はおそらくYYYYMMDDHHmmss
中間の数字の最後のpは拡張子?(pはpng?)
最後は
:plain]|
html
頭は右記から始まる
<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kom314_prog/
続きは、YYYYMMDD/YYYYMMDDHHmmss
続きに拡張子 + "
続きに
alt="markdownの文字そのまま"
方針決め
これルールがわかれば、HTMLから取ってこなくてもMarkDownのを書き換えれますね
まずはMarkDownの中で、
[f:id:kom314_prog:
の文字を<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kom314_prog/
に変換[f:id:kom314_prog:
の後ろの14文字の数字を取得し、左から8文字/14文字に変換後ろの
p:plain]
を.png">
に変換これでひとまず画像のURLを取得できそうですね。
ではコーディングしてみましょう!
実装
実装完了!
Markdown形式の内容の中の画像のURLが
<img>
タグに変換されてますね。最終的にはもっと別の形式に変えるかもしれませんが、ひとまずこれでOK
最終的な修正箇所とソースを張り付けておきます
import os import requests import xmltodict class HatenaApi(): HATENA_API_USER = os.environ.get('HATENA_API_USER') HATENA_API_BLOG = os.environ.get('HATENA_API_BLOG') HATENA_API_KEY = os.environ.get('HATENA_API_KEY') HATENA_API_URL_HEADER = 'https://blog.hatena.ne.jp' HATENA_API_URL_FUTTER = 'atom' IMG_SIMPLE_URL_HEADER = '[f:id:kom314_prog:' IMG_SIMPLE_URL_FOOTER = ':plain]' IMG_SIMPLE_URL_EXTENSION = {'p': 'png'} IMG_FULL_URL_HEADER = '<img src="' IMG_FULL_URL_TEMPLATE = 'https://cdn-ak.f.st-hatena.com/images/fotolife/k/kom314_prog' + \ '/YYYYMMDD/YYYYMMDDHHMMSS.EXTENSION' IMG_FULL_URL_FOOTER = '">' def getAllEntries(self): all_entries = [] url = self.getHatenaApiFirstUrl('entry') while url != '': hatenaApiData = self.getHatenaApi(url) for entry in self.getApiEntries(hatenaApiData): all_entries.append(self.formatEntry(entry)) url = self.getHatenaApiNextUrl(hatenaApiData) return all_entries def getHatenaApi(self, url): auth = self.getHatenaApiAuth() hatena_list = requests.get(url, auth=auth) dict_data = xmltodict.parse(hatena_list.text, encoding='utf-8') return dict_data def getApiEntries(self, hatenaApiData): return self.getDictValue(hatenaApiData, ['feed', 'entry']) def formatEntry(self, entry): hatena_entry_id = self.getHatenaEntryId(entry) category = self.getCategory(entry) title = self.getDictValue(entry, ['title']) summary = self.getDictValue(entry, ['summary', '#text']) content_md = self.getDictValue(entry, ['content', '#text']) content_html = self.getDictValue( entry, ['hatena:formatted-content', '#text']) draft = self.getDictValue(entry, ['app:control', 'app:draft']) updated_at = self.getDictValue(entry, ['updated'], None) edited_at = self.getDictValue(entry, ['app:edited'], None) img_url_list = self.getImgUrlList(content_md) summry_img = self.getSummryImg(img_url_list) content = self.getContent(content_md, img_url_list) print(content) format_entry = {} format_entry['hatena_entry_id'] = hatena_entry_id format_entry['category'] = category format_entry['title'] = title format_entry['summary'] = summary format_entry['summry_img'] = summry_img format_entry['content'] = content format_entry['content_md'] = content_md format_entry['content_html'] = content_html format_entry['draft'] = draft format_entry['updated_at'] = updated_at format_entry['edited_at'] = edited_at return format_entry def getHatenaEntryId(self, entry): id_all = self.getDictValue(entry, ['id']) id = id_all[id_all.rfind('-') + 1:] if id_all else '' return id def getCategory(self, entry): category_all = self.getDictValue(entry, ['category']) if isinstance(category_all, list): category = category_all[0] else: category = category_all return self.getDictValue(category, ['@term']) def getImgUrlList(self, content_md): if not content_md: return '' # 画像の簡易URLを抜き出す img_url_list = [] start_position = 0 end_position = 0 while content_md.find( self.IMG_SIMPLE_URL_HEADER, end_position) >= 0: start_position = content_md.find( self.IMG_SIMPLE_URL_HEADER, end_position) end_position = content_md.find( self.IMG_SIMPLE_URL_FOOTER, start_position) + len(self.IMG_SIMPLE_URL_FOOTER) simple_url = content_md[start_position:end_position] # 簡易URLから日付と拡張子を取得 yyyymmddhhmmss = simple_url[len(self.IMG_SIMPLE_URL_HEADER):len( self.IMG_SIMPLE_URL_HEADER) + 14] simple_url_extension = simple_url[len( self.IMG_SIMPLE_URL_HEADER) + 14:len(self.IMG_SIMPLE_URL_HEADER) + 15] # 直リンク可能なURLを生成する full_url = self.IMG_FULL_URL_TEMPLATE full_url = full_url.replace('YYYYMMDDHHMMSS', yyyymmddhhmmss) full_url = full_url.replace('YYYYMMDD', yyyymmddhhmmss[:8]) full_url = full_url.replace( 'EXTENSION', self.IMG_SIMPLE_URL_EXTENSION[simple_url_extension]) img_url_list.append([simple_url, full_url]) return img_url_list def getSummryImg(self, img_url_list): if not isinstance(img_url_list, list) or len(img_url_list) == 0: return '' if not isinstance(img_url_list[0], list) or len(img_url_list[0]) == 0: return '' return img_url_list[0][1] def getContent(self, content_md, img_url_list): content = content_md for img_url in img_url_list: if not isinstance(img_url, list): break content = content.replace(img_url[0], self.getImgTag(img_url[1])) return content def getImgTag(self, img_url): return self.IMG_FULL_URL_HEADER + img_url + self.IMG_FULL_URL_FOOTER def getHatenaApiFirstUrl(self, action): url = [ self.HATENA_API_URL_HEADER, self.HATENA_API_USER, self.HATENA_API_BLOG, self.HATENA_API_URL_FUTTER, action ] return os.path.join(*url) def getHatenaApiNextUrl(self, hatenaApiEntries): url = '' links = self.getDictValue(hatenaApiEntries, ['feed', 'link']) if isinstance(links, list): for link in links: if self.getDictValue(link, ['@rel']) == 'next': url = self.getDictValue(link, ['@href']) return url def getHatenaApiAuth(self): return (self.HATENA_API_USER, self.HATENA_API_KEY) def getDictValue(self, dictData, keys, emptyValue=''): for key in keys: if isinstance(dictData, dict): if key in dictData: dictData = dictData[key] else: return emptyValue return dictData
おわりに
せっかく今回URL変換したのに、詳細画面を実装してないから最終確認できない!
はやめに詳細画面実装したいなー
ではでは今日はこの辺で!ちゃお~~~!
小村のポートフォリオサイト開発(15) Axiosを使用したAPI呼び出し
こんばんは、小村だよ!
今日も下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
今回は久しぶりにUI側を触るよ!
ここまで書いてきたAPIをついにUI側から呼び出しちゃおうと思います!
目次
- Axiosの利用
- CORSエラーを解消する
- トップページに表示
記録
Axiosの利用
ではでは、APIをUIから呼び出していきまっしょい!
-
- このサイトおしゃれですね。ポートフォリオサイト自体の参考にもよさげ
Nuxt.jsからAPIを呼ぶ際、Axiosというライブラリを使うことが多いです
Nuxt.jsでプロジェクト作成時に、私はAxiosにチェックを入れたのでばっちり導入済でした!
じゃあさくっと動作確認用のコードをUI側に書いてさくっと実行しよう!
ほんとに動作確認のためだけのコードを書いたので実行!
エラーやんけ!!!!!!
びぇ~~~~ん!!!どらえも~~~ん!!!
CORSエラーを解消する
参考:【解決済】Docker上のNuxtでaxiosのCORSエラーにハマりまくった話
解消方法は下記の2つがあるみたいです
メッセージの通りヘッダに
Access-Control-Allow-Origin
を付けるプロキシを設定する
今回は参考サイトの通りproxyを設定してみましょう。
nuxt.config.js
を開き、下記の通り変更します
ひゃっほぉぉぉぉおおおおお!!!!
無事取得できました!!!!!!!!!!
トップページに表示
せっかく取得できたのでサイトに表示してみます!
色々エラーと格闘して最終的にこんな感じになりました!
おわりに
今日はこれでおしまい!
やーーーっとスタートラインに立った感じですかね!
こうやって日を置いてみるとサイトが物足りないな……
色々やること浮かんできますが、一つずつかたずけていきましょう!
ではでは、ちゃお~~~!
小村のポートフォリオサイト開発(14) DjangoRestFramework ここまでのリファクタリング
こんばんは、小村だよ!
[DRF レスポンスデータ]みたいな感じで検索したらこのブログがヒットしてびっくりしたよ!
じゃー今日も下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
前回はAPIのレスポンスのフォーマットを決めたね
目次
- 現状整理
utils.hatena.py
作成views.entry.py
修正
記録
現状整理
まずは現状の
views.entry.py
の中身が下記fat viewもいいところだね!
基本的にviewはコントローラー部分だけ書くべきなのでめちゃんこよくない例です。
このソースをリファクタリングしていきたいと思います
# coding: utf-8 import time import os import requests import xmltodict from rest_framework import status from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response from ..models import Entry from ..serializer import EntryAllSerializer, EntryCreateAndUpdateSerializer class EntryViewSet(viewsets.ModelViewSet): queryset = Entry.objects.all() serializer_class = EntryAllSerializer @action(detail=False, methods=['post']) def capture(self, request): url = self.getHatenaApiUrl('entry') auth = self.getHatenaApiAuth() hatena_entry_ids = [] response = { 'created_count': 0, 'updated_count': 0, 'deleted_count': 0, 'failed_count': 0, 'failed_hatena_entry_ids': [], } while url != '': # 連続で呼び出すの怖いので0.5秒間間を空ける time.sleep(0.5) hatena_list = requests.get(url, auth=auth) dict_data = xmltodict.parse(hatena_list.text, encoding='utf-8') entries = [] if 'feed' in dict_data: if 'entry' in dict_data['feed']: entries = dict_data['feed']['entry'] for entry in entries: if not isinstance(entry, dict): continue # hatena_entry_id取得 hatena_entry_id = entry['id'][ entry['id'].rfind('-') + 1:] if 'id' in entry else '' hatena_entry_ids.append(hatena_entry_id) # category取得 if 'category' in entry: if isinstance(entry['category'], list): category = entry['category'][0]['@term'] else: category = entry['category']['@term'] else: category = '' # title取得 title = entry['title'] if 'title' in entry else '' # summary取得 summary = entry['summary']['#text'] if 'summary' in entry else '' # content_md取得 content_md = entry['content']['#text'] if 'content' in entry else '' # content_html取得 content_html = entry['hatena:formatted-content']['#text'] if 'hatena:formatted-content' in entry else '' # draft取得 draft = entry['app:control']['app:draft'] if 'app:control' in entry else '' # updated_at取得 updated_at = entry['updated'] if 'updated' in entry else None # edited_at取得 edited_at = entry['app:edited'] if 'app:edited' in entry else None # 更新用パラメータ param = { 'hatena_entry_id': hatena_entry_id, 'category': category, 'title': title, 'summary': summary, 'content_md': content_md, 'content_html': content_html, 'draft': draft, 'updated_at': updated_at, 'edited_at': edited_at, } entry = Entry.objects.filter( hatena_entry_id=hatena_entry_id).first() if not entry: # 新規作成 mode = 'create' serializer = EntryCreateAndUpdateSerializer(data=param) else: # 更新 mode = 'update' serializer = EntryCreateAndUpdateSerializer( entry, data=param) if serializer.is_valid(): serializer.save() if mode == 'create': response['created_count'] = response['created_count'] + 1 else: response['updated_count'] = response['updated_count'] + 1 else: response['failed_count'] = response['failed_count'] + 1 response['failed_hatena_entry_ids'].append(hatena_entry_id) # 次のURLを取得 url = '' if 'link' in dict_data['feed']: if isinstance(dict_data['feed']['link'], list): for link in dict_data['feed']['link']: if isinstance(link, dict): if '@rel' in link: if link['@rel'] == 'next': url = link['@href'] # はてなIDが存在しなければ論理削除 response['deleted_count'] = Entry.objects.exclude( hatena_entry_id__in=hatena_entry_ids).delete() return Response(response, status.HTTP_200_OK) def getHatenaApiUrl(self, action): HATENA_API_URL_HEADER = 'https://blog.hatena.ne.jp' HATENA_API_USER = os.environ.get('HATENA_API_USER') HATENA_API_BLOG = os.environ.get('HATENA_API_BLOG') HATENA_API_URL_FUTTER = 'atom' url = [ HATENA_API_URL_HEADER, HATENA_API_USER, HATENA_API_BLOG, HATENA_API_URL_FUTTER, action ] return os.path.join(*url) def getHatenaApiAuth(self): HATENA_API_USER = os.environ.get('HATENA_API_USER') HATENA_API_KEY = os.environ.get('HATENA_API_KEY') return (HATENA_API_USER, HATENA_API_KEY)
utils.hatena.py
作成
ということでutilsフォルダ内にhatena.pyを作成して、はてなブログAPI関係の処理は全てここにまとめようと思います
これによりHatenaApiをインスタンス化して
getAllEntries
を実行するといい感じに加工された記事データが取得できるようになりました
import os import requests import xmltodict class HatenaApi(): HATENA_API_USER = os.environ.get('HATENA_API_USER') HATENA_API_BLOG = os.environ.get('HATENA_API_BLOG') HATENA_API_KEY = os.environ.get('HATENA_API_KEY') HATENA_API_URL_HEADER = 'https://blog.hatena.ne.jp' HATENA_API_URL_FUTTER = 'atom' def getAllEntries(self): all_entries = [] url = self.getHatenaApiFirstUrl('entry') while url != '': hatenaApiData = self.getHatenaApi(url) for entry in self.getApiEntries(hatenaApiData): all_entries.append(self.formatEntry(entry)) url = self.getHatenaApiNextUrl(hatenaApiData) return all_entries def getHatenaApi(self, url): auth = self.getHatenaApiAuth() hatena_list = requests.get(url, auth=auth) dict_data = xmltodict.parse(hatena_list.text, encoding='utf-8') return dict_data def getApiEntries(self, hatenaApiData): return self.getDictValue(hatenaApiData, ['feed', 'entry']) def formatEntry(self, entry): format_entry = {} format_entry['hatena_entry_id'] = self.getHatenaEntryId(entry) format_entry['category'] = self.getCategory(entry) format_entry['title'] = self.getDictValue(entry, ['title']) format_entry['summary'] = self.getDictValue( entry, ['summary', '#text']) format_entry['content_md'] = self.getDictValue( entry, ['content', '#text']) format_entry['content_html'] = self.getDictValue( entry, ['hatena:formatted-content', '#text']) format_entry['draft'] = self.getDictValue( entry, ['app:control', 'app:draft']) format_entry['updated_at'] = self.getDictValue( entry, ['updated'], None) format_entry['edited_at'] = self.getDictValue( entry, ['app:edited'], None) return format_entry def getHatenaEntryId(self, entry): id_all = self.getDictValue(entry, ['id']) id = id_all[id_all.rfind('-') + 1:] if id_all else '' return id def getCategory(self, entry): category_all = self.getDictValue(entry, ['category']) if isinstance(category_all, list): category = category_all[0] else: category = category_all return self.getDictValue(category, ['@term']) def getHatenaApiFirstUrl(self, action): url = [ self.HATENA_API_URL_HEADER, self.HATENA_API_USER, self.HATENA_API_BLOG, self.HATENA_API_URL_FUTTER, action ] return os.path.join(*url) def getHatenaApiNextUrl(self, hatenaApiEntries): url = '' links = self.getDictValue(hatenaApiEntries, ['feed', 'link']) if isinstance(links, list): for link in links: if self.getDictValue(link, ['@rel']) == 'next': url = self.getDictValue(link, ['@href']) return url def getHatenaApiAuth(self): return (self.HATENA_API_USER, self.HATENA_API_KEY) def getDictValue(self, dictData, keys, emptyValue=''): for key in keys: if isinstance(dictData, dict): if key in dictData: dictData = dictData[key] else: return emptyValue return dictData
views.entry.py
修正
あとは今までのentry.pyから上記処理を呼び出して、登録・更新・削除を行うようにします
もうちょっと短くなると思ってたのですが、思いのほか処理減らなかったな……
でもまぁかなり読みやすくなったのでよし!!!
# coding: utf-8 from rest_framework import status from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response from ..models import Entry from ..utils import HatenaApi from ..serializer import EntryAllSerializer, EntryCreateAndUpdateSerializer class EntryViewSet(viewsets.ModelViewSet): queryset = Entry.objects.all() serializer_class = EntryAllSerializer @action(detail=False, methods=['post']) def capture(self, request): hatena_api = HatenaApi() all_entries = hatena_api.getAllEntries() all_entries_sort = sorted(all_entries, key=lambda x: x['edited_at']) response = { 'created_count': 0, 'updated_count': 0, 'deleted_count': 0, 'failed_count': 0, 'failed_hatena_entry_ids': [], } hatena_entry_ids = [] for param in all_entries_sort: hatena_entry_id = param['hatena_entry_id'] hatena_entry_ids.append(hatena_entry_id) # 更新用パラメータ entry = Entry.objects.filter( hatena_entry_id=hatena_entry_id).first() if not entry: # 新規作成 mode = 'create' serializer = EntryCreateAndUpdateSerializer(data=param) else: # 更新 mode = 'update' serializer = EntryCreateAndUpdateSerializer( entry, data=param) if serializer.is_valid(): serializer.save() if mode == 'create': response['created_count'] = response['created_count'] + 1 else: response['updated_count'] = response['updated_count'] + 1 else: response['failed_count'] = response['failed_count'] + 1 response['failed_hatena_entry_ids'].append( hatena_entry_id) # はてなIDが存在しなければ論理削除 response['deleted_count'] = Entry.objects.exclude( hatena_entry_id__in=hatena_entry_ids).delete() return Response(response, status.HTTP_200_OK)
おわりに
リファクタリングが完了しました!
取込系の処理に関してはひとまずこれでよさそうかなーと思います
あ、でもテスト作ってないや。ユニットテスト作らなきゃだね
次回当たりユニットテスト作っていきたいと思います!
ではでは、ちゃお~~~!
小村のポートフォリオサイト開発(13) DjangoRestFramework レスポンスデータフォーマット
こんばんは、小村だよ!
下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
前回記事の論理削除対応をしたね
今回はAPIのレスポンスのフォーマットを決めていきたいと思います
目次
- APIのレスポンスのフォーマットを決める
- 記事取込処理の正常系レスポンスを決める
- 全体共通のエラー時のレスポンスを決める
記録
APIのレスポンスのフォーマットを決める
参考:Udemy REST WebAPI サービス 設計 26. データの内部構造
参考:WebAPIでエラーをどう表現すべき?15のサービスを調査してみた」についてまとめた
- APIのレスポンスは、業務上では下記のようなフォーマットで返してました
{ result: 1 or 0 data: { ... } }
改めて今回調べたところ、resultなどは返さないのが一般的のようですね?
何が一番いいのか色々見ましたが最終的にUdemyで上記講座に書かれていた方針でいきます
{ code: xx detail: '' }
記事取込処理の正常系レスポンスを決める
ではでは、今回の記事取込処理のレスポンスを決めていきましょう
まず、正常に処理が行われたときは下記を返すようにしましょうか
http_status: 200 { created_count: x, updated_count: x, deleted_count: x, failed_count: x, failed_ids: [x, x, x] }
正常かどうかはレスポンスヘッダで確認します。
HTTP 200 OK はリクエストが成功した場合に返すレスポンスコードですね
それから作成、更新、削除件数を返します。
最後に入力チェックではじかれた件数とIDを返すようにしましょうか
そんな感じにViewsを修正します
修正して実行した結果が最後の図。問題なさそうですね!
全体共通のエラー時のレスポンスを決める
エラーが発生した際の処理も決めたいと思います
上記を参考にException Handlerをカスタマイズしたいと思います
- 参考サイトを参考に、
utiles.handler.py
を作成して図のように記載
これをsettingsに上記画像のように書くのだけど、そこで2時間くらいエラーと戦いました。
結果として、上記の書き方です。
- エラーで詰まったら飯食うの大事。一瞬で解けた。フォルダパスの書き方が原因だった
実装してエラー吐いた結果がこれ
本当は出力を
message
にしたかったのにdetail
で出てしまうのは自分の実力不足でする……業務の方のシステムだとdetailなんて吐いてないので、実装覗いてきて改良します
おわりに
やっとこさ取込機能に関する機能の実装は一通りおしまい!
あとはリファクタリングしたらweb側の実装に戻れます!
あともうちょいだ!がんばろー!
ではでは、ちゃお~~~!
小村のポートフォリオサイト開発(12) DjangoRestFramework はてなブログの記事 論理削除対応
こんばんは、小村だよ!
下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
前回はてなブログ記事をすべてDBに書き込むことに成功しました
今回は論理削除対応と銘打って進めていきます
目次
- 削除日時を追加
- 削除日時を入れる処理追加
記録
削除日時を追加
自前のDBはバックアップとしての機能も期待しています。
そのため、はてなブログの記事が消えたとしても、削除はしません
そのかわりに、削除されていたら削除日を更新して記事の表示はしないようにします
ではまずmodelに削除日を用意しましょう
追加した後のマイグレーションファイルは下記のようになりました
削除日時を入れる処理追加
参考:(Django, DRF(Django REST Framework)での論理削除)https://qiita.com/t_toriumi/items/19c590ef3cda8438e741
上記サイトが参考になりそうですね!
サイトを参考に下記のように修正しました。
まずは
views
取込処理をしているなかで存在する
hatena_entry_id
を蓄え、最後になければdelete
かけてますここは特に問題ないですね
本題の
models
あらたに
QuerySet
とModelManager
の概念が加わってます。こちら業務で使ってますが詳しくは調べたことないのであとで調べてみますね
動作としては
delete()を実行すると論理削除する
delete_hard()を実行すると物理削除する
Entry.objectsでデータを覗くと論理削除データは省かれる
Entry.entireでデータを覗くと論理削除データも含む
という動きになります。どちゃくそ便利!
動作確認
そしたらはてなブログで記事を消して、ふたたび取り込みます
再取り込み後のデータが下記。ちゃんと削除日時が入ってますね!
DjangoのQuerySet
とModelManager
について
Modelsで出てきた
QuerySet
とModelManager
について調べてみます業務で使ってるのでなんとなくは使えてますが、改めて意味を調べてみます
Manager
今までなんとなしに
Entry.objects.all()
とかでデータを取得できてたのはこのManagerのおかげデフォルトでobjectsにアクセスした際、動くようになっているようですね。
今回のように
entire
の中でManagerを入れると、Entry.entireでアクセスできるようになります。そうしてアクセスした際の動きを、下記のQuerySetなどで制御するようですね。
QuerySet
QuerySetは実際にDBにアクセスせずにデータを加工したりする仕組みのようです。へー!
models.QuerySetが用意されていて、その中に基本のgetやfilterやexcludeなどが用意されてます
そのうえで、今回のような決まりきった削除などの更新処理も定義できるようですね。
DBにアクセスする際の抽出条件や処理条件を定義する役目ということでしょうね。
おわりに
論理削除の処理と合わせて、Djangoの基礎知識についても知れました
上記記事について熟読ができてないので、改めて後で読んでみようと思います。
次は、APIの返り値の設定をしていきたいと思います。
ではでは、ちゃお~~~!