◇はじめに
過去に投稿した、
◇記事投稿順(2019/06/29追記)
今回は、ベースのWEBアプリを1.の記事で作成し、2.以降の記事で機能の追加や改善を行っています。必要に応じてほかの記事も参照ください。
- WEBアプリの勉強を兼ねてDjangoで備忘録登録アプリを作ってみる
- Djangoで作った備忘録登録WEBアプリの高機能化①(タイトル・タグ・本文検索機能の付加)
- 【本記事】Djangoで作った備忘録登録WEBアプリの高機能化②(タグ一覧表示、タグ別記事の追加など)
◇今回追加した機能
今回は以下の項目のうち、タグの管理メニュー、タグ追加後の画面遷移処理修正について実装をおこないました。
- タイトル検索、タグ検索機能の実装(現状、タグが役に立っていない・・)。
-> こちらで実装済み。- タグの管理メニュー(現状追加しかできない・・)。
- 新規タグ追加画面からタグを追加した場合にtopページに飛んでしまうため、その点の修正。
- フォームの改善(forms.pyの作成)。
- CSSフレームワーク(Bootstrapなど)の導入(今回のアプリレベルでcssファイルが雑多になってきたため・・)
⇒フレームワークどーせ入れるならSass(SCSS or SASS)も試してみたい。
タグ管理としては、タグの一覧表示(使用されている記事が多い順に表示)、記事中のタグをクリックするとそのタグが登録されている記事一覧を表示する(Qiitaの記事のイメージ)といった機能を実装していきます。
◇開発環境(前回と同じ)
- OS : Ubuntu 18.04.2 LTS(Windows Subsystem for Linux)
- 言語 : Python 3.6.7
- Webアプリフレームワーク : Django (2.2)
- DB : SQLite3
◇実装内容
今回の記事では、
- タグ一覧表示ページの作成
- タグクリック時のタグ登録記事一覧表示対応
- 新規タグ登録ページの画面遷移処理修正
という流れで記事を書いています。
◆タグ一覧表示ページの作成
トップページ(記事一覧表示)のとき同様、クラスベースビューの中のListView
を使えばそんなに時間をかけずに実装できそうでしたが、今回はあえて、大元のクラスであるView
を使ってタグ一覧表示ページの作成に挑戦しました。
ちなみに、View
クラスはListView
クラスの親クラスであるBaseListView
の親クラスになり、CreateView
クラスやUpdateView
クラスもView
クラスを継承しています(ソースコード追っていったらそうなってた)。
□urls.py
へのルーティング追加
まず、urls.py
にルーティングを追加します。View
クラスでもas_view()
はサポートされているので、今までと同じ書き方です。
---略---
urlpatterns = [
---略---
path("<int:pk>/update/", views.ArticleUpdateView.as_view(), name="update"),
path("<int:pk>/delete/", views.ArticleDeleteView.as_view(), name="delete"),
path("tag/create/", views.TagCreateView.as_view(), name="create_tag"),
path("tag/list/", views.TagListView.as_view(), name="list_tag"), <-追記部分
]
□views.py
へのビュークラス追加
次に、views.py
にTagListView
クラスを定義していきます。
GETリクエスト時の処理をget関数で定義します。get関数では、Tag
モデルからデータを取得していますが、このときに、
- __それぞれ__のタグと紐付いている記事数を集計する。(
annotate( **** Count('article'))
部分) - その集計した値を
post_count
という名前で指定できるようにする。(post_count=Count('article')
部分) - 紐付いている記事が多い順に並びかえる。(
order_by('-post_count')
部分)
といったことを行っています。
post_count
の部分はイマイチ理解しきれていませんが、(自分の解釈では)annotate()
の関数で指定した条件で集計(Count
、Avg
、Max
など)したものを擬似的なフィールドに代入していて、そのフィールド名をpost_count=
で指定しているのかなという理解です。
参考URL:Aggregation
参考URL:Djangoで、集計処理
参考URL:Django データベース操作 についてのまとめ
そして、その取得したデータ(tag_list
)を辞書にいれ、render
でレスポンスを返しています。
参考URL:ショートカット: render()
---略---
class TagListView(generic.View):
model = Tag
def get(self, request, *args, **kwargs):
tag_list = self.model.objects.annotate(post_count=Count('article')).order_by('-post_count')
for tag in tag_list:
print(tag.name)
print(tag.post_count)
context = {
"object_list": tag_list,
}
return render(
request=request,
template_name="memorandum/list-tag.html",
context=context
)
□テンプレートファイル、cssファイルの追加・修正
次に、テンプレートファイル(list-tag.html
)の新規作成とcssセレクタを追加していきます。
構成は今まであまり変わりませんが、取得したモデルのデータリスト(object_list
)の項目をforループで取得してタグ名とタグが登録されている記事数を表示しています。
このとき、記事数については先ほどのpost_count
で指定することができます。
cssについては、追加したものだけ以下に載せます(display: flex;
だらけになってしまったんで、ホントは共通化すべきかも・・・)。
{% extends 'memorandum/base-layout.html' %}
{% block content %}
<ul class="tag-list">
{% for item in object_list %}
<li class="tag-list-item">
<div class="tag-list-text text-reddish">{{ item.name }}</div>
<div class="tag-list-text background-reddish text-whitish small-circle">{{ item.post_count }}</div>
</li>
{% endfor %}
</ul>
{% endblock content %}
---略---
.text-reddish {
color: tomato;
}
.text-whitish {
color: white;
}
.background-reddish {
background: tomato;
}
.background-whitish {
background: white;
}
.tag-list {
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
list-style-type: none;
margin: 5px 0;
padding: 30px 20px;
}
.tag-list-item {
align-items: center;
background: mistyrose;
color: tomato;
display: flex;
flex-wrap: nowrap;
justify-content: center;
margin: 20px 10px;
padding: 5px;
}
.tag-list-text {
align-items: center;
display: flex;
justify-content: center;
height: 25px;
padding: 2px;
}
.small-circle {
border-radius: 50%;
width: 25px;
}
これでタグ一覧表示ページ(list-tag.html
)ができましたので、トップページ(index.html
)にTAG一覧ボタンを追加し、タグ一覧表示ページにアクセスできるようにします。
---略---
{% block content %}
<div class="local-nav">
<ul class="local-nav-list">
<li class="local-nav-item"><a class="local-nav-link-text item-normal" href="{% url 'memorandum:create' %}">新規記事</a></li>
<li class="local-nav-item"><a class="local-nav-link-text item-normal" href="{% url 'memorandum:list_tag' %}">TAG一覧</a></li>
<li class="local-nav-item"><a class="local-nav-link-text item-normal" href="{% url 'memorandum:create_tag' %}">新規TAG</a></li>
---略---
{% endblock content %}
記事数順にタグを表示して、タグ名の後ろに記事数を表示しています。
これで、タグ一覧表示ページができましたので、次にそれぞれのタグをクリックした際に記事一覧を表示する仕組みをつくっていきます。
◆タグクリック時のタグ登録記事一覧表示対応
つづいて、さきほど作成したタグ一覧表示ページ(list-tag.html
)と最初の記事で作成した記事詳細表示ページ(detail.html
)内にあるタグ名をクリックすると、そのタグが登録されている記事の一覧を表示するようにしていきます。
□views.py
の修正
まず、タグ名をクリックした場合の処理の実装です。
今回は、トップページ(記事一覧表示)に処理を追加していく方針としました(クラス名でいうとArticleListView
)。
なお、このクラスには、すでにPOST時の処理はAjax
用の処理が実装されてしまっているので、今回はGET時にパラメータをつけてタグ名を受け取る形としました。
ArticleListView
クラスのget_queryset()
関数をオーバーライドし、まず元(親クラス)のget_queryset()
を実行します。
その後、GETパラメータ内に"tag"
という項目名がある場合、その項目の値でフィルタリングし、その結果を返します。
※GETパラメータ内に"tag"
という項目名がない場合は元の結果を返す(元々のget_queryset()
関数と同じ挙動になる)ため、単純にトップページにアクセスした場合はすべての記事が表示されます
---略---
class ArticleListView(generic.ListView):
model = Article
# 参照するhtmlファイルを指定
template_name = "memorandum/index.html"
---略---
def get_queryset(self):
query_set = super().get_queryset()
if "tag" in self.request.GET:
query_set = query_set.filter(tag__name__icontains=self.request.GET.get("tag"))
print("get_queryset tag={}".format(self.request.GET.get("tag")))
else:
print("get_queryset tag NOT exist")
return query_set
---略---
□list-tag.html
/detail.html
へのa
タグの追加
つづいて、タグ一覧表示ページ(list-tag.html
)と最初の記事で作成した記事詳細表示ページ(detail.html
)それぞれにa
タグを追加して、クリック時にトップページに飛ぶように変更します。
このとき、
href="{% url 'memorandum:index' %}?tag={{ item.name }}"
として、GETパラメータとしてタグ名をつけることでArticleListView
クラスでタグ名によるフィルタリングが行われます。
{% extends 'memorandum/base-layout.html' %}
{% block content %}
<ul class="tag-list">
{% for item in object_list %}
<a class="tag-link-text" href="{% url 'memorandum:index' %}?tag={{ item.name }}">
<li class="tag-list-item">
<div class="tag-list-text text-reddish">{{ item.name }}</div>
<div class="tag-list-text background-reddish text-whitish small-circle">{{ item.post_count }}</div>
</li>
</a>
{% endfor %}
</ul>
{% endblock content %}
{% extends 'memorandum/base-layout.html' %}
{% block content %}
---略---
<h1 class="article-title">{{ object.title }}</h3>
<ul class="article-tag-group">
タグ:
{% for tag in object.tag.all %}
<a class="tag-link-text" href="{% url 'memorandum:index' %}?tag={{ tag }}">
<li>
<div class="article-tag">{{ tag }}</div>
</li>
</a>
{% endfor %}
</ul>
---略---
{% endblock content %}
これで、タグ一覧表示ページ、記事詳細表示ページ中のタグ名をクリックした場合にそのタグを含む記事一覧が表示されるようになりました。
下の画像では、WSL
のタグをクリックしています(わかりにくいですが、:hover
を使ってWSL
の表示が太文字になっています)。
◆新規タグ登録ページの画面遷移処理修正
つづいて、新規タグ登録完了時の画面遷移を改善していきます。
現状では、新規タグ追加画面でタグを追加した場合、ページが遷移してtopページに飛んでしまいます。
そこで、今回はDjango管理サイトのように、タグ追加のボタンを押すとポップアップ画面を表示し、タグを登録するとポップアップ画面を閉じる仕組みにしていきます。
ちなみに、このポップアップ画面の実装方法については、以下のサイトに詳しく書いてあります。私の記事より詳細に説明がありますんで、必要に応じてご覧ください。
参考URL:Django、ポップアップでデータを追加
□タグ追加用ポップアップ画面の実装(common.js
追加)
まず、ボタンクリック時にタグ追加用ポップアップ画面を表示する処理をJavaScript
で書いていきます。
なお、タグ追加のボタンはindex.html
とcreate.html
の2か所あり、common.js
ファイルを新規追加して、共通に使う関数を実装します。
const addNewTagBtnElement = document.getElementById("js-create-tag-btn");
if(addNewTagBtnElement != null)
{
addNewTagBtnElement.addEventListener("click", function() {
console.log("hoge");
window.open("/memorandum/tag/create/", "_blank", "width=640,height=480");
}, false);
}
ID名がjs-create-tag-btn
のエレメントを取得し、その要素がクリックされた際にwindow.open()
関数を呼び出してポップアップ画面を表示しています。
なお、window.open()
関数では色々なオプションが指定できますが、ブラウザによってサポートされていなオプションもあるようなので、使用する際は注意が必要です。
参考URL:window.open - MDN - Mozilla
参考URL:[JavaScript] 最新ブラウザではwindow.openのオプションは動かない
ちなみに、今回追加したcommon.js
は複数のDjangoテンプレートから呼び出す予定のため、js-create-tag-btn
IDが存在しない場合を考慮して、if(addNewTagBtnElement != null)
でヌルチェックをしています。
□テンプレートファイルの修正(index.html
/create.html
)
つぎに、新規TAG追加ボタンが配置されているindex.html
/create.html
に、さきほど定義したjs-create-tag-btn
IDを追加していきます。
また、その際にa
タグからbutton
タグに修正しています。
---略---
{% block content %}
<div class="local-nav">
<ul class="local-nav-list">
<li class="local-nav-item"><a class="local-nav-link-text item-normal" href="{% url 'memorandum:create' %}">新規記事</a></li>
<li class="local-nav-item"><a class="local-nav-link-text item-normal" href="{% url 'memorandum:list_tag' %}">TAG一覧</a></li>
{% comment %} <li class="local-nav-item"><a class="local-nav-link-text item-normal" href="{% url 'memorandum:create_tag' %}">新規TAG</a></li> {% endcomment %}
<li class="local-nav-item"><button class="local-nav-link-text item-normal" id="js-create-tag-btn" type="button">新規TAG</button></li>
<li class="local-search-item">
<select id="js-search-item-list" name="search-item" size="1">
<option value="title">タイトル検索</option>
<option value="tag">タグ検索</option>
<option value="body">本文検索</option>
</select>
<input id="js-search-text" type="search" placeholder="検索ワード">
<button id="js-search-btn" type="button" class="button-common">Search</button>
</li>
</ul>
</div>
---略---
{% endblock content %}
---略---
<div class="input-form-tag-group partition-bottom-line">
<div class="input-form-position">
<span class="input-label-text">{{ form.tag.label_tag }}</span>
<div class="input-form-tag">{{ form.tag }}</div>
</div>
{% comment %} <a class="update-inner-link-text item-normal" href="{% url 'memorandum:create_tag' %}">Add New TAG</a> {% endcomment %}
<button class="update-inner-link-text button-common" id="js-create-tag-btn" type="button">
New TAG
</button>
</div>
---略---
□タグ追加後のポップアップ画面クローズ処理の追加(views.py
/urls.py
)
つづいて、ポップアップ画面でタグの登録が正しく完了した際に、そのポップアップ画面を閉じるようにしていきます。
views.py
では、TagCreateView
クラスの修正と新規関数(close_window()
)の定義をします。
---略---
class TagCreateView(generic.edit.CreateView):
model = Tag
fields = ["name"]
# 参照するhtmlファイルを指定
template_name = "memorandum/create-tag.html"
success_url = reverse_lazy('memorandum:close') <- 修正部分
---略---
def close_window(request):
return render(request=request, template_name="memorandum/close.html")
success_url
に代入する値を修正し、タグ追加完了後に遷移するページを変更します。
(このあと後述するurls.py
側の修正により、)close_window()
関数が呼び出され、memorandum/close.html
が返されます。
なお、TagCreateView
クラス内で直接memorandum/close.html
を返したい場合は、form_valid()
関数をオーバーライドしてrender()
関数を呼び出す必要がありそうです。
※CreateView
クラスのform_valid()
関数はHttpResponseRedirect()
を呼び出しており、この関数は内部的にreverse()
関数を呼び出しているため、URL直書きをするとエラーが発生する
参考URL:はじめての Django アプリ作成、その 4
urls.py
ではさきほどviews.py
で追加したclose_window()
関数へのルーティングが正しくとおるようにpathを追加します。
---略---
app_name = "memorandum"
urlpatterns = [
path("", views.ArticleListView.as_view(), name="index"),
path("create/", views.ArticleCreateView.as_view(), name="create"),
path("<int:pk>/detail/", views.ArticleDetailView.as_view(), name="detail"),
path("<int:pk>/update/", views.ArticleUpdateView.as_view(), name="update"),
path("<int:pk>/delete/", views.ArticleDeleteView.as_view(), name="delete"),
path("tag/create/", views.TagCreateView.as_view(), name="create_tag"),
path("tag/list/", views.TagListView.as_view(), name="list_tag"),
path("close/", views.close_window, name="close"), <- 追記部分
]
□画面クローズ用ファイルの追加(close.html
/close.js
)
ここまでの実装でclose.html
への遷移が行われるようになったので、最後にウィンドウを閉じる処理と元ページをリロード(追加したタグを反映させるため)する処理を実装します。
{% extends 'memorandum/base-layout.html' %}
{% load static %}
{% block head %}
<script src="{% static 'memorandum/js/close.js' %}" charset="UTF-8" defer></script>
{% endblock head %}
{% block content %}
{% endblock content %}
// console.log("hugahuga");
window.opener.location.reload();
window.close();
ソースコード見たまんまですが、close.html
はclose.js
を呼び出すだけです。
close.js
では、新規タグ登録画面を呼び出した元の親画面をリロードした後、自画面を閉じています。
下のキャプチャ画面は、
- 新規記事登録ページから「New TAG」ボタンを押して、新規タグ登録ページのポップアップ画面を表示した状態
- その後、新規タグを登録した後にポップアップ画面がクローズされ、新規記事登録ページに追加したタグが反映されている状態
の画面です。ここでは、testd
というタグが追加されています。
◇おわりに
- 前回の投稿の記事検索機能の実装につづき、今回はタグの一覧表示や追加について実装することができました。
- 備忘録を登録するだけの最低限のWEBアプリですが、これだけでもすでにCSSが煩雑になってきており、もう少しCSS用のクラス名の命名ルールをしっかり決めておく必要があるなーと感じてます。
- それもあり、つぎはCSS部分の実装を少し改善(CSSフレームワークの導入 or SASSの導入)していければと思います(まずはドットインストールの「Sass/SCSS入門」を見るところからかな・・)。