Skip to content

Commit 55e337a

Browse files
authored
improve related documents (#6939)
1 parent 1f7385c commit 55e337a

File tree

12 files changed

+186
-81
lines changed

12 files changed

+186
-81
lines changed

kitsune/sumo/static/sumo/js/wiki_search.js

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import nunjucksEnv from "sumo/js/nunjucks"; // has to be loaded after templates
77

88
document.addEventListener("DOMContentLoaded", function() {
99
const locale = document.documentElement.lang;
10-
const search = new Search(`/${locale}/search`, { w: 1, format: 'json' });
10+
const search = new Search("/api/1/kb/", { locales: `en-US,${locale}`, categories: "10,20,30,40,50" });
1111

1212
const relatedDocsList = document.getElementById('related-docs-list');
1313
const searchInput = document.getElementById('search-related');
@@ -34,6 +34,9 @@ document.addEventListener("DOMContentLoaded", function() {
3434
closeAfterSelect: true,
3535
maxItems: null, // Allow multiple selections
3636
plugins: {
37+
clear_button: {
38+
title: "Clear All",
39+
},
3740
remove_button: {
3841
title: 'Remove this document'
3942
}
@@ -48,24 +51,9 @@ document.addEventListener("DOMContentLoaded", function() {
4851
return callback();
4952
}
5053

51-
const formattedResults = data.results
52-
.filter(result => result.type === 'document')
53-
.map(item => {
54-
let id = item.id;
55-
if (!id && item.url) {
56-
const match = item.url.match(/\/(\d+)\//);
57-
if (match) {
58-
id = match[1];
59-
}
60-
}
61-
return {
62-
id: id,
63-
title: item.title,
64-
url: item.url
65-
};
66-
})
67-
.filter(item => item.id)
68-
.filter(item => !currentDocId || String(item.id) !== String(currentDocId));
54+
const formattedResults = data.results.filter(
55+
item => !currentDocId || String(item.id) !== String(currentDocId)
56+
);
6957

7058
callback(formattedResults);
7159
});
@@ -110,13 +98,13 @@ document.addEventListener("DOMContentLoaded", function() {
11098
if (emptyMessage) {
11199
emptyMessage.remove();
112100
}
113-
101+
114102
preSelectedOptions.forEach(option => {
115103
const docData = {
116104
id: option.value,
117105
title: option.textContent
118106
};
119-
107+
120108
// Add the option to TomSelect's options and selected items
121109
tomSelect.addOption(docData);
122110
tomSelect.addItem(option.value, true); // true = silent, don't trigger events

kitsune/sumo/static/sumo/scss/components/_wiki.scss

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1317,4 +1317,24 @@ article {
13171317
>h1:before {
13181318
transform: rotate(0);
13191319
}
1320-
}
1320+
}
1321+
1322+
// Styling for the tom-select clear_button plugin.
1323+
.ts-wrapper.multi.plugin-clear_button .ts-control > .clear-button {
1324+
opacity: 100%;
1325+
width: 1rem;
1326+
height: 1rem;
1327+
font-size: 1rem;
1328+
display: inline-flex;
1329+
justify-content: center;
1330+
align-items: center;
1331+
padding-bottom: 8px;
1332+
text-shadow: 0 1px 0 rgba(0, 51, 83, 30%);
1333+
border-radius: 3px;
1334+
background-image: linear-gradient(to bottom, #1da7ee, #178ee9) !important;
1335+
box-shadow: 0 1px 0 rgba(0, 0, 0, 20%),inset 0 1px rgba(255, 255, 255, 3%);
1336+
1337+
&:hover {
1338+
background-image: linear-gradient(to bottom, #008fd8, #0075cf) !important;
1339+
}
1340+
}

kitsune/wiki/api.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
class DocumentShortSerializer(serializers.ModelSerializer):
1414
class Meta:
1515
model = Document
16-
fields = ("title", "slug")
16+
fields = ("id", "title", "slug")
1717

1818

1919
class DocumentDetailSerializer(DocumentShortSerializer):
@@ -37,19 +37,31 @@ class DocumentList(LocaleNegotiationMixin, generics.ListAPIView):
3737
serializer_class = DocumentShortSerializer
3838

3939
def get_queryset(self):
40-
queryset = Document.objects.visible(
41-
self.request.user, category__in=settings.IA_DEFAULT_CATEGORIES
42-
)
40+
locales = self.request.query_params.get("locales")
41+
if locales:
42+
# Multiple locales are separated by commas.
43+
locales = locales.replace(",", " ").split()
44+
elif locale := normalize_language(self.get_locale()):
45+
locales = [locale]
46+
47+
categories = self.request.query_params.get("categories")
48+
if categories:
49+
# Multiple categories are separated by commas.
50+
categories = categories.replace(",", " ").split()
51+
else:
52+
categories = settings.IA_DEFAULT_CATEGORIES
4353

44-
locale = normalize_language(self.get_locale())
4554
product = self.request.query_params.get("product")
4655
topic = self.request.query_params.get("topic")
4756
is_template = bool(self.request.query_params.get("is_template", False))
4857
is_archived = bool(self.request.query_params.get("is_archived", False))
4958
is_redirect = bool(self.request.query_params.get("is_redirect", False))
59+
title_query = self.request.query_params.get("q")
5060

51-
if locale is not None:
52-
queryset = queryset.filter(locale=locale)
61+
queryset = Document.objects.visible(self.request.user, category__in=categories)
62+
63+
if locales:
64+
queryset = queryset.filter(locale__in=locales)
5365

5466
if product is not None:
5567
if locale == settings.WIKI_DEFAULT_LANGUAGE:
@@ -76,6 +88,9 @@ def get_queryset(self):
7688
else:
7789
queryset = queryset.filter(~redirect_filter)
7890

91+
if title_query:
92+
queryset = queryset.filter(title__icontains=title_query)
93+
7994
return queryset
8095

8196

kitsune/wiki/forms.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
from django import forms
44
from django.conf import settings
55
from django.contrib.auth.models import Group
6+
from django.db.models.functions import Coalesce
67
from django.template.defaultfilters import slugify
78
from django.utils.translation import gettext_lazy as _lazy
89
from django.utils.translation import ngettext_lazy as _nlazy
910

1011
from kitsune.products.models import Product, Topic
1112
from kitsune.sumo.form_fields import MultiUsernameField, MultiUsernameFilterField
12-
from kitsune.wiki.config import CATEGORIES, SIGNIFICANCES
13+
from kitsune.wiki.config import (
14+
CANNED_RESPONSES_CATEGORY,
15+
CATEGORIES,
16+
SIGNIFICANCES,
17+
TEMPLATES_CATEGORY,
18+
)
1319
from kitsune.wiki.content_managers import ManualContentManager
1420
from kitsune.wiki.models import MAX_REVISION_COMMENT_LENGTH, Document, DraftRevision, Revision
1521
from kitsune.wiki.tasks import add_short_links
@@ -63,6 +69,9 @@
6369
)
6470
PRODUCT_REQUIRED = _lazy("Please select at least one product.")
6571
TOPIC_REQUIRED = _lazy("Please select at least one topic.")
72+
RELATED_DOCUMENTS_DISALLOWED = _lazy(
73+
"Related documents are not allowed for Templates and Canned Responses."
74+
)
6675

6776

6877
class DocumentForm(forms.ModelForm):
@@ -135,8 +144,11 @@ class DocumentForm(forms.ModelForm):
135144
widget=ProductsWidget(),
136145
)
137146

138-
related_documents = forms.MultipleChoiceField(
139-
label=_lazy("Related documents:"), required=False, widget=RelatedDocumentsWidget()
147+
related_documents = forms.ModelMultipleChoiceField(
148+
required=False,
149+
queryset=Document.objects.none(),
150+
label=_lazy("Related documents:"),
151+
widget=RelatedDocumentsWidget(),
140152
)
141153

142154
locale = forms.CharField(widget=forms.HiddenInput())
@@ -154,10 +166,28 @@ def clean_slug(self):
154166
raise forms.ValidationError(SLUG_INVALID)
155167
return slug
156168

169+
def clean_related_documents(self):
170+
"""Replace any children with their parent documents."""
171+
related_docs = self.cleaned_data.get("related_documents", Document.objects.none())
172+
173+
# In the form, we'll show the translations of the related documents if they exist
174+
# (see kitsune.wiki.views.get_visible_related_documents), but we always store
175+
# related documents as parents.
176+
return Document.objects.filter(
177+
id__in=related_docs.annotate(
178+
root_id=Coalesce("parent_id", "id"),
179+
).values_list("root_id", flat=True)
180+
).distinct()
181+
157182
def clean(self):
158183
cdata = super().clean()
159184
locale = cdata.get("locale")
160185

186+
if cdata.get("related_documents") and (
187+
int(cdata.get("category", 0)) in (TEMPLATES_CATEGORY, CANNED_RESPONSES_CATEGORY)
188+
):
189+
raise forms.ValidationError(RELATED_DOCUMENTS_DISALLOWED)
190+
161191
# Products are required for en-US
162192
product_ids = set(map(int, cdata.get("products", [])))
163193
if locale == settings.WIKI_DEFAULT_LANGUAGE and (not product_ids or len(product_ids) < 1):
@@ -229,6 +259,7 @@ def __init__(self, *args, **kwargs):
229259
can_archive = kwargs.pop("can_archive", False)
230260
can_edit_needs_change = kwargs.pop("can_edit_needs_change", False)
231261
initial_title = kwargs.pop("initial_title", "")
262+
locale = kwargs.pop("locale", None)
232263

233264
super().__init__(*args, **kwargs)
234265

@@ -244,8 +275,13 @@ def __init__(self, *args, **kwargs):
244275
products_field = self.fields["products"]
245276
products_field.choices = Product.active.values_list("id", "title")
246277

278+
# Set up the queryset for the related documents field.
247279
related_documents_field = self.fields["related_documents"]
248-
related_documents_field.choices = Document.objects.values_list("id", "title")
280+
queryset = Document.objects.filter(is_template=False)
281+
locales = [settings.WIKI_DEFAULT_LANGUAGE]
282+
if locale and (locale != settings.WIKI_DEFAULT_LANGUAGE):
283+
locales.append(locale)
284+
related_documents_field.queryset = queryset.filter(locale__in=locales)
249285

250286
# If user hasn't permission to frob is_archived, remove the field. This
251287
# causes save() to skip it as well.

kitsune/wiki/jinja2/wiki/document.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
</section>
125125

126126
{% if not document.is_switching_devices_document %}
127-
{{ related_documents(document) }}
127+
{{ related_documents(related_docs) }}
128128
{% endif %}
129129

130130
</section>

kitsune/wiki/jinja2/wiki/edit_metadata.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ <h1 class="sumo-page-heading">{{ _('<em>Editing Metadata For:</em><br>{title}')|
4747
{% endif %}
4848
{% for field in document_form.visible_fields() if
4949
(field.name != 'restrict_to_groups') and (field.name != 'is_localizable' or not document.translations.exists()) %}
50+
{% if (field.name != 'related_documents') or document.allows_related_documents %}
5051
<div class="field has-large-textarea">
5152
{{ field|label_with_help }}
5253
{% if field.name in ['products', 'topics'] %}
@@ -57,6 +58,7 @@ <h1 class="sumo-page-heading">{{ _('<em>Editing Metadata For:</em><br>{title}')|
5758
{% endif %}
5859
{{ field }}
5960
</div>
61+
{% endif %}
6062
{% endfor %}
6163
{% if document.translations.exists() %}
6264
{{ document_form.is_localizable.as_hidden()|safe }}

kitsune/wiki/jinja2/wiki/includes/document_macros.html

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -418,35 +418,32 @@ <h4 class="document-vote--heading">{{ header }}</h4>
418418
{% endif %}
419419
{%- endmacro %}
420420

421-
{% macro related_documents(document) %}
422-
{% if document.related_documents.count() > 0 %}
423-
{% set docs = document.related_documents.all() %}
421+
{% macro related_documents(related_docs) %}
422+
{% if related_docs %}
424423
<section id="related-documents" class="sumo-page-section wiki-related-documents">
425424
<div class="text-center">
426425
<h2 class="sumo-page-subheading">{{ _('Related Articles') }}</h2>
427426
</div>
428427
<div class="sumo-card-grid">
429428
<div class="scroll-wrap">
430-
{% for related in docs %}
431-
{% if related.is_unrestricted_for(user) %}
432-
<div class="card card--article">
433-
<img class="card--icon-sm" src="{{ webpack_static('protocol/img/icons/reader-mode.svg') }}" alt="todo: title" />
434-
<div class="card--details">
435-
<h3 class="card--title">
436-
<a class="expand-this-link" href="{{ url('wiki.document', related.slug) }}"
437-
data-event-name="link_click"
438-
data-event-parameters='{"link_name": "related.kb-article"}'>
439-
{{ related.title }}
440-
</a>
441-
</h3>
442-
<div class="card--desc">
443-
<p>
444-
{{ related.html|striptags|truncate(length=150) }}
445-
</p>
446-
</div>
429+
{% for related in related_docs %}
430+
<div class="card card--article">
431+
<img class="card--icon-sm" src="{{ webpack_static('protocol/img/icons/reader-mode.svg') }}" alt="todo: title" />
432+
<div class="card--details">
433+
<h3 class="card--title">
434+
<a class="expand-this-link" href="{{ url('wiki.document', related.slug) }}"
435+
data-event-name="link_click"
436+
data-event-parameters='{"link_name": "related.kb-article"}'>
437+
{{ related.title }}
438+
</a>
439+
</h3>
440+
<div class="card--desc">
441+
<p>
442+
{{ related.html|striptags|truncate(length=150) }}
443+
</p>
447444
</div>
448-
</div>
449-
{% endif %}
445+
</div>
446+
</div>
450447
{% endfor %}
451448
</div>
452449
</div>
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<select id="search-related" class="tom-select-related-docs" placeholder="{{ _('Search for documents...') }}">
2-
{% if related_documents.count() %}
3-
{% for doc in related_documents.all() %}
2+
{% if related_documents %}
3+
{% for doc in related_documents %}
44
<option value="{{ doc.pk }}" selected>{{ doc.title }}</option>
55
{% endfor %}
66
{% endif %}
77
</select>
88
<div id="related-docs-list">
9-
{% if not related_documents.count() %}
9+
{% if not related_documents %}
1010
<div class="empty-message">{{ _('No related documents.') }}</div>
1111
{% endif %}
1212
</div>

kitsune/wiki/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,10 @@ def is_linked_in_product(self):
797797
locale__in=("", self.locale), target__contains=f"kb/{self.slug}"
798798
).exists()
799799

800+
@property
801+
def allows_related_documents(self):
802+
return self.category not in [TEMPLATES_CATEGORY, CANNED_RESPONSES_CATEGORY]
803+
800804

801805
class AbstractRevision(models.Model):
802806
# **%(class)s** is being used because it will allow a unique reverse name for the field

0 commit comments

Comments
 (0)