зеркало из https://github.com/mozilla/kitsune.git
Merge pull request #6358 from akatsoulas/zd-seg-tags
Custom Tag model and Zendesk segmentation tags.
This commit is contained in:
Коммит
dcbf7871b4
|
@ -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 ''."""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -17,24 +18,26 @@ class Command(BaseCommand):
|
|||
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)
|
||||
|
@ -45,11 +48,11 @@ class Command(BaseCommand):
|
|||
|
||||
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))
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,225 @@
|
|||
# Generated by Django 4.2.16 on 2024-11-15 02:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
ZENDESK_TAGS = [
|
||||
("Gecko", "seg-gecko"),
|
||||
("Review prompt", "seg-review-prompt"),
|
||||
("Tablet UI", "seg-tablet-ui"),
|
||||
("iPad", "seg-ipad"),
|
||||
("Gestures", "seg-gestures"),
|
||||
("Youtube buffering", "seg-youtube-buffering"),
|
||||
("new tab behavior feature", "seg-new-tab-behavior-feat"),
|
||||
("Tabs:reload-bug", "seg-tabs-reload-bug"),
|
||||
("PDF:disable-PDF-viewer-feat", "seg-disable-pdf-viewer-feat"),
|
||||
("PDF:save-as-pdf", "seg-save-as-pdf"),
|
||||
("Memory issue", "seg-memory-issue"),
|
||||
("Battery drain", "seg-battery-drain"),
|
||||
("Parental settings", "seg-parental-settings"),
|
||||
("Sponsored content", "seg-sponsored-content"),
|
||||
("DoH", "seg-doh"),
|
||||
("Private Browsing", "seg-private-browsing"),
|
||||
("Reader Mode", "seg-reader-mode"),
|
||||
("Bookmark:organization", "seg-organization"),
|
||||
("Bookmarks:import/export-feat", "seg-import-export-feature"),
|
||||
("Shortcuts", "seg-shortcuts"),
|
||||
("Custom-url-as-homepage", "seg-custom-url-as-homepage"),
|
||||
("Download-manager-feat", "seg-download-manager-feat"),
|
||||
("Adblock", "seg-adblock"),
|
||||
("Tab:group-feat", "seg-tab-group-feat"),
|
||||
("Data-collection-concern", "seg-data-collection-concern"),
|
||||
("Wont-load", "seg-wont-load"),
|
||||
("no-need-to-reply", "seg-not-actionable"),
|
||||
("Browser collections", "seg-browser-collections"),
|
||||
("About:config", "seg-about-config"),
|
||||
("Swipe gesture", "seg-swipe-gesture"),
|
||||
("Open last tab", "seg-ios-open-last-tab"),
|
||||
("Optimization app size", "seg-optimization-app-size"),
|
||||
("Rendering issue", "seg-rendering-issue"),
|
||||
("Scrolling", "seg-scrolling"),
|
||||
("No secure connection", "seg-no-secure-connection"),
|
||||
("Desktop mode", "seg-breakage-desktop-mode"),
|
||||
("Android Translate", "seg-android-translate"),
|
||||
("iOS Translate", "seg-ios-translate"),
|
||||
("Compliment", "seg-compliment"),
|
||||
("Competitior", "seg-competitor"),
|
||||
("UI-UIX", "seg-ui-ux"),
|
||||
("Updated review", "seg-updated-review"),
|
||||
("Not Firefox", "seg-not-firefox"),
|
||||
("cancel_no", "seg-cancel-no"),
|
||||
("cancel_yes", "seg-cancel-yes"),
|
||||
("Didnt want Renewal-Forgot to cancel", "seg-cancel-forgot"),
|
||||
("Doesnt fit customer's needs/Missing Features", "seg-cancel-disappointed"),
|
||||
("Duplicate subscription", "seg-cancel-duplicate"),
|
||||
("Forgot Coupon", "seg-cancel-forgot-coupon"),
|
||||
("Personal Reasons", "seg-cancel-personal"),
|
||||
("Remaining subscription from upgrade", "seg-cancel-upgrade-leftover"),
|
||||
("Unauthorized Purchase", "seg-cancel-unauthorized"),
|
||||
("Wrong Plan-Account", "seg-cancel-wrong-plan-acct"),
|
||||
("credit_no", "seg-credit-no"),
|
||||
("credit_yes", "seg-credit-yes"),
|
||||
("refund_no", "seg-refund-no"),
|
||||
("refund_yes", "seg-refund-yes"),
|
||||
("refund-courtesy", "seg-refund-courtesy"),
|
||||
("refund-dispute", "seg-refund-dispute"),
|
||||
("refund-duplicate", "seg-refund-duplicate"),
|
||||
("refund-trial", "seg-refund-trial"),
|
||||
("Apple mail", "seg-apple-mail"),
|
||||
("Google/Gmail", "seg-google-gmail"),
|
||||
("MS mail", "seg-ms-mail"),
|
||||
("Proton mail", "seg-proton-mail"),
|
||||
("Yahoo", "seg-yahoo"),
|
||||
("Other mail domain", "seg-other-mail-domain"),
|
||||
("googleplay", "seg-googleplay"),
|
||||
("ios", "seg-ios"),
|
||||
("linux", "seg-linux"),
|
||||
("linux-debian", "seg-linux-debian"),
|
||||
("linux-fedora", "seg-linux-fedora"),
|
||||
("mac", "seg-mac"),
|
||||
("win10", "seg-win10"),
|
||||
("win11", "seg-win11"),
|
||||
("firefox_accounts", "seg-firefox-accounts"),
|
||||
("firefox-private-network-vpn", "seg-firefox-private-network-vpn"),
|
||||
("hubs", "seg-hubs"),
|
||||
("mdn-plus", "seg-mdn-plus"),
|
||||
("monitor", "seg-monitor"),
|
||||
("mozilla-account", "seg-mozilla-account"),
|
||||
("pocket", "seg-pocket"),
|
||||
("relay", "seg-relay"),
|
||||
("feature-request", "seg-feature-request"),
|
||||
("customer-education/user-confusion", "seg-customer-education-user-confusion"),
|
||||
("feedback", "seg-feedback"),
|
||||
("Troubleshooting", "seg-troubleshooting"),
|
||||
("qa/test-Ticket", "seg-qa-test-ticket"),
|
||||
("sunset-questions", "seg-sunset-questions"),
|
||||
("bug-performance-issue", "seg-bug-performance-issue"),
|
||||
("ux-ui-feedback", "seg-ux-ui-feedback"),
|
||||
("incident", "seg-incident"),
|
||||
("spam", "seg-spam"),
|
||||
("non-descript-inquiry", "seg-non-descript-inquiry"),
|
||||
("undefined-issue-No taxonomy", "seg-undefined-issue-no-taxonomy"),
|
||||
("other-mozilla-product/MOFO", "seg-other-mozilla-product-mofo"),
|
||||
("3rdparty-redirect", "seg-3rd-party-redirect"),
|
||||
(
|
||||
"Accounts::Switched-Lost device-No recovery codes",
|
||||
"seg-acct-switched-lost-device-no-rec-codes",
|
||||
),
|
||||
("Accounts::Codes not working-Accepted", "seg-acct-codes-not-working"),
|
||||
("Accounts::Lost Backup/Recovery-Emergency Code", "seg-acct-lost-backup-code"),
|
||||
("Accounts:Not receiving email verification code", "seg-acct-not-receiving-verification-code"),
|
||||
("Accounts::No longer have access to primary email", "seg-acct-lost-access-to-primary-email"),
|
||||
("Accounts::Change Email", "seg-acct-change-email"),
|
||||
("Accounts::Enable-Disable 2FA", "seg-acct-enable-disable-2fa"),
|
||||
("Accounts::Password reset data loss concerns", "seg-acct-data-loss-concern"),
|
||||
("Accounts::Password not being accepted", "seg-acct-pw-not-accepted"),
|
||||
("Accounts::Add/Remove 3rd party auth", "seg-acct-addremove-3rd-pty-auth"),
|
||||
("Accounts::Hacked Account", "seg-acct-hacked-acct"),
|
||||
("Accounts::Compliance::Account Deletion", "seg-acct-delete"),
|
||||
("Accounts::Compliance::Data Access", "seg-acct-data-access"),
|
||||
("Accounts::Compliance::Unsubscribe Request", "seg-acct-unsubscribe"),
|
||||
("Billing::Declined payment::CVV Fail", "seg-bill-cvv"),
|
||||
("Billing::Declined payment::Bank Declined", "seg-bill-declined"),
|
||||
("Billing::Declined payment::Too Many Tries", "seg-bill-too-many-attempts"),
|
||||
("Billing::Billing Inquiry::Purchase Questions", "seg-bill-purchase-question"),
|
||||
("Billing::Billing Inquiry::Invoice Request", "seg-bill-invoice"),
|
||||
("Billing::Billing Inquiry::Sales Tax Inquiry", "seg-bill-tax-inquiry"),
|
||||
("Billing::Billing Inquiry::Sales tax shouldn't be charged", "seg-bill-no-tax"),
|
||||
("Billing::Billing Inquiry::Wrong Sales Tax amount", "seg-bill-wrong-tax"),
|
||||
("Billing::Upgrade/downgrade subscription::Upgrade Subscription", "seg-bill-upgrade"),
|
||||
("Billing::Upgrade/downgrade subscription::Downgrade Subscription", "seg-bill-downgrade"),
|
||||
("Billing::Billing Inquiry::Unsupported Territory", "seg-bill-unsupported-territory"),
|
||||
("Billing::Billing Inquiry::Update Payment Information", "seg-bill-update-payment-info"),
|
||||
("Monitor::Inaccurate scan results", "seg-mntor-wrong-scan-result"),
|
||||
("Monitor::Slow removal", "seg-mntor-slow-remove"),
|
||||
("Monitor::Broker still showing data", "seg-mntor-data-visible-broker"),
|
||||
("Monitor::Added wrong info", "seg-mntor-add-wrong-info"),
|
||||
("Pocket::Unable to save content", "seg-pk-no-save"),
|
||||
("Pocket:: Third party content issues", "seg-pk-3rd-pty-content"),
|
||||
("Pocket::Creating-Managing Collections", "seg-pk-collections"),
|
||||
("Pocket::Unsupported content types", "seg-pk-unsup-content"),
|
||||
("Pocket::Viewing and Sorting Content", "seg-pk-view-sort"),
|
||||
("Pocket::Exporting content", "seg-pk-export"),
|
||||
("Pocket::Extension Issue", "seg-pk-extension"),
|
||||
("Pocket::Mobile App Issues", "seg-pk-mobile-app"),
|
||||
("Pocket::Deleted content recovery", "seg-pk-delete-content-rec"),
|
||||
("Pocket::Unable to delete content", "seg-pk-no-delete"),
|
||||
("Relay::Calls not forwarded", "seg-relay-call-no-fwd"),
|
||||
("Relay:Change Domain", "seg-relay-chg-domain"),
|
||||
("Relay::Change Phone number", "seg-relay-chg-number"),
|
||||
("Relay::Delay receiving forwarded email", "seg-relay-delay-fwd"),
|
||||
("Relay::Deleted-Missing Mask", "seg-relay-missing-mask"),
|
||||
("Relay::Attachment Limit", "seg-relay-attach-limit"),
|
||||
("Relay::bounced forwarded email", "seg-relay-bounced-fwd-email"),
|
||||
("Relay::Forwards not delivered", "seg-relay-no-fwd-deliver"),
|
||||
("Relay::Icon Blocking input field", "seg-relay-icon-block"),
|
||||
("Relay::Missing labels", "seg-relay-label-missing"),
|
||||
("Relay::Paused Account", "seg-relay-pause-acct"),
|
||||
("Relay::Phone-Text limits not reset", "seg-relay-phone-no-limit-reset"),
|
||||
("Relay::Replies not being sent", "seg-relay-no-reply-sent"),
|
||||
("Relay::Site not accepting mask", "seg-relay-no-mask-accept"),
|
||||
("Relay::Spammed Mask Disabled", "seg-relay-spam-mask"),
|
||||
("Relay::Unable to create domain", "seg-relay-no-create-domain"),
|
||||
("Relay::Unable to create mask", "seg-relay-no-create-mask"),
|
||||
("Relay::Unable to register phone number", "seg-relay-no-reg-phone"),
|
||||
("Relay::Unauthorized mask created", "seg-relay-unauth-mask"),
|
||||
("VPN::Add device", "seg-vpn-add-device"),
|
||||
("VPN::Remove device", "seg-vpn-remove-device"),
|
||||
("VPN::Blocked::Application", "seg-vpn-block-app"),
|
||||
("VPN::Blocked::Service", "seg-vpn-block-svc"),
|
||||
("VPN::Blocked::Website", "seg-vpn-block-site"),
|
||||
("VPN::Sever Unavailable", "seg-vpn-server-unavail"),
|
||||
("VPN::Background Service Error", "seg-vpn-bckgrd-svc-err"),
|
||||
("VPN::No Signal", "seg-vpn-no-signal"),
|
||||
("VPN::Wont Connect", "seg-vpn-wont-connect"),
|
||||
("VPN::Unsupported::iOS", "seg-vpn-unsup-ios"),
|
||||
("VPN::Unsupported::Linux", "seg-vpn-unsup-linux"),
|
||||
("VPN::Unsupported::Mac OS", "seg-vpn-unsup-mac"),
|
||||
("VPN::Unsupported::No OS Named", "Seg-vpn-unsup-none"),
|
||||
("VPN::Unsupported::Hardware", "seg-vpn-unsup-hrdware"),
|
||||
("VPN::Unsupported::Windows", "seg-vpn-unsup-win"),
|
||||
]
|
||||
|
||||
|
||||
def migrate_and_create_tags(apps, schema_editor):
|
||||
Tag = apps.get_model("taggit", "Tag")
|
||||
TaggedItem = apps.get_model("taggit", "TaggedItem")
|
||||
SumoTag = apps.get_model("tags", "SumoTag")
|
||||
SumoTaggedItem = apps.get_model("tags", "SumoTaggedItem")
|
||||
|
||||
tag_mapping = {}
|
||||
for tag in Tag.objects.all():
|
||||
sumo_tag, _ = SumoTag.objects.get_or_create(
|
||||
name=tag.name,
|
||||
slug=tag.slug,
|
||||
)
|
||||
tag_mapping[tag.id] = sumo_tag.id
|
||||
|
||||
for tagged_item in TaggedItem.objects.all():
|
||||
sumo_tag_id = tag_mapping.get(tagged_item.tag_id)
|
||||
if sumo_tag_id:
|
||||
SumoTaggedItem.objects.create(
|
||||
tag_id=sumo_tag_id,
|
||||
content_type=tagged_item.content_type,
|
||||
object_id=tagged_item.object_id,
|
||||
)
|
||||
|
||||
for tag_name, tag_slug in ZENDESK_TAGS:
|
||||
SumoTag.objects.get_or_create(name=tag_name, defaults={"slug": tag_slug})
|
||||
|
||||
|
||||
def reverse_migration(apps, schema_editor):
|
||||
SumoTag = apps.get_model("tags", "SumoTag")
|
||||
SumoTaggedItem = apps.get_model("tags", "SumoTaggedItem")
|
||||
SumoTaggedItem.objects.all().delete()
|
||||
SumoTag.objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("tags", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_and_create_tags, reverse_migration),
|
||||
]
|
|
@ -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."""
|
||||
|
|
Загрузка…
Ссылка в новой задаче