👯

広告を動的に表示したい!【Django】

2024/05/06に公開

静的であれば難しく考えなくてもいい

静的に、固定の広告を表示するだけだと特に難しいことはなく、普通にtemplateにASPのHTMLコードをぺってするだけなんですよね。
でもこれだと完全に固定になってしまう。。
ただずっとこれで運用していました。でもやっぱり変更したいとき煩わしい。。
ってことでModel作成して管理画面で操作できるようにしよう!と思ったわけですよ。
ただ、それだけだと今までより変更が簡単になるだけなんです。
そこで!!
ユーザーの属性によって表示される広告を実装したいなと。
まーいうても大それたことはできないので、templatesのカスタムタグを使用しようかなと考えました!
自分のキャンパスコンパスではユーザーに学年と学科を持たせているのでそれによって変えたいと思います。
https://pus-pass.com/

要件

  • 管理画面で追加削除の操作可能
  • 特定の場所を用意しそこに広告を入れる
  • 広告の種類はHTML
  • ユーザーによって表示する広告を変えたい
  • Views.pyでやると今までの実装分に全部追加しなくちゃいけなくなるから無し

Views.pyでの選択がなくなるので今回はカスタムタグを使用していきます。

要件に従って実装

DB(models.py)

まずテーブル設計ですね。

以下が考慮したい部分と欲しい情報ですね。

  • HTML
  • 配置場所
  • 指定したいユーザー属性が複数(例えば学年と学科など)
  • 会員未登録のユーザーの場合のデフォルトの広告
  • 表示回数

ER図を以下のように作成してみました。
学年と学科はすでに作成済みなのでそのまま使用します。

コードは以下のように実装します。

settings.py
ADS_PLACEMENT_CHOICES = (
    (1, 'トップ'),
    (2, 'サイドバー'),
    (3, 'フッター'),
)
models.py
class AdsManagement(models.Model):
    ad_html = models.TextField(verbose_name="広告用HTML")
    default_ad_html = models.TextField(verbose_name="会員未登録のユーザーの場合のデフォルトの広告HTML", blank=True)
    placement = models.IntegerField(choices=settings.ADS_PLACEMENT_CHOICES, verbose_name="配置場所")
    years = models.ManyToManyField(SchoolYear, verbose_name="学年", related_name="ads_managements")
    classrooms = models.ManyToManyField(Classroom, verbose_name="学科", related_name="ads_managements")
    display_count = models.IntegerField(default=0, verbose_name="表示回数")
    default_display_count = models.IntegerField(default=0, verbose_name="デフォルトの表示回数")

    def __str__(self):
        return f"Advertisement at {self.placement}"

実装したらマイグレーションしてDBに反映します。
次に、実際に表示するTemplateを作成してみます。

テンプレート(〇〇.html)

ほとんどのテンプレートはbase.htmlを継承しているのでbase.htmlに実装します。
pr-wrapperで元々広告を配置していたので今回も入れていきます。
また、作成するカスタムタグを読み込ませるために頭に{% load ads_management %}を入れます。
3はフッターに配置しているからです。

base.html
{% load ads_management %}
...
<div class="pr-wrapper">
    {% ads_management request 3 as ad_html %}
    {{ ad_html|safe }}
</div>

カスタムタグ(ads_management.py)

mainapp/templatetags/ads_management.py
from django import template
from mainapp.models import AdsManagement

register = template.Library()
@register.simple_tag(name='ads_management')
def ads_management(request, placement):
    # 指定された配置場所の広告を全て取得
    ads_placement = AdsManagement.objects.filter(placement=placement)
    first_ad = ads_placement.first()  # この時点で最初の広告を取得
    if not first_ad:
        return ""

    # ユーザーが認証されていない場合は最初のデフォルトのHTMLを返す
    if not request.user.is_authenticated:
        first_ad.default_display_count += 1
        first_ad.save()
        return first_ad.default_ad_html

    # ユーザーの学年と学科を取得
    user_school_year = request.user.school_year
    user_classroom = request.user.classroom
    
    # ユーザーの学年と学科を含む広告をフィルタリングして最初の広告を取得
    ads = ads_placement.filter(
        years=user_school_year,
        classrooms=user_classroom
    ).first()
    if ads:
        ads.display_count += 1
        ads.save()
        return ads.ad_html
    else:
        first_ad.default_display_count += 1
        first_ad.save()
        return first_ad.default_ad_html

settings.pyの中のTEMPLATESのlibrariesの中にカスタムタグを入れて読み込むようにする

settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                # いじらない
            ],
            'libraries': {
                # 追加する
                'ads_management': 'mainapp.templatetags.ads_management',
            }
        },
    },
]

管理画面(admin.py)

登録するためにadmin.pyを実装します。
filter_horizontalを設定することでManyToManyFieldを登録しやすいようにしています。
(以下のような表示になります。)

mainapp/admin.py
from django.contrib import admin
from .models import AdsManagement

class AdsManagementAdmin(admin.ModelAdmin):
    list_display = ('placement', 'display_schoolyears', 'display_classroom', 'display_count', 'default_display_count')
    list_filter = ('placement', 'years', 'classrooms')
    search_fields = ('classrooms',)
    filter_horizontal = ('classrooms', 'years')

    def display_classroom(self, obj):
        return ", ".join([classroom.get_name_display() for classroom in obj.classrooms.all()])
    display_classroom.short_description = 'classroom'
    display_classroom.admin_order_field = 'classroom__name'

    def display_schoolyears(self, obj):
        return ", ".join([school_year.get_name_display() for school_year in obj.years.all()])
    display_schoolyears.short_description = 'schoolyear'
    display_schoolyears.admin_order_field = 'school_year__name'


admin.site.register(AdsManagement, AdsManagementAdmin)

さいごに

まだまだ改善の余地は多く残っていると思いますが、一旦はこれで完成としておきます。
例えばキャッシュにしたり、A/Bテストのために特定のユーザーだけに表示したりなどなど
今回はユーザーの情報によって動的に変化させたいだけが目的だったのでこんな感じにしました。
パフォーマンスはあまり考えていませんが、もう少しやることが終わったらパフォーマンスチューニングやりたいと考えています。
DB移行のタイミングくらいで。。
カスタムタグの学年と学科の取得の部分が気に食わないのでここももう少し頭使いたい気持ちです。
デフォルトだけを用意するのもいいかなーとか考えてます。

Discussion