aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Pipfile2
-rw-r--r--Pipfile.lock142
-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/content/guides/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/content/guides/pydis-guides/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/content/guides/pydis-guides/how-to-write-a-article.md80
-rw-r--r--pydis_site/apps/content/tests/__init__.py0
-rw-r--r--pydis_site/apps/content/tests/test_content/category/_info.yml2
-rw-r--r--pydis_site/apps/content/tests/test_content/category/subcategory/_info.yml2
-rw-r--r--pydis_site/apps/content/tests/test_content/category/subcategory/test4.md6
-rw-r--r--pydis_site/apps/content/tests/test_content/category/test3.md6
-rw-r--r--pydis_site/apps/content/tests/test_content/test.md8
-rw-r--r--pydis_site/apps/content/tests/test_content/test2.md6
-rw-r--r--pydis_site/apps/content/tests/test_utils.py188
-rw-r--r--pydis_site/apps/content/tests/test_views.py161
-rw-r--r--pydis_site/apps/content/urls.py9
-rw-r--r--pydis_site/apps/content/utils.py112
-rw-r--r--pydis_site/apps/content/views/__init__.py4
-rw-r--r--pydis_site/apps/content/views/article_category.py75
-rw-r--r--pydis_site/apps/content/views/articles.py16
-rw-r--r--pydis_site/apps/home/urls.py1
-rw-r--r--pydis_site/settings.py8
-rw-r--r--pydis_site/static/css/content/articles.css7
-rw-r--r--pydis_site/templates/content/article.html75
-rw-r--r--pydis_site/templates/content/listing.html62
-rw-r--r--pydis_site/templates/resources/resources.html2
28 files changed, 926 insertions, 59 deletions
diff --git a/Pipfile b/Pipfile
index 3bc89d62..dae75ea0 100644
--- a/Pipfile
+++ b/Pipfile
@@ -18,6 +18,8 @@ pyyaml = "~=5.1"
pyuwsgi = {version = "~=2.0", sys_platform = "!='win32'"}
sentry-sdk = "~=0.14"
gitpython = "~=3.1.7"
+markdown2 = "~=2.3.9"
+python-dateutil = "~=2.8.1"
[dev-packages]
coverage = "~=5.0"
diff --git a/Pipfile.lock b/Pipfile.lock
index 8c91e0db..18b0d582 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "07f534656de1dd12baafc39bcdb68f143bbe195f8220211724427e15244dc187"
+ "sha256": "c0e53fd7b7c3d2fc62331078d4ba9301a7e848cd39ec8b41f0f7bc6d911a4d88"
},
"pipfile-spec": 6,
"requires": {
@@ -18,11 +18,11 @@
"default": {
"asgiref": {
"hashes": [
- "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
- "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
+ "sha256:a5098bc870b80e7b872bff60bb363c7f2c2c89078759f6c47b53ff8c525a152e",
+ "sha256:cd88907ecaec59d78e4ac00ea665b03e571cb37e3a0e37b3702af1a9e86c365a"
],
"markers": "python_version >= '3.5'",
- "version": "==3.2.10"
+ "version": "==3.3.0"
},
"certifi": {
"hashes": [
@@ -103,11 +103,11 @@
},
"gitpython": {
"hashes": [
- "sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8",
- "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"
+ "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
+ "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
],
"index": "pypi",
- "version": "==3.1.9"
+ "version": "==3.1.11"
},
"idna": {
"hashes": [
@@ -135,44 +135,61 @@
],
"version": "==0.20.1"
},
+ "markdown2": {
+ "hashes": [
+ "sha256:85956d8119fa6378156fef65545d66705a842819d2e1b50379a2b9d2aaa17cf0",
+ "sha256:fef148e5fd68d4532286c3e2943e9d2c076a8ad781b0a70a9d599a0ffe91652d"
+ ],
+ "index": "pypi",
+ "version": "==2.3.10"
+ },
"psycopg2-binary": {
"hashes": [
- "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
- "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
+ "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
- "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
+ "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
+ "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
- "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
- "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
- "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
- "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
- "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
- "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
- "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
- "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
+ "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
+ "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
+ "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
"sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
+ "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
+ "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
"sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd",
+ "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
"sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
- "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
- "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
- "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
- "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
- "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
- "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
- "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
"sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66",
+ "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
+ "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5",
"sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
+ "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
+ "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
"sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449",
- "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
- "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
- "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
- "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
"sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
- "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
+ "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
+ "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
+ "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
+ "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
+ "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
+ "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
+ "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
+ "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
+ "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
+ "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
+ "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679"
],
"index": "pypi",
"version": "==2.8.6"
},
+ "python-dateutil": {
+ "hashes": [
+ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
+ "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
+ ],
+ "index": "pypi",
+ "version": "==2.8.1"
+ },
"pytz": {
"hashes": [
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
@@ -226,11 +243,11 @@
},
"sentry-sdk": {
"hashes": [
- "sha256:1d91a0059d2d8bb980bec169578035c2f2d4b93cd8a4fb5b85c81904d33e221a",
- "sha256:6222cf623e404c3e62b8e0e81c6db866ac2d12a663b7c1f7963350e3f397522a"
+ "sha256:0eea248408d36e8e7037c7b73827bea20b13a4375bf1719c406cae6fcbc094e3",
+ "sha256:5cf36eb6b1dc62d55f3c64289792cbaebc8ffa5a9da14474f49b46d20caa7fc8"
],
"index": "pypi",
- "version": "==0.18.0"
+ "version": "==0.19.1"
},
"six": {
"hashes": [
@@ -250,19 +267,19 @@
},
"sqlparse": {
"hashes": [
- "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
- "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
+ "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
+ "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==0.3.1"
+ "markers": "python_version >= '3.5'",
+ "version": "==0.4.1"
},
"urllib3": {
"hashes": [
- "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
- "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
+ "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
+ "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
- "version": "==1.25.10"
+ "version": "==1.25.11"
},
"whitenoise": {
"hashes": [
@@ -445,19 +462,19 @@
},
"gitpython": {
"hashes": [
- "sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8",
- "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"
+ "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
+ "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
],
"index": "pypi",
- "version": "==3.1.9"
+ "version": "==3.1.11"
},
"identify": {
"hashes": [
- "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4",
- "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d"
+ "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e",
+ "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.5.5"
+ "version": "==1.5.6"
},
"importlib-metadata": {
"hashes": [
@@ -484,11 +501,11 @@
},
"pbr": {
"hashes": [
- "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea",
- "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15"
+ "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
+ "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
],
"markers": "python_version >= '2.6'",
- "version": "==5.5.0"
+ "version": "==5.5.1"
},
"pep8-naming": {
"hashes": [
@@ -500,11 +517,11 @@
},
"pre-commit": {
"hashes": [
- "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a",
- "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70"
+ "sha256:7eadaa7f4547a8a19b83230ce430ba81bbe4797bd41c8d7fb54b246164628d1f",
+ "sha256:8fb2037c404ef8c87125e72564f316cf2bc94fc9c1cb184b8352117de747e164"
],
"index": "pypi",
- "version": "==2.7.1"
+ "version": "==2.8.1"
},
"pycodestyle": {
"hashes": [
@@ -589,24 +606,33 @@
"hashes": [
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
+ "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
+ "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
+ "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
+ "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
+ "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
+ "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
+ "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
+ "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
+ "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
],
"markers": "python_version < '3.8'",
@@ -622,19 +648,19 @@
},
"virtualenv": {
"hashes": [
- "sha256:3d427459dfe5ec3241a6bad046b1d10c0e445940e013c81946458987c7c7e255",
- "sha256:9160a8f6196afcb8bb91405b5362651f302ee8e810fc471f5f9ce9a06b070298"
+ "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2",
+ "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==20.0.32"
+ "version": "==20.1.0"
},
"zipp": {
"hashes": [
- "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b",
- "sha256:eed8ec0b8d1416b2ca33516a37a08892442f3954dee131e92cfd92d8fe3e7066"
+ "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108",
+ "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"
],
"markers": "python_version >= '3.6'",
- "version": "==3.3.0"
+ "version": "==3.4.0"
}
}
}
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/content/guides/_info.yml b/pydis_site/apps/content/resources/content/guides/_info.yml
new file mode 100644
index 00000000..369f05d4
--- /dev/null
+++ b/pydis_site/apps/content/resources/content/guides/_info.yml
@@ -0,0 +1,2 @@
+name: Guides
+description: Python and PyDis guides.
diff --git a/pydis_site/apps/content/resources/content/guides/pydis-guides/_info.yml b/pydis_site/apps/content/resources/content/guides/pydis-guides/_info.yml
new file mode 100644
index 00000000..64111a83
--- /dev/null
+++ b/pydis_site/apps/content/resources/content/guides/pydis-guides/_info.yml
@@ -0,0 +1,2 @@
+name: Python Discord Guides
+description: Python Discord server and community guides.
diff --git a/pydis_site/apps/content/resources/content/guides/pydis-guides/how-to-write-a-article.md b/pydis_site/apps/content/resources/content/guides/pydis-guides/how-to-write-a-article.md
new file mode 100644
index 00000000..ec89988c
--- /dev/null
+++ b/pydis_site/apps/content/resources/content/guides/pydis-guides/how-to-write-a-article.md
@@ -0,0 +1,80 @@
+---
+title: How to Write a Article
+short_description: Learn how to write a article for this website
+icon_class: fas
+icon: fa-info
+---
+
+When you are interested about how to write articles for this site (like this), then you can learn about it here.
+PyDis use Markdown (GitHub Markdown) files for articles.
+
+## Getting Started
+Before you can get started with writing a article, you need idea.
+Best way to find out is your idea good is to discuss about it in #dev-contrib channel. There can other peoples give their opinion about your idea. Even better, open issue in site repository first, then PyDis staff can see it and approve/decline this idea.
+It's good idea to wait for staff decision before starting to write guide to avoid case when you write a long long article, but then this don't get approved.
+
+To start with contributing, you should read [how to contribute to site](https://pythondiscord.com/pages/contributing/site/).
+You should also read our [Git workflow](https://pythondiscord.com/pages/contributing/working-with-git/), because you need to push your guide to GitHub.
+
+## Creating a File
+All articles is located at `site` repository, in `pydis_site/apps/content/resources/content`. Under this is root level articles (.md files) and categories (directories). Learn more about categories in [categories section](#categories).
+
+When you are writing guides, then these are located under `guides` category.
+
+At this point, you will need your article name for filename. Replace all your article name spaces with `-` and make all lowercase. Save this as `.md` (Markdown) file. This name (without Markdown extension) is path of article in URL.
+
+## Markdown Metadata
+Article files have some required metadata, like title, description, relevant pages. Metadata is first thing in file, YAML-like key-value pairs:
+
+```md
+---
+title: My Article
+short_description: This is my short description.
+relevant_links: url1,url2,url3
+relevant_link_values: Text for url1,Text for url2,Text for url3
+---
+
+Here comes content of article...
+```
+
+You can read more about Markdown metadata [here](https://github.com/trentm/python-markdown2/wiki/metadata).
+
+### Fields
+- **Name:** Easily-readable name for your article.
+- **Short Description:** Small, 1-2 line description that describe what your article explain.
+- **Relevant Links and Values:** URLs and values is under different fields, separated with comma.
+- **Icon class:** `icon_class` field have one of the favicons classes. Default is `fab`.
+- **Icon:** `icon` field have favicon name. Default `fa-python`.
+
+## Content
+For content, mostly you can use standard markdown, but there is a few addition that is available.
+
+### IDs for quick jumps
+System automatically assign IDs to headers, so like this header will get ID `ids-for-quick-jumps`.
+
+### Tables
+Tables like in GitHub is supported too:
+
+| This is header | This is too header |
+| -------------- | ------------------ |
+| My item | My item too |
+
+### Codeblocks
+Also this system supports codeblocks and provides syntax highlighting with `highlight.js`.
+To activate syntax highlight, just put language directly after starting backticks.
+
+```py
+import os
+
+path = os.path.join("foo", "bar")
+```
+
+## Categories
+To have some systematic sorting of guides, site support guides categories. Currently this system support only 1 level of categories. Categories live at `site` repo in `pydis_site/apps/content/resources/content` subdirectories. Directory name is path of category in URL. Inside category directory, there is 1 file required: `_info.yml`. This file need 2 key-value pairs defined:
+
+```yml
+name: Category name
+description: Category description
+```
+
+Then all Markdown files in this folder will be under this category.
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/test_content/category/_info.yml b/pydis_site/apps/content/tests/test_content/category/_info.yml
new file mode 100644
index 00000000..8311509d
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_content/category/_info.yml
@@ -0,0 +1,2 @@
+name: My Category
+description: My Description
diff --git a/pydis_site/apps/content/tests/test_content/category/subcategory/_info.yml b/pydis_site/apps/content/tests/test_content/category/subcategory/_info.yml
new file mode 100644
index 00000000..f1c40264
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_content/category/subcategory/_info.yml
@@ -0,0 +1,2 @@
+name: My Category 1
+description: My Description 1
diff --git a/pydis_site/apps/content/tests/test_content/category/subcategory/test4.md b/pydis_site/apps/content/tests/test_content/category/subcategory/test4.md
new file mode 100644
index 00000000..8031131d
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_content/category/subcategory/test4.md
@@ -0,0 +1,6 @@
+---
+title: Test 4
+short_description: Testing 4
+---
+
+This is also test content and in subcategory.
diff --git a/pydis_site/apps/content/tests/test_content/category/test3.md b/pydis_site/apps/content/tests/test_content/category/test3.md
new file mode 100644
index 00000000..03ddd67b
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_content/category/test3.md
@@ -0,0 +1,6 @@
+---
+title: Test 3
+short_description: Testing 3
+---
+
+This is too test content, but in category.
diff --git a/pydis_site/apps/content/tests/test_content/test.md b/pydis_site/apps/content/tests/test_content/test.md
new file mode 100644
index 00000000..175c1fdb
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_content/test.md
@@ -0,0 +1,8 @@
+---
+title: Test
+short_description: Testing
+relevant_links: https://pythondiscord.com/pages/resources/guides/asking-good-questions/,https://pythondiscord.com/pages/resources/guides/help-channels/,https://pythondiscord.com/pages/code-of-conduct/
+relevant_link_values: Asking Good Questions,Help Channel Guide,Code of Conduct
+---
+
+This is test content.
diff --git a/pydis_site/apps/content/tests/test_content/test2.md b/pydis_site/apps/content/tests/test_content/test2.md
new file mode 100644
index 00000000..14d8a54b
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_content/test2.md
@@ -0,0 +1,6 @@
+---
+title: Test 2
+short_description: Testing 2
+---
+
+This is too test content.
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..85f1139a
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_utils.py
@@ -0,0 +1,188 @@
+from datetime import datetime
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+from django.conf import settings
+from django.http import Http404
+from django.test import TestCase, override_settings
+from markdown2 import markdown
+
+from pydis_site.apps.content import utils
+
+BASE_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "content", "tests", "test_content")
+
+
+class TestGetCategory(TestCase):
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_category_successfully(self):
+ """Check does this get right data from category data file."""
+ result = utils.get_category(["category"])
+
+ self.assertEqual(result, {"name": "My Category", "description": "My Description"})
+
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_category_not_exists(self):
+ """Check does this raise 404 error when category don't exists."""
+ with self.assertRaises(Http404):
+ utils.get_category(["invalid"])
+
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_category_not_directory(self):
+ """Check does this raise 404 error when category isn't directory."""
+ with self.assertRaises(Http404):
+ utils.get_category(["test.md"])
+
+
+class TestGetCategories(TestCase):
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ @patch("pydis_site.apps.content.utils.get_category")
+ def test_get_categories(self, get_category_mock):
+ """Check does this return test content categories."""
+ get_category_mock.return_value = {"name": "My Category", "description": "My Description"}
+
+ result = utils.get_categories()
+ get_category_mock.assert_called_once_with(["category"])
+
+ self.assertEqual(
+ result, {"category": {"name": "My Category", "description": "My Description"}}
+ )
+
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_categories_root_path(self):
+ """Check does this doesn't call joinpath when getting root categories."""
+ result = utils.get_categories()
+ self.assertEqual(
+ result, {"category": {"name": "My Category", "description": "My Description"}}
+ )
+
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_categories_in_category(self):
+ """Check does this call joinpath when getting subcategories."""
+ result = utils.get_categories(["category"])
+ self.assertEqual(
+ result, {"subcategory": {"name": "My Category 1", "description": "My Description 1"}}
+ )
+
+
+class TestGetArticles(TestCase):
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_all_root_articles(self):
+ """Check does this return all root level testing content."""
+ result = utils.get_articles()
+
+ for case in ["test", "test2"]:
+ with self.subTest(guide=case):
+ md = markdown(BASE_PATH.joinpath(f"{case}.md").read_text(), extras=["metadata"])
+
+ self.assertIn(case, result)
+ self.assertEqual(md.metadata, result[case])
+
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_all_category_articles(self):
+ """Check does this return all category testing content."""
+ result = utils.get_articles(["category"])
+
+ md = markdown(BASE_PATH.joinpath("category", "test3.md").read_text(), extras=["metadata"])
+
+ self.assertIn("test3", result)
+ self.assertEqual(md.metadata, result["test3"])
+
+
+class TestGetArticle(TestCase):
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_root_article_success(self):
+ """Check does this return article HTML and metadata when root article exist."""
+ result = utils.get_article(["test"])
+
+ md = markdown(
+ BASE_PATH.joinpath("test.md").read_text(),
+ extras=[
+ "metadata",
+ "fenced-code-blocks",
+ "header-ids",
+ "strike",
+ "target-blank-links",
+ "tables",
+ "task_list"
+ ]
+ )
+
+ self.assertEqual(result, {"article": str(md), "metadata": md.metadata})
+
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_root_article_dont_exist(self):
+ """Check does this raise Http404 when root article don't exist."""
+ with self.assertRaises(Http404):
+ utils.get_article(["invalid"])
+
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_category_article_success(self):
+ """Check does this return article HTML and metadata when category guide exist."""
+ result = utils.get_article(["category", "test3"])
+
+ md = markdown(
+ BASE_PATH.joinpath("category", "test3.md").read_text(),
+ extras=[
+ "metadata",
+ "fenced-code-blocks",
+ "header-ids",
+ "strike",
+ "target-blank-links",
+ "tables",
+ "task_list"
+ ]
+ )
+
+ self.assertEqual(result, {"article": str(md), "metadata": md.metadata})
+
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_get_category_article_dont_exist(self):
+ """Check does this raise Http404 when category article don't exist."""
+ with self.assertRaises(Http404):
+ utils.get_article(["category", "invalid"])
+
+ @patch("pydis_site.settings.ARTICLES_PATH", new=BASE_PATH)
+ def test_get_category_article_category_dont_exist(self):
+ """Check does this raise Http404 when category don't exist."""
+ with self.assertRaises(Http404):
+ utils.get_article(["invalid", "some-guide"])
+
+
+class GetGitHubInformationTests(TestCase):
+ @patch("pydis_site.apps.content.utils.requests.get")
+ @patch("pydis_site.apps.content.utils.COMMITS_URL", "foobar")
+ def test_call_get_github_information_requests_get(self, requests_get_mock):
+ """Check does this call requests.get function with proper URL."""
+ utils.get_github_information(["foo"])
+ requests_get_mock.assert_called_once_with("foobar")
+
+ @patch("pydis_site.apps.content.utils.requests.get")
+ def test_github_status_code_200_response(self, requests_get_mock):
+ """Check does this return provided modified date and contributors."""
+ requests_get_mock.return_value = MagicMock(status_code=200)
+ requests_get_mock.return_value.json.return_value = [{
+ "commit": {
+ "committer": {
+ "date": datetime(2020, 10, 1).isoformat(),
+ "name": "foobar",
+ }
+ },
+ "committer": {
+ "html_url": "abc1234"
+ }
+ }]
+ result = utils.get_github_information(["foo"])
+ self.assertEqual(result, {
+ "last_modified": datetime(2020, 10, 1).strftime("%dth %B %Y"),
+ "contributors": {"foobar": "abc1234"}
+ })
+
+ @patch("pydis_site.apps.content.utils.requests.get")
+ def test_github_other_status_code_response(self, requests_get_mock):
+ """Check does this return provided modified date and contributors."""
+ requests_get_mock.return_value = MagicMock(status_code=404)
+ result = utils.get_github_information(["foo"])
+ self.assertEqual(result, {
+ "last_modified": "N/A",
+ "contributors": {}
+ })
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..98b99b83
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_views.py
@@ -0,0 +1,161 @@
+from pathlib import Path
+from unittest.mock import patch
+
+from django.conf import settings
+from django.http import Http404
+from django.test import RequestFactory, TestCase, override_settings
+from django_hosts.resolvers import reverse
+
+from pydis_site.apps.content.views import ArticleOrCategoryView
+
+BASE_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "content", "tests", "test_content")
+
+
+class TestArticlesIndexView(TestCase):
+ @patch("pydis_site.apps.content.views.articles.get_articles")
+ @patch("pydis_site.apps.content.views.articles.get_categories")
+ def test_articles_index_return_200(self, get_categories_mock, get_articles_mock):
+ """Check that content index return HTTP code 200."""
+ get_categories_mock.return_value = {}
+ get_articles_mock.return_value = {}
+
+ url = reverse('content:articles')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ get_articles_mock.assert_called_once()
+ get_categories_mock.assert_called_once()
+
+
+class TestArticleOrCategoryView(TestCase):
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ @patch("pydis_site.apps.content.views.article_category.utils.get_article")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_category")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_github_information")
+ def test_article_return_code_200(self, gh_info_mock, get_category_mock, get_article_mock):
+ get_article_mock.return_value = {"guide": "test", "metadata": {}}
+
+ url = reverse("content:article_category", args=["test2"])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ get_category_mock.assert_not_called()
+ get_article_mock.assert_called_once()
+ gh_info_mock.assert_called_once()
+
+ @patch("pydis_site.apps.content.views.article_category.utils.get_article")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_category")
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_article_return_404(self, get_category_mock, get_article_mock):
+ """Check that return code is 404 when invalid article provided."""
+ get_article_mock.side_effect = Http404("Article not found.")
+
+ url = reverse("content:article_category", args=["invalid-guide"])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 404)
+ get_article_mock.assert_not_called()
+ get_category_mock.assert_not_called()
+
+ @patch("pydis_site.apps.content.views.article_category.utils.get_category")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_articles")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_categories")
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_valid_category_code_200(
+ self,
+ get_categories_mock,
+ get_articles_mock,
+ get_category_mock
+ ):
+ """Check that return code is 200 when visiting valid category."""
+ get_category_mock.return_value = {"name": "test", "description": "test"}
+ get_articles_mock.return_value = {}
+
+ url = reverse("content:article_category", args=["category"])
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ get_articles_mock.assert_called_once()
+ get_category_mock.assert_called_once()
+ get_categories_mock.assert_called_once()
+
+ @patch("pydis_site.apps.content.views.article_category.utils.get_category")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_articles")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_categories")
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_invalid_category_code_404(
+ self,
+ get_categories_mock,
+ get_articles_mock,
+ get_category_mock
+ ):
+ """Check that return code is 404 when trying to visit invalid category."""
+ get_category_mock.side_effect = Http404("Category not found.")
+
+ url = reverse("content:article_category", args=["invalid-category"])
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 404)
+ get_category_mock.assert_not_called()
+ get_articles_mock.assert_not_called()
+ get_categories_mock.assert_not_called()
+
+ @patch("pydis_site.apps.content.views.article_category.utils.get_article")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_category")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_github_information")
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_valid_category_article_code_200(
+ self,
+ gh_info_mock,
+ get_category_mock,
+ get_article_mock
+ ):
+ """Check that return code is 200 when visiting valid category article."""
+ get_article_mock.return_value = {"guide": "test", "metadata": {}}
+
+ url = reverse("content:article_category", args=["category/test3"])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ get_article_mock.assert_called_once()
+ self.assertEqual(get_category_mock.call_count, 2)
+ gh_info_mock.assert_called_once()
+
+ @patch("pydis_site.apps.content.views.article_category.utils.get_article")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_category")
+ @patch("pydis_site.apps.content.views.article_category.utils.get_github_information")
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_invalid_category_article_code_404(
+ self,
+ gh_info_mock,
+ get_category_mock,
+ get_article_mock
+ ):
+ """Check that return code is 200 when trying to visit invalid category article."""
+ get_article_mock.side_effect = Http404("Article not found.")
+
+ url = reverse("content:article_category", args=["category/invalid"])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 404)
+ get_article_mock.assert_not_called()
+ get_category_mock.assert_not_called()
+ gh_info_mock.assert_not_called()
+
+ @override_settings(ARTICLES_PATH=BASE_PATH)
+ def test_article_category_template_names(self):
+ """Check that this return category, article template or raise Http404."""
+ factory = RequestFactory()
+ cases = [
+ {"location": "category", "output": ["content/listing.html"]},
+ {"location": "test", "output": ["content/article.html"]},
+ {"location": "invalid", "output": None, "raises": Http404}
+ ]
+
+ for case in cases:
+ with self.subTest(location=case["location"], output=case["output"]):
+ request = factory.get(f"/articles/{case['location']}")
+ instance = ArticleOrCategoryView()
+ instance.request = request
+ instance.kwargs = {"location": case["location"]}
+
+ if "raises" in case:
+ with self.assertRaises(case["raises"]):
+ instance.get_template_names()
+ else:
+ self.assertEqual(case["output"], instance.get_template_names())
diff --git a/pydis_site/apps/content/urls.py b/pydis_site/apps/content/urls.py
new file mode 100644
index 00000000..49a5a2ef
--- /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.ArticlesView.as_view(), name='articles'),
+ path("<path:location>/", views.ArticleOrCategoryView.as_view(), name='article_category'),
+]
diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py
new file mode 100644
index 00000000..a89db83c
--- /dev/null
+++ b/pydis_site/apps/content/utils.py
@@ -0,0 +1,112 @@
+import os
+from typing import Dict, List, Optional, Union
+
+import requests
+import yaml
+from dateutil import parser
+from django.conf import settings
+from django.http import Http404
+from markdown2 import markdown
+
+COMMITS_URL = "https://api.github.com/repos/{owner}/{name}/commits?path={path}&sha={branch}"
+BASE_ARTICLES_LOCATION = "pydis_site/apps/content/resources/content/"
+
+
+def get_category(path: List[str]) -> Dict[str, str]:
+ """Load category information by name from _info.yml."""
+ path = settings.ARTICLES_PATH.joinpath(*path)
+ if not path.exists() or not path.is_dir():
+ raise Http404("Category not found.")
+
+ return yaml.safe_load(path.joinpath("_info.yml").read_text())
+
+
+def get_categories(path: Optional[List[str]] = None) -> Dict[str, Dict]:
+ """Get all categories information."""
+ categories = {}
+ if path is None:
+ categories_path = settings.ARTICLES_PATH
+ path = []
+ else:
+ categories_path = settings.ARTICLES_PATH.joinpath(*path)
+
+ for name in categories_path.iterdir():
+ if name.is_dir():
+ categories[name.name] = get_category([*path, name.name])
+
+ return categories
+
+
+def get_articles(path: Optional[List[str]] = None) -> Dict[str, Dict]:
+ """Get all root or category articles."""
+ if path is None:
+ base_dir = settings.ARTICLES_PATH
+ else:
+ base_dir = settings.ARTICLES_PATH.joinpath(*path)
+
+ articles = {}
+
+ for item in base_dir.iterdir():
+ if item.is_file() and item.name.endswith(".md"):
+ md = markdown(item.read_text(), extras=["metadata"])
+ articles[os.path.splitext(item.name)[0]] = md.metadata
+
+ return articles
+
+
+def get_article(path: List[str]) -> Dict[str, Union[str, Dict]]:
+ """Get one specific article. When category is specified, get it from there."""
+ article_path = settings.ARTICLES_PATH.joinpath(*path[:-1])
+
+ # We need to include extension MD
+ article_path = article_path.joinpath(f"{path[-1]}.md")
+ if not article_path.exists() or not article_path.is_file():
+ raise Http404("Article not found.")
+
+ html = markdown(
+ article_path.read_text(),
+ extras=[
+ "metadata",
+ "fenced-code-blocks",
+ "header-ids",
+ "strike",
+ "target-blank-links",
+ "tables",
+ "task_list"
+ ]
+ )
+
+ return {"article": str(html), "metadata": html.metadata}
+
+
+def get_github_information(
+ path: List[str]
+) -> Dict[str, Union[List[str], str]]:
+ """Get article last modified date and contributors from GitHub."""
+ result = requests.get(
+ COMMITS_URL.format(
+ owner=settings.SITE_REPOSITORY_OWNER,
+ name=settings.SITE_REPOSITORY_NAME,
+ branch=settings.SITE_REPOSITORY_BRANCH,
+ path=(
+ f"{BASE_ARTICLES_LOCATION}{'/'.join(path[:-1])}"
+ f"{'/' if len(path) > 1 else ''}{path[-1]}.md"
+ )
+ )
+ )
+
+ if result.status_code == 200 and len(result.json()):
+ data = result.json()
+ return {
+ "last_modified": parser.isoparse(
+ data[0]["commit"]["committer"]["date"]
+ ).strftime("%dth %B %Y"),
+ "contributors": {
+ c["commit"]["committer"]["name"]: c["committer"]["html_url"] for c in data
+ }
+ }
+ else:
+ return {
+ "last_modified": "N/A",
+ "contributors": {}
+ }
diff --git a/pydis_site/apps/content/views/__init__.py b/pydis_site/apps/content/views/__init__.py
new file mode 100644
index 00000000..f92660d6
--- /dev/null
+++ b/pydis_site/apps/content/views/__init__.py
@@ -0,0 +1,4 @@
+from .article_category import ArticleOrCategoryView
+from .articles import ArticlesView
+
+__all__ = ["ArticleOrCategoryView", "ArticlesView"]
diff --git a/pydis_site/apps/content/views/article_category.py b/pydis_site/apps/content/views/article_category.py
new file mode 100644
index 00000000..0c22b5e8
--- /dev/null
+++ b/pydis_site/apps/content/views/article_category.py
@@ -0,0 +1,75 @@
+import typing as t
+
+from django.conf import settings
+from django.http import Http404
+from django.views.generic import TemplateView
+
+from pydis_site.apps.content import utils
+
+
+class ArticleOrCategoryView(TemplateView):
+ """Handles article and category pages."""
+
+ def get_template_names(self) -> t.List[str]:
+ """Checks does this use article template or listing template."""
+ location = self.kwargs["location"].split("/")
+ full_location = settings.ARTICLES_PATH.joinpath(*location)
+
+ if full_location.is_dir():
+ template_name = "content/listing.html"
+ elif full_location.with_suffix(".md").is_file():
+ template_name = "content/article.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)
+
+ location: list = self.kwargs["location"].split("/")
+ full_location = settings.ARTICLES_PATH.joinpath(*location)
+
+ if full_location.is_dir():
+ context["category_info"] = utils.get_category(location)
+ context["content"] = utils.get_articles(location)
+ context["categories"] = utils.get_categories(location)
+ # Add trailing slash here to simplify template
+ context["path"] = "/".join(location) + "/"
+ context["in_category"] = True
+ elif full_location.with_suffix(".md").is_file():
+ article_result = utils.get_article(location)
+
+ if len(location) > 1:
+ context["category_data"] = utils.get_category(location[:-1])
+ context["category_data"]["raw_name"] = location[:-1][-1]
+ else:
+ context["category_data"] = {"name": None, "raw_name": None}
+
+ context["article"] = article_result
+ context["relevant_links"] = {
+ link: value for link, value in zip(
+ article_result["metadata"].get("relevant_links", "").split(","),
+ article_result["metadata"].get("relevant_link_values", "").split(",")
+ ) if link != "" and value != ""
+ }
+ context["github_data"] = utils.get_github_information(location)
+ else:
+ raise Http404
+
+ location.pop()
+ breadcrumb_items = []
+ while len(location):
+ breadcrumb_items.insert(
+ 0,
+ {
+ "name": utils.get_category(location)["name"],
+ "path": "/".join(location)
+ }
+ )
+ location.pop()
+
+ context["breadcrumb_items"] = breadcrumb_items
+
+ return context
diff --git a/pydis_site/apps/content/views/articles.py b/pydis_site/apps/content/views/articles.py
new file mode 100644
index 00000000..999002d0
--- /dev/null
+++ b/pydis_site/apps/content/views/articles.py
@@ -0,0 +1,16 @@
+from django.views.generic import TemplateView
+
+from pydis_site.apps.content.utils import get_articles, get_categories
+
+
+class ArticlesView(TemplateView):
+ """Shows all content and categories."""
+
+ template_name = "content/listing.html"
+
+ def get_context_data(self, **kwargs) -> dict:
+ """Add articles and categories data to template context."""
+ context = super().get_context_data(**kwargs)
+ context["content"] = get_articles()
+ context["categories"] = get_categories()
+ return context
diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py
index d7db6ff1..bd7c0625 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('articles/', include('pydis_site.apps.content.urls')),
path('events/', include('pydis_site.apps.events.urls', namespace='events')),
]
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 67afdbcb..d509d980 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -85,6 +85,7 @@ INSTALLED_APPS = [
'pydis_site.apps.home',
'pydis_site.apps.staff',
'pydis_site.apps.resources',
+ 'pydis_site.apps.content',
'pydis_site.apps.events',
'django.contrib.admin',
@@ -280,3 +281,10 @@ BULMA_SETTINGS = {
"footer-padding": "1rem 1.5rem 1rem",
}
}
+
+# Information about site repository
+SITE_REPOSITORY_OWNER = "python-discord"
+SITE_REPOSITORY_NAME = "site"
+SITE_REPOSITORY_BRANCH = "master"
+
+ARTICLES_PATH = Path(BASE_DIR, "pydis_site", "apps", "content", "resources", "content")
diff --git a/pydis_site/static/css/content/articles.css b/pydis_site/static/css/content/articles.css
new file mode 100644
index 00000000..fa7a0ba5
--- /dev/null
+++ b/pydis_site/static/css/content/articles.css
@@ -0,0 +1,7 @@
+.breadcrumb-section {
+ padding: 1rem;
+}
+
+i.has-icon-padding {
+ padding: 0 10px 25px 0;
+}
diff --git a/pydis_site/templates/content/article.html b/pydis_site/templates/content/article.html
new file mode 100644
index 00000000..c7b85567
--- /dev/null
+++ b/pydis_site/templates/content/article.html
@@ -0,0 +1,75 @@
+{% extends 'base/base.html' %}
+{% load static %}
+
+{% block title %}{{ article.metadata.title }}{% endblock %}
+{% block head %}
+ <meta property="og:title" content="Python Discord - {{ article.metadata.title }}" />
+ <meta property="og:type" content="website" />
+ <meta property="og:description" content="{{ article.metadata.short_description }}" />
+ <link rel="stylesheet" href="{% static "css/content/articles.css" %}">
+ <link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/default.min.css">
+ <script src="//cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/highlight.min.js"></script>
+ <script>hljs.initHighlightingOnLoad();</script>
+{% endblock %}
+
+{% block content %}
+ {% include "base/navbar.html" %}
+
+ <section class="breadcrumb-section section">
+ <div class="container">
+ <nav class="breadcrumb is-pulled-left" aria-label="breadcrumbs">
+ <ul>
+ <li><a href="{% url "content:articles" %}">Articles</a></li>
+ {% for item in breadcrumb_items %}
+ <li><a href="{% url "content:article_category" location=item.path %}">{{ item.name }}</a></li>
+ {% endfor %}
+ <li class="is-active"><a href="#">{{ article.metadata.title }}</a></li>
+ </ul>
+ </nav>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="content">
+ <div class="container">
+ <h1 class="title">{{ article.metadata.title }}</h1>
+ <p class="subtitle is-size-6"><strong>Last modified:</strong> {{ github_data.last_modified }}</p>
+ <div class="columns is-variable is-8">
+ <div class="column is-two-thirds">
+ {{ article.article|safe }}
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-header">
+ <p class="card-header-title">Contributors</p>
+ </div>
+ <div class="card-content">
+ {% if github_data.contributors|length %}
+ <div class="tags">
+ {% for user, profile_url in github_data.contributors.items %}
+ <span class="tag"><a href="{{ profile_url }}">{{ user }}</a></span>
+ {% endfor %}
+ </div>
+ {% else %}
+ <p>N/A</p>
+ {% endif %}
+ </div>
+ </div>
+
+ {% if relevant_links|length > 0 %}
+ <div class="box">
+ <p class="menu-label">Relevant links</p>
+ <ul class="menu-list">
+ {% for link, value in relevant_links.items %}
+ <li><a class="has-text-link" href="{{link}}">{{ value }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+{% endblock %}
diff --git a/pydis_site/templates/content/listing.html b/pydis_site/templates/content/listing.html
new file mode 100644
index 00000000..8c06bccc
--- /dev/null
+++ b/pydis_site/templates/content/listing.html
@@ -0,0 +1,62 @@
+{% extends 'base/base.html' %}
+{% load static %}
+
+{% block title %}{{ category_info.name|default:"Articles" }}{% endblock %}
+{% block head %}
+ <meta property="og:title" content="Python Discord - {{ category_info.name|default:"Articles" }}" />
+ <meta property="og:type" content="website" />
+ <meta property="og:description" content="{{ category_info.description }}" />
+ <link rel="stylesheet" href="{% static "css/content/articles.css" %}">
+{% endblock %}
+
+{% block content %}
+ {% include "base/navbar.html" %}
+
+ <section class="breadcrumb-section section">
+ <div class="container">
+ <nav class="breadcrumb is-pulled-left" aria-label="breadcrumbs">
+ <ul>
+ {% if in_category %}
+ <li><a href="{% url "content:articles" %}">Articles</a></li>
+ {% endif %}
+ {% for item in breadcrumb_items %}
+ <li><a href="{% url "content:article_category" location=item.path %}">{{ item.name }}</a></li>
+ {% endfor %}
+ <li class="is-active"><a href="#">{{ category_info.name|default:"Articles" }}</a></li>
+ </ul>
+ </nav>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="content">
+ <h1>{{ category_info.name|default:"Articles" }}</h1>
+ {% for category, data in categories.items %}
+ <div class="box" style="max-width: 800px;">
+ <span class="icon is-size-4 is-medium">
+ <i class="fas fa-folder is-size-3 is-black has-icon-padding" aria-hidden="true"></i>
+ </span>
+
+
+ <a href="{% url "content:article_category" location=path|add:category %}">
+ <span class="is-size-4 has-text-weight-bold">{{ data.name }}</span>
+ </a>
+ <p class="is-italic">{{ data.description }}</p>
+ </div>
+ {% endfor %}
+ {% for article, data in content.items %}
+ <div class="box" style="max-width: 800px;">
+ <span class="icon is-size-4 is-medium">
+ <i class="{{ data.icon_class|default:"fab" }} {{ data.icon|default:"fa-python" }} is-size-3 is-black has-icon-padding" aria-hidden="true"></i>
+ </span>
+ <a href="{% url "content:article_category" location=path|add:article %}">
+ <span class="is-size-4 has-text-weight-bold">{{ data.title }}</span>
+ </a>
+ <p class="is-italic">{{ data.short_description }}</p>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ </section>
+{% endblock %}
diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html
index 6eb32c97..2dc88a8c 100644
--- a/pydis_site/templates/resources/resources.html
+++ b/pydis_site/templates/resources/resources.html
@@ -15,7 +15,7 @@
<h1>Resources</h1>
<div class="tile is-ancestor">
- <a class="tile is-parent" href="/articles/category/guides">
+ <a class="tile is-parent" href="{% url "content:article_category" location="guides" %}">
<article class="tile is-child box hero is-primary is-bold">
<p class="title is-size-1"><i class="fad fa-info-circle" aria-hidden="true"></i> Guides</p>
<p class="subtitle is-size-4">Made by us, for you</p>