Search code examples
djangodjango-querysetwagtaildjango-taggit

AttributeError: 'QuerySet' object has no attribute 'tags' when trying to save quotes and tags with ClusterableModel and ClusterTaggableManager


I am trying to save some quotes from a csv file to my django model using python manage.py shell because I could not use django-import-export to do it. I asked about it at Tags imported but relationship not imported with django-import-export but nobody answered and I could not find more things to try after googling.

After reading the documentation and previous answers here about django querysets and tagging, I managed to save most of each quote but the tags are not saved. The query set does not return tags field, which causes the AttributeError. Please see shell output q: <QuerySet... below

My tag model follows https://docs.wagtail.io/en/stable/reference/pages/model_recipes.html#custom-tag-models. From django-admin, the relationships are working.screenshot So the missing piece of the puzzle is saving the tags. What query should I use to locate the tags field or what method should I use to save the tags?

#The script I'm trying to run in python manage.py shell

import csv
from quotes.models import Quote, TaggedQuote

with open("test_import1.csv", "r", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        if row["book"]:
            item = Quote(
                text=row["text"],
                author=row["author"],
                source_url=row["url"],
                book=row["book"],
            )
        elif not row["book"]:
            item = Quote(text=row["text"], author=row["author"], source_url=row["url"])
        item.save()
        for each in row["tags"].split(", "):
            each = str(each).strip("'")
            q = Quote.objects.filter(text__exact=row["text"].strip('"')).values()
            print(f"each: {each}")
            print(f"q: {q}")
            q.tags.add(each)

Shell output

each: attributednosource
q: <QuerySet [{'id': 56, 'text': "I'm selfish, impatient and a little insecure. I make mistakes, I am out of control and at times hard to handle. But if you can't handle me at my worst, then you sure as hell don't deserve me at my best.", 'author': 'Marilyn Monroe', 'book': '', 'book_url': '', 'source_url': 'https://www.goodreads.com/quotes/tag/love', 'user_id': None, 'imported': True, 'created': datetime.datetime(2021, 9, 1, 9, 53, 25, 224023, tzinfo=<UTC>), 'updated': datetime.datetime(2021, 9, 1, 9, 53, 25, 224084, tzinfo=<UTC>), 'active': True, 'inactive_message': ''}]>
Traceback (most recent call last):
  File "<console>", line 26, in <module>
AttributeError: 'QuerySet' object has no attribute 'tags'

models.py

from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TagBase, ItemBase
from wagtail.snippets.models import register_snippet

from ckeditor.fields import RichTextField


import sys

sys.path.append("..")  # Adds higher directory to python modules path.


@register_snippet
class QuoteTag(TagBase):
    class Meta:
        verbose_name = "Quote Tag"
        verbose_name_plural = "Quote Tags"


class TaggedQuote(ItemBase):
    tag = models.ForeignKey(
        QuoteTag, related_name="tagged_quotes", on_delete=models.CASCADE
    )
    content_object = ParentalKey(
        to="Quote", related_name="tagged_items", on_delete=models.CASCADE
    )


def get_sentinel_user():
    return get_user_model().objects.get_or_create(username="deleted_user")[0]


# DO NOT use set_admin(). Default cannot be a query because it will be executed before migration and cause error.
# def set_admin():
#     return get_user_model().objects.get(pk=1).id


class Quote(ClusterableModel):
    text = RichTextField(
        config_name="awesome_ckeditor", help_text="You must enter some quote content"
    )
    author = models.CharField(
        max_length=300, blank=True, help_text="Person who said/wrote this quote"
    )
    book = models.CharField(
        max_length=300,
        blank=True,
        help_text="If the quote is from a book, enter book title here",
    )
    book_url = models.URLField(max_length=300, blank=True)
    tags = ClusterTaggableManager(through="TaggedQuote", blank=True)
    source_url = models.URLField(
        max_length=300,
        blank=True,
        help_text="If applicable: Paste the url link where you found the quote here",
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        blank=True,
        null=True,
        on_delete=models.SET(get_sentinel_user),
    )
    # DO NOT use default=set_admin(). Default cannot be a query because it will be executed before migration and cause  error.
    # user = models.ForeignKey(CustomUser, blank=True, null=True, default=set_admin(), on_delete=models.SET(get_sentinel_user),)
    imported = models.BooleanField(default=True)
    created = models.DateTimeField(auto_now_add=True, editable=False)
    updated = models.DateTimeField(auto_now=True, editable=False)
    active = models.BooleanField(
        default=True, help_text="Allows moderator to unpublish quote if set to false"
    )  # Allows moderators to hide offensive posts
    inactive_message = models.TextField(
        max_length=500,
        blank=True,
        help_text="Enter the reason for setting this quote to inactive",
    )

    def __str__(self):
        """String repr of this class."""
        return f"{self.text} - {self.author}"

    class Meta:

        verbose_name = "Quote"
        verbose_name_plural = "Quotes"

test_import1.csv

,text,author,tags,url,book
0,"I'm selfish, impatient and a little insecure. I make mistakes, I am out of control and at times hard to handle. But if you can't handle me at my worst, then you sure as hell don't deserve me at my best.",Marilyn Monroe,"attributednosource, best, life, love, mistakes, outofcontrol, truth, worst",https://www.goodreads.com/quotes/tag/love,
1,"You've gotta dance like there's nobody watching, Love like you'll never be hurt, Sing like there's nobody listening, And live like it's heaven on earth.",William W. Purkey,"dance, heaven, hurt, inspirational, life, love, sing",https://www.goodreads.com/quotes/tag/love,
2,You know you're in love when you can't fall asleep because reality is finally better than your dreams.,Dr. Seuss,"attributednosource, dreams, love, reality, sleep",https://www.goodreads.com/quotes/tag/love,
3,A friend is someone who knows all about you and still loves you.,Elbert Hubbard,"friend, friendship, knowledge, love",https://www.goodreads.com/quotes/tag/love,
4,Darkness cannot drive out darkness: only light can do that. Hate cannot drive out hate: only love can do that.,Martin Luther King Jr.,"darkness, driveout, hate, inspirational, light, love, peace",https://www.goodreads.com/quotes/tag/love,A Testament of Hope: The Essential Writings and Speeches
5,We accept the love we think we deserve.,Stephen Chbosky,"inspirational, love",https://www.goodreads.com/quotes/tag/love,The Perks of Being a Wallflower


Solution

  • In q.tags.add(each), q is a list of objects so you can't use tag directly.

    Either iterate through each quote objects:

    for q in Quote.objects.filter(text__exact=row["text"].strip('"')):
        q.tags.add(each)
    

    or get one object via get:

    q = Quote.objects.get(text__exact=row["text"].strip('"'))
    q.tags.add(each)
    

    or filter but return one object for example using first:

    q = Quote.objects.filter(text__exact=row["text"].strip('"')).first()
    q.tags.add(each)