This commit is contained in:
Tasos Katsoulas 2024-11-15 16:59:03 +02:00
Родитель 8a157f72de
Коммит bdd3050bfd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 66D7743B4FCCFCBC
18 изменённых файлов: 200 добавлений и 54 удалений

Просмотреть файл

@ -9,7 +9,6 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import pagination, permissions, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from taggit.models import Tag
from kitsune.products.api_utils import TopicField
from kitsune.products.models import Product, Topic
@ -30,6 +29,7 @@ from kitsune.sumo.api_utils import (
SplitSourceField,
)
from kitsune.sumo.utils import is_ratelimited
from kitsune.tags.models import SumoTag
from kitsune.tags.utils import add_existing_tag
from kitsune.users.api import ProfileFKSerializer
from kitsune.users.models import Profile
@ -386,7 +386,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
for tag in tags:
try:
add_existing_tag(tag, question.tags)
except Tag.DoesNotExist:
except SumoTag.DoesNotExist:
raise GenericAPIException(
status.HTTP_403_FORBIDDEN, "You are not authorized to create new tags."
)

Просмотреть файл

@ -2,7 +2,6 @@ from django.shortcuts import get_object_or_404
from django.utils.feedgenerator import Atom1Feed
from django.utils.html import escape, strip_tags
from django.utils.translation import gettext as _
from taggit.models import Tag
from kitsune.products.models import Product, Topic
from kitsune.questions import config
@ -10,6 +9,7 @@ from kitsune.questions.models import Question
from kitsune.sumo.feeds import Feed
from kitsune.sumo.templatetags.jinja_helpers import urlparams
from kitsune.sumo.urlresolvers import reverse
from kitsune.tags.models import SumoTag
class QuestionsFeed(Feed):
@ -80,7 +80,7 @@ class QuestionsFeed(Feed):
class TaggedQuestionsFeed(QuestionsFeed):
def get_object(self, request, tag_slug):
return get_object_or_404(Tag, slug=tag_slug)
return get_object_or_404(SumoTag, slug=tag_slug)
def title(self, tag):
return _("Recently updated questions tagged %s" % tag.name)

Просмотреть файл

@ -0,0 +1,30 @@
# Generated by Django 4.2.16 on 2024-11-15 06:58
from django.db import migrations, models
import kitsune.tags.models
class Migration(migrations.Migration):
dependencies = [
("tags", "0001_initial"),
("questions", "0018_auto_20241105_0532"),
]
operations = [
migrations.AlterField(
model_name="aaqconfig",
name="associated_tags",
field=models.ManyToManyField(blank=True, null=True, to="tags.sumotag"),
),
migrations.AlterField(
model_name="question",
name="tags",
field=kitsune.tags.models.BigVocabTaggableManager(
help_text="A comma-separated list of tags.",
through="tags.SumoTaggedItem",
to="tags.SumoTag",
verbose_name="Tags",
),
),
]

Просмотреть файл

@ -21,7 +21,6 @@ from django.utils import translation
from django.utils.translation import pgettext
from elasticsearch import ElasticsearchException
from product_details import product_details
from taggit.models import Tag
from kitsune.flagit.models import FlaggedObject
from kitsune.products.models import Product, Topic
@ -33,7 +32,7 @@ from kitsune.sumo.models import LocaleField, ModelBase
from kitsune.sumo.templatetags.jinja_helpers import urlparams, wiki_to_html
from kitsune.sumo.urlresolvers import reverse
from kitsune.sumo.utils import chunked
from kitsune.tags.models import BigVocabTaggableManager
from kitsune.tags.models import BigVocabTaggableManager, SumoTag
from kitsune.tags.utils import add_existing_tag
from kitsune.upload.models import ImageAttachment
from kitsune.wiki.models import Document
@ -312,7 +311,7 @@ class Question(AAQBase):
if os := self.metadata.get("os"):
try:
add_existing_tag(os, self.tags)
except Tag.DoesNotExist:
except SumoTag.DoesNotExist:
pass
product_md = self.metadata.get("product")
topic_md = self.metadata.get("category")
@ -825,7 +824,7 @@ class AAQConfig(ModelBase):
title = models.CharField(max_length=255, default="")
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="aaq_configs")
pinned_articles = models.ManyToManyField(Document, null=True, blank=True)
associated_tags = models.ManyToManyField(Tag, null=True, blank=True)
associated_tags = models.ManyToManyField(SumoTag, null=True, blank=True)
enabled_locales = models.ManyToManyField(QuestionLocale)
# Whether the configuration is active or not. Only one can be active per product
is_active = models.BooleanField(default=False)

Просмотреть файл

@ -6,7 +6,6 @@ from actstream.models import Action, Follow
from django.contrib.contenttypes.models import ContentType
from django.core.management import call_command
from django.db.models import Q
from taggit.models import Tag
import kitsune.sumo.models
from kitsune.flagit.models import FlaggedObject
@ -33,6 +32,7 @@ from kitsune.questions.tests import (
from kitsune.search.tests import Elastic7TestCase
from kitsune.sumo import googleanalytics
from kitsune.sumo.tests import TestCase
from kitsune.tags.models import SumoTag
from kitsune.tags.tests import TagFactory
from kitsune.tags.utils import add_existing_tag
from kitsune.users.tests import UserFactory
@ -226,9 +226,9 @@ class TestQuestionMetadata(TestCase):
def test_auto_tagging(self):
"""Make sure tags get applied based on metadata on first save."""
Tag.objects.create(slug="green", name="green")
Tag.objects.create(slug="troubleshooting", name="Troubleshooting")
Tag.objects.create(slug="firefox", name="Firefox")
SumoTag.objects.get_or_create(name="green", defaults={"slug": "green"})
SumoTag.objects.get_or_create(name="Troubleshooting", defaults={"slug": "troubleshooting"})
SumoTag.objects.get_or_create(name="Firefox", defaults={"slug": "firefox"})
q = self.question
q.product = ProductFactory(slug="firefox")
q.topic = TopicFactory(slug="troubleshooting")
@ -520,7 +520,7 @@ class AddExistingTagTests(TestCase):
def test_add_existing_no_such_tag(self):
"""Assert add_existing_tag doesn't work when the tag doesn't exist."""
with self.assertRaises(Tag.DoesNotExist):
with self.assertRaises(SumoTag.DoesNotExist):
add_existing_tag("nonexistent tag", self.untagged_question.tags)

Просмотреть файл

@ -9,7 +9,6 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.core import mail
from pyquery import PyQuery as pq
from taggit.models import Tag
from kitsune.products.tests import ProductFactory, TopicFactory
from kitsune.questions.events import QuestionReplyEvent, QuestionSolvedEvent
@ -19,6 +18,7 @@ from kitsune.questions.views import NO_TAG, UNAPPROVED_TAG
from kitsune.sumo.templatetags.jinja_helpers import urlparams
from kitsune.sumo.tests import TestCase, attrs_eq, emailmessage_raise_smtp, get, post
from kitsune.sumo.urlresolvers import reverse
from kitsune.tags.models import SumoTag
from kitsune.tags.tests import TagFactory
from kitsune.tidings.models import Watch
from kitsune.upload.models import ImageAttachment
@ -965,7 +965,7 @@ class TaggingViewTestsAsAdmin(TestCase):
u = UserFactory()
add_permission(u, Question, "tag_question")
add_permission(u, Tag, "add_tag")
add_permission(u, SumoTag, "add_tag")
self.client.login(username=u.username, password="testpass")
self.question = QuestionFactory()

Просмотреть файл

@ -30,7 +30,6 @@ from django.utils.translation import gettext_lazy as _lazy
from django.views.decorators.http import require_GET, require_http_methods, require_POST
from django_user_agents.utils import get_user_agent
from sentry_sdk import capture_exception
from taggit.models import Tag
from zenpy.lib.exception import APIException
from kitsune.access.decorators import login_required, permission_required
@ -62,6 +61,7 @@ from kitsune.sumo.utils import (
set_aaq_context,
simple_paginate,
)
from kitsune.tags.models import SumoTag
from kitsune.tags.utils import add_existing_tag
from kitsune.tidings.events import ActivationRequestFailed
from kitsune.tidings.models import Watch
@ -242,7 +242,7 @@ def question_list(request, product_slug=None, topic_slug=None):
if tagged:
tag_slugs = tagged.split(",")
tags = Tag.objects.filter(slug__in=tag_slugs)
tags = SumoTag.objects.filter(slug__in=tag_slugs)
if tags:
for t in tags:
question_qs = question_qs.filter(tags__name__in=[t.name])
@ -1006,7 +1006,7 @@ def add_tag(request, question_id):
try:
question, canonical_name = _add_tag(request, question_id)
except Tag.DoesNotExist:
except SumoTag.DoesNotExist:
template_data = _answers_data(request, question_id)
template_data["tag_adding_error"] = UNAPPROVED_TAG
template_data["tag_adding_value"] = request.POST.get("tag-name", "")
@ -1032,14 +1032,14 @@ def add_tag_async(request, question_id):
"""
try:
question, canonical_name = _add_tag(request, question_id)
except Tag.DoesNotExist:
except SumoTag.DoesNotExist:
return HttpResponse(
json.dumps({"error": str(UNAPPROVED_TAG)}), content_type="application/json", status=400
)
if canonical_name:
question.clear_cached_tags()
tag = Tag.objects.get(name=canonical_name)
tag = SumoTag.objects.get(name=canonical_name)
tag_url = urlparams(
reverse("questions.list", args=[question.product_slug]), tagged=tag.slug
)
@ -1429,13 +1429,13 @@ def _add_tag(request, question_id):
Tag name (case-insensitive) must be in request.POST['tag-name'].
If no tag name is provided or Tag.DoesNotExist is raised, return None.
If no tag name is provided or SumoTag.DoesNotExist is raised, return None.
Otherwise, return the canonicalized tag name.
"""
if tag_name := request.POST.get("tag-name", "").strip():
question = get_object_or_404(Question, pk=question_id)
# This raises Tag.DoesNotExist if the tag doesn't exist.
# This raises SumoTag.DoesNotExist if the tag doesn't exist.
canonical_name = add_existing_tag(tag_name, question.tags)
return question, canonical_name

Просмотреть файл

@ -1,13 +1,14 @@
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.db.models.signals import m2m_changed, post_delete, post_save
from kitsune.questions.models import Answer, AnswerVote, Question, QuestionVote
from kitsune.search.decorators import search_receiver
from kitsune.search.es_utils import (
index_object,
delete_object,
index_object,
index_objects_bulk,
remove_from_field,
)
from kitsune.search.decorators import search_receiver
from kitsune.questions.models import Question, QuestionVote, Answer, AnswerVote
from taggit.models import Tag
from kitsune.tags.models import SumoTag
@search_receiver(post_save, Question)
@ -30,7 +31,7 @@ def handle_answer_delete(instance, **kwargs):
index_object.delay("QuestionDocument", instance.question_id)
@search_receiver(post_delete, Tag)
@search_receiver(post_delete, SumoTag)
def handle_tag_delete(instance, **kwargs):
remove_from_field.delay("QuestionDocument", "question_tag_ids", instance.pk)

Просмотреть файл

@ -7,7 +7,8 @@ from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from taggit.models import Tag
from kitsune.tags.models import SumoTag
# TODO: Factor out dependency on taggit so it can be a generic large-vocab
@ -73,7 +74,7 @@ class TagWidget(Widget):
output += "</li>"
return output
tags = Tag.objects.filter(name__in=tag_names)
tags = SumoTag.objects.filter(name__in=tag_names)
representations = [render_one(t) for t in tags]
return "\n".join(representations)
@ -83,7 +84,7 @@ class TagWidget(Widget):
"" if self.read_only or self.async_urls else " deferred"
)
if not self.read_only:
vocab = [t.name for t in Tag.objects.only("name").all()]
vocab = [t.name for t in SumoTag.objects.only("name").all()]
output += ' data-tag-vocab-json="%s"' % escape(json.dumps(vocab))
output += ">"
@ -150,7 +151,7 @@ class TagField(MultipleChoiceField):
def valid_value(self, value):
"""Check the validity of a single tag."""
return Tag.objects.filter(name=value).exists()
return SumoTag.objects.filter(name=value).exists()
def to_python(self, value):
"""Ignore the input field if it's blank; don't make a tag called ''."""

Просмотреть файл

@ -0,0 +1,72 @@
# Generated by Django 4.2.16 on 2024-11-15 02:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
]
operations = [
migrations.CreateModel(
name="SumoTag",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("name", models.CharField(max_length=100, unique=True, verbose_name="name")),
(
"slug",
models.SlugField(
allow_unicode=True, max_length=100, unique=True, verbose_name="slug"
),
),
("is_archived", models.BooleanField(default=False)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["name", "-updated"],
},
),
migrations.CreateModel(
name="SumoTaggedItem",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("object_id", models.IntegerField(db_index=True, verbose_name="object ID")),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(app_label)s_%(class)s_tagged_items",
to="contenttypes.contenttype",
verbose_name="content type",
),
),
(
"tag",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tagged_items",
to="tags.sumotag",
),
),
],
options={
"abstract": False,
},
),
]

Просмотреть файл

@ -1,6 +1,6 @@
from django.db import models
from taggit.managers import TaggableManager
from kitsune.tags.forms import TagField
from taggit.models import GenericTaggedItemBase, TagBase
class BigVocabTaggableManager(TaggableManager):
@ -10,6 +10,26 @@ class BigVocabTaggableManager(TaggableManager):
"""
def formfield(self, form_class=TagField, **kwargs):
def __init__(self, *args, **kwargs):
kwargs.setdefault("through", SumoTaggedItem)
super().__init__(*args, **kwargs)
def formfield(self, form_class=None, **kwargs):
"""Swap in our custom TagField."""
return super(BigVocabTaggableManager, self).formfield(form_class, **kwargs)
from kitsune.tags.forms import TagField
form_class = form_class or TagField
return super().formfield(form_class, **kwargs)
class SumoTag(TagBase):
is_archived = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["name", "-updated"]
class SumoTaggedItem(GenericTaggedItemBase):
tag = models.ForeignKey(SumoTag, related_name="tagged_items", on_delete=models.CASCADE)

Просмотреть файл

@ -1,7 +1,8 @@
import json
from django_jinja import library
from taggit.models import Tag
from kitsune.tags.models import SumoTag
@library.global_function
@ -13,4 +14,4 @@ def tags_to_text(tags):
@library.global_function
def tag_vocab():
"""Returns the tag vocabulary as a JSON object."""
return json.dumps(dict((t[0], t[1]) for t in Tag.objects.values_list("name", "slug")))
return json.dumps(dict((t[0], t[1]) for t in SumoTag.objects.values_list("name", "slug")))

Просмотреть файл

@ -1,14 +1,13 @@
import factory
from django.template.defaultfilters import slugify
import factory
from taggit.models import Tag
from kitsune.sumo.tests import FuzzyUnicode
from kitsune.tags.models import SumoTag
class TagFactory(factory.django.DjangoModelFactory):
class Meta:
model = Tag
model = SumoTag
name = FuzzyUnicode()
slug = factory.LazyAttribute(lambda o: slugify(o.name))

Просмотреть файл

@ -1,8 +1,7 @@
from unittest.mock import Mock
from taggit.models import Tag
from kitsune.sumo.tests import TestCase
from kitsune.tags.models import SumoTag
from kitsune.tags.templatetags.jinja_helpers import tags_to_text
@ -23,6 +22,6 @@ class TestTagsToText(TestCase):
def _tag(slug):
tag = Mock(spec=Tag)
tag = Mock(spec=SumoTag)
tag.slug = slug
return tag

Просмотреть файл

@ -1,6 +1,6 @@
"""Fairly generic tagging utilities headed toward a dedicated app"""
from taggit.models import Tag
from kitsune.tags.models import SumoTag
def add_existing_tag(tag_name, tag_manager):
@ -8,12 +8,12 @@ def add_existing_tag(tag_name, tag_manager):
Given a tag name and a TaggableManager, have the manager add the tag of
that name. The tag is matched case-insensitively. If there is no such tag,
raise Tag.DoesNotExist.
raise SumoTag.DoesNotExist.
Return the canonically cased name of the tag.
"""
# TODO: Think about adding a new method to _TaggableManager upstream.
tag = Tag.objects.get(name__iexact=tag_name)
tag = SumoTag.objects.get(name__iexact=tag_name)
tag_manager.add(tag)
return tag.name

Просмотреть файл

@ -1,7 +1,6 @@
from django.core.management.base import BaseCommand
from taggit.models import TaggedItem
from kitsune.tags.models import SumoTaggedItem
from kitsune.wiki.models import Document
@ -13,5 +12,5 @@ class Command(BaseCommand):
print("### This file is generated by ./manage.py dump_topics. ###")
print("##########################################################")
print("from django.utils.translation import pgettext\n")
for tag in TaggedItem.tags_for(Document):
for tag in SumoTaggedItem.tags_for(Document):
print('pgettext("KB Topic", """{tag}""")'.format(tag=tag.name))

Просмотреть файл

@ -0,0 +1,25 @@
# Generated by Django 4.2.16 on 2024-11-15 02:38
from django.db import migrations
import kitsune.tags.models
class Migration(migrations.Migration):
dependencies = [
("tags", "0001_initial"),
("wiki", "0016_alter_document_contributors"),
]
operations = [
migrations.AlterField(
model_name="document",
name="tags",
field=kitsune.tags.models.BigVocabTaggableManager(
help_text="A comma-separated list of tags.",
through="tags.SumoTaggedItem",
to="tags.SumoTag",
verbose_name="Tags",
),
),
]

Просмотреть файл

@ -6,12 +6,12 @@ from datetime import datetime
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ValidationError
from taggit.models import TaggedItem
from kitsune.products.tests import ProductFactory, TopicFactory
from kitsune.sumo.apps import ProgrammingError
from kitsune.sumo.tests import TestCase
from kitsune.sumo.urlresolvers import reverse
from kitsune.tags.models import SumoTaggedItem
from kitsune.users.tests import GroupFactory, UserFactory, add_permission
from kitsune.wiki.config import (
CATEGORIES,
@ -69,10 +69,10 @@ class DocumentTests(TestCase):
# This works because Django's delete() sees the `tags` many-to-many
# field (actually a manager) and follows the reference.
d = DocumentFactory(tags=["grape"])
self.assertEqual(1, TaggedItem.objects.count())
self.assertEqual(1, SumoTaggedItem.objects.count())
d.delete()
self.assertEqual(0, TaggedItem.objects.count())
self.assertEqual(0, SumoTaggedItem.objects.count())
def test_category_inheritance(self):
"""A document's categories must always be those of its parent."""