зеркало из https://github.com/mozilla/kitsune.git
Use internal IA for the AAQ instead of a config.
This commit is contained in:
Родитель
66ab0809ea
Коммит
41cc8d6bc4
|
@ -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 web’s 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 web’s 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",
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче