【アプリ開発日記48週目】演習履歴を保存&表示、フィルタリング ~復習編②~
前回までで「解きたい問題のフィルター機能」と「問題ページへの遷移」を実装してきました。今回は
過去の○×の最新履歴を取得、それに応じてフェーズでフィルターをかけられるようにする
を実装することで、開始時に「×だけ」「まだ解いたことのない問題だけ」のように絞って演習できるようになるので、勉強効率もグッと上がります。
ということでさっそく始めていきましょう!
1,演習履歴の保存&更新
まず現状確認。これまでのスクショ下部に記載されていた「履歴」ですが、まだ保存も更新も、問題ごとに取得してくる処理も実装していません。
これらの処理を作った上で、「問題一覧」ページでその問題の最新履歴を取得&問題リスト作成時にそれらを使ってフィルタリングできるようにしていきます。
まず「保存」処理は
フェーズボタンを押した時に「履歴を保存」する処理
すでに押された状態で変更した時「履歴を更新」する処理
が実行できれば良さそうですね!
今まで「node-fetch」(サーバー側のfetch)を使ってきたのですが、原因不明のエラーに悩まされていたので、これを機に「axios」で作成しました。
できた保存処理がこちら!
/component/HistoryList.js
const createPhase = async () => {
const current_id = location.pathname.split('/')[1];
const phase = document.getElementById('create-phase').innerHTML;
// データベースに保存
const data = { user: userId, book: quiz.book, section: quiz.section, quiz: Number(current_id), phase: 'good' };
const res = await axiosPost('result-post', data);
// localStorageに追加
localStorage.setItem('solved_count', Number(localStorage.getItem('solved_count'))+1);
if (phase == 'normal' || phase == 'bad') addMisslist();
// 表の○×を変更
changePhase();
console.log('localStorageに追加しました!', localStorage.getItem('miss_list'));
}
/lib/post.js
import axios from "axios";
const SERVERURL = 'http://127.0.0.1:8000/'
export async function axiosPost(url, data) {
return new Promise(async (resolve, reject) => {
await axios.post(`${SERVERURL}api/${url}/`, data)
.then(res => {
if (res.status != 201) console.log(`例外発生時の処理 : ${res.status}`);
console.log(res.data);
resolve(res.data);
})
.catch(err => {
console.log(err.response);
reject(err.response); // エラー時に実行
});
});
}
ちなみに、2つ目のaxiosの処理を
export async function axiosPost(url, data) {
const res = await axios.post(`${SERVERURL}api/${url}/`, data)
.catch(err => {
console.log(err.response);
return err.response
}).then(res => {
if (res.status != 201) console.log(`例外発生時の処理 : ${res.status}`);
// console.log(res);
return res.data.id;
});
};
のようにしてreturnで結果を取得しようとすると、axios内の処理が終わる前に親関数が先進んでしまうため、うまく取得できません。以下の記事にわかりやすく解説されていました!
post部分は他でもよく使い、またこの14行って意外と場所とって見づらくなってしまうので、別ファイルで作成することにしました。これをPUTに置き換え、idを指定して送信すれば更新も完成です!
2,問題ページで履歴を表示
さきほど履歴を保存するにあたって、phase(○や×など)の他に問題のidも外部キーで保存しておきました。今度は「この問題idを持った履歴を取得」していきます!
今までセクションで問題一覧などの取得をしてきましたが、それをいじれば良さそうですね。Django側ではListAPIViewを使います。
class ResultByQuizView(generics.ListAPIView): # 一覧
serializer_class = ResultSerializer
permission_classes = (AllowAny,)
def get_queryset(self):
quizid = self.request.query_params.get('quizid')
queryset = Result.objects.all().filter(quiz__id=quizid).order_by('-id')
for i in queryset:
print(i)
return queryset
結果を表示すると……
無事この問題の履歴だけを取得できました! まだ結果欄だけすべて「正解」と表示されていますが、そもそも今回は正解不正解ではなくphaseによる5段階評価を想定しているので、いずれ無くすかもしれません。
ついでに、演習中にphaseボタンを押すと一行追加され、データの保存と同時に最新の結果も表示されるようにしました!
3,問題選択画面で、対象の問題の最新履歴を取得
久しぶりに問題選択画面へ。今度はこのページから履歴を取得しますが、前回と異なるのは
問題のidではなく、セクションのidに一致した履歴を取得する
全てではなく、最新の履歴だけ(履歴同士の問題idが重複しない)
1は同じ処理を置き換えるだけで解決しますが、問題は2つ目。はじめはdistinct()を使う予定でしたが、調べていると遅いらしい、という情報もありました。一方、すべて取得・for文回して重複していないレコードのみを取り出すという方法もありますが、javascriptも決して演算が速くはないらしい?ので、このあたりは慎重に決める必要がありそうです。
どれが最善策なのかはわかりませんが、今回はとりあえず表示が目的なので、後者で取得しました!
今回は外部キーも多用していますし、一度データベースについて勉強しておいたほうが良さそうですね。
とりあえず全取得。これにforかけて最新の問題idとphaseだけ取得すると
無事最新だけ揃いました! …と言ってもまだ一問しか解いていなかったのと、初期の失敗作のnullしかありませんが、、、
と、ともかく「対象の問題の最新履歴を取得」はクリアとしましょう……
4,フィルタリングの実装
いよいよ今回の本題です!2週間前あんなに面倒がっていたこの処理ですが、いざやってみれば、という感じもあります。フレームワーク様々ですね。
さきほど最新履歴を取得したので、今度は前々回の「シャッフル」などのように、phaseボタンを押すと問題リストが変わるようにします。
その時の問題リストでは [ '1-309', '1-310', '2-410' ] のように「セクションid - 問題id」を配列にして並び替えていました。今回はそれを
「'1-309-good'」のように「セクションid - 問題id - phase」
にしていきます。まずphaseを追加して……
問題リスト作成時に、例えば「△と×が押されていたらphaseが"normal"か"bad"の場合のみ問題リストに追加する」という処理を加えれば良さそうです。現在ある問題に5種類いずれかのphaseをつけた演習履歴をデータベースに追加して、改めて上記のように表示してみると…
全問題に最新履歴を追加できました! この状態で、問題ページへの遷移だけせずに「normal」「bad」だけ選択し、問題リストに追加・表示してみると……
無事normalとbadの問題だけ問題リストに追加されました! あとは問題ページに遷移すれば……
演習履歴が「△」「×」、つまり間違えた問題だけ解き直せるようになりました!!! やったぜ。
おわりに
今回の1を今朝10時頃に始めて途中脱線しながらも15時10分には4の冒頭までたどり着いているので、なんだかんだ言って多少の時間があれば簡単なシステム自体は形にできるのかな、とも感じた今回です。ちなみに16時12分に4も書き終えました。
最初の頃はreactの癖(useState!?なんだそれは…)なりAPIの取得(そもそもシリアライザーって何?みたいな)で敬遠していたnextjsですが、こんなに簡単に設計できるようになってくると、むしろ大好き手放せないってなってきます。まだISRとか名前だけで全然わからない状態ですが!
レンダリング方法の比較は、もう少しあとかもですね。とりあえず次回で以前Djangoで作成していたウェブアプリのレベルまで持っていき(まだ見た目だけでデータが反映されていないところが多々ある)、そこから今回の大きな特徴「インタラクティブ」(自分たちで問題投稿したり、共有したり、プルリクエストしたり?など)を実装していきます。
ではでは!
この記事が気に入ったらサポートをしてみませんか?