Tags and Tag Database in Django-Taggit

Hi, So I am new to Django-taggit and I am slowly only learning how to implement tags in django. So, My problem is I already have a model named Genre and other model named Book so what i want to do is I want to implement the names of Genre as tags in Book Model in a field named genre which is genre_tag, So any idea that how do i achieve it and i want to use these tags in add or manage book forums as well. Well, I would also like some advice on Django-taggit like whether are there any resources from where i can learn .
models.py:

class Genre(models.Model):
    name = models.CharField(max_length=300)
    description = models.TextField(blank=True, null=True)
    date_created = models.DateField(auto_now=True)

    class Meta:
        verbose_name_plural = "List of Genres"

    def __str__(self):
        return str(f"{self.name}")


class Book(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text="Unique ID for this particular book across the library")
    title = models.CharField(max_length=750)
    author = models.CharField(max_length=350)
    pages = models.PositiveIntegerField()
    genre_tag = TaggableManager()
    year_published = models.PositiveIntegerField(validators=[MinValueValidator(1700), MaxValueValidator(datetime.now().year)], default=datetime.now().year, help_text="Use the following format: <YYYY>")
    book_format = models.CharField(max_length=50, default='paperback', choices=FORMAT_CHOICES)
    is_borrowed = models.BooleanField(default=False)

    class Meta:
        verbose_name_plural = "List of Books"

    def __str__(self):
        return str(f"{self.id} - {self.title}")

forms.py:

class SaveBook (forms.ModelForm):
    title = forms.CharField(label="Book Title", max_length=750, widget=forms.TextInput(attrs={"class": "form-control rounded = 0"}))
    author = forms.CharField(label="Book Author", max_length=350, widget=forms.TextInput(attrs={"class": "form-control rounded-0"}))
    pages = forms.IntegerField(label="Number of Pages in Book", widget=forms.NumberInput(attrs={"class": "form-control rounded-0"}))
    year_published = forms.IntegerField(label="Year Published", widget=forms.DateInput(format=['%Y'], attrs={"class": "form-control rounded-0"}))
    genre_tag = forms.CharField(label="Book Genre", widget=TagWidget(attrs={"class": "form-control rounded-0", "data-tags": ",".join(Genre.objects.values_list('name', flat=True))}))
    book_format = forms.ChoiceField(choices=FORMAT_CHOICES, label="Book Format", widget=forms.Select(attrs={"class": "form-control form-select rounded-0"}))
    class Meta:
        model = Book
        fields = ['title', 'author', 'pages', 'year_published', 'genre_tag' ,'book_format']

    def clean_genre(self):
        genres = self.cleaned_data.get('genre')
        if not genres:
            raise forms.ValidationError("Please select at least one genre.")
        return genres

have you read “Custom tag” of “cutomizing taggit”?
https://django-taggit.readthedocs.io/en/latest/custom_tagging.html#custom-tag

sample

from django.db import models
from django.utils.translation import gettext_lazy as _

from taggit.managers import TaggableManager
from taggit.models import TagBase, GenericTaggedItemBase


class MyCustomTag(TagBase):
    # ... fields here

    class Meta:
        verbose_name = _("Tag")
        verbose_name_plural = _("Tags")

    # ... methods (if any) here


class TaggedWhatever(GenericTaggedItemBase):
    # TaggedWhatever can also extend TaggedItemBase or a combination of
    # both TaggedItemBase and GenericTaggedItemBase. GenericTaggedItemBase
    # allows using the same tag for different kinds of objects, in this
    # example Food and Drink.

    # Here is where you provide your custom Tag class.
    tag = models.ForeignKey(
        MyCustomTag,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_items",
    )


class Food(models.Model):
    # ... fields here

    tags = TaggableManager(through=TaggedWhatever)


class Drink(models.Model):
    # ... fields here

    tags = TaggableManager(through=TaggedWhatever)

example

from django.db import models

from taggit.managers import TaggableManager
from taggit.models import TagBase, GenericTaggedItemBase

class Genre(TagBase):
   ...

class Tagged(GenericTaggedItemBase):
    tag = models.ForeignKey(
        Genre,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_items",
    )

class Book(models.Model):
    genre_tag = TaggableManager(through=Tagged)
    ...
    
1 Like

Thank you, this solves my models.py issue but is the forms.py correct and I would like my views.py to get validated:
models.py:

class Genre(TagBase):
    name = models.CharField(max_length=300)
    description = models.TextField(blank=True, null=True)
    date_created = models.DateField(auto_now=True)

    class Meta:
        verbose_name_plural = "List of Genres"

    def __str__(self):
        return str(f"{self.name}")
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)
    
class Tagged(GenericTaggedItemBase):
    tag = models.ForeignKey(Genre, on_delete=models.CASCADE, related_name="%(app_label)s_%(class)s_items")

class Book(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text="Unique ID for this particular book across the library")
    title = models.CharField(max_length=750)
    author = models.CharField(max_length=350)
    pages = models.PositiveIntegerField()
    genre = TaggableManager(through=Tagged)
    year_published = models.PositiveIntegerField(validators=[MinValueValidator(1700), MaxValueValidator(datetime.now().year)], default=datetime.now().year, help_text="Use the following format: <YYYY>")
    book_format = models.CharField(max_length=50, default='paperback', choices=FORMAT_CHOICES)
    is_borrowed = models.BooleanField(default=False)

    class Meta:
        verbose_name_plural = "List of Books"

    def __str__(self):
        return str(f"{self.id} - {self.title}")

views.py:

@login_required
def save_book(request):
    resp = {'status': 'failed', 'msg': ''}
    if request.method == 'POST':
        post = request.POST
        if not post['id'] == '':
            book = Book.objects.get(id=post['id'])
            form = SaveBook(request.POST, instance=book)
        else:
            form = SaveBook(request.POST)
        
        if form.is_valid():
            form.save()
            form.save_m2m()
            if post['id'] == '':
                messages.success(request, "Book has been saved successfully.")
            else:
                messages.success(request, "Book has been updated successfully.")
            resp['status'] = 'success'
        else:
            for field in form:
                for error in field.errors:
                    if not resp['msg'] == '':
                        resp['msg'] += str('<br/>')
                    resp['msg'] += str(f'[{field.name}] {error}')
    else:
        resp['msg'] = "There's no data sent on the request"

    return HttpResponse(json.dumps(resp), content_type="application/json")

@login_required
def manage_book(request, pk = None):
    context = context_data(request)
    context['page'] = 'manage_book'
    context['page_title'] = 'Manage Book'
    if pk is None:
        context['book_form'] = SaveBook()
        context['book'] = {}
    else:
        book = Book.objects.get(id=pk)
        context['book_form'] = SaveBook(instance=book)
        context['book'] = book
    
    context['genres'] = list(Genre.objects.values_list('name', flat=True))
    return render(request, "manage_book.html", context)

forms.py:

class SaveBook (forms.ModelForm):
    title = forms.CharField(label="Book Title", max_length=750, widget=forms.TextInput(attrs={"class": "form-control rounded = 0"}))
    author = forms.CharField(label="Book Author", max_length=350, widget=forms.TextInput(attrs={"class": "form-control rounded-0"}))
    pages = forms.IntegerField(label="Number of Pages in Book", widget=forms.NumberInput(attrs={"class": "form-control rounded-0"}))
    year_published = forms.IntegerField(label="Year Published", widget=forms.DateInput(format=['%Y'], attrs={"class": "form-control rounded-0"}))
    genre = forms.CharField(label="Book Genre", widget=TagWidget(attrs={"class": "form-control rounded-0", "data-tags": ",".join(Genre.objects.values_list('name', flat=True))}))
    book_format = forms.ChoiceField(choices=FORMAT_CHOICES, label="Book Format", widget=forms.Select(attrs={"class": "form-control form-select rounded-0"}))
    class Meta:
        model = Book
        fields = ['title', 'author', 'pages', 'year_published', 'genre' ,'book_format']

    def clean_genre(self):
        genres = self.cleaned_data.get('genre')
        if not genres:
            raise forms.ValidationError("Please select at least one genre.")
        return genres

Additional Question, How to get autocomplete or search feature for Tag field in template (HTML) is it by select2 or by simple JQuery?

Why would you do that?

from django.db import models

class Book(models.Model):
    genre_tag = TaggableManager(through=Tagged, blank=False)
from django.forms import forms

class form(forms.form):
  Meta:
    field = '__all__'
    widgets = {'genre': forms.CheckboxSelectMultiple}

Ok my point is that I want to implement the tagging from select2 and so checkbox wouldnt be a valid field so then how do i implement it then:
Dynamic option creation | Select2 - The jQuery replacement for select boxes

Are you want to this?

from django.forms import forms

class form(forms.form):
  Meta:
    field = '__all__'
    widgets = {'genre': forms.Select}

Ok will try it and get back to you…

I had tried a different approach but i got it figured. Thanks.

what is different approach???

So, here is my approach:
forms.py:

class SaveBook(forms.ModelForm):
genre = forms.ModelMultipleChoiceField(label="Book Genre", queryset=Genre.objects.all(), widget=forms.SelectMultiple(attrs={"class": "form-control rounded-0"}))
    class Meta:
        model = Book
        fields = ['title', 'author', 'pages', 'year_published', 'book_format', 'genre']

views.py:

def search_genre(request):
    if request.method in ['POST', 'GET'] and request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        term = request.GET.get('term', '')
        if term:
            genres = Genre.objects.filter(name__icontains=term)
        else:
            genres = Genre.objects.all()

        genre_list = [{"id": genre.id, "text": genre.name} for genre in genres]
        return JsonResponse(genre_list, safe=False)
    return JsonResponse({'error': 'Bad request'}, status=400)
# usual save approach 

html script for select2:

<script>
$(function() {
    $('#uni_modal').on('shown.bs.modal', function(){
        $('#{{book_form.genre.auto_id}}').select2({
            tags: true,
            tokenSeparator: [', '],
            placeholder: "Select a genre",
            width: "100%",
            dropdownParent:$('#uni_modal'),
            multiple: true,
            ajax: {
                url: "{% url 'search-genre' %}",
                dataType: 'json',
                delay: 250,
                data: function(params) {
                    return {
                        term: params.term
                    };
                },
                processResults: function(data) {
                    return {
                        results: $.map(data, function(item) {
                            return { id: item.id, text: item.text };
                        })
                    };
                },
                cache: true
            },
            createTag: function(params){
                var term = $.trim(params.term);
                if (term === '') {
                    return null;
                }
                return {
                    id: term,
                    text: term,
                    newTag: true
                };
            },
            insertTag: function(data, tag){
                data.push(tag);
            }
        });
    });
});
</script>

Oh… you seems like wanted a search function.
Then you can use the access tag list in {% for tag in form.fields[{tagfield}].queryset %}{% endfor %} in your template:

1 Like