aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps
diff options
context:
space:
mode:
authorGravatar Kieran Siek <[email protected]>2021-03-26 14:40:59 +0800
committerGravatar GitHub <[email protected]>2021-03-26 14:40:59 +0800
commit494c63a51a778253e8f90868338d42bd41a700a9 (patch)
treeb48cf80d9b5af914e058d5bd0c616f81972e040b /pydis_site/apps
parentMerge pull request #427 from python-discord/ks123/dewikification/event-pages (diff)
parentRemove guide reference to `markdown2`. (diff)
Merge pull request #393 from ks129/guides-app
Dewikification - Create content app
Diffstat (limited to 'pydis_site/apps')
-rw-r--r--pydis_site/apps/content/__init__.py0
-rw-r--r--pydis_site/apps/content/apps.py7
-rw-r--r--pydis_site/apps/content/migrations/__init__.py0
-rw-r--r--pydis_site/apps/content/resources/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/guides/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md143
-rw-r--r--pydis_site/apps/content/tests/__init__.py0
-rw-r--r--pydis_site/apps/content/tests/helpers.py84
-rw-r--r--pydis_site/apps/content/tests/test_utils.py91
-rw-r--r--pydis_site/apps/content/tests/test_views.py145
-rw-r--r--pydis_site/apps/content/urls.py9
-rw-r--r--pydis_site/apps/content/utils.py57
-rw-r--r--pydis_site/apps/content/views/__init__.py3
-rw-r--r--pydis_site/apps/content/views/page_category.py61
-rw-r--r--pydis_site/apps/home/urls.py1
16 files changed, 607 insertions, 0 deletions
diff --git a/pydis_site/apps/content/__init__.py b/pydis_site/apps/content/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/content/__init__.py
diff --git a/pydis_site/apps/content/apps.py b/pydis_site/apps/content/apps.py
new file mode 100644
index 00000000..1e300a48
--- /dev/null
+++ b/pydis_site/apps/content/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class ContentConfig(AppConfig):
+ """Django AppConfig for content app."""
+
+ name = 'content'
diff --git a/pydis_site/apps/content/migrations/__init__.py b/pydis_site/apps/content/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/content/migrations/__init__.py
diff --git a/pydis_site/apps/content/resources/_info.yml b/pydis_site/apps/content/resources/_info.yml
new file mode 100644
index 00000000..583cab18
--- /dev/null
+++ b/pydis_site/apps/content/resources/_info.yml
@@ -0,0 +1,2 @@
+name: Pages
+description: Guides, articles, and pages hosted on the site.
diff --git a/pydis_site/apps/content/resources/guides/_info.yml b/pydis_site/apps/content/resources/guides/_info.yml
new file mode 100644
index 00000000..59c60a7b
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/_info.yml
@@ -0,0 +1,2 @@
+name: Guides
+description: Made by us, for you.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml
new file mode 100644
index 00000000..7c9a2225
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml
@@ -0,0 +1,2 @@
+name: Python Discord Guides
+description: Guides related to the Python Discord server and community.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md b/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md
new file mode 100644
index 00000000..f258ef74
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md
@@ -0,0 +1,143 @@
+---
+title: How to Contribute a Page
+description: Learn how to write and publish a page to this website.
+icon_class: fas
+icon: fa-info
+relevant_links:
+ Contributing to Site: https://pythondiscord.com/pages/contributing/site/
+ Using Git: https://pythondiscord.com/pages/contributing/working-with-git/
+---
+
+Pages, which include guides, articles, and other static content, are stored in markdown files in the `site` repository on Github.
+If you are interested in writing or modifying pages seen here on the site, follow the steps below.
+
+For further assistance and help with contributing pages, send a message to the `#dev-contrib` channel in the Discord server!
+
+## Prerequisites
+Before working on a new page, you have to [setup the site project locally](https://pythondiscord.com/pages/contributing/site/).
+It is also a good idea to familiarize yourself with the [git workflow](https://pythondiscord.com/pages/contributing/working-with-git/), as it is part of the contribution workflow.
+
+Additionally, please submit your proposed page or modification to a page as an [issue in the site repository](https://github.com/python-discord/site/issues), or discuss it in the `#dev-contrib` channel in the server.
+As website changes require staff approval, discussing the page content beforehand helps with accelerating the contribution process, and avoids wasted work in the event the proposed page is not accepted.
+
+## Creating the Page
+All pages are located in the `site` repo, at the path `pydis_site/apps/content/resources/`. This is the root folder, which corresponds to the URL `www.pythondiscord.com/pages/`.
+For example, the file `pydis_site/apps/content/resources/hello-world.md` will result in a page available at `www.pythondiscord.com/pages/hello-world`.
+
+Nested folders represent page categories on the website. Each folder under the root folder must include a `_info.yml` file with the following:
+
+```yml
+name: Category name
+description: Category description
+```
+
+All the markdown files in this folder will then be under this category.
+
+## Writing the Page
+Files representing pages are in `.md` (Markdown) format, with all-lowercase filenames and spaces replaced with `-` characters.
+
+Each page must include required metadata, and optionally additional metadata to modify the appearance of the page.
+The metadata is written in YAML, and should be enclosed in triple dashes `---` *at the top of the markdown file*.
+
+**Example:**
+```yaml
+---
+title: How to Contribute a Page
+description: Learn how to write and publish a page to this website.
+icon_class: fas
+icon: fa-info
+relevant_links:
+ Contributing to Site: https://pythondiscord.com/pages/contributing/site/
+ Using Git: https://pythondiscord.com/pages/contributing/working-with-git/
+---
+
+Pages, which include guides, articles, and other static content,...
+```
+
+### Required Fields
+- **title:** Easily-readable title for your article.
+- **description:** Short, 1-2 line description of the page's content.
+
+### Optional Fields
+- **icon_class:** Favicon class for the category entry for the page. Default: `fab`
+- **icon:** Favicon for the category entry for the page. Default: `fa-python` <i class="fab fa-python is-black" aria-hidden="true"></i>
+- **relevant_links:** A YAML dictionary containing `text:link` pairs. See the example above.
+
+## Extended Markdown
+
+Apart from standard Markdown, certain additions are available:
+
+### Abbreviations
+HTML `<abbr>` tags can be used in markdown using this format:
+
+**Markdown:**
+```nohighlight
+This website is HTML generated from YAML and Markdown.
+
+*[HTML]: Hyper Text Markup Language
+*[YAML]: YAML Ain't Markup Language
+```
+
+**Output:**
+
+This website is <abbr title="Hyper Text Markup Language">HTML</abbr>
+generated from <abbr title="YAML Ain't Markup Language">YAML</abbr> and Markdown.
+
+---
+
+### Footnotes
+**Markdown:**
+```nohighlight
+This footnote[^1] links to the bottom[^custom_label] of the page[^3].
+
+[^1]: Footnote labels start with a caret `^`.
+[^3]: The footnote link is numbered based on the order of the labels.
+[^custom label]: Footnote labels can contain any text within square brackets.
+```
+
+**Output:**
+
+This footnote[^1] links to the bottom[^custom label] of the page[^3].
+
+[^1]: Footnote labels start with a caret `^`.
+[^3]: The footnote link is numbered based on the order of the labels.
+[^custom label]: Footnote labels can contain any text within square brackets.
+
+---
+
+### Tables
+
+**Markdown:**
+```nohighlight
+| This is header | This is another header |
+| -------------- | ---------------------- |
+| An item | Another item |
+```
+
+**Output:**
+
+| This is header | This is another header |
+| -------------- | ---------------------- |
+| An item | Another item |
+
+---
+
+### Codeblock Syntax Highlighting
+Syntax highlighting is provided by `highlight.js`.
+To activate syntax highlighting, put the language directly after the starting backticks.
+
+**Markdown:**
+````nohighlight
+```python
+import os
+
+path = os.path.join("foo", "bar")
+```
+````
+
+**Output:**
+```python
+import os
+
+path = os.path.join("foo", "bar")
+```
diff --git a/pydis_site/apps/content/tests/__init__.py b/pydis_site/apps/content/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/content/tests/__init__.py
diff --git a/pydis_site/apps/content/tests/helpers.py b/pydis_site/apps/content/tests/helpers.py
new file mode 100644
index 00000000..4e0cca34
--- /dev/null
+++ b/pydis_site/apps/content/tests/helpers.py
@@ -0,0 +1,84 @@
+from pyfakefs.fake_filesystem_unittest import TestCase
+
+# Valid markdown content with YAML metadata
+MARKDOWN_WITH_METADATA = """
+---
+title: TestTitle
+description: TestDescription
+relevant_links:
+ Python Discord: https://pythondiscord.com
+ Discord: https://discord.com
+---
+# This is a header.
+"""
+
+MARKDOWN_WITHOUT_METADATA = """#This is a header."""
+
+# Valid YAML in a _info.yml file
+CATEGORY_INFO = """
+name: Category Name
+description: Description
+"""
+
+# The HTML generated from the above markdown data
+PARSED_HTML = (
+ '<h1 id="this-is-a-header">This is a header.'
+ '<a class="headerlink" href="#this-is-a-header" title="Permanent link">&para;</a></h1>'
+)
+
+# The YAML metadata parsed from the above markdown data
+PARSED_METADATA = {
+ "title": "TestTitle", "description": "TestDescription",
+ "relevant_links": {
+ "Python Discord": "https://pythondiscord.com",
+ "Discord": "https://discord.com"
+ }
+}
+
+# The YAML data parsed from the above _info.yml file
+PARSED_CATEGORY_INFO = {"name": "Category Name", "description": "Description"}
+
+
+class MockPagesTestCase(TestCase):
+ """
+ TestCase with a fake filesystem for testing.
+
+ Structure:
+ ├── _info.yml
+ ├── root.md
+ ├── root_without_metadata.md
+ ├── not_a_page.md
+ ├── tmp
+ |   ├── _info.yml
+ |   └── category_without_info
+ └── category
+    ├── _info.yml
+    ├── with_metadata.md
+    └── subcategory
+    ├── with_metadata.md
+       └── without_metadata.md
+ """
+
+ def setUp(self):
+ """Create the fake filesystem."""
+ self.setUpPyfakefs()
+
+ self.fs.create_file("_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file("root.md", contents=MARKDOWN_WITH_METADATA)
+ self.fs.create_file("root_without_metadata.md", contents=MARKDOWN_WITHOUT_METADATA)
+ self.fs.create_file("not_a_page.md/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file("category/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file("category/with_metadata.md", contents=MARKDOWN_WITH_METADATA)
+ self.fs.create_file("category/subcategory/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file(
+ "category/subcategory/with_metadata.md", contents=MARKDOWN_WITH_METADATA
+ )
+ self.fs.create_file(
+ "category/subcategory/without_metadata.md", contents=MARKDOWN_WITHOUT_METADATA
+ )
+
+ # There is always a `tmp` directory in the filesystem, so make it a category
+ # for testing purposes.
+ # See: https://jmcgeheeiv.github.io/pyfakefs/release/usage.html#os-temporary-directories
+ self.fs.create_file("tmp/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_dir("tmp/category_without_info")
diff --git a/pydis_site/apps/content/tests/test_utils.py b/pydis_site/apps/content/tests/test_utils.py
new file mode 100644
index 00000000..58175d6f
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_utils.py
@@ -0,0 +1,91 @@
+from pathlib import Path
+
+from django.http import Http404
+
+from pydis_site.apps.content import utils
+from pydis_site.apps.content.tests.helpers import (
+ MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA
+)
+
+
+class GetCategoryTests(MockPagesTestCase):
+ """Tests for the get_category function."""
+
+ def test_get_valid_category(self):
+ result = utils.get_category(Path("category"))
+
+ self.assertEqual(result, {"name": "Category Name", "description": "Description"})
+
+ def test_get_nonexistent_category(self):
+ with self.assertRaises(Http404):
+ utils.get_category(Path("invalid"))
+
+ def test_get_category_with_path_to_file(self):
+ # Valid categories are directories, not files
+ with self.assertRaises(Http404):
+ utils.get_category(Path("root.md"))
+
+ def test_get_category_without_info_yml(self):
+ # Categories should provide an _info.yml file
+ with self.assertRaises(FileNotFoundError):
+ utils.get_category(Path("tmp/category_without_info"))
+
+
+class GetCategoriesTests(MockPagesTestCase):
+ """Tests for the get_categories function."""
+
+ def test_get_root_categories(self):
+ result = utils.get_categories(Path("."))
+
+ info = PARSED_CATEGORY_INFO
+ self.assertEqual(result, {"category": info, "tmp": info, "not_a_page.md": info})
+
+ def test_get_categories_with_subcategories(self):
+ result = utils.get_categories(Path("category"))
+
+ self.assertEqual(result, {"subcategory": PARSED_CATEGORY_INFO})
+
+ def test_get_categories_without_subcategories(self):
+ result = utils.get_categories(Path("category/subcategory"))
+
+ self.assertEqual(result, {})
+
+
+class GetCategoryPagesTests(MockPagesTestCase):
+ """Tests for the get_category_pages function."""
+
+ def test_get_pages_in_root_category_successfully(self):
+ """The method should successfully retrieve page metadata."""
+ root_category_pages = utils.get_category_pages(Path("."))
+ self.assertEqual(
+ root_category_pages, {"root": PARSED_METADATA, "root_without_metadata": {}}
+ )
+
+ def test_get_pages_in_subcategories_successfully(self):
+ """The method should successfully retrieve page metadata."""
+ category_pages = utils.get_category_pages(Path("category"))
+
+ # Page metadata is properly retrieved
+ self.assertEqual(category_pages, {"with_metadata": PARSED_METADATA})
+
+
+class GetPageTests(MockPagesTestCase):
+ """Tests for the get_page function."""
+
+ def test_get_page(self):
+ cases = [
+ ("Root page with metadata", "root.md", PARSED_HTML, PARSED_METADATA),
+ ("Root page without metadata", "root_without_metadata.md", PARSED_HTML, {}),
+ ("Page with metadata", "category/with_metadata.md", PARSED_HTML, PARSED_METADATA),
+ ("Page without metadata", "category/subcategory/without_metadata.md", PARSED_HTML, {}),
+ ]
+
+ for msg, page_path, expected_html, expected_metadata in cases:
+ with self.subTest(msg=msg):
+ html, metadata = utils.get_page(Path(page_path))
+ self.assertEqual(html, expected_html)
+ self.assertEqual(metadata, expected_metadata)
+
+ def test_get_nonexistent_page_returns_404(self):
+ with self.assertRaises(Http404):
+ utils.get_page(Path("invalid"))
diff --git a/pydis_site/apps/content/tests/test_views.py b/pydis_site/apps/content/tests/test_views.py
new file mode 100644
index 00000000..560378bc
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_views.py
@@ -0,0 +1,145 @@
+from pathlib import Path
+from unittest import TestCase
+
+from django.http import Http404
+from django.test import RequestFactory, SimpleTestCase, override_settings
+from pyfakefs import fake_filesystem_unittest
+
+from pydis_site.apps.content.tests.helpers import (
+ MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA
+)
+from pydis_site.apps.content.views import PageOrCategoryView
+
+
+# Set the module constant within Patcher to use the fake filesystem
+# https://jmcgeheeiv.github.io/pyfakefs/master/usage.html#modules-to-reload
+with fake_filesystem_unittest.Patcher() as _:
+ BASE_PATH = Path(".")
+
+
+@override_settings(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):
+ self.ViewClass.full_location = Path(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):
+ self.ViewClass.full_location = Path(path)
+ with self.assertRaises(Http404):
+ self.ViewClass.get_template_names()
+
+ 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["name"]),
+ ]
+
+ 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_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()
+ self.assertEquals(
+ context["breadcrumb_items"],
+ [
+ {"name": PARSED_CATEGORY_INFO["name"], "path": "."},
+ {"name": PARSED_CATEGORY_INFO["name"], "path": "category"},
+ {"name": PARSED_CATEGORY_INFO["name"], "path": "category/subcategory"},
+ ]
+ )
diff --git a/pydis_site/apps/content/urls.py b/pydis_site/apps/content/urls.py
new file mode 100644
index 00000000..c11b222a
--- /dev/null
+++ b/pydis_site/apps/content/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+
+from . import views
+
+app_name = "content"
+urlpatterns = [
+ path("", views.PageOrCategoryView.as_view(), name='pages'),
+ path("<path:location>/", views.PageOrCategoryView.as_view(), name='page_category'),
+]
diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py
new file mode 100644
index 00000000..726c991f
--- /dev/null
+++ b/pydis_site/apps/content/utils.py
@@ -0,0 +1,57 @@
+from pathlib import Path
+from typing import Dict, Tuple
+
+import frontmatter
+import markdown
+import yaml
+from django.http import Http404
+from markdown.extensions.toc import TocExtension
+
+
+def get_category(path: Path) -> Dict[str, str]:
+ """Load category information by name from _info.yml."""
+ if not path.is_dir():
+ raise Http404("Category not found.")
+
+ return yaml.safe_load(path.joinpath("_info.yml").read_text(encoding="utf-8"))
+
+
+def get_categories(path: Path) -> Dict[str, Dict]:
+ """Get information for all categories."""
+ categories = {}
+
+ for item in path.iterdir():
+ if item.is_dir():
+ categories[item.name] = get_category(item)
+
+ return categories
+
+
+def get_category_pages(path: Path) -> Dict[str, Dict]:
+ """Get all page names and their metadata at a category path."""
+ pages = {}
+
+ for item in path.glob("*.md"):
+ if item.is_file():
+ pages[item.stem] = frontmatter.load(item).metadata
+
+ return pages
+
+
+def get_page(path: Path) -> Tuple[str, Dict]:
+ """Get one specific page."""
+ if not path.is_file():
+ raise Http404("Page not found.")
+
+ metadata, content = frontmatter.parse(path.read_text(encoding="utf-8"))
+ html = markdown.markdown(
+ content,
+ extensions=[
+ "extra",
+ # Empty string for marker to disable text searching for [TOC]
+ # By using a metadata key instead, we save time on long markdown documents
+ TocExtension(title="Table of Contents:", permalink=True, marker="")
+ ]
+ )
+
+ return str(html), metadata
diff --git a/pydis_site/apps/content/views/__init__.py b/pydis_site/apps/content/views/__init__.py
new file mode 100644
index 00000000..70ea1c7a
--- /dev/null
+++ b/pydis_site/apps/content/views/__init__.py
@@ -0,0 +1,3 @@
+from .page_category import PageOrCategoryView
+
+__all__ = ["PageOrCategoryView"]
diff --git a/pydis_site/apps/content/views/page_category.py b/pydis_site/apps/content/views/page_category.py
new file mode 100644
index 00000000..eec4e7e5
--- /dev/null
+++ b/pydis_site/apps/content/views/page_category.py
@@ -0,0 +1,61 @@
+import typing as t
+from pathlib import Path
+
+from django.conf import settings
+from django.http import Http404
+from django.views.generic import TemplateView
+
+from pydis_site.apps.content import utils
+
+
+class PageOrCategoryView(TemplateView):
+ """Handles pages and page categories."""
+
+ def dispatch(self, request: t.Any, *args, **kwargs) -> t.Any:
+ """Conform URL path location to the filesystem path."""
+ self.location = Path(kwargs.get("location", ""))
+ self.full_location = settings.PAGES_PATH / self.location
+
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_template_names(self) -> t.List[str]:
+ """Checks if the view uses the page template or listing template."""
+ if self.full_location.is_dir():
+ template_name = "content/listing.html"
+ elif self.full_location.with_suffix(".md").is_file():
+ template_name = "content/page.html"
+ else:
+ raise Http404
+
+ return [template_name]
+
+ def get_context_data(self, **kwargs) -> t.Dict[str, t.Any]:
+ """Assign proper context variables based on what resource user requests."""
+ context = super().get_context_data(**kwargs)
+
+ if self.full_location.is_dir():
+ context["categories"] = utils.get_categories(self.full_location)
+ context["pages"] = utils.get_category_pages(self.full_location)
+
+ category = utils.get_category(self.full_location)
+ context["page_title"] = category["name"]
+ context["page_description"] = category["description"]
+
+ context["path"] = f"{self.location}/" # Add trailing slash here to simplify template
+ elif self.full_location.with_suffix(".md").is_file():
+ page, metadata = utils.get_page(self.full_location.with_suffix(".md"))
+ context["page"] = page
+ context["page_title"] = metadata["title"]
+ context["page_description"] = metadata["description"]
+ context["relevant_links"] = metadata.get("relevant_links", {})
+ else:
+ raise Http404
+
+ context["breadcrumb_items"] = [
+ {
+ "name": utils.get_category(settings.PAGES_PATH / location)["name"],
+ "path": str(location)
+ } for location in reversed(self.location.parents)
+ ]
+
+ return context
diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py
index d7db6ff1..3c716875 100644
--- a/pydis_site/apps/home/urls.py
+++ b/pydis_site/apps/home/urls.py
@@ -8,5 +8,6 @@ urlpatterns = [
path('', HomeView.as_view(), name='home'),
path('admin/', admin.site.urls),
path('resources/', include('pydis_site.apps.resources.urls')),
+ path('pages/', include('pydis_site.apps.content.urls')),
path('events/', include('pydis_site.apps.events.urls', namespace='events')),
]