aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Pipfile3
-rw-r--r--Pipfile.lock494
-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
-rw-r--r--pydis_site/settings.py8
-rw-r--r--pydis_site/static/css/content/page.css31
-rw-r--r--pydis_site/templates/content/base.html36
-rw-r--r--pydis_site/templates/content/listing.html27
-rw-r--r--pydis_site/templates/content/page.html31
-rw-r--r--pydis_site/templates/resources/resources.html2
24 files changed, 1055 insertions, 184 deletions
diff --git a/Pipfile b/Pipfile
index 3bc89d62..c51215b6 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"
+markdown = "~=3.3.4"
+python-frontmatter = "~=1.0"
[dev-packages]
coverage = "~=5.0"
@@ -34,6 +36,7 @@ mccabe = "~=0.6.1"
pep8-naming = "~=0.9"
pre-commit = "~=2.1"
unittest-xml-reporting = "~=3.0"
+pyfakefs = "~=4.4.0"
[requires]
python_version = "3.7"
diff --git a/Pipfile.lock b/Pipfile.lock
index 8c91e0db..6125e538 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "07f534656de1dd12baafc39bcdb68f143bbe195f8220211724427e15244dc187"
+ "sha256": "dc85f4f1b6ef58c2832d79542d6e88729cd2464bdf9324fef4d0757561f07ca7"
},
"pipfile-spec": 6,
"requires": {
@@ -18,33 +18,34 @@
"default": {
"asgiref": {
"hashes": [
- "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
- "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
+ "sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
+ "sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
],
"markers": "python_version >= '3.5'",
- "version": "==3.2.10"
+ "version": "==3.3.1"
},
"certifi": {
"hashes": [
- "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
- "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
+ "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
+ "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
- "version": "==2020.6.20"
+ "version": "==2020.12.5"
},
"chardet": {
"hashes": [
- "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
- "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+ "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
+ "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
- "version": "==3.0.4"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==4.0.0"
},
"django": {
"hashes": [
- "sha256:2d14be521c3ae24960e5e83d4575e156a8c479a75c935224b671b1c6e66eddaf",
- "sha256:313d0b8f96685e99327785cc600a5178ca855f8e6f4ed162e671e8c3cf749739"
+ "sha256:2afe4900667bcceac792fa34b4fb25448c4fd950d8b32c5508b3442c4b10442a",
+ "sha256:6f13c3e8109236129c49d65a42fbf30c928e66b05ca6862246061b9343ecbaf2"
],
"index": "pypi",
- "version": "==3.0.10"
+ "version": "==3.0.13"
},
"django-environ": {
"hashes": [
@@ -103,11 +104,11 @@
},
"gitpython": {
"hashes": [
- "sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8",
- "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"
+ "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b",
+ "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"
],
"index": "pypi",
- "version": "==3.1.9"
+ "version": "==3.1.14"
},
"idna": {
"hashes": [
@@ -117,6 +118,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10"
},
+ "importlib-metadata": {
+ "hashes": [
+ "sha256:742add720a20d0467df2f444ae41704000f50e1234f46174b51f9c6031a1bd71",
+ "sha256:b74159469b464a99cb8cc3e21973e4d96e05d3024d337313fedb618a6e86e6f4"
+ ],
+ "markers": "python_version < '3.8'",
+ "version": "==3.7.3"
+ },
"libsass": {
"hashes": [
"sha256:1521d2a8d4b397c6ec90640a1f6b5529077035efc48ef1c2e53095544e713d1b",
@@ -135,18 +144,29 @@
],
"version": "==0.20.1"
},
+ "markdown": {
+ "hashes": [
+ "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49",
+ "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"
+ ],
+ "index": "pypi",
+ "version": "==3.3.4"
+ },
"psycopg2-binary": {
"hashes": [
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
"sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
+ "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
"sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
"sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
+ "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
"sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
"sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
"sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
"sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
+ "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
"sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
"sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
"sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
@@ -173,16 +193,25 @@
"index": "pypi",
"version": "==2.8.6"
},
+ "python-frontmatter": {
+ "hashes": [
+ "sha256:766ae75f1b301ffc5fe3494339147e0fd80bc3deff3d7590a93991978b579b08",
+ "sha256:e98152e977225ddafea6f01f40b4b0f1de175766322004c826ca99842d19a7cd"
+ ],
+ "index": "pypi",
+ "version": "==1.0.0"
+ },
"pytz": {
"hashes": [
- "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
- "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
+ "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
+ "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
],
- "version": "==2020.1"
+ "version": "==2021.1"
},
"pyuwsgi": {
"hashes": [
"sha256:0bd14517398f494d828d77a9bf72b5a6cbef0112e1cc05e9a0080fa8828ccfa0",
+ "sha256:149675b2e020b0e833e8b871a545751ca346cbfed85c8fd2b320a01d40dc3d8f",
"sha256:285e263a9094389f13cfdefd033a4e99fbed3ad120dba9ac5093846cc03ac5ab",
"sha256:297d1d0b8c472374b12eda7f17a9f5de67cf516612e42b71a7636afb9d1e3974",
"sha256:5439f0f3ef5d6bf1622f341662d04c1d92b88889db40b295419e5fe75a7c7d45",
@@ -192,6 +221,7 @@
"sha256:90e4235020048456ad867aefc383cdf5528b7f6e327555ceec579c428a828759",
"sha256:94d4287b155aa789ce4b6f671c981f7d6c58fc3113330e2f29ac7926cb854645",
"sha256:a425f562f382a097ca49df26b70d47d12f0cf0abf233610f3f58b1f7f780355e",
+ "sha256:ac79dead0685beab5ecfe0926426849a44c5572528f89bb17f6ecf5eb561024e",
"sha256:bddeb8df77010d0f842068765a0b3155fdcfd847f14bc1ba89fc7e37914a13d2",
"sha256:dac4a04dc0f69d641dba984e83214d2c2cc098496c5d5585e7d3f4e7a9190f84"
],
@@ -201,36 +231,54 @@
},
"pyyaml": {
"hashes": [
- "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
- "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
- "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
- "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
- "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
- "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
- "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
- "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
- "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
- "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
- "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
+ "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
+ "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
+ "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
+ "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
+ "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
+ "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
+ "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
+ "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
+ "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
+ "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
+ "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
+ "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
+ "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
+ "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
+ "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
+ "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
+ "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
+ "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
+ "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
+ "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
+ "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
+ "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
+ "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
+ "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
+ "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
+ "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
+ "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
+ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
+ "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
],
"index": "pypi",
- "version": "==5.3.1"
+ "version": "==5.4.1"
},
"requests": {
"hashes": [
- "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
- "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
+ "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
+ "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"index": "pypi",
- "version": "==2.24.0"
+ "version": "==2.25.1"
},
"sentry-sdk": {
"hashes": [
- "sha256:1d91a0059d2d8bb980bec169578035c2f2d4b93cd8a4fb5b85c81904d33e221a",
- "sha256:6222cf623e404c3e62b8e0e81c6db866ac2d12a663b7c1f7963350e3f397522a"
+ "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237",
+ "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b"
],
"index": "pypi",
- "version": "==0.18.0"
+ "version": "==0.20.3"
},
"six": {
"hashes": [
@@ -242,27 +290,36 @@
},
"smmap": {
"hashes": [
- "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4",
- "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"
+ "sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714",
+ "sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==3.0.4"
+ "version": "==3.0.5"
},
"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"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
+ "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
+ "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
+ ],
+ "markers": "python_version < '3.8'",
+ "version": "==3.7.4.3"
},
"urllib3": {
"hashes": [
- "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
- "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
+ "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
+ "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
],
"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.26.4"
},
"whitenoise": {
"hashes": [
@@ -271,6 +328,14 @@
],
"index": "pypi",
"version": "==5.2.0"
+ },
+ "zipp": {
+ "hashes": [
+ "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76",
+ "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==3.4.1"
}
},
"develop": {
@@ -283,18 +348,18 @@
},
"attrs": {
"hashes": [
- "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
- "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
+ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
+ "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==20.2.0"
+ "version": "==20.3.0"
},
"bandit": {
"hashes": [
- "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952",
- "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"
+ "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07",
+ "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"
],
- "version": "==1.6.2"
+ "version": "==1.7.0"
},
"cfgv": {
"hashes": [
@@ -306,43 +371,61 @@
},
"coverage": {
"hashes": [
- "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
- "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
- "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
- "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
- "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
- "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
- "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
- "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
- "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
- "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
- "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
- "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
- "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
- "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
- "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
- "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
- "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
- "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
- "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
- "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
- "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
- "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
- "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
- "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
- "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
- "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
- "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
- "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
- "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
- "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
- "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
- "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
- "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
- "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
- ],
- "index": "pypi",
- "version": "==5.3"
+ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
+ "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
+ "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
+ "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
+ "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
+ "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
+ "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
+ "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
+ "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
+ "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
+ "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
+ "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
+ "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
+ "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
+ "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
+ "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
+ "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
+ "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
+ "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
+ "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
+ "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
+ "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
+ "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
+ "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
+ "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
+ "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
+ "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
+ "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
+ "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
+ "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
+ "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
+ "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
+ "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
+ "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
+ "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
+ "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
+ "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
+ "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
+ "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
+ "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
+ "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
+ "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
+ "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
+ "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
+ "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
+ "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
+ "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
+ "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
+ "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
+ "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
+ "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
+ "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
+ ],
+ "index": "pypi",
+ "version": "==5.5"
},
"distlib": {
"hashes": [
@@ -360,19 +443,19 @@
},
"flake8": {
"hashes": [
- "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
- "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
+ "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff",
+ "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"
],
"index": "pypi",
- "version": "==3.8.4"
+ "version": "==3.9.0"
},
"flake8-annotations": {
"hashes": [
- "sha256:0bcebb0792f1f96d617ded674dca7bf64181870bfe5dace353a1483551f8e5f1",
- "sha256:bebd11a850f6987a943ce8cdff4159767e0f5f89b3c88aca64680c2175ee02df"
+ "sha256:40a4d504cdf64126ea0bdca39edab1608bc6d515e96569b7e7c3c59c84f66c36",
+ "sha256:eabbfb2dd59ae0e9835f509f930e79cd99fa4ff1026fe6ca073503a57407037c"
],
"index": "pypi",
- "version": "==2.4.1"
+ "version": "==2.6.1"
},
"flake8-bandit": {
"hashes": [
@@ -383,19 +466,19 @@
},
"flake8-bugbear": {
"hashes": [
- "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63",
- "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162"
+ "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538",
+ "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703"
],
"index": "pypi",
- "version": "==20.1.4"
+ "version": "==20.11.1"
},
"flake8-docstrings": {
"hashes": [
- "sha256:3d5a31c7ec6b7367ea6506a87ec293b94a0a46c0bce2bb4975b7f1d09b6f3717",
- "sha256:a256ba91bc52307bef1de59e2a009c3cf61c3d0952dbe035d6ff7208940c2edc"
+ "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde",
+ "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"
],
"index": "pypi",
- "version": "==1.5.0"
+ "version": "==1.6.0"
},
"flake8-import-order": {
"hashes": [
@@ -422,11 +505,11 @@
},
"flake8-tidy-imports": {
"hashes": [
- "sha256:62059ca07d8a4926b561d392cbab7f09ee042350214a25cf12823384a45d27dd",
- "sha256:c30b40337a2e6802ba3bb611c26611154a27e94c53fc45639e3e282169574fd3"
+ "sha256:52e5f2f987d3d5597538d5941153409ebcab571635835b78f522c7bf03ca23bc",
+ "sha256:76e36fbbfdc8e3c5017f9a216c2855a298be85bc0631e66777f4e6a07a859dc4"
],
"index": "pypi",
- "version": "==4.1.0"
+ "version": "==4.2.1"
},
"flake8-todo": {
"hashes": [
@@ -445,27 +528,27 @@
},
"gitpython": {
"hashes": [
- "sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8",
- "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"
+ "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b",
+ "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"
],
"index": "pypi",
- "version": "==3.1.9"
+ "version": "==3.1.14"
},
"identify": {
"hashes": [
- "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4",
- "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d"
+ "sha256:39c0b110c9d0cd2391b6c38cd0ff679ee4b4e98f8db8b06c5d9d9e502711a1e1",
+ "sha256:efbf090a619255bc31c4fbba709e2805f7d30913fd4854ad84ace52bd276e2f6"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.5.5"
+ "markers": "python_full_version >= '3.6.1'",
+ "version": "==2.2.0"
},
"importlib-metadata": {
"hashes": [
- "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
- "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"
+ "sha256:742add720a20d0467df2f444ae41704000f50e1234f46174b51f9c6031a1bd71",
+ "sha256:b74159469b464a99cb8cc3e21973e4d96e05d3024d337313fedb618a6e86e6f4"
],
"markers": "python_version < '3.8'",
- "version": "==2.0.0"
+ "version": "==3.7.3"
},
"mccabe": {
"hashes": [
@@ -484,11 +567,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,52 +583,78 @@
},
"pre-commit": {
"hashes": [
- "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a",
- "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70"
+ "sha256:94c82f1bf5899d56edb1d926732f4e75a7df29a0c8c092559c77420c9d62428b",
+ "sha256:de55c5c72ce80d79106e48beb1b54104d16495ce7f95b0c7b13d4784193a00af"
],
"index": "pypi",
- "version": "==2.7.1"
+ "version": "==2.11.1"
},
"pycodestyle": {
"hashes": [
- "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
- "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
+ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
+ "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.6.0"
+ "version": "==2.7.0"
},
"pydocstyle": {
"hashes": [
- "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325",
- "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"
+ "sha256:164befb520d851dbcf0e029681b91f4f599c62c5cd8933fd54b1bfbd50e89e1f",
+ "sha256:d4449cf16d7e6709f63192146706933c7a334af7c0f083904799ccb851c50f6d"
],
- "markers": "python_version >= '3.5'",
- "version": "==5.1.1"
+ "markers": "python_version >= '3.6'",
+ "version": "==6.0.0"
+ },
+ "pyfakefs": {
+ "hashes": [
+ "sha256:082d863e0e2a74351f697da404e329a91e18e5055942e59d1b836e8459b2c94c",
+ "sha256:1ac3b2845dabe69af56c20691b9347914581195ccdde352535fb7d4ff0055c19"
+ ],
+ "index": "pypi",
+ "version": "==4.4.0"
},
"pyflakes": {
"hashes": [
- "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
- "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
+ "sha256:910208209dcea632721cb58363d0f72913d9e8cf64dc6f8ae2e02a3609aba40d",
+ "sha256:e59fd8e750e588358f1b8885e5a4751203a0516e0ee6d34811089ac294c8806f"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.2.0"
+ "version": "==2.3.0"
},
"pyyaml": {
"hashes": [
- "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
- "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
- "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
- "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
- "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
- "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
- "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
- "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
- "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
- "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
- "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
+ "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
+ "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
+ "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
+ "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
+ "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
+ "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
+ "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
+ "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
+ "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
+ "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
+ "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
+ "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
+ "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
+ "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
+ "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
+ "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
+ "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
+ "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
+ "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
+ "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
+ "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
+ "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
+ "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
+ "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
+ "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
+ "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
+ "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
+ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
+ "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
],
"index": "pypi",
- "version": "==5.3.1"
+ "version": "==5.4.1"
},
"six": {
"hashes": [
@@ -557,60 +666,79 @@
},
"smmap": {
"hashes": [
- "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4",
- "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"
+ "sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714",
+ "sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==3.0.4"
+ "version": "==3.0.5"
},
"snowballstemmer": {
"hashes": [
- "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
- "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
+ "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2",
+ "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"
],
- "version": "==2.0.0"
+ "version": "==2.1.0"
},
"stevedore": {
"hashes": [
- "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62",
- "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"
+ "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
+ "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
],
"markers": "python_version >= '3.6'",
- "version": "==3.2.2"
+ "version": "==3.3.0"
},
"toml": {
"hashes": [
- "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
- "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
- "version": "==0.10.1"
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==0.10.2"
},
"typed-ast": {
"hashes": [
- "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
- "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
- "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
- "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
- "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
- "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
- "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
- "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
- "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
- "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
- "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
- "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
- "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
- "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
- "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
- "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
- "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
- "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
- "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
- "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
- "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
+ "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1",
+ "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d",
+ "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6",
+ "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd",
+ "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37",
+ "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151",
+ "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07",
+ "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440",
+ "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70",
+ "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496",
+ "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea",
+ "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400",
+ "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc",
+ "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606",
+ "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc",
+ "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581",
+ "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412",
+ "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a",
+ "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2",
+ "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787",
+ "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f",
+ "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937",
+ "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64",
+ "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487",
+ "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b",
+ "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41",
+ "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a",
+ "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3",
+ "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166",
+ "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"
+ ],
+ "markers": "python_version < '3.8'",
+ "version": "==1.4.2"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
+ "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
+ "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
],
"markers": "python_version < '3.8'",
- "version": "==1.4.1"
+ "version": "==3.7.4.3"
},
"unittest-xml-reporting": {
"hashes": [
@@ -622,19 +750,19 @@
},
"virtualenv": {
"hashes": [
- "sha256:3d427459dfe5ec3241a6bad046b1d10c0e445940e013c81946458987c7c7e255",
- "sha256:9160a8f6196afcb8bb91405b5362651f302ee8e810fc471f5f9ce9a06b070298"
+ "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107",
+ "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==20.0.32"
+ "version": "==20.4.3"
},
"zipp": {
"hashes": [
- "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b",
- "sha256:eed8ec0b8d1416b2ca33516a37a08892442f3954dee131e92cfd92d8fe3e7066"
+ "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76",
+ "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"
],
"markers": "python_version >= '3.6'",
- "version": "==3.3.0"
+ "version": "==3.4.1"
}
}
}
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')),
]
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 67afdbcb..3abf556a 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"
+
+PAGES_PATH = Path(BASE_DIR, "pydis_site", "apps", "content", "resources")
diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css
new file mode 100644
index 00000000..57d7472b
--- /dev/null
+++ b/pydis_site/static/css/content/page.css
@@ -0,0 +1,31 @@
+.breadcrumb-section {
+ padding: 1rem;
+}
+
+i.has-icon-padding {
+ padding: 0 10px 25px 0;
+}
+
+/*
+ * Move padding padding from <pre> tag to hljs <code> tags so the padding
+ * space is colored the same as the background of hljs <code> blocks.
+ */
+.content pre {
+ padding: 0;
+}
+
+code.hljs {
+ padding: 1.75em 2em;
+}
+
+/*
+ * Show header permalink on hover.
+ */
+.headerlink {
+ display: none;
+ padding-left: 0.5em;
+}
+
+:is(h1, h2, h3, h4, h5, h6):hover > .headerlink {
+ display: inline;
+}
diff --git a/pydis_site/templates/content/base.html b/pydis_site/templates/content/base.html
new file mode 100644
index 00000000..19eec5d4
--- /dev/null
+++ b/pydis_site/templates/content/base.html
@@ -0,0 +1,36 @@
+{% extends 'base/base.html' %}
+{% load static %}
+
+{% block title %}{{ page_title }}{% endblock %}
+{% block head %}
+ <meta property="og:title" content="Python Discord - {{ page_title }}" />
+ <meta property="og:type" content="website" />
+ <meta property="og:description" content="{{ page_description }}" />
+ <link rel="stylesheet" href="{% static "css/content/page.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>
+ {% for item in breadcrumb_items %}
+ <li><a href="{% url "content:page_category" location=item.path %}">{{ item.name }}</a></li>
+ {% endfor %}
+ <li class="is-active"><a href="#">{{ page_title }}</a></li>
+ </ul>
+ </nav>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <div class="content">
+ <h1 class="title">{{ page_title }}</h1>
+ {% block page_content %}{% endblock %}
+ </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..6de306b0
--- /dev/null
+++ b/pydis_site/templates/content/listing.html
@@ -0,0 +1,27 @@
+{% extends 'content/base.html' %}
+
+{% block page_content %}
+ {% 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:page_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 page, data in pages.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:page_category" location=path|add:page %}">
+ <span class="is-size-4 has-text-weight-bold">{{ data.title }}</span>
+ </a>
+ <p class="is-italic">{{ data.description }}</p>
+ </div>
+ {% endfor %}
+{% endblock %}
diff --git a/pydis_site/templates/content/page.html b/pydis_site/templates/content/page.html
new file mode 100644
index 00000000..06d74208
--- /dev/null
+++ b/pydis_site/templates/content/page.html
@@ -0,0 +1,31 @@
+{% extends 'content/base.html' %}
+
+{% block head %}
+ {{ block.super }}
+ <link rel="stylesheet"
+ href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/atom-one-dark-reasonable.min.css">
+ <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/highlight.min.js"></script>
+ <script>hljs.initHighlightingOnLoad();</script>
+{% endblock %}
+
+{% block page_content %}
+ {% if relevant_links|length > 0 %}
+ <div class="columns is-variable is-8">
+ <div class="column is-two-thirds">
+ {{ page|safe }}
+ </div>
+ <div class="column">
+ <div class="box">
+ <p class="menu-label">Relevant links</p>
+ <ul class="menu-list">
+ {% for value, link in relevant_links.items %}
+ <li><a class="has-text-link" href="{{link}}">{{ value }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+ </div>
+ {% else %}
+ <div>{{ page|safe }}</div>
+ {% endif %}
+{% endblock %}
diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html
index 6eb32c97..491bc55e 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:page_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>