0
2

ポートフォリオ作成時学んだことまとめ

Last updated at Posted at 2023-09-20

プライマリキーについて

重複がある可能性があるフィールドに設定してはいけない!

Djangoでmodelのプライマリキーを設定する

Djangoで普通にmodelを作成すると「id」がプライマリキーになる(特に意識してなかった)

primary_key=True

にするとその設定したカラムがプライマリキーになりidは生成されなくなる

Djangoには複合主キー設定はできず、unique_togetherは「ユニーク制約」。あくまで制約らしい。

設定の仕方としては

models.py
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')

新規登録とトークンを発行してメールアドレス認証を行う

forms.py
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
views.py
#新規登録用
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をテンプレートに渡す
 
urls.py
urlpatterns=[
    path('activate/<uidb64>/<token>',ActivateView.as_view(),name='activate'),
]
activate.html
{% 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に適応する。

forms.py
#ユーザ情報を更新するフォーム
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')
views.py
#ユーザ情報更新する
@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'
を忘れない

user_edit.html
{% 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 %}

複数のフォームをを表示して一括で複数のテーブルを更新する(新規と更新を対応,外部キーをもったテーブルに対応)

forms.py
#ユーザ情報を更新するフォーム
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')
views.py
#ユーザ情報更新する
@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)
スクリーンショット 2023-09-27 20.27.11.png
もしくはItemPicturesからItemへForeignKeyでつなげて多対1の関係にするか。スクリーンショット 2023-09-27 20.28.53.png
ImageFeildは単一画像を保存するカラムだが、こちらを複数登録する形。
スクリーンショット 2023-09-27 20.29.00.png

触ってみた感じとなんとなくでItemsからManyToManyで繋げる方がわかりやすいかんじはするけど、
今回アップロードする画像はそのItemsにしか由来しないのでItemPicturesから多をだすのは意味がない気がしてItemPictureから多対1になるようにしてみたけどどうなんだろう。

参照と逆参照についてまとめる

よくわかっていなかったのでまとめる
○参照
ある子モデルがどの親モデルに紐づいているか確認する
→子モデル中のForeignKeyから、親モデルの情報を取得する

○逆参照
ある親モデルにはどの子モデルが紐づいているか確認する
→親モデルから、ForeignKeyで自身を参照している子モデルの情報を取得する

こちらから引用させてもらいました
【Django】親モデルから子モデルへ逆参照する方法【prefetch_related】

今回は親Itemsから子ItemPicturesへ逆参照したかったのでこちらの方法を使って↓
Djangoのテーブル間リレーションシップを理解する

逆参照によってItemPicturesのpictureをとりだすことができた

top.html
{% 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を下に記述しておく

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が別に存在しているという状況(違う画像)
スクリーンショット 2023-10-02 19.52.12.png

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全体のコードも載せておく

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で使っていく。まず全体のコードを載せておく

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パラメータを取得する

基本的なことかもしれないけど新しい発見だったのでメモしておく

views.py
#問い合わせページ
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に直接絞り込みの条件をかけるからそっちのほうがややこしくないかもしれない

models.py・HPを参考にした方
#検索窓クエリセット用モデル
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・HPを参考にした方
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")
forms.py・HPを参考にした方
#検索窓フォーム
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に直接絞り込み条件をかいたほう(こっちのほうがわかりやすいかも)

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つの方法|オススメは?

models.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)

これをviews.pyでsearchメゾットとして使うわけだけどややこしくなった

views.py
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   

定義したフォームはこちら

forms.py
#検索窓フォーム
class ItemSearchForm(forms.Form):
    CATEGORY_NAME=[
        ('家具','家具'),
        ('家電','家電'),
        ('自転車','自転車'),
        ('楽器','楽器'),
        ('生活雑貨','生活雑貨'),
        ('子供用品','子供用品'),
        ('ホビー、ゲーム','ホビー、ゲーム'),
        ('服/ファッション','服/ファッション'),
        ('靴/バック','靴/バック'),
        ('コスメ/ヘルスケア','コスメ/ヘルスケア'),
        ('食品','食品'),
        ('スポーツ','スポーツ'),
        ('その他','その他'),
    ]
    search = forms.CharField(required=False,
                             widget=forms.TextInput(
            attrs={'placeholder':'フリーワード検索'}))
    category = forms.ChoiceField(
        required=False,
        choices=[('','全てのカテゴリー')] + CATEGORY_NAME
    )

テンプレートはこちら

top.html
{#検索窓#}
<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にかきこんでいく

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はこちら

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">&laquo;</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">&raquo;</span>
            <span class="sr-only">次へ</span>
          </a>
        </li>
      {% endif %}
    </ul>
</nav>

a href ="?{%url_replace ~~~ %}" 部を設定してあげることでカスタムテンプレートタグをつかうことができる

データ件数を取得して表示

cityごとに何件あるかをTopViewにget_context_dataでわたしたかった。コードは下記のように書く

views.py
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へ逆参照しているという意味

テンプレートでエイリアスでとりだすとはこういうこと

top.html
{% 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が実装できた記録として残しておく

top.html
<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の設定をする

top.html
{% 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を実装する

views.py
#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を書く

filtered_items.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には描画エリアを示唆する

top.html
<div id="result-area">
  <!-- 絞り込まれたItemsがここに表示されます -->
</div>

画像クリックで簡単に拡大する方法

リンク
こちらで作成できた。感謝

ヘッダー画像作成、アイコン、ロゴ作成

Canva
アイコン
ロゴ

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2