1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
|
import textwrap
from pathlib import Path
from unittest import TestCase, mock
import django.test
import markdown
from django.http import Http404
from django.test import RequestFactory, SimpleTestCase, override_settings
from django.urls import reverse
from pydis_site.apps.content.models import Commit, Tag
from pydis_site.apps.content.tests.helpers import (
BASE_PATH, MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA
)
from pydis_site.apps.content.tests.test_utils import TEST_COMMIT_KWARGS
from pydis_site.apps.content.views import PageOrCategoryView
def patch_dispatch_attributes(view: PageOrCategoryView, location: str) -> None:
"""
Set the attributes set in the `dispatch` method manually.
This is necessary because it is never automatically called during tests.
"""
view.location = Path(BASE_PATH, location)
# URL location on the filesystem
view.full_location = view.location
# Possible places to find page content information
view.category_path = view.full_location
view.page_path = view.full_location.with_suffix(".md")
@override_settings(CONTENT_PAGES_PATH=BASE_PATH)
class PageOrCategoryViewTests(MockPagesTestCase, SimpleTestCase, TestCase):
"""Tests for the PageOrCategoryView class."""
def setUp(self):
"""Set test helpers, then set up fake filesystem."""
self.factory = RequestFactory()
self.view = PageOrCategoryView.as_view()
self.ViewClass = PageOrCategoryView()
super().setUp()
# Integration tests
def test_valid_page_or_category_returns_200(self):
cases = [
("Page at root", "root"),
("Category page", "category"),
("Page in category", "category/with_metadata"),
("Subcategory page", "category/subcategory"),
("Page in subcategory", "category/subcategory/with_metadata"),
]
for msg, path in cases:
with self.subTest(msg=msg, path=path):
request = self.factory.get(f"/{path}")
response = self.view(request, location=path)
self.assertEqual(response.status_code, 200)
def test_nonexistent_page_returns_404(self):
with self.assertRaises(Http404):
request = self.factory.get("/invalid")
self.view(request, location="invalid")
# Unit tests
def test_get_template_names_returns_correct_templates(self):
category_template = "content/listing.html"
page_template = "content/page.html"
cases = [
("root", page_template),
("root_without_metadata", page_template),
("category/with_metadata", page_template),
("category/subcategory/with_metadata", page_template),
("category", category_template),
("category/subcategory", category_template),
]
for path, expected_template in cases:
with self.subTest(path=path, expected_template=expected_template):
patch_dispatch_attributes(self.ViewClass, path)
self.assertEqual(self.ViewClass.get_template_names(), [expected_template])
def test_get_template_names_with_nonexistent_paths_returns_404(self):
for path in ("invalid", "another_invalid", "nonexistent"):
with self.subTest(path=path):
patch_dispatch_attributes(self.ViewClass, path)
with self.assertRaises(Http404):
self.ViewClass.get_template_names()
def test_get_template_names_returns_page_template_for_category_with_page(self):
"""Make sure the proper page is returned for category locations with pages."""
patch_dispatch_attributes(self.ViewClass, "tmp")
self.assertEqual(self.ViewClass.get_template_names(), ["content/page.html"])
def test_get_context_data_with_valid_page(self):
"""The method should return required fields in the template context."""
request = self.factory.get("/root")
self.ViewClass.setup(request)
self.ViewClass.dispatch(request, location="root")
cases = [
("Context includes HTML page content", "page", PARSED_HTML),
("Context includes page title", "page_title", PARSED_METADATA["title"]),
(
"Context includes page description",
"page_description",
PARSED_METADATA["description"]
),
(
"Context includes relevant link names and URLs",
"relevant_links",
PARSED_METADATA["relevant_links"]
),
]
context = self.ViewClass.get_context_data()
for msg, key, expected_value in cases:
with self.subTest(msg=msg):
self.assertEqual(context[key], expected_value)
def test_get_context_data_with_valid_category(self):
"""The method should return required fields in the template context."""
request = self.factory.get("/category")
self.ViewClass.setup(request)
self.ViewClass.dispatch(request, location="category")
cases = [
(
"Context includes subcategory names and their information",
"categories",
{"subcategory": PARSED_CATEGORY_INFO}
),
(
"Context includes page names and their metadata",
"pages",
{"with_metadata": PARSED_METADATA}
),
(
"Context includes page description",
"page_description",
PARSED_CATEGORY_INFO["description"]
),
("Context includes page title", "page_title", PARSED_CATEGORY_INFO["title"]),
]
context = self.ViewClass.get_context_data()
for msg, key, expected_value in cases:
with self.subTest(msg=msg):
self.assertEqual(context[key], expected_value)
def test_get_context_data_for_category_with_page(self):
"""Make sure the proper page is returned for category locations with pages."""
request = self.factory.get("/category")
self.ViewClass.setup(request)
self.ViewClass.dispatch(request, location="tmp")
context = self.ViewClass.get_context_data()
expected_page_context = {
"page": PARSED_HTML,
"page_title": PARSED_METADATA["title"],
"page_description": PARSED_METADATA["description"],
"relevant_links": PARSED_METADATA["relevant_links"],
"subarticles": [{"path": "category", "name": "Category Name"}]
}
for key, expected_value in expected_page_context.items():
with self.subTest():
self.assertEqual(context[key], expected_value)
def test_get_context_data_breadcrumbs(self):
"""The method should return correct breadcrumbs."""
request = self.factory.get("/category/subcategory/with_metadata")
self.ViewClass.setup(request)
self.ViewClass.dispatch(request, location="category/subcategory/with_metadata")
context = self.ViewClass.get_context_data()
# Convert to paths to avoid dealing with non-standard path separators
for item in context["breadcrumb_items"]:
item["path"] = Path(item["path"])
self.assertEqual(
context["breadcrumb_items"],
[
{"name": PARSED_CATEGORY_INFO["title"], "path": Path(".")},
{"name": PARSED_CATEGORY_INFO["title"], "path": Path("category")},
{"name": PARSED_CATEGORY_INFO["title"], "path": Path("category/subcategory")},
]
)
class TagViewTests(django.test.TestCase):
"""Tests for the TagView class."""
def setUp(self):
"""Set test helpers, then set up fake filesystem."""
super().setUp()
self.commit = Commit.objects.create(**TEST_COMMIT_KWARGS)
def test_routing(self):
"""Test that the correct template is returned for each route."""
Tag.objects.create(name="example", last_commit=self.commit)
Tag.objects.create(name="grouped-tag", group="group-name", last_commit=self.commit)
cases = [
("/pages/tags/example/", "content/tag.html"),
("/pages/tags/group-name/", "content/listing.html"),
("/pages/tags/group-name/grouped-tag/", "content/tag.html"),
]
for url, template in cases:
with self.subTest(url=url):
response = self.client.get(url)
self.assertEqual(200, response.status_code)
self.assertTemplateUsed(response, template)
def test_valid_tag_returns_200(self):
"""Test that a page is returned for a valid tag."""
Tag.objects.create(name="example", body="This is the tag body.", last_commit=self.commit)
response = self.client.get("/pages/tags/example/")
self.assertEqual(200, response.status_code)
self.assertIn("This is the tag body", response.content.decode("utf-8"))
self.assertTemplateUsed(response, "content/tag.html")
def test_invalid_tag_404(self):
"""Test that a tag which doesn't exist raises a 404."""
with mock.patch("pydis_site.apps.content.utils.fetch_tags", autospec=True):
response = self.client.get("/pages/tags/non-existent/")
self.assertEqual(404, response.status_code)
def test_context_tag(self):
"""Test that the context contains the required data for a tag."""
body = textwrap.dedent("""
---
unused: frontmatter
----
Tag content here.
""")
tag = Tag.objects.create(name="example", body=body, last_commit=self.commit)
response = self.client.get("/pages/tags/example/")
expected = {
"page_title": "example",
"page": markdown.markdown("Tag content here."),
"tag": tag,
"breadcrumb_items": [
{"name": "Pages", "path": "."},
{"name": "Tags", "path": "tags"},
]
}
for key in expected:
self.assertEqual(
expected[key], response.context.get(key), f"context.{key} did not match"
)
def test_context_grouped_tag(self):
"""
Test the context for a tag in a group.
The only difference between this and a regular tag are the breadcrumbs,
so only those are checked.
"""
Tag.objects.create(
name="example", body="Body text", group="group-name", last_commit=self.commit
)
response = self.client.get("/pages/tags/group-name/example/")
self.assertListEqual([
{"name": "Pages", "path": "."},
{"name": "Tags", "path": "tags"},
{"name": "group-name", "path": "tags/group-name"},
], response.context.get("breadcrumb_items"))
def test_group_page(self):
"""Test rendering of a group's root page."""
Tag.objects.create(name="tag-1", body="Body 1", group="group-name", last_commit=self.commit)
Tag.objects.create(name="tag-2", body="Body 2", group="group-name", last_commit=self.commit)
Tag.objects.create(name="not-included", last_commit=self.commit)
response = self.client.get("/pages/tags/group-name/")
content = response.content.decode("utf-8")
self.assertInHTML("<div class='level-left'>group-name</div>", content)
self.assertInHTML(
f"<a class='level-item fab fa-github' href='{Tag.URL_BASE}/group-name'>",
content
)
self.assertIn(">tag-1</span>", content)
self.assertIn(">tag-2</span>", content)
self.assertNotIn(
">not-included</span>",
content,
"Tags not in this group shouldn't be rendered."
)
self.assertInHTML("<p>Body 1</p>", content)
def test_markdown(self):
"""Test that markdown content is rendered properly."""
body = textwrap.dedent("""
```py
Hello world!
```
**This text is in bold**
""")
Tag.objects.create(name="example", body=body, last_commit=self.commit)
response = self.client.get("/pages/tags/example/")
content = response.content.decode("utf-8")
self.assertInHTML('<code class="language-py">Hello world!</code>', content)
self.assertInHTML("<strong>This text is in bold</strong>", content)
def test_embed(self):
"""Test that an embed from the frontmatter is treated correctly."""
body = textwrap.dedent("""
---
embed:
title: Embed title
image:
url: https://google.com
---
Tag body.
""")
Tag.objects.create(name="example", body=body, last_commit=self.commit)
response = self.client.get("/pages/tags/example/")
content = response.content.decode("utf-8")
self.assertInHTML('<img alt="Embed title" src="https://google.com"/>', content)
self.assertInHTML("<p>Tag body.</p>", content)
def test_embed_title(self):
"""Test that the page title gets set to the embed title."""
body = textwrap.dedent("""
---
embed:
title: Embed title
---
""")
Tag.objects.create(name="example", body=body, last_commit=self.commit)
response = self.client.get("/pages/tags/example/")
self.assertEqual(
"Embed title",
response.context.get("page_title"),
"The page title must match the embed title."
)
def test_hyperlinked_item(self):
"""Test hyperlinking of tags works as intended."""
filler_before, filler_after = "empty filler text\n\n", "more\nfiller"
body = filler_before + "`!tags return`" + filler_after
Tag.objects.create(name="example", body=body, last_commit=self.commit)
other_url = reverse("content:tag", kwargs={"location": "return"})
response = self.client.get("/pages/tags/example/")
self.assertEqual(
markdown.markdown(filler_before + f"[`!tags return`]({other_url})" + filler_after),
response.context.get("page")
)
def test_hyperlinked_group(self):
"""Test hyperlinking with a group works as intended."""
Tag.objects.create(
name="example", body="!tags group-name grouped-tag", last_commit=self.commit
)
Tag.objects.create(name="grouped-tag", group="group-name")
other_url = reverse("content:tag", kwargs={"location": "group-name/grouped-tag"})
response = self.client.get("/pages/tags/example/")
self.assertEqual(
markdown.markdown(f"[!tags group-name grouped-tag]({other_url})"),
response.context.get("page")
)
def test_hyperlinked_extra_text(self):
"""Test hyperlinking when a tag is followed by extra, unrelated text."""
Tag.objects.create(
name="example", body="!tags other unrelated text", last_commit=self.commit
)
Tag.objects.create(name="other")
other_url = reverse("content:tag", kwargs={"location": "other"})
response = self.client.get("/pages/tags/example/")
self.assertEqual(
markdown.markdown(f"[!tags other]({other_url}) unrelated text"),
response.context.get("page")
)
def test_tags_have_no_edit_on_github_link(self):
"""Tags should not have the standard edit on GitHub link."""
# The standard "Edit on GitHub" link should not be displayed on tags
# because they have their own GitHub icon that links there.
Tag.objects.create(name="example", body="Joe William Banks", last_commit=self.commit)
response = self.client.get("/pages/tags/example/")
self.assertNotContains(response, "Edit on GitHub")
def test_tag_root_page(self):
"""Test the root tag page which lists all tags."""
Tag.objects.create(name="tag-1", last_commit=self.commit)
Tag.objects.create(name="tag-2", last_commit=self.commit)
Tag.objects.create(name="tag-3", last_commit=self.commit)
response = self.client.get("/pages/tags/")
content = response.content.decode("utf-8")
self.assertTemplateUsed(response, "content/listing.html")
self.assertInHTML('<div class="level-left">Tags</div>', content)
for tag_number in range(1, 4):
self.assertIn(f"tag-{tag_number}</span>", content)
|