Skip to content

Commit

Permalink
Merge pull request #6358 from akatsoulas/zd-seg-tags
Browse files Browse the repository at this point in the history
Custom Tag model and Zendesk segmentation tags.
  • Loading branch information
akatsoulas authored Nov 18, 2024
2 parents 8a157f7 + 7594fd7 commit dcbf787
Show file tree
Hide file tree
Showing 22 changed files with 438 additions and 64 deletions.
4 changes: 2 additions & 2 deletions kitsune/questions/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
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
Expand All @@ -30,6 +29,7 @@
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
Expand Down Expand Up @@ -386,7 +386,7 @@ def add_tags(self, request, pk=None):
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."
)
Expand Down
4 changes: 2 additions & 2 deletions kitsune/questions/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
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
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):
Expand Down Expand Up @@ -80,7 +80,7 @@ def item_pubdate(self, item):

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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
7 changes: 3 additions & 4 deletions kitsune/questions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
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
Expand All @@ -33,7 +32,7 @@
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
Expand Down Expand Up @@ -312,7 +311,7 @@ def auto_tag(self):
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")
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions kitsune/questions/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
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
Expand All @@ -33,6 +32,7 @@
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
Expand Down Expand Up @@ -226,9 +226,9 @@ def test_clear_mutable_metadata(self):

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")
Expand Down Expand Up @@ -520,7 +520,7 @@ def test_add_existing_case_insensitive(self):

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)


Expand Down
4 changes: 2 additions & 2 deletions kitsune/questions/tests/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
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
Expand All @@ -19,6 +18,7 @@
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
Expand Down Expand Up @@ -965,7 +965,7 @@ def setUp(self):

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()
Expand Down
14 changes: 7 additions & 7 deletions kitsune/questions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
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
Expand Down Expand Up @@ -62,6 +61,7 @@
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
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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", "")
Expand All @@ -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
)
Expand Down Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions kitsune/search/signals/questions.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)

Expand Down
9 changes: 5 additions & 4 deletions kitsune/tags/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
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
Expand Down Expand Up @@ -73,7 +74,7 @@ def render_one(tag):
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)

Expand All @@ -83,7 +84,7 @@ def render(self, name, value, attrs=None, renderer=None):
"" 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 += ">"

Expand Down Expand Up @@ -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 ''."""
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.core.management.base import BaseCommand
from fuzzywuzzy import fuzz
from taggit.models import Tag, TaggedItem

from kitsune.tags.models import SumoTag, SumoTaggedItem

SIMILARITY_THRESHOLD = 75

Expand All @@ -17,24 +18,26 @@ def recursively_merge_tags(tag_ids):
if primary_tag_id in deleted_tags:
continue

primary_tag = Tag.objects.get(id=primary_tag_id)
primary_tag = SumoTag.objects.get(id=primary_tag_id)
if primary_tag.slug.startswith("seg-"):
continue

for secondary_tag_id in tag_ids[i + 1 :]:
if secondary_tag_id in deleted_tags:
continue

secondary_tag = Tag.objects.get(id=secondary_tag_id)
secondary_tag = SumoTag.objects.get(id=secondary_tag_id)
similarity = fuzz.ratio(primary_tag.name, secondary_tag.name)
if similarity >= SIMILARITY_THRESHOLD:
duplicate_conflicts = TaggedItem.objects.filter(
duplicate_conflicts = SumoTaggedItem.objects.filter(
tag=secondary_tag,
object_id__in=TaggedItem.objects.filter(tag=primary_tag).values_list(
"object_id", flat=True
),
object_id__in=SumoTaggedItem.objects.filter(
tag=primary_tag
).values_list("object_id", flat=True),
)
duplicate_conflicts.delete()

TaggedItem.objects.filter(tag=secondary_tag).update(tag=primary_tag)
SumoTaggedItem.objects.filter(tag=secondary_tag).update(tag=primary_tag)

secondary_tag.delete()
deleted_tags.add(secondary_tag_id)
Expand All @@ -45,11 +48,11 @@ def recursively_merge_tags(tag_ids):

if merged_any:
remaining_tag_ids = (
Tag.objects.exclude(id__in=deleted_tags)
SumoTag.objects.exclude(id__in=deleted_tags)
.order_by("-id")
.values_list("id", flat=True)
)
return recursively_merge_tags(list(remaining_tag_ids))

tag_ids = Tag.objects.all().order_by("-id").values_list("id", flat=True)
tag_ids = SumoTag.objects.all().order_by("-id").values_list("id", flat=True)
recursively_merge_tags(list(tag_ids))
Loading

0 comments on commit dcbf787

Please sign in to comment.