Use internal IA for the AAQ instead of a config.

This commit is contained in:
Tasos Katsoulas 2024-08-05 16:23:11 +03:00 коммит произвёл Ryan Johnson
Родитель 66ab0809ea
Коммит 41cc8d6bc4
Не найден ключ, соответствующий данной подписи
48 изменённых файлов: 541 добавлений и 952 удалений

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

@ -26,7 +26,7 @@ and follow the following steps.
```
3. Pull base Kitsune Docker images, install node packages and build the Webpack bundle, and create your database.
On non-Apple silicon:
On non-Apple silicon:
```
make init
@ -39,12 +39,12 @@ and follow the following steps.
```
Then:
```
make build
```
3. Run Kitsune.
4. Run Kitsune.
```
make run
@ -163,7 +163,7 @@ running this command::
Create a topic in the admin interface with its `slug` set to `download-and-install` and its product set to the product you just created.
3. Finally add an AAQ locale for that product.
You can do this through the admin interface at `/admin/questions/questionlocale/add/`.
You can do this through the admin interface at `/admin/questions/aaqconfig`.
### Get search working
@ -215,7 +215,7 @@ every time you commit,
pre-commit will check your changes for style problems.
To run it manually you can use the command:
```bash
```bash
$ pre-commit run
```
@ -230,7 +230,7 @@ For more details see the [pre-commit docs](https://pre-commit.com).
### Product Details Initialization
!!! note
!!! note
One of the packages Kitsune uses, ``product_details``, needs to fetch
JSON files containing historical Firefox version data and write them

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

@ -12,7 +12,7 @@ from kitsune.community.utils import (
)
from kitsune.forums.models import Thread
from kitsune.products.models import Product
from kitsune.questions.models import QuestionLocale
from kitsune.questions.models import AAQConfig
from kitsune.search.base import SumoSearchPaginator
from kitsune.search.search import ProfileSearch
from kitsune.sumo.parser import get_object_fallback
@ -59,7 +59,7 @@ def home(request):
# If the locale is enabled for the Support Forum, show the top
# contributors for that locale
if locale in QuestionLocale.objects.locales_list():
if locale in AAQConfig.objects.locales_list():
data["top_contributors_questions"], _ = top_contributors_questions(
locale=locale, product=product
)
@ -117,7 +117,7 @@ def top_contributors(request, area):
results, total = top_contributors_questions(
locale=locale, product=product, count=page_size, page=page
)
locales = QuestionLocale.objects.locales_list()
locales = AAQConfig.objects.locales_list()
case "kb":
results, total = top_contributors_kb(product=product, count=page_size, page=page)
locales = None

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

@ -70,6 +70,6 @@ class ZendeskForm(forms.Form):
if product.slug not in PRODUCTS_WITH_OS:
del self.fields["os"]
def send(self, user, product_config):
def send(self, user, product):
client = ZendeskClient()
return client.create_ticket(user, self.cleaned_data, product_config)
return client.create_ticket(user, self.cleaned_data, product)

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

@ -109,7 +109,7 @@ class ZendeskClient(object):
user=zendesk_user_id, identity=ZendeskIdentity(id=identity_id, value=email)
)
def create_ticket(self, user, ticket_fields, product_config):
def create_ticket(self, user, ticket_fields, product):
"""Create a ticket in Zendesk."""
custom_fields = [
{"id": settings.ZENDESK_PRODUCT_FIELD_ID, "value": ticket_fields.get("product")},
@ -117,7 +117,7 @@ class ZendeskClient(object):
{"id": settings.ZENDESK_COUNTRY_FIELD_ID, "value": ticket_fields.get("country")},
]
ticket_kwargs = {
"subject": ticket_fields.get("subject") or f"{product_config['name']} support",
"subject": ticket_fields.get("subject") or f"{product.title} support",
"comment": {"body": ticket_fields.get("description") or str(NO_RESPONSE)},
"ticket_form_id": settings.ZENDESK_TICKET_FORM_ID,
}

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

@ -14,16 +14,15 @@ from django.conf import settings
from django.contrib.postgres.aggregates import StringAgg
from django.db.models import Count, Exists, F, Max, OuterRef, Q, Subquery
from django.db.models.functions import Coalesce
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _lazy
from django.utils.translation import pgettext_lazy
from django.utils.translation import gettext as _
from markupsafe import Markup
from kitsune.dashboards import LAST_30_DAYS, PERIODS
from kitsune.dashboards.models import WikiDocumentVisits
from kitsune.questions.models import QuestionLocale
from kitsune.questions.models import AAQConfig
from kitsune.sumo.redis_utils import RedisError, redis_client
from kitsune.sumo.templatetags.jinja_helpers import urlparams
from kitsune.sumo.urlresolvers import reverse
@ -232,7 +231,7 @@ def l10n_overview_rows(locale, product=None, user=None):
if product:
total = total.filter(products=product)
if not product.questions_locales.filter(locale=locale).exists():
if not product.questions_enabled(locale):
# The product does not have a forum for this locale.
ignore_categories.append(CANNED_RESPONSES_CATEGORY)
@ -709,7 +708,7 @@ class MostVisitedTranslationsReadout(MostVisitedDefaultLanguageReadout):
if self.product:
qs = qs.filter(products=self.product)
if not self.product.questions_locales.filter(locale=self.locale).exists():
if not self.product.questions_enabled(locale=self.locale):
# The product does not have a forum for this locale.
ignore_categories.append(CANNED_RESPONSES_CATEGORY)
@ -1132,7 +1131,7 @@ class CannedResponsesReadout(Readout):
@classmethod
def should_show_to(cls, request):
return request.LANGUAGE_CODE in QuestionLocale.objects.locales_list()
return request.LANGUAGE_CODE in AAQConfig.objects.locales_list()
def get_queryset(self, max=None):
qs = (

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

@ -85,16 +85,15 @@ class PostsTemplateTests(TestCase):
pq(response.content)('link[rel="canonical"]')[0].attrib["href"],
)
# TODO: This test should be enabled once the responsive redesign milestone is complete.
# def test_long_title_truncated_in_crumbs(self):
# """A very long thread title gets truncated in the breadcrumbs"""
# t = ThreadFactory(title="A thread with a very very very very long title")
# PostFactory(thread=t)
#
# response = get(self.client, "forums.posts", args=[t.forum.slug, t.id])
# doc = pq(response.content)
# crumb = doc("#breadcrumbs li:last-child")
# self.assertEqual(crumb.text(), "A thread with a very very very very...")
def test_long_title_truncated_in_crumbs(self):
"""A very long thread title gets truncated in the breadcrumbs"""
t = ThreadFactory(title="A thread with a very very very very long title")
PostFactory(thread=t)
response = get(self.client, "forums.posts", args=[t.forum.slug, t.id])
doc = pq(response.content)
crumb = doc("#breadcrumbs li:last-child")
self.assertEqual(crumb.text(), "A thread with a very very very very...")
def test_edit_post_moderator(self):
"""Editing post as a moderator works."""

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

@ -11,7 +11,5 @@
{{ content_editor(form.information) }}
<input type="submit" name="save" value="{{ _('Save') }}" />
</form>
{# TODO: Preview <div id="doc-content"></div> #}
</article>
{% endblock %}

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

@ -89,6 +89,7 @@ class TopicAdmin(admin.ModelAdmin):
list_editable = ("display_order", "visible", "in_aaq", "in_nav", "is_archived")
list_filter = (
ArchivedFilter,
"in_aaq",
"parent",
"slug",
)

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

@ -27,8 +27,8 @@
</div>
{%- endmacro %}
{% macro topic_metadata(topics, product=None, product_key=None) %}
{% if product_key and not settings.READ_ONLY %}
{% macro topic_metadata(topics, product=None) %}
{% if product and not settings.READ_ONLY %}
<section class="support-callouts mzp-l-content sumo-page-section--inner">
<div class="card card--ribbon is-inverse heading-is-one-line">
<div class="card--details">
@ -52,11 +52,11 @@
{% endif %}
</p>
<a class="sumo-button primary-button button-lg"
href="{{ url('questions.aaq_step2', product_key=product_key) }}"
href="{{ url('questions.aaq_step2', product_slug=product.slug) }}"
data-event-name="link_click"
data-event-parameters='{
"link_name": "aaq-banner.aaq-step-2",
"link_detail": "{{ product_key }}"
"link_detail": "{{ product.slug }}"
}'>
{% if product and product.has_ticketing_support %}
{{ _('Get Support') }}

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

@ -150,7 +150,7 @@
{{ help_topics(topics) }}
</div>
{{ topic_metadata(topics, product=product, product_key=product_key) }}
{{ topic_metadata(topics, product=product) }}
{% if featured %}
<section class="mzp-l-content mzp-l-content sumo-page-section--inner">

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

@ -9,4 +9,11 @@ class NonArchivedManager(Manager):
class ProductManager(NonArchivedManager):
def with_question_forums(self, request):
return self.filter(questions_locales__locale=request.LANGUAGE_CODE).filter(codename="")
return (
self.filter(
aaq_configs__is_active=True,
aaq_configs__enabled_locales__locale=request.LANGUAGE_CODE,
)
.filter(codename="")
.distinct()
)

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

@ -3,8 +3,6 @@
from django.db import migrations
from kitsune.questions.config import products as PRODUCTS_CONFIG
# The key is the topic slug to migrate to and the value is the list
# of topic slugs to migrate from,
# If the topic for each product does not exist, it will be created

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

@ -80,7 +80,7 @@ class Product(BaseProductTopic):
return bool(self.codename)
def questions_enabled(self, locale):
return self.questions_locales.filter(locale=locale).exists()
return self.aaq_configs.filter(is_active=True, enabled_locales__locale=locale).exists()
def get_absolute_url(self):
return reverse("products.product", kwargs={"slug": self.slug})

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

@ -5,6 +5,7 @@ from pyquery import PyQuery as pq
from kitsune.products.models import HOT_TOPIC_SLUG
from kitsune.products.tests import ProductFactory, TopicFactory
from kitsune.questions.models import QuestionLocale
from kitsune.questions.tests import AAQConfigFactory
from kitsune.search.tests import Elastic7TestCase
from kitsune.sumo.urlresolvers import reverse
from kitsune.wiki.tests import ApprovedRevisionFactory, DocumentFactory, HelpfulVoteFactory
@ -19,7 +20,7 @@ class ProductViewsTestCase(Elastic7TestCase):
locale, _ = QuestionLocale.objects.get_or_create(locale=settings.LANGUAGE_CODE)
for i in range(3):
p = ProductFactory(visible=True)
p.questions_locales.add(locale)
AAQConfigFactory(product=p, enabled_locales=[locale], is_active=True)
# GET the products page and verify the content.
r = self.client.get(reverse("products"), follow=True)
@ -32,7 +33,7 @@ class ProductViewsTestCase(Elastic7TestCase):
# Create a product.
p = ProductFactory()
locale, _ = QuestionLocale.objects.get_or_create(locale=settings.LANGUAGE_CODE)
p.questions_locales.add(locale)
AAQConfigFactory(product=p, enabled_locales=[locale], is_active=True)
# Create some topics.
TopicFactory(slug=HOT_TOPIC_SLUG, products=[p], visible=True)

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

@ -7,7 +7,6 @@ from django.shortcuts import get_object_or_404, redirect, render
from product_details import product_details
from kitsune.products.models import Product, Topic, TopicSlugHistory
from kitsune.questions import config as aaq_config
from kitsune.wiki.decorators import check_simple_wiki_locale
from kitsune.wiki.facets import documents_for, topics_for
from kitsune.wiki.models import Document, Revision
@ -22,15 +21,6 @@ def product_list(request):
return render(request, template, {"products": products})
def _get_aaq_product_key(slug):
product_key = ""
for k, v in aaq_config.products.items():
if isinstance(v, dict):
if v.get("product") == slug:
product_key = k
return product_key or None
@check_simple_wiki_locale
def product_landing(request, slug):
"""The product landing page."""
@ -59,7 +49,6 @@ def product_landing(request, slug):
request,
"products/product.html",
{
"product_key": _get_aaq_product_key(product.slug),
"product": product,
"products": Product.active.filter(visible=True),
"topics": topics_for(request.user, product=product, parent=None),
@ -105,7 +94,7 @@ def document_listing(request, topic_slug, product_slug=None, subtopic_slug=None)
product = get_object_or_404(Product, slug=product_slug)
request.session["aaq_context"] = {
"has_ticketing_support": product.has_ticketing_support,
"key": _get_aaq_product_key(product_slug),
"product_slug": product_slug,
"has_public_forum": product.questions_enabled(locale=request.LANGUAGE_CODE),
}

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

@ -1,20 +1,69 @@
import json
from django import forms
from django.conf import settings
from django.contrib import admin
from kitsune.questions.models import QuestionLocale, AAQConfig
from kitsune.questions.models import AAQConfig, QuestionLocale
class QuestionLocaleAdmin(admin.ModelAdmin):
list_display = ("locale",)
ordering = ("locale",)
filter_horizontal = ("products",)
class PrettyJSONEncoder(json.JSONEncoder):
def __init__(self, *args, indent, sort_keys, **kwargs):
super().__init__(*args, indent=2, sort_keys=True, **kwargs)
admin.site.register(QuestionLocale, QuestionLocaleAdmin)
class AAQConfigForm(forms.ModelForm):
extra_fields = forms.JSONField(encoder=PrettyJSONEncoder)
class Meta:
model = AAQConfig
fields = "__all__"
class QuestionLocaleInlineForm(forms.ModelForm):
locale = forms.ChoiceField(
choices=settings.LANGUAGE_CHOICES_ENGLISH,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk:
self.fields["locale"].initial = self.instance.questionlocale.locale
def save(self, commit=True):
locale_code = self.cleaned_data["locale"]
if self.has_changed() and self.instance:
question_locale, _ = QuestionLocale.objects.get_or_create(locale=locale_code)
self.instance.questionlocale = question_locale
super().save(commit)
return self.instance
class Meta:
model = QuestionLocale
fields = ["locale"]
class QuestionLocaleAdmin(admin.TabularInline):
form = QuestionLocaleInlineForm
model = AAQConfig.enabled_locales.through
extra = 0
class AAQConfigAdmin(admin.ModelAdmin):
list_display = ("product",)
form = AAQConfigForm
list_display = ("title", "product", "is_active")
autocomplete_fields = ("pinned_articles",)
list_editable = ("is_active",)
inlines = [QuestionLocaleAdmin]
fields = (
"title",
"product",
"is_active",
"pinned_articles",
"associated_tags",
"extra_fields",
)
admin.site.register(AAQConfig, AAQConfigAdmin)

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

@ -1,7 +1,3 @@
from collections import OrderedDict
from django.utils.translation import gettext_lazy as _lazy
# The number of answers per page.
ANSWERS_PER_PAGE = 20
@ -17,531 +13,3 @@ OFFTOPIC_TAG_NAME = "offtopic"
# How long until a question is automatically taken away from a user
TAKE_TIMEOUT = 600
# AAQ config:
products = OrderedDict(
[
(
"desktop",
{
"name": _lazy("Firefox"),
"subtitle": _lazy("Web browser for Windows, Mac and Linux"),
"extra_fields": ["troubleshooting", "ff_version", "os"],
"tags": ["desktop"],
"product": "firefox",
"categories": OrderedDict(
[
# TODO: Just use the IA topics for this.
# See bug 979397
(
"install-and-update",
{
"name": _lazy("Install and update"),
"topic": "install-and-update",
"tags": ["install-and-update"],
},
),
(
"protect-your-privacy",
{
"name": _lazy("Protect your privacy"),
"topic": "protect-your-privacy",
"tags": ["protect-your-privacy"],
},
),
(
"customize",
{
"name": _lazy("Customize settings and preferences"),
"topic": "customize-settings-and-preferences",
"tags": ["customize-settings-and-preferences"],
},
),
(
"troubleshooting",
{
"name": _lazy("Troubleshooting"),
"topic": "troubleshooting",
"tags": ["troubleshooting"],
},
),
(
"tips",
{
"name": _lazy("Tips and tricks"),
"topic": "tips",
"tags": ["tips"],
},
),
(
"bookmarks",
{
"name": _lazy("Bookmarks"),
"topic": "bookmarks",
"tags": ["bookmarks"],
},
),
(
"cookies",
{
"name": _lazy("Cookies"),
"topic": "cookies",
"tags": ["cookies"],
},
),
(
"tabs",
{
"name": _lazy("Tabs"),
"topic": "tabs",
"tags": ["tabs"],
},
),
(
"website-breakages",
{
"name": _lazy("Website breakages"),
"topic": "website-breakages",
"tags": ["website-breakages"],
},
),
(
"sync",
{
"name": _lazy("Firefox Sync"),
"topic": "sync",
"tags": ["sync"],
},
),
(
"other",
{
"name": _lazy("Other"),
"topic": "other",
"tags": ["other"],
},
),
]
),
},
),
(
"mobile",
{
"name": _lazy("Firefox for Android"),
"subtitle": _lazy("Web browser for Android smartphones and tablets"),
"extra_fields": ["ff_version", "os"],
"tags": ["mobile"],
"product": "mobile",
"categories": OrderedDict(
[
# TODO: Just use the IA topics for this.
# See bug 979397
(
"install-and-update",
{
"name": _lazy("Install and update"),
"topic": "install-and-update",
"tags": ["install-and-update"],
},
),
(
"protect-your-privacy",
{
"name": _lazy("Protect your privacy"),
"topic": "protect-your-privacy",
"tags": ["protect-your-privacy"],
},
),
(
"customize",
{
"name": _lazy("Customize settings and preferences"),
"topic": "customize-settings-and-preferences",
"tags": ["customize-settings-and-preferences"],
},
),
(
"troubleshooting",
{
"name": _lazy("Troubleshooting"),
"topic": "troubleshooting",
"tags": ["troubleshooting"],
},
),
(
"tips",
{
"name": _lazy("Tips and tricks"),
"topic": "tips",
"tags": ["tips"],
},
),
(
"bookmarks",
{
"name": _lazy("Bookmarks"),
"topic": "bookmarks",
"tags": ["bookmarks"],
},
),
(
"cookies",
{
"name": _lazy("Cookies"),
"topic": "cookies",
"tags": ["cookies"],
},
),
(
"tabs",
{
"name": _lazy("Tabs"),
"topic": "tabs",
"tags": ["tabs"],
},
),
(
"websites",
{
"name": _lazy("Websites"),
"topic": "websites",
"tags": ["websites"],
},
),
(
"sync",
{
"name": _lazy("Firefox Sync"),
"topic": "sync",
"tags": ["sync"],
},
),
(
"other",
{
"name": _lazy("Other"),
"topic": "other",
"tags": ["other"],
},
),
]
),
},
),
(
"ios",
{
"name": _lazy("Firefox for iOS"),
"subtitle": _lazy("Firefox for iPhone, iPad and iPod touch devices"),
"extra_fields": ["ff_version", "os"],
"tags": ["ios"],
"product": "ios",
"categories": OrderedDict(
[
(
"install-and-update",
{
"name": _lazy("Install and update"),
"topic": "install-and-update",
"tags": ["install-and-update"],
},
),
(
"how-to-use-firefox-ios",
{
"name": _lazy("How to use Firefox for iOS"),
"topic": "how-to-use-firefox-ios",
"tags": ["how-to-use-firefox-ios"],
},
),
(
"troubleshooting",
{
"name": _lazy("Troubleshooting"),
"topic": "troubleshooting",
"tags": ["troubleshooting"],
},
),
(
"sync",
{
"name": _lazy("Sync"),
"topic": "save-and-share-firefox-ios",
"tags": ["sync"],
},
),
(
"bookmarks",
{
"name": _lazy("Bookmarks"),
"topic": "bookmarks-and-tabs-firefox-ios",
"tags": ["bookmarks"],
},
),
(
"tabs",
{
"name": _lazy("Tabs"),
"topic": "bookmarks-and-tabs-firefox-ios",
"tags": ["tabs"],
},
),
(
"protect-your-privacy",
{
"name": _lazy("Protect your privacy"),
"topic": "protect-your-privacy",
"tags": ["protect-your-privacy"],
},
),
]
),
},
),
(
"firefox-enterprise",
{
"name": _lazy("Firefox for Enterprise"),
"subtitle": _lazy("Firefox Quantum for businesses"),
"extra_fields": ["ff_version", "os"],
"tags": [],
"product": "firefox-enterprise",
"categories": OrderedDict(
[
(
"install-and-update",
{
"name": _lazy("Install and update"),
"topic": "install-and-update",
"tags": ["deployment", "install-and-update"],
},
),
(
"install-and-manage-add-ons",
{
"name": _lazy("Install and manage add-ons"),
"topic": "install-and-manage-add-ons",
"tags": ["customization", "install-and-manage-add-ons"],
},
),
(
"customize",
{
"name": _lazy("Customize settings and preferences"),
"topic": "customize-settings-and-preferences",
"tags": ["customize-settings-and-preferences"],
},
),
]
),
},
),
(
"mdn-plus",
{
"name": _lazy("MDN Plus"),
"subtitle": _lazy("MDN Plus provides a custom experience for MDN supporters."),
"extra_fields": [],
"tags": ["mdn-plus"],
"product": "mdn-plus",
"categories": OrderedDict([]),
},
),
(
"firefox-private-network-vpn",
{
"name": _lazy("Mozilla VPN"),
"subtitle": _lazy("VPN for Windows 10, Android and iOS devices"),
"extra_fields": [],
"tags": [],
"product": "firefox-private-network-vpn",
"categories": OrderedDict(
[
(
"technical",
{
"name": _lazy("Technical"),
"topic": "technical",
"tags": ["technical"],
},
),
(
"accounts",
{
"name": _lazy("Accounts"),
"topic": "accounts",
"tags": ["accounts"],
},
),
(
"Payments",
{
"name": _lazy("Payments"),
"topic": "payments",
"tags": ["payments"],
},
),
(
"Troubleshooting",
{
"name": _lazy("Troubleshooting"),
"topic": "troubleshooting",
"tags": ["troubleshooting"],
},
),
]
),
},
),
(
"relay",
{
"name": _lazy("Firefox Relay"),
"subtitle": _lazy("Service that lets you create aliases to hide your real email"),
"extra_fields": [],
"tags": ["relay"],
"product": "relay",
"categories": OrderedDict([]),
},
),
(
"monitor",
{
"name": _lazy("Mozilla Monitor"),
"subtitle": _lazy("Stay informed and take back control of your exposed data"),
"extra_fields": [],
"tags": ["monitor"],
"product": "monitor",
"categories": OrderedDict([]),
},
),
(
"pocket",
{
"name": _lazy("Pocket"),
"subtitle": _lazy("The webs most intriguing articles"),
"extra_fields": [],
"tags": ["pocket"],
"product": "pocket",
"categories": OrderedDict([]),
},
),
(
"thunderbird",
{
"name": _lazy("Thunderbird"),
"subtitle": _lazy("Email software for Windows, Mac and Linux"),
"extra_fields": [],
"tags": [],
"product": "thunderbird",
"categories": OrderedDict(
[
# TODO: Just use the IA topics for this.
# See bug 979397
(
"install-and-update",
{
"name": _lazy("Install and update"),
"topic": "install-and-update",
"tags": ["install-and-update"],
},
),
(
"protect-your-privacy",
{
"name": _lazy("Protect your privacy"),
"topic": "protect-your-privacy",
"tags": ["protect-your-privacy"],
},
),
(
"customize",
{
"name": _lazy("Customize settings and preferences"),
"topic": "customize-settings-and-preferences",
"tags": ["customize-settings-and-preferences"],
},
),
(
"troubleshooting",
{
"name": _lazy("Troubleshooting"),
"topic": "troubleshooting",
"tags": ["troubleshooting"],
},
),
(
"calendar",
{
"name": _lazy("Calendar"),
"topic": "calendar",
"tags": ["calendar"],
},
),
(
"other",
{
"name": _lazy("Other"),
"topic": "other",
"tags": ["other"],
},
),
]
),
},
),
(
"focus",
{
"name": _lazy("Firefox Focus"),
"subtitle": _lazy("Automatic privacy browser and content blocker"),
"extra_fields": ["ff_version", "os"],
"tags": ["focus-firefox"],
"product": "focus-firefox",
"categories": OrderedDict(
[
(
"Focus-ios",
{
"name": _lazy("Firefox Focus for iOS"),
"topic": "Focus-ios",
"tags": ["Focus-ios"],
},
),
(
"firefox-focus-android",
{
"name": _lazy("Firefox Focus for Android"),
"topic": "firefox-focus-android",
"tags": ["firefox-focus-android"],
},
),
]
),
},
),
(
"mozilla-account",
{
"name": _lazy("Mozilla Account"),
"subtitle": _lazy("Mozilla account is the account system for Mozilla"),
"extra_fields": [],
"tags": [],
"product": "mozilla-account",
"categories": OrderedDict([]),
},
),
]
)
def add_backtrack_keys(products):
"""Insert 'key' keys so we can go from product or category back to key."""
for p_k, p_v in products.items():
p_v["key"] = p_k
for c_k, c_v in p_v["categories"].items():
c_v["key"] = c_k
add_backtrack_keys(products)

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

@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _lazy
from kitsune.products.models import Topic
from kitsune.questions.events import QuestionReplyEvent
from kitsune.questions.models import Answer, Question
from kitsune.questions.models import AAQConfig, Answer, Question
from kitsune.questions.utils import remove_pii
from kitsune.sumo.forms import KitsuneBaseForumForm
from kitsune.upload.models import ImageAttachment
@ -64,10 +64,7 @@ class EditQuestionForm(forms.ModelForm):
class Meta:
model = Question
fields = [
"title",
"content",
]
fields = ["title", "content"]
def __init__(self, product=None, *args, **kwargs):
"""Init the form.
@ -81,51 +78,8 @@ class EditQuestionForm(forms.ModelForm):
extra_fields = []
if product:
extra_fields += product.get("extra_fields", [])
if "sites_affected" in extra_fields:
field = forms.CharField(
label=SITE_AFFECTED_LABEL,
initial="http://",
required=False,
max_length=255,
widget=forms.TextInput(),
)
self.fields["sites_affected"] = field
if "crash_id" in extra_fields:
field = forms.CharField(
label=CRASH_ID_LABEL,
help_text=CRASH_ID_HELP,
required=False,
max_length=255,
widget=forms.TextInput(),
)
self.fields["crash_id"] = field
if "frequency" in extra_fields:
field = forms.ChoiceField(
label=FREQUENCY_LABEL, choices=FREQUENCY_CHOICES, required=False
)
self.fields["frequency"] = field
if "started" in extra_fields:
field = forms.CharField(
label=STARTED_LABEL,
required=False,
max_length=255,
widget=forms.TextInput(),
)
self.fields["started"] = field
if "addon" in extra_fields:
field = forms.CharField(
label=ADDON_LABEL,
required=False,
max_length=255,
widget=forms.TextInput(),
)
self.fields["addon"] = field
aaq_config = AAQConfig.objects.get(product=product, is_active=True)
extra_fields = aaq_config.extra_fields
if "ff_version" in extra_fields:
self.fields["ff_version"] = forms.CharField(
@ -158,7 +112,7 @@ class EditQuestionForm(forms.ModelForm):
def metadata_field_keys(self):
"""Returns the keys of the metadata fields for the current
form instance"""
non_metadata_fields = ["title", "content", "email", "notifications"]
non_metadata_fields = ["title", "content", "email", "notifications", "category"]
def metadata_filter(x):
return x not in non_metadata_fields
@ -208,7 +162,12 @@ class EditQuestionForm(forms.ModelForm):
class NewQuestionForm(EditQuestionForm):
"""Form to start a new question"""
category = forms.ChoiceField(label=CATEGORY_LABEL, choices=[])
category = forms.ModelChoiceField(
label=CATEGORY_LABEL,
queryset=Topic.objects.none(),
empty_label="Please select",
required=True,
)
# Collect user agent only when making a question for the first time.
# Otherwise, we could grab moderators' user agents.
@ -223,22 +182,17 @@ class NewQuestionForm(EditQuestionForm):
super(NewQuestionForm, self).__init__(product=product, *args, **kwargs)
if product:
category_choices = [
(key, value["name"]) for key, value in product["categories"].items()
]
category_choices.insert(0, ("", "Please select"))
self.fields["category"].choices = category_choices
topics = Topic.active.filter(products=product, in_aaq=True)
self.fields["category"].queryset = topics
def save(self, user, locale, product, product_config, *args, **kwargs):
def save(self, user, locale, product, *args, **kwargs):
self.instance.creator = user
self.instance.locale = locale
self.instance.product = product
category_config = product_config["categories"][self.cleaned_data["category"]]
if category_config:
t = category_config.get("topic")
if t:
self.instance.topic = Topic.active.get(slug=t, products=product)
category = self.cleaned_data["category"]
if category:
self.instance.topic = category
question = super(NewQuestionForm, self).save(*args, **kwargs)
@ -254,11 +208,6 @@ class NewQuestionForm(EditQuestionForm):
# User successfully submitted a new question
question.add_metadata(**self.cleaned_metadata)
if product_config:
# TODO: This add_metadata call should be removed once we are
# fully IA-driven (sync isn't special case anymore).
question.add_metadata(product=product_config["key"])
# The first time a question is saved, automatically apply some tags:
question.auto_tag()

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

@ -7,33 +7,27 @@
<div class="sumo-page-section--inner">
<div id="product-picker" class="sumo-card-grid stack-on-mobile">
<div class="scroll-wrap">
{% for key, product in products.items() %}
<div class="card card--product zoom-on-hover">
{% if key == 'other' %}
<img src="{{ webpack_static('sumo/img/mozilla-icon.png') }}"
alt="{{ _(product.name) }}"
class="card--icon">
{% else %}
<img src="{{ image_for_product(product.product) }}"
{% for product in products %}
<div class="card card--product zoom-on-hover">
<img src="{{ image_for_product(product) }}"
alt="{{ pgettext('DB: products.Product.title', product.title) }}"
class="card--icon">
{% endif %}
<div class="card--details">
<h3 class="card--title">
<a class="expand-this-link title"
href="{{ url('questions.aaq_step2', product_key=key) }}"
data-event-name="link_click"
data-event-parameters='{
"link_name": "aaq-step-2",
"link_detail": "{{ product.slug }}"
}'>
{{ product.name }}
</a>
</h3>
<p class="card--desc">{{ product.subtitle }}</p>
<div class="card--details">
<h3 class="card--title">
<a class="expand-this-link title"
href="{{ url('questions.aaq_step2', product_slug=product.slug) }}"
data-event-name="link_click"
data-event-parameters='{
"link_name": "aaq-step-2",
"link_detail": "{{ product.slug }}"
}'>
{{ product.title }}
</a>
</h3>
<p class="card--desc">{{ product.description }}</p>
</div>
</div>
</div>
{% endfor %}
<div class="card card--centered-button">
<a class="sumo-button primary-button button-lg" href="{{ url('questions.home') }}">{{ _('Browse All Product Forums')}}</a>
@ -43,7 +37,7 @@
</div>
{%- endmacro %}
{% macro progress_bar(step, product_key=None) %}
{% macro progress_bar(step, product_slug=None) %}
<ul class="progress">
<li class="progress--item {% if step > 1 %}is-complete{% elif step == 1 %}is-current{% endif %}">
<a class="progress--link"
@ -64,8 +58,8 @@
</li>
<li class="progress--item {% if step > 2 %}is-complete{% elif step == 2 %}is-current{% endif %}">
<a class="progress--link"
{% if step == 3 and product_key %}
href="{{ url('questions.aaq_step2', product_key) }}"
{% if step == 3 and product_slug %}
href="{{ url('questions.aaq_step2', product_slug) }}"
{% else %}
href="#" disabled
{% endif %}>
@ -102,7 +96,7 @@
{{ _('Ask a Question') }}
</h2>
{% if aaq_context %}
{% set link_detail = aaq_context.key %}
{% set link_detail = aaq_context.product_slug %}
{% if request.user.is_authenticated %}
{% if aaq_context.has_ticketing_support %}
<p> {{ _('Still need help? Continue to contact our support team.') }}</p>
@ -122,7 +116,7 @@
{% set next_step = url('wiki.document', 'get-community-support')|urlparams(exit_aaq=1) %}
{% else %}
{% set link_name = "aaq-widget.aaq-step-3" %}
{% set next_step = url('questions.aaq_step3', product_key=aaq_context.key) %}
{% set next_step = url('questions.aaq_step3', product_slug=aaq_context.product_slug) %}
{% endif %}
{% else %}
<p>{{ _('Still need help? Continue to ask your question and get help.') }}</p>
@ -184,7 +178,7 @@
<div class="text-center-to-left-on-large aaq-popular-topics">
<h2 class="sumo-page-subheading">{{ _('Popular Topics') }}</h2>
</div>
{{ help_topics(topics, product_slug=product.product, new_tab=True) }}
{{ help_topics(topics, product_slug=product.slug, new_tab=True) }}
</div>
</section>
</div>

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

@ -4,9 +4,6 @@ Base template for creating a new or editing an existing question. The cases for
showing uneditable product and category names are handled here. Fancier
behaviors like editing them can be provided by overriding blocks.
If there's a `form`, we also expect `current_product` and `current_category` so
we can compute the edit-title URL.
#}
{% extends "questions/base.html" %}
{% from "layout/errorlist.html" import errorlist %}
@ -22,7 +19,7 @@ we can compute the edit-title URL.
<div id="main-content" class="aaq">
<article class="main mzp-l-content sumo-page-section--inner">
{% if current_step %}
{{ progress_bar(current_step, product_key=current_product.key) }}
{{ progress_bar(current_step, product_slug=current_product.slug) }}
{% if current_step > 1 and not has_ticketing_support %}
{{ scam_banner() }}
@ -30,7 +27,7 @@ we can compute the edit-title URL.
{% endif %}
{% block formwrap %}
<img class="page-heading--logo" src="{{ image_for_product(current_product.get('product', '')) }}" alt="{{ current_product.get('name', '') }} logo" />
<img class="page-heading--logo" src="{{ image_for_product(current_product) }}" alt="{{ current_product.title }} logo" />
{# TODO: hook this up to the backend when subproducts are in.
<div class="mzp-c-menu-list subheading-dropdown">

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

@ -510,10 +510,6 @@
<section id="more-system-details" class="mzp-u-modal-content text-body-md" title="{{ _('Additional System Details') }}" data-target="#show-more-details">
<h2 class="sumo-page-subheading">{{ _('Additional System Details') }}</h2>
{% if question.metadata.sites_affected %}
<h3 class="sumo-card-heading">{{ _('Sites Affected') }}</h3>
<p>{{ question.metadata.sites_affected }}</p>
{% endif %}
{% if question.metadata.crash_id %}
<h3 class="sumo-card-heading">{{ _('Crash ID') }}</h3>
<p>{{ question.metadata.crash_id }}</p>

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

@ -102,8 +102,8 @@
{% endif %}
</div>
<div class="sumo-l-two-col--sidebar forum--masthead-cta">
{% if product_key %}
{% set aaq_url = url('questions.aaq_step2', product_key=product_key) %}
{% if product_slug and not multiple_products %}
{% set aaq_url = url('questions.aaq_step2', product_slug=product_slug) %}
{% else %}
{% set aaq_url = url('questions.aaq_step1') %}
{% endif %}

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

@ -73,9 +73,9 @@ class QuestionManager(Manager):
return self.filter(solution__isnull=False)
class QuestionLocaleManager(Manager):
class AAQConfigManager(Manager):
def locales_list(self):
return self.values_list("locale", flat=True)
return self.get_queryset().values_list("enabled_locales__locale", flat=True).distinct()
class AnswerManager(Manager):

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

@ -0,0 +1,51 @@
# Generated by Django 4.2.14 on 2024-08-02 04:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wiki", "0016_alter_document_contributors"),
("taggit", "0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx"),
("questions", "0001_squashed_0013_alter_question_is_archived"),
]
operations = [
migrations.AddField(
model_name="aaqconfig",
name="associated_tags",
field=models.ManyToManyField(blank=True, null=True, to="taggit.tag"),
),
migrations.AddField(
model_name="aaqconfig",
name="enabled_locales",
field=models.ManyToManyField(to="questions.questionlocale"),
),
migrations.AddField(
model_name="aaqconfig",
name="extra_fields",
field=models.JSONField(blank=True, default=list),
),
migrations.AddField(
model_name="aaqconfig",
name="is_active",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="aaqconfig",
name="title",
field=models.CharField(default="", max_length=255),
),
migrations.AlterField(
model_name="aaqconfig",
name="pinned_articles",
field=models.ManyToManyField(blank=True, null=True, to="wiki.document"),
),
migrations.AddConstraint(
model_name="aaqconfig",
constraint=models.UniqueConstraint(
fields=("product", "is_active"), name="unique_active_config"
),
),
]

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

@ -0,0 +1,59 @@
# Generated by Django 4.2.14 on 2024-08-02 05:24
from django.db import migrations
from django.db.models import Count
def migrate_question_locales(apps, schema_editor):
AAQConfig = apps.get_model("questions", "AAQConfig")
QuestionLocale = apps.get_model("questions", "QuestionLocale")
for question_locale in QuestionLocale.objects.all():
for product in question_locale.products.all():
if not product.is_archived:
title = f"{product.title} Configuration"
aaq_config, created = AAQConfig.objects.get_or_create(
product=product, defaults={"title": title}
)
if not created:
aaq_config.title = title
aaq_config.is_active = False
aaq_config.enabled_locales.add(question_locale)
match product.slug:
case "firefox":
aaq_config.extra_fields = ["troubleshooting", "ff_version", "os"]
case "mobile":
aaq_config.extra_fields = ["ff_version", "os"]
case "ios":
aaq_config.extra_fields = ["ff_version", "os"]
case "firefox-enterprise":
aaq_config.extra_fields = ["ff_version", "os"]
case "focus-firefox":
aaq_config.extra_fields = ["ff_version", "os"]
case _:
pass
aaq_config.save()
# Best effort to enable by default configurations
annotated_configs = (
AAQConfig.objects.filter(product__is_archived=False)
.values("product")
.annotate(config_count=Count("product"))
)
unique_products = annotated_configs.filter(config_count=1)
AAQConfig.objects.filter(product__in=[i["product"] for i in unique_products]).update(
is_active=True
)
def backwards(apps, schema_editor): ...
class Migration(migrations.Migration):
dependencies = [
("questions", "0014_aaqconfig_associated_tags_aaqconfig_enabled_locales_and_more"),
]
operations = [migrations.RunPython(migrate_question_locales, backwards)]

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

@ -0,0 +1,17 @@
# Generated by Django 4.2.14 on 2024-08-02 06:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("questions", "0015_auto_20240802_0524"),
]
operations = [
migrations.RemoveField(
model_name="questionlocale",
name="products",
),
]

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

@ -25,7 +25,7 @@ from taggit.models import Tag
from kitsune.flagit.models import FlaggedObject
from kitsune.products.models import Product, Topic
from kitsune.questions import config
from kitsune.questions.managers import AnswerManager, QuestionLocaleManager, QuestionManager
from kitsune.questions.managers import AAQConfigManager, AnswerManager, QuestionManager
from kitsune.questions.tasks import update_answer_pages, update_question_votes
from kitsune.sumo.i18n import split_into_language_and_path
from kitsune.sumo.models import LocaleField, ModelBase
@ -37,7 +37,6 @@ from kitsune.tags.utils import add_existing_tag
from kitsune.upload.models import ImageAttachment
from kitsune.wiki.models import Document
log = logging.getLogger("k.questions")
VOTE_METADATA_MAX_LENGTH = 1000
@ -246,12 +245,13 @@ class Question(AAQBase):
@property
def product_config(self):
"""Return the product config this question is about or an empty
mapping if unknown."""
md = self.metadata
if "product" in md:
return config.products.get(md["product"], {})
return {}
"""Return the product config this question is about or None"""
try:
aaq_config = AAQConfig.objects.get(is_active=True, product=self.product)
except AAQConfig.DoesNotExist:
return None
else:
return aaq_config
@property
def product_slug(self):
@ -263,22 +263,17 @@ class Question(AAQBase):
return self._product_slug
@property
def category_config(self):
"""Return the category this question refers to or an empty mapping if
unknown."""
md = self.metadata
if self.product_config and "category" in md:
return self.product_config["categories"].get(md["category"], {})
return {}
def auto_tag(self):
"""Apply tags to myself that are implied by my metadata.
You don't need to call save on the question after this.
"""
to_add = self.product_config.get("tags", []) + self.category_config.get("tags", [])
to_add = []
if product_config := self.product_config:
for tag in product_config.associated_tags.all():
to_add.append(tag)
version = self.metadata.get("ff_version", "")
# Remove the beta (b*), aurora (a2) or nightly (a1) suffix.
@ -299,15 +294,19 @@ class Question(AAQBase):
to_add.append("Firefox %s" % version)
to_add.append("beta")
self.tags.add(*to_add)
# Add a tag for the OS if it already exists as a tag:
os = self.metadata.get("os")
if os:
if os := self.metadata.get("os"):
try:
add_existing_tag(os, self.tags)
except Tag.DoesNotExist:
pass
product_md = self.metadata.get("product")
topic_md = self.metadata.get("category")
if self.product and not product_md:
to_add.append(self.product.slug)
if self.topic and not topic_md:
to_add.append(self.topic.slug)
self.tags.add(*to_add)
def get_absolute_url(self):
# Note: If this function changes, we need to change it in
@ -800,20 +799,34 @@ class QuestionVisits(ModelBase):
class QuestionLocale(ModelBase):
locale = LocaleField(choices=settings.LANGUAGE_CHOICES_ENGLISH, unique=True)
products = models.ManyToManyField(Product, related_name="questions_locales")
objects = QuestionLocaleManager()
class Meta:
verbose_name = "AAQ enabled locale"
def __str__(self) -> str:
return self.locale
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)
pinned_articles = models.ManyToManyField(Document, null=True, blank=True)
associated_tags = models.ManyToManyField(Tag, 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)
extra_fields = models.JSONField(default=list, blank=True)
objects = AAQConfigManager()
class Meta:
verbose_name = "AAQ configuration"
constraints = [
models.UniqueConstraint(fields=["product", "is_active"], name="unique_active_config")
]
def __str__(self):
return f"{self.product} Configuration"
class Answer(AAQBase):

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

@ -2,7 +2,15 @@ from datetime import datetime
import factory
from kitsune.questions.models import Answer, AnswerVote, Question, QuestionLocale, QuestionVote
from kitsune.products.tests import ProductFactory
from kitsune.questions.models import (
AAQConfig,
Answer,
AnswerVote,
Question,
QuestionLocale,
QuestionVote,
)
from kitsune.sumo.tests import FuzzyUnicode, TestCase
from kitsune.users.tests import UserFactory
@ -53,15 +61,34 @@ class QuestionLocaleFactory(factory.django.DjangoModelFactory):
class Meta:
model = QuestionLocale
class AAQConfigFactory(factory.django.DjangoModelFactory):
class Meta:
model = AAQConfig
title = FuzzyUnicode()
product = factory.SubFactory(ProductFactory)
is_active = True
@factory.post_generation
def products(obj, create, extracted, **kwargs):
def enabled_locales(obj, create, extracted, **kwargs):
if not create:
# Simple build, do nothing
return
if extracted is not None:
for product in extracted:
obj.products.add(product)
for locale in extracted:
obj.enabled_locales.add(locale)
@factory.post_generation
def associated_tags(obj, create, extracted, **kwargs):
if not create:
# Simple build, do nothing
return
if extracted is not None:
for tag in extracted:
obj.associated_tags.add(tag)
class AnswerFactory(factory.django.DjangoModelFactory):

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

@ -12,6 +12,7 @@ from kitsune.products.tests import ProductFactory, TopicFactory
from kitsune.questions import api
from kitsune.questions.models import Answer, Question
from kitsune.questions.tests import (
AAQConfigFactory,
AnswerFactory,
AnswerVoteFactory,
QuestionFactory,
@ -535,8 +536,11 @@ class TestQuestionViewSet(TestCase):
def test_auto_tagging(self):
"""Test that questions created via the API are auto-tagged."""
TagFactory(name="desktop")
q = QuestionFactory()
tag = TagFactory(name="desktop")
product = ProductFactory()
AAQConfigFactory(product=product, is_active=True, associated_tags=[tag])
q = QuestionFactory(product=product)
self.client.force_authenticate(user=q.creator)
tags_eq(q, [])

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

@ -1,9 +1,11 @@
import json
from collections import OrderedDict
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from kitsune.products.tests import ProductFactory, TopicFactory
from kitsune.questions.forms import NewQuestionForm, WatchQuestionForm
from kitsune.questions.tests import AAQConfigFactory, QuestionLocaleFactory
from kitsune.sumo.tests import TestCase
from kitsune.users.tests import UserFactory
@ -38,60 +40,35 @@ class TestNewQuestionForm(TestCase):
def setUp(self):
super(TestNewQuestionForm, self).setUp()
self.locale = QuestionLocaleFactory(locale=settings.LANGUAGE_CODE)
self.product = ProductFactory(slug="firefox")
self.aaq_config = AAQConfigFactory(
product=self.product,
enabled_locales=[self.locale],
is_active=True,
extra_fields=["troubleshooting", "ff_version", "os"],
)
def test_metadata_keys(self):
"""Test metadata_field_keys property."""
# Test the default form
form = NewQuestionForm()
expected = ["category", "useragent"]
expected = ["useragent"]
actual = form.metadata_field_keys
self.assertEqual(expected, actual)
# Test the form with a product
product = {
"key": "desktop",
"name": "Firefox on desktop",
"categories": OrderedDict(
[
(
"cookies",
{
"name": "Cookies",
"topic": "cookies",
"tags": ["cookies"],
},
)
]
),
"extra_fields": ["troubleshooting", "ff_version", "os"],
}
form = NewQuestionForm(product=product)
expected = ["troubleshooting", "ff_version", "os", "useragent", "category"]
form = NewQuestionForm(product=self.product)
expected = ["troubleshooting", "ff_version", "os", "useragent"]
actual = form.metadata_field_keys
self.assertEqual(sorted(expected), sorted(actual))
def test_cleaned_metadata(self):
"""Test the cleaned_metadata property."""
# Test with no metadata
data = {"title": "Lorem", "content": "ipsum", "email": "t@t.com"}
product = {
"key": "desktop",
"name": "Firefox on desktop",
"categories": OrderedDict(
[
(
"cookies",
{
"name": "Cookies",
"topic": "cookies",
"tags": ["cookies"],
},
)
]
),
"extra_fields": ["troubleshooting", "ff_version", "os"],
}
form = NewQuestionForm(product=product, data=data)
topic = TopicFactory(slug="cookies", products=[self.product], in_aaq=True)
data = {"title": "Lorem", "content": "ipsum", "email": "t@t.com", "category": topic.id}
form = NewQuestionForm(product=self.product, data=data)
form.is_valid()
expected = {}
actual = form.cleaned_metadata
@ -99,7 +76,7 @@ class TestNewQuestionForm(TestCase):
# Test with metadata
data["os"] = "Linux"
form = NewQuestionForm(product=product, data=data)
form = NewQuestionForm(product=self.product, data=data)
form.is_valid()
expected = {"os": "Linux"}
actual = form.cleaned_metadata
@ -107,7 +84,7 @@ class TestNewQuestionForm(TestCase):
# Add an empty metadata value
data["ff_version"] = ""
form = NewQuestionForm(product=product, data=data)
form = NewQuestionForm(product=self.product, data=data)
form.is_valid()
expected = {"os": "Linux"}
actual = form.cleaned_metadata
@ -125,7 +102,7 @@ class TestNewQuestionForm(TestCase):
},
}
)
form = NewQuestionForm(product=product, data=data)
form = NewQuestionForm(product=self.product, data=data)
form.is_valid()
expected = {
"os": "Linux",

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

@ -9,7 +9,7 @@ from taggit.models import Tag
import kitsune.sumo.models
from kitsune.flagit.models import FlaggedObject
from kitsune.questions import config
from kitsune.products.tests import ProductFactory, TopicFactory
from kitsune.questions.models import (
AlreadyTakenException,
Answer,
@ -199,20 +199,6 @@ class TestQuestionMetadata(TestCase):
self.question.add_metadata(crash_id="1234567890")
self.assertEqual("1234567890", self.question.metadata["crash_id"])
def test_product_property(self):
"""Test question.product property."""
self.question.add_metadata(product="desktop")
self.assertEqual(config.products["desktop"], self.question.product_config)
def test_category_property(self):
"""Test question.category property."""
self.question.add_metadata(product="desktop")
self.question.add_metadata(category="troubleshooting")
self.assertEqual(
config.products["desktop"]["categories"]["troubleshooting"],
self.question.category_config,
)
def test_clear_mutable_metadata(self):
"""Make sure it works and clears the internal cache.
@ -240,13 +226,14 @@ class TestQuestionMetadata(TestCase):
"""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")
q = self.question
q.add_metadata(
product="desktop", category="troubleshooting", ff_version="3.6.8", os="GREen"
)
q.product = ProductFactory(slug="firefox")
q.topic = TopicFactory(slug="troubleshooting")
q.add_metadata(ff_version="3.6.8", os="GREen")
q.save()
q.auto_tag()
tags_eq(q, ["desktop", "troubleshooting", "Firefox 3.6.8", "Firefox 3.6", "green"])
tags_eq(q, ["firefox", "troubleshooting", "Firefox 3.6.8", "Firefox 3.6", "green"])
def test_auto_tagging_aurora(self):
"""Make sure versions with prerelease suffix are tagged properly."""

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

@ -14,7 +14,7 @@ from taggit.models import Tag
from kitsune.products.tests import ProductFactory, TopicFactory
from kitsune.questions.events import QuestionReplyEvent, QuestionSolvedEvent
from kitsune.questions.models import Answer, Question, QuestionLocale, VoteMetadata
from kitsune.questions.tests import AnswerFactory, QuestionFactory, tags_eq
from kitsune.questions.tests import AAQConfigFactory, AnswerFactory, QuestionFactory, tags_eq
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
@ -1217,9 +1217,10 @@ class QuestionsTemplateTestCase(TestCase):
class QuestionsTemplateTestCaseNoFixtures(TestCase):
def test_locked_questions_dont_appear(self):
"""Locked questions are not listed on the no-replies list."""
QuestionFactory()
QuestionFactory()
QuestionFactory(is_locked=True)
p = ProductFactory()
QuestionFactory(product=p)
QuestionFactory(product=p)
QuestionFactory(product=p, is_locked=True)
url = reverse("questions.list", args=["all"])
url = urlparams(url, filter="no-replies")
@ -1240,16 +1241,16 @@ class QuestionEditingTests(TestCase):
def test_extra_fields(self):
"""The edit-question form should show appropriate metadata fields."""
question_id = QuestionFactory().id
product = ProductFactory()
question_id = QuestionFactory(product=product).id
AAQConfigFactory(product=product)
response = get(self.client, "questions.edit_question", kwargs={"question_id": question_id})
self.assertEqual(response.status_code, 200)
# Make sure each extra metadata field is in the form:
doc = pq(response.content)
q = Question.objects.get(pk=question_id)
extra_fields = q.product_config.get("extra_fields", []) + q.category_config.get(
"extra_fields", []
)
extra_fields = q.product_config.extra_fields
for field in extra_fields:
assert doc("input[name=%s]" % field) or doc("textarea[name=%s]" % field), (
"The %s field is missing from the edit page." % field
@ -1267,8 +1268,9 @@ class QuestionEditingTests(TestCase):
def test_post(self):
"""Posting a valid edit form should save the question."""
p = ProductFactory(slug="desktop")
p = ProductFactory(slug="firefox")
q = QuestionFactory(product=p)
AAQConfigFactory(product=p)
response = post(
self.client,
"questions.edit_question",
@ -1299,7 +1301,6 @@ class AAQTemplateTestCase(TestCase):
"title": "A test question",
"content": "I have this question that I hope...",
"category": "troubleshooting",
"sites_affected": "http://example.com",
"ff_version": "3.6.6",
"os": "Intel Mac OS X 10.6",
"plugins": "* Shockwave Flash 10.1 r53",
@ -1332,30 +1333,36 @@ class AAQTemplateTestCase(TestCase):
super(AAQTemplateTestCase, self).setUp()
self.user = UserFactory()
self.product = ProductFactory(title="Firefox", slug="firefox")
self.aaq_config = AAQConfigFactory(product=self.product, is_active=True)
self.client.login(username=self.user.username, password="testpass")
def _post_new_question(self, locale=None):
"""Post a new question and return the response."""
product = ProductFactory(title="Firefox", slug="firefox")
for loc_code in (settings.LANGUAGE_CODE, "pt-BR"):
loc, _ = QuestionLocale.objects.get_or_create(locale=loc_code)
product.questions_locales.add(loc)
TopicFactory(title="Troubleshooting", slug="troubleshooting", products=[product])
self.aaq_config.enabled_locales.add(loc)
topic = TopicFactory(
title="Troubleshooting", slug="troubleshooting", products=[self.product], in_aaq=True
)
self.data["category"] = topic.id
extra = {}
if locale is not None:
q_loc, _ = QuestionLocale.objects.get_or_create(locale=locale)
self.aaq_config.enabled_locales.add(q_loc)
extra["locale"] = locale
url = urlparams(reverse("questions.aaq_step3", args=["desktop"], **extra))
url = urlparams(reverse("questions.aaq_step3", args=["firefox"], **extra))
# Set 'in-aaq' for the session. It isn't already set because this
# test doesn't do a GET of the form first.
s = self.client.session
s["in-aaq"] = True
s.save()
self.client.session["in-aaq"] = True
self.client.session.save()
foo = self.client.post(url, self.data, follow=True)
return foo
return self.client.post(url, self.data, follow=True)
def test_full_workflow(self):
self.aaq_config.extra_fields = ["troubleshooting"]
self.aaq_config.save()
response = self._post_new_question()
self.assertEqual(200, response.status_code)
assert "Done!" in pq(response.content)("ul.user-messages li").text()
@ -1407,31 +1414,22 @@ class AAQTemplateTestCase(TestCase):
response = self.client.get(url)
self.assertEqual(404, response.status_code)
def test_invalid_category_302(self):
ProductFactory(slug="firefox")
url = reverse("questions.aaq_step3", args=["desktop", "lipsum"])
response = self.client.get(url)
self.assertEqual(302, response.status_code)
class ProductForumTemplateTestCase(TestCase):
def test_product_forum_listing(self):
firefox = ProductFactory(title="Firefox", slug="firefox")
android = ProductFactory(title="Firefox for Android", slug="mobile")
fxos = ProductFactory(title="Firefox OS", slug="firefox-os")
openbadges = ProductFactory(title="Open Badges", slug="open-badges")
mza = ProductFactory(title="Mozilla Account", slug="mozilla-account")
lcl, _ = QuestionLocale.objects.get_or_create(locale=settings.LANGUAGE_CODE)
firefox.questions_locales.add(lcl)
android.questions_locales.add(lcl)
fxos.questions_locales.add(lcl)
AAQConfigFactory(product=firefox, is_active=True, enabled_locales=[lcl])
AAQConfigFactory(product=android, is_active=True, enabled_locales=[lcl])
response = self.client.get(reverse("questions.home"))
self.assertEqual(200, response.status_code)
doc = pq(response.content)
self.assertEqual(3, len(doc(".product-list .product")))
self.assertEqual(2, len(doc(".product-list .product")))
product_list_html = doc(".product-list").html()
assert firefox.title in product_list_html
assert android.title in product_list_html
assert fxos.title in product_list_html
assert openbadges.title not in product_list_html
assert mza.title not in product_list_html

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

@ -7,8 +7,20 @@ from pyquery import PyQuery as pq
from kitsune.flagit.models import FlaggedObject
from kitsune.products.tests import ProductFactory, TopicFactory
from kitsune.questions.models import Answer, AnswerVote, Question, QuestionLocale, QuestionVote
from kitsune.questions.tests import AnswerFactory, QuestionFactory
from kitsune.questions.models import (
AAQConfig,
Answer,
AnswerVote,
Question,
QuestionLocale,
QuestionVote,
)
from kitsune.questions.tests import (
AAQConfigFactory,
AnswerFactory,
QuestionFactory,
QuestionLocaleFactory,
)
from kitsune.questions.views import parse_troubleshooting
from kitsune.search.tests import Elastic7TestCase
from kitsune.sumo.templatetags.jinja_helpers import urlparams
@ -23,24 +35,23 @@ class AAQSearchTests(Elastic7TestCase):
def test_ratelimit(self):
"""Make sure posting new questions is ratelimited"""
p = ProductFactory(slug="firefox")
locale, _ = QuestionLocale.objects.get_or_create(locale=settings.LANGUAGE_CODE)
AAQConfigFactory(product=p, enabled_locales=[locale], is_active=True)
topic = TopicFactory(slug="troubleshooting", products=[p], in_aaq=True)
data = {
"title": "A test question",
"content": "I have this question that I hope...",
"sites_affected": "http://example.com",
"category": topic.id,
"ff_version": "3.6.6",
"os": "Intel Mac OS X 10.6",
"category": "troubleshooting",
"plugins": "* Shockwave Flash 10.1 r53",
"useragent": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X "
"10.6; en-US; rv:1.9.2.6) Gecko/20100625 "
"Firefox/3.6.6",
}
p = ProductFactory(slug="firefox")
locale, _ = QuestionLocale.objects.get_or_create(locale=settings.LANGUAGE_CODE)
p.questions_locales.add(locale)
TopicFactory(slug="troubleshooting", products=[p])
url = urlparams(
reverse("questions.aaq_step3", args=["desktop", "troubleshooting"]),
reverse("questions.aaq_step3", args=["firefox"]),
search="A test question",
)
@ -75,7 +86,7 @@ class AAQTests(TestCase):
def setUp(self):
product = ProductFactory(title="Firefox", slug="firefox")
locale, _ = QuestionLocale.objects.get_or_create(locale=settings.LANGUAGE_CODE)
product.questions_locales.add(locale)
AAQConfigFactory(product=product, enabled_locales=[locale], is_active=True)
def test_non_authenticated_user(self):
"""
@ -107,7 +118,7 @@ class AAQTests(TestCase):
"""
user = UserFactory(is_superuser=False)
self.client.login(username=user.username, password="testpass")
url = reverse("questions.aaq_step3", args=["desktop"])
url = reverse("questions.aaq_step3", args=["firefox"])
response = self.client.get(url, follow=True)
assert not template_used(response, "users/auth.html")
assert template_used(response, "questions/new_question.html")
@ -257,10 +268,10 @@ class TestQuestionList(TestCase):
for locale in (settings.LANGUAGE_CODE, "pt-BR"):
QuestionLocale.objects.get_or_create(locale=locale)
self.assertEqual(Question.objects.count(), 0)
p = ProductFactory(slug="firefox")
TopicFactory(title="Fix problems", slug="fix-problems", products=[p])
AAQConfigFactory(product=p, is_active=True, enabled_locales=QuestionLocale.objects.all())
QuestionFactory(title="question cupcakes?", product=p, locale="en-US")
QuestionFactory(title="question donuts?", product=p, locale="en-US")
@ -552,17 +563,16 @@ class TestEditDetails(TestCase):
assert u.has_perm("questions.change_question")
self.user = u
for locale in (settings.LANGUAGE_CODE, "hu"):
QuestionLocale.objects.get_or_create(locale=locale)
AAQConfigFactory(
enabled_locales=[
QuestionLocaleFactory(locale=settings.LANGUAGE_CODE),
QuestionLocaleFactory(locale="hu"),
]
)
p = ProductFactory()
t = TopicFactory(products=[p])
q = QuestionFactory(product=p, topic=t)
self.product = p
self.topic = t
self.question = q
self.product = ProductFactory()
self.topic = TopicFactory(products=[self.product])
self.question = QuestionFactory(product=self.product, topic=self.topic)
def _request(self, user=None, data=None):
"""Make a request to edit details"""
@ -654,7 +664,7 @@ class TestEditDetails(TestCase):
def test_change_locale(self):
locale = "hu"
assert locale in QuestionLocale.objects.locales_list()
assert locale in AAQConfig.objects.locales_list()
assert locale != self.question.locale
data = {"product": self.product.id, "topic": self.topic.id, "locale": locale}

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

@ -33,14 +33,8 @@ urlpatterns = [
path("mozilla/location/", views.aaq_location_proxy, name="questions.location_proxy"),
# AAQ
re_path(r"^new$", views.aaq, name="questions.aaq_step1"),
re_path(r"^new/(?P<product_key>[\w\-]+)$", views.aaq_step2, name="questions.aaq_step2"),
re_path(r"^new/(?P<product_key>[\w\-]+)/form$", views.aaq_step3, name="questions.aaq_step3"),
# maintain backwards compatibility with old aaq urls:
re_path(
r"^new/(?P<product_key>[\w\-]+)/(?P<category_key>[\w\-]+)",
views.aaq_step3,
name="questions.aaq_step3",
),
re_path(r"^new/(?P<product_slug>[\w\-]+)$", views.aaq_step2, name="questions.aaq_step2"),
re_path(r"^new/(?P<product_slug>[\w\-]+)/form$", views.aaq_step3, name="questions.aaq_step3"),
# TODO: Factor out `/(?P<question_id>\d+)` below
re_path(r"^(?P<question_id>\d+)$", views.question_details, name="questions.details"),
re_path(r"^(?P<question_id>\d+)/edit$", views.edit_question, name="questions.edit_question"),

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

@ -82,9 +82,9 @@ def get_featured_articles(product, locale):
locale__in=(locale, settings.WIKI_DEFAULT_LANGUAGE)
)
if (
localized_article := article
if article.locale == locale
else article.translated_to(locale)
localized_article := (
article if article.locale == locale else article.translated_to(locale)
)
)
]
else:

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

@ -19,6 +19,7 @@ from django.http import (
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
JsonResponse,
)
@ -36,7 +37,6 @@ from kitsune.access.decorators import login_required, permission_required
from kitsune.customercare.forms import ZendeskForm
from kitsune.flagit.models import FlaggedObject
from kitsune.products.models import Product, Topic, TopicSlugHistory
from kitsune.products.views import _get_aaq_product_key
from kitsune.questions import config
from kitsune.questions.events import QuestionReplyEvent, QuestionSolvedEvent
from kitsune.questions.feeds import AnswersFeed, QuestionsFeed, TaggedQuestionsFeed
@ -47,7 +47,7 @@ from kitsune.questions.forms import (
NewQuestionForm,
WatchQuestionForm,
)
from kitsune.questions.models import Answer, AnswerVote, Question, QuestionLocale, QuestionVote
from kitsune.questions.models import AAQConfig, Answer, AnswerVote, Question, QuestionVote
from kitsune.questions.utils import get_featured_articles, get_mobile_product_from_ua
from kitsune.sumo.decorators import ratelimit
from kitsune.sumo.i18n import split_into_language_and_path
@ -160,9 +160,11 @@ def question_list(request, product_slug=None, topic_slug=None):
request, messages.WARNING, "You cannot list all questions at this time."
)
return HttpResponseRedirect("/")
products = Product.active.with_question_forums(request)
# Get all topics
topics = []
if topic_slug:
try:
topic_history = TopicSlugHistory.objects.get(slug=topic_slug)
@ -250,14 +252,13 @@ def question_list(request, product_slug=None, topic_slug=None):
# Filter by products.
multiple = False
product_key = None
if products:
# This filter will match if any of the products on a question have the
# correct id.
question_qs = question_qs.filter(product__in=products)
multiple = len(products) > 1
if not multiple:
product_key = _get_aaq_product_key(products[0].slug)
product_slug = products[0].slug
# Filter by topic.
if topics:
@ -266,7 +267,7 @@ def question_list(request, product_slug=None, topic_slug=None):
question_qs = question_qs.filter(topic__in=topics)
# Filter by locale for AAQ locales, and by locale + default for others.
if request.LANGUAGE_CODE in QuestionLocale.objects.locales_list():
if request.LANGUAGE_CODE in AAQConfig.objects.locales_list():
locale_query = Q(locale=request.LANGUAGE_CODE)
else:
locale_query = Q(locale=request.LANGUAGE_CODE)
@ -341,7 +342,7 @@ def question_list(request, product_slug=None, topic_slug=None):
"topic_list": topic_list,
"topics": topics,
"selected_topic_slug": topics[0].slug if topics else None,
"product_key": product_key,
"product_slug": product_slug,
"topic_navigation": topic_navigation,
}
@ -476,7 +477,7 @@ def edit_details(request, question_id):
locale = request.POST.get("locale")
# If locale is not in AAQ_LANGUAGES throws a ValueError
tuple(QuestionLocale.objects.locales_list()).index(locale)
tuple(AAQConfig.objects.locales_list()).index(locale)
except (Product.DoesNotExist, Topic.DoesNotExist, ValueError):
return HttpResponseBadRequest()
@ -495,15 +496,28 @@ def aaq_location_proxy(request):
return JsonResponse(response.json())
def aaq(request, product_key=None, category_key=None, step=1, is_loginless=False):
def aaq(request, product_slug=None, step=1, is_loginless=False):
"""Ask a new question."""
# After the migration to a DB based AAQ, we need to account for
# product slugs that were not present in the questions config.
should_redirect = True
match product_slug:
case "desktop":
product_slug = "firefox"
case "focus":
product_slug = "focus-firefox"
case _:
should_redirect = False
if should_redirect:
return HttpResponsePermanentRedirect(reverse("questions.aaq_step2", args=[product_slug]))
template = "questions/new_question.html"
# Check if the user is using a mobile device,
# render step 2 if they are
product_key = product_key or request.GET.get("product")
if product_key is None:
product_slug = product_slug or request.GET.get("product")
if product_slug is None:
change_product = False
if request.GET.get("q") == "change_product":
change_product = True
@ -512,43 +526,41 @@ def aaq(request, product_key=None, category_key=None, step=1, is_loginless=False
if is_mobile_device and not change_product:
user_agent = request.META.get("HTTP_USER_AGENT", "")
product_key = get_mobile_product_from_ua(user_agent)
if product_key:
if product_slug := get_mobile_product_from_ua(user_agent):
# redirect needed for InAAQMiddleware
step_2 = reverse("questions.aaq_step2", kwargs={"product_key": product_key})
step_2 = reverse("questions.aaq_step2", args=[product_slug])
return HttpResponseRedirect(step_2)
# Return 404 if the product doesn't exist in config
product_config = config.products.get(product_key)
if product_key and not product_config:
raise Http404
# If the selected product doesn't exist in DB, render a 404
if step > 1:
# Return 404 if the products does not have an AAQ form or if it is archived
product = None
products_with_aaqs = Product.active.with_question_forums(request)
if product_slug:
try:
product = Product.active.get(slug=product_config["product"])
product = Product.active.get(slug=product_slug)
except Product.DoesNotExist:
raise Http404
has_public_forum = product.questions_enabled(locale=request.LANGUAGE_CODE)
has_ticketing_support = product.has_ticketing_support
request.session["aaq_context"] = {
"key": product_key,
"has_public_forum": has_public_forum,
"has_ticketing_support": has_ticketing_support,
}
else:
if product not in products_with_aaqs:
raise Http404
context = {
"products": config.products,
"current_product": product_config,
"products": products_with_aaqs,
"current_product": product,
"current_step": step,
"host": Site.objects.get_current().domain,
"is_loginless": is_loginless,
"ga_content_group": f"aaq-step-{step}",
}
# If the selected product doesn't exist in DB, render a 404
if step > 1:
context["has_ticketing_support"] = has_ticketing_support
context["ga_products"] = f"/{product.slug}/"
has_public_forum = product.questions_enabled(locale=request.LANGUAGE_CODE)
request.session["aaq_context"] = {
"product_slug": product_slug,
"has_public_forum": has_public_forum,
"has_ticketing_support": product.has_ticketing_support,
}
context["has_ticketing_support"] = product.has_ticketing_support
context["ga_products"] = f"/{product_slug}/"
if step == 2:
context["featured"] = get_featured_articles(product, locale=request.LANGUAGE_CODE)
@ -558,7 +570,7 @@ def aaq(request, product_key=None, category_key=None, step=1, is_loginless=False
context["cancel_url"] = get_next_url(request) or (
reverse("products.product", args=[product.slug])
if is_loginless
else reverse("questions.aaq_step2", args=[product_key])
else reverse("questions.aaq_step2", args=[product_slug])
)
# Check if the selected product has a forum in the user's locale
@ -576,7 +588,7 @@ def aaq(request, product_key=None, category_key=None, step=1, is_loginless=False
return HttpResponseRedirect(path)
if has_ticketing_support:
if product.has_ticketing_support:
zendesk_form = ZendeskForm(
data=request.POST or None,
product=product,
@ -586,7 +598,7 @@ def aaq(request, product_key=None, category_key=None, step=1, is_loginless=False
if zendesk_form.is_valid() and not is_ratelimited(request, "loginless", "3/d"):
try:
zendesk_form.send(request.user, product_config)
zendesk_form.send(request.user, product)
email = zendesk_form.cleaned_data["email"]
messages.add_message(
request,
@ -616,9 +628,8 @@ def aaq(request, product_key=None, category_key=None, step=1, is_loginless=False
return render(request, template, context)
form = NewQuestionForm(
product=product_config,
product=product,
data=request.POST or None,
initial={"category": category_key},
)
context["form"] = form
@ -627,7 +638,6 @@ def aaq(request, product_key=None, category_key=None, step=1, is_loginless=False
user=request.user,
locale=request.LANGUAGE_CODE,
product=product,
product_config=product_config,
)
if form.cleaned_data.get("is_spam"):
@ -665,23 +675,26 @@ def aaq(request, product_key=None, category_key=None, step=1, is_loginless=False
return render(request, template, context)
def aaq_step2(request, product_key):
def aaq_step2(request, product_slug):
"""Step 2: The product is selected."""
return aaq(request, product_key=product_key, step=2)
return aaq(request, product_slug=product_slug, step=2)
def aaq_step3(request, product_key, category_key=None):
def aaq_step3(request, product_slug):
"""Step 3: Show full question form."""
# Since removing the @login_required decorator for MA form
# need to catch unauthenticated, non-MA users here """
referer = request.META.get("HTTP_REFERER", "")
is_loginless = (product_key in settings.LOGIN_EXCEPTIONS) and any(
is_loginless = (product_slug in settings.LOGIN_EXCEPTIONS) and any(
uri in referer
for uri in settings.MOZILLA_ACCOUNT_ARTICLES
+ [
path.removeprefix(f"/{request.LANGUAGE_CODE}")
for path in (reverse("users.auth"), reverse("questions.aaq_step3", args=[product_key]))
for path in (
reverse("users.auth"),
reverse("questions.aaq_step3", args=[product_slug]),
)
]
)
@ -691,8 +704,7 @@ def aaq_step3(request, product_key, category_key=None):
return aaq(
request,
is_loginless=is_loginless,
product_key=product_key,
category_key=category_key,
product_slug=product_slug,
step=3,
)
@ -714,13 +726,13 @@ def edit_question(request, question_id):
initial = question.metadata.copy()
initial.update(title=question.title, content=question.content)
form = EditQuestionForm(
product=question.product_config,
product=question.product,
initial=initial,
)
else:
form = EditQuestionForm(
data=request.POST,
product=question.product_config,
product=question.product,
)
if form.is_valid():
@ -746,8 +758,7 @@ def edit_question(request, question_id):
"question": question,
"form": form,
"images": images,
"current_product": question.product_config,
"current_category": question.category_config,
"current_product": question.product,
}
return render(request, "questions/edit_question.html", context)

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

@ -329,11 +329,11 @@ class SumoSearch(SumoSearchInterface):
@overload
def __getitem__(self, key: int) -> dict:
...
pass
@overload
def __getitem__(self, key: slice) -> list[dict]:
...
pass
def __getitem__(self, key):
if self.last_key is None or self.last_key != key:

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

@ -24,7 +24,7 @@ class SearchMixin(object):
@classmethod
def get_mapping_type(cls):
"""Return the MappingType for this model"""
...
pass
def index_later(self):
"""Register myself to be indexed at the end of the request."""
@ -58,45 +58,45 @@ class SearchMappingType(object):
@classmethod
def search(cls):
...
pass
@classmethod
def get_index(cls):
...
pass
@classmethod
def get_index_group(cls):
...
pass
@classmethod
def get_query_fields(cls):
"""Return the list of fields for query"""
...
pass
@classmethod
def get_localized_fields(cls):
...
pass
@classmethod
def get_indexable(cls, seconds_ago=0):
...
pass
@classmethod
def reshape(cls, results):
...
pass
@classmethod
def index(cls, *args, **kwargs):
...
pass
@classmethod
def unindex(cls, *args, **kwargs):
...
pass
@classmethod
def morelikethis(cls, id_, s, fields):
"""MoreLikeThis API"""
...
pass
# class RecordManager(models.Manager):

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

@ -19,9 +19,6 @@ class TestSearchSEO(Elastic7TestCase):
self.assertTrue("text/html" in response["content-type"])
doc = pq(response.content)
self.assertEqual(doc('meta[name="robots"]').attr("content"), "noindex, nofollow")
# TODO: Are these old Webtrends meta tags even useful any longer?
self.assertEqual(doc('meta[name="WT.oss"]').attr("content"), "firefox")
self.assertEqual(doc('meta[name="WT.oss_r"]').attr("content"), "0")
def test_simple_search_json(self):
"""
@ -44,9 +41,6 @@ class TestSearchSEO(Elastic7TestCase):
self.assertTrue("text/html" in response["content-type"])
doc = pq(response.content)
self.assertEqual(doc('meta[name="robots"]').attr("content"), "noindex, nofollow")
# TODO: Are these old Webtrends meta tags even useful any longer?
self.assertFalse(doc.find('meta[name="WT.oss"]'))
self.assertFalse(doc.find('meta[name="WT.oss_r"]'))
def test_invalid_search_json(self):
"""

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

@ -3,7 +3,7 @@ from datetime import datetime
from django.conf import settings
from django.utils import translation
from kitsune.questions.models import QuestionLocale
from kitsune.questions.models import AAQConfig
def global_settings(request):
@ -22,7 +22,7 @@ def i18n(request):
def aaq_languages(request):
"""Adds the list of AAQ languages to the context."""
return {"AAQ_LANGUAGES": QuestionLocale.objects.locales_list()}
return {"AAQ_LANGUAGES": AAQConfig.objects.locales_list()}
def current_year(request):

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

@ -2,4 +2,4 @@ from wagtail.documents.models import AbstractDocument
class WagtailDocument(AbstractDocument):
...
pass

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

@ -188,8 +188,9 @@ class SetRemoteAddrFromForwardedForMiddlewareTestCase(TestCase):
(2, "3.3.3.3, 4.4.4.4,5.5.5.5", "3.3.3.3"),
(2, "999.255.255.1, 4.4.4.4,5.5.5.5", "127.0.0.1"),
]:
with self.settings(TRUSTED_PROXY_COUNT=proxy_count), self.subTest(
f"{proxy_count} with {forwarded_for}"
with (
self.settings(TRUSTED_PROXY_COUNT=proxy_count),
self.subTest(f"{proxy_count} with {forwarded_for}"),
):
request = rf.get("/", HTTP_X_FORWARDED_FOR=forwarded_for)
mw(request)

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

@ -329,9 +329,11 @@ class Event(object):
getattr(Watch, "uncached", Watch.objects)
.filter(
user_condition,
Q(content_type=ContentType.objects.get_for_model(cls.content_type))
if cls.content_type
else Q(),
(
Q(content_type=ContentType.objects.get_for_model(cls.content_type))
if cls.content_type
else Q()
),
Q(object_id=object_id) if object_id else Q(),
event_type=cls.event_type,
)

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

@ -12,10 +12,10 @@
{% block contentwrap %}
{% set aaq_context = request.session.get('aaq_context') %}
{% if aaq_context and aaq_context.key %}
{% if aaq_context and aaq_context.product_slug %}
<section class="sumo-page-section question-masthead shade-bg">
<div class="mzp-l-content">
{{ progress_bar(3, product_key=aaq_context.key) }}
{{ progress_bar(3, product_slug=aaq_context.product_slug) }}
</div>
</section>
{% endif %}
@ -50,7 +50,7 @@
</p>
<div class="trouble-text">
<p class="help-text">
<a href="{{ url('questions.aaq_step3', product_key='mozilla-account') }}">{{ _("I can't sign in to my Mozilla account") }}</a>
<a href="{{ url('questions.aaq_step3', product_slug='mozilla-account') }}">{{ _("I can't sign in to my Mozilla account") }}</a>
</p>
</div>
</div>

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

@ -92,7 +92,7 @@
{{ document_content(document, fallback_reason, request, settings, document_css_class, any_localizable_revision, full_locale_name) }}
{% if document.gets_mozilla_account_cta %}
{{ inpage_contact_cta(product=product, product_key='mozilla-account') }}
{{ inpage_contact_cta(product=product, product_slug='mozilla-account') }}
{% endif %}
{% set share_link = document.share_link or (document.parent and document.parent.share_link) %}

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

@ -426,8 +426,8 @@
{% endif %}
{% endmacro %}
{% macro inpage_contact_cta(product=None, product_key=None) %}
{% if product_key %}
{% macro inpage_contact_cta(product=None, product_slug=None) %}
{% if product_slug %}
<section class="support-callouts mzp-l-content sumo-page-section--inner">
<div class="card card--ribbon is-inverse heading-is-one-line">
<div class="card--details">
@ -446,11 +446,11 @@
{{ _("If you've tried the steps above and you're still unable to sign in, send a message to our support team.") }}
</p>
<a class="sumo-button primary-button button-lg"
href="{{ url('questions.aaq_step3', product_key=product_key) }}"
href="{{ url('questions.aaq_step3', product_slug=product_slug) }}"
data-event-name="link_click"
data-event-parameters='{
"link_name": "in-article-aaq-banner.aaq-step-3",
"link_detail": "{{ product_key }}"
"link_detail": "{{ product_slug }}"
}'>
{{ _('Contact Support') }}
</a>

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

@ -90,7 +90,7 @@ def doc_page_cache(view):
if (
request.user.is_authenticated
or request.GET.get("redirect") == "no"
or request.session.get("product_key")
or request.session.get("product_slug")
or settings.DEV
):
return view(request, document_slug, *args, **kwargs)
@ -608,7 +608,6 @@ def edit_document_metadata(request, document_slug, revision_id=None):
try:
doc = doc_form.save(None)
except (TitleCollision, SlugCollision) as metadata_error:
# TODO: .add_error() when we upgrade to Django 1.7
errors = doc_form._errors.setdefault("title", ErrorList())
message = "The {type} you selected is already in use."
message = message.format(

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

@ -16,5 +16,5 @@ class ContactSupportMessages:
"Pocket": "The webs most intriguing articles",
"Thunderbird": "Email software for Windows, Mac and Linux",
"Firefox Focus": "Automatic privacy browser and content blocker",
"Mozilla Account": "Mozilla account is the account system for Mozilla"
"Mozilla Account": "Mozilla account is the account system for Mozilla",
}