プライマリキーについて
重複がある可能性があるフィールドに設定してはいけない!
Djangoで普通にmodelを作成すると「id」がプライマリキーになる(特に意識してなかった)
primary_key=True
にするとその設定したカラムがプライマリキーになりidは生成されなくなる
Djangoには複合主キー設定はできず、unique_togetherは「ユニーク制約」。あくまで制約らしい。
設定の仕方としては
user_id = models.AutoField(primary_key=True)
選択肢をもたせたモデルの作成
class User(AbstractBaseUser,PermissionsMixin):
LANG_TYPES = (
('man', '男性'),
('woman', '女性'),
('no_answer', '無回答'),
)
gender=models.CharField(max_length=50,choices=LANG_TYPES,default='no_answer')
新規登録とトークンを発行してメールアドレス認証を行う
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
from django.urls import reverse_lazy
from django.conf import settings
#トークン生成用
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode,urlsafe_base64_decode
from django.core.mail import send_mail
User = get_user_model()
class UserLoginForm(forms.Form):
email = forms.EmailField(label='メールアドレス')
password= forms.CharField(label='パスワード',widget=forms.PasswordInput)
#新規登録用
subject="仮登録メール"
message_template="""
仮登録ありがとうございます。
以下URLをクリックして登録を完了してください。
"""
#URLを生成する関数
def get_activate_url(user):
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = default_token_generator.make_token(user)
return settings.FRONTEND_URL + "accounts/activate/{}/{}".format(uid,token)
class SignUpForm(UserCreationForm):
username=forms.CharField(label='ニックネーム')
email=forms.EmailField(label='メールアドレス')
class Meta:
model = User
fields=('username','email','password1','password2')
def save(self,commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
#POSTされたらget_activate_url関数で作られたURLをメッセージにくっつけて送信
if commit:
user.save()
activate_url = get_activate_url(user)
message = message_template + activate_url
from_email=settings.DEFAULT_FROM_EMAIL
# user.email_user(subject,message)
send_mail(subject,message,from_email,[user.email])
return user
#ユーザを有効化する関数
def activate_user(uidb64,token):
try:
uid = urlsafe_base64_decode(uidb64).decode() #uidを復元
user = User.objects.get(pk=uid) #userを見つける
except Exception:
return False
#tokenのチェックもOKならis_activeをTrueにする
if default_token_generator.check_token(user,token):
user.is_active =True
user.save()
return True
return False
#新規登録用
class SignUpView(CreateView):
form_class=SignUpForm
success_url=reverse_lazy('accounts:user_login')
template_name='signup.html'
class ActivateView(TemplateView):
template_name='activate.html'
def get(self,request,uidb64,token,*args,**kwargs): #URLのuidb64とtokenを取得,それらをactivate_user関数に渡す
result = activate_user(uidb64,token) #正しく行われればresultにはTrueがはいる
return super().get(request,result=result,**kwargs) #ここでのgetはTemplateViewのget。コンテキストとしてresultをテンプレートに渡す
urlpatterns=[
path('activate/<uidb64>/<token>',ActivateView.as_view(),name='activate'),
]
{% extends 'base.html'%}
{% block content%}
<h2>アクティベート画面</h2>
{% if result %}
<p>メール認証が成功しました</p>
<p>ようこそ!さっそくログインしましょう</p>
<p><a href = "{% url 'accounts:user_login'%}">ログイン</a></p>
{% else %}
<p>無効なリンクです</p>
<p>もう一度最初から登録してください</p>
{% endif %}
{% endblock %}
クラスを用いて認証する方法もみつけたので引用元をのせておく
仮登録した後、メールから本登録させる(Django)
パスワードリセットを実装する
Python + Djangoでパスワードリセット画面を実装する
こちらを参考にPasswordResetViewなどを継承したクラスで作成した
3-7. PasswordResetViewを使用してパスワードリセット画面を作成する
メールの記載内容などを変更するためにこちらのHPを参考に編集した
ログイン中のユーザ情報を取得
#マイページ
class MypageView(TemplateView):
template_name='mypage.html'
def get(self,request,**kwargs):
context={
'user':self.request.user
}
return self.render_to_response(context)
テンプレート先でuser.usernameのように使う
ユーザ情報を更新する
pictureフィールドで使うデフォルトのFileFieldでは「クリア」機能と現在の画像のパスが表示されてしまったのでそれを表示しなくするためにwidget=forms.FileInputを追加。
このままでは画像はアップロードする機能はあるが、空白をクリックする形になってしまったのでuser_edit.htmlを編集。
classというのはBootstrapのクラスっぽくそれをuser_edit.htmlに適応する。
#ユーザ情報を更新するフォーム
class UserEditForm(forms.ModelForm):
LANG_TYPES = (
('man', '男性'),
('woman', '女性'),
('no_answer', '無回答'),
)
username=forms.CharField(label='ニックネーム',max_length=30)
gender=forms.ChoiceField(label='性別',choices=LANG_TYPES)
comment=forms.CharField(label='コメント',required=False,widget=forms.Textarea(attrs={
'placeholder':'取引しやすくなるように自己紹介文を書こう'
}))
picture=forms.FileField(label='プロフィール画像',required=False,widget=forms.FileInput(attrs={'class':'custom-file-input'}),)
class Meta:
model =User
fields=('username','gender','comment','picture')
#ユーザ情報更新する
@login_required
def user_edit(request):
user_edit_form=UserEditForm(request.POST or None,request.FILES or None,instance=request.user)
if user_edit_form.is_valid():
messages.success(request,'更新完了しました')
user_edit_form.save()
return redirect('accounts:mypage')
return render(request,'user_edit.html',context={
'user_edit_form':user_edit_form
})
フォームで画像を更新するためにはenctype='multipart/form-data'
を忘れない
{% extends 'base.html'%}
{% block content%}
<h2>ここはユーザ情報更新画面です</h2>
<form method='POST' enctype='multipart/form-data'>
{% csrf_token %}
{{ form.non_field_errors }}
<table>
{% for field in user_edit_form %}
<tr>
<td>{{ field.label }}</td>
<td>
{% if field.name == 'picture' %}
<div class='custom-file'>
{{ field }}
<label class="custom-file-label" for="{{ field.id_for_label }}">Choose file</label>
</div>
{% else %}
{{field}}
{% endif %}
</td>
<td>{{ field.errors }}</td>
</tr>
{% endfor %}
</table>
<input type="submit" value='更新する'>
</form>
{% endblock %}
複数のフォームをを表示して一括で複数のテーブルを更新する(新規と更新を対応,外部キーをもったテーブルに対応)
#ユーザ情報を更新するフォーム
class UserEditForm(forms.ModelForm):
LANG_TYPES = (
('man', '男性'),
('woman', '女性'),
('no_answer', '無回答'),
)
username=forms.CharField(label='ニックネーム',max_length=30)
gender=forms.ChoiceField(label='性別',choices=LANG_TYPES)
comment=forms.CharField(label='コメント',required=False,widget=forms.Textarea(attrs={
'placeholder':'取引しやすくなるように自己紹介文を書こう'
}))
picture=forms.FileField(label='プロフィール画像',required=False,widget=forms.FileInput(attrs={'class':'custom-file-input'}),)
class Meta:
model =User
fields=('username','gender','comment','picture')
#住所更新フォーム
class UserAddressEditForm(forms.ModelForm):
CITY_NAMES=(
('池田市','池田市'),
('泉大津市','泉大津市'),
('泉佐野市','泉佐野市'),
('和泉市','和泉市'),
('茨木市','茨木市'),
('大阪狭山市','大阪狭山市'),
('大阪市','大阪市'),
('貝塚市','貝塚市'),
('柏原市','柏原市'),
('交野市','交野市'),
('河南町','河南町'),
('河内長野市','河内長野市'),
('岸和田市','岸和田市'),
('熊取町','熊取町'),
('堺市','堺市'),
('四條畷市','四條畷市'),
('島本町','島本町'),
('吹田市','吹田市'),
('摂津市','摂津市'),
('泉南市','泉南市'),
('太子町','太子町'),
('大東市','大東市'),
('高石市','高石市'),
('高槻市','高槻市'),
('田尻町','田尻町'),
('忠岡町','忠岡町'),
('千早赤阪村','千早赤阪村'),
('豊中市','豊中市'),
('豊能町','豊能町'),
('富田林市','富田林市'),
('寝屋川市','寝屋川市'),
('能勢町','能勢町'),
('羽曳野市','羽曳野市'),
('阪南市','阪南市'),
('東大阪市','東大阪市'),
('枚方市','枚方市'),
('藤井寺市','藤井寺市'),
('松原市','松原市'),
('岬町','岬町'),
('箕面市','箕面市'),
('守口市','守口市'),
('八尾市','八尾市'),
)
prefecture = forms.CharField(label='都道府県',max_length=10,disabled=True,initial='大阪府')
city= forms.ChoiceField(label='市町村',choices=CITY_NAMES)
address = forms.CharField(label='その他住所',max_length=30)
class Meta:
model=Addresses
fields=('prefecture','city','address')
#ユーザ情報更新する
@login_required
def user_edit(request):
user_edit_form=UserEditForm(request.POST or None,request.FILES or None,instance=request.user)
#Addressesが新規作成なのか更新なのかで分岐させる
try:
addresses=request.user.addresses
except Addresses.DoesNotExist:
addresses = None
if addresses: #Addressesが存在すれば取得してフォームにいれる
user_address_edit_form=UserAddressEditForm(request.POST or None,instance=addresses) #instanceにログインしているユーザのaddressesを指定
else: #AddressesがなければAddressesは取得しない
user_address_edit_form=UserAddressEditForm(request.POST or None)
if user_edit_form.is_valid() and user_address_edit_form.is_valid():
user_edit_form.save()
if addresses: #住所更新の場合
user_address_edit_form.save()
else: #住所新規作成の場合
user_address_edit_form.save(commit=False).user=request.user #Addressesのuserにログインしているユーザをいれてから
user_address_edit_form.save() #user,prefecture,city,addressが揃ったので保存できる
return redirect('accounts:mypage')
else:
messages.error(request,'更新に問題があります')
return render(request,'user_edit.html',context={
'user_edit_form':user_edit_form,
'user_address_edit_form':user_address_edit_form
})
ManyToManyの頭の中整理とお役立ち記事
manytomany頭整理
本は著者を複数持っている
著者も複数本を持っている?☜ここがなぞ
foreignkeyはitemsはいっぱいあるが、あくまでuserを1人だけ
持ちたい時に使う
manytomanyはitemsもいっぱいあるが
そのitemもuserをいっぱい持ちたい時に使う
ManyToManyを設定しなかった側からも逆参照なんかができる
このあたりで実装できないか?
Djangoの多対多関係モデルで簡易タグ機能を作る
↓こちら図解がめっちゃわかりやすくて助かった
やさしい図解で学ぶ 中間テーブル 多対多 概念編
↓同じ人のER図解説についてもわかりやすそうだったのでのせておく
やさしい図解で学ぶ ER図 表記法一覧
related_nameによって呼び出すテーブルの重複を避ける
ForeignKeyなどで複数のカラムで同じ親を参照していた場合に
Djangoがどちらから呼び出せばいいか迷ってしまうのを避けるために
related_nameを設定することでそれに応じた項目を呼び出すことができるらしい。
いい記事を見つけたのでメモ
related_nameの存在意義とは?
あとで使えそうなのでメモ
Djangoで複数ファイル(画像)のプレビューとアップロード
ForeignKeyとManyToManyでどちらを使えばいいか結構悩んだが
ItemsとItemPictureを並べてみたとき
ItemsからManyToManyで複数参照できるようにするか(下の画像はFavorited_byはManyToMany)
もしくはItemPicturesからItemへForeignKeyでつなげて多対1の関係にするか。
ImageFeildは単一画像を保存するカラムだが、こちらを複数登録する形。
触ってみた感じとなんとなくでItemsからManyToManyで繋げる方がわかりやすいかんじはするけど、
今回アップロードする画像はそのItemsにしか由来しないのでItemPicturesから多をだすのは意味がない気がしてItemPictureから多対1になるようにしてみたけどどうなんだろう。
参照と逆参照についてまとめる
よくわかっていなかったのでまとめる
○参照
ある子モデルがどの親モデルに紐づいているか確認する
→子モデル中のForeignKeyから、親モデルの情報を取得する
○逆参照
ある親モデルにはどの子モデルが紐づいているか確認する
→親モデルから、ForeignKeyで自身を参照している子モデルの情報を取得する
こちらから引用させてもらいました
【Django】親モデルから子モデルへ逆参照する方法【prefetch_related】
今回は親Itemsから子ItemPicturesへ逆参照したかったのでこちらの方法を使って↓
Djangoのテーブル間リレーションシップを理解する
逆参照によってItemPicturesのpictureをとりだすことができた
{% for object in object_list%}
<table style = border:solid;>
<tr>
<td>商品名</td>
<td><a href="{% url 'boards:item_detail' object.item_id %}">{{object.name}}</a></td>
</tr>
<tr>
<td>地域</td>
<td>{{object.user.addresses.city}}</td>
</tr>
<tr>
<td>商品写真</td>
<td>
{% for picture in object.itempictures_set.all %} #ここがポイント
<img width=100px; height=100px; src="{{picture.picture.url}}">
{% endfor %}
</td>
</tr>
</table>
<br>
{% endfor %}
複数のフォームを送信時、ForeignKeyなどの外部キーインスタンスを保存したい場合と画像の複数アップロード
かなり沼ってしまったのでメモ。
前提としてImageFieldは単一の画像しか保存できない。そこで
上で記述したManyToManyではなくItemsとItemPicturesが1対多となるようにItemPictures側でForeignKeyを設定している。models.pyを下に記述しておく
from django.db import models
from accounts.models import User
class Items(models.Model):
CATEGORY_NAME=(
('家具','家具'),
('家電','家電'),
('自転車','自転車'),
('楽器','楽器'),
('生活雑貨','生活雑貨'),
('子供用品','子供用品'),
('ホビー、ゲーム','ホビー、ゲーム'),
('服/ファッション','服/ファッション'),
('靴/バック','靴/バック'),
('コスメ/ヘルスケア','コスメ/ヘルスケア'),
('食品','食品'),
('スポーツ','スポーツ'),
('その他','その他'),
)
item_id = models.AutoField(primary_key=True)
user = models.ForeignKey(User,on_delete=models.CASCADE)
name = models.CharField(max_length=100)
comment = models.TextField(max_length=1000)
favorited_by = models.ManyToManyField(User,related_name='favorite_items',blank=True)
category = models.CharField(max_length=30,choices=CATEGORY_NAME)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = '出品'
verbose_name_plural = '出品一覧'
def __str__(self):
return self.name
class ItemPictures(models.Model):
item_picture_id = models.AutoField(primary_key=True)
user = models.ForeignKey(User,on_delete=models.CASCADE)
item = models.ForeignKey(Items,on_delete=models.CASCADE)
picture = models.ImageField(upload_to='item_pictures/')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = '出品写真'
verbose_name_plural = '出品写真一覧'
def __str__(self):
return str(self.item.name)
保存に成功したItemPicturesのテーブルを貼っておく。
要はitem_idが共通で(同じアイテムで)、item_picture_idが別に存在しているという状況(違う画像)
forms.pyではItemsとItemPicturesと二つのモデルなのでそれぞれのModelFormを継承したフォームを作成して、views.pyで二つのフォームを表示するという方法をとる。ポイントは複数画像を選択可能にするため'multiple':'True'とすること。
class ItemPicturesCreateForm(forms.ModelForm):
picture = forms.ImageField(label='商品画像',
widget=forms.FileInput(attrs={'multiple':'True'}),)
forms.py全体のコードも載せておく
from django import forms
from .models import Items,ItemPictures
#商品新規投稿フォーム1
class ItemCreateForm(forms.ModelForm):
CATEGORY_NAME=(
('家具','家具'),
('家電','家電'),
('自転車','自転車'),
('楽器','楽器'),
('生活雑貨','生活雑貨'),
('子供用品','子供用品'),
('ホビー、ゲーム','ホビー、ゲーム'),
('服/ファッション','服/ファッション'),
('靴/バック','靴/バック'),
('コスメ/ヘルスケア','コスメ/ヘルスケア'),
('食品','食品'),
('スポーツ','スポーツ'),
('その他','その他'),
)
name = forms.CharField(label='商品名',max_length=100)
comment = forms.CharField(
label='コメント',
max_length=1000,
widget=forms.Textarea(
attrs={
'placeholder':'魅力的な商品説明を書こう!',
}
)
)
category = forms.ChoiceField(label='カテゴリー',choices=CATEGORY_NAME)
class Meta:
model = Items
fields = ('name','comment','category',)
#商品新規投稿フォーム2
class ItemPicturesCreateForm(forms.ModelForm):
picture = forms.ImageField(label='商品画像',
widget=forms.FileInput(attrs={'multiple':'True'}),)
class Meta:
model = ItemPictures
fields = ('picture',)
上記のフォームをviews.pyで使っていく。まず全体のコードを載せておく
#商品投稿用(画像複数アップお試し)
def item_create(request):
item_create_form = ItemCreateForm(request.POST or None)
item_picture_create_form = ItemPicturesCreateForm(request.POST or None, request.FILES or None)
if item_create_form.is_valid() and item_picture_create_form.is_valid():
#Itemsのuserフィールドにrequest.userを入れて保存
item_instance = item_create_form.save(commit=False)
item_instance.user = request.user
item_instance.save()
#ItemPicturesに各フィールドをいれる
upload_pictures = request.FILES.getlist('picture',) #複数あげられた画像をリストとして取得
for picture in upload_pictures:
picture_instance = ItemPictures(
picture = picture,
#他のフィールドをいれる?
user = request.user,
item = item_instance
)
picture_instance.save()
return redirect('accounts:mypage')
context = {
'item_create_form':item_create_form,
'item_picture_create_form':item_picture_create_form
}
return render(request,'item_create.html',context)
解説
item_create_form = ItemCreateForm(request.POST or None)
item_picture_create_form = ItemPicturesCreateForm(request.POST or None, request.FILES or None)
if item_create_form.is_valid() and item_picture_create_form.is_valid():
1.ItemCreateFormとItemPicturesCreateFormでフォームのインスタンス化をする。
フォーム入力のバリデーションが両方ただしく終われば下記を実行。
#Itemsのuserフィールドにrequest.userを入れて保存
item_instance = item_create_form.save(commit=False)
item_instance.user = request.user
item_instance.save()
2.Itemsにはuserという外部キーをもったフィールドがあるのでそれをいれてあげないとNOTNULL制約に違反してしまうので、ログインしているユーザをいれる
#ItemPicturesに各フィールドをいれる
upload_pictures = request.FILES.getlist('picture',) #複数あげられた画像をリストとして取得
for picture in upload_pictures:
picture_instance = ItemPictures(
picture = picture,
#他のフィールドをいれる?
user = request.user,
item = item_instance
)
picture_instance.save()
return redirect('accounts:mypage')
3.ここが今回沼ったところ。
getlistはあげられた画像をリストとしてまとめる
upload_picturesの中には複数選択した画像がリストになっているのでそれらをfor文でまわし、ItemPicturesモデルに一つ一ついれていく。その際にItemPicturesモデルには外部キーがuserとitemが存在しているのでそれらをいれてあげるのだけどitemにはitem_create_form.save(commit=False)としてインスタンス化したitem_instanceをいれてあげることで外部キーを入れることが可能となる
今回参考にさせていただいたページDjangoで複数ファイル(画像)のプレビューとアップロード
URLパラメータを取得する
基本的なことかもしれないけど新しい発見だったのでメモしておく
#問い合わせページ
class ContactFormView(CreateView):
model = Contact
template_name = 'item_contact.html'
form_class = ContactForm
success_url = 'item_contact_result.html'
def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
try:
#URLパラメータからitem_idを取得する
item_id = self.kwargs.get('item_id')
except Items.DoesNotExist:
item_id=None
#item_idを使ってそのItemsのインスタンスを取得
item =Items.objects.get(item_id=item_id) #ここ少し詰まった。解説は後述
context['user']=self.request.user
context['item']=item
return context
get_context_dataはテンプレートに変数を渡す。
今回はURLパラメータにitem_idを持たせて遷移させているのでそれを
item_id = self.kwargs.get('item_id')
で取得する。それを使ってItemsモデルから対象のデータを取得してテンプレートに渡す
ひとつだけつまったことがある
item =Items.objects.get(item_id=item_id)
最初getではなくfilterで取得していたのでテンプレート先で{{item.name}}のようにしても表示できなかった。これはなぜかというとfilterでとってくるとQueryset型というDjango特有の取り出し方をするためであるようであった。一応filterで撮ってくる場合でも
items = Items.objects.filter(item_id=item_id)
if items.exists():
item = items.first() # クエリセットから最初のアイテムを取得
else:
item = None
context['item']=item
とすると問題ないらしい。要するにgetとfilterでは出力の型が違うということ
【django】QuerySetのfilterの使い方まとめ
検索窓の実装
参考にさせてもらったHP【Django】検索機能が実装できる2つの方法|オススメは?
上記のHPを参考に検索用のItemsモデルマネージャーとクエリセットようのモデルを作ってモデルマネージャーに組み込んでそれをviewsでItems.object.search使うという方法(searchは関数名)がとれた。下にはじめに作った方のコードを載せるが、2つ目に作った方のコードの方がviews.pyに直接絞り込みの条件をかけるからそっちのほうがややこしくないかもしれない
#検索窓クエリセット用モデル
class ItemsQuerySet(models.QuerySet):
def search(self,query=None):
qs = self #object.allとかそれらを含む
if query is not None:
or_lookup = ( #qオブジェクトを作成(なにで検索可能にするか)
Q(name__icontains=query)|
Q(comment__icontains=query)|
Q(category__icontains=query)|
Q(user__addresses__city__icontains=query)|
Q(user__addresses__address__icontains=query)
)
qs=qs.filter(or_lookup).distinct()#重複を除く
return qs.order_by("-created_at")
#検索窓用Itemsモデルマネージャー
class ItemsModelManager(models.Manager):
def get_queryset(self):
return ItemsQuerySet(self.model,using=self._db)
def search(self,query=None):
return self.get_queryset().search(query=query)
class TopView(ListView):
model = Items
template_name='top.html'
# context_object_name='items' #top.htmlでobject_listでではくitemsでfor文を回すことができるようになる
paginate_by=5
def get_context_data(self,**kwargs):
context= super().get_context_data(**kwargs)
context['form']=ItemSearchForm()
return context
def get_queryset(self):
qs = super(TopView,self).get_queryset()
form = ItemSearchForm(self.request.GET)
if form.is_valid():
search_query = form.cleaned_data['search']
category_filter = form.cleaned_data['category']
if search_query:
qs = Items.objects.search(query=search_query) #モデルマネージャの関数
elif search_query == None:
qs = Items.objects.all()
if category_filter:
qs = qs.filter(category=category_filter)
return qs.order_by("-created_at")
#検索窓フォーム
class ItemSearchForm(forms.Form):
CATEGORY_NAME=[
('家具','家具'),
('家電','家電'),
('自転車','自転車'),
('楽器','楽器'),
('生活雑貨','生活雑貨'),
('子供用品','子供用品'),
('ホビー、ゲーム','ホビー、ゲーム'),
('服/ファッション','服/ファッション'),
('靴/バック','靴/バック'),
('コスメ/ヘルスケア','コスメ/ヘルスケア'),
('食品','食品'),
('スポーツ','スポーツ'),
('その他','その他'),
]
search = forms.CharField(required=False,
widget=forms.TextInput(
attrs={'placeholder':'フリーワード検索'}))
category = forms.ChoiceField(
required=False,
choices=[('','全てのカテゴリー')] + CATEGORY_NAME
)
views.pyに直接絞り込み条件をかいたほう(こっちのほうがわかりやすいかも)
#class TopViewなど省略
def get_queryset(self):
qs = super(TopView, self).get_queryset()
form = ItemSearchForm(self.request.GET)
if form.is_valid():
search_query = form.cleaned_data['search']
category_filter = form.cleaned_data['category']
if search_query:
qs = qs.filter(
Q(name__icontains=search_query) |
Q(comment__icontains=search_query) |
Q(user__addresses__city__icontains=search_query)
).distinct()
if category_filter:
qs = qs.filter(category=category_filter)
return qs.order_by('-created_at')
検索窓とページネーションの実装
検索してページネーションすると検索結果が外れてしまうのでここの実装でかなり沼ったのでメモしておく
沼った原因は色々あるけどsetting.pyにページネーション用のカスタムテンプレートタグを追加するのを知らなかったのでこちらを参考に追加Djangoでページネーションを実装する方法【django.core.paginator】【パラメータ両立】
ListViewを継承したクラスでget_context_dataでフォームをわたし、get_querysetでフォームがis_validしたさいにqsを絞り込んでqsを返すという実装をする。
基本はこちらのHPを参考にしたDjangoで作る家計簿アプリ ③検索とページネーションの実装
今回models.pyでモデルマネージャーとクエリセット用のモデルを用意したけどややこしくなってしまったので初めからviews.pyでQオブジェクトをつかってしぼりこむほうがよかったかもしれない。いちおうコードは載せておく
Qオブジェクトやフィルタメゾットについてはこちらが勉強になった【Django】検索機能が実装できる2つの方法|オススメは?
#検索窓用モデル
class ItemsQuerySet(models.QuerySet):
def search(self,query=None):
qs = self #object.allとかそれらを含む
if query is not None:
or_lookup = ( #qオブジェクトを作成(なにで検索可能にするか)
Q(name__icontains=query)|
Q(comment__icontains=query)|
Q(category__icontains=query)|
Q(user__addresses__city__icontains=query)|
Q(user__addresses__address__icontains=query)
)
qs=qs.filter(or_lookup).distinct()#重複を除く
return qs.order_by("-created_at")
#検索窓用Itemsモデルマネージャー
class ItemsModelManager(models.Manager):
def get_queryset(self):
return ItemsQuerySet(self.model,using=self._db)
def search(self,query=None):
return self.get_queryset().search(query=query)
これをviews.pyでsearchメゾットとして使うわけだけどややこしくなった
class TopView(ListView):
model = Items
template_name = 'top.html'
paginate_by = 5
def get_queryset(self):
# ListViewのデフォルトのget_querysetを呼び出し、クエリセットを取得
qs = super().get_queryset()
self.form = form = ItemSearchForm(self.request.GET or None)
if form.is_valid():
search_query = form.cleaned_data['search']
category_filter = form.cleaned_data['category']
#フリーワードの空白を区切り、順番に絞るand検索
if search_query:
for word in search_query.split():
qs = Items.objects.search(query=word) #searchはmodels.pyで定義したメゾットのことで、こちらでQオブジェクトを使って絞り込む方がわかりやすかもしれないとおもった
#カテゴリがあればさらに絞り込む
if category_filter:
qs = qs.filter(category=category_filter)
return qs
def get_context_data(self,**kwargs):
context= super().get_context_data(**kwargs)
context['form']=self.form
return context
定義したフォームはこちら
#検索窓フォーム
class ItemSearchForm(forms.Form):
CATEGORY_NAME=[
('家具','家具'),
('家電','家電'),
('自転車','自転車'),
('楽器','楽器'),
('生活雑貨','生活雑貨'),
('子供用品','子供用品'),
('ホビー、ゲーム','ホビー、ゲーム'),
('服/ファッション','服/ファッション'),
('靴/バック','靴/バック'),
('コスメ/ヘルスケア','コスメ/ヘルスケア'),
('食品','食品'),
('スポーツ','スポーツ'),
('その他','その他'),
]
search = forms.CharField(required=False,
widget=forms.TextInput(
attrs={'placeholder':'フリーワード検索'}))
category = forms.ChoiceField(
required=False,
choices=[('','全てのカテゴリー')] + CATEGORY_NAME
)
テンプレートはこちら
{#検索窓#}
<form method="get" action="">
{{ form.search }}
{{ form.category }}
<button class="btn btn-primary" type="submit">検索</button>
</form>
{#検索リセットボタン#}
<form method="get" action="{% url 'boards:top'%}">
<button class="btn btn-primary" type="submit">検索条件をリセット</button>
</form>
ここからページネーションの実装
boardsアプリ直下にtemplatetagsというディレクトリを用意する(名前は間違ってはいけない)
templatetagsには二つのファイルを用意する
・__init__.py(中身の記入はなし)
・paginate.py(名前は自由。こちらにカスタムテンプレートタグをつくる)
Djangoで作る家計簿アプリ ③検索とページネーションの実装
こちらのとおりにpaginate.pyにかきこんでいく
from django import template
register = template.Library()
@register.simple_tag
def url_replace(request, field, value):
"""
GETパラメータの一部を置き換える。
"""
url_dict = request.GET.copy()
url_dict[field] = str(value)
return url_dict.urlencode()
top.htmlはこちら
{% load paginate%} #先頭でpaginate.pyを読み込む
#省略
{# ページネーション #}
<nav aria-label="Page navigation">
<ul class="pagination text-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% url_replace request 'page' page_obj.previous_page_number %}" aria-label="Previous">
<span aria-hidden="true">«</span>
<span class="sr-only">前へ</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active"><a class="page-link" href="?{% url_replace request 'page' num %}">{{ num }}</a></li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item"><a class="page-link" href="?{% url_replace request 'page' num %}">{{ num }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% url_replace request 'page' page_obj.next_page_number %}" aria-label="Next">
<span aria-hidden="true">»</span>
<span class="sr-only">次へ</span>
</a>
</li>
{% endif %}
</ul>
</nav>
a href ="?{%url_replace ~~~ %}" 部を設定してあげることでカスタムテンプレートタグをつかうことができる
データ件数を取得して表示
cityごとに何件あるかをTopViewにget_context_dataでわたしたかった。コードは下記のように書く
def get_context_data(self,**kwargs):
context= super().get_context_data(**kwargs)
#cityの件数を取得
city_items_counts = Addresses.objects.values('city').annotate(item_count=Count('user__items')).order_by('city')
context['city_items_counts']=city_items_counts
return context
values、annotateについてわからなかったのでしらべてみた
そもそもobjectsはモデルマネージャ。.values()や.all()なんかはメゾットというらしい
そのあたり全然知らなかったのでこちらが勉強になった
Django objectsの使い方を簡単解説
.valuesというのは辞書型でとるメゾットらしい
.annotateとはテンプレートで使うエイリアスをつけるメゾット
.annotate(item_count=Count('user__items))とは
テンプレートにてfor文を回したときにitem_countという名ででとりだせる
'user__items'とはAddressesからuserに参照してitemsへ逆参照しているという意味
テンプレートでエイリアスでとりだすとはこういうこと
{% for city_item_count in city_items_counts %}
<p>{{city_item_count.city}}({{city_item_count.item_count}}件)</p>
{% endfor%}
#city_item_countで渡されたコンテキストのitem_countという名前で件数が入ってる
Ajaxを使ったのでメモ
Ajaxを使って市町村名をクリックしたときにそれらのItemsをとってきて非同期で表示するコードができたけど、思っていたかんじにならなそうなのでAjaxが実装できた記録として残しておく
<ul>
{% for city_count in city_items_counts %}
<li><a class="city-link" href="#" data-city="{{ city_count.city }}">{{ city_count.city }} ({{ city_count.item_count }})</a></li>
{% endfor %}
</ul>
↑ここで重要なのはclassとdata-〇〇の設定だけ
script内でajaxの設定をする
{% block javascript %}
<script>
$(document).ready(function(){
//city-linkクラスを持つリンクがクリックされたとき
$(".city-link").click(function (event){
event.preventDefault();
//クリックされたcityのデータ属性を取得
var selectedCity = $(this).data("city");
//Ajaxリクエストを送信
$.ajax({
type:"GET",
url:"{% url 'boards:ajax_selected_city'%}", //Ajaxエンドポイント
data:{city:selectedCity}, //選択されたcityを送信
success:function(data){
//結果を受け取り、表示エリアを更新
$("#result-area").html(data);
}
});
});
});
</script>
{% endblock%}
コードでなにをしているかはコメントを参照
#result-areaはテンプレートで
次にAjaxリクエストを処理する「エンドポイント」をviewとして作成
エンドポイントがわからなかったのでこちらで勉強Ajax(非同期通信)の基礎知識
【$.ajax】誰でも分かる!ajaxでAPIを実装する
#ajaxで市町村を絞り込む
def ajax_selected_city(request):
selected_city = request.GET.get('city') #cityにはvar selectedCityがゲット入ってくる?
#選択されたcityでItemsを絞り込む
filtered_items = Items.objects.filter(user__addresses__city=selected_city)
return render(request,'filtered_items.html',{'filtered_items':filtered_items})
request.GET.get('city')で取得しているのはdataで送られたcity:selectedCityかな?
filtered_items.htmlで表示したいhtmlを書く
{% for item in filtered_items %}
<p>{{item.user.addresses.city}}の出品</p>
<table style = border:solid;>
<tr>
<td>商品名</td>
<td><a href="{% url 'boards:item_detail' item.item_id %}">{{item.name}}</a></td>
</tr>
<tr>
<td>カテゴリー</td>
<td>{{item.category}}</td>
</tr>
<tr>
<td>地域</td>
{% if item.user.addresses.city %}
<td>{{item.user.addresses.city}}</td>
{% else %}
<td>未登録</td>
{% endif %}
</tr>
<tr>
<td>商品写真</td>
<td>
{% for picture in item.itempictures_set.all %}
<img width=100px; height=100px; src="{{picture.picture.url}}">
{% endfor %}
</td>
</tr>
</table>
<br>
{% empty %}
<p>出品がありません</p>
{% endfor %}
top.htmlには描画エリアを示唆する
<div id="result-area">
<!-- 絞り込まれたItemsがここに表示されます -->
</div>
画像クリックで簡単に拡大する方法
リンク
こちらで作成できた。感謝