見出し画像

【Django】カテゴリ分類されたタグを動的に生成する

あるモデルXXに、中間モデルTag を経由して、モデルTagChoiceでカテゴリごとに分類されたTagをつけるときに苦戦したのでメモ。

■要件

あるモデルXXに複数選択可能なタグを付与する
タグの選択肢自体も都度作成出来るように、選択肢モデルを作る
タグの選択肢にはカテゴリをつけて、カテゴリごとに分類して表示する

■モデル

models.py

# tagの選択肢
class TagChoice(models.Model):
   name = models.CharField(max_length=300)
   category = models.CharField(max_length=300)
   created_at = models.DateTimeField(auto_now_add=True)
   updated_at = models.DateTimeField(auto_now=True)

   def __str__(self):
       return "{} : {}".format(self.category, self.name)


# タグ:モデルXXごとに紐づく
class Tag(models.Model):
   tagged_XX = models.ForeignKey(
           XX,
           verbose_name="タグ付けXX",
           on_delete=models.SET_NULL, null=True,
           related_name="tagged_XX",
           )
   tags = models.ManyToManyField(TagChoice)
   created_at = models.DateTimeField(auto_now_add=True)
   updated_at = models.DateTimeField(auto_now=True)

   def __str__(self):
       return "{} : {}".format(self.tagged_XX, self.tags.all())

   def get_absolute_url(self):
       return reverse('XXs:detail', kwargs={'pk': self.tagged_XX.pk})

■フォーム

from .widgets import CustomCheckboxSelectMultiple

class TagsForm(forms.ModelForm):
   class Meta:
       model = Tag
       fields = ['tags']
   tags = forms.ModelMultipleChoiceField(
       label='タグ',
       queryset=TagChoice.objects.all(),
       required=False,
       widget=CustomCheckboxSelectMultiple,
   )

   def __init__(self, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self.fields["tags"].required = True

参考:Django、カスタムチェックボックスの作成 :css略

■View

views.py
# 不要な物も入っているけれど適宜削除なり書き換えるなりしてください

# url: path('XX/<int:yy>/tag_create/', views.create_tags, name='tag_create'),


def create_tags(request,yy):
   results = {}
   tagged_XX = XX.objects.get(pk=yy)
   tag_XX, created = Tag.objects.get_or_create(tagged_XX=tagged_XX)
   ret = ''
   if request.method == 'POST':
       form = TagsForm(request.POST, instance=tag_XX)
       # 入力されたデータの受取
       if form.is_valid():
           tags = form.cleaned_data.get('tags').all()
           tag_XX.tags.add(*tags)
           tag_XX.tagged_XX = tagged_XX
           form.save()

           ret = 'OK'
           c = {'results': results,'ret':ret}
           # CFRF対策(必須)
           c.update(csrf(request))
           return HttpResponseRedirect(reverse_lazy('XX:detail', kwargs={'pk': yy}))

   else:
       form = TagsForm()
       c = {
           'form': form,
           'ret':ret,
           'XX': XX.objects.get(pk=yy),
           'tag_categories': list(reversed(TagChoice.objects.all().values_list('category', flat=True).distinct()))
           }
       try:
           init_XX = XX.objects.get(pk=yy)
           form.fields['tags'].initial = Tag.objects.get(tagged_XX=init_XX).tags.all()
       except Exception as e:
           form.fields['tags'].initial = None

   return render(request,'XX/create_tags.html',c)

■HTML

create_tag.html
#該当部のみ#

{% load index %}

            {% for ctgr in tag_categories %}
             <div class="field">
               {{ctgr}}
               {% for field in form %}
                 {% for f in field %}
                   {% if ctgr|stringformat:"s" in field|index:forloop.counter0|stringformat:"s" %}
                       {{ field|index:forloop.counter0|cut:ctgr }}
                       {% if field.help_text %}
                           <span class="helptext">{{ field.help_text }}</span>
                       {% endif %}
                       {{ field.errors }}
                   {% endif %}
                 {% endfor %}
               {% endfor %}
             </div>
            {% endfor %}

forループを何重にも入れ子してしまっているが、カテゴリごとに表示をだし分けるために、こうなってしまった。

参考:index.py はここを参考にした。


スタートアップ支援と、社内スタートアップをしながら、実務未経験から独学でプログラミング勉強中。主にpython。スクレイピング、機械学習から始めて、最近はDjangoを学習し、WEBサービス作成中。独学されている初心者の方向けに、技術の記事を中心に書いていきます。