diff options
author | 2021-05-14 17:46:00 +0100 | |
---|---|---|
committer | 2021-05-14 17:46:00 +0100 | |
commit | 2836913d98e762d819d6d50ec668112077e8a3a9 (patch) | |
tree | ccb02577b40ad4c92b56c8c6d3ae1c53e90afe26 | |
parent | Merge pull request #486 from python-discord/bast0006-patch-error-page-margins (diff) | |
parent | Remove stale dependency. (diff) |
Merge pull request #487 from python-discord/dewikification
381 files changed, 7823 insertions, 6045 deletions
diff --git a/.coveragerc b/.coveragerc index 4906c86a..b4a9bbe4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,7 +6,6 @@ source = pydis_site/apps/api pydis_site/apps/home pydis_site/apps/staff - pydis_site/apps/wiki omit = */migrations/* */admin.py @@ -116,7 +116,6 @@ rethinkdb_data/ # Node modules node_modules/ -media/ pip-wheel-metadata/ staticfiles/ diff --git a/.mdlrc b/.mdlrc deleted file mode 100644 index 0c02cde4..00000000 --- a/.mdlrc +++ /dev/null @@ -1 +0,0 @@ -rules '~MD024' diff --git a/LICENSE-SimpleMDE b/LICENSE-SimpleMDE deleted file mode 100644 index d9f79afc..00000000 --- a/LICENSE-SimpleMDE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Next Step Webs, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. @@ -10,15 +10,14 @@ django-filter = "~=2.1.0" django-hosts = "~=4.0" djangorestframework = "~=3.11.0" psycopg2-binary = "~=2.8" -django-simple-bulma = "~=1.2" +django-simple-bulma = "~=2.1" whitenoise = "~=5.0" requests = "~=2.21" -pygments = "~=2.7.4" -wiki = "~=0.6.0" pyyaml = "~=5.1" gunicorn = "~=20.0.4" -django-allauth = "~=0.41" sentry-sdk = "~=0.19" +markdown = "~=3.3.4" +python-frontmatter = "~=1.0" [dev-packages] coverage = "~=5.0" @@ -34,6 +33,7 @@ flake8-todo = "~=0.7" mccabe = "~=0.6.1" pep8-naming = "~=0.9" pre-commit = "~=2.1" +pyfakefs = "~=4.4.0" coveralls = "~=2.1" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 166ab19c..7cd05feb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7a765746a43af6ba7a25609e9035ae27dcbf98cc80ada7f6213cd16c452247a9" + "sha256": "261e66197bd52c5de02101479ffae9f1fb2e86609de992644e7dc78aa38689bf" }, "pipfile-spec": 6, "requires": { @@ -21,15 +21,9 @@ "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" ], + "markers": "python_version >= '3.6'", "version": "==3.3.4" }, - "bleach": { - "hashes": [ - "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", - "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" - ], - "version": "==3.1.5" - }, "certifi": { "hashes": [ "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", @@ -37,79 +31,14 @@ ], "version": "==2020.12.5" }, - "cffi": { - "hashes": [ - "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", - "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", - "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", - "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", - "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", - "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", - "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", - "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", - "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", - "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", - "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", - "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", - "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", - "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", - "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", - "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", - "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", - "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", - "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", - "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", - "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", - "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", - "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", - "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", - "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", - "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", - "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", - "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", - "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", - "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", - "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", - "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", - "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", - "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", - "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", - "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", - "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" - ], - "version": "==1.14.5" - }, "chardet": { "hashes": [ "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.0.0" }, - "cryptography": { - "hashes": [ - "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", - "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", - "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", - "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", - "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", - "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", - "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", - "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", - "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", - "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", - "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", - "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" - ], - "version": "==3.4.7" - }, - "defusedxml": { - "hashes": [ - "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", - "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" - ], - "version": "==0.7.1" - }, "django": { "hashes": [ "sha256:9bc7aa619ed878fedba62ce139abe663a147dccfd20e907725ec11e02a1ca225", @@ -118,20 +47,6 @@ "index": "pypi", "version": "==3.0.14" }, - "django-allauth": { - "hashes": [ - "sha256:e51af457466022f52154d74c8523ac69375120fad2acce6e239635d85e610b25" - ], - "index": "pypi", - "version": "==0.44.0" - }, - "django-classy-tags": { - "hashes": [ - "sha256:25eb4f95afee396148683bfb4811b83b3f5729218d73ad0a3399271a6f9fcc49", - "sha256:d59d98bdf96a764dcf7a2929a86439d023b283a9152492811c7e44fc47555bc9" - ], - "version": "==2.0.0" - }, "django-environ": { "hashes": [ "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde", @@ -156,41 +71,13 @@ "index": "pypi", "version": "==4.0" }, - "django-js-asset": { - "hashes": [ - "sha256:8ec12017f26eec524cab436c64ae73033368a372970af4cf42d9354fcb166bdd", - "sha256:c163ae80d2e0b22d8fb598047cd0dcef31f81830e127cfecae278ad574167260" - ], - "version": "==1.2.2" - }, - "django-mptt": { - "hashes": [ - "sha256:90eb236eb4f1a92124bd7c37852bbe09c0d21158477cc237556d59842a91c509", - "sha256:dfdb3af75ad27cdd4458b0544ec8574174f2b90f99bc2cafab6a15b4bc1895a8" - ], - "version": "==0.11.0" - }, - "django-nyt": { - "hashes": [ - "sha256:235c6132325b9d1fc15e64303a15552857f13f9bf94f0ae6b6fed75581a696f0", - "sha256:a0d9b14c06507af17774a47a461c44c9da683fa1fc23425f9d14f2fc842e07fb" - ], - "version": "==1.1.6" - }, - "django-sekizai": { - "hashes": [ - "sha256:5c5e16845d37ce822fc655ce79ec02715191b3d03330b550997bcb842cf24fdf", - "sha256:e829f09b0d6bf01ee5cde05de1fb3faf2fbc5df66dc4dc280fbaac224ca4336f" - ], - "version": "==2.0.0" - }, "django-simple-bulma": { "hashes": [ - "sha256:79928fa983151947c635acf65fa5177ca775db98c8d53ddf1c785fe48c727466", - "sha256:e5cff3fc5f0d45558362ab8d0e11f92887c4fc85616f77daa6174940f94b12c7" + "sha256:38530d787b2b6a091b480f7cbb8841a3b3709733ebfa5e543ae339c3d8fece03", + "sha256:dfc34839e050d5e4749498806ed7ee8c77794021efa54ab224a2de925c644fea" ], "index": "pypi", - "version": "==1.3.2" + "version": "==2.2.0" }, "djangorestframework": { "hashes": [ @@ -213,6 +100,7 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "libsass": { @@ -235,62 +123,11 @@ }, "markdown": { "hashes": [ - "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17", - "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59" - ], - "version": "==3.2.2" - }, - "oauthlib": { - "hashes": [ - "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", - "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" - ], - "version": "==3.1.0" - }, - "packaging": { - "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" - ], - "version": "==20.9" - }, - "pillow": { - "hashes": [ - "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5", - "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4", - "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9", - "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a", - "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9", - "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727", - "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120", - "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c", - "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2", - "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797", - "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b", - "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f", - "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef", - "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232", - "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb", - "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9", - "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812", - "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178", - "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b", - "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5", - "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b", - "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1", - "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713", - "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4", - "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484", - "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c", - "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9", - "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388", - "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d", - "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602", - "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9", - "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e", - "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2" - ], - "version": "==8.2.0" + "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49", + "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c" + ], + "index": "pypi", + "version": "==3.3.4" }, "psycopg2-binary": { "hashes": [ @@ -333,44 +170,13 @@ "index": "pypi", "version": "==2.8.6" }, - "pycparser": { + "python-frontmatter": { "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "version": "==2.20" - }, - "pygments": { - "hashes": [ - "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", - "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" + "sha256:766ae75f1b301ffc5fe3494339147e0fd80bc3deff3d7590a93991978b579b08", + "sha256:e98152e977225ddafea6f01f40b4b0f1de175766322004c826ca99842d19a7cd" ], "index": "pypi", - "version": "==2.7.4" - }, - "pyjwt": { - "extras": [ - "crypto" - ], - "hashes": [ - "sha256:a5c70a06e1f33d81ef25eecd50d50bd30e34de1ca8b2b9fa3fe0daaabcf69bf7", - "sha256:b70b15f89dc69b993d8a8d32c299032d5355c82f9b5b7e851d1a6d706dffe847" - ], - "version": "==2.0.1" - }, - "pyparsing": { - "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" - ], - "version": "==2.4.7" - }, - "python3-openid": { - "hashes": [ - "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf", - "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b" - ], - "version": "==3.2.0" + "version": "==1.0.0" }, "pytz": { "hashes": [ @@ -422,13 +228,6 @@ "index": "pypi", "version": "==2.25.1" }, - "requests-oauthlib": { - "hashes": [ - "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" - ], - "version": "==1.3.0" - }, "sentry-sdk": { "hashes": [ "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237", @@ -439,23 +238,18 @@ }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "version": "==1.15.0" - }, - "sorl-thumbnail": { - "hashes": [ - "sha256:c56cd651feab3bdc415d5301600198e2e70c08234dad48b8f6cfa4746cc102c7", - "sha256:fbe6dfd66a1aceb7e0203895ff5622775e50266f8d8cfd841fe1500bd3e19018" - ], - "version": "==12.7.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" }, "sqlparse": { "hashes": [ "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" ], + "markers": "python_version >= '3.5'", "version": "==0.4.1" }, "urllib3": { @@ -463,15 +257,9 @@ "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.26.4" }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, "whitenoise": { "hashes": [ "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7", @@ -479,14 +267,6 @@ ], "index": "pypi", "version": "==5.2.0" - }, - "wiki": { - "hashes": [ - "sha256:aad8b8ef6f669a6f11453ea2f35722065cf6281cc558e789c49cb2800ba0b6e5", - "sha256:eac841fba33d317b0ce038023f14db427735c279642a22ffdce98e2575e20086" - ], - "index": "pypi", - "version": "==0.6" } }, "develop": { @@ -499,10 +279,11 @@ }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "version": "==20.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" }, "bandit": { "hashes": [ @@ -523,6 +304,7 @@ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], + "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -530,6 +312,7 @@ "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.0.0" }, "coverage": { @@ -620,11 +403,11 @@ }, "flake8": { "hashes": [ - "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378", - "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a" + "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", + "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" ], "index": "pypi", - "version": "==3.9.1" + "version": "==3.9.2" }, "flake8-annotations": { "hashes": [ @@ -682,11 +465,11 @@ }, "flake8-tidy-imports": { "hashes": [ - "sha256:52e5f2f987d3d5597538d5941153409ebcab571635835b78f522c7bf03ca23bc", - "sha256:76e36fbbfdc8e3c5017f9a216c2855a298be85bc0631e66777f4e6a07a859dc4" + "sha256:d6e64cb565ca9474d13d5cb3f838b8deafb5fed15906998d4a674daf55bd6d89", + "sha256:e66d46f58ed108f36da920e7781a728dc2d8e4f9269e7e764274105700c0a90c" ], "index": "pypi", - "version": "==4.2.1" + "version": "==4.3.0" }, "flake8-todo": { "hashes": [ @@ -700,20 +483,23 @@ "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" ], + "markers": "python_version >= '3.4'", "version": "==4.0.7" }, "gitpython": { "hashes": [ - "sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e", - "sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867" + "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135", + "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e" ], - "version": "==3.1.15" + "markers": "python_version >= '3.5'", + "version": "==3.1.17" }, "identify": { "hashes": [ "sha256:9bcc312d4e2fa96c7abebcdfb1119563b511b5e3985ac52f60d9116277865b2e", "sha256:ad9f3fa0c2316618dc4d840f627d474ab6de106392a4f00221820200f490f5a8" ], + "markers": "python_full_version >= '3.6.1'", "version": "==2.2.4" }, "idna": { @@ -721,6 +507,7 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "mccabe": { @@ -740,10 +527,11 @@ }, "pbr": { "hashes": [ - "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", - "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" + "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", + "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" ], - "version": "==5.5.1" + "markers": "python_version >= '2.6'", + "version": "==5.6.0" }, "pep8-naming": { "hashes": [ @@ -766,6 +554,7 @@ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.7.0" }, "pydocstyle": { @@ -773,13 +562,23 @@ "sha256:164befb520d851dbcf0e029681b91f4f599c62c5cd8933fd54b1bfbd50e89e1f", "sha256:d4449cf16d7e6709f63192146706933c7a334af7c0f083904799ccb851c50f6d" ], + "markers": "python_version >= '3.6'", "version": "==6.0.0" }, + "pyfakefs": { + "hashes": [ + "sha256:082d863e0e2a74351f697da404e329a91e18e5055942e59d1b836e8459b2c94c", + "sha256:1ac3b2845dabe69af56c20691b9347914581195ccdde352535fb7d4ff0055c19" + ], + "index": "pypi", + "version": "==4.4.0" + }, "pyflakes": { "hashes": [ "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.3.1" }, "pyyaml": { @@ -827,16 +626,18 @@ }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "version": "==1.15.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" }, "smmap": { "hashes": [ "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" ], + "markers": "python_version >= '3.5'", "version": "==4.0.0" }, "snowballstemmer": { @@ -851,6 +652,7 @@ "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" ], + "markers": "python_version >= '3.6'", "version": "==3.3.0" }, "toml": { @@ -858,29 +660,24 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, - "typing-extensions": { - "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" - ], - "version": "==3.7.4.3" - }, "urllib3": { "hashes": [ "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.26.4" }, "virtualenv": { "hashes": [ - "sha256:09c61377ef072f43568207dc8e46ddeac6bcdcaf288d49011bda0e7f4d38c4a2", - "sha256:a935126db63128861987a7d5d30e23e8ec045a73840eeccb467c148514e29535" + "sha256:307a555cf21e1550885c82120eccaf5acedf42978fd362d32ba8410f9593f543", + "sha256:72cf267afc04bf9c86ec932329b7e94db6a0331ae9847576daaa7ca3c86b29a4" ], - "version": "==20.4.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.4.6" } } } diff --git a/docs/README.md b/docs/README.md index 2e9f15a1..5af383b4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,3 +11,7 @@ This directory contains useful documentation for working with and using our site * [Development with Docker](setup.md#development-with-docker) * [Development with `pip`](setup.md#development-with-pip) + +> Note: If you're looking to add redirects to the site, the `redirects` app is **NOT** the way to go. +> Convenience redirects should use our [Cloudflare Worker](https://github.com/python-discord/workers/tree/main/short-urls). +> Ask in the server if you have any questions! diff --git a/docs/setup.md b/docs/setup.md index 18f5ee97..d992067e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -79,8 +79,6 @@ Make sure you add the following to your hosts file: 127.0.0.1 api.pythondiscord.local 127.0.0.1 staff.pythondiscord.local 127.0.0.1 admin.pythondiscord.local -127.0.0.1 wiki.pythondiscord.local -127.0.0.1 ws.pythondiscord.local ``` When trying to access the site, you'll be using the domains above instead of the usual `localhost:8000`. diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py index df67cf71..e69de29b 100644 --- a/pydis_site/__init__.py +++ b/pydis_site/__init__.py @@ -1,9 +0,0 @@ -from wiki.plugins.macros.mdx import toc - -# Remove the toc header prefix. There's no option for this, so we gotta monkey patch it. -toc.HEADER_ID_PREFIX = '' - -# Empty list of validators for Allauth to ponder over. This is referred to in settings.py -# by a string because Allauth won't let us just give it a list _there_, we have to point -# at a list _somewhere else_ instead. -VALIDATORS = [] diff --git a/pydis_site/apps/home/forms/__init__.py b/pydis_site/apps/content/__init__.py index e69de29b..e69de29b 100644 --- a/pydis_site/apps/home/forms/__init__.py +++ 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/tests/__init__.py b/pydis_site/apps/content/migrations/__init__.py index e69de29b..e69de29b 100644 --- a/pydis_site/tests/__init__.py +++ 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..6553dcc6 --- /dev/null +++ b/pydis_site/apps/content/resources/_info.yml @@ -0,0 +1,2 @@ +title: Pages +description: Guides, articles, and pages hosted on the site. diff --git a/pydis_site/apps/content/resources/code-of-conduct.md b/pydis_site/apps/content/resources/code-of-conduct.md new file mode 100644 index 00000000..6302438e --- /dev/null +++ b/pydis_site/apps/content/resources/code-of-conduct.md @@ -0,0 +1,100 @@ +--- +title: Python Discord Code of Conduct +description: The Code of Conduct for our community. +icon: fab fa-discord +--- + +# Code of Conduct + +We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic. + +##### Examples of behavior that contributes to creating a positive environment include: + +* Being kind and courteous to others +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Collaborating with other community members +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +##### Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and sexual attention or advances +* The use of inappropriate images, including in a community member's avatar +* The use of inappropriate language, including in a community member's nickname +* Any spamming, flaming, baiting or other attention-stealing behavior +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Discussing topics that are overly polarizing, sensitive, or incite arguments. This includes the discussion of polarizing political views, violence, suicide, and rape. +* Responding with “RTFM”, "just google it” or similar phrases in response to help requests +* Other conduct which could reasonably be considered inappropriate + +### Our Goal + +The goal of this document is to set the overall tone for our community. +This isn’t an exhaustive list of things you can and can't do. +Rather, take this document in the spirit in which it’s intended, and try to be your best self. + +We value many things beyond technical expertise, including collaboration and supporting others within our community. +Providing a positive experience for other community members can have a much more significant impact than simply providing the correct answer. + +### Scope + +This Code of Conduct applies to all spaces managed by Python Discord. +This includes, but is not limited to, the Discord server, our repositories on GitHub, the YouTube-channel, and meet-ups. +In addition, violations of this code outside these spaces may affect a person's ability to participate within them. + +The Python Code of Conduct applies equally to all members of the community, including staff. + +--- + +# Code of Conduct Policies + +### Moderation Policy + +These are the policies for upholding our community’s rules and the code of conduct. +If you want to report a situation that needs to be reviewed by our moderation team, please see our [reporting guide](#reporting-guide). + +1. The [Python Discord Code of Conduct](#code-of-conduct) and the [Community Rules](/pages/rules) are enforced by the moderation team, which consists of users with the Moderators, Admins or Owners role on the Python Discord server. +2. Behavior that moderators find inappropriate, whether listed in the code of conduct, the community rules, or not, is also not allowed. +3. Complaints about moderation in-channel are not allowed. If a moderator takes an action or makes a decision you do not agree with, please send a Direct Message (DM) to ModMail from our Discord server. +4. If you disagree with a moderation action or decision taken against you, you may appeal the action or decision by following the [Appeal Procedure](#appeal-procedure). + +### Reporting Guide + +Instances of behaviors that violate the Python Discord Code of Conduct or rules may be reported by any member of the community. +Community members are encouraged to report these situations, including situations they witness involving other community members. + +You may report in the following ways: + +* By tagging the `@Moderators` role on the Discord server in situations that require immediate moderator attention. +* By sending a direct message (DM) to ModMail from our Discord server. +* By sending an email to [[email protected]](mailto:[email protected]) + +### Appeal Procedure + +If you wish to appeal a decision or action taken by the moderation team, you can do so in one of the following ways: + +* By sending an email to [[email protected]](mailto:[email protected]) +* By sending a direct message (DM) to ModMail from our Discord server. + +Please provide all relevant information in your appeal, including: + +* Your Discord username and id. +* The decision or action you are appealing. +* The reason for your appeal. + +Appeals will be discussed internally with the moderation team, but will be kept confidential otherwise. + +--- + +#### Attribution + +This Code of Conduct and parts of the policies are adapted from the [Adafruit Community Code of Conduct](https://github.com/adafruit/Adafruit_Community_Code_of_Conduct/blob/master/code-of-conduct.md), [Django Code of Conduct](https://www.djangoproject.com/conduct/), and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). + +#### License + + +All content on this page is licensed under a [Creative Commons Attribution](https://creativecommons.org/licenses/by/3.0/) license. diff --git a/pydis_site/apps/content/resources/frequently-asked-questions.md b/pydis_site/apps/content/resources/frequently-asked-questions.md new file mode 100644 index 00000000..ed797532 --- /dev/null +++ b/pydis_site/apps/content/resources/frequently-asked-questions.md @@ -0,0 +1,131 @@ +--- +title: Frequently Asked Questions +description: The Python Discord FAQ. +icon: fab fa-discord +toc: 4 +--- + +Welcome to Python Discord! Sometimes in our channels we get similar questions asked every so often. +We've compiled the most frequently asked questions and provided our response to them. +This FAQ is aimed at answering questions about the Python Discord community. If you have question about Python, feel free to ask in `#python-general`. + +## Staff & Roles + +#### **Q: How do I get the helper role / become moderator / join staff?** + +There are no applications to be come a Helper, Moderator, Admin, or other staff role. +To become a Helper, which is our base staff role, people are nominated by a staff member and are later put up to a vote by Moderators, Admins, and Owners. +If the candidate received enough votes unanimously, then we offer them the Helper role. +This whole process takes place in channels only viewable to staff members. + +Being a Helper is not only about helping people with Python questions or helping with our projects, but is also about demonstrating an understanding of our community's culture. +To read more about what we look for in a Helper and to read about our internal staff roles (Moderators, Leads, Core Developer, Admin), check out [this page](/pages/server-info/roles/). + + +#### **Q: What is this role on the server?** + +We document the purposes of our most important roles on our website. Check it out [here](/pages/server-info/roles/). + +The roles that are not documented on that page are for seasonal events. These are specific to those events and don't impact permissions on the server. + + +#### **Q: What perks are there for nitro boosters? Can I keep the nitro role once the boost expires?** + +People who boost our server automatically get a bright cyan role color, but this is purely cosmetic and there aren't any other incentives to give us Nitro boosts. +Discord itself manages that specific role, so the role is automatically removed when the boost expires. + +Patrons who donate via [our Patreon](https://www.patreon.com/python_discord) also get a blue role and this is also a purely cosmetic role. +We do appreciate our Patreon supporters and our Nitro Boosters! + +We have received suggestions to give Nitro boosters non-cosmetic perks like a Nitro boosters lounge or the ability to use emoji reactions in certain channels, though that isn't something we will consider as we don't want to gate any part of the server behind a paywall. + + +#### **Q: I'd like to report inappropriate behavior on the server. How do I do that?** + +To report inappropriate or rule-breaking behaviour on the server, please send a direct message to our `@ModMail`. +You should find the ModMail bot at the top of the server member list. +If it's an urgent situation that needs immediate moderator attention, such as spam or NSFW content, then you can ping the `@Moderators` role in the server. + +## Questions about our bots + +#### **Q: Can I get Python bot on my server?** + +There isn't a way to invite `@Python` to other servers. +`@Python` is closely tied to our server architecture and wouldn't be able to properly function without a specific set-up. +If you are interested in `@Python` though, you can host your own instance of it. +The entire project is open source and can be found on [our github](https://github.com/python-discord/bot). + +#### **Q: What is the Zoot bot?** + +Zoot is an instance of [Metricity](https://github.com/python-discord/metricity). +It collects advanced metrics about the usage of the server. +Message content is not stored or collected. +You can view what data we collect in our [data privacy policy](/pages/privacy/). + + +#### **Q: Do any of the bots do X? Can I contribute to the bot?** + +We have two bots that provide functionality in our server: `@Python` and `@Sir Lancebot`. + +* `@Python` is the bot that helps manage certain server functionality (i.e. our help channel and moderation systems). +* `@Sir Lancebot` is our community bot that is designed as an entry level project for people to learn about open source contribution. +* `@Sir Lancebot` contains our fun and silly commands, like `.battleship`, `.BunnyNameGenerator`, `.http_status` which provides dog and cat HTTP status codes, and more! + +You can check out [`@Python` here on github](https://github.com/python-discord/bot), and check out [`@Sir Lancebot` here](https://github.com/python-discord/sir-lancebot). +If you have any questions about how to contribute, drop by the `#dev-contrib` channel in server. + +## Server Specific Questions + +#### **Q: Why are the help channels named after elements/food?** + +We want to keep the help channels uniquely named with it being somewhat easy to remember, so we decided on elements/food. `#help-1` doesn't work as well as `help-carbon` (`help-strawberry`). +If we had them numbered, they would quickly move out of order and possibly cause confusion for newer members. + + +#### **Q: Why can't I upload a specific file type?** + +The only file types that we allow on this server are those that Discord supports a native preview for. +This is because it's easier and safer for people on the server since they do not need to download a file to view it. +It's also to ease the burden on our moderators, otherwise they would have to download and check the files posted to the server. + +If you want to share code please use our hosted hastebin, [paste.pythondiscord.com](http://paste.pythondiscord.com). + + +#### **Q: Why is this permission not allowed in that channel?** + +Our general policy is to allow permissions unless proven that they are negatively affecting the channel. +If a certain channel doesn't have a permission, chances are it was allowed there at some point, but the cost of moderating or managing it outweighed the benefit. +Feel free to ask in `#community-meta` if you'd like the reasoning behind any specific decision. + + +#### **Q: Can we have a channel to show off projects or a channel to find people to work on projects with?** + +We previously had these channels, though they unfortunately did not work out the way we had hoped. +Engagement was low and they were a large burden on our moderators due to the number of low quality or rule violating posts. + +In general, a real-time chat client isn't the best avenue for showing off your projects or finding collaborators because messages are typically only seen by those actively engaged at the time they are posted. +You're welcome to showcase your projects in our off-topic channels or on a different platform like Reddit. + + +#### **Q: Can I make a recommendation about a specific feature in the server?** + +If you want to make a recommendation or suggestion about the server feel free to post in `#community-meta`. +You can also open an issue on our meta repo on GitHub, which can be found [here](https://github.com/python-discord/meta). + + +#### **Q: Why did the icon change?** + +While we love our blurple Python logo, we also enjoy celebrating other events throughout the year, like Advent of Code, Pride Month, Black History Month, Valentine's Day, Diwali, and more! In the spirit of those celebrations, we like to have some fun and change our icon instead. +If you're wondering why it's changed this time, check out `#changelog` on the server, as the reasoning for the recent change will be there. + +If you'd like to contribute and create a Python Discord server icon for us to use, check out [our branding repo](https://github.com/python-discord/branding) for what we currently have and talk to us in the `#media-branding` channel in the server. + +## Misc + +#### **Q: Can I interact with the data collected on the server?** + +Unfortunately we don't allow direct interaction with the metrics we collect on the server. +The data we do collect is used for moderation purposes, please see [our Privacy Policy](/pages/privacy/) on what data is collected and how we use it. +Legend has it that if you say "SQL" or "graphs" enough times, a `@joe` might appear and provide some graphs and run queries you might have in mind. + +We do have some public stats available to view here: [https://stats.pythondiscord.com/](https://stats.pythondiscord.com/) 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..2f65eaf9 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/_info.yml @@ -0,0 +1,2 @@ +title: 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..c126a68a --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml @@ -0,0 +1,2 @@ +title: Python Discord Guides +description: Guides related to the Python Discord server and community. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md b/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md new file mode 100644 index 00000000..971989a9 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md @@ -0,0 +1,180 @@ +--- +title: Asking Good Questions +description: A guide for how to ask good questions in our community. +icon: fab fa-discord +toc: 3 +--- + +This document is intended to provide you with the information you need to get help as quickly and effectively as possible. +If you're stuck on a problem or you just don't understand something, you should always feel welcome to ask. + +# Before You Ask + +Before you ask your question, there are a few things you can do to find an answer on your own. +Experienced developers will do the following: + +* Read the official documentation for whatever you're working with +* Use a debugger to inspect your code +* Examine the traceback when your code raises an exception +* Do some research online - for example, on Stack Overflow +* Read the source code for whatever you're working with + +Essentially, doing your research is the first step towards a solution to any problem. +If your problem isn't extremely general, we're going to be doing exactly these steps ourselves when helping you, so doing the legwork beforehand saves everyone a lot of time. + +If none of the above steps help you or you're not sure how to do some of the above steps, feel free to ask us for help. + +# A Good Question + +When you're ready to ask a question, there's a few things you should have to hand before forming a query. + +* A code example that illustrates your problem +* If possible, make this a minimal example rather than an entire application +* Details on how you attempted to solve the problem on your own +* Full version information - for example, "Python 3.6.4 with `discord.py 1.0.0a`" +* The full traceback if your code raises an exception +* Do not curate the traceback as you may inadvertently exclude information crucial to solving your issue + +Your question should be informative, but to the point. +More importantly, how you phrase your question and how you address those that may help you is crucial. +Courtesy never hurts, and please type using correctly-spelled and grammatical language as far as you possibly can. + +When you're inspecting a problem, don't be quick to assume that you've found a bug, or that your approach is correct. +While it helps to detail what exactly you're trying to do, you should also be able to give us the bigger picture - describe the goal, not just the step. +Describe the problem's symptoms in chronological order - not your guesses as to their cause. + +| Bad Questions | Good Questions | +| ------------- | -------------- | +| Where can I find information on discord.py? | I used Google to try to find more information about "discord.py 1.0.0a", but I couldn't really find anything useful. Does anyone know where I might find a guide to writing commands using this library? | +| Pillow puts my text at the bottom of the image instead of where I wanted it. Why is it broken? | Pillow appears to insert text at the bottom of the image if the given X coordinate is negative. I had a look at the documentation and searched Stack Overflow, but I couldn't find any information on using negative coordinates to position text. Has anyone attempted this? | +| I'm having some trouble writing a YouTube random URL generator - can anyone help? | My YouTube random URL generator appears to be returning false positives for tested URLs, stating that a URL points to a real video when that video doesn't actually exist. Obviously there's some issue with how this is checked, but I can't put my finger on it. Is there anything I can check? | +| I was given this assignment by my teacher, but I'm not sure how to approach it. Does anyone have any ideas? | I have a list of numbers - how do I calculate how many of them are even? Is there a way to remove all the odd numbers from my list? Are there quick ways to find the average of a list of numbers, or add them all together? | + + +# What Not To Ask +--- + +#### Q: Can I ask a question? + +Yes. Always yes. Just ask. + +#### Q: Is anyone here good at Flask / Pygame / PyCharm? + +There are two problems with this question: + +1. This kind of question does not manage to pique anyone's interest, so you're less likely to get an answer overall. + On the other hand, a question like `"Is it possible to get PyCharm to automatically compile SCSS into CSS files"` is much more likely to be interesting to someone. + Sometimes, the best answers come from someone who does not already know the answer, but who finds the question interesting enough to go search for the answer on your behalf. +2. When you qualify your question by first asking if someone is good at something, you are filtering out potential answerers. + [Not only are people bad at judging their own skill at something](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect), but the truth is that even someone who has zero experience with the framework you're having trouble with might still be of excellent help to you. + +So instead of asking if someone is good at something, simply ask your question right away. + +#### Q: Can I use `str()` on a `discord.py` Channel object? + +Try it yourself and see. Experimentation is a great way to learn, and you'll save a lot of time by just trying things out. Don't be afraid of your computer! + +#### Q: My code doesn't work + +This isn't a question, and it provides absolutely no context or information. +Depending on the mood of the people that are around, you may even find yourself ignored. +Don't be offended by this - just try again with a better question. + +#### Q: Can anyone help me break into someone's Facebook account / write a virus / download videos from YouTube? + +We will absolutely not help you with hacking, pirating, or any other illegal activity. +A question like this is likely to be followed up with a ban if the person asking it doesn't back down quickly. + +#### Q: Can I send you a private message? + +Sure, but keep in mind that our staff members will not provide help via DMs. +We prefer that questions are answered in a public channel so lurkers can learn from them. + +#### Q: Can you help me over Teamviewer? + +No, sorry. + + +# Examining Tracebacks + +Usually, the first sign of trouble is that when you run your code, it raises an exception. +For beginning programmers, the traceback that's generated for the exception may feel overwhelming and discouraging at first. +However, in time, most developers start to appreciate the extensive information contained in the traceback as it helps them track down the error in their code. +So, don't panic and take a moment to carefully review the information provided to you. + +### Reading the Traceback + +```py +Traceback (most recent call last): + File "my_python_file.py", line 6, in <module> + spam = division(a=10, b=0) + File "my_python_file.py", line 2, in division + answer = a / b +ZeroDivisionError: division by zero +``` + +In general, the best strategy is to read the traceback from bottom to top. +As you can see in the example above, the last line of the traceback contains the actual exception that was raised by your code. +In this case, `ZeroDivisionError: division by zero`, clearly indicates the problem: We're trying to divide by zero somewhere in our code and that obviously can't be right. +However, while we now know which exception was raised, we still need to trace the exception back to the error in our code. + +To do so, we turn to the lines above the exception. +Reading from bottom to top again, we first encounter the line where the exception was raised: `answer = a / b`. +Directly above it, we can see that this line of code was `line 2` of the file `my_python_file.py` and that it's in the scope of the function `division`. +At this point, it's a good idea to inspect the code referenced here to see if we can spot an obvious mistake: + +```py +# Python Code +1| def division(a, b): +2| answer = a / b +3| return answer +``` + +Unfortunately, there's no obvious mistake in the code at this point, although one thing we do see here is that this function divides `a` by `b` and that the exception will only occur if `b` is somehow assigned the numeric value `0`. + +Keeping that observation in the back of our minds, we continue reading the traceback from bottom to top. The next thing we encounter is `spam = division(a=10, b=0)` from `line 6` of the file `my_python_file.py`. +In this case, `<module>` tells us that the code is in the global scope of that file. +While it's already clear from the traceback what's going wrong here, we're passing `b=0` to the function `division`, inspecting the code shows us the same: + +```python +5| spam = division(a=10, b=0) +6| print(spam) +``` + +We have now traced back the exception to a line of code calling the division function with a divisor of `0`. +Obviously, this is a simplified example, but the exact same steps apply to more complex situations as well. + +### The Error is Sometimes in the Line Before the Line in the Traceback + +Sometimes, the actual error is in the line just before the one referenced in the traceback. +This usually happens when we've inadvertently omitted a character meant to close an expression, like a brace, bracket, or parenthesis. +For instance, the following snippet of code will generate a traceback pointing at the line after the one in which we've missed the closing parenthesis: + +```python +# Python Code +1| print("Hello, world!" +2| print("This is my first Python program!") + +# Terminal output +Traceback (most recent call last): + File "my_python_file.py", line 2 + print("This is my first Python program!") + ^ +SyntaxError: invalid syntax +``` + +The reason this may happen is that Python allows for [implicit line continuation](https://docs.python.org/3/reference/lexical_analysis.html#implicit-line-joining) and will only notice the error when the expression does not continue as expected on the next line. +So, it's always a good idea to also check the line before the one mentioned in the traceback! + +### More Information on Exceptions + +Further information on exceptions can be found in the official Python documentation: + +* [The built-in exceptions page](https://docs.python.org/3/library/exceptions.html) lists all the built-in exceptions along with a short description of the exception. + If you're unsure of the meaning of an exception in your traceback, this is a good place to start. +* [The errors and exceptions chapter in the official tutorial ](https://docs.python.org/3/tutorial/errors.html) gives an overview of errors and exceptions in Python. + Besides explaining what exceptions are, it also explains how to handle expected exceptions graciously to keep your application from crashing when an expected exception is raised and how to define custom exceptions specific to your application. + +If you encounter an exception specific to an external module or package, it's usually a good idea to check the documentation of that package to see if the exception is documented. +Another option is to paste a part of the traceback, usually the last line, into your favorite search engine to see if anyone else has encountered a similar problem. +More often than not, you will be able to find a solution to your problem this way. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md b/pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md new file mode 100644 index 00000000..cde7d63e --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md @@ -0,0 +1,165 @@ +--- +title: "Code Reviews: A Primer" +description: An introduction and guide to the world of code reviews! +icon: fab fa-github +toc: 3 +--- + +## The What, the Why, and the How + +##### The What & The Why + +This is a guide that will cover the unsung hero of the coding process: code reviews. +Whether you're working professionally or on an open-source project on GitHub, you'll find that code reviews are an essential part of the process. + +So what exactly is a code review? The closest thing to compare it to is proofreading. +In proofreading, you look at an essay someone wrote and give suggestions on how things could be written better or point out mistakes that the writer may have missed. +Code reviewing is the same way. +You're given some code that someone wrote and see if you can find ways to improve it or point mistakes that the coder may have missed. + +"Hemlock", you might say, "Why should I care? Why would this be important?" The thing to remember is that coding is a team effort. +Code reviews help the programs we write to be the very best they can be. +We're all human, and mistakes are made, but through working together, we can correct those mistakes (as best we can) before they get merged into production. +Even if you're not the best coder in the world, you may spot things that even a professional programmer might miss! + +##### The How + +And now for the most important part of the guide. +This will detail the general process of how to write a code review, what to look for, how to do basic testing, and how to submit your review. +Big thanks to Akarys, who wrote this section of the guide! + +--- + +## Our Code Review Process + +> Note: Everything described in this guide is the writer's way of doing code reviews. +> This is purely informative; there's nothing wrong with branching off of this guide and creating your own method. +> We would even encourage you to do so! + +We usually do reviews in 3 steps: + +1. Analyze the issue(s) and the pull request (PR) +2. Analyze and review the code +3. Test the functionality + +So let's jump right into it! + +## Step 1: Analyzing the Issue(s) and the Pull Request + +Every issue and pull request has a reason for being. +While it's possible to do a code review without knowing that reason, it will make your life easier to get the full context of why it exists and what it's trying to do. +Let's take a look at how we can get that context. + +### Analyzing the Issue(s) + +In almost every case, the PR will have some linked issues. +On the right-hand side of the PR's page in GitHub, you'll see a section labeled "Linked issues". +Reading the issue(s) and their comments will allow you to know why certain decisions were made. +This will be invaluable when you review the implementation. +The author of the issue may suggest the code be written one way but the PR ends up writing it another. +This happens during the development process: New ideas are thought of, optimizations are found, other ideas are removed, etc. +Even if there are changes, functionality or fix that the issue covers should be covered by the PR. + +### Analyzing the PR + +The author of the PR will (or should) have made comments detailing how they implemented the code and whether it differs from the proposed implementation in the issue. +If they do things differently than suggested, they should also have explained why those changes were made. + +Remember that just like in the issue, PR comments have value. +Some of your future concerns about the implementation or even review comments may have already been discussed. +Although, if you disagree with the decision, feel free to comment about it! The more input we get from our reviewers, the better! +The last thing you need to remember is that commit messages are important. They're notes from the PR's author that give details as to what they changed. This can help you keep track of what change happened at what point in development, which can help you get some context about a given change. + +Now that you know more about the changes, let's start the fun stuff: the code review! + +## Step 2: Reviewing the Code Itself + +After all, that's why we're here! Let's dive right in! + +### The 1st Read: Getting a Sense of the Code + +It's impossible to understand the code in the PR your first time through. +Don't be surprised if you have to read over it a few times in order to get the whole picture. +When reading the code for the first time, we would recommend you to only try to get a sense of how the code flow. +What does this mean, you may ask? +Well, it is about knowing how every individual piece fits together to achieve the desired goal. + +Pay close attention to how the functions and classes are called. +Make sure to read the comments and docstrings; they'll help explain sections of the code that may be confusing. +Finally, remember that your goal at the moment is to get a general idea of how the code achieves its goal. +You don't have to understand the finer points at this stage. + +### The 2nd Read: Looking at Every Little Detail + +Now that you know how the code flows, you can take a closer look at the code. +Comment on anything you think could be done better. +This includes, but is not limited to: + +* The general structure of the code +* The variable names +* The algorithm(s) used +* The use or the lack of use of already existing code or a library +* Blocks of code that could benefit from comments +* Spelling +* Anything you see that doesn't seem quite right is worth commenting on. Discussing the things you find benefits everyone involved! + +Another good technique is to imagine how you would have implemented a specific functionality and compare it with the proposed implementation. +GitHub has a feature allowing you to mark files as read, and it's recommended to take advantage of it so that you don't lose your place if you take a break. +Now that you know what to comment on, let's take a closer look at how to comment. + +### Leaving Good Review Comments + +When leaving a comment, don't forget that we can't know what you're thinking; you have to write it down. +Your comment should describe why you think this should be changed and how you propose to change it. +Note that you can omit the latter in some cases if it is outside of your area of expertise for instance. +There's nothing wrong with using your comments to ask questions! If there's something you're not sure about, don't hesitate to ask in your comment. +It might indicate that the PR's author needs to add comments, change variable or function names, or even change a block of code entirely. + +GitHub has a handy feature that allows you to leave suggestions. +This means that the author can drop your suggestion into their PR with a click of a button. +If you provide one, great! It will speed up the review process even more! +On the opposite side, note that you aren't required to do all of that when leaving a comment. If you are fixing a typo, leaving a suggestion is enough. + +If you have concerns about a particular piece of code for example a race condition, it is totally okay to point it out in a comment, even if you don't have a suggested way to fix it. + +## Testing the Functionalities + +A code review isn't only about looking at the code; it's also about testing it. +Always try to make sure that the code is working properly before approving the changes. + +Something else to note: you are free to review code without testing functionality. +That's totally okay! Just make sure to mention when you submit your review. + +### Reviewing the Functionality + +When reviewing functionality, keep asking yourself how you would have designed it and then compare your idea with the implementation. +Note that you are allowed to give suggestions about how the functionality should be designed. + +Start with the basic usages and work your way up to more complicated ones. +Verify that everything is working as intended. +If it isn't, leave a comment giving what you've done, what should have happened, and what actually happened, along with any potential logs or errors you could have. +If you like, you can even try to pinpoint the issue and find a fix for it. We would be very grateful if you did! + +### Objective: Break it + +Good functionality should be able to handle edge cases. +Try to throw every edge case you might think of and see how it responds. This might not be needed in some cases, but it's essential for security-related work. +If the implementation has a security breach, you should absolutely find it! Just like in the previous section, if you find something and comment on it, great! +If you manage to find a way to fix it and suggest the fix, even better! + +### But what if the Project doesn't even start? + +This is a tricky one. Sometimes the project won't even start, keeping you from testing it all together. + +In this case, you should try to investigate if it is failing because of an error in the code. If it ends up being because of the functionality, you should comment on that. + +If you aren't sure why it isn't starting, feel free to ask us! + +## Final Words + +You did it! +You are now ready to take on the wild world of code reviewing! +We know that process can seem long, tedious, and not feel like a necessary task, but it is! +We are very grateful to you for reading through this and for your potential future code reviews. +We couldn't move forward without you. +Thank you! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md new file mode 100644 index 00000000..f8baa354 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md @@ -0,0 +1,115 @@ +--- +title: Contributing +description: A guide to contributing to our open source projects. +icon: fab fa-github +--- + +Our projects on Python Discord are open source and [available on Github](https://github.com/python-discord). If you would like to contribute, consider one of the following projects: + +<!-- Project cards --> +<div class="columns is-multiline is-centered is-3 is-variable"> + <div class="column is-one-third-desktop is-half-tablet"> + <div class="card github-card"> + <div class="card-header"> + <div class="card-header-title is-centered"> + <a class="is-size-5" href="https://github.com/python-discord/sir-lancebot"> + <i class="fab fa-github"></i> <strong >Sir Lancebot</strong> + </a> + </div> + </div> + <div class="card-content"> + <div class="content"> + Our community-driven Discord bot. + </div> + <div class="tags has-addons"> + <span class="tag is-dark">Difficulty</span> + <span class="tag is-primary">Beginner</span> + </div> + </div> + <div class="card-footer"> + <a href="https://github.com/python-discord/sir-lancebot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="far fa-exclamation-circle"></i> Issues</a> + <a href="https://github.com/python-discord/sir-lancebot/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i> PRs</a> + </div> + <div class="card-footer"> + <a href="/pages/guides/pydis-guides/contributing/sir-lancebot" class="card-footer-item"><i class="fas fa-cogs"></i> Setup and Configuration Guide</a> + </div> + </div> + </div> + <div class="column is-one-third-desktop is-half-tablet"> + <div class="card github-card"> + <div class="card-header"> + <div class="card-header-title is-centered"> + <a href="https://github.com/python-discord/bot"> + <strong class="is-size-5"><i class="fab fa-github"></i> Bot</strong> + </a> + </div> + </div> + <div class="card-content"> + <div class="content"> + The community and moderation Discord bot. + </div> + <div class="tags has-addons"> + <span class="tag is-dark">Difficulty</span> + <span class="tag is-warning">Intermediate</span> + </div> + </div> + <div class="card-footer"> + <a href="https://github.com/python-discord/bot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="far fa-exclamation-circle"></i> Issues</a> + <a href="https://github.com/python-discord/bot/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i> PRs</a> + </div> + <div class="card-footer"> + <a href="/pages/guides/pydis-guides/contributing/bot" class="card-footer-item"><i class="fas fa-cogs"></i> Setup and Configuration Guide</a> + </div> + </div> + </div> + <div class="column is-one-third-desktop is-half-tablet"> + <div class="card github-card"> + <div class="card-header"> + <div class="card-header-title is-centered"> + <a href="https://github.com/python-discord/site"> + <strong class="is-size-5"><i class="fab fa-github"></i> Site</strong> + </a> + </div> + </div> + <div class="card-content"> + <div class="content"> + The website, subdomains and API. + </div> + <div class="tags has-addons"> + <span class="tag is-dark">Difficulty</span> + <span class="tag is-danger">Advanced</span> + </div> + </div> + <div class="card-footer"> + <a href="https://github.com/python-discord/site/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="far fa-exclamation-circle"></i> Issues</a> + <a href="https://github.com/python-discord/site/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i> PRs</a> + </div> + <div class="card-footer"> + <a href="/pages/guides/pydis-guides/contributing/site" class="card-footer-item"><i class="fas fa-cogs"></i> Setup and Configuration Guide</a> + </div> + </div> + </div> +</div> + +If you don't understand anything or need clarification, feel free to ask any staff member with the **@PyDis Core Developers** role in the server. We're always happy to help! + +### Useful Resources + +[Style Guide](./style-guide/) - Information regarding the code styles you should follow when working on our projects.<br> +[Review Guide](../code-reviews-primer/) - A guide to get you started on doing code reviews. + +## Contributors Community +We are very happy to have many members in our community that contribute to [our open source projects](https://github.com/python-discord/). +Whether it's writing code, reviewing pull requests, or contributing graphics for our events, it’s great to see so many people being motivated to help out. +As a token of our appreciation, those who have made significant contributions to our projects will receive a special **@Contributors** role on our server that makes them stand out from other members. +That way, they can also serve as guides to others who are looking to start contributing to our open source projects or open source in general. + +#### Guidelines for the @Contributors Role + +One question we get a lot is what the requirements for the **@Contributors** role are. +As it’s difficult to precisely quantify contributions, we’ve come up with the following guidelines for the role: + +- The member has made several significant contributions to our projects. +- The member has a positive influence in our contributors subcommunity. + +The role will be assigned at the discretion of the Admin Team in consultation with the Core Developers Team. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml new file mode 100644 index 00000000..4a338463 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml @@ -0,0 +1,3 @@ +title: Contributing +description: How to contribute to our open source projects on Github. +icon: fab fa-github diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md new file mode 100644 index 00000000..a48b7300 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md @@ -0,0 +1,209 @@ +--- +title: Contributing to Bot +description: A guide to setting up and configuring Bot. +icon: fab fa-github +toc: 1 +--- + +# Requirements +* [Python 3.8](https://www.python.org/downloads/) +* [Pipenv](https://github.com/pypa/pipenv#installation) + * `pip install pipenv` +* [Git](https://git-scm.com/downloads) + * [Windows](https://git-scm.com/download/win) + * [MacOS](https://git-scm.com/download/mac) or `brew install git` + * [Linux](https://git-scm.com/download/linux) +* A running webserver for the [site](../site) + * Follow the linked guide only if you don't want to use Docker or if you plan to do development on the site project too. + +## Using Docker + +Both the site and the bot can be started using Docker. +Using Docker is generally recommended (but not strictly required) because it abstracts away some additional set up work, especially for the site. +However, if you plan to attach a debugger to either the site or the bot, run the respective project directly on your system (AKA the _host_) instead. + +The requirements for Docker are: + +* [Docker CE](https://docs.docker.com/install/) +* [Docker Compose](https://docs.docker.com/compose/install/) (This already comes bundled on macOS and Windows, so you shouldn't need to install it) + * `pip install docker-compose` + +--- +# Fork the project +You will need access to a copy of the git repository of your own that will allow you to edit the code and push your commits to. +Creating a copy of a repository under your own account is called a _fork_. + +* [Learn how to create a fork of the repository here.](../forking-repository) + +This is where all your changes and commits will be pushed to, and from where your PRs will originate from. + +For any staff member, since you have write permissions already to the original repository, you can just create a feature branch to push your commits to instead. + +--- +# Development environment +1. [Clone your fork to a local project directory](../cloning-repository/) +2. [Install the project's dependencies](../installing-project-dependencies/) +3. [Prepare your hosts file (Optional)](../hosts-file/) + +--- +# Test server and bot account +You will need your own test server and bot account on Discord to test your changes to the bot. + +* [**Create a test server**](../setting-test-server-and-bot-account#setting-up-a-test-server) +* [**Create a bot account**](../setting-test-server-and-bot-account#setting-up-a-bot-account) +* Invite it to the server you just created. + +### Privileged Intents + +With `discord.py` 1.5 and later, it is now necessary to explicitly request that your Discord bot receives certain gateway events. +The Python bot requires the `Server Member Intent` to function. +In order to enable it, visit the [Developer Portal](https://discord.com/developers/applications/) (from where you copied your bot's login token) and scroll down to the `Privileged Gateway Intents` section. +The `Presence Intent` is not necessary and can be left disabled. + +If your bot fails to start with a `PrivilegedIntentsRequired` exception, this indicates that the required intent was not enabled. + +### Server Setup + +Setup categories, channels, emojis, roles, and webhooks in your server. To see what needs to be added, please refer to the following sections in the `config-default.yml` file: + +* `style.emojis` +* `guild.categories` +* `guild.channels` +* `guild.roles` +* `guild.webhooks` + +We understand this is tedious and are working on a better solution for setting up test servers. +In the meantime, [here](https://discord.new/zmHtscpYN9E3) is a template for you to use.<br> + +--- +# Configure the bot +You will need to copy IDs of the test Discord server, as well as the created channels and roles to paste in the config file. +If you're not sure how to do this, [check out the information over here.](../setting-test-server-and-bot-account#obtain-the-ids) + +1. Create a copy of `config-default.yml` named `config.yml` in the same directory. +2. Set `guild.id` to your test servers's ID. +3. Change the IDs in the [sections](#server-setup) mentioned earlier to match the ones in your test server. +4. Set `urls.site_schema` and `urls.site_api_schema` to `"http://"`. +5. Set `urls.site`: + - If running the webserver in Docker, set it to `"web:8000"`. + - If the site container is running separately (i.e. started from a clone of the site repository), then [COMPOSE_PROJECT_NAME](../docker/#compose-project-names) has to be set to use this domain. If you choose not to set it, the domain in the following step can be used instead. + - If running the webserver locally and the hosts file has been configured, set it to `"pythondiscord.local:8000"`. + - Otherwise, use whatever domain corresponds to the server where the site is being hosted. +6. Set `urls.site_api` to whatever value you assigned to `urls.site` with `api` prefixed to it, for example if you set `urls.site` to `web:8000` then set `urls.site_api` to `api.web:8000`. +7. Setup the environment variables listed in the section below. + +### Environment variables + +These contain various settings used by the bot. +To learn how to set environment variables, read [this page](../configure-environment-variables) first. + +The following is a list of all available environment variables used by the bot: + +| Variable | Required | Description | +| -------- | -------- | -------- | +| `BOT_TOKEN` | Always | Your Discord bot account's token (see [Test server and bot account](#test-server-and-bot-account)). | +| `BOT_API_KEY` | When running bot without Docker | Used to authenticate with the site's API. When using Docker to run the bot, this is automatically set. By default, the site will always have the API key shown in the example below. | +| `REDDIT_CLIENT_ID` | reddit cog | OAuth2 client ID for authenticating with the [reddit API](https://github.com/reddit-archive/reddit/wiki/OAuth2). | +| `REDDIT_SECRET` | reddit cog | OAuth2 secret for authenticating with the reddit API. *Leave empty if you're not using the reddit API.* | +| `BOT_SENTRY_DSN` | When connecting the bot to sentry | The DSN of the sentry monitor. | +| `REDIS_PASSWORD` | When not using FakeRedis | The password to connect to the redis database. *Leave empty if you're not using REDIS.* | + +--- + +If you are running on the host, while not required, we advise you set `use_fakeredis` to `true` in the config file during development to avoid the need of setting up a Redis server. +It does mean you may lose persistent data on restart but this is non-critical. +Otherwise, you should set up a Redis instance and fill in the necessary config. +{: .notification .is-warning } + +--- + +Example `.env` file: + +```shell +BOT_TOKEN=YourDiscordBotTokenHere +BOT_API_KEY=badbot13m0n8f570f942013fc818f234916ca531 +REDDIT_CLIENT_ID=YourRedditClientIDHere +REDDIT_SECRET=YourRedditSecretHere +``` + +--- +# Run the project + +The bot can run with or without Docker. +When using Docker, the site, which is a prerequisite, can be automatically set up too. +If you don't use Docker, you have to first follow [the site guide](../site/) to set it up yourself. +The bot and site can be started using independent methods. +For example, the site could run with Docker and the bot could run directly on your system (AKA the _host_) or vice versa. + +## Run with Docker + +The following sections describe how to start either the site, bot, or both using Docker. +If you are not interested in using Docker, see [this page](../site/) for setting up the site and [this section](#run-on-the-host) for running the bot. + +If you get any Docker related errors, reference the [Possible Issues](../docker#possible-issues) section of the Docker page. + +### Site and bot + +This method will start both the site and the bot using Docker. + +Start the containers using Docker Compose while inside the root of the project directory: + +```shell +docker-compose up +``` + +The `-d` option can be appended to the command to run in detached mode. +This runs the containers in the background so the current terminal session is available for use with other things. + +### Site only + +This method will start only the site using Docker. + +```shell +docker-compose up site +``` + +See [this section](#run-on-the-host) for how to start the bot on the host. + +### Bot only + +This method will start only the bot using Docker. +The site has to have been started somehow beforehand. + +Start the bot using Docker Compose while inside the root of the project directory: + +```shell +docker-compose up --no-deps bot +``` + +## Run on the host + +Running on the host is particularly useful if you wish to debug the bot. +The site has to have been started somehow beforehand. + +```shell +pipenv run start +``` + +--- +## Working with Git +Now that you have everything setup, it is finally time to make changes to the bot! +If you have not yet [read the contributing guidelines](../contributing-guidelines), now is a good time. +Contributions that do not adhere to the guidelines may be rejected. + +Notably, version control of our projects is done using Git and Github. +It can be intimidating at first, so feel free to ask for any help in the server. + +[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/) + +## Adding new statistics + +Details on how to add new statistics can be found on the [statistic infrastructure page](https://blog.pythondiscord.com/statistics-infrastructure). +We are always open to more statistics so add as many as you can! + +## Running tests + +[This section](https://github.com/python-discord/bot/blob/main/tests/README.md#tools) of the README in the `tests` repository will explain how to run tests. +The whole document explains how unittesting works, and how it fits in the context of our project. + +Have fun! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md new file mode 100644 index 00000000..fad54374 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md @@ -0,0 +1,31 @@ +--- +title: Cloning a Repository +description: A guide to cloning git repositories. +icon: fab fa-github +--- + +> **Note:** The process varies depending on your choice of code editor / IDE, so refer to one of the following guides: + +- [Cloning with PyCharm](#cloning-with-pycharm) +- [Cloning with the command line](#cloning-with-the-command-line) + +The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. You should have already retrieved your fork's Git URL as described in [**Creating a Fork**](../forking-repository). + +--- + +## Cloning with PyCharm +1. Load up PyCharm and click `Get from VCS`.<br> + +2. Enter the URL of your forked repository. +3. Change the directory if you desire and click `Clone`.<br> + + +--- + +## Cloning with the command line +1. Clone your forked repository using `git clone` followed by your fork's Git URL. Then, change your working directory to the repository. +```shell +$ git clone https://github.com/<your username>/sir-lancebot +... +$ cd sir-lancebot +``` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md new file mode 100644 index 00000000..8b8e3f95 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md @@ -0,0 +1,23 @@ +--- +title: Configure Environment Variables +description: A guide to configuring environment variables. +icon: fas fa-cog +--- + +1. Create a text file named **.env** in your project root (that's the base folder of your repository): + * Unix/Git Bash: `touch /path/to/project/.env` + * Windows CMD: `type nul > \path\to\project\.env` (The error *The system cannot find the file specified* can be safely ignored.) +> **Note:** The entire file name is literally `.env` +2. Open the file with any text editor. +3. Each environment variable is on its own line, with the variable and the value separated by a `=` sign. + +Example: + +* Set the environment variable `SEASONALBOT_DEBUG` to `True`: +``` +SEASONALBOT_DEBUG=True +``` +* Set the environment variable `CHANNEL_ANNOUNCEMENTS` to `12345`: +``` +CHANNEL_ANNOUNCEMENTS=12345 +``` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md new file mode 100644 index 00000000..aa784a50 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md @@ -0,0 +1,30 @@ +--- +title: Contributing Guidelines +description: Guidelines to adhere to when contributing to our projects. +--- + +Thank you for your interest in our projects! + +If you are interested in contributing, **this page contains the golden rules to follow when contributing.** +Supplemental information [can be found here](./supplemental-information/). +Do note that failing to comply with our guidelines may lead to a rejection of the contribution. + +If you are confused by any of these rules, feel free to ask us in the `#dev-contrib` channel in our [Discord server.](https://discord.gg/python) + +# The Golden Rules of Contributing + +1. **Lint before you push.** We have simple but strict style rules that are enforced through linting. +You must always lint your code before committing or pushing. +[Using tools](./supplemental-information/#linting-and-precommit) such as `precommit` or `black` can make this easier. +2. **Make great commits.** +Great commits should be atomic, with a commit message explaining what and why. +More on that can be found in [this section](./supplemental-information/#writing-good-commit-messages). +3. **Do not open a pull request if you aren't assigned to the issue.** +If someone is already working on it, consider offering to collaborate with that person. +4. **Use assets licensed for public use.** +Whenever the assets are images, audio or even code, they must have a license compatible with our projects. +5. **Follow the [Python Discord Code of Conduct](https://pydis.com/coc).** +We aim to foster a welcoming and friendly environment on our open source projects. +We take violations of our Code of Conduct very seriously, and may respond with moderator action. + +Welcome to our projects! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml new file mode 100644 index 00000000..80c8e772 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml @@ -0,0 +1,2 @@ +title: Contributing Guidelines +description: Guidelines to adhere to when contributing to our projects. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md new file mode 100644 index 00000000..78a27173 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md @@ -0,0 +1,97 @@ +--- +title: Supplemental Information +description: Additional information related to our contributing guidelines. +--- + +This page contains additional information concerning a specific part of our development pipeline. + +## Writing Good Commit Messages + +A well-structured git log is key to a project's maintainability; it provides insight into when and *why* things were done for future maintainers of the project. + +Commits should be as narrow in scope as possible. +Commits that span hundreds of lines across multiple unrelated functions and/or files are very hard for maintainers to follow. +After about a week they'll probably be hard for you to follow, too. + +Please also avoid making minor commits for fixing typos or linting errors. +*[Don’t forget to lint before you push!](https://soundcloud.com/lemonsaurusrex/lint-before-you-push)* + +A more in-depth guide to writing great commit messages can be found in Chris Beam's *[How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/).* + +## Code Style + +All of our projects have a certain project-wide style that contributions should attempt to maintain consistency with. +During PR review, it's not unusual for style adjustments to be requested. + +[This page](../../style-guide/) will reference the differences between our projects and what is recommended by [PEP 8.](https://www.python.org/dev/peps/pep-0008/) + +## Linting and Precommit + +On most of our projects, we use `flake8` and `precommit` to ensure that the code style is consistent across the code base. + +Running `flake8` will warn you about any potential style errors in your contribution. +You must always check it **before pushing**. +Your commit will be rejected by the build server if it fails to lint. + +`precommit` is a powerful tool that helps you automatically lint before you commit. +If the linter complains, the commit is aborted so that you can fix the linting errors before committing again. +That way, you never commit the problematic code in the first place! + +Please refer to the project-specific documentation to see how to setup and run those tools. +In most cases, it is either `pipenv run [lint | precommit]` or `poetry run [lint | precommit]`. + +## Type Hinting + +[PEP 484](https://www.python.org/dev/peps/pep-0484/) formally specifies type hints for Python functions, added to the Python Standard Library in version 3.5. +Type hints are recognized by most modern code editing tools and provide useful insight into both the input and output types of a function, preventing the user from having to go through the codebase to determine these types. + +For example: + +```python +import typing + +def foo(input_1: int, input_2: typing.Dict[str, str]) -> bool: + ... +``` + +This tells us that `foo` accepts an `int` and a `dict`, with `str` keys and values, and returns a `bool`. + +If the project is running Python 3.9 or above, you can use `dict` instead of `typing.Dict`. +See [PEP 585](https://www.python.org/dev/peps/pep-0585/) for more information. + +All function declarations should be type hinted in code contributed to the PyDis organization. + +## Logging + +Instead of using `print` statements for logging, we use the built-in [`logging`](https://docs.python.org/3/library/logging.html) module. +Here is an example usage: + +```python +import logging + +log = logging.getLogger(__name__) # Get a logger bound to the module name. +# This line is usually placed under the import statements at the top of the file. + +log.trace("This is a trace log.") +log.warning("BEEP! This is a warning.") +log.critical("It is about to go down!") +``` + +Print statements should be avoided when possible. +Our projects currently defines logging levels as follows, from lowest to highest severity: + +- **TRACE:** These events should be used to provide a *verbose* trace of every step of a complex process. This is essentially the `logging` equivalent of sprinkling `print` statements throughout the code. +- **Note:** This is a PyDis-implemented logging level. It may not be available on every project. +- **DEBUG:** These events should add context to what's happening in a development setup to make it easier to follow what's going while workig on a project. This is in the same vein as **TRACE** logging but at a much lower level of verbosity. +- **INFO:** These events are normal and don't need direct attention but are worth keeping track of in production, like checking which cogs were loaded during a start-up. +- **WARNING:** These events are out of the ordinary and should be fixed, but can cause a failure. +- **ERROR:** These events can cause a failure in a specific part of the application and require urgent attention. +- **CRITICAL:** These events can cause the whole application to fail and require immediate intervention. + +Any logging above the **INFO** level will trigger a [Sentry](http://sentry.io) issue and alert the Core Developer team. + +## Draft Pull Requests + +Github [provides a PR feature](https://github.blog/2019-02-14-introducing-draft-pull-requests/) that allows the PR author to mark it as a Draft when opening it. This provides both a visual and functional indicator that the contents of the PR are in a draft state and not yet ready for formal review. + +This feature should be utilized in place of the traditional method of prepending `[WIP]` to the PR title. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md new file mode 100644 index 00000000..63be9f3e --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md @@ -0,0 +1,120 @@ +--- +title: Working with Docker & Docker Compose +description: Guide to running our projects with Docker and Docker CE. +icon: fab fa-docker +toc: 2 +--- + +Both our [Site](../site/) and [Bot](../bot/) projects use Docker and Docker-Compose during development in order to provide an easy to setup and consistent development environment. + +Consider reading some of the following topics if you're interested in learning more about Docker itself: + + * [**What is Docker?**](https://docs.docker.com/engine/docker-overview/) + * [**How can I learn to use it for my own stuff?**](https://docs.docker.com/get-started/) + * [**What about Docker Compose, what's it for?**](https://docs.docker.com/compose/) + +# Docker Installation +You can find installation guides available for your respective OS from the official Docker documentation: +[https://docs.docker.com/install/](https://docs.docker.com/install/) + +## After Installing on Linux +If you're on Linux, there's a few extra things you should do: + +1. [**Add your user to the `docker` user group so you don't have to use `sudo` when running docker or docker-compose.**](#add-user-group) +2. [**Start up the Docker service.**](#run-the-service) +3. [**Set the Docker service to start on boot.**](#start-on-boot) **(optional)** + +### Run the Service +Most linux distributions **systemd**, you can start the service with: +```shell +$ sudo systemctl start docker +``` + +### Add User Group +```shell +$ sudo groupadd docker +$ sudo usermod -aG docker $USER +``` +Log out and log back in to ensure your group changes work. + +### Start on Boot +```shell +$ sudo systemctl enable docker +``` + +# Possible Issues +### Couldn't connect to Docker daemon +```shell +ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running? +``` +**Problem**<br> +Your Docker service is either not started, or you haven't yet installed Docker. + +**Solution**<br> +[Start the service](#run-the-service) or ensure it's installed. +If it's not, [install it](#docker-installation). + +### Error loading config file +```plaintext +WARNING: Error loading config file: /home/user/.docker/config.json - +stat /home/user/.docker/config.json: permission denied +``` +**Problem**<br> +You initially ran Docker using `sudo` before adding your user to the `docker` group, resulting in your `~/.docker/` directory being created with incorrect permissions. + +**Solution**<br> +Remove the existing `~/.docker/` directory. It will be automatically re-created with the correct permissions. + +### Drive has not been shared (Windows users) + +When attempting to run the `docker-compose up` command on a Windows machine, you receive the following or similar error message: +```text +ERROR: for bot_bot_1 Cannot create container for service bot: b'Drive has not been shared' +``` +**Problem**<br> +Windows has not been configured to share drives with Docker. + +**Solution**<br> +> NOTE: Solution requires Windows user credentials for an account that has administrative privileges. + +1. Right-click the Docker icon in the Windows system tray, and choose "Settings" from the context menu.<br> + + +2. Click the "Shared Drives" label at the left, and check the box next to the drive letter where your project is stored.<br> + + +3. Click "Apply" and enter appropriate Windows credentials (likely just your own account, if you have administrative privileges). + +4. Re-run the `docker-compose up` command. + +# Compose Project Names +When you launch services from a docker-compose, you'll notice the name of the containers aren't just the service name. +You'll see this when launching your compose, as well as being able to be seen in the command `docker-compose ps` which will list the containers. +It should match something like this: +``` +site_site_1 +``` +This matched the following container name format: +``` +projectname_servicename_1 +``` +By default, your project name will match the name of the folder your project is inside in all lowercase. + +You can specify a custom project name by adding a `COMPOSE_PROJECT_NAME` variable to your `.env` file before launching the compose: +``` +COMPOSE_PROJECT_NAME=site +``` +Containers with the same project name end up connected to the same network by default. +For example, the `site` container connects with `postgres` via the matching hostname inside the container. +Even if you didn't expose a port to the host, the two containers would be able to talk to each other. + +You can have two different projects able to communicate in the same way by having them use the same project name. +We use this feature to allow the `bot` container to communicate with a separate local copy of `site` that may need to be tested during development. + +By default, the `bot` container could launch with a name of `bot_bot_1` and the `site` container with a name of `site_site_1`. Since the prefixes are different, they're in distinct projects, so can't talk with each other. + +If we got to the bot's `.env` file, and add the line below, we can set `bot` to run in the same project as `site`: +``` +COMPOSE_PROJECT_NAME=site +``` +Now they can talk to each other! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md new file mode 100644 index 00000000..07535dbe --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md @@ -0,0 +1,18 @@ +--- +title: Forking a Repository +description: A guide to forking git repositories. +icon: fab fa-github +--- + +Before contributing to any project, you will have to fork the project, ie. create your own online copy of the project. +The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. + +1. Navigate to the repository page and press the `Fork` button at the top of the page. + +2. Fork it to your account.<br> + +3. Later, you will need the Git URL of your forked repository in order to clone it. +In your newly forked repository, copy the Git URL by clicking the green `Code` button, then click the Copy Link button. + + +> If you have SSH set up with GitHub, you may instead click the `SSH` button above the Copy Link button to get the SSH URL. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md new file mode 100644 index 00000000..5d55a7f3 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md @@ -0,0 +1,46 @@ +--- +title: Preparing Your Hosts file +description: How to setup your hosts file for project usage. +icon: fas fa-cog +toc: 3 +--- + +# What's a hosts file? +The hosts file maps a hostname/domain to an IP address, allowing you to visit a given domain on your browser and have it resolve by your system to the given IP address, even if it's pointed back to your own system or network. + +When staging a local [Site](https://pythondiscord.com/pages/contributing/site/) project, you will need to add some entries to your hosts file so you can visit the site with the domain `http://pythondiscord.local` + +# What to add +You would add the following entries to your hosts file. + +```plaintext +127.0.0.1 pythondiscord.local +127.0.0.1 api.pythondiscord.local +127.0.0.1 staff.pythondiscord.local +127.0.0.1 admin.pythondiscord.local +``` + +# How to add it + +### Linux +1. Run `sudo nano /etc/hosts` +2. Enter your user password. +3. Add the new content at the bottom of the file. +4. Use `CTRL+X` +5. Enter `y` to save. + +_This covers most linux distributions that come with `nano`, however you're welcome to use whatever CLI text editor you're comfortable with instead._ + +### Windows +1. Open Notepad as Administrator. +2. Open the file `C:\Windows\System32\Drivers\etc\hosts` +3. Add the new content at the bottom of the file. +4. Save. + +### MacOS +1. Run `sudo nano /private/etc/hosts` in Terminal. +2. Enter your user password. +3. Add the new content at the bottom of the file. +4. Use `CTRL+X` +5. Enter `y` to save. +6. Flush your DNS by running `dscacheutil -flushcache` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md new file mode 100644 index 00000000..4432236e --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md @@ -0,0 +1,38 @@ +--- +title: Installing Project Dependencies +description: A guide to installing the dependencies of our projects. +icon: fab fa-python +--- + +> **Note:** The process varies depending on your choice of code editor / IDE, so refer to one of the following guides: + +- [Installing dependencies with PyCharm](#installing-dependencies-with-pycharm) +- [Installing dependencies with the command line](#installing-dependencies-with-the-command-line) + +The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. +You should have already cloned your fork as described in [**Cloning a Repository**](../cloning-repository). + +--- + +## Installing dependencies with PyCharm +1. Load up your project in PyCharm. +2. Go to the Project Settings by clicking `File`, then `Settings...`. Alternatively, use the shortcut key `Ctrl+Alt+S`. +3. Navigate to `Project Interpreter`, then click the gear icon and click `Add`. + +4. In the popup window, click `Pipenv Environment`, make sure `Install packages from Pipfile` is checked, then click `OK`. + +5. PyCharm will automatically install the packages required into a virtual environment. + + +--- + +## Installing dependencies with the command line +1. Make sure you are in the project directory. +2. Install project and development dependencies: +```shell +$ pipenv sync --dev +``` +* Remember to also set up pre-commit hooks to ensure your pushed commits will never fail linting: +```shell +$ pipenv run precommit +``` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md new file mode 100644 index 00000000..9151e5e3 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md @@ -0,0 +1,121 @@ +--- +title: Issues +description: Guide to Github issues. +icon: fab fa-github +--- + +## What are Issues? + +Issues are tickets that allow us to manage all the suggested features, bugs noticed and discussions about a project. + +An Issue ticket should have a simple, easy to understand title and a clearly written description outlining any of the available details. +Once an Issue is created, people can comment on it and if work is to be actioned due to it, it can be assigned to a contributor so others know that it's being worked on already. + +## How do I make an Issue? + +**Before making an Issue, search the existing ones!** +Often, an Issue ticket already exists within the scope of what you might be considering, so be sure to do a search beforehand and if there it, add any new information or suggestions to the comments on the existing Issue instead, or just add a thumbs up if you agree with it. + +If you don't see one existing, then: + +1. Click the `Issues` tab in a repository:<br> + + +2. Click `New Issue`:<br> + + +3. Enter the title and description for your issue, then click `Submit new issue`:<br> +{: width="600" } + +## What should I put as a title? + +A good title is short and to the point as to what the Issue is about. + +Avoid some of the following: + +- Writing a long title +- Being too vague +- Using informal language +- Using languages other than English + +## What makes a good description? + +A good description is well structured and contains all the information available to you at the time of creating it. If additional information comes to light, it can either be added in edits to the description afterwards, or as part of comments. + +Try to avoid: + +- Masses of unstructured blocks of never ending text +- Including unnecessary details that aren't constructive to the discussion + +Definitely try to: + +- Include relevant images in the description if it involves visual aspects +- Make use of [Github's markdown syntax](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax) for text formatting + +## What are labels? + +Labels allow us to better organise Issues by letting us view what type of Issue it is, how it might impact the codebase and at what stage it's at. + +In our repositories, we try to prefix labels belonging to the same group, for example the label groups `status` or `type`. We will be trying to keep to the same general structure across our project repositories, but just have a look at the full lables list in the respective repository to get a clear idea what's available. + +If you're a contributor, you can add relevant labels yourself to any new Issue ticket you create. + +### General label groups + +These label groups should be present on most of our main project repositories and can serve as a guide as to how they're used. + +#### area +Signifies what section of the project/codebase the Issue is focusing on addressing or discussing. Only one area should be selected usually, as the most focused area should be the selected one. Exceptions exist for Issues that impact multiple areas equally, but are generally not the norm. + +#### priority +How urgent the Issue should be addressed: + +- `critical` - Super important, likely a bug that's impacting the project severely at this moment. +- `high` - Important, impacts the project heavily and/or is time sensitive. +- `normal` - Would be convenient if it's addressed. +- `low` - Doesn't require us to look at any time soon. + +#### status +Where this issue is at currently: + +- `deferred` - Is being put off until a later date +- `planning` - The Issue is being discussed, implementation is not decided or ready to begin. +- `stale` - Hasn't been addressed or contributed to in a long time. Worth reconsidering as worth keeping open or bumped in priority if it needs to be done to get it out. +- `stalled` - Something else has prevented this Issue from moving forward for now. +- `WIP` - The issue is actively being worked on by someone already. + +#### type +What's the purpose of the Issue: + +- `bug` - Addresses a bug in the code +- `enhancement` - Changes or improves on an existing feature. +- `feature` - Addresses a possible new feature. +- `question` - Isn't addressing any code changes, only a discussion or clarification. + +#### Non-group labels +There are 4 labels that aren't in groups as they are globally recognised and shouldn't be renamed: + +- `duplicate` - Marks the Issue as being the same or within scope of an existing one +- `good first issue` - Marks the Issue as being suitable to work on by beginners +- `help wanted` - More people are needed to work on this Issue +- `invalid` - Marks the Issue as not being a proper Issue. + +## Assignments + +Once an Issue is not in the planning/discussing stage and is approved to be worked on, it can be assigned to someone interested in it. + +### Can I assign myself? + +Only staff can assign themselves to a ticket. +If a general contributor assigns themself, they'll be unassigned. + +### How do I get assigned? + +**First check that someone else isn't already assigned.** + +Once you're sure it's available and ready to be worked on, you can leave a comment in the Issue ticket. +Generally, it's first-come first served, so a staff member will usually assign you within the day if they confirm it's clear to do so. + +#### Do I get first preference to work on it if I made the Issue ticket? +As long as you say you'd like to work on it within the description of your ticket or be the first to request so in a comment. +If you forget to say so and someone else asks to be assigned, we aren't likely to unassign them afterwards, so it's entirely up to the discretion of the other person in that case. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md new file mode 100644 index 00000000..c14fe50d --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md @@ -0,0 +1,68 @@ +--- +title: Setting Up a Test Server and Bot Account +description: How to get started with testing our bots. +icon: fab fa-discord +--- + +## Setting up a Test Server + +1. Create a Discord Server if you haven't got one already to use for testing. + +--- + +## Setting up a Bot Account + +1. Go to the [Discord Developers Portal](https://discordapp.com/developers/applications/). +2. Click on the `New Application` button, enter your desired bot name, and click `Create`. +3. In your new application, go to the `Bot` tab, click `Add Bot`, and confirm `Yes, do it!` +4. Change your bot's `Public Bot` setting off so only you can invite it, save, and then get your **Bot Token** with the `Copy` button. +> **Note:** **DO NOT** post your bot token anywhere public, or it can and will be compromised. +5. Save your **Bot Token** somewhere safe to use in the project settings later. +6. In the `General Information` tab, grab the **Client ID**. +7. Replace `<CLIENT_ID_HERE>` in the following URL and visit it in the browser to invite your bot to your new test server. +```plaintext +https://discordapp.com/api/oauth2/authorize?client_id=<CLIENT_ID_HERE>&permissions=8&scope=bot +``` +Optionally, you can generate your own invite url in the `OAuth` tab, after selecting `bot` as the scope. + +--- + +## Obtain the IDs + +First, enable developer mode in your client so you can easily copy IDs. + +1. Go to your `User Settings` and click on the `Appearance` tab. +2. Under `Advanced`, enable `Developer Mode`. + +#### Guild ID + +Right click the server icon and click `Copy ID`. + +#### Channel ID + +Right click a channel name and click `Copy ID`. + +#### Role ID + +Right click a role and click `Copy ID`. +The easiest way to do this is by going to the role list in the guild's settings. + +#### Emoji ID + +Insert the emoji into the Discord text box, then add a backslash `\` right before the emoji and send the message. +The result should be similar to the following + +```plaintext +<:bbmessage:511950877733552138> +``` + +The long number you see, in this case `511950877733552138`, is the emoji's ID. + +#### Webhook ID + +Once a [webhook](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) is created, the ID is found in the penultimate part of the URL. +For example, in the following URL, `661995360146817053` is the ID of the webhook. + +```plaintext +https://discordapp.com/api/webhooks/661995360146817053/t-9mI2VehOGcPuPS_F8R-6mB258Ob6K7ifhtoxerCvWyM9VEQug-anUr4hCHzdbhzfbz +``` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md new file mode 100644 index 00000000..4ff98095 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md @@ -0,0 +1,119 @@ +--- +title: Contributing to Sir Lancebot +description: A guide to setting up and configuring Sir Lancebot. +icon: fab fa-github +toc: 1 +--- + +> Before contributing, please ensure you read the [contributing guidelines](../contributing-guidelines) in full. + +--- +# Requirements +- [Python 3.8](https://www.python.org/downloads/) +- [Pipenv](https://github.com/pypa/pipenv/blob/master/docs/install.rst#-installing-pipenv) +- [Git](https://git-scm.com/downloads) + - [Windows Installer](https://git-scm.com/download/win) + - [MacOS Installer](https://git-scm.com/download/mac) or `brew install git` + - [Linux](https://git-scm.com/download/linux) + +## Using Docker +Sir Lancebot can be started using Docker. Using Docker is generally recommended (but not strictly required) because it abstracts away some additional set up work. + +The requirements for Docker are: + +* [Docker CE](https://docs.docker.com/install/) +* [Docker Compose](https://docs.docker.com/compose/install/) + * `pip install docker-compose` + * This is only a required step for linux. Docker comes bundled with docker-compose on Mac OS and Windows. + +--- + +# Fork the Project +You will need your own remote (online) copy of the project repository, known as a *fork*. + +- [**Learn how to create a fork of the repository here.**](../forking-repository) + +You will do all your work in the fork rather than directly in the main repository. + +--- + +# Development Environment +1. Once you have your fork, you will need to [**clone the repository to your computer**](../cloning-repository). +2. After cloning, proceed to [**install the project's dependencies**](../installing-project-dependencies). (This is not required if using Docker) + +--- +# Test Server and Bot Account + +You will need your own test server and bot account on Discord to test your changes to the bot. + +1. [**Create a test server**](../setting-test-server-and-bot-account#setting-up-a-test-server). +2. [**Create a bot account**](../setting-test-server-and-bot-account#setting-up-a-bot-account) and invite it to the server you just created. +3. Create the following text channels: + * `#announcements` + * `#dev-log` + * `#sir-lancebot-commands` +4. Create the following roles: + * `@Admin` +5. Note down the IDs for your server, as well as any channels and roles created. + * [**Learn how to obtain the ID of a server, channel or role here.**](../setting-test-server-and-bot-account#obtain-the-ids) + +--- + +## Environment variables +You will have to setup environment variables: + +* [**Learn how to set environment variables here.**](../configure-environment-variables) + +The following variables are needed for running Sir Lancebot: + +| Environment Variable | Description | +| -------- | -------- | +| `BOT_TOKEN` | Bot Token from the [Discord developer portal](https://discord.com/developers/applications) | +| `BOT_GUILD` | ID of the Discord Server | +| `BOT_ADMIN_ROLE_ID` | ID of the role `@Admins` | +| `ROLE_HELPERS` | ID of the role `@Helpers` | +| `CHANNEL_ANNOUNCEMENTS` | ID of the `#announcements` channel | +| `CHANNEL_DEVLOG` | ID of the `#dev-log` channel | +| `CHANNEL_COMMUNITY_BOT_COMMANDS` | ID of the `#sir-lancebot-commands` channel | + +[**Full environment variable reference for this project.**](./env-var-reference) + +--- + +While not required, we advise you set `USE_FAKEREDIS` to `true` in development to avoid the need of setting up a Redis server. +It does mean you may lose persistent data on restart but this is non-critical. +Otherwise, please see the below linked guide for Redis related variables. +{: .notification .is-warning } + +--- +# Run the project +The sections below describe the two ways you can run this project. We recomend Docker as it requires less setup. + +## Run with Docker +Make sure to have Docker running, then use the Docker command `docker-compose up` in the project root. +The first time you run this command, it may take a few minutes while Docker downloads and installs Sir Lancebot's dependencies. + +```shell +$ docker-compose up +``` + +If you get any Docker related errors, reference the [Possible Issues](./docker/possible-issues) section of the Docker page. +{: .notification .is-warning } + +## Run on the host +After installing project dependencies use the pipenv command `pipenv run start` in the project root. + +```shell +$ pipenv run start +``` + +--- + +# Working with Git +Now that you have everything setup, it is finally time to make changes to the bot! If you have not yet [read the contributing guidelines](https://github.com/python-discord/sir-lancebot/blob/main/CONTRIBUTING.md), now is a good time. Contributions that do not adhere to the guidelines may be rejected. + +Notably, version control of our projects is done using Git and Github. It can be intimidating at first, so feel free to ask for any help in the server. + +[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/) + +Have fun! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml new file mode 100644 index 00000000..349e6149 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml @@ -0,0 +1,2 @@ +title: Contributing to Sir Lancebot +description: A guide to setting up and configuring Sir Lancebot. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md new file mode 100644 index 00000000..066b703e --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md @@ -0,0 +1,68 @@ +--- +title: Sir-Lancebot Environment Variable Reference +description: The full environment variable reference for Sir-Lancebot. +toc: 2 +--- +## General Variables +The following variables are needed for running Sir Lancebot: + +| Environment Variable | Description | +| -------- | -------- | +| `BOT_TOKEN` | Bot Token from the [Discord developer portal](https://discord.com/developers/applications) | +| `BOT_GUILD` | ID of the Discord Server | +| `BOT_ADMIN_ROLE_ID` | ID of the role @Admins | +| `ROLE_HELPERS` | ID of the role @Helpers | +| `CHANNEL_ANNOUNCEMENTS` | ID of the #announcements channel | +| `CHANNEL_DEVLOG` | ID of the #dev-log channel | +| `CHANNEL_COMMUNITY_BOT_COMMANDS` | ID of the #sir-lancebot-commands channel | + +--- +## Debug Variables +Additionally, you may find the following environment variables useful during development: + +| Environment Variable | Description | +| -------- | -------- | +| `BOT_DEBUG` | Debug mode of the bot | False | +| `PREFIX` | The bot's invocation prefix | `.` | +| `CYCLE_FREQUENCY` | Amount of days between cycling server icon | 3 | +| `MONTH_OVERRIDE` | Interger in range `[0, 12]`, overrides current month w.r.t. seasonal decorators | +| `REDIS_HOST` | The address to connect to for the Redis database. | +| `REDIS_PORT` | | +| `REDIS_PASSWORD` | | +| `USE_FAKEREDIS` | If the FakeRedis module should be used. Set this to true if you don't have a Redis database setup. | +| `BOT_SENTRY_DSN` | The DSN of the sentry monitor. | +| `TRASHCAN_EMOJI` | The emoji to use for the trashcan during paginated embeds | + + +--- +## Tokens/APIs +If you will be working with an external service, you might have to set one of these tokens: + +| Token | Description | +| -------- | -------- | +| `GITHUB_TOKEN` | Personal access token for GitHub, raises rate limits from 60 to 5000 requests per hour. | +| `GIPHY_TOKEN` | Required for API access. [Docs](https://developers.giphy.com/docs/api) | +| `OMDB_API_KEY` | Required for API access. [Docs](http://www.omdbapi.com/) | +| `YOUTUBE_API_KEY` | An OAuth Key or Token are required for API access. [Docs](https://developers.google.com/youtube/v3/docs#calling-the-api) | +| `TMDB_API_KEY` | Required for API access. [Docs](https://developers.themoviedb.org/3/getting-started/introduction) | +| `NASA_API_KEY` | Required for API access. [Docs](https://api.nasa.gov/) | +| `IGDB_API_KEY` | Required for API access. A Twitch account is needed. [Docs](https://api-docs.igdb.com/#about) | +| `WOLFRAM_API_KEY` | | +| `UNSPLASH_KEY` | Required for API access. Use the `access_token` given by Unsplash. [Docs](https://unsplash.com/documentation) | + +--- +## Seasonal Cogs +These variables might come in handy while working on certain cogs: + +| Cog | Environment Variable | Description | +| -------- | -------- | -------- | +| Advent of Code | `AOC_LEADERBOARDS` | List of leaderboards seperated by `::`. Each entry should have an `id,session cookie,join code` seperated by commas in that order. | +| Advent of Code | `AOC_STAFF_LEADERBOARD_ID` | Integer ID of the staff leaderboard. | +| Advent of Code | `AOC_ROLE_ID` | ID of the advent of code role. +| Advent of Code | `AOC_IGNORED_DAYS` | Comma seperated list of days to ignore while calulating score. | +| Advent of Code | `AOC_YEAR` | Debug variable to change the year used for AoC. | +| Advent of Code | `AOC_CHANNEL_ID` | The ID of the #advent-of-code channel | +| Advent of Code | `AOC_FALLBACK_SESSION` | | +| Valentines | `LOVEFEST_ROLE_ID` | | +| Wolfram | `WOLFRAM_USER_LIMIT_DAY` | | +| Wolfram | `WOLFRAM_GUILD_LIMIT_DAY` | | diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md new file mode 100644 index 00000000..75d27d99 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md @@ -0,0 +1,144 @@ +--- +title: Contributing to Site +description: A guide to setting up and configuring Site. +icon: fab fa-github +toc: 1 +--- + +# Requirements + +- [Python 3.8](https://www.python.org/downloads/) +- [Pipenv](https://github.com/pypa/pipenv#installation) + - `pip install pipenv` +- [Git](https://git-scm.com/downloads) + - [Windows](https://git-scm.com/download/win) + - [MacOS](https://git-scm.com/download/mac) or `brew install git` + - [Linux](https://git-scm.com/download/linux) + +Using Docker (recommended): + +- [Docker CE](https://docs.docker.com/install/) +- [Docker Compose](https://docs.docker.com/compose/install/) + - `pip install docker-compose` + +Without Docker: + +- [PostgreSQL](https://www.postgresql.org/download/) + - Note that if you wish, the webserver can run on the host and still use Docker for PostgreSQL. + +--- +# Fork the project + +You will need access to a copy of the git repository of your own that will allow you to edit the code and push your commits to. +Creating a copy of a repository under your own account is called a _fork_. + +- [Learn how to create a fork of the repository here.](../forking-repository/) + +This is where all your changes and commits will be pushed to, and from where your PRs will originate from. + +For any Core Developers, since you have write permissions already to the original repository, you can just create a feature branch to push your commits to instead. + +--- +# Development environment + +1. [Clone your fork to a local project directory](../cloning-repository/) +2. [Install the project's dependencies](../installing-project-dependencies/) +3. [Prepare your hosts file](../hosts-file/) + +## Without Docker + +Some additional steps are needed when not using Docker. Docker abstracts away these steps which is why using it is generally recommended. + +### 1. PostgreSQL setup + +Enter psql, a terminal-based front-end to PostgreSQL: + +```shell +psql -qd postgres +``` + +Run the following queries to create the user and database: + +```sql +CREATE USER pysite WITH SUPERUSER PASSWORD 'pysite'; +CREATE DATABASE pysite WITH OWNER pysite; +``` + +Finally, enter `/q` to exit psql. + +### 2. Environment variables + +These contain various settings used by the website. To learn how to set environment variables, read [this page](../configure-environment-variables/) first. + +```shell +DATABASE_URL=postgres://pysite:pysite@localhost:7777/pysite +METRICITY_DB_URL=postgres://pysite:pysite@localhost:7777/metricity +DEBUG=1 +SECRET_KEY=suitable-for-development-only +STATIC_ROOT=staticfiles +``` + +#### Notes regarding `DATABASE_URL` + +- If the database is hosted locally i.e. on the same machine as the webserver, then use `localhost` for the host. Windows and macOS users may need to use the [Docker host IP](../hosts-file/#windows) instead. +- If the database is running in Docker, use port `7777`. Otherwise, use `5432` as that is the default port used by PostegreSQL. +- If you configured PostgreSQL in a different manner or you are not hosting it locally, then you will need to determine the correct host and port yourself. +The user, password, and database name should all still be `pysite` unless you deviated from the setup instructions in the previous section. + +--- +# Run the project + +The project can be started with Docker or by running it directly on your system. + +## Run with Docker + +Start the containers using Docker Compose: + +```shell +docker-compose up +``` + +The `-d` option can be appended to the command to run in detached mode. This runs the containers in the background so the current terminal session is available for use with other things. + +If you get any Docker related errors, reference the [Possible Issues](https://pythondiscord.com/pages/contributing/docker/#possible-issues") section of the Docker page. +{: .notification .is-warning } + +## Run on the host + +Running on the host is particularily useful if you wish to debug the site. The [environment variables](#2-environment-variables) shown in a previous section need to have been configured. + +### Database + +First, start the PostgreSQL database. +Note that this can still be done with Docker even if the webserver will be running on the host - simply adjust the `DATABASE_URL` environment variable accordingly. + +If you chose to use Docker for just the database, use Docker Compose to start the container: + +```shell +docker-compose up postgres +``` + +If you're not using Docker, then use [pg_ctl](https://www.postgresql.org/docs/current/app-pg-ctl.html) or your system's service manager if PostgreSQL isn't already running. + +### Webserver + +Starting the webserver is done simply through pipenv: + +```shell +pipenv run start +``` + +--- +# Working on the project + +The development environment will watch for code changes in your project directory and will restart the server when a module has been edited automatically. +Unless you are editing the Dockerfile or docker-compose.yml, you shouldn't need to manually restart the container during a developing session. + +[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/) + +--- +# Django admin site + +Django provides an interface for administration with which you can view and edit the models among other things. + +It can be found at [http://admin.pythondiscord.local:8000](http://admin.pythondiscord.local:8000). The default credentials are `admin` for the username and `admin` for the password. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md new file mode 100644 index 00000000..f9962990 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md @@ -0,0 +1,211 @@ +--- +title: Style Guide +description: Coding conventions for our Open Source projects. +icon: fab fa-python +--- + +> A style guide is about consistency. +> Consistency with this style guide is important. +> Consistency within a project is more important. +> Consistency within one module or function is the most important. + +> However, know when to be inconsistent -- sometimes style guide recommendations just aren't applicable. +> When in doubt, use your best judgment. Look at other examples and decide what looks best. And don't hesitate to ask! + +> — [PEP 8, the general Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/) + +All of our projects have a certain project-wide style that contributions should attempt to maintain consistency with. +During PR review, it's not unusual for style adjustment requests to be commented. + +We've added below a guideline to aid new contributors, allowing them to refer to it during development, to help get more familiar and to hopefully lessen some of the frustrations that come from first-time contributions. + +Anything that isn't defined below falls back onto the [PEP 8 guidelines](https://www.python.org/dev/peps/pep-0008/), so be sure to reference it also. + +# Code Structure +## Maximum Line Length +Each project has specified their respective maximum line lengths. +Generally, we try to keep this at 100 or 120 characters, making our length longer than the typical 79 characters. + +Most IDEs and smarter editors will use the lint settings we store in the project's `tox.ini` or `.flake8` file after you install the appropriate development packages, so should conflict with our suggested project rules. +If your editor does not have this ability but instead requires setting it manually, make sure to change it to the appropriate length specified in these files. + +## Line Breaks +Avoid breaking a line far earlier than necessary, such as: + +```py +array = [ # there was plenty of room on this line + 1, 2, 3, + 4, 5, 6 +] +``` + +Try instead to make use of the space you're allowed to use appropriately: +```py +array = [1, 2, 3, 4, 5, 6] +``` + +Any line continuations must be indented a full level, i.e. 4 spaces. So don't do: +```py +def an_example_function_definition_that_is_kinda_long( + variable_name_of_the_first_positional_argument, # only 2 spaces on the indent + variable_name_of_the_second_positional_argument # same here +) +``` + +Do instead: +```py +def an_example_function_definition_that_is_kinda_long( + variable_name_of_the_first_positional_argument, + variable_name_of_the_second_positional_argument +) +``` + +### Bracket and Item Arrangement +In the case where items contained in brackets need to be broken across multiple lines, items should be dropped to a new line after the opening bracket with an additional level of indentation. +The closing bracket ends on it's own new line, on the same indentation level as the opening bracket. + +Avoid doing: +```py +def long_function_name_that_is_taking_up_too_much_space(var_one, var_two, var_three, # didn't drop a line after the brackets + var_four, var_five, var_six, + var_seven, var_eight): + print(var_one) +``` +```py +def long_function_name_that_is_taking_up_too_much_space( + var_one, + var_two, + var_three, + var_four, + var_five, + var_six, + var_seven, + var_eight): # didn't drop the closing bracket to a new line + print(var_one) +``` + +Instead the correct style is: +```py +def long_function_name_that_is_taking_up_too_much_space( + var_one, + var_two, + var_three, + var_four, + var_five, + var_six, + var_seven, + var_eight +): + print(var_one) +``` + +## Imports +Our projects require correctly ordering imports based on the pycharm import order rules. +If you use Pycharm as your main IDE, you can also use the `CTRL+ALT+O` shortcut to automatically reorder your imports to the correct style. + +There's three groups of imports which are defined in the following order: + +- Standard library +- 3rd party +- Local + +Each group must be ordered alphabetically, with uppercase modules coming before lowercase. +```py +from packagename import A, Z, c, e +``` + +Direct imports must be distinct, so you cannot do: +```py +import os, sys +``` +Instead do: +```py +import os +import sys +``` + +Absolute referencing for local project modules are preferenced over relative imports. + +Wildcard imports should be avoided. + +# Strings +## Quote Marks +Preference is to use double-quotes (`"`) wherever possible. +Single quotes should only be used for cases where it is logical. +Exceptions might include: + +- using a key string within an f-string: `f"Today is {data['day']}"`. +- using double quotes within a string: `'She said "oh dear" in response'` + +Docstrings must use triple double quotes (`"""`). + +## Docstrings +All public methods and functions should have docstrings defined. + +### Line Structure +Single-line docstrings can have the quotes on the same line: +```py +def add(a, b): + """Add two arguments together.""" + return a + b +``` + +Docstrings that require multiple lines instead keep both sets of triple quotes on their own lines: +```py +def exponent(base, exponent=2): + """ + Calculate the base raised to the exponents power. + + Default is 2 due to a squared base being the most common usage at this time. + """ + return a ** b +``` + +### Spacing +Functions and methods should not have an extra empty newline after the docstring. +```py +def greeting(name): + """Build a greeting string using the given name.""" + return f"Welcome, {name}" +``` + +Class docstrings do require an extra newline. +```py +class SecretStuffCog(commands.Cog): + """Handle the secret commands that must never been known.""" + + def __init__(self, bot): + ... +``` + +### Mood +Imperative mood and present tense usage is preferenced when writing docstrings. + +Imperative mood is a certain grammatical form of writing that expresses a clear command to do something. + +**Use:** "Build an information embed."<br> +**Don't use:** "Returns an embed containing information." + +Present tense defines that the work being done is now, in the present, rather than in the past or future. + +**Use:** "Build an information embed."<br> +**Don't use:** "Built an information embed." or "Will build an information embed." + +# Type Annotations +Functions are required to have type annotations as per the style defined in [PEP 484](https://www.python.org/dev/peps/pep-0484/). + +A function without annotations might look like: +```py +def divide(a, b): + """Divide the two given arguments.""" + return a / b +``` + +With annotations, the arguments and the function are annotated with their respective types: +```py +def divide(a: int, b: int) -> float: + """Divide the two given arguments.""" + return a / b +``` + +In previous examples, we have purposely omitted annotations to keep focus on the specific points they represent. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md new file mode 100644 index 00000000..26c89b56 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md @@ -0,0 +1,23 @@ +--- +title: Working with Git +description: Basic workflows when using git. +icon: fab fa-git-alt +--- + +Working with git can be daunting, but it is a powerful tool for collaboration and version control. +Below are links to regular workflows for working with Git using PyCharm or the CLI. + +> **What's the difference?**<br> +> The integrated Git tool built into PyCharm offers a more visual and abstract way to use Git to manage your files.<br> +> However, the CLI offers more minute control and functionality compared to the GUI, which may not always do exactly what you want. + +* [**Working with Git in PyCharm**](./pycharm) +* [**Working with the Git CLI**](./cli) + +--- + +**Resources to learn Git** + +* [The Git Book](https://git-scm.com/book) +* [Corey Schafer's Youtube Tutorials](https://www.youtube.com/watch?v=HVsySz-h9r4&list=PL-osiE80TeTuRUfjRe54Eea17-YfnOOAx) +* [GitHub Git Resources Portal](https://try.github.io/) diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml new file mode 100644 index 00000000..68ef3fd6 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml @@ -0,0 +1,3 @@ +title: Working with Git +description: Basic workflows when using git. +icon: fab fa-git-alt diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md new file mode 100644 index 00000000..5f196837 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md @@ -0,0 +1,121 @@ +--- +title: Working with the Git CLI +description: Basic workflow when using the git CLI. +toc: 2 +--- + +This is the basic workflow when working with Git with CLI. For the PyCharm version of the guide, [**click here**](../pycharm). +The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. + +> **Note:** This is a guide only meant to get you started with git. For in-depth resources, check the [**Working with Git**](..) page. + +--- + +## Adding the Upstream Remote +Adding a *remote* to the main GitHub repository you forked off will allow you to later update your fork with changes from the main repository. + +Generally, a *remote* designates a repository that is on GitHub or another external location rather than on your computer. +The `origin` remote will refer to your fork on GitHub. The `upstream` remote will refer to the main repository on GitHub. +```sh +$ git remote add upstream https://github.com/python-discord/sir-lancebot.git +``` +If you use SSH, use `[email protected]:python-discord/sir-lancebot.git` for the upstream URL instead. + +--- + +## Creating a New Branch +You will be committing your changes to a new branch rather than to `main`. +Using branches allows you to work on muiltiple pull requests without conflicts. + +You can name your branch whatever you want, but it's recommended to name it something succint and relevant to the changes you will be making. + +Run the following commands to create a new branch. Replace `branch_name` with the name you wish to give your branch. +```sh +$ git fetch --all +... +$ git checkout --no-track -b branch_name upstream/main +``` + +--- + +## Staging Changes +Files in git can be in one of four different states: + +- *Staged*: These files have been modified and will be committed. +- *Unstaged*: These files were already present but have been modified. +- *Untracked*: These files are new to the repository. +- *Ignored*: Specified in a `.gitignore` file in the project root, these files will never be committed, remaining only on your computer. + +As you can see, only staged files will end up being committed. +You can get an overview of this using `git status`. +If you wish to commit unstaged or untracked files, you will need to add them with `git add` first. +```sh +# Add files individually +$ git add path/to/file.py path/to/other/file.py + +# Add all unstaged and untracked files in a directory +$ git add path/to/directory + +# Add all unstaged and untracked files in the project +$ git add . + +# Add all tracked and modified files in the project +$ git add -u + +# Unstage a file +$ git reset -- path/to/file.py +``` + +--- + +## Discarding Changes +Be careful, these operations are **irreversible**! +```sh +# Discard changes to an unstaged file +$ git checkout -- path/to/file.py + +# Discard ALL uncommitted changes +$ git reset --hard HEAD +``` + +--- + +## Committing Changes +The basic command for committing staged changes is `git commit`. All commits must have a message attached to them. +```sh +# Commit staged changes and open your default editor to write the commit message +$ git commit + +# Specify the message directly +$ git commit -m "Turn pride avatar into an embed" + +# Commit all staged and unstaged changes. This will NOT commit untracked files +$ git commit -a -m "Update d.py documentation link" +``` + +--- + +## Pushing Commits +Commits remain local (ie. only on your computer) until they are pushed to the remote repository (ie. GitHub). + +The first time you push on your new branch, you'll need to set the upstream when you push: +```sh +$ git push -u origin branch_name +``` +Any subsequent pushes can be done with just `git push`. + +--- + +## Pulling Changes +Sometimes you want to update your repository with changes from GitHub. +This could be the case if you were working on the pull request on two different computers and one of them has an outdated local repository. + +You can pull the changes from GitHub with: +```sh +$ git pull +``` +You can also pull changes from other branches such as from branch `main` in `upstream`: +```sh +$ git pull upstream main +``` +This should generally only be needed if there are [merge conflicts](https://help.github.com/en/articles/about-merge-conflicts) that you need to resolve manually. Conflicts arise when you change the same code that someone else has changed and pushed since you last updated your local repository. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md new file mode 100644 index 00000000..3f7fefa0 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md @@ -0,0 +1,67 @@ +--- +title: Working with Git in PyCharm +description: Basic workflow when using git in PyCharm. +toc: 2 +--- + +This is the basic workflow when working with Git with PyCharm. For the CLI version of the guide, [**click here**](../cli). +The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. + +> **Note:** This is a guide only meant to get you started with git. For in-depth resources, check the [**Working with Git**](wiki:/contributing/working-with-git/) page. + +--- + +## Adding the Upstream Remote +> Adding a *remote* to the main GitHub repository you forked off will allow you to later update your fork with changes from the main repository. + +> Generally, a *remote* designates a repository that is on GitHub or another external location rather than on your computer. The `origin` remote will refer to your fork on GitHub. The `upstream` remote will refer to the main repository on GitHub. + +1. In the menu bar, navigate to `Git` -> `Remotes...`.<br> + +2. In the popup menu, click the `+` icon, set `upstream` as the name, set the URL as the URL for the main repository on GitHub.<br> + +3. Click `OK`. + +--- + +## Creating a New Branch +> You will be committing your changes to a new branch rather than to `main`. Using branches allows you to work on multiple pull requests at the same time without conflicts. + +> You can name your branch whatever you want, but it's recommended to name it something succint and relevant to the changes you will be making. + +> Before making new branches, be sure to checkout the `main` branch and ensure it's up to date. + +1. In the bottom right corner, click on `main` and then click `New Branch`.<br> + + +--- + +## Committing Changes +After making changes to the project files, you can commit by clicking the commit button that's part of the Git actions available in the top right corner of your workspace: + + + +The flow of making a commit is as follows: + +1. Select the files you wish to commit. +2. Write a brief description of what your commit is. This is your *commit message*. +3. See the actual changes your commit will be making, and optionally tick/untick specific changes to only commit the changes you want. +4. Click `Commit`.<br> + + +--- + +## Pushing Changes +When you are ready to have your commits be available in your remote fork, navigate to `Git` -> `Push...`. +Select the commits you want to push, make sure the remote branch is your intended branch to push to, and click `Push`. + + + +--- + +## Pulling Changes +> Sometimes you want to update your repository with changes from GitHub. This could be the case if you were working on the pull request on two different computers and one of them has an outdated local repository. + +To do that, navigate to `Git` -> `Pull...`. From there, select the *remote* and the branches to pull from, then click `Pull`. + + diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md b/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md new file mode 100644 index 00000000..2a6e7781 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md @@ -0,0 +1,78 @@ +--- +title: Help Channels +description: How do help channels work in the Python Discord community? +icon: fab fa-discord +relevant_links: + Asking Good Questions: ../asking-good-questions + Role Guide: /pages/server-info/roles + Helping Others: ../helping-others +--- + +On the 5th of April 2020, we introduced a new help channel system at Python Discord. This article is a supplementary guide to explain precisely where to go to find help. + +We have two different kinds of help channels in our community - **Topical help channels**, and **general help channels**. +Where you should go depends on what you need help with. +These channels also attract different helpers, and move at different speeds, which affects the kind of help you're likely to receive, and how fast you get that help. + +# Topical Help Channels + +The topical help channels move at a slower pace than the general help channels. +They also sometimes attract domain experts - for example, `#async-and-concurrency` has CPython contributors who helped write asyncio, and in `#game-development` you can find the creators and maintainers of several game frameworks. +If your question fits into the domain of one of our topical help channels, and if you're not in a big hurry, then this is probably the best place to ask for help. + + + +Some of the topical help channels have a broad scope, so they can cover many (somewhat) related topics. +For example, `#data-science-and-ai` covers scientific Python, statistics, and machine learning, while `#algos-and-data-structs` covers everything from data structures and algorithms to maths. + +To help you navigate this, we've added a list of suggested topics in the topic of every channel. +If you're not sure where to post, feel free to ask us which channel is relevant for a topic in `#community-meta`. + +# General Help Channels + +Our general help channels move at a fast pace, and attract a far more diverse spectrum of helpers. +This is a great choice for a generic Python question, and a good choice if you need an answer as soon as possible. +It's particularly important to [ask good questions](..guides/asking-good-questions) when asking in these channels, or you risk not getting an answer and having your help channel be claimed by someone else. + +## How To Claim a Channel + +There are always 3 available help channels waiting to be claimed in the **Python Help: Available** category. + + + +In order to claim one, simply start typing your question into one of these channels. Once your question has been posted, you have claimed this channel, and the channel will be moved down to the **Python Help: Occupied** category. + +If you're unable to type into these channels, this means you're currently **on cooldown**. In order to prevent someone from claiming all the channels for themselves, **we only allow someone to claim a new help channel every 15 minutes**. However, if you close your help channel using the `!dormant` command, this cooldown is reset early. + + +*This message is always posted when a channel becomes available for use.* + +## Q: For how long is the channel mine? + +The channel is yours until it has been inactive for **30 minutes**. When this happens, we move the channel down to the **Python Help: Dormant** category, and make the channel read-only. After a while, the channel will be rotated back into **Python Help: Available** for the next question. Please try to resist the urge to continue bumping the channel so that it never gets marked as inactive. If nobody is answering your question, you should try to reformulate the question to increase your chances of getting help. + + +*You'll see this message in your channel when the channel is marked as inactive.* + +## Q: I don't need my help channel anymore, my question was answered. What do I do? + +Once you have finished with your help channel you or a staff member can run `!dormant`. This will move the channel to the **Python Help: Dormant** category where it will sit until it is returned to circulation. You will only be able to run the command if you claimed the channel from the available category, you cannot close channels belonging to others. + +## Q: Are only Helpers supposed to answer questions? + +Absolutely not. We strongly encourage all members of the community to help answer questions. If you'd like to help answer some questions, simply head over to one of the help channels that are currently in use. These can be found in the **Python Help: Occupied** category. + + + +Anyone can type in these channels, and users who are particularly helpful [may be offered a chance to join the staff on Python Discord](/pages/server-info/roles/#note-regarding-staff-roles). + +## Q: I lost my help channel! + +No need to panic. +Your channel was probably just marked as dormant. +All the dormant help channels are still available at the bottom of the channel list, in the **Python Help: Dormant** category, and also through search. +If you're not sure what the name of your help channel was, you can easily find it by using the Discord Search feature. +Try searching for `from:<your nickname>` to find the last messages sent by yourself, and from there you will be able to jump directly into the channel by pressing the Jump button on your message. + + +*The dormant help channels can be found at the bottom of the channel list.* diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md b/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md new file mode 100644 index 00000000..d126707d --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md @@ -0,0 +1,140 @@ +--- +title: Helping Others +description: The staff's take on how to help others in our community. +icon: fab fa-discord +relevant_links: + Asking Good Questions: ../asking-good-questions + Help Channel Guide: ../help-channel-guide + Code of Conduct: /pages/code-of-conduct/ +toc: 2 +--- + +Python Discord has a lot of people asking questions, be it in the help channels, topical channels, or any other part of the server. +Therefore, you might sometimes want to give people the answers you have in mind. +But you might not be sure how best to approach the issue, or maybe you'd like to see how others handle it. +This article aims to present a few of the general principles which guide the staff on a day-to-day basis on the server. + +## Understanding the Problem + +Some people are good at asking for help. +They might be able to present their problem accurately and concisely, in which case discussing the problem and giving tips is simple and straight-forward. +But not everyone might be able to do that for their current issue. +Maybe they just don't know what they don't know. + +If you feel like there's a gap in your understanding of the problem, it's often a good first step to query the asker for more information. Some of this information might be: + +* More code +* The way in which the code doesn't work (if that is the issue), be it an exception message with traceback, undesired output, etc. +* You can sometimes infer what the problem is yourself by requesting short examples of the desired output for specific input. + +At this point, it's probably better being safe than sorry. +You don't want to accidentally pursue a direction that isn't even related to the real issue, as it could lead to frustration on both sides. +Beginners especially can be prone to asking a question which presents their attempt at solving the problem, instead of presenting the problem itself. +This is often called an [XY problem](https://xyproblem.info/). + +Even if eventually you can't help, simply clarifying what the problem is can help others join in, and give their input. + +> #### Example 1: +> A person might ask: *"How do I look at the values inside my function from outside?"* +> +> What they might be asking is: *"How do I return a value from a function?"* + + +## Understanding the Helpee + +Assuming you know what the problem is, it's vital to gauge the level of knowledge of the person asking the question. +There's a stark difference between explaining how to do something to someone experienced, who only lacks knowledge on the specific topic; and someone who is still learning the basics of the language. + +Try adjusting the solutions you present and their complexity accordingly. +Teaching new concepts allows the helpee to learn, but presenting too complex of a solution might overwhelm them, and the help session might not achieve its purpose. + +> #### Example 2: +> A user might ask how to count how often each word appears in a given text. +> You might lean towards solving it using `collections.Counter`, but is it really the right approach? +> If the user doesn't know yet how to update and access a dictionary, it might be better to start there. + +Generally, you should consider what approach will bring the most value to the person seeking help, instead of what is the most optimal, or "right" solution. + +Usually, understanding a person's level can be achieved after a short conversation (such as trying to understand the problem), or simply by seeing the person's code and what they need help with. +At other times, it might not be as obvious, and it might be a good idea to kindly inquire about the person's experience with the language and programming in general. + + +## Teach a Man to Fish... + +The path is often more important than the answer. +Your goal should primarily be to allow the helpee to apply, at least to a degree, the concepts you introduce in your answer. +Otherwise, they might keep struggling with the same problem over and over again. +That means that simply showing your answer might close the help channel for the moment, but won't be very helpful in the long-term. + +A common approach is to walk the helpee through to an answer: + +* Break the task into smaller parts that will be easier to handle, and present them step by step. + Try to think of the order of the steps you yourself would take to reach the solution, and the concepts they need to understand for each of those steps. + If one step requires the helpee to understand several new concepts, break it down further. +* Ask instructive questions that might help the person think in the right direction. + +> #### Example 3: +> +> **user**: "Hey, how can I create a sudoku solver?"<br> +> *helper1 proceeds to paste 40 lines of sudoku solving code*<br> +> **helper2**: "Are you familiar with lists / recursion / backtracking?"<br> +> *helper2 proceeds to give the information the user lacks* +> +> With the first replier, there's a much smaller chance of the helpee understanding how the problem was solved, and gaining new tools for future projects. +> It's much more helpful in the long run to explain the new concepts or guide them to resources where they can learn. +> +> This is also an example of gauging the level of the person you're talking to. +> You can't properly help if you don't know what they already learned. +> If they don't know recursion, it's going to take a slower, more detailed approach if you try to explain backtracking to them. +> Likewise if they're not familiar with lists. +> +> The same applies to presenting external resources. +> If they only just started programming, pasting a link to the Python documentation is going to be of little help. +> They are unlikely to be able to get around that website and understand what you expect them to read. In contrast, for a more seasoned programmer, the docs might be just what they need. + + +## Add a Grain of Salt + +Giving people more information is generally a good thing, but it's important to remember that the person helped is trying to soak in as much as they can. +Too much irrelevant information can confuse them, and make them lose sight of the actual solution. +Presenting a solution that is considered a bad practice might be useful in certain situations, but it's important to make sure they are aware it's not a good solution, so they don't start using it themselves. + +> #### Example 4: +> +> **user**: "How can I print all elements in a list?"<br> +> **helper1**: "You can do it like so:"<br> +> +> for element in your_list: +> print(element) +> +> **helper2**: "You can also do it like this:"<br> +> +> for i in range(len(your_list)): +> print(your_list[i]) +> +> The second replier gave a valid solution, but it's important that he clarifies that it is concidered a bad practice in Python, and that the first solution should usually be used in this case. + + +## It's OK to Step Away + +Sometimes you might discover you don't have the best chemistry with the person you're talking to. +Maybe there's a language barrier you don't manage to overcome. +In other cases, you might find yourself getting impatient or sarcastic, maybe because you already answered the question being asked three times in the past hour. + +That's OK- remember you can step away at any time and let others take over. +You're here helping others on your own free time (and we really appreciate it!), and have no quotas to fill. + +At other times, you might start talking with someone and realize you're not experienced in the topic they need help with. +There's nothing wrong with admitting you lack the specific knowledge required in this case, and wishing them good luck. +We can't know everything. + +Remember that helping means thinking of what's best for them, and we also wouldn't want to see you become agitated. +We're all here because we enjoy doing this. + + +## Respect Others Giving Help + +You might sometimes see others giving help and guiding others to an answer. +Throwing in additional ideas is great, but please remember both teaching and learning takes concentration, and you stepping in might break it. +You might have another idea to suggest, but you see that there's already a person helping, and that they're handling the situation. +In that case, it might be a good idea to patiently observe, and wait for a good opportunity to join in so as to not be disruptive. 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..716250b1 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md @@ -0,0 +1,217 @@ +--- +title: How to Contribute a Page +description: Learn how to write and publish a page to this website. +icon: fas fa-info +relevant_links: + Contributing to Site: https://pythondiscord.com/pages/contributing/site/ + Using Git: https://pythondiscord.com/pages/contributing/working-with-git/ +toc: 4 +--- + +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`. + +#### Page Categories +Nested folders represent page categories on the website. Each folder under the root folder must include a `_info.yml` file with the following: + +```yml +title: Category name +description: Category description +icon: fas fa-folder # Optional +``` + +All the markdown files in this folder will then be under this category. + +#### Having the Category Also Be a Page +In order to make categories a page, just create a page **with the same name as the category folder** in the category's parent directory. + +```plaintext +guides +├── contributing.md +├── contributing +│ ├── _info.yml +│ └── bot.md +└── _info.yml +``` + +In the above example, `www.pythondiscord.com/guides/` will list `Contributing` as a category entry with information from `contributing/_info.yml`. + +However, `www.pythondiscord.com/guides/contributing` will render `contributing.md` rather than show the category contents. +A dropdown menu will be automatically generated in the top right corner of the page listing the children of the category page. + +Therefore, `www.pythondiscord.com/guides/contributing/bot` will then render `bot.md`, with backlinks to `contributing.md`. + +## 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: fas 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:** Icon for the category entry for the page. Default: `fab 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. +- **toc:** A number representing the smallest heading tag to show in the table of contents. + See: [Table of Contents](#table-of-contents) + +## 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") +``` + +--- + +### HTML Attributes +To add HTML attributes to certain lines/paragraphs, [see this page](https://python-markdown.github.io/extensions/attr_list/#the-list) for the format and where to put it. + +This can be useful for setting the image size when adding an image using markdown (see the [Image Captions](#image-captions) section for an example), or for adding bulma styles to certain elements (like the warning notification [here](/pages/guides/pydis-guides/contributing/sir-lancebot#setup-instructions)).<br> +**This should be used sparingly, as it reduces readability and simplicity of the article.** + +--- + +### Image Captions +To add an image caption, place a sentence with italics *right below* the image link + +**Markdown:** +```nohighlight +{: width="400" } +*Summmer Code Jam 2020 banner with event information.* +``` + +**Output:** + +{: width="400"} +*Summer Code Jam 2020 banner with event information.* + +> Note: To display a regular italicized line below an image, leave an empty line between the two. + +--- + +### Table of Contents +In order to show the table of contents on a page, simply define the `toc` key in the page metadata. + +The value of the `toc` key corresponds to the smallest heading to list in the table of contents. +For example, with markdown content like this: + +```markdown +# Header 1 +words +### Header 3 +more words +# Another Header 1 +## Header 2 +even more words +``` + +and `toc: 2` in the page metadata, only `Header 1`, `Another Header 1` and `Header 2` will be listed in the table of contents. + +To use a custom label in the table of contents for a heading, set the `data-toc-label` attribute in the heading line. See [HTML Attributes](#html-attributes) for more information. + +```markdown +# Header 1 {: data-toc-label="Header One" } +``` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md b/pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md new file mode 100644 index 00000000..f8031834 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md @@ -0,0 +1,32 @@ +--- +title: Conversation Etiquette in Our Off-Topic Channels +description: Guidelines on conversation etiquette. +icon: fab fa-discord +--- + +## Why do we need off-topic etiquette? +Everyone wants to have good conversations in our off-topic channels, but with tens of thousands of members, this might mean different things to different people. +To facilitate the best experience for everyone, here are some guidelines on conversation etiquette. + +## Three things you shouldn't do +1. Don't interrupt active conversations + * There's three off-topic channels which can support three simultaneous conversations. + If one is active and you have something you'd like to discuss, try a different channel. +2. Don't post memes unless they're relevant to a conversation + * There are better places to share memes; if you have a meme you think is worth sharing, try to find a relevant subreddit, like [/r/ProgrammerHumor](https://www.reddit.com/r/ProgrammerHumor/). +3. Don't snap at people + * We are a large, diverse community. Different native languages, experiences, and ages mean miscommunications happen. Always try to assume the best in other community members. + +## Three things you should do +1. Ask away + * If you have a question that isn't about Python, just ask it in an inactive off-topic channel. + If someone sees your question who knows the answer, they will answer you. + "Why is my wifi not working?", "how do I tune a guitar?", "is there a server for C#?", are all fair game for questions to ask. + If your question relates to Python, try to find the most suitable channel to ask your question, or open a help session. +2. When in doubt, ask someone to clarify what they mean + * If you're not sure you properly understand someone, ask them to clarify. + Text isn't necessarily the easiest way for everyone to communicate, so it makes life easier if we're all on the same page. +3. Join in! + * The off-topic channels have lots of fun or interesting conversations; if someone is talking about something you're interested in, don't be scared to hop into the conversation. + +While you can discuss other topics than Python in the off-topic channels, the [ordinary rules](/pages/rules/) still apply. diff --git a/pydis_site/apps/content/resources/guides/python-guides/_info.yml b/pydis_site/apps/content/resources/guides/python-guides/_info.yml new file mode 100644 index 00000000..67730962 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/python-guides/_info.yml @@ -0,0 +1,3 @@ +title: Python Guides +description: Guides related to the Python Programming Language. +icon: fab fa-python diff --git a/pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md b/pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md new file mode 100644 index 00000000..356d63bd --- /dev/null +++ b/pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md @@ -0,0 +1,72 @@ +--- +title: Creating a Unix-style Python Environment on Windows +description: How to setup Python for Windows. +--- + +Many programmers use Linux or macOS operating systems for their work, though newcomers to programming will likely want to get started on the computer they already own, which will often be running Windows. +This guide will help you install Python on Windows. + +Programmers also need to become comfortable using a command prompt (also known as a terminal), and many guides for both beginning and advanced programming will often tell you certain commands to run. +The Windows command prompt has different names for similar commands that are available on Linux and macOS. +This guide will also help you set up a command prompt called Git Bash, which will support many of the commands available on Linux and macOS. + +## Installing Python +Python can be downloaded from the Python website on the [downloads page](https://www.python.org/downloads/). +The website will automatically present you with a download button for the latest release of the Windows version when you access the site from a Windows machine. + +Once the download is complete, you can begin the installation. +Select "Customize Installation". +The default settings for "Optional Features" are sufficient and you can click "Next". + +The next step is to decide on a location where the Python executable can be stored on your computer. +This should be a location that's easy for you to remember. +One possibility is to create a folder called "Python" at the root of your hard drive. +Once you have selected a location, you can click "Install", as no other settings on this screen need to be adjusted. +This will complete the installation. + +## Installing a text editor +You will also need a text editor for writing Python programs, and for subsequent steps of this guide. +Powerful programs called integrated development environments (IDEs) like PyCharm and Visual Studio Code contain text editors, but they also contain many other features with uses that aren't immediately obvious to new programmers. + +[Notepad++](https://notepad-plus-plus.org/) is a popular text editor for both beginners and advanced users who prefer a simpler interface. +Other editors we recommend can be found (https://pythondiscord.com/resources/tools/#editors)[here]. + +## Installing Git Bash +Git is a command line program that helps you keep track of changes to your code, among other things. +Many developers use it, and while you may not need it right away, it is useful to install it because it comes with Git Bash. +On the "Select Components" screen, no settings need to be changed. +The next screen will ask what text editor you want to use with Git. Vim is the default choice, though Vim is widely considered difficult to learn, so you may choose to select Notepad++ or whichever text editor you may have installed previously. + +For all remaining screens in the installation, the default selections are fine. + +## Configuring .bashrc +`.bashrc` is a file where we tell Git Bash where the Python executable is. +First, open Git Bash, and as your first command, type `echo ~` and hit enter. +This will most likely print `c/Users/YourUsername` to the terminal. +Navigate to this location in your file explorer, though keep in mind that Windows will display `c/Users/YourUsername` as `C:\Users\YourUsername`. +In this folder, there will be a file called `.bashrc`; open it with your text editor of choice. + +For this step, you will need to remember where you installed Python earlier. +In whichever folder that was, there is a file called `python.exe`; this is the executable that will run your Python programs. +Copy the full path of this file, starting from `C:`. +If you used the example location given earlier, it will be located at `C:\Python\python.exe`. + +In the `.bashrc` file, add a line to the end of the file saying `alias python='C:\\Python\\python.exe`, where `C:\\Python\\python.exe` is the location of your `python.exe` file, but each folder is separated by two backslashes instead of one. +The two backslashes are because a single backslash is used as an [escape character](https://en.wikipedia.org/wiki/Escape_character). +Save the file, and then type `source ~/.bashrc` to activate the change you have made. + +Finally, enter `python -c 'import sys; print(sys.executable)'` into Git Bash. +(If you attempt to copy and paste this into the terminal using Ctrl+V, it might not work, though Shift+Insert will.) +If all the steps have been followed correctly, this will print the location of your `python.exe` file and demonstrate that your environment is set up correctly. +You can hereafter use the `python` command in Git Bash to run any Python program that you write. + +## Running a test program +At any location on your computer, create a file named `hello.py` and open it with your text editor. +The program need only be one line: `print('Hello world!')`. +Save this file. + +To run this program in Git Bash, navigate to where it is saved on your hard drive. +If you know the path to this location, you can use the `cd` command ("cd" stands for "change directory") to navigate to it. +If it's saved to your desktop, `cd /c/Users/YourUsername/Desktop` will take you there. +Otherwise if you have the directory open in your file explorer, you can right click anywhere in the white space of the file explorer window (not on top of a file) and select "Git Bash Here". +Once you're there, type `python hello.py`, and the program will run. diff --git a/pydis_site/apps/content/resources/guides/python-guides/discordpy.md b/pydis_site/apps/content/resources/guides/python-guides/discordpy.md new file mode 100644 index 00000000..b0b2fad1 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/python-guides/discordpy.md @@ -0,0 +1,230 @@ +--- +title: Discord.py Learning Guide +description: A learning guide for the discord.py bot framework written by members of our community. +icon: fab fa-python +toc: 2 +--- + +<!-- discord.py Badge --> +<a href="https://github.com/Rapptz/discord.py/"> + <div class="tags has-addons"> + <span class="tag is-dark">discord.py</span><span class="tag is-info">≥1.0</span> + </div> +</a> + +Interest in creating a Discord bot is a common introduction to the world of programming in our community. + +Using it as your first project in programming while trying to learn is a double-edged sword. +A large number of concepts need to be understood before becoming proficient at creating a bot, making the journey of learning and completing the project more arduous than more simple projects designed specifically for beginners. +However in return, you get the opportunity to expose yourself to many more aspects of Python than you normally would and so it can be an amazingly rewarding experience when you finally reach your goal. + +Another excellent aspect of building bots is that it has a huge scope as to what you can do with it, almost only limited by your own imagination. +This means you can continue to learn and apply more advanced concepts as you grow as a programmer while still building bots, so learning it can be a useful and enjoyable skillset. + +This page provides resources to make the path to learning as clear and easy as possible, and collates useful examples provided by the community that may address common ideas and concerns that are seen when working on Discord bots. + +## Essential References + +Official Documentation: [https://discord.py.readthedocs.io](https://discordpy.readthedocs.io/) + +Source Repository: [https://github.com/Rapptz/discord.py](https://github.com/Rapptz/discord.py) + +## Creating a Discord Bot Account + +1. Navigate to [https://discord.com/developers/applications](https://discord.com/developers/applications) and log in. +2. Click on `New Application`. +3. Enter the application's name. +4. Click on `Bot` on the left side settings menu. +5. Click `Add Bot` and confirm with `Yes, do it!`. + +### Client ID +Your Client ID is the same as the User ID of your Bot. +You will need this when creating an invite URL. + +You can find your Client ID located on the `General Information` settings page of your Application, under the `Name` field. + +Your Client ID is not a secret, and does not need to be kept private. + +### Bot Token + +Your Bot Token is the token that authorises your Bot account with the API. +Think of it like your Bot's API access key. +With your token, you can interact with any part of the API that's available to bots. + +You can find your Bot Token located on the Bot settings page of your Application, under the Username field. +You can click the Copy button to copy it without revealing it manually. + +**Your Bot Token is a secret, and must be kept private.** +If you leak your token anywhere other people has access to see it, no matter the duration, you should reset your Bot Token. + +To reset your token, go to the Bot settings page of your Application, and click the Regenerate button. +Be sure to update the token you're using for your bot script to this new one, as the old one will not work anymore. + +### Permissions Integer + +Discord Permissions are typically represented by a Permissions Integer which represents all the Permissions that have been allowed. + +You can find a reference to all the available Discord Permissions, their bitwise values and their descriptions here:<br> +[https://discordapp.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags](https://discordapp.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags) + +If you want to create your own Permissions Integer, you can generate it in the `Bot` settings page of your Application, located at the bottom of the page. + +Tick the permissions you want to be allowing, and it'll update the `Permissions Integer` field, which you can use in your Bot Invite URL to set your bot's default permissions when users go to invite it. + +### Bot Invite URL + +Bot's cannot use a server invite link. Instead, they have to be invited by a member with the Manage Server permission. + +The Bot Invite URL is formatted like: +`https://discordapp.com/oauth2/authorize?client_id={CLIENT_ID}&scope=bot&permissions={PERMISSIONS_INTEGER}` + +You can create the Invite URL for your bot by replacing: + +* `{CLIENT_ID}` with your [Client ID](#client-id) +* `{PERMISSIONS_INTEGER}` with the [Permissions Integer](#permissions-integer) + +You can also generate it with the [Permissions Calculator](https://discordapi.com/permissions.html tool) tool. + +## Using the Basic Client (`discord.Client`) { data-toc-label="Using the Basic Client" } + +Below are the essential resources to read over to get familiar with the basic functionality of `discord.py`. + +* [Basic event usage](https://discordpy.readthedocs.io/en/latest/intro.html#basic*concepts) +* [Simple bot walkthrough](https://discordpy.readthedocs.io/en/latest/quickstart.html#a*minimal*bot) +* [Available events reference](https://discordpy.readthedocs.io/en/latest/api.html#event*reference) +* [General API reference](https://discordpy.readthedocs.io/en/latest/api.html) + +## Using the Commands Extension (`commands.Bot`) { data-toc-label="Using the Commands Extension" } + +The Commands Extension has a explanatory documentation walking you through not only what it is and it's basic usage, but also more advanced concepts. +Be sure to read the prose documentation in full at:<br> +[https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html](https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html) + +It fully covers: +* How to create bot using the Commands Extension +* How to define commands and their arguments +* What the Context object is +* Argument Converters +* Error Handling basics +* Command checks + +You will also need to reference the following resources: +* [Commands Extension exclusive events](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#event-reference) +* [Commands Extension API reference](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html) + +## FAQ + +The documentation covers some basic FAQ's, and they are recommended to be read beforehand, and referenced before asking for help in case it covers your issue: +[https://discordpy.readthedocs.io/en/latest/faq.html](https://discordpy.readthedocs.io/en/latest/faq.html) + +## Usage Examples + +### Official Examples and Resources + +The official examples can be found on the [source repository](https://github.com/Rapptz/discord.py/tree/master/examples). + +The most commonly referenced examples are: + +* [Basic Commands Extension Bot](https://github.com/Rapptz/discord.py/blob/master/examples/basic_bot.py) +* [Background Task Example](https://github.com/Rapptz/discord.py/blob/master/examples/background_task.py) + +### Permissions Documentation + +* [Role Management 101](https://support.discordapp.com/hc/en-us/articles/214836687-Role-Management-101) +* [Full Permissions Documentation](https://discordapp.com/developers/docs/topics/permissions) + +### Community Examples and Resources + +The `discord.py` developer community over time have shared examples and references with each other.<br> +The following are a collated list of the most referenced community examples. + +#### Extensions / Cogs +* [Extension/Cog Example](https://gist.github.com/EvieePy/d78c061a4798ae81be9825468fe146be) - *Credit to EvieePy* +* [Available Cog Methods](https://gist.github.com/Ikusaba-san/69115b79d33e05ed07ec4a4f14db83b1) - *Credit to MIkusaba* + +#### Error Handling +* [Decent Error Handling Example](https://gist.github.com/EvieePy/7822af90858ef65012ea500bcecf1612) - *Credit to EvieePy* + +#### Embeds +* [Embed Live Designer and Visualiser](https://leovoel.github.io/embed-visualizer/) - *Credit to leovoel* +* [Embed Element Reference](https://cdn.discordapp.com/attachments/84319995256905728/252292324967710721/embed.png)<br> +{: width="200" } + +##### Using Local Images in Embeds +```py +filename = "image.png" + +f = discord.File("some_file_path", filename=filename) +embed = discord.Embed() + +embed.set_image(url=f"attachment://{filename}") +await messagable.send(file=f, embed=embed) +``` + +##### Embed Limits + +| **Element** | **Characters** | +| -------------- | ---------------------- | +| Title | 256 | +| Field Name | 256 | +| Field Value | 1024 | +| Description | 2048 | +| Footer | 2048 | +| **Entire Embed** | **6000** + +| **Element** | **Count** | +| -------------- | ---------------------- | +| Fields | 25 | + +#### Emoji + +- [Bot's Using Emoji](https://gist.github.com/scragly/b8d20aece2d058c8c601b44a689a47a0) + +#### Activity Presence + +- [Setting Bot's Discord Activity](https://gist.github.com/scragly/2579b4d335f87e83fbacb7dfd3d32828) + +#### Image Processing + +- [PIL Image Processing Example Cog](https://gist.github.com/Gorialis/e89482310d74a90a946b44cf34009e88) - *Credit to Gorialis* + +### Systemd Service +**botname.service**<br> +```ini +[Unit] +Description=My Bot Name +After=network-online.target + +[Service] +Type=simple +WorkingDirectory=/your/bots/directory +ExecStart=/usr/bin/python3 /your/bots/directory/file.py +User=username +Restart=on-failure + +[Install] +WantedBy=network-online.target +``` + +**Directory**<br> +`/usr/local/lib/systemd/system` + +**Service Commands**<br> +Refresh systemd after unit file changes:<br> +`systemctl daemon-reload` + +Set service to start on boot:<br> +`systemctl enable botname` + +Start service now:<br> +`systemctl start botname` + +Stop service:<br> +`systemctl stop botname` + +**Viewing Logs**<br> +All logs:<br> +`journalctl -u botname` + +Recent logs and continue printing new logs live:<br> +`journalctl -fu mybot` diff --git a/pydis_site/apps/content/resources/guides/python-guides/mutability.md b/pydis_site/apps/content/resources/guides/python-guides/mutability.md new file mode 100644 index 00000000..185dc87c --- /dev/null +++ b/pydis_site/apps/content/resources/guides/python-guides/mutability.md @@ -0,0 +1,55 @@ +--- +title: Mutability and Immutability in Python +description: "Mutable and immutable data types: what they are and how they work." +--- + +Consider this example: +```python +>>> s = "hello" +>>> s.upper() +'HELLO' +>>> s +'hello' +``` +This might break your expectations. +After all, you've called the `upper()` method on `s`, so why didn't it change? That's because strings are _immutable_: you can't change them in-place, only create new ones. +In this example, `.upper()` just cannot change the string stored in `s`. + +How do you make `s` store `'HELLO'` instead of `'hello'` then? That's possible. +Even though you can't change the original string, you can create a new one, which is like the old one, but with all letters in upper case. + +In other words, `s.upper()` doesn't change an existing string. +It just returns a new one. +```python +>>> s = 'hello' +>>> s = s.upper() +>>> s +'HELLO' +``` + +Let's examine what's going on here. +At first, the variable `s` refers to some object, the string `'hello'`. + + + +When you call `s.upper()`, a new string, which contains the characters `'HELLO'`, gets created. + + + +This happens even if you just call `s.upper()` without any assignment, on its own line: +```python +"hello".upper() +``` +In this case, a new object will be created and discarded right away. + +Then the assignment part comes in: the name `s` gets disconnected from `'hello'`, and gets connected to `'HELLO'`. + + + +Now we can say that `'HELLO'` is stored in the `s` variable. + +Then, because no variables refer to the _object_ `'hello'`, it gets eaten by the garbage collector. + + + +It means that the memory reserved for that object will be freed. If that didn't happen, the 'garbage' would accumulate over time and fill up all the RAM. diff --git a/pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md b/pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md new file mode 100644 index 00000000..45ad60b1 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md @@ -0,0 +1,290 @@ +--- +title: Function Parameters and Arguments in Python +description: An in-depth look at function parameters and arguments, and how to use them. +toc: 1 +--- + +A Python function is utilised in two steps: + +1. The function definition/signature (used just once). +2. The function invocation/call (used many times). + +The function definition uses parameters, whereas the function call uses arguments: + +```python +def foo(this_is_a_parameter): + print(this_is_a_parameter) + +foo(this_is_an_argument) +``` + +An important detail to be aware of is that by default any argument used to call a function in Python can be used as both a positional and a keyword argument—just not at the same time. +A function call may contain a mixture of positional and keyword arguments, and—unless otherwise specified—an argument can reference the parameters in the function definition positionally, or by name (keyword). + +# Positional Arguments + +```python +def foo(a, b, c): + print(a, b, c) + +>>> foo(1, 2, 3) +1 2 3 +``` + +In the above function definition we have three parameters `a`, `b`, and `c`. + +When we invoke the function with the arguments `1`, `2`, and `3`, the function will map these values in the exact order given to the parameters in the function definition. +With no keyword reference given they become positional arguments. + +# Keyword Arguments + +```python +def foo(a, b, c): + print(a, b, c) + +>>> foo(1, 2, 3) +1 2 3 + +>>> foo(c=3, b=2, a=1) +1 2 3 +``` + +As you can see, `foo(1, 2, 3)` and `foo(c=3, b=2, a=1)` are identical. +Referencing a function parameter by its name means that we are using a keyword argument. +The order in which keyword arguments are given does not matter. + +# Mixing Positional and Keyword Arguments + +So what happens if we want to mix the positional argument mapping with keyword arguments? + +Python prioritises the mapping of positional arguments to their parameter names before the mapping of keywords. + +```python +def foo(a, b, c): + print(a, b, c) + +>>> foo(1, c=3, b=2) +1 2 3 +``` + +Passing a keyword argument using the name of a parameter that has already been given will not work: + +```python +>>> foo(1, 2, a=3) +TypeError: foo() got multiple values for argument 'a' + +>>> foo(1, b=2, b=3) +SyntaxError: keyword argument repeated +``` + +Attempting to pass positional arguments after a keyword argument will also not work: + +```python +>>> foo(a=1, 2, 3) +SyntaxError: positional argument follows keyword argument +``` + +# Default Parameter Values + +Although the syntax is similar, these are not to be confused with keyword arguments.<br> +Default parameter values appear within the function definition and allow us to conveniently set a default value. This means that if any argument is omitted, its default value will be used as the argument. + +```python +def foo(a=0, b=0, c=0): + print(a, b, c) + +>>> foo() +0 0 0 + +>>> foo(1, 2, 3) +1 2 3 + +>>> foo(c=3, b=2) +0 2 3 + +>>> foo(1, c=3) +1 0 3 +``` + +Using default parameter values does not change how a function can be invoked with arguments: + +```python +>>> foo(1, 2, a=3) +TypeError: foo() got multiple values for argument 'a' + +>>> foo(1, b=2, b=3) +SyntaxError: keyword argument repeated + +>>> foo(a=1, 2, 3) +SyntaxError: positional argument follows keyword argument +``` + +You must specify any parameters without a default value before those with default values: + +```python +def foo(a=0, b): + ^ +SyntaxError: non-default argument follows default argument +``` + +# Positional-only Parameters +[Python 3.8](https://docs.python.org/3/whatsnew/3.8.html#positional-only-parameters) / [PEP 570](https://www.python.org/dev/peps/pep-0570/) introduces the possibility to specify which parameters are required to be positional-only via a bare `/` parameter within a function definition. + +```python +def foo(a=0, b=0, /, c=0, d=0): + print(a, b, c, d) +``` + +The parameters defined before the bare `/` are now considered to be positional-only and keyword mapping will no longer work on them.<br> +In the above function definition `a` and `b` are now positional-only parameters. + +These function calls will still work: + +```python +>>> foo() +0 0 0 0 + +>>> foo(1) +1 0 0 0 + +>>> foo(1, 2, 3, 4) +1 2 3 4 + +>>> foo(1, 2, d=4, c=3) +1 2 3 4 + +>>> foo(1, d=4, c=3) +1 0 3 4 + +>>> foo(c=3, d=4) +0 0 3 4 +``` + +However, attempting to pass keyword arguments for `a` or `b` will fail: + +```python +>>> foo(1, b=2, c=3, d=4) +TypeError: foo() got some positional-only arguments passed as keyword arguments: 'b' +``` + +### Q: Why is this useful? + +#### Keyword Argument Freedom + +Passing a keyword argument using the name of a parameter that has already been given will not work. +This becomes an issue if we require keyword arguments that use the same parameter names as defined in the function signature, such as via callback functions. + +```python +def foo(a, **kwargs): + print(a, kwargs) + +>>> foo(a=1, a=2) +SyntaxError: keyword argument repeated + +>>> foo(1, a=2) +TypeError: foo() got multiple values for argument 'a' +``` + +#### Backwards Compatibility + +Because Python allows that an argument by default can be either positional or keyword, a user is free to choose either option. +Unfortunately, this forces the author to keep the given parameter names as they are if they wish to support backwards compatibility, as changing the parameter names can cause dependent code to break. +Enforcing positional-only parameters gives the author the freedom to separate the variable name used within the function from its usage outside of it. + +```python +def calculate(a, b): + # do something with a and b + +>>> calculate(1, 2) +``` + +A user could call this function using `a` or `b` as keywords, which the author may have not intended: + +```python +>>> calculate(a=1, b=2) +``` + +However, by using `/`, the user will no longer be able to invoke using `a` or `b` as keywords, and the author is also free to rename these parameters: + +```python +def calculate(x, y, /): + # do something with x and y + +>>> calculate(1, 2) +``` + +# Keyword-only Parameters + +Similarly to enforcing positional-only parameters, we can also enforce keyword-only parameters using a bare `*` parameter. +The parameters defined after the bare `*` are now considered to be keyword-only. + +```python +def foo(a=0, b=0, /, c=0, *, d=0): + print(a, b, c, d) + +>>> foo() +0 0 0 0 + +>>> foo(1, 2, 3) +1 2 3 0 + +>>> foo(1, 2, d=4, c=3) +1 2 3 4 + +>>> foo(1, d=4, c=3) +1 0 3 4 +``` + +Although `c` can be either a positional or keyword argument, if we attempt to pass `d` as a non-keyword argument, it will fail: + +```python +>>> foo(1, 2, 3, 4) +TypeError: foo() takes from 0 to 3 positional arguments but 4 were given +``` + +At least one named parameter must be provided after a bare `*` parameter. +Writing a function definition similar to what is shown below would not make sense, as without the context of a named parameter the bare `*` can simply be omitted. + +```python +def foo(a=0, *, **kwargs): + ^ +SyntaxError: named arguments must follow bare * +``` + +### Q: Why is this useful? + +The main benefit of using keyword-only parameters is when they are used together with positional-only parameters to remove ambiguity. + +However, it may sometimes also be desirable to use keyword-only arguments on their own.<br> +If we were to expose a function as part of an API, we may want the parameter names to carry explicit meaning. + +Without using keyword names when invoking the function it can be unclear as to what the provided arguments are for. +Additionally, a user could also choose to interchange positional arguments with keyword arguments, which can potentially add to the confusion. + +```python +def update(identity=None, name=None, description=None): + # handle the parameters + +>>> update("value 1", "value 2", "value 3") + +>>> update(1234, "value 1", description="value 2") +``` + +Enforcing the keyword names is clearer, as it carries context without needing to look at the function definition: + +```python +def update(*, identity=None, name=None, description=None): + # handle the parameters + +>>> update(identity=1234, name="value 1", description="value 2") +``` + +# Summary + +* Unless otherwise specified, an argument can be both positional and keyword. +* Positional arguments, when provided, must be in sequence. +* Positional arguments must be used before keyword arguments. +* Keyword arguments may be in any order. +* A default parameter value is used when the argument is omitted. +* A bare `/` used as a parameter in a function definition enforces positional-only parameters to its left. +* A bare `*` used as a parameter in a function definition enforces keyword-only parameters to its right. diff --git a/pydis_site/apps/content/resources/privacy.md b/pydis_site/apps/content/resources/privacy.md new file mode 100644 index 00000000..a2ab6f87 --- /dev/null +++ b/pydis_site/apps/content/resources/privacy.md @@ -0,0 +1,12 @@ +--- +title: Privacy Policy +description: Our server's privacy policy. +icon: fab fa-discord +--- + +You should be redirected. If you are not, [please click here](https://www.notion.so/pythondiscord/Python-Discord-Privacy-ee2581fea4854ddcb1ebc06c1dbb9fbd). + +<script> + // Redirect visitor to the privacy page + window.location.href = "https://www.notion.so/pythondiscord/Python-Discord-Privacy-ee2581fea4854ddcb1ebc06c1dbb9fbd"; +</script> diff --git a/pydis_site/apps/content/resources/rules.md b/pydis_site/apps/content/resources/rules.md new file mode 100644 index 00000000..27f03f07 --- /dev/null +++ b/pydis_site/apps/content/resources/rules.md @@ -0,0 +1,44 @@ +--- +title: Python Discord Rules +description: The rules of our community. +icon: fab fa-discord +--- +We have a small but strict set of rules on our server. Please read over them and take them on board. If you don't understand a rule or need to report an incident, please send a direct message to <code>@ModMail</code>! + +> 1. Follow the [Discord Community Guidelines](https://discordapp.com/guidelines) and [Terms Of Service](https://discordapp.com/terms). +> 2. Follow the [Python Discord Code of Conduct](/pages/code-of-conduct/). +> 3. Listen to and respect staff members and their instructions. +> 4. This is an English-speaking server, so please speak English to the best of your ability. +> 5. Do not provide or request help on projects that may break laws, breach terms of services, be considered malicious or inappropriate. Do not help with ongoing exams. Do not provide or request solutions for graded assignments, although general guidance is okay. +> 6. No spamming or unapproved advertising, including requests for paid work. Open-source projects can be shared with others in #python-general and code reviews can be asked for in a help channel. +> 7. Keep discussions relevant to channel topics and guidelines. + +# Nickname Policy + +In order to keep things pleasant and workable for both users and staff members, we enforce the following requirements regarding your nickname. + +1. No blank or "invisible" names +2. No slurs or other offensive sentiments +3. No noisy unicode characters - for example, z̯̯͡a̧͎̺̻̝͕̠l̡͓̫̣g̹̲o̡̼̘ or byte order marks +4. No nicknames designed to annoy other users + +Staff reserves the right to change the nickname of any user for any reason. Failure to comply with these requirements may result in you losing the right to change your nickname. We also reserve the right to discipline users with offensive usernames, regardless of the nickname they're using. + + +# Infractions + +We have a generally no-nonsense policy when it comes to our rules. If you notice someone breaking them, feel free to mention or DM a staff member and we'll try to deal with it as soon as possible. + +The possible actions we take based on infractions can include the following: + +* A public verbal or textual warning +* Forced nick changes, where appropriate +* A short temporary mute +* A long temporary mute +* A kick from the server +* A temporary ban from the server +* A permanent ban from the server + +While we do discuss more serious matters internally before handing out a punishment, simpler infractions are dealt with directly by individual staffers and the punishment they hand out is left to their own discretion. + +If you receive an infraction and would like to appeal it, send an e-mail to [[email protected]](mailto:[email protected]). diff --git a/pydis_site/apps/content/resources/security-notice.md b/pydis_site/apps/content/resources/security-notice.md new file mode 100644 index 00000000..e3630ae1 --- /dev/null +++ b/pydis_site/apps/content/resources/security-notice.md @@ -0,0 +1,37 @@ +--- +title: Security Notice +description: How vulnerabilities in our projects should be reported. +icon: fas fa-shield-alt +--- + +This is the security notice for all Python Discord repositories. +The notice explains how vulnerabilities should be reported. + +# Reporting a Vulnerability + +If you've found a vulnerability, we would like to know so we can fix it before it is released publicly. +**Do not open a GitHub issue for a found vulnerability**. + +Send details to [[email protected]](mailto:[email protected]) or through a Discord direct message to an Admin of Python Discord, including: + +* the website, page or repository where the vulnerability can be observed +* a brief description of the vulnerability +* optionally the type of vulnerability and any related [OWASP category](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_2017_Project) +* non-destructive exploitation details + +We will do our best to reply as fast as possible. + +# Scope + +The following vulnerabilities **are not** in scope: + +* volumetric vulnerabilities, for example overwhelming a service with a high volume of requests +* reports indicating that our services do not fully align with “best practice”, for example missing security headers + +If you aren't sure, you can still reach out via email or direct message. + +--- + +This notice is inspired by the [GDS Security Notice](https://github.com/alphagov/.github/blob/master/SECURITY.md). + +*Version 2021-03* diff --git a/pydis_site/apps/content/resources/server-info/_info.yml b/pydis_site/apps/content/resources/server-info/_info.yml new file mode 100644 index 00000000..52df0f8d --- /dev/null +++ b/pydis_site/apps/content/resources/server-info/_info.yml @@ -0,0 +1,3 @@ +title: Server Information +description: Information on roles, tooling, and infrastructure at Python Discord. +icon: fab fa-discord diff --git a/pydis_site/apps/content/resources/server-info/roles.md b/pydis_site/apps/content/resources/server-info/roles.md new file mode 100644 index 00000000..716f5b1e --- /dev/null +++ b/pydis_site/apps/content/resources/server-info/roles.md @@ -0,0 +1,131 @@ +--- +title: Python Discord Server Roles +description: Information on the various roles at Python Discord. +icon: fab fa-discord +--- + +# Basic Roles + +### <span class="fas fa-circle" style="color:#6e6e6e"></span> Announcements +**Description:** A role pinged by Admins when an announcement is made in the `#announcements` channel. + +**How to get it:** Run the command `!subscribe` in the `#bot-commands` channel. +To unsubscribe, run `!unsubscribe` in the `#bot-commands` channel. + + +### <span class="fas fa-circle" style="color:#6e6e6e"></span> Voice Verified +**Description:** A role that lets people speak in voice channels. + +**How to get it:** Send `!voiceverify` in the `#voice-verification` channel. +There are multiple requirements listed there for getting the role. + +--- + +# Server Support Roles + +### <span class="fas fa-circle" style="color:#55cc6c"></span> Contributors +**Description:** A role given by staff to people who make substantial contributions to any of the server's [open source repositories](https://github.com/python-discord/) (Sir Lancebot, Python, the site, the branding repo, etc..).<br> +This includes writing pull requests for open issues, and also for reviewing open pull requests (**we really need reviewers!**) + +**How to get it:** Contribute to the projects! +There is no minimum requirements, but the role is **not** assigned for every single contribution. +Read more about this in the [Guidelines for the Contributors Role](/pages/contributing/#guidelines-for-the-contributors-role) on the Contributing page. + +--- + +# Financial Support Roles + +### <span class="fas fa-circle" style="color:#46e6e8"></span> Nitro Boosters +**Description:** A vanity role for people who boost the server with their nitro subscription. + +**How to get it:** Boost the server with a nitro subscription. + + +### <span class="fas fa-circle" style="color:#46e6e8"></span> <span class="fas fa-circle" style="color:#3e7be9"></span> <span class="fas fa-circle" style="color:#2a82bd"></span> Patrons +**Description:** A vanity role for Patreon patrons of the server. + +**How to get it:** [Become a patron here!](https://www.patreon.com/python_discord) + +--- + +# Staff Roles +#### Note regarding staff roles: +##### Q: How do I apply for Helper/Moderator/Admin? +There is no application, and there are no public nominations. Staff keep an eye out for potential candidates, and people nominated (by staff) are put in a pool for evaluation. After a period of time the candidate for a certain role is voted on by staff higher up the hierarchy. + +##### Q: How do I become Helper? +See the description of a Helper. Being active in helping others, providing good help, contributing to our projects, and abiding by our rules go a long way towards catching staff attention, and make the server a better place for both beginners and advanced Python devs. + +##### Role Expectations +In addition to the informal descriptions below, we've also written down a more formal list of expectations that come with each staff role. While this list is mostly for internal use, you can read it [here](/pages/server-info/staff-role-expectations/). + +### <span class="fas fa-circle" style="color:#f85950"></span> Owners +**Description:** Owners of the server. + +### <span class="fas fa-circle" style="color:#ff784d"></span> Admins +**Description:** Staff who administrate the server, its function, its staff, and are involved in deciding the direction of the server. + +### <span class="fas fa-circle" style="color:#1abc9c"></span> Domain Leads +**Description:** Staff in charge of a certain domain such as moderation, events, and outreach. A lead will have a second role specifying their domain. + +### <span class="fas fa-circle" style="color:#8dc2ba"></span> Project Leads +**Description:** Staff in charge of a certain project that require special attention, such as a YouTube video series or our new forms page. + +### <span class="fas fa-circle" style="color:#ff9f1b"></span> Moderators +**Description:** Staff who moderate the server, enforce the rules, and coordinate with staff to support the server. + +### <span class="fas fa-circle" style="color:#a1d1ff"></span> PyDis Core Developers +**Description:** A role for staff who are critical contributors to the server's core projects, like the [bot](https://github.com/python-discord/bot) and the [site](https://github.com/python-discord/site), and are in charge of managing the repositories. + +### <span class="fas fa-circle" style="color:#a1d1ff"></span> DevOps +**Description:** A role for staff involved with the DevOps toolchain of our core projects. + +### <span class="fas fa-circle" style="color:#f8d188"></span> Project Teams +**Description:** Staff can join teams which work on specific projects in the organisation, such as our code jams, media projects, and more. + +### <span class="fas fa-circle" style="color:#eecd36"></span> Helpers +**Description:** This is the core staff role in our organization: All staff members have the Helpers role. + +In general, being a helper means that you provide substantial help for the server's function, and have a good understanding of the culture and rules of the server. + +Helpers assist in the help channels, demonstrate proficiency in the language, and have strong teaching and explanation skills. +Otherwise they might assist in other areas of the organization, such as being a core developer, events team member, or moderator. + +Being a helper is also more than just quantity of messages, it's about quality. We watch and we pick these people out of the crowd, because we believe that they're a valuable asset to the community, and want our users to know that they're someone that can be relied on for answers and help. + +--- + +# Code Jam Roles +### <span class="fas fa-circle" style="color:#f87dc8"></span> Code Jam Champions +**Description:** A vanity role for winners of past code jams. + +**How to get it:** Win a code jam! + + +### <span class="fas fa-circle" style="color:#28866c"></span> Code Jam Leaders +**Description:** A temporary role for the duration of a code jam given to team leaders. + +**How to get it:** Team leaders are picked from the participants by the Events Team, and assigned for the duration of a jam. + + +### <span class="fas fa-circle" style="color:#229939"></span> Code Jam Participants +**Description:** A temporary role for the duration of a code jam given to participants. + +**How to get it:** Qualify for and participate in a code jam. + +*Note: Similar roles may exist for a game jam.* + + +--- + +# Miscellaneous Roles + +### <span class="fas fa-circle" style="color:#9f3fee"></span> Partners +**Description:** Representatives of communities we are partnered with. For a list of partnered communities, see the `#partners` channel. + +*Note: Not related to [Discord Partners](https://discordapp.com/partners), which our server is currently a part of.* + +### <span class="fas fa-circle" style="color:#c77cfa"></span> Python Community +**Description:** Prominent people in the Python ecosystem. +Typically this will be people who have written books, people who speak at PyCon, YouTube content creators, podcasters, or notable contributors to a Python runtime or a major Python module. +These members will have a meta role attached to further explain why they have this role, for example `CPython: Core Developer`. diff --git a/pydis_site/apps/content/resources/server-info/staff-role-expectations.md b/pydis_site/apps/content/resources/server-info/staff-role-expectations.md new file mode 100644 index 00000000..286386d7 --- /dev/null +++ b/pydis_site/apps/content/resources/server-info/staff-role-expectations.md @@ -0,0 +1,67 @@ +--- +title: Staff Role Expectations +description: List of expectations that come with being on the staff team at Python Discord. +icon: fab fa-discord +--- + +This page has a list of expectations that come with having a certain staff role in our community. +While the term "expectations" may sound a bit formal, it's important to keep in mind that everyone with a staff role is just a volunteer and that this list is a way of having a clear overview of what each role entails. + +This document is mostly meant for internal reference. +If you want a more informal description of each staff role, take a look at our [roles page](/pages/server-info/roles/#staff-roles). + +## Expectations + +### <span class="fas fa-circle" style="color:#eecd36"></span> Helpers + +* In general, helpers participate in Python-related channels (e.g. Help Channels, Topical Channels) and help other members of our community. +* Helpers may also help the community by taking up organizational tasks. +* There are no real requirements for the level of activity a helper has to have, although we do expect their activity level to be more than "nothing". + +### <span class="fas fa-circle" style="color:#ff9f1b"></span> Moderators + +* Moderators moderate our community and participate in moderation discussions in our moderators channel. +* While moderators don't need to have high levels of activity, we do expect some form of consistent activity. +This means that consistently being active a few times a month is better than having one day with a lot of activity per year. +Having some kind of consistent activity helps moderators bond with the rest of the moderation team and helps them to stay up to date with the moderation policy. +* **Moderators are not required to fulfill the helper criteria in addition to this,** although it's is obviously appreciated if they do. + +### <span class="fas fa-circle" style="color:#ff784d"></span> Admins + +* Admins are expected to work on tasks that directly improve the community on a regular basis. +* Examples of these tasks include: + * Doing pull request reviews; + * Being involved in events; + * Overseeing road map items; + * Solving critical issues; + * Handling raids; + * Joining our meetings (if in a compatible timezone); + * Actioning issues on the organisation repo; + * Improving our infrastructure; + * Writing documentation or guides; + * Recruiting and on-boarding new staff members; + * Calling staff votes for nominees; + * Having one-on-ones with moderators. +* Admins are also expected to keep each other updated on the status of the tasks they are working on. + +### <span class="fas fa-circle" style="color:#f85950"></span> Owners + +**In addition to** the regular Admin criteria, Owners also have to: + +* Join staff/admin meetings as often as possible and lead those meetings. +* Help identify the most critical tasks and try to distribute them among the Admins during the weekly Admin meeting. +* Make sure that no one is "blocked" in performing their tasks. +* Ensure that the community isn’t neglecting important responsibilities. +* Manage partnerships, sponsorships and speak on behalf of the community in public settings. + +--- + +## Staff Management +First of all, it's important to appreciate that everything staff members do in this community is voluntary and the expectations listed above are not meant to change that. +**This means it's absolutely fine for all staff members to take breaks or vacations from their activities in the community when they need to.** +We will never hold it against someone if they are temporarily away from their responsibilities. + +At the same time, it's only natural for a community like ours that there's some amount of staff turnover as personal interests and circumstances change. +Going forward, we will periodically review the activity of individual staff members and open a dialogue with staff members who are currently not meeting the expectations to see what can be done. +It might happen that we come to conclusion that it's better for a staff member to step down from their current position. +Do note that there are no hard feelings involved if that happens; we just want to make sure that the current staffing reflects the people who are still interested in volunteering in this community. diff --git a/pydis_site/templates/wiki/includes/messages.html b/pydis_site/apps/content/tests/__init__.py index e69de29b..e69de29b 100644 --- a/pydis_site/templates/wiki/includes/messages.html +++ 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..29140375 --- /dev/null +++ b/pydis_site/apps/content/tests/helpers.py @@ -0,0 +1,91 @@ +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 +toc: 0 +--- +# This is a header. +""" + +MARKDOWN_WITHOUT_METADATA = """#This is a header.""" + +# Valid YAML in a _info.yml file +CATEGORY_INFO = """ +title: 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">¶</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" + }, + "toc": 0 +} + +# The YAML data parsed from the above _info.yml file +PARSED_CATEGORY_INFO = {"title": "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.md + ├── tmp + | ├── _info.yml + | └── category + | ├── _info.yml + | └── subcategory_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_file("tmp.md", contents=MARKDOWN_WITH_METADATA) + self.fs.create_file("tmp/category/_info.yml", contents=CATEGORY_INFO) + self.fs.create_dir("tmp/category/subcategory_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..6612e44c --- /dev/null +++ b/pydis_site/apps/content/tests/test_utils.py @@ -0,0 +1,93 @@ +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, {"title": "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/subcategory_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): + # TOC is a special case because the markdown converter outputs the TOC as HTML + updated_metadata = {**PARSED_METADATA, "toc": '<div class="toc">\n<ul></ul>\n</div>\n'} + cases = [ + ("Root page with metadata", "root.md", PARSED_HTML, updated_metadata), + ("Root page without metadata", "root_without_metadata.md", PARSED_HTML, {}), + ("Page with metadata", "category/with_metadata.md", PARSED_HTML, updated_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..74d38f78 --- /dev/null +++ b/pydis_site/apps/content/tests/test_views.py @@ -0,0 +1,184 @@ +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(".") + + +def patch_dispatch_attributes(view: PageOrCategoryView, location: str) -> None: + """ + Set the attributes set in the `dispatch` method manually. + + This is necessary because it is never automatically called during tests. + """ + view.location = Path(location) + + # URL location on the filesystem + view.full_location = view.location + + # Possible places to find page content information + view.category_path = view.full_location + view.page_path = view.full_location.with_suffix(".md") + + +@override_settings(CONTENT_PAGES_PATH=BASE_PATH) +class PageOrCategoryViewTests(MockPagesTestCase, SimpleTestCase, TestCase): + """Tests for the PageOrCategoryView class.""" + + def setUp(self): + """Set test helpers, then set up fake filesystem.""" + self.factory = RequestFactory() + self.view = PageOrCategoryView.as_view() + self.ViewClass = PageOrCategoryView() + super().setUp() + + # Integration tests + def test_valid_page_or_category_returns_200(self): + cases = [ + ("Page at root", "root"), + ("Category page", "category"), + ("Page in category", "category/with_metadata"), + ("Subcategory page", "category/subcategory"), + ("Page in subcategory", "category/subcategory/with_metadata"), + ] + for msg, path in cases: + with self.subTest(msg=msg, path=path): + request = self.factory.get(f"/{path}") + response = self.view(request, location=path) + self.assertEqual(response.status_code, 200) + + def test_nonexistent_page_returns_404(self): + with self.assertRaises(Http404): + request = self.factory.get("/invalid") + self.view(request, location="invalid") + + # Unit tests + def test_get_template_names_returns_correct_templates(self): + category_template = "content/listing.html" + page_template = "content/page.html" + cases = [ + ("root", page_template), + ("root_without_metadata", page_template), + ("category/with_metadata", page_template), + ("category/subcategory/with_metadata", page_template), + ("category", category_template), + ("category/subcategory", category_template), + ] + + for path, expected_template in cases: + with self.subTest(path=path, expected_template=expected_template): + patch_dispatch_attributes(self.ViewClass, path) + self.assertEqual(self.ViewClass.get_template_names(), [expected_template]) + + def test_get_template_names_with_nonexistent_paths_returns_404(self): + for path in ("invalid", "another_invalid", "nonexistent"): + with self.subTest(path=path): + patch_dispatch_attributes(self.ViewClass, path) + with self.assertRaises(Http404): + self.ViewClass.get_template_names() + + def test_get_template_names_returns_page_template_for_category_with_page(self): + """Make sure the proper page is returned for category locations with pages.""" + patch_dispatch_attributes(self.ViewClass, "tmp") + self.assertEqual(self.ViewClass.get_template_names(), ["content/page.html"]) + + def test_get_context_data_with_valid_page(self): + """The method should return required fields in the template context.""" + request = self.factory.get("/root") + self.ViewClass.setup(request) + self.ViewClass.dispatch(request, location="root") + + cases = [ + ("Context includes HTML page content", "page", PARSED_HTML), + ("Context includes page title", "page_title", PARSED_METADATA["title"]), + ( + "Context includes page description", + "page_description", + PARSED_METADATA["description"] + ), + ( + "Context includes relevant link names and URLs", + "relevant_links", + PARSED_METADATA["relevant_links"] + ), + ] + context = self.ViewClass.get_context_data() + for msg, key, expected_value in cases: + with self.subTest(msg=msg): + self.assertEqual(context[key], expected_value) + + def test_get_context_data_with_valid_category(self): + """The method should return required fields in the template context.""" + request = self.factory.get("/category") + self.ViewClass.setup(request) + self.ViewClass.dispatch(request, location="category") + + cases = [ + ( + "Context includes subcategory names and their information", + "categories", + {"subcategory": PARSED_CATEGORY_INFO} + ), + ( + "Context includes page names and their metadata", + "pages", + {"with_metadata": PARSED_METADATA} + ), + ( + "Context includes page description", + "page_description", + PARSED_CATEGORY_INFO["description"] + ), + ("Context includes page title", "page_title", PARSED_CATEGORY_INFO["title"]), + ] + + context = self.ViewClass.get_context_data() + for msg, key, expected_value in cases: + with self.subTest(msg=msg): + self.assertEqual(context[key], expected_value) + + def test_get_context_data_for_category_with_page(self): + """Make sure the proper page is returned for category locations with pages.""" + request = self.factory.get("/category") + self.ViewClass.setup(request) + self.ViewClass.dispatch(request, location="tmp") + + context = self.ViewClass.get_context_data() + expected_page_context = { + "page": PARSED_HTML, + "page_title": PARSED_METADATA["title"], + "page_description": PARSED_METADATA["description"], + "relevant_links": PARSED_METADATA["relevant_links"], + "subarticles": [{"path": "category", "name": "Category Name"}] + } + for key, expected_value in expected_page_context.items(): + with self.subTest(): + self.assertEqual(context[key], expected_value) + + def test_get_context_data_breadcrumbs(self): + """The method should return correct breadcrumbs.""" + request = self.factory.get("/category/subcategory/with_metadata") + self.ViewClass.setup(request) + self.ViewClass.dispatch(request, location="category/subcategory/with_metadata") + + context = self.ViewClass.get_context_data() + self.assertEquals( + context["breadcrumb_items"], + [ + {"name": PARSED_CATEGORY_INFO["title"], "path": "."}, + {"name": PARSED_CATEGORY_INFO["title"], "path": "category"}, + {"name": PARSED_CATEGORY_INFO["title"], "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..d3f270ff --- /dev/null +++ b/pydis_site/apps/content/utils.py @@ -0,0 +1,64 @@ +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"): + # Only list page if there is no category with the same name + if item.is_file() and not item.with_suffix("").is_dir(): + 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")) + toc_depth = metadata.get("toc", 1) + + md = markdown.Markdown( + 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(permalink=True, marker="", toc_depth=toc_depth) + ] + ) + html = md.convert(content) + + # Don't set the TOC if the metadata does not specify one + if "toc" in metadata: + metadata["toc"] = md.toc + + 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..5af77aff --- /dev/null +++ b/pydis_site/apps/content/views/page_category.py @@ -0,0 +1,95 @@ +import typing as t +from pathlib import Path + +import frontmatter +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", "")) + + # URL location on the filesystem + self.full_location = settings.CONTENT_PAGES_PATH / self.location + + # Possible places to find page content information + self.category_path = self.full_location + self.page_path = self.full_location.with_suffix(".md") + + 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.page_path.is_file(): + template_name = "content/page.html" + elif self.category_path.is_dir(): + template_name = "content/listing.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.page_path.is_file(): + context.update(self._get_page_context(self.page_path)) + elif self.category_path.is_dir(): + context.update(self._get_category_context(self.category_path)) + context["path"] = f"{self.location}/" # Add trailing slash to simplify template + else: + raise Http404 + + # Add subarticle information for dropdown menu if the page is also a category + if self.page_path.is_file() and self.category_path.is_dir(): + context["subarticles"] = [] + for entry in self.category_path.iterdir(): + entry_info = {"path": entry.stem} + if entry.suffix == ".md" and not entry.with_suffix("").is_dir(): + entry_info["name"] = frontmatter.load(entry).metadata["title"] + elif entry.is_dir(): + entry_info["name"] = utils.get_category(entry)["title"] + else: # pragma: no cover + # TODO: Remove coverage.py pragma in Python 3.10 + # See: https://github.com/nedbat/coveragepy/issues/198 + continue + context["subarticles"].append(entry_info) + + context["breadcrumb_items"] = [ + { + "name": utils.get_category(settings.CONTENT_PAGES_PATH / location)["title"], + "path": str(location) + } for location in reversed(self.location.parents) + ] + + return context + + @staticmethod + def _get_page_context(path: Path) -> t.Dict[str, t.Any]: + page, metadata = utils.get_page(path) + return { + "page": page, + "page_title": metadata["title"], + "page_description": metadata["description"], + "relevant_links": metadata.get("relevant_links", {}), + "toc": metadata.get("toc") + } + + @staticmethod + def _get_category_context(path: Path) -> t.Dict[str, t.Any]: + category = utils.get_category(path) + return { + "categories": utils.get_categories(path), + "pages": utils.get_category_pages(path), + "page_title": category["title"], + "page_description": category["description"], + "icon": category.get("icon"), + } diff --git a/pydis_site/apps/events/__init__.py b/pydis_site/apps/events/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/events/__init__.py diff --git a/pydis_site/apps/events/apps.py b/pydis_site/apps/events/apps.py new file mode 100644 index 00000000..a1cf09ef --- /dev/null +++ b/pydis_site/apps/events/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class EventsConfig(AppConfig): + """Django AppConfig for events app.""" + + name = 'events' diff --git a/pydis_site/apps/events/migrations/__init__.py b/pydis_site/apps/events/migrations/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/events/migrations/__init__.py diff --git a/pydis_site/apps/events/tests/__init__.py b/pydis_site/apps/events/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/events/tests/__init__.py diff --git a/pydis_site/apps/events/tests/test_views.py b/pydis_site/apps/events/tests/test_views.py new file mode 100644 index 00000000..23c9e596 --- /dev/null +++ b/pydis_site/apps/events/tests/test_views.py @@ -0,0 +1,42 @@ +from pathlib import Path + +from django.conf import settings +from django.test import TestCase, override_settings +from django_hosts.resolvers import reverse + + +PAGES_PATH = Path(settings.BASE_DIR, "pydis_site", "templates", "events", "test-pages") + + +class IndexTests(TestCase): + def test_events_index_response_200(self): + """Should return response code 200 when visiting index of events.""" + url = reverse("events:index") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + + +class PageTests(TestCase): + @override_settings(EVENTS_PAGES_PATH=PAGES_PATH) + def test_valid_event_page_reponse_200(self): + """Should return response code 200 when visiting valid event page.""" + pages = ( + reverse("events:page", ("my-event",)), + reverse("events:page", ("my-event/subpage",)), + ) + for page in pages: + with self.subTest(page=page): + resp = self.client.get(page) + self.assertEqual(resp.status_code, 200) + + @override_settings(EVENTS_PAGES_PATH=PAGES_PATH) + def test_invalid_event_page_404(self): + """Should return response code 404 when visiting invalid event page.""" + pages = ( + reverse("events:page", ("invalid",)), + reverse("events:page", ("invalid/invalid",)) + ) + for page in pages: + with self.subTest(page=page): + resp = self.client.get(page) + self.assertEqual(resp.status_code, 404) diff --git a/pydis_site/apps/events/urls.py b/pydis_site/apps/events/urls.py new file mode 100644 index 00000000..9a65cf1f --- /dev/null +++ b/pydis_site/apps/events/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from pydis_site.apps.events.views import IndexView, PageView + +app_name = "events" +urlpatterns = [ + path("", IndexView.as_view(), name="index"), + path("<path:path>/", PageView.as_view(), name="page"), +] diff --git a/pydis_site/apps/events/views/__init__.py b/pydis_site/apps/events/views/__init__.py new file mode 100644 index 00000000..8a107e2f --- /dev/null +++ b/pydis_site/apps/events/views/__init__.py @@ -0,0 +1,4 @@ +from .index import IndexView +from .page import PageView + +__all__ = ["IndexView", "PageView"] diff --git a/pydis_site/apps/events/views/index.py b/pydis_site/apps/events/views/index.py new file mode 100644 index 00000000..7ffba74a --- /dev/null +++ b/pydis_site/apps/events/views/index.py @@ -0,0 +1,7 @@ +from django.views.generic import TemplateView + + +class IndexView(TemplateView): + """Events index page view.""" + + template_name = "events/index.html" diff --git a/pydis_site/apps/events/views/page.py b/pydis_site/apps/events/views/page.py new file mode 100644 index 00000000..1622ad70 --- /dev/null +++ b/pydis_site/apps/events/views/page.py @@ -0,0 +1,25 @@ +from typing import List + +from django.conf import settings +from django.http import Http404 +from django.views.generic import TemplateView + + +class PageView(TemplateView): + """Handles event pages showing.""" + + def get_template_names(self) -> List[str]: + """Get specific template names.""" + path: str = self.kwargs['path'] + page_path = settings.EVENTS_PAGES_PATH / path + if page_path.is_dir(): + page_path = page_path / "_index.html" + path = f"{path}/_index.html" + else: + page_path = settings.EVENTS_PAGES_PATH / f"{path}.html" + path = f"{path}.html" + + if not page_path.exists(): + raise Http404 + + return [f"events/{settings.EVENTS_PAGES_PATH.name}/{path}"] diff --git a/pydis_site/apps/home/__init__.py b/pydis_site/apps/home/__init__.py index ecfab449..e69de29b 100644 --- a/pydis_site/apps/home/__init__.py +++ b/pydis_site/apps/home/__init__.py @@ -1 +0,0 @@ -default_app_config = "pydis_site.apps.home.apps.HomeConfig" diff --git a/pydis_site/apps/home/apps.py b/pydis_site/apps/home/apps.py deleted file mode 100644 index 55a393a9..00000000 --- a/pydis_site/apps/home/apps.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Any, Dict - -from django.apps import AppConfig - - -class HomeConfig(AppConfig): - """Django AppConfig for the home app.""" - - name = 'pydis_site.apps.home' - signal_listener = None - - def ready(self) -> None: - """Run when the app has been loaded and is ready to serve requests.""" - from pydis_site.apps.home.signals import AllauthSignalListener - - self.signal_listener = AllauthSignalListener() - self.patch_allauth() - - def patch_allauth(self) -> None: - """Monkey-patches Allauth classes so we never collect email addresses.""" - # Imported here because we can't import it before our apps are loaded up - from allauth.socialaccount.providers.base import Provider - - def extract_extra_data(_: Provider, data: Dict[str, Any]) -> Dict[str, Any]: - """ - Extracts extra data for a SocialAccount provided by Allauth. - - This is our version of this function that strips the email address from incoming extra - data. We do this so that we never have to store it. - - This is monkey-patched because most OAuth providers - or at least the ones we care - about - all use the function from the base Provider class. This means we don't have - to make a new Django app for each one we want to work with. - """ - data["email"] = "" - return data - - Provider.extract_extra_data = extract_extra_data diff --git a/pydis_site/apps/home/forms/account_deletion.py b/pydis_site/apps/home/forms/account_deletion.py deleted file mode 100644 index eec70bea..00000000 --- a/pydis_site/apps/home/forms/account_deletion.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.forms import CharField, Form - - -class AccountDeletionForm(Form): - """Account deletion form, to collect username for confirmation of removal.""" - - username = CharField( - label="Username", - required=True - ) diff --git a/pydis_site/apps/home/resources/books/byte_of_python.yaml b/pydis_site/apps/home/resources/books/byte_of_python.yaml deleted file mode 100644 index f3eca902..00000000 --- a/pydis_site/apps/home/resources/books/byte_of_python.yaml +++ /dev/null @@ -1,17 +0,0 @@ -description: A free book on programming using the Python language. It serves as a - tutorial or guide to the Python language for a beginner audience. If all you know - about computers is how to save text files, then this is the book for you. -name: A Byte of Python -payment: optional -payment_description: A free e-book is available online, a paper version can be bought - from lulu.com. -urls: -- icon: regular/link - title: E-book - url: https://python.swaroopch.com/ -- icon: regular/book - title: Buy the book - url: http://www.lulu.com/shop/swaroop-c-h/a-byte-of-python/paperback/product-21142968.html -- icon: regular/tablet-alt - title: Kindle edition - url: https://www.amazon.com/Byte-Python-Swaroop-C-H-ebook/dp/B00FJ7S2JU/ diff --git a/pydis_site/apps/home/resources/communities/adafruit.yaml b/pydis_site/apps/home/resources/communities/adafruit.yaml deleted file mode 100644 index 193f7364..00000000 --- a/pydis_site/apps/home/resources/communities/adafruit.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: 'Adafruit is an open-source electronics manufacturer that makes all the - components you need to start your own Python-powered hardware projects. - - - Their official community host regular show-and-tells, provide help with your projects, - and the Adafruit devs do all the CircuitPython development right out in the open. - Join the Maker Revolution today!' -name: 'Discord: Adafruit' -payment: free -payment_description: null -urls: -- icon: branding/discord - title: Adafruit Discord - url: https://discord.gg/adafruit diff --git a/pydis_site/apps/home/resources/communities/functional_programming.yaml b/pydis_site/apps/home/resources/communities/functional_programming.yaml deleted file mode 100644 index ab99f264..00000000 --- a/pydis_site/apps/home/resources/communities/functional_programming.yaml +++ /dev/null @@ -1,10 +0,0 @@ -description: Functional Programming is a server for discussing functional languages - like Haskell, Idris, Elixir and Lisp as well as related academic fields such as - type theory, category theory, proof assistants, and more! -name: 'Discord: Functional Programming' -payment: free -payment_description: null -urls: -- icon: branding/discord - title: Functional Programming Discord - url: https://discord.gg/kWJYurV diff --git a/pydis_site/apps/home/resources/communities/pallets.yaml b/pydis_site/apps/home/resources/communities/pallets.yaml deleted file mode 100644 index e5a18983..00000000 --- a/pydis_site/apps/home/resources/communities/pallets.yaml +++ /dev/null @@ -1,10 +0,0 @@ -description: The Pallets Projects develop Python libraries such as the Flask web framework, - the Jinja templating library, and the Click command line toolkit. Join to discuss - and get help from the Pallets community. -name: 'Discord: The Pallets Project' -payment: free -payment_description: null -urls: -- icon: branding/discord - title: The Pallets Project Discord - url: https://discord.gg/t6rrQZH diff --git a/pydis_site/apps/home/resources/communities/rlbot.yaml b/pydis_site/apps/home/resources/communities/rlbot.yaml deleted file mode 100644 index c62e0260..00000000 --- a/pydis_site/apps/home/resources/communities/rlbot.yaml +++ /dev/null @@ -1,11 +0,0 @@ -description: RLBot is a community of programmers making awesome Rocket League bots. - They've created a framework that you can use to write bots in a number of languages - (including Python), and they host regular tournaments where botmakers can pit their - creations against each other. -name: 'Discord: RLBot' -payment: free -payment_description: null -urls: -- icon: branding/discord - title: RLBot Discord - url: https://discord.gg/4JJdJKb diff --git a/pydis_site/apps/home/resources/communities/subreddit.yaml b/pydis_site/apps/home/resources/communities/subreddit.yaml deleted file mode 100644 index 217a84ac..00000000 --- a/pydis_site/apps/home/resources/communities/subreddit.yaml +++ /dev/null @@ -1,8 +0,0 @@ -description: News about the Python programming language, and language-related discussion -name: 'Subreddit: r/Python' -payment: free -payment_description: null -urls: -- icon: branding/reddit-alien - title: r/Python on Reddit - url: https://www.reddit.com/r/Python/ diff --git a/pydis_site/apps/home/resources/editors/atom.yaml b/pydis_site/apps/home/resources/editors/atom.yaml deleted file mode 100644 index f05e45a3..00000000 --- a/pydis_site/apps/home/resources/editors/atom.yaml +++ /dev/null @@ -1,12 +0,0 @@ -description: A free Electron-based editor, a "hackable text editor for the 21st century", maintained - by the GitHub team. -name: Atom -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://atom.io/ -- icon: branding/github - title: GitHub - url: https://github.com/atom/atom diff --git a/pydis_site/apps/home/resources/editors/mu_editor.yaml b/pydis_site/apps/home/resources/editors/mu_editor.yaml deleted file mode 100644 index cb44d750..00000000 --- a/pydis_site/apps/home/resources/editors/mu_editor.yaml +++ /dev/null @@ -1,12 +0,0 @@ -description: An editor aimed at beginners for the purpose of learning how to code - without the distractions more advanced editors sometimes cause. -name: Mu-Editor -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://codewith.mu/en/ -- icon: branding/github - title: GitHub - url: https://github.com/mu-editor/mu/ diff --git a/pydis_site/apps/home/resources/editors/sublime_text.yaml b/pydis_site/apps/home/resources/editors/sublime_text.yaml deleted file mode 100644 index 97952d35..00000000 --- a/pydis_site/apps/home/resources/editors/sublime_text.yaml +++ /dev/null @@ -1,9 +0,0 @@ -description: A powerful Python-backed editor with great community support and a wealth - of extensions. -name: Sublime Text -payment: optional -payment_description: Nagware; will ask you to buy the full version after every X saves -urls: -- icon: regular/link - title: Website - url: https://www.sublimetext.com/ diff --git a/pydis_site/apps/home/resources/editors/visual_studio_code.yaml b/pydis_site/apps/home/resources/editors/visual_studio_code.yaml deleted file mode 100644 index 4e1f946f..00000000 --- a/pydis_site/apps/home/resources/editors/visual_studio_code.yaml +++ /dev/null @@ -1,11 +0,0 @@ -description: A fully-featured editor based on Electron, extendable with plugins. -name: Visual Studio Code -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://code.visualstudio.com/ -- icon: branding/github - title: GitHub - url: https://github.com/Microsoft/vscode diff --git a/pydis_site/apps/home/resources/ides/_category_info.yaml b/pydis_site/apps/home/resources/ides/_category_info.yaml deleted file mode 100644 index d331c95d..00000000 --- a/pydis_site/apps/home/resources/ides/_category_info.yaml +++ /dev/null @@ -1,2 +0,0 @@ -description: Fully-integrated development environments for serious Python work -name: IDEs diff --git a/pydis_site/apps/home/resources/ides/pycharm.yaml b/pydis_site/apps/home/resources/ides/pycharm.yaml deleted file mode 100644 index 4624cb41..00000000 --- a/pydis_site/apps/home/resources/ides/pycharm.yaml +++ /dev/null @@ -1,10 +0,0 @@ -description: The very best Python IDE, with a wealth of advanced features and convenience - functions. -name: PyCharm -payment: optional -payment_description: There's a free Community Edition and a paid-for Professional - Edition with more features available -urls: -- icon: regular/link - title: Website - url: https://www.jetbrains.com/pycharm/ diff --git a/pydis_site/apps/home/resources/ides/spyder.yaml b/pydis_site/apps/home/resources/ides/spyder.yaml deleted file mode 100644 index 146b3549..00000000 --- a/pydis_site/apps/home/resources/ides/spyder.yaml +++ /dev/null @@ -1,12 +0,0 @@ -description: The Scientific PYthon Development EnviRonment. Simpler and lighter than - PyCharm, but still packs a punch. -name: Spyder -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://www.spyder-ide.org/ -- icon: branding/github - title: GitHub - url: https://github.com/spyder-ide/spyder diff --git a/pydis_site/apps/home/resources/ides/thonny.yaml b/pydis_site/apps/home/resources/ides/thonny.yaml deleted file mode 100644 index d660094b..00000000 --- a/pydis_site/apps/home/resources/ides/thonny.yaml +++ /dev/null @@ -1,12 +0,0 @@ -description: A Python IDE specifially aimed at learning programming. Has a lot of - helpful features to help you understand your code. -name: Thonny -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://thonny.org/ -- icon: branding/github - title: GitHub - url: https://github.com/thonny/thonny/ diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/_category_info.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/_category_info.yaml deleted file mode 100644 index 08501627..00000000 --- a/pydis_site/apps/home/resources/interactive_learning_tools/_category_info.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: Learn Python with interactive content like courses, games and programming - challenges. -name: Interactive Learning Tools diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/automate_the_boring_stuff.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/automate_the_boring_stuff.yaml deleted file mode 100644 index 02d76b3b..00000000 --- a/pydis_site/apps/home/resources/interactive_learning_tools/automate_the_boring_stuff.yaml +++ /dev/null @@ -1,11 +0,0 @@ -description: The interactive course version of Al Sweigart's excellent book for beginners, - taught by the author himself. This link has a discounted version of the course which - will always cost 10 dollars. Thanks, Al! -name: Automate the Boring Stuff with Python -payment: paid -payment_description: Paid course with a certificate of completion. Some sample videos - are available for free. -urls: -- icon: regular/graduation-cap - title: Udemy Course - url: https://www.udemy.com/automate/?couponCode=FOR_LIKE_10_BUCKS diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/learn_to_program.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/learn_to_program.yaml deleted file mode 100644 index 265f1644..00000000 --- a/pydis_site/apps/home/resources/interactive_learning_tools/learn_to_program.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: A 2-part course that teaches Python. Primarily intended for high school - students and first-year university students who want to learn programming. -name: 'University of Toronto: Learn to Program' -payment: optional -payment_description: You can pay to enroll for a graded certificate, or choose to - audit for free. -urls: -- icon: regular/graduation-cap - title: 'Part 1: The Fundamentals' - url: https://www.coursera.org/learn/learn-to-program -- icon: regular/graduation-cap - title: 'Part 2: Crafting Quality Code' - url: https://www.coursera.org/learn/program-code diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/mit_python.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/mit_python.yaml deleted file mode 100644 index 464b8d4a..00000000 --- a/pydis_site/apps/home/resources/interactive_learning_tools/mit_python.yaml +++ /dev/null @@ -1,10 +0,0 @@ -description: This MITx offering teaches computer science with Python. It covers computational - thinking, algorithms, data structures and the Python programming language itself. -name: 'MIT: Introduction to Computer Science and Programming Using Python' -payment: optional -payment_description: You can pay to enroll for a graded certificate, or choose to - take the full course for free. -urls: -- icon: regular/graduation-cap - title: edX Course - url: https://www.edx.org/course/introduction-computer-science-mitx-6-00-1x-11 diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/programming_for_everybody.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/programming_for_everybody.yaml deleted file mode 100644 index a6d7abe1..00000000 --- a/pydis_site/apps/home/resources/interactive_learning_tools/programming_for_everybody.yaml +++ /dev/null @@ -1,10 +0,0 @@ -description: A 5-part specialization course that teaches Python from scratch. The - course has no pre-requisites and avoids all but the simplest mathematics. -name: 'University of Michigan: Programming for Everybody' -payment: optional -payment_description: You can pay to enroll for a graded certificate and a capstone - project, or choose to audit for free. -urls: -- icon: regular/graduation-cap - title: Python for Everyone Specialization - url: https://www.coursera.org/learn/python diff --git a/pydis_site/apps/home/resources/misc/_category_info.yaml b/pydis_site/apps/home/resources/misc/_category_info.yaml deleted file mode 100644 index 4fdc4bf7..00000000 --- a/pydis_site/apps/home/resources/misc/_category_info.yaml +++ /dev/null @@ -1,2 +0,0 @@ -description: Resources which do not fit into the other categories -name: Miscellaneous diff --git a/pydis_site/apps/home/resources/misc/good_first_issue_tag.yaml b/pydis_site/apps/home/resources/misc/good_first_issue_tag.yaml deleted file mode 100644 index 35d7a8a4..00000000 --- a/pydis_site/apps/home/resources/misc/good_first_issue_tag.yaml +++ /dev/null @@ -1,10 +0,0 @@ -description: Searching for opportunities to contribute to a Python project? GitHub - repository maintainers often mark issues appropriate for novice users with the 'Good - First Issue' tag. These issues can be explored directly on GitHub. -name: GitHub's 'Good First Issue' Tag -payment: free -payment_description: null -urls: -- icon: branding/github - title: GitHub - url: https://github.com/search?utf8=%E2%9C%93&q=label%3A%22good+first+issue%22+language%3APython+state%3Aopen&type=Issues&ref=advsearch&l=Python&l= diff --git a/pydis_site/apps/home/resources/misc/python_cheat_sheet.yaml b/pydis_site/apps/home/resources/misc/python_cheat_sheet.yaml deleted file mode 100644 index 8c82a5a9..00000000 --- a/pydis_site/apps/home/resources/misc/python_cheat_sheet.yaml +++ /dev/null @@ -1,9 +0,0 @@ -description: A Python 3 cheat sheet with useful information and tips, as well as common - pitfalls for beginners. This is a PDF. -name: Python Cheat Sheet -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://perso.limsi.fr/pointal/_media/python:cours:mementopython3-english.pdf diff --git a/pydis_site/apps/home/resources/podcasts/_category_info.yaml b/pydis_site/apps/home/resources/podcasts/_category_info.yaml deleted file mode 100644 index a0f9025c..00000000 --- a/pydis_site/apps/home/resources/podcasts/_category_info.yaml +++ /dev/null @@ -1,2 +0,0 @@ -description: Notable podcasts about the Python ecosystem -name: Podcasts diff --git a/pydis_site/apps/home/resources/tutorials/_category_info.yaml b/pydis_site/apps/home/resources/tutorials/_category_info.yaml deleted file mode 100644 index a9adc106..00000000 --- a/pydis_site/apps/home/resources/tutorials/_category_info.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: Tutorials and references for those that are just getting started with - python -name: Tutorials diff --git a/pydis_site/apps/home/resources/tutorials/corey_schafer.yaml b/pydis_site/apps/home/resources/tutorials/corey_schafer.yaml deleted file mode 100644 index 9fff4bbf..00000000 --- a/pydis_site/apps/home/resources/tutorials/corey_schafer.yaml +++ /dev/null @@ -1,7 +0,0 @@ -description: An in-depth look at the Python programming language, from one of - YouTube's most popular Python tutors. -payment: free -urls: - - icon: branding/youtube, - title: YouTube, - url: https://www.youtube.com/playlist?list=PL-osiE80TeTt2d9bfVyTiXJA-UTHn6WwU diff --git a/pydis_site/apps/home/resources/tutorials/get_started_with_flask.yaml b/pydis_site/apps/home/resources/tutorials/get_started_with_flask.yaml deleted file mode 100644 index 11dd2a4d..00000000 --- a/pydis_site/apps/home/resources/tutorials/get_started_with_flask.yaml +++ /dev/null @@ -1,9 +0,0 @@ -description: A fully featured mega-tutorial for learning how to create web applications - with the Flask framework. -name: Get Started with Flask Web Development -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world diff --git a/pydis_site/apps/home/resources/tutorials/getting_started_with_python.yaml b/pydis_site/apps/home/resources/tutorials/getting_started_with_python.yaml deleted file mode 100644 index 777f2fe3..00000000 --- a/pydis_site/apps/home/resources/tutorials/getting_started_with_python.yaml +++ /dev/null @@ -1,12 +0,0 @@ -description: The list of resources for programmers and non-programmers from Python's - official beginners' guide -name: Getting Started with Python -payment: free -payment_description: null -urls: -- icon: regular/link - title: Beginners Guide for Non-Programmers - url: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers -- icon: regular/link - title: Beginners Guide for Programmers - url: https://wiki.python.org/moin/BeginnersGuide/Programmers diff --git a/pydis_site/apps/home/resources/tutorials/hitchhikers_guide_to_python.yaml b/pydis_site/apps/home/resources/tutorials/hitchhikers_guide_to_python.yaml deleted file mode 100644 index 38eebb56..00000000 --- a/pydis_site/apps/home/resources/tutorials/hitchhikers_guide_to_python.yaml +++ /dev/null @@ -1,10 +0,0 @@ -description: This opinionated guide exists to provide both novice and expert Python - developers a best practice handbook to the installation, configuration, and usage - of Python on a daily basis. -name: The Hitchhiker's Guide to Python -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://python-guide.org/ diff --git a/pydis_site/apps/home/resources/tutorials/sentdex.yaml b/pydis_site/apps/home/resources/tutorials/sentdex.yaml deleted file mode 100644 index cae2695b..00000000 --- a/pydis_site/apps/home/resources/tutorials/sentdex.yaml +++ /dev/null @@ -1,8 +0,0 @@ -description: A Python basics tutorial based around Python 3.7. -name: Python Tutorials by Sentdex on YouTube -payment: free -payment_description: null -urls: -- icon: branding/youtube - title: YouTube - url: https://www.youtube.com/playlist?list=PLQVvvaa0QuDeAams7fkdcwOGBpGdHpXln diff --git a/pydis_site/apps/home/resources/tutorials/simple_guide_to_git.yaml b/pydis_site/apps/home/resources/tutorials/simple_guide_to_git.yaml deleted file mode 100644 index acf76efe..00000000 --- a/pydis_site/apps/home/resources/tutorials/simple_guide_to_git.yaml +++ /dev/null @@ -1,8 +0,0 @@ -description: A simple, no-nonsense guide to the basics of using Git. -name: A Simple Guide to Git -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: http://rogerdudler.github.io/git-guide/ diff --git a/pydis_site/apps/home/signals.py b/pydis_site/apps/home/signals.py deleted file mode 100644 index 8af48c15..00000000 --- a/pydis_site/apps/home/signals.py +++ /dev/null @@ -1,314 +0,0 @@ -from contextlib import suppress -from typing import List, Optional, Type - -from allauth.account.signals import user_logged_in -from allauth.socialaccount.models import SocialAccount, SocialLogin -from allauth.socialaccount.providers.base import Provider -from allauth.socialaccount.providers.discord.provider import DiscordProvider -from allauth.socialaccount.signals import ( - pre_social_login, social_account_added, social_account_removed, - social_account_updated) -from django.contrib.auth.models import Group, User as DjangoUser -from django.db.models.signals import post_delete, post_save, pre_save - -from pydis_site.apps.api.models import User as DiscordUser -from pydis_site.apps.staff.models import RoleMapping - - -class AllauthSignalListener: - """ - Listens to and processes events via the Django Signals system. - - Django Signals is basically an event dispatcher. It consists of Signals (which are the events) - and Receivers, which listen for and handle those events. Signals are triggered by Senders, - which are essentially just any class at all, and Receivers can filter the Signals they listen - for by choosing a Sender, if required. - - Signals themselves define a set of arguments that they will provide to Receivers when the - Signal is sent. They are always keyword arguments, and Django recommends that all Receiver - functions accept them as `**kwargs` (and will supposedly error if you don't do this), - supposedly because Signals can change in the future and your receivers should still work. - - Signals do provide a list of their arguments when they're initially constructed, but this - is purely for documentation purposes only and Django does not enforce it. - - The Django Signals docs are here: https://docs.djangoproject.com/en/2.2/topics/signals/ - """ - - def __init__(self): - post_save.connect(self.user_model_updated, sender=DiscordUser) - - post_delete.connect(self.mapping_model_deleted, sender=RoleMapping) - pre_save.connect(self.mapping_model_updated, sender=RoleMapping) - - pre_social_login.connect(self.social_account_updated) - social_account_added.connect(self.social_account_updated) - social_account_updated.connect(self.social_account_updated) - social_account_removed.connect(self.social_account_removed) - - user_logged_in.connect(self.user_logged_in) - - def user_logged_in(self, sender: Type[DjangoUser], **kwargs) -> None: - """ - Processes Allauth login signals to ensure a user has the correct perms. - - This method tries to find a Discord SocialAccount for a user - this should always - be the case, but the admin user likely won't have one, so we do check for it. - - After that, we try to find the user's stored Discord account details, provided by the - bot on the server. Finally, we pass the relevant information over to the - `_apply_groups()` method for final processing. - """ - user: DjangoUser = kwargs["user"] - - try: - account: SocialAccount = SocialAccount.objects.get( - user=user, provider=DiscordProvider.id - ) - except SocialAccount.DoesNotExist: - return # User's never linked a Discord account - - try: - discord_user: DiscordUser = DiscordUser.objects.get(id=int(account.uid)) - except DiscordUser.DoesNotExist: - return - - self._apply_groups(discord_user, account) - - def social_account_updated(self, sender: Type[SocialLogin], **kwargs) -> None: - """ - Processes Allauth social account update signals to ensure a user has the correct perms. - - In this case, a SocialLogin is provided that we can check against. We check that this - is a Discord login in order to ensure that future OAuth logins using other providers - don't break things. - - Like most of the other methods that handle signals, this method defers to the - `_apply_groups()` method for final processing. - """ - social_login: SocialLogin = kwargs["sociallogin"] - - account: SocialAccount = social_login.account - provider: Provider = account.get_provider() - - if not isinstance(provider, DiscordProvider): - return - - try: - user: DiscordUser = DiscordUser.objects.get(id=int(account.uid)) - except DiscordUser.DoesNotExist: - return - - self._apply_groups(user, account) - - def social_account_removed(self, sender: Type[SocialLogin], **kwargs) -> None: - """ - Processes Allauth social account reomval signals to ensure a user has the correct perms. - - In this case, a SocialAccount is provided that we can check against. If this is a - Discord OAuth being removed from the account, we want to ensure that the user loses - their permissions groups as well. - - While this isn't a realistic scenario to reach in our current setup, I've provided it - for the sake of covering any edge cases and ensuring that SocialAccounts can be removed - from Django users in the future if required. - - Like most of the other methods that handle signals, this method defers to the - `_apply_groups()` method for final processing. - """ - account: SocialAccount = kwargs["socialaccount"] - provider: Provider = account.get_provider() - - if not isinstance(provider, DiscordProvider): - return - - try: - user: DiscordUser = DiscordUser.objects.get(id=int(account.uid)) - except DiscordUser.DoesNotExist: - return - - self._apply_groups(user, account, deletion=True) - - def mapping_model_deleted(self, sender: Type[RoleMapping], **kwargs) -> None: - """ - Processes deletion signals from the RoleMapping model, removing perms from users. - - We need to do this to ensure that users aren't left with permissions groups that - they shouldn't have assigned to them when a RoleMapping is deleted from the database, - and to remove their staff status if they should no longer have it. - """ - instance: RoleMapping = kwargs["instance"] - - for user in instance.group.user_set.all(): - # Firstly, remove their related user group - user.groups.remove(instance.group) - - with suppress(SocialAccount.DoesNotExist, DiscordUser.DoesNotExist): - # If we get either exception, then the user could not have been assigned staff - # with our system in the first place. - - social_account = SocialAccount.objects.get(user=user, provider=DiscordProvider.id) - discord_user = DiscordUser.objects.get(id=int(social_account.uid)) - - mappings = RoleMapping.objects.filter(role__id__in=discord_user.roles).all() - is_staff = any(m.is_staff for m in mappings) - - if user.is_staff != is_staff: - user.is_staff = is_staff - user.save(update_fields=("is_staff", )) - - def mapping_model_updated(self, sender: Type[RoleMapping], **kwargs) -> None: - """ - Processes update signals from the RoleMapping model. - - This method is in charge of figuring out what changed when a RoleMapping is updated - (via the Django admin or otherwise). It operates based on what was changed, and can - handle changes to both the role and permissions group assigned to it. - """ - instance: RoleMapping = kwargs["instance"] - raw: bool = kwargs["raw"] - - if raw: - # Fixtures are being loaded, so don't touch anything - return - - old_instance: Optional[RoleMapping] = None - - if instance.id is not None: - # We don't try to catch DoesNotExist here because we can't test for it, - # it should never happen (unless we have a bad DB failure) but I'm still - # kind of antsy about not having the extra security here. - - old_instance = RoleMapping.objects.get(id=instance.id) - - if old_instance: - self.mapping_model_deleted(RoleMapping, instance=old_instance) - - accounts = SocialAccount.objects.filter( - uid__in=(u.id for u in DiscordUser.objects.filter(roles__contains=[instance.role.id])) - ) - - for account in accounts: - account.user.groups.add(instance.group) - - if instance.is_staff and not account.user.is_staff: - account.user.is_staff = instance.is_staff - account.user.save(update_fields=("is_staff", )) - else: - discord_user = DiscordUser.objects.get(id=int(account.uid)) - - mappings = RoleMapping.objects.filter( - role__id__in=discord_user.roles - ).exclude(id=instance.id).all() - is_staff = any(m.is_staff for m in mappings) - - if account.user.is_staff != is_staff: - account.user.is_staff = is_staff - account.user.save(update_fields=("is_staff",)) - - def user_model_updated(self, sender: Type[DiscordUser], **kwargs) -> None: - """ - Processes update signals from the Discord User model, assigning perms as required. - - When a user's roles are changed on the Discord server, this method will ensure that - the user has only the permissions groups that they should have based on the RoleMappings - that have been set up in the Django admin. - - Like some of the other signal handlers, this method ensures that a SocialAccount exists - for this Discord User, and defers to `_apply_groups()` to do the heavy lifting of - ensuring the permissions groups are correct. - """ - instance: DiscordUser = kwargs["instance"] - raw: bool = kwargs["raw"] - - # `update_fields` could be used for checking changes, but it's None here due to how the - # model is saved without using that argument - so we can't use it. - - if raw: - # Fixtures are being loaded, so don't touch anything - return - - try: - account: SocialAccount = SocialAccount.objects.get( - uid=str(instance.id), provider=DiscordProvider.id - ) - except SocialAccount.DoesNotExist: - return # User has never logged in with Discord on the site - - self._apply_groups(instance, account) - - def _apply_groups( - self, user: DiscordUser, account: SocialAccount, deletion: bool = False - ) -> None: - """ - Ensures that the correct permissions are set for a Django user based on the RoleMappings. - - This (private) method is designed to check a Discord User against a given SocialAccount, - and makes sure that the Django user associated with the SocialAccount has the correct - permissions groups. - - While it would be possible to get the Discord User object with just the SocialAccount - object, the current approach results in less queries. - - The `deletion` parameter is used to signify that the user's SocialAccount is about - to be removed, and so we should always remove all of their permissions groups. The - same thing will happen if the user is no longer actually on the Discord server, as - leaving the server does not currently remove their SocialAccount from the database. - """ - mappings = RoleMapping.objects.all() - - try: - current_groups: List[Group] = list(account.user.groups.all()) - except SocialAccount.user.RelatedObjectDoesNotExist: - return # There's no user account yet, this will be handled by another receiver - - # Ensure that the username on this account is correct - new_username = f"{user.name}#{user.discriminator}" - - if account.user.username != new_username: - account.user.username = new_username - account.user.first_name = new_username - - if not user.in_guild: - deletion = True - - if deletion: - # They've unlinked Discord or left the server, so we have to remove their groups - # and their staff status - - if current_groups: - # They do have groups, so let's remove them - account.user.groups.remove( - *(mapping.group for mapping in mappings) - ) - - if account.user.is_staff: - # They're marked as a staff user and they shouldn't be, so let's fix that - account.user.is_staff = False - else: - new_groups = [] - is_staff = False - - for role in user.roles: - try: - mapping = mappings.get(role__id=role) - except RoleMapping.DoesNotExist: - continue # No mapping exists - - new_groups.append(mapping.group) - - if mapping.is_staff: - is_staff = True - - account.user.groups.add( - *[group for group in new_groups if group not in current_groups] - ) - - account.user.groups.remove( - *[mapping.group for mapping in mappings if mapping.group not in new_groups] - ) - - if account.user.is_staff != is_staff: - account.user.is_staff = is_staff - - account.user.save() diff --git a/pydis_site/apps/home/templatetags/extra_filters.py b/pydis_site/apps/home/templatetags/extra_filters.py index d63b3245..89b45831 100644 --- a/pydis_site/apps/home/templatetags/extra_filters.py +++ b/pydis_site/apps/home/templatetags/extra_filters.py @@ -11,7 +11,7 @@ def starts_with(value: str, arg: str) -> bool: Usage: ```django - {% if request.url | starts_with:"/wiki" %} + {% if request.url | starts_with:"/events" %} ... {% endif %} ``` diff --git a/pydis_site/apps/home/templatetags/wiki_extra.py b/pydis_site/apps/home/templatetags/wiki_extra.py deleted file mode 100644 index b4b720bf..00000000 --- a/pydis_site/apps/home/templatetags/wiki_extra.py +++ /dev/null @@ -1,132 +0,0 @@ -from typing import Any, Dict, List, Type, Union - -from django import template -from django.forms import BooleanField, BoundField, CharField, Field, ImageField, ModelChoiceField -from django.template import Context -from django.template.loader import get_template -from django.utils.safestring import SafeText, mark_safe -from wiki.editors.markitup import MarkItUpWidget -from wiki.forms import WikiSlugField -from wiki.models import URLPath -from wiki.plugins.notifications.forms import SettingsModelChoiceField - -TEMPLATE_PATH = "wiki/forms/fields/{0}.html" - -TEMPLATES: Dict[Type, str] = { - BooleanField: TEMPLATE_PATH.format("boolean"), - CharField: TEMPLATE_PATH.format("char"), - ImageField: TEMPLATE_PATH.format("image"), - - ModelChoiceField: TEMPLATE_PATH.format("model_choice"), - SettingsModelChoiceField: TEMPLATE_PATH.format("model_choice"), - WikiSlugField: TEMPLATE_PATH.format("wiki_slug_render"), -} - - -register = template.Library() - - -def get_unbound_field(field: Union[BoundField, Field]) -> Field: - """ - Unwraps a bound Django Forms field, returning the unbound field. - - Bound fields often don't give you the same level of access to the field's underlying attributes, - so sometimes it helps to have access to the underlying field object. - """ - while isinstance(field, BoundField): - field = field.field - - return field - - -def render(template_path: str, context: Dict[str, Any]) -> SafeText: - """ - Renders a template at a specified path, with the provided context dictionary. - - This was extracted mostly for the sake of mocking it out in the tests - but do note that - the resulting rendered template is wrapped with `mark_safe`, so it will not be escaped. - """ - return mark_safe(get_template(template_path).render(context)) # noqa: S703, S308 - - -def render_field(field: Field, render_labels: bool = True) -> SafeText: - """ - Renders a form field using a custom template designed specifically for the wiki forms. - - As the wiki uses custom form rendering logic, we were unable to make use of Crispy Forms for - it. This means that, in order to customize the form fields, we needed to be able to render - the fields manually. This function handles that logic. - - Sometimes we don't want to render the label that goes with a field - the `render_labels` - argument defaults to True, but can be set to False if the label shouldn't be rendered. - - The label rendering logic is left up to the template. - - Usage: `{% render_field field_obj [render_labels=True/False] %}` - """ - unbound_field = get_unbound_field(field) - - if not isinstance(render_labels, bool): - render_labels = True - - template_path = TEMPLATES.get(unbound_field.__class__, TEMPLATE_PATH.format("in_place_render")) - is_markitup = isinstance(unbound_field.widget, MarkItUpWidget) - context = {"field": field, "is_markitup": is_markitup, "render_labels": render_labels} - - return render(template_path, context) - - [email protected]_tag(takes_context=True) -def get_field_options(context: Context, field: BoundField) -> str: - """ - Retrieves the field options for a multiple choice field, and stores it in the context. - - This tag exists because we can't call functions within Django templates directly, and is - only made use of in the template for ModelChoice (and derived) fields - but would work fine - with anything that makes use of your standard `<select>` element widgets. - - This stores the parsed options under `options` in the context, which will subsequently - be available in the template. - - Usage: - - ```django - {% get_field_options field_object %} - - {% if options %} - {% for group_name, group_choices, group_index in options %} - ... - {% endfor %} - {% endif %} - ``` - """ - widget = field.field.widget - - if field.value() is None: - value: List[str] = [] - else: - value = [str(field.value())] - - context["options"] = widget.optgroups(field.name, value) - return "" - - -def render_urlpath(value: Union[URLPath, str]) -> str: - """ - Simple filter to render a URLPath (or string) into a template. - - This is used where the wiki intends to render a path - mostly because if you just - `str(url_path)`, you'll actually get a path that starts with `(root)` instead of `/`. - - We support strings here as well because the wiki is very inconsistent about when it - provides a string versus when it provides a URLPath, and it was too much work to figure out - and account for it in the templates. - - Usage: `{{ url_path | render_urlpath }}` - """ - if isinstance(value, str): - return value or "/" - - return value.path or "/" diff --git a/pydis_site/apps/home/tests/test_signal_listener.py b/pydis_site/apps/home/tests/test_signal_listener.py deleted file mode 100644 index d99d81a5..00000000 --- a/pydis_site/apps/home/tests/test_signal_listener.py +++ /dev/null @@ -1,458 +0,0 @@ -from unittest import mock - -from allauth.account.signals import user_logged_in -from allauth.socialaccount.models import SocialAccount, SocialLogin -from allauth.socialaccount.providers import registry -from allauth.socialaccount.providers.discord.provider import DiscordProvider -from allauth.socialaccount.providers.github.provider import GitHubProvider -from allauth.socialaccount.signals import ( - pre_social_login, social_account_added, social_account_removed, - social_account_updated) -from django.contrib.auth.models import Group, User as DjangoUser -from django.db.models.signals import post_save, pre_save -from django.test import TestCase - -from pydis_site.apps.api.models import Role, User as DiscordUser -from pydis_site.apps.home.signals import AllauthSignalListener -from pydis_site.apps.staff.models import RoleMapping - - -class SignalListenerTests(TestCase): - @classmethod - def setUpTestData(cls): - """ - Executed when testing begins in order to set up database fixtures required for testing. - - This sets up quite a lot of stuff, in order to try to cover every eventuality while - ensuring that everything works when every possible situation is in the database - at the same time. - - That does unfortunately mean that half of this file is just test fixtures, but I couldn't - think of a better way to do this. - """ - # This needs to be registered so we can test the role linking logic with a user that - # doesn't have a Discord account linked, but is logged in somehow with another account - # type anyway. The logic this is testing was designed so that the system would be - # robust enough to handle that case, but it's impossible to fully test (and therefore - # to have coverage of) those lines without an extra provider, and GH was the second - # provider it was built with in mind. - registry.register(GitHubProvider) - - cls.admin_role = Role.objects.create( - id=0, - name="admin", - colour=0, - permissions=0, - position=0 - ) - - cls.moderator_role = Role.objects.create( - id=1, - name="moderator", - colour=0, - permissions=0, - position=1 - ) - - cls.unmapped_role = Role.objects.create( - id=2, - name="unmapped", - colour=0, - permissions=0, - position=1 - ) - - cls.admin_group = Group.objects.create(name="admin") - cls.moderator_group = Group.objects.create(name="moderator") - - cls.admin_mapping = RoleMapping.objects.create( - role=cls.admin_role, - group=cls.admin_group, - is_staff=True - ) - - cls.moderator_mapping = RoleMapping.objects.create( - role=cls.moderator_role, - group=cls.moderator_group, - is_staff=False - ) - - cls.discord_user = DiscordUser.objects.create( - id=0, - name="user", - discriminator=0, - ) - - cls.discord_unmapped = DiscordUser.objects.create( - id=2, - name="unmapped", - discriminator=0, - ) - - cls.discord_unmapped.roles.append(cls.unmapped_role.id) - cls.discord_unmapped.save() - - cls.discord_not_in_guild = DiscordUser.objects.create( - id=3, - name="not-in-guild", - discriminator=0, - in_guild=False - ) - - cls.discord_admin = DiscordUser.objects.create( - id=1, - name="admin", - discriminator=0, - ) - - cls.discord_admin.roles = [cls.admin_role.id] - cls.discord_admin.save() - - cls.discord_moderator = DiscordUser.objects.create( - id=4, - name="admin", - discriminator=0, - ) - - cls.discord_moderator.roles = [cls.moderator_role.id] - cls.discord_moderator.save() - - cls.django_user_discordless = DjangoUser.objects.create(username="no-discord") - cls.django_user_never_joined = DjangoUser.objects.create(username="never-joined") - - cls.social_never_joined = SocialAccount.objects.create( - user=cls.django_user_never_joined, - provider=DiscordProvider.id, - uid=5 - ) - - cls.django_user = DjangoUser.objects.create(username="user") - - cls.social_user = SocialAccount.objects.create( - user=cls.django_user, - provider=DiscordProvider.id, - uid=cls.discord_user.id - ) - - cls.social_user_github = SocialAccount.objects.create( - user=cls.django_user, - provider=GitHubProvider.id, - uid=cls.discord_user.id - ) - - cls.social_unmapped = SocialAccount( - # We instantiate it and don't put it in the DB. This is (surprisingly) - # a realistic test case, so we need to check for it - - provider=DiscordProvider.id, - uid=5, - user_id=None # No relation exists at all - ) - - cls.django_admin = DjangoUser.objects.create( - username="admin", - is_staff=True, - is_superuser=True - ) - - cls.social_admin = SocialAccount.objects.create( - user=cls.django_admin, - provider=DiscordProvider.id, - uid=cls.discord_admin.id - ) - - cls.django_moderator = DjangoUser.objects.create( - username="moderator", - is_staff=False, - is_superuser=False - ) - - cls.social_moderator = SocialAccount.objects.create( - user=cls.django_moderator, - provider=DiscordProvider.id, - uid=cls.discord_moderator.id - ) - - def test_model_save(self): - """Test signal handling for when Discord user model objects are saved to DB.""" - mock_obj = mock.Mock() - - with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj): - AllauthSignalListener() - - post_save.send( - DiscordUser, - instance=self.discord_user, - raw=True, - created=None, # Not realistic, but we don't use it - using=None, # Again, we don't use it - update_fields=False # Always false during integration testing - ) - - mock_obj.assert_not_called() - - post_save.send( - DiscordUser, - instance=self.discord_user, - raw=False, - created=None, # Not realistic, but we don't use it - using=None, # Again, we don't use it - update_fields=False # Always false during integration testing - ) - - mock_obj.assert_called_with(self.discord_user, self.social_user) - - def test_pre_social_login(self): - """Test the pre-social-login Allauth signal handling.""" - mock_obj = mock.Mock() - - discord_login = SocialLogin(self.django_user, self.social_user) - github_login = SocialLogin(self.django_user, self.social_user_github) - unmapped_login = SocialLogin(self.django_user, self.social_unmapped) - - with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj): - AllauthSignalListener() - - # Don't attempt to apply groups if the user doesn't have a linked Discord account - pre_social_login.send(SocialLogin, sociallogin=github_login) - mock_obj.assert_not_called() - - # Don't attempt to apply groups if the user hasn't joined the Discord server - pre_social_login.send(SocialLogin, sociallogin=unmapped_login) - mock_obj.assert_not_called() - - # Attempt to apply groups if everything checks out - pre_social_login.send(SocialLogin, sociallogin=discord_login) - mock_obj.assert_called_with(self.discord_user, self.social_user) - - def test_social_added(self): - """Test the social-account-added Allauth signal handling.""" - mock_obj = mock.Mock() - - discord_login = SocialLogin(self.django_user, self.social_user) - github_login = SocialLogin(self.django_user, self.social_user_github) - unmapped_login = SocialLogin(self.django_user, self.social_unmapped) - - with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj): - AllauthSignalListener() - - # Don't attempt to apply groups if the user doesn't have a linked Discord account - social_account_added.send(SocialLogin, sociallogin=github_login) - mock_obj.assert_not_called() - - # Don't attempt to apply groups if the user hasn't joined the Discord server - social_account_added.send(SocialLogin, sociallogin=unmapped_login) - mock_obj.assert_not_called() - - # Attempt to apply groups if everything checks out - social_account_added.send(SocialLogin, sociallogin=discord_login) - mock_obj.assert_called_with(self.discord_user, self.social_user) - - def test_social_updated(self): - """Test the social-account-updated Allauth signal handling.""" - mock_obj = mock.Mock() - - discord_login = SocialLogin(self.django_user, self.social_user) - github_login = SocialLogin(self.django_user, self.social_user_github) - unmapped_login = SocialLogin(self.django_user, self.social_unmapped) - - with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj): - AllauthSignalListener() - - # Don't attempt to apply groups if the user doesn't have a linked Discord account - social_account_updated.send(SocialLogin, sociallogin=github_login) - mock_obj.assert_not_called() - - # Don't attempt to apply groups if the user hasn't joined the Discord server - social_account_updated.send(SocialLogin, sociallogin=unmapped_login) - mock_obj.assert_not_called() - - # Attempt to apply groups if everything checks out - social_account_updated.send(SocialLogin, sociallogin=discord_login) - mock_obj.assert_called_with(self.discord_user, self.social_user) - - def test_social_removed(self): - """Test the social-account-removed Allauth signal handling.""" - mock_obj = mock.Mock() - - with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj): - AllauthSignalListener() - - # Don't attempt to remove groups if the user doesn't have a linked Discord account - social_account_removed.send(SocialLogin, socialaccount=self.social_user_github) - mock_obj.assert_not_called() - - # Don't attempt to remove groups if the social account doesn't map to a Django user - social_account_removed.send(SocialLogin, socialaccount=self.social_unmapped) - mock_obj.assert_not_called() - - # Attempt to remove groups if everything checks out - social_account_removed.send(SocialLogin, socialaccount=self.social_user) - mock_obj.assert_called_with(self.discord_user, self.social_user, deletion=True) - - def test_logged_in(self): - """Test the user-logged-in Allauth signal handling.""" - mock_obj = mock.Mock() - - with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj): - AllauthSignalListener() - - # Don't attempt to apply groups if the user doesn't have a linked Discord account - user_logged_in.send(DjangoUser, user=self.django_user_discordless) - mock_obj.assert_not_called() - - # Don't attempt to apply groups if the user hasn't joined the Discord server - user_logged_in.send(DjangoUser, user=self.django_user_never_joined) - mock_obj.assert_not_called() - - # Attempt to apply groups if everything checks out - user_logged_in.send(DjangoUser, user=self.django_user) - mock_obj.assert_called_with(self.discord_user, self.social_user) - - def test_apply_groups_admin(self): - """Test application of groups by role, relating to an admin user.""" - handler = AllauthSignalListener() - - self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - - # Apply groups based on admin role being present on Discord - handler._apply_groups(self.discord_admin, self.social_admin) - self.assertTrue(self.admin_group in self.django_admin.groups.all()) - - # Remove groups based on the user apparently leaving the server - handler._apply_groups(self.discord_admin, self.social_admin, True) - self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - - # Apply the admin role again - handler._apply_groups(self.discord_admin, self.social_admin) - - # Remove all of the roles from the user - self.discord_admin.roles.clear() - - # Remove groups based on the user no longer having the admin role on Discord - handler._apply_groups(self.discord_admin, self.social_admin) - self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - - self.discord_admin.roles.append(self.admin_role.id) - self.discord_admin.save() - - def test_apply_groups_moderator(self): - """Test application of groups by role, relating to a non-`is_staff` moderator user.""" - handler = AllauthSignalListener() - - self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - - # Apply groups based on moderator role being present on Discord - handler._apply_groups(self.discord_moderator, self.social_moderator) - self.assertTrue(self.moderator_group in self.django_moderator.groups.all()) - - # Remove groups based on the user apparently leaving the server - handler._apply_groups(self.discord_moderator, self.social_moderator, True) - self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - - # Apply the moderator role again - handler._apply_groups(self.discord_moderator, self.social_moderator) - - # Remove all of the roles from the user - self.discord_moderator.roles.clear() - - # Remove groups based on the user no longer having the moderator role on Discord - handler._apply_groups(self.discord_moderator, self.social_moderator) - self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - - self.discord_moderator.roles.append(self.moderator_role.id) - self.discord_moderator.save() - - def test_apply_groups_other(self): - """Test application of groups by role, relating to non-standard cases.""" - handler = AllauthSignalListener() - - self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - - # No groups should be applied when there's no user account yet - handler._apply_groups(self.discord_unmapped, self.social_unmapped) - self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - - # No groups should be applied when there are only unmapped roles to match - handler._apply_groups(self.discord_unmapped, self.social_user) - self.assertEqual(self.django_user.groups.all().count(), 0) - - # No groups should be applied when the user isn't in the guild - handler._apply_groups(self.discord_not_in_guild, self.social_user) - self.assertEqual(self.django_user.groups.all().count(), 0) - - def test_role_mapping_str(self): - """Test that role mappings stringify correctly.""" - self.assertEqual( - str(self.admin_mapping), - f"@{self.admin_role.name} -> {self.admin_group.name}" - ) - - def test_role_mapping_changes(self): - """Test that role mapping listeners work when changes are made.""" - # Set up (just for this test) - self.django_moderator.groups.add(self.moderator_group) - self.django_admin.groups.add(self.admin_group) - - self.assertEqual(self.django_moderator.groups.all().count(), 1) - self.assertEqual(self.django_admin.groups.all().count(), 1) - - # Test is_staff changes - self.admin_mapping.is_staff = False - self.admin_mapping.save() - - self.assertFalse(self.django_moderator.is_staff) - self.assertFalse(self.django_admin.is_staff) - - self.admin_mapping.is_staff = True - self.admin_mapping.save() - - self.django_admin.refresh_from_db(fields=("is_staff", )) - self.assertTrue(self.django_admin.is_staff) - - # Test mapping deletion - self.admin_mapping.delete() - - self.django_admin.refresh_from_db(fields=("is_staff",)) - self.assertEqual(self.django_admin.groups.all().count(), 0) - self.assertFalse(self.django_admin.is_staff) - - # Test mapping update - self.moderator_mapping.group = self.admin_group - self.moderator_mapping.save() - - self.assertEqual(self.django_moderator.groups.all().count(), 1) - self.assertTrue(self.admin_group in self.django_moderator.groups.all()) - - # Test mapping creation - new_mapping = RoleMapping.objects.create( - role=self.admin_role, - group=self.moderator_group, - is_staff=True - ) - - self.assertEqual(self.django_admin.groups.all().count(), 1) - self.assertTrue(self.moderator_group in self.django_admin.groups.all()) - - self.django_admin.refresh_from_db(fields=("is_staff",)) - self.assertTrue(self.django_admin.is_staff) - - new_mapping.delete() - - # Test mapping creation (without is_staff) - new_mapping = RoleMapping.objects.create( - role=self.admin_role, - group=self.moderator_group, - ) - - self.assertEqual(self.django_admin.groups.all().count(), 1) - self.assertTrue(self.moderator_group in self.django_admin.groups.all()) - - self.django_admin.refresh_from_db(fields=("is_staff",)) - self.assertFalse(self.django_admin.is_staff) - - # Test that nothing happens when fixtures are loaded - pre_save.send(RoleMapping, instance=new_mapping, raw=True) - - self.assertEqual(self.django_admin.groups.all().count(), 1) - self.assertTrue(self.moderator_group in self.django_admin.groups.all()) diff --git a/pydis_site/apps/home/tests/test_views.py b/pydis_site/apps/home/tests/test_views.py index 40c80205..bd1671b1 100644 --- a/pydis_site/apps/home/tests/test_views.py +++ b/pydis_site/apps/home/tests/test_views.py @@ -1,198 +1,5 @@ -from allauth.socialaccount.models import SocialAccount -from django.contrib.auth.models import User -from django.http import HttpResponseRedirect from django.test import TestCase -from django_hosts.resolvers import get_host, reverse, reverse_host - - -def check_redirect_url( - response: HttpResponseRedirect, reversed_url: str, strip_params=True -) -> bool: - """ - Check whether a given redirect response matches a specific reversed URL. - - Arguments: - * `response`: The HttpResponseRedirect returned by the test client - * `reversed_url`: The URL returned by `reverse()` - * `strip_params`: Whether to strip URL parameters (following a "?") from the URL given in the - `response` object - """ - host = get_host(None) - hostname = reverse_host(host) - - redirect_url = response.url - - if strip_params and "?" in redirect_url: - redirect_url = redirect_url.split("?", 1)[0] - - result = reversed_url == f"//{hostname}{redirect_url}" - return result - - -class TestAccountDeleteView(TestCase): - def setUp(self) -> None: - """Create an authorized Django user for testing purposes.""" - self.user = User.objects.create( - username="user#0000" - ) - - def test_redirect_when_logged_out(self): - """Test that the user is redirected to the homepage when not logged in.""" - url = reverse("account_delete") - resp = self.client.get(url) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, reverse("home"))) - - def test_get_when_logged_in(self): - """Test that the view returns a HTTP 200 when the user is logged in.""" - url = reverse("account_delete") - - self.client.force_login(self.user) - resp = self.client.get(url) - self.client.logout() - - self.assertEqual(resp.status_code, 200) - - def test_post_invalid(self): - """Test that the user is redirected when the form is filled out incorrectly.""" - url = reverse("account_delete") - - self.client.force_login(self.user) - - resp = self.client.post(url, {}) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, url)) - - resp = self.client.post(url, {"username": "user"}) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, url)) - - self.client.logout() - - def test_post_valid(self): - """Test that the account is deleted when the form is filled out correctly..""" - url = reverse("account_delete") - - self.client.force_login(self.user) - - resp = self.client.post(url, {"username": "user#0000"}) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, reverse("home"))) - - with self.assertRaises(User.DoesNotExist): - User.objects.get(username=self.user.username) - - self.client.logout() - - -class TestAccountSettingsView(TestCase): - def setUp(self) -> None: - """Create an authorized Django user for testing purposes.""" - self.user = User.objects.create( - username="user#0000" - ) - - self.user_unlinked = User.objects.create( - username="user#9999" - ) - - self.user_unlinked_discord = User.objects.create( - username="user#1234" - ) - - self.user_unlinked_github = User.objects.create( - username="user#1111" - ) - - self.github_account = SocialAccount.objects.create( - user=self.user, - provider="github", - uid="0" - ) - - self.discord_account = SocialAccount.objects.create( - user=self.user, - provider="discord", - uid="0000" - ) - - self.github_account_secondary = SocialAccount.objects.create( - user=self.user_unlinked_discord, - provider="github", - uid="1" - ) - - self.discord_account_secondary = SocialAccount.objects.create( - user=self.user_unlinked_github, - provider="discord", - uid="1111" - ) - - def test_redirect_when_logged_out(self): - """Check that the user is redirected to the homepage when not logged in.""" - url = reverse("account_settings") - resp = self.client.get(url) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, reverse("home"))) - - def test_get_when_logged_in(self): - """Test that the view returns a HTTP 200 when the user is logged in.""" - url = reverse("account_settings") - - self.client.force_login(self.user) - resp = self.client.get(url) - self.client.logout() - - self.assertEqual(resp.status_code, 200) - - self.client.force_login(self.user_unlinked) - resp = self.client.get(url) - self.client.logout() - - self.assertEqual(resp.status_code, 200) - - self.client.force_login(self.user_unlinked_discord) - resp = self.client.get(url) - self.client.logout() - - self.assertEqual(resp.status_code, 200) - - self.client.force_login(self.user_unlinked_github) - resp = self.client.get(url) - self.client.logout() - - self.assertEqual(resp.status_code, 200) - - def test_post_invalid(self): - """Test the behaviour of invalid POST submissions.""" - url = reverse("account_settings") - - self.client.force_login(self.user_unlinked) - - resp = self.client.post(url, {"provider": "discord"}) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, reverse("home"))) - - resp = self.client.post(url, {"provider": "github"}) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, reverse("home"))) - - self.client.logout() - - def test_post_valid(self): - """Ensure that GitHub is unlinked with a valid POST submission.""" - url = reverse("account_settings") - - self.client.force_login(self.user) - - resp = self.client.post(url, {"provider": "github"}) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, reverse("home"))) - - with self.assertRaises(SocialAccount.DoesNotExist): - SocialAccount.objects.get(user=self.user, provider="github") - - self.client.logout() +from django_hosts.resolvers import reverse class TestIndexReturns200(TestCase): @@ -201,29 +8,3 @@ class TestIndexReturns200(TestCase): url = reverse('home') resp = self.client.get(url) self.assertEqual(resp.status_code, 200) - - -class TestTimelineReturns200(TestCase): - def test_timeline_returns_200(self): - """Check that the timeline page returns a HTTP 200 response.""" - url = reverse('timeline') - resp = self.client.get(url) - self.assertEqual(resp.status_code, 200) - - -class TestLoginCancelledReturns302(TestCase): - def test_login_cancelled_returns_302(self): - """Check that the login cancelled redirect returns a HTTP 302 response.""" - url = reverse('socialaccount_login_cancelled') - resp = self.client.get(url) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, reverse("home"))) - - -class TestLoginErrorReturns302(TestCase): - def test_login_error_returns_302(self): - """Check that the login error redirect returns a HTTP 302 response.""" - url = reverse('socialaccount_login_error') - resp = self.client.get(url) - self.assertEqual(resp.status_code, 302) - self.assertTrue(check_redirect_url(resp, reverse("home"))) diff --git a/pydis_site/apps/home/tests/test_wiki_templatetags.py b/pydis_site/apps/home/tests/test_wiki_templatetags.py deleted file mode 100644 index e1e2a02c..00000000 --- a/pydis_site/apps/home/tests/test_wiki_templatetags.py +++ /dev/null @@ -1,238 +0,0 @@ -from unittest.mock import Mock, create_autospec - -from django.forms import ( - BooleanField, BoundField, CharField, ChoiceField, Field, Form, ImageField, - ModelChoiceField -) -from django.template import Context, Template -from django.test import TestCase -from wiki.editors.markitup import MarkItUpWidget -from wiki.forms import WikiSlugField -from wiki.models import Article, URLPath as _URLPath -from wiki.plugins.notifications.forms import SettingsModelChoiceField - -from pydis_site.apps.home.templatetags import wiki_extra - -URLPath = Mock(_URLPath) - - -class TestURLPathFilter(TestCase): - TEMPLATE = Template( - """ - {% load wiki_extra %} - {{ obj|render_urlpath }} - """ - ) - - def test_str(self): - context = {"obj": "/path/"} - rendered = self.TEMPLATE.render(Context(context)) - - self.assertEqual(rendered.strip(), "/path/") - - def test_str_empty(self): - context = {"obj": ""} - rendered = self.TEMPLATE.render(Context(context)) - - self.assertEqual(rendered.strip(), "/") - - def test_urlpath(self): - url_path = URLPath() - url_path.path = "/path/" - - context = {"obj": url_path} - rendered = self.TEMPLATE.render(Context(context)) - - self.assertEqual(rendered.strip(), "/path/") - - def test_urlpath_root(self): - url_path = URLPath() - url_path.path = None - - context = {"obj": url_path} - rendered = self.TEMPLATE.render(Context(context)) - - self.assertEqual(rendered.strip(), "/") - - -class TestRenderField(TestCase): - TEMPLATE = Template( - """ - {% load wiki_extra %} - {% render_field field %} - """ - ) - - TEMPLATE_NO_LABELS = Template( - """ - {% load wiki_extra %} - {% render_field field render_labels=False %} - """ - ) - - TEMPLATE_LABELS_NOT_BOOLEAN = Template( - """ - {% load wiki_extra %} - {% render_field field render_labels="" %} - """ - ) - - def test_bound_field(self): - unbound_field = Field() - field = BoundField(Form(), unbound_field, "field") - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - def test_bound_field_no_labels(self): - unbound_field = Field() - field = BoundField(Form(), unbound_field, "field") - - context = Context({"field": field}) - self.TEMPLATE_NO_LABELS.render(context) - - def test_bound_field_labels_not_boolean(self): - unbound_field = Field() - field = BoundField(Form(), unbound_field, "field") - - context = Context({"field": field}) - self.TEMPLATE_LABELS_NOT_BOOLEAN.render(context) - - def test_unbound_field(self): - field = Field() - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - def test_unbound_field_no_labels(self): - field = Field() - - context = Context({"field": field}) - self.TEMPLATE_NO_LABELS.render(context) - - def test_unbound_field_labels_not_boolean(self): - field = Field() - - context = Context({"field": field}) - self.TEMPLATE_LABELS_NOT_BOOLEAN.render(context) - - -class TestRenderFieldTypes(TestCase): - TEMPLATE = Template( - """ - {% load wiki_extra %} - {% render_field field %} - """ - ) - - @classmethod - def setUpClass(cls): - cls._wiki_extra_render = wiki_extra.render - wiki_extra.render = create_autospec(wiki_extra.render, return_value="") - - @classmethod - def tearDownClass(cls): - wiki_extra.render = cls._wiki_extra_render - del cls._wiki_extra_render - - def test_field_boolean(self): - field = BooleanField() - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - template_path = "wiki/forms/fields/boolean.html" - context = {"field": field, "is_markitup": False, "render_labels": True} - - wiki_extra.render.assert_called_with(template_path, context) - - def test_field_char(self): - field = CharField() - field.widget = None - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - template_path = "wiki/forms/fields/char.html" - context = {"field": field, "is_markitup": False, "render_labels": True} - - wiki_extra.render.assert_called_with(template_path, context) - - def test_field_char_markitup(self): - field = CharField() - field.widget = MarkItUpWidget() - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - template_path = "wiki/forms/fields/char.html" - context = {"field": field, "is_markitup": True, "render_labels": True} - - wiki_extra.render.assert_called_with(template_path, context) - - def test_field_image(self): - field = ImageField() - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - template_path = "wiki/forms/fields/image.html" - context = {"field": field, "is_markitup": False, "render_labels": True} - - wiki_extra.render.assert_called_with(template_path, context) - - def test_field_model_choice(self): - field = ModelChoiceField(Article.objects.all()) - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - template_path = "wiki/forms/fields/model_choice.html" - context = {"field": field, "is_markitup": False, "render_labels": True} - - wiki_extra.render.assert_called_with(template_path, context) - - def test_field_settings_model_choice(self): - field = SettingsModelChoiceField(Article.objects.all()) - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - template_path = "wiki/forms/fields/model_choice.html" - context = {"field": field, "is_markitup": False, "render_labels": True} - - wiki_extra.render.assert_called_with(template_path, context) - - def test_field_wiki_slug(self): - field = WikiSlugField() - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - template_path = "wiki/forms/fields/wiki_slug_render.html" - context = {"field": field, "is_markitup": False, "render_labels": True} - - wiki_extra.render.assert_called_with(template_path, context) - - -class TestGetFieldOptions(TestCase): - TEMPLATE = Template( - """ - {% load wiki_extra %} - {% get_field_options field %} - """ - ) - - def test_get_field_options(self): - unbound_field = ChoiceField() - field = BoundField(Form(), unbound_field, "field") - - context = Context({"field": field}) - self.TEMPLATE.render(context) - - def test_get_field_options_value(self): - unbound_field = ChoiceField() - field = BoundField(Form(initial={"field": "Value"}), unbound_field, "field") - - context = Context({"field": field}) - self.TEMPLATE.render(context) diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index 14d118f8..1e2af8f3 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -1,42 +1,15 @@ -from allauth.account.views import LogoutView -from django.conf import settings -from django.conf.urls.static import static from django.contrib import admin -from django.contrib.messages import ERROR from django.urls import include, path -from pydis_site.utils.views import MessageRedirectView -from .views import AccountDeleteView, AccountSettingsView, HomeView, timeline +from .views import HomeView, timeline app_name = 'home' urlpatterns = [ - # We do this twice because Allauth expects specific view names to exist path('', HomeView.as_view(), name='home'), - path('', HomeView.as_view(), name='socialaccount_connections'), - - path('pages/', include('wiki.urls')), - - path('accounts/', include('allauth.socialaccount.providers.discord.urls')), - path('accounts/', include('allauth.socialaccount.providers.github.urls')), - - path( - 'accounts/login/cancelled', MessageRedirectView.as_view( - pattern_name="home", message="Login cancelled." - ), name='socialaccount_login_cancelled' - ), - path( - 'accounts/login/error', MessageRedirectView.as_view( - pattern_name="home", message="Login encountered an unknown error, please try again.", - message_level=ERROR - ), name='socialaccount_login_error' - ), - - path('accounts/settings', AccountSettingsView.as_view(), name="account_settings"), - path('accounts/delete', AccountDeleteView.as_view(), name="account_delete"), - - path('logout', LogoutView.as_view(), name="logout"), - + path('', include('pydis_site.apps.redirect.urls')), path('admin/', admin.site.urls), - path('notifications/', include('django_nyt.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')), path('timeline/', timeline, name="timeline"), -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +] diff --git a/pydis_site/apps/home/views/__init__.py b/pydis_site/apps/home/views/__init__.py index 36b88b1b..28cc4d65 100644 --- a/pydis_site/apps/home/views/__init__.py +++ b/pydis_site/apps/home/views/__init__.py @@ -1,4 +1,3 @@ -from .account import DeleteView as AccountDeleteView, SettingsView as AccountSettingsView from .home import HomeView, timeline -__all__ = ["AccountDeleteView", "AccountSettingsView", "HomeView", "timeline"] +__all__ = ["HomeView", "timeline"] diff --git a/pydis_site/apps/home/views/account/__init__.py b/pydis_site/apps/home/views/account/__init__.py deleted file mode 100644 index 3b3250ea..00000000 --- a/pydis_site/apps/home/views/account/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .delete import DeleteView -from .settings import SettingsView - -__all__ = ["DeleteView", "SettingsView"] diff --git a/pydis_site/apps/home/views/account/delete.py b/pydis_site/apps/home/views/account/delete.py deleted file mode 100644 index 798b8a33..00000000 --- a/pydis_site/apps/home/views/account/delete.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.messages import ERROR, INFO, add_message -from django.http import HttpRequest, HttpResponse -from django.shortcuts import redirect, render -from django.urls import reverse -from django.views import View - -from pydis_site.apps.home.forms.account_deletion import AccountDeletionForm - - -class DeleteView(LoginRequiredMixin, View): - """Account deletion view, for removing linked user accounts from the DB.""" - - def __init__(self, *args, **kwargs): - self.login_url = reverse("home") - super().__init__(*args, **kwargs) - - def get(self, request: HttpRequest) -> HttpResponse: - """HTTP GET: Return the view template.""" - return render( - request, "home/account/delete.html", - context={"form": AccountDeletionForm()} - ) - - def post(self, request: HttpRequest) -> HttpResponse: - """HTTP POST: Process the deletion, as requested by the user.""" - form = AccountDeletionForm(request.POST) - - if not form.is_valid() or request.user.username != form.cleaned_data["username"]: - add_message(request, ERROR, "Please enter your username exactly as shown.") - - return redirect(reverse("account_delete")) - - request.user.delete() - add_message(request, INFO, "Your account has been deleted.") - - return redirect(reverse("home")) diff --git a/pydis_site/apps/home/views/account/settings.py b/pydis_site/apps/home/views/account/settings.py deleted file mode 100644 index 3a817dbc..00000000 --- a/pydis_site/apps/home/views/account/settings.py +++ /dev/null @@ -1,59 +0,0 @@ -from allauth.socialaccount.models import SocialAccount -from allauth.socialaccount.providers import registry -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.messages import ERROR, INFO, add_message -from django.http import HttpRequest, HttpResponse -from django.shortcuts import redirect, render -from django.urls import reverse -from django.views import View - - -class SettingsView(LoginRequiredMixin, View): - """ - Account settings view, for managing and deleting user accounts and connections. - - This view actually renders a template with a bare modal, and is intended to be - inserted into another template using JavaScript. - """ - - def __init__(self, *args, **kwargs): - self.login_url = reverse("home") - super().__init__(*args, **kwargs) - - def get(self, request: HttpRequest) -> HttpResponse: - """HTTP GET: Return the view template.""" - context = { - "groups": request.user.groups.all(), - - "discord": None, - "github": None, - - "discord_provider": registry.provider_map.get("discord"), - "github_provider": registry.provider_map.get("github"), - } - - for account in SocialAccount.objects.filter(user=request.user).all(): - if account.provider == "discord": - context["discord"] = account - - if account.provider == "github": - context["github"] = account - - return render(request, "home/account/settings.html", context=context) - - def post(self, request: HttpRequest) -> HttpResponse: - """HTTP POST: Process account disconnections.""" - provider = request.POST["provider"] - - if provider == "github": - try: - account = SocialAccount.objects.get(user=request.user, provider=provider) - except SocialAccount.DoesNotExist: - add_message(request, ERROR, "You do not have a GitHub account linked.") - else: - account.delete() - add_message(request, INFO, "The social account has been disconnected.") - else: - add_message(request, ERROR, f"Unknown provider: {provider}") - - return redirect(reverse("home")) diff --git a/pydis_site/apps/redirect/__init__.py b/pydis_site/apps/redirect/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/redirect/__init__.py diff --git a/pydis_site/apps/redirect/apps.py b/pydis_site/apps/redirect/apps.py new file mode 100644 index 00000000..9b70d169 --- /dev/null +++ b/pydis_site/apps/redirect/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class RedirectConfig(AppConfig): + """AppConfig instance for Redirect app.""" + + name = 'redirect' diff --git a/pydis_site/apps/redirect/migrations/__init__.py b/pydis_site/apps/redirect/migrations/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/redirect/migrations/__init__.py diff --git a/pydis_site/apps/redirect/redirects.yaml b/pydis_site/apps/redirect/redirects.yaml new file mode 100644 index 00000000..ce789b61 --- /dev/null +++ b/pydis_site/apps/redirect/redirects.yaml @@ -0,0 +1,194 @@ +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# The redirects here are for dewikification backwards compatibility +# and SHOULD NOT be used for adding new redirects for convenience. +# +# Convenience redirects should be added using our cloudflare worker +# at https://github.com/python-discord/workers/tree/main/short-urls +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +# Root pages +roles_redirect: + original_path: pages/roles/ + redirect_route: "content:page_category" + redirect_arguments: ["server-info/roles"] + +roles_expectations_redirect: + original_path: pages/roles/staff-role-expectations/ + redirect_route: "content:page_category" + redirect_arguments: ["server-info/staff-role-expectations"] + +contributing_redirect: + original_path: pages/contributing/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/pydis-guides/contributing"] + +# Guides +guides_redirect: + original_path: pages/resources/guides/ + redirect_route: "content:page_category" + redirect_arguments: ["guides"] + +# - Python guides +discord_py_redirect: + original_path: pages/resources/guides/discordpy/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/python-guides/discordpy"] + +mutability_redirect: + original_path: pages/resources/guides/core-concepts/mutability/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/python-guides/mutability"] + +function_params_redirect: + original_path: pages/resources/guides/core-concepts/parameters-and-arguments/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/python-guides/parameters-and-arguments"] + +# - Pydis guides +help_channel_redirect: + original_path: pages/resources/guides/help-channels/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/pydis-guides/help-channel-guide"] + +good_questions_redirect: + original_path: pages/resources/guides/asking-good-questions/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/pydis-guides/asking-good-questions"] + +helping_others_redirect: + original_path: pages/resources/guides/helping-others/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/pydis-guides/helping-others"] + +code_review_redirect: + original_path: pages/resources/guides/code-reviews-primer/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/pydis-guides/code-reviews-primer"] + +off-topic_redirect: + original_path: pages/resources/guides/off-topic-etiquette/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/pydis-guides/off-topic-etiquette"] + +# Resources +resources_index_redirect: + original_path: pages/resources/ + redirect_route: "resources:index" + +resources_resources_redirect: + original_path: pages/resources/<str:category>/ + redirect_route: "resources:resources" + +# Events +events_index_redirect: + original_path: pages/events/ + redirect_route: "events:index" + +events_code_jams_index_redirect: + original_path: pages/code-jams/ + redirect_route: "events:page" + redirect_arguments: ["code-jams"] + +events_code_jams_one_redirect: + original_path: pages/code-jams/code-jam-1-snakes-bot/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/1"] + +events_code_jams_two_redirect: + original_path: pages/code-jams/code-jam-2/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/2"] + +events_code_jams_three_redirect: + original_path: pages/code-jams/code-jam-3/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/3"] + +events_code_jams_four_redirect: + original_path: pages/code-jams/code-jam-4/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/4"] + +events_code_jams_five_redirect: + original_path: pages/code-jams/code-jam-5/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/5"] + +events_code_jams_six_redirect: + original_path: pages/code-jams/code-jam-6/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/6"] + +events_code_jams_six_rules_redirect: + original_path: pages/code-jams/code-jam-6/rules/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/6/rules"] + +events_code_jams_seven_redirect: + original_path: pages/code-jams/code-jam-7/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/7"] + +events_code_jams_seven_rules_redirect: + original_path: pages/code-jams/code-jam-7/rules/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/7/rules"] + +events_code_jams_how_to_use_git_redirect: + original_path: pages/code-jams/using-git/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/using-git"] + +events_code_jams_judging_redirect: + original_path: pages/code-jams/judging/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/judging"] + +events_code_jams_pull_request_redirect: + original_path: pages/code-jams/pull-request/ + redirect_route: "events:page" + redirect_arguments: ["code-jams/pull-request"] + +events_game_jams_twenty_twenty_index_redirect: + original_path: pages/events/game-jam-2020/ + redirect_route: "events:page" + redirect_arguments: ["game-jams/2020"] + +events_game_jams_twenty_twenty_judging_redirect: + original_path: pages/events/game-jam-2020/judging/ + redirect_route: "events:page" + redirect_arguments: ["game-jams/2020/judging"] + +events_game_jams_twenty_twenty_project_setup_redirect: + original_path: pages/events/game-jam-2020/project-setup/ + redirect_route: "events:page" + redirect_arguments: ["game-jams/2020/project-setup"] + +events_game_jams_twenty_twenty_rules_redirect: + original_path: pages/events/game-jam-2020/rules/ + redirect_route: "events:page" + redirect_arguments: ["game-jams/2020/rules"] + +events_game_jams_twenty_twenty_technical_requirements_redirect: + original_path: pages/events/game-jam-2020/technical-requirements + redirect_route: "events:page" + redirect_arguments: ["game-jams/2020/technical-requirements"] + +# This are overrides for the contributing prefix redirect +security_notice_redirect: + original_path: pages/contributing/security-notice/ + redirect_route: "content:page_category" + redirect_arguments: ["security-notice"] + +sir-lancebot_env_var_redirect: + original_path: pages/contributing/sir-lancebot/sir-lancebot-env-var-reference/ + redirect_route: "content:page_category" + redirect_arguments: ["guides/pydis-guides/contributing/sir-lancebot/env-var-reference"] + +# Prefix redirects +# Prefix redirects must be last in each group. +guides_pydis_guides_contributing_prefix_redirect: + original_path: pages/contributing/<path:path>/ # path:path will be joined together with static arguments. + redirect_route: "content:page_category" + redirect_arguments: ["guides/pydis-guides/contributing/"] # It is important to put / at end in prefix redirect! + prefix_redirect: true diff --git a/pydis_site/apps/redirect/tests.py b/pydis_site/apps/redirect/tests.py new file mode 100644 index 00000000..fce2642f --- /dev/null +++ b/pydis_site/apps/redirect/tests.py @@ -0,0 +1,61 @@ +import yaml +from django.conf import settings +from django.test import TestCase +from django.urls import reverse + +TESTING_ARGUMENTS = { + "resources_resources_redirect": ("reading",), + "guides_pydis_guides_contributing_prefix_redirect": ("sir-lancebot/env-var-reference",), +} + + +class RedirectTests(TestCase): + """Survival tests for redirects.""" + + def test_redirects(self) -> None: + """ + Should redirect to given route based on redirect rules. + + Makes sure that every redirect: + 1. Redirects only once. + 2. Redirects to right URL. + 3. Resulting page status code is 200. + """ + for name, data in yaml.safe_load(settings.REDIRECTIONS_PATH.read_text()).items(): + with self.subTest( + original_path=data["original_path"], + redirect_route=data["redirect_route"], + name=name, + redirect_arguments=tuple(data.get("redirect_arguments", ())), + args=TESTING_ARGUMENTS.get(name, ()) + ): + resp = self.client.get( + reverse( + f"home:redirect:{name}", + args=TESTING_ARGUMENTS.get(name, ()) + ), + follow=True + ) + + if data.get("prefix_redirect", False): + expected_args = ( + "".join( + tuple(data.get("redirect_arguments", ())) + + TESTING_ARGUMENTS.get(name, ()) + ), + ) + else: + expected_args = ( + TESTING_ARGUMENTS.get(name, ()) + tuple(data.get("redirect_arguments", ())) + ) + + self.assertEqual(1, len(resp.redirect_chain)) + self.assertRedirects( + resp, + reverse( + f"home:{data['redirect_route']}", + args=expected_args + ), + status_code=302 + ) + self.assertEqual(resp.status_code, 200) diff --git a/pydis_site/apps/redirect/urls.py b/pydis_site/apps/redirect/urls.py new file mode 100644 index 00000000..6187af17 --- /dev/null +++ b/pydis_site/apps/redirect/urls.py @@ -0,0 +1,19 @@ +import yaml +from django.conf import settings +from django.urls import path + +from pydis_site.apps.redirect.views import CustomRedirectView + +app_name = "redirect" +urlpatterns = [ + path( + data["original_path"], + CustomRedirectView.as_view( + pattern_name=data["redirect_route"], + static_args=tuple(data.get("redirect_arguments", ())), + prefix_redirect=data.get("prefix_redirect", False) + ), + name=name + ) + for name, data in yaml.safe_load(settings.REDIRECTIONS_PATH.read_text()).items() +] diff --git a/pydis_site/apps/redirect/views.py b/pydis_site/apps/redirect/views.py new file mode 100644 index 00000000..9dc9881a --- /dev/null +++ b/pydis_site/apps/redirect/views.py @@ -0,0 +1,26 @@ +import typing as t + +from django.views.generic import RedirectView + + +class CustomRedirectView(RedirectView): + """Extended RedirectView for manual route args.""" + + # We want temporary redirects for the time being, after this is running on prod and + # stable we can enable permanent redirects. + permanent = False + static_args = () + prefix_redirect = False + + @classmethod + def as_view(cls, **initkwargs): + """Overwrites original as_view to add static args.""" + return super().as_view(**initkwargs) + + def get_redirect_url(self, *args, **kwargs) -> t.Optional[str]: + """Extends default behaviour to use static args.""" + args = self.static_args + args + tuple(kwargs.values()) + if self.prefix_redirect: + args = ("".join(args),) + + return super().get_redirect_url(*args) diff --git a/pydis_site/apps/resources/__init__.py b/pydis_site/apps/resources/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/resources/__init__.py diff --git a/pydis_site/apps/resources/apps.py b/pydis_site/apps/resources/apps.py new file mode 100644 index 00000000..e0c235bd --- /dev/null +++ b/pydis_site/apps/resources/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ResourcesConfig(AppConfig): + """AppConfig instance for Resources app.""" + + name = 'resources' diff --git a/pydis_site/apps/resources/migrations/__init__.py b/pydis_site/apps/resources/migrations/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/resources/migrations/__init__.py diff --git a/pydis_site/apps/home/resources/communities/_category_info.yaml b/pydis_site/apps/resources/resources/communities/_category_info.yaml index eccb8b80..b9cb6533 100644 --- a/pydis_site/apps/home/resources/communities/_category_info.yaml +++ b/pydis_site/apps/resources/resources/communities/_category_info.yaml @@ -1,2 +1,2 @@ -description: Partnered communities that share part of our mission +description: Partnered communities that share part of our mission. name: Communities diff --git a/pydis_site/apps/resources/resources/communities/adafruit.yaml b/pydis_site/apps/resources/resources/communities/adafruit.yaml new file mode 100644 index 00000000..e5c81a6c --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/adafruit.yaml @@ -0,0 +1,15 @@ +description: Adafruit is an open-source electronics manufacturer + that makes all the components you need to start your own Python-powered hardware projects. + Their official community host regular show-and-tells, + provide help with your projects, + and the Adafruit devs do all the CircuitPython Development right out in the open. +title_image: https://www.mouser.com/images/suppliers/logos/adafruit.png +title_url: https://discord.gg/adafruit +position: 4 +urls: +- icon: branding/discord + url: https://discord.gg/adafruit + color: blurple +- icon: regular/link + url: https://adafruit.com/ + color: teal diff --git a/pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml b/pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml new file mode 100644 index 00000000..335ac507 --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml @@ -0,0 +1,9 @@ +description: We have listed our favourite communities, + but there are many more excellent communities out there! + An awesome list collating the best programming related Discord servers is available on GitHub + and has all sorts of topics from blockchain to virtual reality! +title_icon: branding/github +title_icon_color: black +title_url: https://github.com/mhxion/awesome-programming-discord +name: awesome-programming-discord +position: 10 diff --git a/pydis_site/apps/resources/resources/communities/kivy.yaml b/pydis_site/apps/resources/resources/communities/kivy.yaml new file mode 100644 index 00000000..601d7dba --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/kivy.yaml @@ -0,0 +1,18 @@ +description: The Kivy project, through the Kivy framework and its sister projects, + aims to provide all the tools to create desktop and mobile applications in Python. + Allowing rapid development of multitouch applications with custom and exciting user interfaces. +icon_image: https://raw.githubusercontent.com/kivy/kivy-website/master/logos/kivy-logo-black-256.png +icon_size: 50 +title_image: https://i.imgur.com/EVP3jZR.png +title_url: https://discord.gg/djPtTRJ +position: 5 +urls: + - icon: branding/discord + url: https://discord.gg/djPtTRJ + color: blurple + - icon: regular/link + url: https://kivy.org/ + color: teal + - icon: branding/github + url: https://github.com/kivy + color: black diff --git a/pydis_site/apps/resources/resources/communities/microsoft.yaml b/pydis_site/apps/resources/resources/communities/microsoft.yaml new file mode 100644 index 00000000..b36c3a85 --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/microsoft.yaml @@ -0,0 +1,12 @@ +description: Microsoft Python is a Discord server for discussing all things relating to using Python with Microsoft products, + they have channels for Azure, VS Code, IoT, Data Science and much more! +title_image: https://1000logos.net/wp-content/uploads/2017/04/Microsoft-Logo.png +title_url: https://discord.gg/b8YJQPx +position: 1 +urls: + - icon: branding/discord + url: https://discord.gg/b8YJQPx + color: blurple + - icon: regular/link + url: https://www.microsoft.com/en-us/boards/pycon2020.aspx + color: teal diff --git a/pydis_site/apps/resources/resources/communities/pallets.yaml b/pydis_site/apps/resources/resources/communities/pallets.yaml new file mode 100644 index 00000000..239b1491 --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/pallets.yaml @@ -0,0 +1,13 @@ +description: The Pallets Projects develop Python libraries such as the Flask web framework, + the Jinja templating library, and the Click command line toolkit. Join to discuss + and get help from the Pallets community. +title_image: https://i.imgur.com/sV9Ypdf.png +title_url: https://discord.gg/t6rrQZH +position: 6 +urls: + - icon: branding/discord + url: https://discord.gg/t6rrQZH + color: blurple + - icon: regular/link + url: https://www.palletsprojects.com/ + color: teal diff --git a/pydis_site/apps/resources/resources/communities/panda3d.yaml b/pydis_site/apps/resources/resources/communities/panda3d.yaml new file mode 100644 index 00000000..4235793d --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/panda3d.yaml @@ -0,0 +1,12 @@ +description: Panda3D is a Python-focused 3-D framework for rapid development of games, + visualizations, and simulations, written in C++ with an emphasis on performance and flexibility. +title_image: http://www.panda3d.org/wp-content/uploads/2019/01/panda3d_logo.png +title_url: https://discord.gg/9XsucTT +position: 9 +urls: + - icon: branding/discord + url: https://discord.gg/9XsucTT + color: blurple + - icon: regular/link + url: https://www.panda3d.org/ + color: teal diff --git a/pydis_site/apps/resources/resources/communities/people_postgres_data.yaml b/pydis_site/apps/resources/resources/communities/people_postgres_data.yaml new file mode 100644 index 00000000..1c17d343 --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/people_postgres_data.yaml @@ -0,0 +1,18 @@ +description: People, Postgres, Data specializes in building users of Postgres + and related ecosystem including but not limited to technologies such as RDS Postgres, + Aurora for Postgres, Google Postgres, PostgreSQL.Org Postgres, Greenplum, Timescale and ZomboDB. + They take a holistic approach to their community inviting not only technical topics but Professional Development + and Life in general including movies, games, books and travel. +title_image: https://media.discordapp.net/attachments/748954447857844318/750519488268730377/people_postgres_data.png +title_url: https://discord.gg/Ujw8m8v +position: 2 +urls: + - icon: branding/discord + url: https://discord.gg/Ujw8m8v + color: bluple + - icon: regular/link + url: https://postgresconf.org/ + color: teal + - icon: branding/reddit + url: https://reddit.com/r/postgresql + color: orangered diff --git a/pydis_site/apps/resources/resources/communities/pyglet.yaml b/pydis_site/apps/resources/resources/communities/pyglet.yaml new file mode 100644 index 00000000..784f514e --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/pyglet.yaml @@ -0,0 +1,15 @@ +description: Pyglet is a powerful, + yet easy to use Python library for developing games and other visually-rich applications on Windows, + Mac OS X and Linux. It supports windowing, user interface event handling, Joysticks, OpenGL graphics, + loading images and videos, and playing sounds and music. All of this with a friendly Pythonic API, + that's simple to learn and doesn't get in your way. +title_image: https://i.imgur.com/LfQwXUe.png +title_url: https://discord.gg/QXyegWe +position: 8 +urls: + - icon: branding/discord + url: https://discord.gg/QXyegWe + color: blurple + - icon: regular/link + url: http://pyglet.org/ + color: teal diff --git a/pydis_site/apps/resources/resources/communities/real_python.yaml b/pydis_site/apps/resources/resources/communities/real_python.yaml new file mode 100644 index 00000000..1fc74d93 --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/real_python.yaml @@ -0,0 +1,12 @@ +description: Dan Bader's treasure trove of quizzes, tutorials and interactive content for learning Python. + An absolute goldmine. +title_image: https://i.imgur.com/WDqhZ36.png +title_url: https://realpython.com/ +position: 3 +urls: + - icon: regular/link + url: https://realpython.com/ + color: teal + - icon: branding/youtube + url: https://www.youtube.com/channel/UCI0vQvr9aFn27yR6Ej6n5UA + color: youtube-red diff --git a/pydis_site/apps/resources/resources/communities/rlbot.yaml b/pydis_site/apps/resources/resources/communities/rlbot.yaml new file mode 100644 index 00000000..d12c1dec --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/rlbot.yaml @@ -0,0 +1,13 @@ +description: RLBot is a community of programmers making awesome Rocket League bots. + They've created a framework that you can use to write bots in a number of languages (including Python), + and they host regular tournaments where botmakers can pit their creations against each other. +title_image: https://i.imgur.com/S8L1muZ.png +title_url: https://discord.gg/4JJdJKb +position: 7 +urls: + - icon: branding/discord + url: https://discord.gg/4JJdJKb + color: blurple + - icon: regular/link + url: https://www.rlbot.org/ + color: teal diff --git a/pydis_site/apps/resources/resources/communities/subreddit.yaml b/pydis_site/apps/resources/resources/communities/subreddit.yaml new file mode 100644 index 00000000..d3ddb15a --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/subreddit.yaml @@ -0,0 +1,6 @@ +description: News about the Python programming language, and language-related discussion. +name: r/Python +title_icon: branding/reddit +title_icon_color: orangered +title_url: https://www.reddit.com/r/Python/ +position: 0 diff --git a/pydis_site/apps/resources/resources/courses/_category_info.yaml b/pydis_site/apps/resources/resources/courses/_category_info.yaml new file mode 100644 index 00000000..948b48de --- /dev/null +++ b/pydis_site/apps/resources/resources/courses/_category_info.yaml @@ -0,0 +1,4 @@ +description: Listing of best Python courses. +name: Courses +default_icon: regular/graduation-cap +default_icon_color: black diff --git a/pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml b/pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml new file mode 100644 index 00000000..66034ea2 --- /dev/null +++ b/pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml @@ -0,0 +1,5 @@ +description: The interactive course version of Al Sweigart's excellent book for beginners, taught by the author himself. + This link has a discounted version of the course which will always cost 10 dollars. Thanks, Al! +name: Automate the Boring Stuff with Python +title_url: https://www.udemy.com/automate/?couponCode=FOR_LIKE_10_BUCKS +position: 3 diff --git a/pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml b/pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml new file mode 100644 index 00000000..5560b2cb --- /dev/null +++ b/pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml @@ -0,0 +1,6 @@ +description: This MITx offering teaches computer science with Python. + It covers computational thinking, algorithms, data structures + and the Python programming language itself. +name: 'MIT: Introduction to Computer Science and Programming' +title_url: https://www.edx.org/course/introduction-computer-science-mitx-6-00-1x-11 +position: 1 diff --git a/pydis_site/apps/resources/resources/courses/practical_python_programming.yaml b/pydis_site/apps/resources/resources/courses/practical_python_programming.yaml new file mode 100644 index 00000000..b801ca8c --- /dev/null +++ b/pydis_site/apps/resources/resources/courses/practical_python_programming.yaml @@ -0,0 +1,9 @@ +description: Created and taught by <a href="https://dabeaz.com/">David Beazley</a>, + this course is a conversion of his instructor-led Python training course used for corporate training + and professional development. It has been in continual development since 2007 + and battle tested in real-world classrooms. Usually, it’s taught in-person over the span of three + or four days–requiring approximately 25-35 hours of intense work. + This includes the completion of approximately 130 hands-on coding exercises. +name: Practical Python Programming +title_url: https://dabeaz-course.github.io/practical-python/ +position: 4 diff --git a/pydis_site/apps/resources/resources/courses/university_of_michigan.yaml b/pydis_site/apps/resources/resources/courses/university_of_michigan.yaml new file mode 100644 index 00000000..3efe7640 --- /dev/null +++ b/pydis_site/apps/resources/resources/courses/university_of_michigan.yaml @@ -0,0 +1,5 @@ +description: A 5-part specialization course that teaches Python from scratch. + The course has no pre-requisites and avoids all but the simplest mathematics. +name: 'University of Michigan: Programming for Everybody' +title_url: https://www.coursera.org/learn/python +position: 2 diff --git a/pydis_site/apps/resources/resources/courses/university_of_toronto.yaml b/pydis_site/apps/resources/resources/courses/university_of_toronto.yaml new file mode 100644 index 00000000..0a7839de --- /dev/null +++ b/pydis_site/apps/resources/resources/courses/university_of_toronto.yaml @@ -0,0 +1,11 @@ +description: A 2-part course that teaches Python. Primarily intended for high school students + and first-year university students who want to learn programming. +name: 'University of Toronto: Learn to Program' +position: 0 +urls: + - icon: regular/graduation-cap + url: https://www.coursera.org/learn/learn-to-program + color: orangered + - icon: regular/graduation-cap + url: https://www.coursera.org/learn/program-code + color: youtube-red diff --git a/pydis_site/apps/resources/resources/interactive/_category_info.yaml b/pydis_site/apps/resources/resources/interactive/_category_info.yaml new file mode 100644 index 00000000..7e8f34d9 --- /dev/null +++ b/pydis_site/apps/resources/resources/interactive/_category_info.yaml @@ -0,0 +1,4 @@ +description: Learn Python with interactive courses, games, and programming challenges. +name: Interactive +default_icon: branding/python +default_icon_color: black diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/code_combat.yaml b/pydis_site/apps/resources/resources/interactive/code_combat.yaml index 39c25f0d..30f20c28 100644 --- a/pydis_site/apps/home/resources/interactive_learning_tools/code_combat.yaml +++ b/pydis_site/apps/resources/resources/interactive/code_combat.yaml @@ -1,13 +1,11 @@ description: Learn Python while gaming - an open-source project with thousands of contributors, which teaches you Python through a deep, top-down RPG. name: Code Combat -payment: optional -payment_description: A wealth of free content is available, but you can also pay for - more +position: 0 urls: - icon: regular/link - title: Website url: https://codecombat.com/ + color: teal - icon: branding/github - title: GitHub url: https://github.com/codecombat/codecombat + color: black diff --git a/pydis_site/apps/resources/resources/interactive/edublocks.yaml b/pydis_site/apps/resources/resources/interactive/edublocks.yaml new file mode 100644 index 00000000..7c6ca02b --- /dev/null +++ b/pydis_site/apps/resources/resources/interactive/edublocks.yaml @@ -0,0 +1,10 @@ +description: EduBlocks provides a simple drag and drop interface to help beginners get to grips + with the key concepts of Python. There is built-in support for modules such as random, + turtle, processing and pygal to play around with, + and it even allows you to export the Python code you have written in the graphical editor. + EduBlocks also has integration with BBC micro:bit, + Raspberry Pi and CircuitPython allowing you to write code for these devices graphically + and export the code to run on actual devices. +name: EduBlocks +title_url: https://edublocks.org/ +position: 5 diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/exercism.yaml b/pydis_site/apps/resources/resources/interactive/exercism.yaml index 3adb4138..68b458d0 100644 --- a/pydis_site/apps/home/resources/interactive_learning_tools/exercism.yaml +++ b/pydis_site/apps/resources/resources/interactive/exercism.yaml @@ -3,12 +3,11 @@ description: Level up your programming skills with more than 2600 exercises acro where you can get your code reviewed for each solution you submit. The mentors will give you insightful advice to make you a better programmer. name: exercism.io -payment: free -payment_description: null +position: 1 urls: - icon: regular/link - title: Website url: https://exercism.io/ + color: teal - icon: branding/github - title: GitHub url: https://github.com/exercism/python + color: black diff --git a/pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml b/pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml new file mode 100644 index 00000000..937831fa --- /dev/null +++ b/pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml @@ -0,0 +1,8 @@ +description: Learn Python with a wide range of high quality, project-based lessons. + Keep track of your progress as you cover all the basic concepts a Python programmer needs to know, + as well as touching on more advanced areas such as web development with Django, + natural language processing with NLTK, and data science with NumPy, pandas, and scikit-learn! + It requires a paid subscription, but a free trial is available. +name: JetBrains Academy +title_url: https://www.jetbrains.com/academy/ +position: 6 diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/python_morsels.yaml b/pydis_site/apps/resources/resources/interactive/python_morsels.yaml index f883f8b7..879500eb 100644 --- a/pydis_site/apps/home/resources/interactive_learning_tools/python_morsels.yaml +++ b/pydis_site/apps/resources/resources/interactive/python_morsels.yaml @@ -6,10 +6,5 @@ description: 'Learn to write more idiomatic Python code with deliberate practice to the exercise with explanations of each one. Each exercise will include automated tests and some may include bonuses for a little more of a challenge!' name: Python Morsels -payment: paid -payment_description: Paid service with monthly and annual plans. A 4 week free trial - is available without needing to enter payment information. -urls: -- icon: regular/link - title: Website - url: https://www.pythonmorsels.com/ +title_url: https://www.pythonmorsels.com/ +position: 3 diff --git a/pydis_site/apps/resources/resources/interactive/python_tutor.yaml b/pydis_site/apps/resources/resources/interactive/python_tutor.yaml new file mode 100644 index 00000000..64b50d09 --- /dev/null +++ b/pydis_site/apps/resources/resources/interactive/python_tutor.yaml @@ -0,0 +1,4 @@ +description: Write Python code in your web browser, and see it visualized step by step. +name: Python Tutor +title_url: https://www.pythontutor.com/ +position: 2 diff --git a/pydis_site/apps/resources/resources/interactive/sololearn.yaml b/pydis_site/apps/resources/resources/interactive/sololearn.yaml new file mode 100644 index 00000000..51dceb2a --- /dev/null +++ b/pydis_site/apps/resources/resources/interactive/sololearn.yaml @@ -0,0 +1,7 @@ +description: SoloLearn's Python 3 course serves as a simple and convenient introduction to Python. + Containing exercises and quizzes in modules to cover introductory subjects of the language, + you can pick it up and put it down between your busier aspects of life thanks to both PC + and mobile apps being available to use. +name: SoloLearn +title_url: https://www.sololearn.com/Course/Python/ +position: 4 diff --git a/pydis_site/apps/resources/resources/podcasts/_category_info.yaml b/pydis_site/apps/resources/resources/podcasts/_category_info.yaml new file mode 100644 index 00000000..1d2d3ba5 --- /dev/null +++ b/pydis_site/apps/resources/resources/podcasts/_category_info.yaml @@ -0,0 +1,4 @@ +description: Notable podcasts about the Python ecosystem. +name: Podcasts +default_icon: regular/microphone-alt +default_icon_color: black diff --git a/pydis_site/apps/home/resources/podcasts/podcast_dunder_init.yaml b/pydis_site/apps/resources/resources/podcasts/podcast_dunder_init.yaml index 8f0cac8b..efe1601f 100644 --- a/pydis_site/apps/home/resources/podcasts/podcast_dunder_init.yaml +++ b/pydis_site/apps/resources/resources/podcasts/podcast_dunder_init.yaml @@ -1,9 +1,5 @@ description: The podcast about Python and the people who make it great. Weekly long-form interviews with the creators of notable Python packages. name: Podcast.__init__ -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://www.podcastinit.com/ +title_url: https://www.podcastinit.com/ +position: 2 diff --git a/pydis_site/apps/home/resources/podcasts/python_bytes.yaml b/pydis_site/apps/resources/resources/podcasts/python_bytes.yaml index a3368d23..4f817f26 100644 --- a/pydis_site/apps/home/resources/podcasts/python_bytes.yaml +++ b/pydis_site/apps/resources/resources/podcasts/python_bytes.yaml @@ -1,9 +1,5 @@ description: A byte-sized podcast where Michael Kennedy and Brian Okken work through this week's notable Python headlines. name: Python Bytes -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://pythonbytes.fm/ +title_url: https://pythonbytes.fm/ +position: 1 diff --git a/pydis_site/apps/home/resources/podcasts/talk_python_to_me.yaml b/pydis_site/apps/resources/resources/podcasts/talk_python_to_me.yaml index 5ed101c4..5ce21fd7 100644 --- a/pydis_site/apps/home/resources/podcasts/talk_python_to_me.yaml +++ b/pydis_site/apps/resources/resources/podcasts/talk_python_to_me.yaml @@ -1,9 +1,5 @@ description: The essential weekly Python podcast. Michael Kennedy and a prominent name within the Python community dive into a topic that relates to their experience. name: Talk Python To Me -payment: free -payment_description: null -urls: -- icon: regular/link - title: Website - url: https://talkpython.fm/ +title_url: https://talkpython.fm/ +position: 0 diff --git a/pydis_site/apps/resources/resources/podcasts/test_and_code.yaml b/pydis_site/apps/resources/resources/podcasts/test_and_code.yaml new file mode 100644 index 00000000..d5751577 --- /dev/null +++ b/pydis_site/apps/resources/resources/podcasts/test_and_code.yaml @@ -0,0 +1,5 @@ +description: Brian Okken's weekly podcast on testing. Usually deals with Python, + but also covers many language-agnostic topics from the testing and DevOps world. +name: Test & Code +title_url: https://testandcode.com/ +position: 3 diff --git a/pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml b/pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml new file mode 100644 index 00000000..dea894ea --- /dev/null +++ b/pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml @@ -0,0 +1,7 @@ +description: A weekly Python podcast hosted by Christopher Bailey with interviews, + coding tips, and conversation with guests from the Python community. + The show covers a wide range of topics including Python programming best practices, + career tips, and related software development topics. +name: The Real Python Podcast +title_url: https://realpython.com/podcasts/rpp/ +position: 4 diff --git a/pydis_site/apps/resources/resources/reading/_category_info.yaml b/pydis_site/apps/resources/resources/reading/_category_info.yaml new file mode 100644 index 00000000..64b87e47 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/_category_info.yaml @@ -0,0 +1,2 @@ +description: Books and tutorials related to Python and popular third-party libraries and frameworks. +name: Reading diff --git a/pydis_site/apps/home/resources/books/_category_info.yaml b/pydis_site/apps/resources/resources/reading/books/_category_info.yaml index e3b89ad3..ae092a20 100644 --- a/pydis_site/apps/home/resources/books/_category_info.yaml +++ b/pydis_site/apps/resources/resources/reading/books/_category_info.yaml @@ -1,2 +1,5 @@ -description: The best books for learning Python or Python Frameworks +description: The best books for learning Python or Python Frameworks. name: Books +default_icon: branding/python +default_icon_color: black +position: 0 diff --git a/pydis_site/apps/home/resources/books/automate_the_boring_stuff.yaml b/pydis_site/apps/resources/resources/reading/books/automate_the_boring_stuff.yaml index 3a9045a5..3812029c 100644 --- a/pydis_site/apps/home/resources/books/automate_the_boring_stuff.yaml +++ b/pydis_site/apps/resources/resources/reading/books/automate_the_boring_stuff.yaml @@ -4,13 +4,11 @@ description: One of the best books out there for Python beginners. This book wil the web, manipulating files and automating keyboard and mouse input. Ideal for an office worker who wants to make himself more useful. name: Automate the Boring Stuff with Python -payment: optional -payment_description: A free e-book is available on the website, but you can buy it - on Amazon if you want to support the author. +position: 2 urls: -- icon: regular/link - title: E-book +- icon: regular/book url: https://automatetheboringstuff.com/ + color: black - icon: branding/amazon - title: Amazon url: https://www.amazon.com/Automate-Boring-Stuff-Python-Programming/dp/1593275994/ + color: amazon-orange diff --git a/pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml b/pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml new file mode 100644 index 00000000..1f9642ad --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml @@ -0,0 +1,15 @@ +description: A free book on programming using the Python language. + It serves as a tutorial or guide to the Python language for a beginner audience. + If all you know about computers is how to save text files, then this is the book for you. +name: A Byte of Python +position: 1 +urls: +- icon: regular/link + url: https://python.swaroopch.com/ + color: teal +- icon: regular/book + url: http://www.lulu.com/shop/swaroop-c-h/a-byte-of-python/paperback/product-21142968.html + color: black +- icon: branding/amazon + url: https://www.amazon.com/Byte-Python-Swaroop-C-H-ebook/dp/B00FJ7S2JU/ + color: amazon-orange diff --git a/pydis_site/apps/home/resources/books/effective_python.yaml b/pydis_site/apps/resources/resources/reading/books/effective_python.yaml index 7f9d0dea..becd0578 100644 --- a/pydis_site/apps/home/resources/books/effective_python.yaml +++ b/pydis_site/apps/resources/resources/reading/books/effective_python.yaml @@ -1,14 +1,15 @@ description: A book that gives 90 best practices for writing excellent Python. Great for intermediates. name: Effective Python -payment: paid +position: 3 urls: - icon: regular/link - title: Website url: https://effectivepython.com/ + color: teal - icon: branding/amazon - title: Amazon url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134853989 + color: amazon-orange + title: Amazon - icon: branding/github - title: GitHub url: https://github.com/bslatkin/effectivepython + color: black diff --git a/pydis_site/apps/home/resources/books/flask_web_development.yaml b/pydis_site/apps/resources/resources/reading/books/flask_web_development.yaml index 613e0e50..cc83a331 100644 --- a/pydis_site/apps/home/resources/books/flask_web_development.yaml +++ b/pydis_site/apps/resources/resources/reading/books/flask_web_development.yaml @@ -1,14 +1,14 @@ description: A comprehensive Flask walkthrough that has you building a complete social blogging application from scratch. name: Flask Web Development -payment: paid +position: 6 urls: - icon: regular/link - title: Website url: http://shop.oreilly.com/product/0636920031116.do + color: teal - icon: branding/amazon - title: Amazon url: https://www.amazon.com/Flask-Web-Development-Developing-Applications/dp/1449372627 + color: amazon-orange - icon: branding/github - title: GitHub url: https://github.com/miguelgrinberg/flasky + color: black diff --git a/pydis_site/apps/home/resources/books/fluent_python.yaml b/pydis_site/apps/resources/resources/reading/books/fluent_python.yaml index ebfd5f91..92f4bbab 100644 --- a/pydis_site/apps/home/resources/books/fluent_python.yaml +++ b/pydis_site/apps/resources/resources/reading/books/fluent_python.yaml @@ -1,14 +1,14 @@ description: A veritable tome of intermediate and advanced Python information. A must-read - for any Python professional. + for any Python professional. By far the most recommended book for intermediates. name: Fluent Python -payment: paid +position: 7 urls: - icon: regular/link - title: Website url: https://www.oreilly.com/library/view/fluent-python/9781491946237/ + color: teal - icon: branding/amazon - title: Amazon url: https://www.amazon.com/Fluent-Python-Concise-Effective-Programming/dp/1491946008 + color: amazon-orange - icon: branding/github - title: GitHub url: https://github.com/fluentpython + color: black diff --git a/pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml b/pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml new file mode 100644 index 00000000..906860c7 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml @@ -0,0 +1,11 @@ +description: A best practice handbook for both novice and expert Python developers to the installation, + configuration, and usage of Python on a daily basis. +name: The Hitchhiker's Guide to Python +position: 0 +urls: +- icon: regular/link + url: https://python-guide.org/ + color: teal +- icon: branding/amazon + url: https://www.amazon.com/Hitchhikers-Guide-Python-Practices-Development/dp/1491933178/ + color: amazon-orange diff --git a/pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml b/pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml new file mode 100644 index 00000000..27fad4f7 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml @@ -0,0 +1,9 @@ +description: Inferential Thinking is the textbook for the <a href="http://data8.org/">Foundations of Data Science</a> course at UC Berkley. + It introduces you the fundamentals of both Data Science and Python at a level accessible to all. + It is available both through your browser and in PDF form. +name: Inferential Thinking +position: 13 +urls: + - icon: regular/link + url: https://www.inferentialthinking.com/chapters/intro + color: teal diff --git a/pydis_site/apps/home/resources/books/mission_python.yaml b/pydis_site/apps/resources/resources/reading/books/mission_python.yaml index 8cd91979..c4a48b7e 100644 --- a/pydis_site/apps/home/resources/books/mission_python.yaml +++ b/pydis_site/apps/resources/resources/reading/books/mission_python.yaml @@ -3,11 +3,11 @@ description: Learn programming and Python while building a complete and awesome images, and walk-throughs make this a pleasure to both read and follow along. Excellent book for beginners. name: Mission Python -payment: paid +position: 5 urls: - icon: regular/link - title: Website url: https://www.sean.co.uk/books/mission-python/index.shtm + color: teal - icon: branding/amazon - title: Amazon url: https://www.amazon.com/Mission-Python-Code-Space-Adventure/dp/1593278578 + color: amazon-orange diff --git a/pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml b/pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml new file mode 100644 index 00000000..974b0e50 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml @@ -0,0 +1,10 @@ +description: '"Neural Networks From Scratch" is a book intended to teach you how to build neural networks on your own, + without any libraries, so you can better understand deep learning and how all of the elements work. + This is so you can go out and do new/novel things with deep learning as well as to become more successful with even more basic models. + This book is to accompany the usual free tutorial videos and sample code from youtube.com/sentdex.' +name: Neural Networks from Scratch in Python +position: 11 +urls: + - icon: regular/link + url: https://nnfs.io/ + color: teal diff --git a/pydis_site/apps/home/resources/books/python_cookbook.yaml b/pydis_site/apps/resources/resources/reading/books/python_cookbook.yaml index 9fab8e48..032f8c64 100644 --- a/pydis_site/apps/home/resources/books/python_cookbook.yaml +++ b/pydis_site/apps/resources/resources/reading/books/python_cookbook.yaml @@ -1,14 +1,14 @@ -description: Complete with 'recipes' for various Python topics, including moving from - Python 2 to Python 3.3 +description: A book full of very smart problem-solving recipes for various Python topics, + including moving from Python 2 to Python 3. name: Python Cookbook -payment: paid +position: 8 urls: - icon: regular/link - title: Website url: http://shop.oreilly.com/product/0636920027072.do + color: teal - icon: branding/amazon - title: Amazon url: https://www.amazon.com/Python-Cookbook-Third-David-Beazley/dp/1449340377 + color: amazon-orange - icon: branding/github - title: GitHub url: https://github.com/dabeaz/python-cookbook + color: black diff --git a/pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml b/pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml new file mode 100644 index 00000000..3cbf19c8 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml @@ -0,0 +1,20 @@ +description: "This fast-paced, thorough introduction to programming with Python will have you writing programs, + solving problems, and making things that work in no time. + In the first half of the book, you’ll learn basic programming concepts, such as variables, lists, classes, and loops, + and practice writing clean code with exercises for each topic. + You’ll also learn how to make your programs interactive and test your code safely before adding it to a project. + In the second half, you’ll put your new knowledge into practice with three substantial projects: + a Space Invaders–inspired arcade game, a set of data visualizations with Python’s handy libraries, + and a simple web app you can deploy online." +name: Python Crash Course +position: 12 +urls: + - icon: regular/link + url: https://nostarch.com/pythoncrashcourse2e + color: teal + - icon: branding/amazon + url: https://www.amazon.com/Python-Crash-Course-Project-Based-Introduction/dp/1593276036 + color: amazon-orange + - icon: branding/github + url: https://ehmatthes.github.io/pcc/ + color: black diff --git a/pydis_site/apps/home/resources/books/python_tricks.yaml b/pydis_site/apps/resources/resources/reading/books/python_tricks.yaml index 0638058c..c0941809 100644 --- a/pydis_site/apps/home/resources/books/python_tricks.yaml +++ b/pydis_site/apps/resources/resources/reading/books/python_tricks.yaml @@ -2,11 +2,11 @@ description: Full of useful Python tips, tricks and features. Get this if you ha a good grasp of the basics and want to take your Python skills to the next level, or are a experienced programmer looking to add to your toolbelt. name: Python Tricks -payment: paid +position: 4 urls: - icon: regular/link - title: Website url: https://realpython.com/products/python-tricks-book/ + color: teal - icon: branding/amazon - title: Amazon url: https://www.amazon.com/Python-Tricks-Buffet-Awesome-Features/dp/1775093301 + color: amazon-orange diff --git a/pydis_site/apps/resources/resources/reading/books/think_python.yaml b/pydis_site/apps/resources/resources/reading/books/think_python.yaml new file mode 100644 index 00000000..6de87043 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/books/think_python.yaml @@ -0,0 +1,17 @@ +description: Think Python is an introduction to Python programming for beginners. + It starts with basic concepts of programming, + and is carefully designed to define all terms when they are first used and to develop each new concept in a logical progression. + Larger pieces, like recursion and object-oriented programming are divided into a sequence of smaller steps + and introduced over the course of several chapters. +name: Think Python +position: 10 +urls: + - icon: regular/link + url: https://greenteapress.com/wp/think-python-2e/ + color: teal + - icon: branding/amazon + url: https://www.amazon.com/gp/product/1491939362 + color: amazon-orange + - icon: branding/github + url: https://github.com/AllenDowney/ThinkPython2 + color: black diff --git a/pydis_site/apps/home/resources/books/two_scoops_of_django.yaml b/pydis_site/apps/resources/resources/reading/books/two_scoops_of_django.yaml index 85cfa0fc..7d83e7c4 100644 --- a/pydis_site/apps/home/resources/books/two_scoops_of_django.yaml +++ b/pydis_site/apps/resources/resources/reading/books/two_scoops_of_django.yaml @@ -1,14 +1,14 @@ -description: This book is chock-full of material that will help you with your Django - projects. +description: Tips, tricks, and best practices for your Django project. + A highly recommended resource for Django web developers. name: Two Scoops of Django -payment: paid +position: 9 urls: - icon: regular/link - title: Website url: https://twoscoopspress.com/products/two-scoops-of-django-1-11 + color: teal - icon: branding/amazon - title: Amazon url: https://www.amazon.com/Two-Scoops-Django-Best-Practices/dp/0981467342 + color: amazon-orange - icon: branding/github - title: GitHub url: https://github.com/twoscoops/two-scoops-of-django-2.0-code-examples + color: black diff --git a/pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml b/pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml new file mode 100644 index 00000000..a18b837d --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml @@ -0,0 +1,5 @@ +description: Tutorials and references for those that are just getting started with Python. +name: Tutorials +default_icon: branding/python +default_icon_color: black +position: 1 diff --git a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml new file mode 100644 index 00000000..d1d9a7d2 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml @@ -0,0 +1,5 @@ +description: A big list of excellent resources for getting started making Kivy applications. +name: Getting Started with Kivy +title_url: https://blog.kivy.org/2019/12/getting-started-with-kivy/ +icon_image: https://raw.githubusercontent.com/kivy/kivy-website/master/logos/kivy-logo-black-256.png +position: 3 diff --git a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml new file mode 100644 index 00000000..3250a7c4 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml @@ -0,0 +1,5 @@ +description: A list of beginner resources for programmers with no prior developer experience, + from Python's official guide. +name: Getting Started with Python for Non-Programmers +title_url: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers +position: 1 diff --git a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml new file mode 100644 index 00000000..b65e0e12 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml @@ -0,0 +1,5 @@ +description: A list of beginner resources for programmers coming from other languages, + from Python's official guide. +name: Getting Started with Python for Programmers +title_url: https://wiki.python.org/moin/BeginnersGuide/Programmers +position: 0 diff --git a/pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml b/pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml new file mode 100644 index 00000000..70ac49ef --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml @@ -0,0 +1,5 @@ +description: A Python 3 cheat sheet with useful information and tips, as well as common + pitfalls for beginners. This is a PDF. +name: Python Cheat Sheet +title_url: https://perso.limsi.fr/pointal/_media/python:cours:mementopython3-english.pdf +position: 6 diff --git a/pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml b/pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml new file mode 100644 index 00000000..625d57c8 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml @@ -0,0 +1,5 @@ +description: This guide is a comprehensive resource for contributing to Python – for both new and experienced contributors. + It is maintained by the same community that maintains Python. +name: Python Developer's Guide +title_url: https://devguide.python.org/ +position: 2 diff --git a/pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml b/pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml new file mode 100644 index 00000000..a505715d --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml @@ -0,0 +1,6 @@ +description: A simple, no-nonsense guide to the basics of using Git. +name: A Simple Guide to Git +title_url: http://rogerdudler.github.io/git-guide/ +title_icon: branding/github +title_icon_color: black +position: 4 diff --git a/pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml b/pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml new file mode 100644 index 00000000..8d61ea73 --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml @@ -0,0 +1,4 @@ +description: Miguel Grinberg's fully featured mega-tutorial for learning how to create web applications with the Flask framework. +name: The Flask Mega-Tutorial +title_url: https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world +position: 5 diff --git a/pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml b/pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml new file mode 100644 index 00000000..a25a84fd --- /dev/null +++ b/pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml @@ -0,0 +1,8 @@ +description: Python, being a beautifully designed high-level and interpreter-based programming language, + provides us with many features for the programmer's comfort. + But sometimes, the outcomes of a Python snippet may not seem obvious at first sight. + Here's a fun project attempting to explain what exactly is happening under the hood for some counter-intuitive snippets + and lesser-known features in Python. +name: WTF Python +title_url: https://github.com/satwikkansal/wtfpython +position: 7 diff --git a/pydis_site/apps/resources/resources/tools/_category_info.yaml b/pydis_site/apps/resources/resources/tools/_category_info.yaml new file mode 100644 index 00000000..6b16baa6 --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/_category_info.yaml @@ -0,0 +1,4 @@ +description: This page is a curated list of tools that we regularly recommend in the community. + If you have a suggestion for something to add to this page, please create an issue in + <a href="https://github.com/python-discord/meta/issues">our meta repo</a>, and we'll consider adding it. +name: Tools diff --git a/pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml b/pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml new file mode 100644 index 00000000..e770db07 --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml @@ -0,0 +1,5 @@ +description: Accessibility tools that help people write Python code. +name: Accessibility Tools +default_icon: branding/python +default_icon_color: black +position: 2 diff --git a/pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml b/pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml new file mode 100644 index 00000000..39372956 --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml @@ -0,0 +1,7 @@ +description: Screen readers are software programs that allow blind + or visually impaired users to read the text displayed on a computer screen with a speech synthesizer or braille display. + There are many different screen reader program options, + with this link describing many of them and their capabilities. +name: Screen Readers - American Foundation for the Blind +title_url: https://www.afb.org/blindness-and-low-vision/using-technology/assistive-technology-products/screen-readers +position: 1 diff --git a/pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml b/pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml new file mode 100644 index 00000000..9df5f66f --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml @@ -0,0 +1,6 @@ +description: Talon is a tool being built that aims to bring programming, + realtime video gaming, command line, and full desktop computer proficiency to people + who have limited or no use of their hands. +name: Talon Voice +title_url: https://talonvoice.com/ +position: 0 diff --git a/pydis_site/apps/home/resources/editors/_category_info.yaml b/pydis_site/apps/resources/resources/tools/editors/_category_info.yaml index f8dc1413..3cdfff3a 100644 --- a/pydis_site/apps/home/resources/editors/_category_info.yaml +++ b/pydis_site/apps/resources/resources/tools/editors/_category_info.yaml @@ -1,2 +1,5 @@ description: Lightweight code editors supporting Python name: Editors +default_icon: branding/python +default_icon_color: black +position: 1 diff --git a/pydis_site/apps/resources/resources/tools/editors/atom.yaml b/pydis_site/apps/resources/resources/tools/editors/atom.yaml new file mode 100644 index 00000000..c44f9b5b --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/editors/atom.yaml @@ -0,0 +1,5 @@ +description: A free Electron-based editor, a "hackable text editor for the 21st century", maintained + by the GitHub team. +name: Atom +title_url: https://atom.io/ +position: 0 diff --git a/pydis_site/apps/resources/resources/tools/editors/google_collab.yaml b/pydis_site/apps/resources/resources/tools/editors/google_collab.yaml new file mode 100644 index 00000000..302c3e2e --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/editors/google_collab.yaml @@ -0,0 +1,7 @@ +description: Google Collab is a high-powered custom version of Jupyter Notebook which supports e.g. + !apt-get to install arbitrary Debian packages to the runtime, which is very generous with CPU and memory, + and well-integrated with Google Drive. + You can share your Collab Notebooks with other people and work collaboratively. +name: Google Collab +title_url: https://colab.research.google.com/notebooks/intro.ipynb +position: 4 diff --git a/pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml b/pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml new file mode 100644 index 00000000..b92bac9d --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml @@ -0,0 +1,7 @@ +description: An editor aimed at beginners for the purpose of learning how to code + without the distractions more advanced editors sometimes cause. + Particularly useful for use with microcontrollers, + with built-in tools to interact with Adafruit and Arduino boards. +name: Mu-Editor +title_url: https://codewith.mu/ +position: 3 diff --git a/pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml b/pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml new file mode 100644 index 00000000..3c6e7e84 --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml @@ -0,0 +1,5 @@ +description: A powerful Python-backed editor with great community support and a wealth + of extensions. +name: Sublime Text +title_url: https://www.sublimetext.com/ +position: 2 diff --git a/pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml b/pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml new file mode 100644 index 00000000..e3737ca7 --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml @@ -0,0 +1,4 @@ +description: A fully-featured editor based on Electron, extendable with plugins. +name: Visual Studio Code +title_url: https://code.visualstudio.com/ +position: 1 diff --git a/pydis_site/apps/resources/resources/tools/ides/_category_info.yaml b/pydis_site/apps/resources/resources/tools/ides/_category_info.yaml new file mode 100644 index 00000000..614625a6 --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/ides/_category_info.yaml @@ -0,0 +1,5 @@ +description: Fully-integrated development environments for serious Python work. +name: IDEs +default_icon: branding/python +default_icon_color: black +position: 0 diff --git a/pydis_site/apps/resources/resources/tools/ides/pycharm.yaml b/pydis_site/apps/resources/resources/tools/ides/pycharm.yaml new file mode 100644 index 00000000..b959b0f8 --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/ides/pycharm.yaml @@ -0,0 +1,5 @@ +description: The very best Python IDE, with a wealth of advanced features and convenience + functions. +name: PyCharm +title_url: https://www.jetbrains.com/pycharm/ +position: 0 diff --git a/pydis_site/apps/resources/resources/tools/ides/repl_it.yaml b/pydis_site/apps/resources/resources/tools/ides/repl_it.yaml new file mode 100644 index 00000000..8cd14e14 --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/ides/repl_it.yaml @@ -0,0 +1,5 @@ +description: A free, collaborative, in-browser IDE to code in 50+ languages — + without spending a second on setup. +name: repl.it +title_url: https://repl.it/ +position: 3 diff --git a/pydis_site/apps/resources/resources/tools/ides/spyder.yaml b/pydis_site/apps/resources/resources/tools/ides/spyder.yaml new file mode 100644 index 00000000..c2f9c2dc --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/ides/spyder.yaml @@ -0,0 +1,5 @@ +description: The Scientific Python Development Environment. + Simpler and lighter than PyCharm, but still packs a punch. +name: Spyder +title_url: https://www.spyder-ide.org/ +position: 1 diff --git a/pydis_site/apps/resources/resources/tools/ides/thonny.yaml b/pydis_site/apps/resources/resources/tools/ides/thonny.yaml new file mode 100644 index 00000000..3581e1cd --- /dev/null +++ b/pydis_site/apps/resources/resources/tools/ides/thonny.yaml @@ -0,0 +1,5 @@ +description: A Python IDE specifially aimed at learning programming. Has a lot of + helpful features to help you understand your code. +name: Thonny +title_url: https://thonny.org/ +position: 2 diff --git a/pydis_site/apps/resources/resources/videos/_category_info.yaml b/pydis_site/apps/resources/resources/videos/_category_info.yaml new file mode 100644 index 00000000..8192e021 --- /dev/null +++ b/pydis_site/apps/resources/resources/videos/_category_info.yaml @@ -0,0 +1,2 @@ +description: Excellent Youtube channels with content related to Python. +name: Videos diff --git a/pydis_site/apps/resources/resources/videos/corey_schafer.yaml b/pydis_site/apps/resources/resources/videos/corey_schafer.yaml new file mode 100644 index 00000000..a7cca18a --- /dev/null +++ b/pydis_site/apps/resources/resources/videos/corey_schafer.yaml @@ -0,0 +1,19 @@ +description: 'Corey has a number of exceptionally high quality tutorial series + on everything from Python basics to Django and Flask: + <ul> + <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTskrapNbzXhwoFUiLCjGgY7">Python Programming Beginner Tutorials</a></li> + <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTsqhIuOqKhwlXsIBIdSeYtc">Python OOP Tutorials - Working With Classes</a></li> + <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTs4UjLw5MM6OjgkjFeUxCYH">Flask Tutorials</a></li> + <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTtoQCKZ03TU5fNfx2UY6U4p">Django Tutorials</a></li> + </ul> + Check out his channel for more video series! + ' +title_image: https://i.imgur.com/KIfWw3b.png +position: 0 +urls: + - icon: branding/youtube + url: https://www.youtube.com/channel/UCCezIgC97PvUuR4_gbFUs5g + color: youtube-red + - icon: regular/link + url: https://coreyms.com/ + color: teal diff --git a/pydis_site/apps/resources/resources/videos/jetbrains.yaml b/pydis_site/apps/resources/resources/videos/jetbrains.yaml new file mode 100644 index 00000000..5d130db6 --- /dev/null +++ b/pydis_site/apps/resources/resources/videos/jetbrains.yaml @@ -0,0 +1,12 @@ +description: A collection of videos made by the PyCharm team at JetBrains on subjects such as TDD, + Django, pytest and much more!<br><br> + Episodes of their "What does this package do?" series go over all sorts of libraries in Python + both in the standard library and from the community and give a video explanation of the key concepts. +icon_image: https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/JetBrains_Logo_2016.svg/1200px-JetBrains_Logo_2016.svg.png +icon_size: 50 +title_image: https://resources.jetbrains.com/storage/products/pycharm/img/meta/pycharm_logo_300x300.png +position: 3 +urls: + - icon: branding/youtube + url: https://www.youtube.com/channel/UCak6beUTLlVmf0E4AmnQkmw + color: youtube-red diff --git a/pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml b/pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml new file mode 100644 index 00000000..488cfa83 --- /dev/null +++ b/pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml @@ -0,0 +1,13 @@ +description: 'JimShapedCoding contains a set of YouTube tutorials covering things from Flask to GUI development in Python: + <ul> + <li><a href="https://www.youtube.com/playlist?list=PLOkVupluCIjuPtTkhO6jmA76uQR994Wvi">Flask tutorials</a></li> + <li><a href="https://www.youtube.com/watch?v=0tqZ6rMcqGE&list=PLOkVupluCIjuAzAmDNUXcD-0Fsb8sbv9F">GUI tutorials</a></li> + <li><a href="https://www.youtube.com/watch?v=qMrAFscMBBc&list=PLOkVupluCIjvORWaF4kG-sXLgbVemYpEi">Django tutorials</a></li> + </ul> + Check out his channel for more videos!' +title_image: https://i.imgur.com/DlovZPf.png +position: 5 +urls: + - icon: branding/youtube + url: https://www.youtube.com/channel/UCU8d7rcShA7MGuDyYH1aWGg + color: youtube-red diff --git a/pydis_site/apps/resources/resources/videos/microsoft.yaml b/pydis_site/apps/resources/resources/videos/microsoft.yaml new file mode 100644 index 00000000..720ee202 --- /dev/null +++ b/pydis_site/apps/resources/resources/videos/microsoft.yaml @@ -0,0 +1,18 @@ +description: A trove of tutorials & guides for developers from Microsoft's Developer hub. + Follow the links below for a series of high-quality Python tutorials for beginners. + <ul> + <li><a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHhS8VzuMCfQD4uJ9yne1mE6">Python for Beginners</a></li> + <li><a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHiXd-nEby-TMCoUNwhbLUnj">More Python for Beginners</a></li> + <li><a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHhHnCUVtR8UHS7eLl33zfJ-">Even More Python for Beginners - Data Tools</a></li> + </ul> + Microsoft's Python Development Team also runs a Discord Server for discussions of Python in the Microsoft ecosystem, + including Visual Studio Code and Azure. +title_image: http://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE2qVsJ?ver=3f74 +position: 4 +urls: + - icon: branding/youtube + url: https://www.youtube.com/channel/UCsMica-v34Irf9KVTh6xx-g + color: youtube-red + - icon: branding/discord + url: https://aka.ms/python-discord + color: blurple diff --git a/pydis_site/apps/resources/resources/videos/python_discord.yaml b/pydis_site/apps/resources/resources/videos/python_discord.yaml new file mode 100644 index 00000000..04235b08 --- /dev/null +++ b/pydis_site/apps/resources/resources/videos/python_discord.yaml @@ -0,0 +1,8 @@ +description: It's our channel! We are slowly gathering content here directly related to Python, + our community and the events we host. Come check us out! +title_image: https://raw.githubusercontent.com/python-discord/branding/master/logos/logo_banner/logo_site_banner_dark_512.png +position: 2 +urls: + - icon: branding/youtube + url: https://www.youtube.com/pythondiscord + color: youtube-red diff --git a/pydis_site/apps/resources/resources/videos/sentdex.yaml b/pydis_site/apps/resources/resources/videos/sentdex.yaml new file mode 100644 index 00000000..4e5f54c6 --- /dev/null +++ b/pydis_site/apps/resources/resources/videos/sentdex.yaml @@ -0,0 +1,22 @@ +description: 'An enormous amount of Python content for all skill levels + from the most popular Python YouTuber on the web. + <ul> + <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDeAams7fkdcwOGBpGdHpXln">Learning to program with Python 3 (py 3.7)</a></li> + <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDfwnDTZWw8H3hN_VRQfq8rF">Kivy - Mobile and Desktop App Dev w/ Python</a></li> + <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDfSfqQuee6K8opKtZsh7sA9">Data Analysis w/ Python 3 and Pandas</a></li> + <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDfKTOs3Keq_kaG2P55YRn5v">Machine Learning with Python</a></li> + </ul> + Check out his channel for more video series! + ' +title_image: https://i.imgur.com/kJgWZIu.png +position: 1 +urls: + - icon: branding/youtube + url: https://www.youtube.com/user/sentdex + color: youtube-red + - icon: branding/discord + url: https://discord.gg/sentdex + color: blurple + - icon: regular/link + url: https://pythonprogramming.net/ + color: teal diff --git a/pydis_site/apps/resources/templatetags/__init__.py b/pydis_site/apps/resources/templatetags/__init__.py new file mode 100644 index 00000000..2b266b94 --- /dev/null +++ b/pydis_site/apps/resources/templatetags/__init__.py @@ -0,0 +1,3 @@ +from .as_icon import as_icon + +__all__ = ["as_icon"] diff --git a/pydis_site/apps/resources/templatetags/as_icon.py b/pydis_site/apps/resources/templatetags/as_icon.py new file mode 100644 index 00000000..b211407c --- /dev/null +++ b/pydis_site/apps/resources/templatetags/as_icon.py @@ -0,0 +1,14 @@ +from django import template + +register = template.Library() + + +def as_icon(icon: str) -> str: + """Convert icon string in format 'type/icon' to fa-icon HTML classes.""" + icon_type, icon_name = icon.split("/") + if icon_type.lower() == "branding": + icon_type = "fab" + else: + icon_type = "fas" + return f'{icon_type} fa-{icon_name}' diff --git a/pydis_site/apps/resources/tests/__init__.py b/pydis_site/apps/resources/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/resources/tests/__init__.py diff --git a/pydis_site/apps/resources/tests/test_as_icon.py b/pydis_site/apps/resources/tests/test_as_icon.py new file mode 100644 index 00000000..5b33910d --- /dev/null +++ b/pydis_site/apps/resources/tests/test_as_icon.py @@ -0,0 +1,28 @@ +from django.test import TestCase + +from pydis_site.apps.resources.templatetags import as_icon + + +class TestAsIcon(TestCase): + """Tests for `as_icon` templatetag.""" + + def test_as_icon(self): + """Should return proper icon type class and icon class based on input.""" + test_cases = [ + { + "input": "regular/icon", + "output": "fas fa-icon", + }, + { + "input": "branding/brand", + "output": "fab fa-brand", + }, + { + "input": "fake/my-icon", + "output": "fas fa-my-icon", + } + ] + + for case in test_cases: + with self.subTest(input=case["input"], output=case["output"]): + self.assertEqual(case["output"], as_icon(case["input"])) diff --git a/pydis_site/apps/resources/tests/test_views.py b/pydis_site/apps/resources/tests/test_views.py new file mode 100644 index 00000000..53685eef --- /dev/null +++ b/pydis_site/apps/resources/tests/test_views.py @@ -0,0 +1,34 @@ +from pathlib import Path +from unittest.mock import patch + +from django.conf import settings +from django.test import TestCase +from django_hosts import reverse + +TESTING_RESOURCES_PATH = Path( + settings.BASE_DIR, "pydis_site", "apps", "resources", "tests", "testing_resources" +) + + +class TestResourcesView(TestCase): + def test_resources_index_200(self): + """Check does index of resources app return 200 HTTP response.""" + url = reverse("resources:index") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + +class TestResourcesListView(TestCase): + @patch("pydis_site.apps.resources.views.resources_list.RESOURCES_PATH", TESTING_RESOURCES_PATH) + def test_valid_resource_list_200(self): + """Check does site return code 200 when visiting valid resource list.""" + url = reverse("resources:resources", ("testing",)) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + @patch("pydis_site.apps.resources.views.resources_list.RESOURCES_PATH", TESTING_RESOURCES_PATH) + def test_invalid_resource_list_404(self): + """Check does site return code 404 when trying to visit invalid resource list.""" + url = reverse("resources:resources", ("invalid",)) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml new file mode 100644 index 00000000..bae17ea3 --- /dev/null +++ b/pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml @@ -0,0 +1 @@ +name: Testing diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml new file mode 100644 index 00000000..eaac32d9 --- /dev/null +++ b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml @@ -0,0 +1 @@ +name: Foobar diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml new file mode 100644 index 00000000..22835090 --- /dev/null +++ b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml @@ -0,0 +1 @@ +name: Resource Test diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml new file mode 100644 index 00000000..61df6173 --- /dev/null +++ b/pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml @@ -0,0 +1 @@ +name: My Resource diff --git a/pydis_site/apps/resources/urls.py b/pydis_site/apps/resources/urls.py new file mode 100644 index 00000000..19142081 --- /dev/null +++ b/pydis_site/apps/resources/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from pydis_site.apps.resources import views + +app_name = "resources" +urlpatterns = [ + path("", views.ResourcesView.as_view(), name="index"), + path("<str:category>/", views.ResourcesListView.as_view(), name="resources") +] diff --git a/pydis_site/apps/resources/utils.py b/pydis_site/apps/resources/utils.py new file mode 100644 index 00000000..1855fc80 --- /dev/null +++ b/pydis_site/apps/resources/utils.py @@ -0,0 +1,42 @@ +import typing as t +from pathlib import Path + +import yaml + + +def get_resources(path: Path) -> t.List[t.Dict]: + """Loads resource YAMLs from provided path.""" + resources = [] + + for item in path.iterdir(): + if item.is_file() and item.suffix == ".yaml" and item.name != "_category_info.yaml": + resources.append(yaml.safe_load(item.read_text())) + + return resources + + +def get_subcategories(path: Path) -> t.List[t.Dict]: + """Loads resources subcategories with their resources by provided path.""" + subcategories = [] + + for item in path.iterdir(): + if item.is_dir() and item.joinpath("_category_info.yaml").exists(): + subcategories.append({ + "category_info": { + **yaml.safe_load( + item.joinpath("_category_info.yaml").read_text() + ), + "raw_name": item.name + }, + "resources": [ + yaml.safe_load(subitem.read_text()) + for subitem in item.iterdir() + if ( + subitem.is_file() + and subitem.suffix == ".yaml" + and subitem.name != "_category_info.yaml" + ) + ] + }) + + return subcategories diff --git a/pydis_site/apps/resources/views/__init__.py b/pydis_site/apps/resources/views/__init__.py new file mode 100644 index 00000000..8eb383b5 --- /dev/null +++ b/pydis_site/apps/resources/views/__init__.py @@ -0,0 +1,4 @@ +from .resources import ResourcesView +from .resources_list import ResourcesListView + +__all__ = ["ResourcesView", "ResourcesListView"] diff --git a/pydis_site/apps/resources/views/resources.py b/pydis_site/apps/resources/views/resources.py new file mode 100644 index 00000000..25ce3e50 --- /dev/null +++ b/pydis_site/apps/resources/views/resources.py @@ -0,0 +1,7 @@ +from django.views.generic import TemplateView + + +class ResourcesView(TemplateView): + """View for resources index page.""" + + template_name = "resources/resources.html" diff --git a/pydis_site/apps/resources/views/resources_list.py b/pydis_site/apps/resources/views/resources_list.py new file mode 100644 index 00000000..55f22993 --- /dev/null +++ b/pydis_site/apps/resources/views/resources_list.py @@ -0,0 +1,39 @@ +from pathlib import Path +from typing import Any, Dict + +import yaml +from django.conf import settings +from django.http import Http404 +from django.views.generic import TemplateView + +from pydis_site.apps.resources.utils import get_resources, get_subcategories + +RESOURCES_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "resources", "resources") + + +class ResourcesListView(TemplateView): + """Shows specific resources list.""" + + template_name = "resources/resources_list.html" + + def get_context_data(self, **kwargs) -> Dict[str, Any]: + """Add resources and subcategories data into context.""" + context = super().get_context_data(**kwargs) + + resource_path = RESOURCES_PATH / self.kwargs["category"] + if ( + not resource_path.is_dir() + or not resource_path.joinpath("_category_info.yaml").exists() + ): + raise Http404 + + context["resources"] = get_resources(resource_path) + context["subcategories"] = get_subcategories(resource_path) + context["category_info"] = { + **yaml.safe_load( + resource_path.joinpath("_category_info.yaml").read_text() + ), + "raw_name": resource_path.name + } + + return context diff --git a/pydis_site/apps/staff/admin.py b/pydis_site/apps/staff/admin.py deleted file mode 100644 index 94cd83c5..00000000 --- a/pydis_site/apps/staff/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.contrib import admin - -from .models import RoleMapping - - -admin.site.register(RoleMapping) diff --git a/pydis_site/apps/staff/migrations/0003_delete_rolemapping.py b/pydis_site/apps/staff/migrations/0003_delete_rolemapping.py new file mode 100644 index 00000000..e9b6114e --- /dev/null +++ b/pydis_site/apps/staff/migrations/0003_delete_rolemapping.py @@ -0,0 +1,16 @@ +# Generated by Django 3.0.9 on 2020-10-04 17:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('staff', '0002_add_is_staff_to_role_mappings'), + ] + + operations = [ + migrations.DeleteModel( + name='RoleMapping', + ), + ] diff --git a/pydis_site/apps/staff/models/__init__.py b/pydis_site/apps/staff/models/__init__.py deleted file mode 100644 index b49b6fd0..00000000 --- a/pydis_site/apps/staff/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .role_mapping import RoleMapping - -__all__ = ["RoleMapping"] diff --git a/pydis_site/apps/staff/models/role_mapping.py b/pydis_site/apps/staff/models/role_mapping.py deleted file mode 100644 index 8a1fac2e..00000000 --- a/pydis_site/apps/staff/models/role_mapping.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.contrib.auth.models import Group -from django.db import models - -from pydis_site.apps.api.models import Role - - -class RoleMapping(models.Model): - """A mapping between a Discord role and Django permissions group.""" - - role = models.OneToOneField( - Role, - on_delete=models.CASCADE, - help_text="The Discord role to use for this mapping.", - unique=True, # Unique in order to simplify group assignment logic - ) - - group = models.OneToOneField( - Group, - on_delete=models.CASCADE, - help_text="The Django permissions group to use for this mapping.", - unique=True, # Unique in order to simplify group assignment logic - ) - - is_staff = models.BooleanField( - help_text="Whether this role mapping relates to a Django staff group", - default=False - ) - - def __str__(self): - """Returns the mapping, for display purposes.""" - return f"@{self.role.name} -> {self.group.name}" diff --git a/pydis_site/apps/staff/urls.py b/pydis_site/apps/staff/urls.py index a564d516..ca8d1a0f 100644 --- a/pydis_site/apps/staff/urls.py +++ b/pydis_site/apps/staff/urls.py @@ -1,5 +1,3 @@ -from django.conf import settings -from django.conf.urls.static import static from django.urls import path from .viewsets import LogView @@ -7,4 +5,4 @@ from .viewsets import LogView app_name = 'staff' urlpatterns = [ path('bot/logs/<int:pk>/', LogView.as_view(), name="logs"), -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +] diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 300452fa..7df7ad85 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -13,19 +13,14 @@ https://docs.djangoproject.com/en/2.1/ref/settings/ import os import secrets import sys -import typing +from pathlib import Path import environ import sentry_sdk -from django.contrib.messages import constants as messages from sentry_sdk.integrations.django import DjangoIntegration from pydis_site.constants import GIT_SHA -if typing.TYPE_CHECKING: - from django.contrib.auth.models import User - from wiki.models import Article - env = environ.Env( DEBUG=(bool, False), SITE_DSN=(str, "") @@ -67,46 +62,29 @@ else: ) SECRET_KEY = env('SECRET_KEY') - # Application definition - INSTALLED_APPS = [ 'pydis_site.apps.api', 'pydis_site.apps.home', 'pydis_site.apps.staff', + 'pydis_site.apps.resources', + 'pydis_site.apps.content', + 'pydis_site.apps.events', + 'pydis_site.apps.redirect', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', - 'django.contrib.humanize.apps.HumanizeConfig', 'django.contrib.sessions', 'django.contrib.messages', - 'django.contrib.sites.apps.SitesConfig', + 'django.contrib.sites', 'django.contrib.staticfiles', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - - 'allauth.socialaccount.providers.discord', - 'allauth.socialaccount.providers.github', - 'django_hosts', 'django_filters', - 'django_nyt.apps.DjangoNytConfig', 'django_simple_bulma', - 'mptt', 'rest_framework', - 'rest_framework.authtoken', - 'sekizai', - 'sorl.thumbnail', - - 'wiki.apps.WikiConfig', - - 'wiki.plugins.images.apps.ImagesConfig', - 'wiki.plugins.links.apps.LinksConfig', - 'wiki.plugins.redlinks.apps.RedlinksConfig', - 'wiki.plugins.notifications.apps.NotificationsConfig', # Required for migrations + 'rest_framework.authtoken' ] MIDDLEWARE = [ @@ -137,12 +115,9 @@ TEMPLATES = [ 'context_processors': [ 'django.template.context_processors.debug', - 'django.template.context_processors.media', 'django.template.context_processors.request', - 'django.template.context_processors.static', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - "sekizai.context_processors.sekizai", "pydis_site.context_processors.git_sha_processor" ], }, @@ -192,9 +167,6 @@ STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'pydis_site', 'static')] STATIC_ROOT = env('STATIC_ROOT', default='/app/staticfiles') -MEDIA_URL = '/media/' -MEDIA_ROOT = env('MEDIA_ROOT', default='/site/media') - STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', @@ -270,22 +242,10 @@ LOGGING = { } } -# Django Messages framework config -MESSAGE_TAGS = { - messages.DEBUG: 'primary', - messages.INFO: 'info', - messages.SUCCESS: 'success', - messages.WARNING: 'warning', - messages.ERROR: 'danger', -} - # Custom settings for django-simple-bulma BULMA_SETTINGS = { "variables": { # If you update these colours, please update the notification.css file "primary": "#7289DA", # Discord blurple - - # "orange": "", # Apparently unused, but the default is fine - # "yellow": "", # The default yellow looks pretty good "green": "#32ac66", # Colour picked after Discord discussion "turquoise": "#7289DA", # Blurple, because Bulma uses this regardless of `primary` above "blue": "#2482c1", # Colour picked after Discord discussion @@ -298,96 +258,24 @@ BULMA_SETTINGS = { "dimensions": "16 24 32 48 64 96 128 256 512", # Possible image dimensions "navbar-height": "4.75rem", "footer-padding": "1rem 1.5rem 1rem", - } -} - -# Required for the wiki -LOGIN_URL = "/admin/login" # Update this when the real login system is in place -SITE_ID = 1 - -WIKI_ACCOUNT_HANDLING = False -WIKI_ACCOUNT_SIGNUP_ALLOWED = False - -WIKI_ANONYMOUS = True -WIKI_ANONYMOUS_WRITE = False - -WIKI_MARKDOWN_KWARGS = { - "extension_configs": { - "wiki.plugins.macros.mdx.toc": { - "anchorlink": True, - "baselevel": 2 - } - }, "extensions": [ - "markdown.extensions.abbr", - "markdown.extensions.attr_list", - "markdown.extensions.extra", - "markdown.extensions.footnotes", - "markdown.extensions.nl2br", - "markdown.extensions.sane_lists", - - "wiki.core.markdown.mdx.codehilite", - "wiki.core.markdown.mdx.previewlinks", - "wiki.core.markdown.mdx.responsivetable", - "wiki.plugins.macros.mdx.toc", - "wiki.plugins.macros.mdx.wikilinks", - ] -} - -WIKI_MESSAGE_TAG_CSS_CLASS = { - messages.DEBUG: "", # is-info isn't distinctive enough from blurple - messages.ERROR: "is-danger", - messages.INFO: "is-primary", - messages.SUCCESS: "is-success", - messages.WARNING: "is-warning", + "tooltip-max-width": "30rem", + }, + "extensions": [ + "bulma-dropdown", + "bulma-navbar-burger", + ], } -WIKI_MARKDOWN_SANITIZE_HTML = False - - -# Wiki permissions - - -def WIKI_CAN_DELETE(article: "Article", user: "User") -> bool: # noqa: N802 - """Check whether a user may delete an article.""" - return user.has_perm('wiki.delete_article') - +# Information about site repository +SITE_REPOSITORY_OWNER = "python-discord" +SITE_REPOSITORY_NAME = "site" +SITE_REPOSITORY_BRANCH = "master" -def WIKI_CAN_MODERATE(article: "Article", user: "User") -> bool: # noqa: N802 - """Check whether a user may moderate an article.""" - return user.has_perm('wiki.moderate') +# Path for events pages +EVENTS_PAGES_PATH = Path(BASE_DIR, "pydis_site", "templates", "events", "pages") +# Path for content pages +CONTENT_PAGES_PATH = Path(BASE_DIR, "pydis_site", "apps", "content", "resources") -def WIKI_CAN_WRITE(article: "Article", user: "User") -> bool: # noqa: N802 - """Check whether a user may create or edit an article.""" - return user.has_perm('wiki.change_article') - - -# Django Allauth stuff - -AUTHENTICATION_BACKENDS = ( - # Needed to login by username in Django admin, regardless of `allauth` - 'django.contrib.auth.backends.ModelBackend', - - # `allauth` specific authentication methods, such as login by e-mail - 'allauth.account.auth_backends.AuthenticationBackend', -) - -ACCOUNT_ADAPTER = "pydis_site.utils.account.AccountAdapter" -ACCOUNT_EMAIL_REQUIRED = False # Undocumented allauth setting; don't require emails -ACCOUNT_EMAIL_VERIFICATION = "none" # No verification required; we don't use emails for anything -ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" - -# We use this validator because Allauth won't let us actually supply a list with no validators -# in it, and we can't just give it a lambda - that'd be too easy, I suppose. -ACCOUNT_USERNAME_VALIDATORS = "pydis_site.VALIDATORS" - -LOGIN_REDIRECT_URL = "home" -SOCIALACCOUNT_ADAPTER = "pydis_site.utils.account.SocialAccountAdapter" -SOCIALACCOUNT_PROVIDERS = { - "discord": { - "SCOPE": [ - "identify", - ], - "AUTH_PARAMS": {"prompt": "none"} - } -} +# Path for redirection links +REDIRECTIONS_PATH = Path(BASE_DIR, "pydis_site", "apps", "redirect", "redirects.yaml") diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index a1d325f9..f3fe1e44 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -1,5 +1,6 @@ html { overflow: auto; + scroll-behavior: smooth; } body.site { @@ -124,19 +125,18 @@ button.is-size-navbar-menu, a.is-size-navbar-menu { } } -/* Fix for modals being behind the navbar */ - -.modal * { - z-index: 1020; -} - -.modal-background { - z-index: 1010; +/* 16:9 aspect ratio fixing */ +.force-aspect-container { + position: relative; + padding-bottom: 56.25%; } -/* Wiki style tweaks */ -.codehilite-wrap { - margin-bottom: 1em; +.force-aspect-content { + top: 0; + left: 0; + width: 100%; + height: 100%; + position: absolute; } /* 16:9 aspect ratio fixing */ @@ -152,3 +152,10 @@ button.is-size-navbar-menu, a.is-size-navbar-menu { height: 100%; position: absolute; } + +/* Tone down animations to avoid motion triggers */ +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} diff --git a/pydis_site/static/css/base/notification.css b/pydis_site/static/css/base/notification.css deleted file mode 100644 index b2824641..00000000 --- a/pydis_site/static/css/base/notification.css +++ /dev/null @@ -1,99 +0,0 @@ -/* On-page message styling */ - -@keyframes message-slide-in { - 0% { - transform: translateX(100%); - } - - 100% { - transform: translateX(0); - } -} - -div.messages { - animation: 0.5s ease-out 0s 1 message-slide-in; - padding: 0.5rem; - position: fixed; - right: 0; - top: 76px; - - z-index: 1000; /* On top of everything else */ -} - -/* Discord light theme inspired notifications */ - -.messages .notification { - background-color: #fdfdfd; /* Discord embed background */ - border: #eeeeee 1px solid; /* Discord embed border */ - border-left: #4f545c 4px solid; /* Discord default embed colour */ - color: #4a4a4a; /* Bulma default colour */ -} - -.messages .notification.is-primary { - background-color: #fdfdfd; - border-left-color: #7289DA; - color: #4a4a4a; /* Bulma default colour */ -} - -.messages .notification.is-info { - background-color: #fdfdfd; - border-left-color: #1c8ad3; /* Bulma default colour */ - color: #4a4a4a; /* Bulma default colour */ -} - -.messages .notification.is-success { - background-color: #fdfdfd; - border-left-color: #21c65c; - color: #4a4a4a; /* Bulma default colour */ -} - -.messages .notification.is-warning { - background-color: #fdfdfd; - border-left-color: #ffdd57; /* Bulma default colour */ - color: #4a4a4a; /* Bulma default colour */ -} - -.messages .notification.is-danger { - background-color: #fdfdfd; - border-left-color: #ff3860; /* Bulma default colour */ - color: #4a4a4a; /* Bulma default colour */ -} - -/* Discord dark theme inspired notifications */ - -.messages .notification.is-dark { - background-color: #33353C; /* Discord embed background */ - border: #36393f 1px solid; /* Discord embed border */ - border-left: #4f545c 4px solid; /* Discord default embed colour */ - color: #fff; /* Bulma default colour */ -} - -.messages .notification.is-dark.is-primary { - background-color: #33353C; - border-left-color: #7289DA; - color: #fff; /* Bulma default colour */ -} - -.messages .notification.is-dark.is-info { - background-color: #33353C; - border-left-color: #1c8ad3; /* Bulma default colour */ - color: #fff; /* Bulma default colour */ -} - -.messages .notification.is-dark.is-success { - background-color: #33353C; - border-left-color: #21c65c; - color: #fff; /* Bulma default colour */ -} - -.messages .notification.is-dark.is-warning { - background-color: #33353C; - border-left-color: #ffdd57; /* Bulma default colour */ - color: #fff; /* Bulma default colour */ -} - -.messages .notification.is-dark.is-danger { - background-color: #33353C; - border-left-color: #ff3860; /* Bulma default colour */ - color: #fff; /* Bulma default colour */ -} diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css new file mode 100644 index 00000000..d831f86d --- /dev/null +++ b/pydis_site/static/css/content/page.css @@ -0,0 +1,79 @@ +.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; +} + +/* + * Display <em> tags immediately following <img> tags like figure subcaptions. + * Note: There must not be a newline between the image and the italicized line + * for this to work. Otherwise, it's regular markdown. + * + * Image caption: + * + *  + * *This is my caption.* + * + */ +img + em { + /* Place the caption on it's own line */ + display: block; + white-space: pre; + + /* Style */ + font-size: .875em; +} + +/* + * Remove extra padding on the left of TOC items + */ +ul.menu-list.toc { + margin-left: 0; +} + +/* + * Remove bullets set by the markdown extension, since bulma adds vertical + * lines to represent nesting + */ +.toc li { + list-style-type: none; +} +/* ..but we still want bullets on the top <ul> items */ +.toc > ul > li { + list-style-type: disc; +} + +/* + * Increase space between images and any text above the image in a list. + * This is used instead of a newline between the image and the text in markdown + * so the image remains left-aligned with the list item. + */ +li img { + margin-top: 0.5em; +} diff --git a/pydis_site/static/css/events/base.css b/pydis_site/static/css/events/base.css new file mode 100644 index 00000000..266bca1d --- /dev/null +++ b/pydis_site/static/css/events/base.css @@ -0,0 +1,12 @@ +.breadcrumb-section { + padding: 1rem; +} + +pre { + /* + * Style it the same as the <code> tag, since highlight.js does not style + * backgrounds of <pre> tags but bulma does, resulting in a weird off-white + * border. + */ + background-color: #282c34; +} diff --git a/pydis_site/static/css/pygments/darcula.css b/pydis_site/static/css/pygments/darcula.css deleted file mode 100644 index d58a5219..00000000 --- a/pydis_site/static/css/pygments/darcula.css +++ /dev/null @@ -1,91 +0,0 @@ -/* Dracula Theme v1.2.5 - * - * https://github.com/zenorocha/dracula-theme - * - * Copyright 2016, All rights reserved - * - * Code licensed under the MIT license - * http://zenorocha.mit-license.org - * - * @author Rob G <[email protected]> - * @author Chris Bracco <[email protected]> - * @author Zeno Rocha <[email protected]> - */ - -.codehilite .hll { background-color: #f1fa8c } -.codehilite pre { background: #282a36; color: #f8f8f2 } -.codehilite .c { color: #6272a4 } /* Comment */ -.codehilite .err { color: #f8f8f2 } /* Error */ -.codehilite .g { color: #f8f8f2 } /* Generic */ -.codehilite .k { color: #ff79c6 } /* Keyword */ -.codehilite .l { color: #f8f8f2 } /* Literal */ -.codehilite .n { color: #f8f8f2 } /* Name */ -.codehilite .o { color: #ff79c6 } /* Operator */ -.codehilite .x { color: #f8f8f2 } /* Other */ -.codehilite .p { color: #f8f8f2 } /* Punctuation */ -.codehilite .ch { color: #6272a4 } /* Comment.Hashbang */ -.codehilite .cm { color: #6272a4 } /* Comment.Multiline */ -.codehilite .cp { color: #ff79c6 } /* Comment.Preproc */ -.codehilite .cpf { color: #6272a4 } /* Comment.PreprocFile */ -.codehilite .c1 { color: #6272a4 } /* Comment.Single */ -.codehilite .cs { color: #6272a4 } /* Comment.Special */ -.codehilite .gd { color: #8b080b } /* Generic.Deleted */ -.codehilite .ge { color: #f8f8f2; text-decoration: underline } /* Generic.Emph */ -.codehilite .gr { color: #f8f8f2 } /* Generic.Error */ -.codehilite .gh { color: #f8f8f2; font-weight: bold } /* Generic.Heading */ -.codehilite .gi { color: #f8f8f2; font-weight: bold } /* Generic.Inserted */ -.codehilite .go { color: #44475a } /* Generic.Output */ -.codehilite .gp { color: #f8f8f2 } /* Generic.Prompt */ -.codehilite .gs { color: #f8f8f2 } /* Generic.Strong */ -.codehilite .gu { color: #f8f8f2; font-weight: bold } /* Generic.Subheading */ -.codehilite .gt { color: #f8f8f2 } /* Generic.Traceback */ -.codehilite .kc { color: #ff79c6 } /* Keyword.Constant */ -.codehilite .kd { color: #8be9fd; font-style: italic } /* Keyword.Declaration */ -.codehilite .kn { color: #ff79c6 } /* Keyword.Namespace */ -.codehilite .kp { color: #ff79c6 } /* Keyword.Pseudo */ -.codehilite .kr { color: #ff79c6 } /* Keyword.Reserved */ -.codehilite .kt { color: #8be9fd } /* Keyword.Type */ -.codehilite .ld { color: #f8f8f2 } /* Literal.Date */ -.codehilite .m { color: #bd93f9 } /* Literal.Number */ -.codehilite .s { color: #f1fa8c } /* Literal.String */ -.codehilite .na { color: #50fa7b } /* Name.Attribute */ -.codehilite .nb { color: #8be9fd; font-style: italic } /* Name.Builtin */ -.codehilite .nc { color: #50fa7b } /* Name.Class */ -.codehilite .no { color: #f8f8f2 } /* Name.Constant */ -.codehilite .nd { color: #f8f8f2 } /* Name.Decorator */ -.codehilite .ni { color: #f8f8f2 } /* Name.Entity */ -.codehilite .ne { color: #f8f8f2 } /* Name.Exception */ -.codehilite .nf { color: #50fa7b } /* Name.Function */ -.codehilite .nl { color: #8be9fd; font-style: italic } /* Name.Label */ -.codehilite .nn { color: #f8f8f2 } /* Name.Namespace */ -.codehilite .nx { color: #f8f8f2 } /* Name.Other */ -.codehilite .py { color: #f8f8f2 } /* Name.Property */ -.codehilite .nt { color: #ff79c6 } /* Name.Tag */ -.codehilite .nv { color: #8be9fd; font-style: italic } /* Name.Variable */ -.codehilite .ow { color: #ff79c6 } /* Operator.Word */ -.codehilite .w { color: #f8f8f2 } /* Text.Whitespace */ -.codehilite .mb { color: #bd93f9 } /* Literal.Number.Bin */ -.codehilite .mf { color: #bd93f9 } /* Literal.Number.Float */ -.codehilite .mh { color: #bd93f9 } /* Literal.Number.Hex */ -.codehilite .mi { color: #bd93f9 } /* Literal.Number.Integer */ -.codehilite .mo { color: #bd93f9 } /* Literal.Number.Oct */ -.codehilite .sa { color: #f1fa8c } /* Literal.String.Affix */ -.codehilite .sb { color: #f1fa8c } /* Literal.String.Backtick */ -.codehilite .sc { color: #f1fa8c } /* Literal.String.Char */ -.codehilite .dl { color: #f1fa8c } /* Literal.String.Delimiter */ -.codehilite .sd { color: #f1fa8c } /* Literal.String.Doc */ -.codehilite .s2 { color: #f1fa8c } /* Literal.String.Double */ -.codehilite .se { color: #f1fa8c } /* Literal.String.Escape */ -.codehilite .sh { color: #f1fa8c } /* Literal.String.Heredoc */ -.codehilite .si { color: #f1fa8c } /* Literal.String.Interpol */ -.codehilite .sx { color: #f1fa8c } /* Literal.String.Other */ -.codehilite .sr { color: #f1fa8c } /* Literal.String.Regex */ -.codehilite .s1 { color: #f1fa8c } /* Literal.String.Single */ -.codehilite .ss { color: #f1fa8c } /* Literal.String.Symbol */ -.codehilite .bp { color: #f8f8f2; font-style: italic } /* Name.Builtin.Pseudo */ -.codehilite .fm { color: #50fa7b } /* Name.Function.Magic */ -.codehilite .vc { color: #8be9fd; font-style: italic } /* Name.Variable.Class */ -.codehilite .vg { color: #8be9fd; font-style: italic } /* Name.Variable.Global */ -.codehilite .vi { color: #8be9fd; font-style: italic } /* Name.Variable.Instance */ -.codehilite .vm { color: #8be9fd; font-style: italic } /* Name.Variable.Magic */ -.codehilite .il { color: #bd93f9 } /* Literal.Number.Integer.Long */ diff --git a/pydis_site/static/css/pygments/friendly.css b/pydis_site/static/css/pygments/friendly.css deleted file mode 100644 index 24831c44..00000000 --- a/pydis_site/static/css/pygments/friendly.css +++ /dev/null @@ -1,71 +0,0 @@ -/* Friendly, a very light theme. */ - -.codehilite .hll { background-color: #ffffcc } -.codehilite pre { background: #f0f0f0; } -.codehilite .c { color: #60a0b0; font-style: italic } /* Comment */ -.codehilite .err { border: 1px solid #FF0000 } /* Error */ -.codehilite .k { color: #007020; font-weight: bold } /* Keyword */ -.codehilite .o { color: #666666 } /* Operator */ -.codehilite .ch { color: #60a0b0; font-style: italic } /* Comment.Hashbang */ -.codehilite .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */ -.codehilite .cp { color: #007020 } /* Comment.Preproc */ -.codehilite .cpf { color: #60a0b0; font-style: italic } /* Comment.PreprocFile */ -.codehilite .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */ -.codehilite .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */ -.codehilite .gd { color: #A00000 } /* Generic.Deleted */ -.codehilite .ge { font-style: italic } /* Generic.Emph */ -.codehilite .gr { color: #FF0000 } /* Generic.Error */ -.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.codehilite .gi { color: #00A000 } /* Generic.Inserted */ -.codehilite .go { color: #888888 } /* Generic.Output */ -.codehilite .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ -.codehilite .gs { font-weight: bold } /* Generic.Strong */ -.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.codehilite .gt { color: #0044DD } /* Generic.Traceback */ -.codehilite .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ -.codehilite .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ -.codehilite .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ -.codehilite .kp { color: #007020 } /* Keyword.Pseudo */ -.codehilite .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ -.codehilite .kt { color: #902000 } /* Keyword.Type */ -.codehilite .m { color: #40a070 } /* Literal.Number */ -.codehilite .s { color: #4070a0 } /* Literal.String */ -.codehilite .na { color: #4070a0 } /* Name.Attribute */ -.codehilite .nb { color: #007020 } /* Name.Builtin */ -.codehilite .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ -.codehilite .no { color: #60add5 } /* Name.Constant */ -.codehilite .nd { color: #555555; font-weight: bold } /* Name.Decorator */ -.codehilite .ni { color: #d55537; font-weight: bold } /* Name.Entity */ -.codehilite .ne { color: #007020 } /* Name.Exception */ -.codehilite .nf { color: #06287e } /* Name.Function */ -.codehilite .nl { color: #002070; font-weight: bold } /* Name.Label */ -.codehilite .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ -.codehilite .nt { color: #062873; font-weight: bold } /* Name.Tag */ -.codehilite .nv { color: #bb60d5 } /* Name.Variable */ -.codehilite .ow { color: #007020; font-weight: bold } /* Operator.Word */ -.codehilite .w { color: #bbbbbb } /* Text.Whitespace */ -.codehilite .mb { color: #40a070 } /* Literal.Number.Bin */ -.codehilite .mf { color: #40a070 } /* Literal.Number.Float */ -.codehilite .mh { color: #40a070 } /* Literal.Number.Hex */ -.codehilite .mi { color: #40a070 } /* Literal.Number.Integer */ -.codehilite .mo { color: #40a070 } /* Literal.Number.Oct */ -.codehilite .sa { color: #4070a0 } /* Literal.String.Affix */ -.codehilite .sb { color: #4070a0 } /* Literal.String.Backtick */ -.codehilite .sc { color: #4070a0 } /* Literal.String.Char */ -.codehilite .dl { color: #4070a0 } /* Literal.String.Delimiter */ -.codehilite .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ -.codehilite .s2 { color: #4070a0 } /* Literal.String.Double */ -.codehilite .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ -.codehilite .sh { color: #4070a0 } /* Literal.String.Heredoc */ -.codehilite .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ -.codehilite .sx { color: #c65d09 } /* Literal.String.Other */ -.codehilite .sr { color: #235388 } /* Literal.String.Regex */ -.codehilite .s1 { color: #4070a0 } /* Literal.String.Single */ -.codehilite .ss { color: #517918 } /* Literal.String.Symbol */ -.codehilite .bp { color: #007020 } /* Name.Builtin.Pseudo */ -.codehilite .fm { color: #06287e } /* Name.Function.Magic */ -.codehilite .vc { color: #bb60d5 } /* Name.Variable.Class */ -.codehilite .vg { color: #bb60d5 } /* Name.Variable.Global */ -.codehilite .vi { color: #bb60d5 } /* Name.Variable.Instance */ -.codehilite .vm { color: #bb60d5 } /* Name.Variable.Magic */ -.codehilite .il { color: #40a070 } /* Literal.Number.Integer.Long */ diff --git a/pydis_site/static/css/pygments/monokai.css b/pydis_site/static/css/pygments/monokai.css deleted file mode 100644 index e0a1708a..00000000 --- a/pydis_site/static/css/pygments/monokai.css +++ /dev/null @@ -1,72 +0,0 @@ -/* Monokai */ - -.codehilite .hll { background-color: #49483e } -.codehilite pre { background: #272822; color: #f8f8f2 } -.codehilite .c { color: #75715e } /* Comment */ -.codehilite .err { color: #960050; background-color: #1e0010 } /* Error */ -.codehilite .k { color: #66d9ef } /* Keyword */ -.codehilite .l { color: #ae81ff } /* Literal */ -.codehilite .n { color: #f8f8f2 } /* Name */ -.codehilite .o { color: #f92672 } /* Operator */ -.codehilite .p { color: #f8f8f2 } /* Punctuation */ -.codehilite .ch { color: #75715e } /* Comment.Hashbang */ -.codehilite .cm { color: #75715e } /* Comment.Multiline */ -.codehilite .cp { color: #75715e } /* Comment.Preproc */ -.codehilite .cpf { color: #75715e } /* Comment.PreprocFile */ -.codehilite .c1 { color: #75715e } /* Comment.Single */ -.codehilite .cs { color: #75715e } /* Comment.Special */ -.codehilite .gd { color: #f92672 } /* Generic.Deleted */ -.codehilite .ge { font-style: italic } /* Generic.Emph */ -.codehilite .gi { color: #a6e22e } /* Generic.Inserted */ -.codehilite .gs { font-weight: bold } /* Generic.Strong */ -.codehilite .gu { color: #75715e } /* Generic.Subheading */ -.codehilite .kc { color: #66d9ef } /* Keyword.Constant */ -.codehilite .kd { color: #66d9ef } /* Keyword.Declaration */ -.codehilite .kn { color: #f92672 } /* Keyword.Namespace */ -.codehilite .kp { color: #66d9ef } /* Keyword.Pseudo */ -.codehilite .kr { color: #66d9ef } /* Keyword.Reserved */ -.codehilite .kt { color: #66d9ef } /* Keyword.Type */ -.codehilite .ld { color: #e6db74 } /* Literal.Date */ -.codehilite .m { color: #ae81ff } /* Literal.Number */ -.codehilite .s { color: #e6db74 } /* Literal.String */ -.codehilite .na { color: #a6e22e } /* Name.Attribute */ -.codehilite .nb { color: #f8f8f2 } /* Name.Builtin */ -.codehilite .nc { color: #a6e22e } /* Name.Class */ -.codehilite .no { color: #66d9ef } /* Name.Constant */ -.codehilite .nd { color: #a6e22e } /* Name.Decorator */ -.codehilite .ni { color: #f8f8f2 } /* Name.Entity */ -.codehilite .ne { color: #a6e22e } /* Name.Exception */ -.codehilite .nf { color: #a6e22e } /* Name.Function */ -.codehilite .nl { color: #f8f8f2 } /* Name.Label */ -.codehilite .nn { color: #f8f8f2 } /* Name.Namespace */ -.codehilite .nx { color: #a6e22e } /* Name.Other */ -.codehilite .py { color: #f8f8f2 } /* Name.Property */ -.codehilite .nt { color: #f92672 } /* Name.Tag */ -.codehilite .nv { color: #f8f8f2 } /* Name.Variable */ -.codehilite .ow { color: #f92672 } /* Operator.Word */ -.codehilite .w { color: #f8f8f2 } /* Text.Whitespace */ -.codehilite .mb { color: #ae81ff } /* Literal.Number.Bin */ -.codehilite .mf { color: #ae81ff } /* Literal.Number.Float */ -.codehilite .mh { color: #ae81ff } /* Literal.Number.Hex */ -.codehilite .mi { color: #ae81ff } /* Literal.Number.Integer */ -.codehilite .mo { color: #ae81ff } /* Literal.Number.Oct */ -.codehilite .sa { color: #e6db74 } /* Literal.String.Affix */ -.codehilite .sb { color: #e6db74 } /* Literal.String.Backtick */ -.codehilite .sc { color: #e6db74 } /* Literal.String.Char */ -.codehilite .dl { color: #e6db74 } /* Literal.String.Delimiter */ -.codehilite .sd { color: #e6db74 } /* Literal.String.Doc */ -.codehilite .s2 { color: #e6db74 } /* Literal.String.Double */ -.codehilite .se { color: #ae81ff } /* Literal.String.Escape */ -.codehilite .sh { color: #e6db74 } /* Literal.String.Heredoc */ -.codehilite .si { color: #e6db74 } /* Literal.String.Interpol */ -.codehilite .sx { color: #e6db74 } /* Literal.String.Other */ -.codehilite .sr { color: #e6db74 } /* Literal.String.Regex */ -.codehilite .s1 { color: #e6db74 } /* Literal.String.Single */ -.codehilite .ss { color: #e6db74 } /* Literal.String.Symbol */ -.codehilite .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ -.codehilite .fm { color: #a6e22e } /* Name.Function.Magic */ -.codehilite .vc { color: #f8f8f2 } /* Name.Variable.Class */ -.codehilite .vg { color: #f8f8f2 } /* Name.Variable.Global */ -.codehilite .vi { color: #f8f8f2 } /* Name.Variable.Instance */ -.codehilite .vm { color: #f8f8f2 } /* Name.Variable.Magic */ -.codehilite .il { color: #ae81ff } /* Literal.Number.Integer.Long */ diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css new file mode 100644 index 00000000..cf4cb472 --- /dev/null +++ b/pydis_site/static/css/resources/resources.css @@ -0,0 +1,29 @@ +.box, .tile.is-parent { + transition: 0.1s ease-out; +} +.box { + min-height: 15vh; +} +.tile.is-parent:hover .box { + box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); +} +.tile.is-parent:hover { + padding: 0.65rem 0.85rem 0.85rem 0.65rem; + filter: saturate(1.1) brightness(1.1); +} + +#readingBlock { + background-image: linear-gradient(141deg, #911eb4 0%, #b631de 71%, #cf4bf7 100%); +} + +#interactiveBlock { + background-image: linear-gradient(141deg, #d05600 0%, #da722a 71%, #e68846 100%); +} + +#communitiesBlock { + background-image: linear-gradient(141deg, #3b756f 0%, #3a847c 71%, #41948b 100%); +} + +#podcastsBlock { + background-image: linear-gradient(141deg, #232382 0%, #30309c 71%, #4343ad 100%); +} diff --git a/pydis_site/static/css/resources/resources_list.css b/pydis_site/static/css/resources/resources_list.css new file mode 100644 index 00000000..33129c87 --- /dev/null +++ b/pydis_site/static/css/resources/resources_list.css @@ -0,0 +1,55 @@ +.breadcrumb-section { + padding: 1rem; +} + +i.resource-icon.is-orangered { + color: #FE640A; +} + +i.resource-icon.is-orangered:hover { + color: #fe9840; +} + +i.resource-icon.is-blurple { + color: #7289DA; +} + +i.resource-icon.is-blurple:hover { + color: #93a8da; +} + +i.resource-icon.is-teal { + color: #95DBE5; +} + +i.resource-icon.is-teal:hover { + color: #a9f5ff; +} + +i.resource-icon.is-youtube-red { + color: #BB0000; +} + +i.resource-icon.is-youtube-red:hover { + color: #f80000; +} + +i.resource-icon.is-amazon-orange { + color: #FF9900; +} + +i.resource-icon.is-amazon-orange:hover { + color: #ffb71a; +} + +i.resource-icon.is-black { + color: #000000; +} + +i.resource-icon.is-black { + color: #191919; +} + +i.has-icon-padding { + padding: 0 10px 25px 0; +} diff --git a/pydis_site/static/css/wiki/simplemde-fixes.css b/pydis_site/static/css/wiki/simplemde-fixes.css deleted file mode 100644 index a9e3e1dd..00000000 --- a/pydis_site/static/css/wiki/simplemde-fixes.css +++ /dev/null @@ -1,7 +0,0 @@ -.CodeMirror-line { - margin-bottom: 0 !important; -} - -.CodeMirror { - height: 30rem; -} diff --git a/pydis_site/static/css/wiki/simplemde.min.css b/pydis_site/static/css/wiki/simplemde.min.css deleted file mode 100644 index a0ae10cf..00000000 --- a/pydis_site/static/css/wiki/simplemde.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/** - * simplemde v1.11.2 - * Copyright Next Step Webs, Inc. - * @link https://github.com/NextStepWebs/simplemde-markdown-editor - * @license MIT - */ -.CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:none;font-variant-ligatures:none}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;min-height:300px;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-scroll{min-height:300px}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)} diff --git a/pydis_site/static/css/wiki/style.css b/pydis_site/static/css/wiki/style.css deleted file mode 100644 index 9d619e8b..00000000 --- a/pydis_site/static/css/wiki/style.css +++ /dev/null @@ -1,89 +0,0 @@ -#wikiNavbar { - min-height: 3rem; - z-index: 1; -} - -#wikiNavbar .container { - min-height: 3rem; -} - -#wikiNavbar .navbar-brand { - min-height: 3rem; -} - -.breadcrumb-section { - padding: 1rem; -} - -div.control.is-fullwidth { - width: 100%; -} - -ul.pagination-list { - list-style: none; - margin: 0; -} - -ul.pagination-list li + li { - margin: 0; -} - -.pagination { - margin-top: 0.5rem; - margin-bottom: 0.5rem; -} - -/* FA icons used in wiki articles */ -i.is-orangered { - color: #FE640A; -} - -i.is-orangered:hover { - color: #fe9840; -} - -i.is-blurple { - color: #7289DA; -} - -i.is-blurple:hover { - color: #93a8da; -} - -i.is-black { - color: #000000; -} - -i.is-black:hover { - color: #191919; -} - -i.is-youtube-red { - color: #BB0000; -} - -i.is-youtube-red:hover { - color: #f80000; -} - -i.is-amazon-orange { - color: #FF9900; -} - -i.is-amazon-orange:hover { - color: #ffb71a; -} - -i.is-teal { - color: #95DBE5; -} - -i.is-teal:hover { - color: #a9f5ff; -} - - - -i.has-icon-padding { - padding: 0 10px 25px 0; -} diff --git a/pydis_site/static/images/content/contributing/docker_settings.png b/pydis_site/static/images/content/contributing/docker_settings.png Binary files differnew file mode 100644 index 00000000..03beeebf --- /dev/null +++ b/pydis_site/static/images/content/contributing/docker_settings.png diff --git a/pydis_site/static/images/content/contributing/docker_shared_drives.png b/pydis_site/static/images/content/contributing/docker_shared_drives.png Binary files differnew file mode 100644 index 00000000..c86bbdba --- /dev/null +++ b/pydis_site/static/images/content/contributing/docker_shared_drives.png diff --git a/pydis_site/static/images/content/contributing/fork_button.png b/pydis_site/static/images/content/contributing/fork_button.png Binary files differnew file mode 100644 index 00000000..4eab9e0d --- /dev/null +++ b/pydis_site/static/images/content/contributing/fork_button.png diff --git a/pydis_site/static/images/content/contributing/fork_clone.png b/pydis_site/static/images/content/contributing/fork_clone.png Binary files differnew file mode 100644 index 00000000..ad966ee3 --- /dev/null +++ b/pydis_site/static/images/content/contributing/fork_clone.png diff --git a/pydis_site/static/images/content/contributing/fork_user.png b/pydis_site/static/images/content/contributing/fork_user.png Binary files differnew file mode 100644 index 00000000..513f8841 --- /dev/null +++ b/pydis_site/static/images/content/contributing/fork_user.png diff --git a/pydis_site/static/images/content/contributing/github_issues_tab.png b/pydis_site/static/images/content/contributing/github_issues_tab.png Binary files differnew file mode 100644 index 00000000..1d1fc617 --- /dev/null +++ b/pydis_site/static/images/content/contributing/github_issues_tab.png diff --git a/pydis_site/static/images/content/contributing/github_new_issue.png b/pydis_site/static/images/content/contributing/github_new_issue.png Binary files differnew file mode 100644 index 00000000..1d661428 --- /dev/null +++ b/pydis_site/static/images/content/contributing/github_new_issue.png diff --git a/pydis_site/static/images/content/contributing/github_sample_issue.png b/pydis_site/static/images/content/contributing/github_sample_issue.png Binary files differnew file mode 100644 index 00000000..a8c2ac0a --- /dev/null +++ b/pydis_site/static/images/content/contributing/github_sample_issue.png diff --git a/pydis_site/static/images/wiki/contributing/pycharm_branch.png b/pydis_site/static/images/content/contributing/pycharm_branch.png Binary files differindex d9cde97a..d9cde97a 100644 --- a/pydis_site/static/images/wiki/contributing/pycharm_branch.png +++ b/pydis_site/static/images/content/contributing/pycharm_branch.png diff --git a/pydis_site/static/images/content/contributing/pycharm_checkout.png b/pydis_site/static/images/content/contributing/pycharm_checkout.png Binary files differnew file mode 100644 index 00000000..f719dfb7 --- /dev/null +++ b/pydis_site/static/images/content/contributing/pycharm_checkout.png diff --git a/pydis_site/static/images/wiki/contributing/pycharm_commit.png b/pydis_site/static/images/content/contributing/pycharm_commit.png Binary files differindex 03223688..03223688 100644 --- a/pydis_site/static/images/wiki/contributing/pycharm_commit.png +++ b/pydis_site/static/images/content/contributing/pycharm_commit.png diff --git a/pydis_site/static/images/wiki/contributing/pycharm_commit_button.png b/pydis_site/static/images/content/contributing/pycharm_commit_button.png Binary files differindex ae2745b0..ae2745b0 100644 --- a/pydis_site/static/images/wiki/contributing/pycharm_commit_button.png +++ b/pydis_site/static/images/content/contributing/pycharm_commit_button.png diff --git a/pydis_site/static/images/content/contributing/pycharm_create_project.png b/pydis_site/static/images/content/contributing/pycharm_create_project.png Binary files differnew file mode 100644 index 00000000..63cd5626 --- /dev/null +++ b/pydis_site/static/images/content/contributing/pycharm_create_project.png diff --git a/pydis_site/static/images/wiki/contributing/pycharm_interpreter.png b/pydis_site/static/images/content/contributing/pycharm_interpreter.png Binary files differindex cb757f84..cb757f84 100644 --- a/pydis_site/static/images/wiki/contributing/pycharm_interpreter.png +++ b/pydis_site/static/images/content/contributing/pycharm_interpreter.png diff --git a/pydis_site/static/images/wiki/contributing/pycharm_pipenv.png b/pydis_site/static/images/content/contributing/pycharm_pipenv.png Binary files differindex feb9b1d7..feb9b1d7 100644 --- a/pydis_site/static/images/wiki/contributing/pycharm_pipenv.png +++ b/pydis_site/static/images/content/contributing/pycharm_pipenv.png diff --git a/pydis_site/static/images/wiki/contributing/pycharm_pipenv_success.png b/pydis_site/static/images/content/contributing/pycharm_pipenv_success.png Binary files differindex 03fa3244..03fa3244 100644 --- a/pydis_site/static/images/wiki/contributing/pycharm_pipenv_success.png +++ b/pydis_site/static/images/content/contributing/pycharm_pipenv_success.png diff --git a/pydis_site/static/images/content/contributing/pycharm_pull.png b/pydis_site/static/images/content/contributing/pycharm_pull.png Binary files differnew file mode 100644 index 00000000..0640564e --- /dev/null +++ b/pydis_site/static/images/content/contributing/pycharm_pull.png diff --git a/pydis_site/static/images/content/contributing/pycharm_push.png b/pydis_site/static/images/content/contributing/pycharm_push.png Binary files differnew file mode 100644 index 00000000..1b51c085 --- /dev/null +++ b/pydis_site/static/images/content/contributing/pycharm_push.png diff --git a/pydis_site/static/images/content/contributing/pycharm_remotes.png b/pydis_site/static/images/content/contributing/pycharm_remotes.png Binary files differnew file mode 100644 index 00000000..3fe565c8 --- /dev/null +++ b/pydis_site/static/images/content/contributing/pycharm_remotes.png diff --git a/pydis_site/static/images/content/contributing/pycharm_upstream.png b/pydis_site/static/images/content/contributing/pycharm_upstream.png Binary files differnew file mode 100644 index 00000000..2ccb5827 --- /dev/null +++ b/pydis_site/static/images/content/contributing/pycharm_upstream.png diff --git a/pydis_site/static/images/content/discordpy_embed.png b/pydis_site/static/images/content/discordpy_embed.png Binary files differnew file mode 100644 index 00000000..9ed911a6 --- /dev/null +++ b/pydis_site/static/images/content/discordpy_embed.png diff --git a/pydis_site/static/images/content/help_channels/available_channels.png b/pydis_site/static/images/content/help_channels/available_channels.png Binary files differnew file mode 100644 index 00000000..0b9cfd03 --- /dev/null +++ b/pydis_site/static/images/content/help_channels/available_channels.png diff --git a/pydis_site/static/images/content/help_channels/available_message.png b/pydis_site/static/images/content/help_channels/available_message.png Binary files differnew file mode 100644 index 00000000..05f6ec7d --- /dev/null +++ b/pydis_site/static/images/content/help_channels/available_message.png diff --git a/pydis_site/static/images/content/help_channels/dormant_channels.png b/pydis_site/static/images/content/help_channels/dormant_channels.png Binary files differnew file mode 100644 index 00000000..2c53de87 --- /dev/null +++ b/pydis_site/static/images/content/help_channels/dormant_channels.png diff --git a/pydis_site/static/images/content/help_channels/dormant_message.png b/pydis_site/static/images/content/help_channels/dormant_message.png Binary files differnew file mode 100644 index 00000000..ac806459 --- /dev/null +++ b/pydis_site/static/images/content/help_channels/dormant_message.png diff --git a/pydis_site/static/images/content/help_channels/occupied_channels.png b/pydis_site/static/images/content/help_channels/occupied_channels.png Binary files differnew file mode 100644 index 00000000..6ccb4ed6 --- /dev/null +++ b/pydis_site/static/images/content/help_channels/occupied_channels.png diff --git a/pydis_site/static/images/content/help_channels/topical_channels.png b/pydis_site/static/images/content/help_channels/topical_channels.png Binary files differnew file mode 100644 index 00000000..63b48e7b --- /dev/null +++ b/pydis_site/static/images/content/help_channels/topical_channels.png diff --git a/pydis_site/static/images/content/mutability/hello_gets_eaten.png b/pydis_site/static/images/content/mutability/hello_gets_eaten.png Binary files differnew file mode 100644 index 00000000..08e9f2cb --- /dev/null +++ b/pydis_site/static/images/content/mutability/hello_gets_eaten.png diff --git a/pydis_site/static/images/content/mutability/s_gets_assigned_to_HELLO.png b/pydis_site/static/images/content/mutability/s_gets_assigned_to_HELLO.png Binary files differnew file mode 100644 index 00000000..5f2baf0d --- /dev/null +++ b/pydis_site/static/images/content/mutability/s_gets_assigned_to_HELLO.png diff --git a/pydis_site/static/images/content/mutability/s_refers_hello.png b/pydis_site/static/images/content/mutability/s_refers_hello.png Binary files differnew file mode 100644 index 00000000..59d84211 --- /dev/null +++ b/pydis_site/static/images/content/mutability/s_refers_hello.png diff --git a/pydis_site/static/images/content/mutability/s_upper_creates_HELLO.png b/pydis_site/static/images/content/mutability/s_upper_creates_HELLO.png Binary files differnew file mode 100644 index 00000000..6aa1e8d3 --- /dev/null +++ b/pydis_site/static/images/content/mutability/s_upper_creates_HELLO.png diff --git a/pydis_site/static/images/wiki/contributing/fork_button.png b/pydis_site/static/images/wiki/contributing/fork_button.png Binary files differdeleted file mode 100644 index 955c93dd..00000000 --- a/pydis_site/static/images/wiki/contributing/fork_button.png +++ /dev/null diff --git a/pydis_site/static/images/wiki/contributing/fork_url.png b/pydis_site/static/images/wiki/contributing/fork_url.png Binary files differdeleted file mode 100644 index 57a8e421..00000000 --- a/pydis_site/static/images/wiki/contributing/fork_url.png +++ /dev/null diff --git a/pydis_site/static/images/wiki/contributing/fork_user.png b/pydis_site/static/images/wiki/contributing/fork_user.png Binary files differdeleted file mode 100644 index b8317cab..00000000 --- a/pydis_site/static/images/wiki/contributing/fork_user.png +++ /dev/null diff --git a/pydis_site/static/images/wiki/contributing/pycharm_checkout.png b/pydis_site/static/images/wiki/contributing/pycharm_checkout.png Binary files differdeleted file mode 100644 index d679a467..00000000 --- a/pydis_site/static/images/wiki/contributing/pycharm_checkout.png +++ /dev/null diff --git a/pydis_site/static/images/wiki/contributing/pycharm_pull.png b/pydis_site/static/images/wiki/contributing/pycharm_pull.png Binary files differdeleted file mode 100644 index f82725fa..00000000 --- a/pydis_site/static/images/wiki/contributing/pycharm_pull.png +++ /dev/null diff --git a/pydis_site/static/images/wiki/contributing/pycharm_push.png b/pydis_site/static/images/wiki/contributing/pycharm_push.png Binary files differdeleted file mode 100644 index a21a18b1..00000000 --- a/pydis_site/static/images/wiki/contributing/pycharm_push.png +++ /dev/null diff --git a/pydis_site/static/images/wiki/contributing/pycharm_remotes.png b/pydis_site/static/images/wiki/contributing/pycharm_remotes.png Binary files differdeleted file mode 100644 index b2e4239c..00000000 --- a/pydis_site/static/images/wiki/contributing/pycharm_remotes.png +++ /dev/null diff --git a/pydis_site/static/images/wiki/contributing/pycharm_test_clone.png b/pydis_site/static/images/wiki/contributing/pycharm_test_clone.png Binary files differdeleted file mode 100644 index 6410b847..00000000 --- a/pydis_site/static/images/wiki/contributing/pycharm_test_clone.png +++ /dev/null diff --git a/pydis_site/static/images/wiki/contributing/pycharm_upstream.png b/pydis_site/static/images/wiki/contributing/pycharm_upstream.png Binary files differdeleted file mode 100644 index 70c60ea2..00000000 --- a/pydis_site/static/images/wiki/contributing/pycharm_upstream.png +++ /dev/null diff --git a/pydis_site/static/js/base/modal.js b/pydis_site/static/js/base/modal.js deleted file mode 100644 index eccc8845..00000000 --- a/pydis_site/static/js/base/modal.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - modal.js: A simple way to wire up Bulma modals. - - This library is intended to be used with Bulma's modals, as described in the - official Bulma documentation. It's based on the JavaScript that Bulma - themselves use for this purpose on the modals documentation page. - - Note that, just like that piece of JavaScript, this library assumes that - you will only ever want to have one modal open at once. - */ - -"use strict"; - -// Event handler for the "esc" key, for closing modals. - -document.addEventListener("keydown", (event) => { - const e = event || window.event; - - if (e.code === "Escape" || e.keyCode === 27) { - closeModals(); - } -}); - -// An array of all the modal buttons we've already set up - -const modal_buttons = []; - -// Public API functions - -function setupModal(target) { - // Set up a modal's events, given a DOM element. This can be - // used later in order to set up a modal that was added after - // this library has been run. - - // We need to collect a bunch of elements to work with - const modal_background = Array.from(target.getElementsByClassName("modal-background")); - const modal_close = Array.from(target.getElementsByClassName("modal-close")); - - const modal_head = Array.from(target.getElementsByClassName("modal-card-head")); - const modal_foot = Array.from(target.getElementsByClassName("modal-card-foot")); - - const modal_delete = []; - const modal_button = []; - - modal_head.forEach((element) => modal_delete.concat(Array.from(element.getElementsByClassName("delete")))); - modal_foot.forEach((element) => modal_button.concat(Array.from(element.getElementsByClassName("button")))); - - // Collect all the elements that can be used to close modals - const modal_closers = modal_background.concat(modal_close).concat(modal_delete).concat(modal_button); - - // Assign click events for closing modals - modal_closers.forEach((element) => { - element.addEventListener("click", () => { - closeModals(); - }); - }); - - setupOpeningButtons(); -} - -function setupOpeningButtons() { - // Wire up all the opening buttons, avoiding buttons we've already wired up. - const modal_opening_buttons = Array.from(document.getElementsByClassName("modal-button")); - - modal_opening_buttons.forEach((element) => { - if (!modal_buttons.includes(element)) { - element.addEventListener("click", () => { - openModal(element.dataset.target); - }); - - modal_buttons.push(element); - } - }); -} - -function openModal(target) { - // Open a modal, given a string ID - const element = document.getElementById(target); - - document.documentElement.classList.add("is-clipped"); - element.classList.add("is-active"); -} - -function closeModals() { - // Close all open modals - const modals = Array.from(document.getElementsByClassName("modal")); - document.documentElement.classList.remove("is-clipped"); - - modals.forEach((element) => { - element.classList.remove("is-active"); - }); -} - -(function () { - // Set up all the modals currently on the page - const modals = Array.from(document.getElementsByClassName("modal")); - - modals.forEach((modal) => setupModal(modal)); - setupOpeningButtons(); -}()); diff --git a/pydis_site/static/js/wiki/create.js b/pydis_site/static/js/wiki/create.js deleted file mode 100644 index e02d75a3..00000000 --- a/pydis_site/static/js/wiki/create.js +++ /dev/null @@ -1,13 +0,0 @@ -//<![CDATA[ -(function($) { - $(document).ready(function (){ - $("#id_title").keyup(function () { - var e = $("#id_slug")[0]; - if(!e._changed) { - slug = URLify(this.value, 50); - e.value = slug; - } - }); - }); -})(jQuery); -//]]> diff --git a/pydis_site/static/js/wiki/dropdown.js b/pydis_site/static/js/wiki/dropdown.js deleted file mode 100644 index a914a4ab..00000000 --- a/pydis_site/static/js/wiki/dropdown.js +++ /dev/null @@ -1,35 +0,0 @@ -(function() { - window.dropdowns = {}; - - let elements = document.getElementsByClassName("dropdown"); - - for (let element of elements) { - let menu_element = element.getElementsByClassName("dropdown-menu")[0]; - - function show() { - $(element).addClass("is-active"); - } - - function hide() { - $(element).removeClass("is-active"); - } - - function handle_event(e) { - show(); - - $(document.body).on("click." + menu_element.id, function() { - hide(); - - $(document.body).off("click." + menu_element.id); - }); - - e.stopPropagation(); - } - - $(element).click(handle_event); - $(element).hover(handle_event); - $(element).mouseleave(hide); - - window.dropdowns[menu_element.id] = element; - } -})(); diff --git a/pydis_site/static/js/wiki/edit.js b/pydis_site/static/js/wiki/edit.js deleted file mode 100644 index 0af44431..00000000 --- a/pydis_site/static/js/wiki/edit.js +++ /dev/null @@ -1,58 +0,0 @@ -$(document).ready(function() { - let article_edit_form = $("#article_edit_form"); - let click_time = 0; - - $("#article_edit_form :input").change(function() { - article_edit_form.data("changed",true); - }); - - if (article_edit_form.find(".alert-danger").length > 0 || article_edit_form.find(".has-error").length > 0 ) { - // Set the forms status as "changed" if there was a submission error - article_edit_form.data("changed",true); - } - - window.onbeforeunload = confirmOnPageExit; - - article_edit_form.on("submit", function (ev) { - now = Date.now(); - elapsed = now-click_time; - click_time = now; - if (elapsed < 3000) - ev.preventDefault(); - window.onbeforeunload = null; - return true; - }); - $("#id_preview").click(function () { - open_modal("previewModal"); - return true; - }); - $("#id_preview_save_changes").on("click", function (ev) { - ev.preventDefault(); - $("#id_save").trigger("click"); - }); -}); - -var confirmOnPageExit = function (e) { - if ($("#article_edit_form").data("changed")) { - e = e || window.event; - var message = "You have unsaved changes!"; - // For IE6-8 and Firefox prior to version 4 - if (e) { - e.returnValue = message; - } - // For Chrome, Safari, IE8+ and Opera 12+ - return message; - } else { - // If the form hasn't been changed, don't display the pop-up - return; - } -}; - -$(document).ready( function() { - $('.sidebar-form').each(function () { - $(this).submit( function() { - this.unsaved_article_title.value = $('#id_title').val(); - this.unsaved_article_content.value = $('#id_content').val(); - }); - }); -}); diff --git a/pydis_site/static/js/wiki/editor_sidebar.js b/pydis_site/static/js/wiki/editor_sidebar.js deleted file mode 100644 index 0f17c109..00000000 --- a/pydis_site/static/js/wiki/editor_sidebar.js +++ /dev/null @@ -1 +0,0 @@ -bulmaAccordion.attach(); diff --git a/pydis_site/static/js/wiki/history.js b/pydis_site/static/js/wiki/history.js deleted file mode 100644 index 1f71e911..00000000 --- a/pydis_site/static/js/wiki/history.js +++ /dev/null @@ -1,12 +0,0 @@ -function showPreviewModal(revision_id, action_url, change_revision_url) { - let iframe = $("#previewWindow"); - - iframe.attr("src", action_url + "?r=" + revision_id); - - console.log(revision_id); - console.log(action_url + "?r=" + revision_id); - console.log(change_revision_url); - - $('#previewModal .switch-to-revision').attr('href', change_revision_url); - open_modal('previewModal'); -} diff --git a/pydis_site/static/js/wiki/image_sidebar.js b/pydis_site/static/js/wiki/image_sidebar.js deleted file mode 100644 index 9ac9f79d..00000000 --- a/pydis_site/static/js/wiki/image_sidebar.js +++ /dev/null @@ -1,32 +0,0 @@ -$("#id_image_insert").click(function(e) { -e.preventDefault(); - -let image_id_element = document.getElementById("img_id"), - align_element = document.getElementById("img_align"), - size_element = document.getElementById("img_size"), - caption_element = document.getElementById("img_caption"), - - editor = window.editors["id_content"]; - -editor.insert_image_wiki( - image_id_element.value, align_element.value, - size_element.value, caption_element.value -); - -$("#imgModal").removeClass("is-active"); // Close modal - -// Reset form -image_id_element.value = 0; -align_element.selectedIndex = 0; -size_element.selectedIndex = 0; -caption_element.value = ""; -}); - -function insert_image(image_id) { -document.getElementById("img_id").value = image_id; -open_modal("imgModal"); -} - -function add_image(form) { - $(form).submit(); -} diff --git a/pydis_site/static/js/wiki/links_sidebar.js b/pydis_site/static/js/wiki/links_sidebar.js deleted file mode 100644 index f50d968d..00000000 --- a/pydis_site/static/js/wiki/links_sidebar.js +++ /dev/null @@ -1,30 +0,0 @@ -$(document).ready(function() { - function search(query) { - query = encodeURIComponent(query); - return fetch(window.links_fetch_url + `?query=${query}`).then(function(response) { - return response.json(); - }).then(function(data){ - return data.map(function(element) { - return {label: element, value: element}; - }) - }); - } - - function selected(state) { - let value = state.value; - wikiInsertLink(value); - document.getElementById("page_title_input").value = ""; - } - - bulmahead("page_title_input", "page_title_menu", search, selected, 10); -}); - -function wikiInsertLink(value) { - let editor = window.editors["id_content"]; - - editor.insert_text(value); -} - -function setFetchURL(url) { - window.links_fetch_url = url; -} diff --git a/pydis_site/static/js/wiki/load_editor.js b/pydis_site/static/js/wiki/load_editor.js deleted file mode 100644 index 589d8a75..00000000 --- a/pydis_site/static/js/wiki/load_editor.js +++ /dev/null @@ -1,96 +0,0 @@ -(function() { - window.editors = {}; // So that other scripts can get at 'em - - const headingAction = { - name: "heading", - action: SimpleMDE.toggleHeadingSmaller, - className: "fa fa-heading", - title: "Heading", - }; - const imageAction = { - name: "image", - action: SimpleMDE.drawImage, - className: "fa fa-image", - title: "Insert image", - }; - - const imageAlign = "align:{ALIGN} "; - const imageSize = "size:{SIZE}"; - - let elements = document.getElementsByClassName("simple-mde"); - - function add_insert_image_wiki(editor) { - editor.insert_image_wiki = function(id, align, size, caption) { - let contents = "", - doc = editor.codemirror.getDoc(), - cursor = doc.getCursor(); - - if (typeof align !== "undefined" && align.length) { - contents = contents + imageAlign.replace("{ALIGN}", align); - } - - if (typeof size !== "undefined" && size.length) { - contents = contents + imageSize.replace("{SIZE}", size); - } - - contents = `\n[image:${id} ${contents}]`; - - if (typeof caption !== "undefined" && caption.length) { - contents = contents + "\n" + ` ${caption}` - } - - doc.replaceRange(contents, cursor); - } - } - - function add_insert_text(editor) { - editor.insert_text = function(text) { - let doc = editor.codemirror.getDoc(), - cursor = doc.getCursor(); - - doc.replaceRange(text, cursor); - } - } - - for (let element of elements) { - let editor = new SimpleMDE({ - "element": element, - - autoDownloadFontAwesome: false, // We already have the pro one loaded - - autosave: { - enabled: false, - // uniqueId: element.id + "@" + window.location.href, - }, - - blockStyles: { - bold: "**", - code: "```", - italic: "_", - }, - - forceSync: true, - indentWithTabs: false, - initialValue: element.value, - lineWrapping: true, - placeholder: "**Write some _markdown_!**", - spellChecker: false, - tabSize: 4, - - toolbar: [ - "bold", "italic", "strikethrough", headingAction, "|", - "code", "quote", "unordered-list", "ordered-list", "|", - "link", imageAction, "table", "horizontal-rule", "|", - "preview", "side-by-side", "fullscreen", "|", - "guide" - ], - - status: false, - }); - - add_insert_image_wiki(editor); - add_insert_text(editor); - - window.editors[element.id] = editor; - } -})(); diff --git a/pydis_site/static/js/wiki/modal.js b/pydis_site/static/js/wiki/modal.js deleted file mode 100644 index 1eb7b056..00000000 --- a/pydis_site/static/js/wiki/modal.js +++ /dev/null @@ -1,14 +0,0 @@ -function open_modal(id) { - let element = document.getElementById(id); - - $(element).addClass("is-active"); - - $(element).find(".modal-background").click(function() { - $(element).removeClass("is-active"); - }); - - $(element).find("[aria-label=\"close\"]").click(function(e) { - $(element).removeClass("is-active"); - e.preventDefault(); - }); -} diff --git a/pydis_site/static/js/wiki/move.js b/pydis_site/static/js/wiki/move.js deleted file mode 100644 index ddab06f5..00000000 --- a/pydis_site/static/js/wiki/move.js +++ /dev/null @@ -1,8 +0,0 @@ -$('#id_destination').after($('#dest_selector').remove()); -$('#id_destination').attr('type', 'hidden'); - -function select_path(path, title) { - $('#id_destination').val(path); - if (title == "(root)") title = ""; - $('#dest_selector .dest_selector_title').html(title ? title : " / "); -} diff --git a/pydis_site/static/js/wiki/simplemde.min.js b/pydis_site/static/js/wiki/simplemde.min.js deleted file mode 100644 index 012d39a3..00000000 --- a/pydis_site/static/js/wiki/simplemde.min.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * simplemde v1.11.2 - * Copyright Next Step Webs, Inc. - * @link https://github.com/NextStepWebs/simplemde-markdown-editor - * @license MIT - */ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.SimpleMDE=e()}}(function(){var e;return function t(e,n,r){function i(a,l){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!l&&s)return s(a,!0);if(o)return o(a,!0);var c=new Error("Cannot find module '"+a+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[a]={exports:{}};e[a][0].call(u.exports,function(t){var n=e[a][1][t];return i(n?n:t)},u,u.exports,t,e,n,r)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;a<r.length;a++)i(r[a]);return i}({1:[function(e,t,n){"use strict";function r(){for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",t=0,n=e.length;n>t;++t)s[t]=e[t],c[e.charCodeAt(t)]=t;c["-".charCodeAt(0)]=62,c["_".charCodeAt(0)]=63}function i(e){var t,n,r,i,o,a,l=e.length;if(l%4>0)throw new Error("Invalid string. Length must be a multiple of 4");o="="===e[l-2]?2:"="===e[l-1]?1:0,a=new u(3*l/4-o),r=o>0?l-4:l;var s=0;for(t=0,n=0;r>t;t+=4,n+=3)i=c[e.charCodeAt(t)]<<18|c[e.charCodeAt(t+1)]<<12|c[e.charCodeAt(t+2)]<<6|c[e.charCodeAt(t+3)],a[s++]=i>>16&255,a[s++]=i>>8&255,a[s++]=255&i;return 2===o?(i=c[e.charCodeAt(t)]<<2|c[e.charCodeAt(t+1)]>>4,a[s++]=255&i):1===o&&(i=c[e.charCodeAt(t)]<<10|c[e.charCodeAt(t+1)]<<4|c[e.charCodeAt(t+2)]>>2,a[s++]=i>>8&255,a[s++]=255&i),a}function o(e){return s[e>>18&63]+s[e>>12&63]+s[e>>6&63]+s[63&e]}function a(e,t,n){for(var r,i=[],a=t;n>a;a+=3)r=(e[a]<<16)+(e[a+1]<<8)+e[a+2],i.push(o(r));return i.join("")}function l(e){for(var t,n=e.length,r=n%3,i="",o=[],l=16383,c=0,u=n-r;u>c;c+=l)o.push(a(e,c,c+l>u?u:c+l));return 1===r?(t=e[n-1],i+=s[t>>2],i+=s[t<<4&63],i+="=="):2===r&&(t=(e[n-2]<<8)+e[n-1],i+=s[t>>10],i+=s[t>>4&63],i+=s[t<<2&63],i+="="),o.push(i),o.join("")}n.toByteArray=i,n.fromByteArray=l;var s=[],c=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array;r()},{}],2:[function(e,t,n){},{}],3:[function(e,t,n){(function(t){"use strict";function r(){try{var e=new Uint8Array(1);return e.foo=function(){return 42},42===e.foo()&&"function"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(t){return!1}}function i(){return a.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function o(e,t){if(i()<t)throw new RangeError("Invalid typed array length");return a.TYPED_ARRAY_SUPPORT?(e=new Uint8Array(t),e.__proto__=a.prototype):(null===e&&(e=new a(t)),e.length=t),e}function a(e,t,n){if(!(a.TYPED_ARRAY_SUPPORT||this instanceof a))return new a(e,t,n);if("number"==typeof e){if("string"==typeof t)throw new Error("If encoding is specified then the first argument must be a string");return u(this,e)}return l(this,e,t,n)}function l(e,t,n,r){if("number"==typeof t)throw new TypeError('"value" argument must not be a number');return"undefined"!=typeof ArrayBuffer&&t instanceof ArrayBuffer?d(e,t,n,r):"string"==typeof t?f(e,t,n):p(e,t)}function s(e){if("number"!=typeof e)throw new TypeError('"size" argument must be a number')}function c(e,t,n,r){return s(t),0>=t?o(e,t):void 0!==n?"string"==typeof r?o(e,t).fill(n,r):o(e,t).fill(n):o(e,t)}function u(e,t){if(s(t),e=o(e,0>t?0:0|m(t)),!a.TYPED_ARRAY_SUPPORT)for(var n=0;t>n;n++)e[n]=0;return e}function f(e,t,n){if("string"==typeof n&&""!==n||(n="utf8"),!a.isEncoding(n))throw new TypeError('"encoding" must be a valid string encoding');var r=0|v(t,n);return e=o(e,r),e.write(t,n),e}function h(e,t){var n=0|m(t.length);e=o(e,n);for(var r=0;n>r;r+=1)e[r]=255&t[r];return e}function d(e,t,n,r){if(t.byteLength,0>n||t.byteLength<n)throw new RangeError("'offset' is out of bounds");if(t.byteLength<n+(r||0))throw new RangeError("'length' is out of bounds");return t=void 0===r?new Uint8Array(t,n):new Uint8Array(t,n,r),a.TYPED_ARRAY_SUPPORT?(e=t,e.__proto__=a.prototype):e=h(e,t),e}function p(e,t){if(a.isBuffer(t)){var n=0|m(t.length);return e=o(e,n),0===e.length?e:(t.copy(e,0,0,n),e)}if(t){if("undefined"!=typeof ArrayBuffer&&t.buffer instanceof ArrayBuffer||"length"in t)return"number"!=typeof t.length||K(t.length)?o(e,0):h(e,t);if("Buffer"===t.type&&J(t.data))return h(e,t.data)}throw new TypeError("First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.")}function m(e){if(e>=i())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i().toString(16)+" bytes");return 0|e}function g(e){return+e!=e&&(e=0),a.alloc(+e)}function v(e,t){if(a.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"binary":case"raw":case"raws":return n;case"utf8":case"utf-8":case void 0:return q(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return $(e).length;default:if(r)return q(e).length;t=(""+t).toLowerCase(),r=!0}}function y(e,t,n){var r=!1;if((void 0===t||0>t)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),0>=n)return"";if(n>>>=0,t>>>=0,t>=n)return"";for(e||(e="utf8");;)switch(e){case"hex":return I(this,t,n);case"utf8":case"utf-8":return N(this,t,n);case"ascii":return E(this,t,n);case"binary":return O(this,t,n);case"base64":return M(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return P(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function x(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function b(e,t,n,r){function i(e,t){return 1===o?e[t]:e.readUInt16BE(t*o)}var o=1,a=e.length,l=t.length;if(void 0!==r&&(r=String(r).toLowerCase(),"ucs2"===r||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;o=2,a/=2,l/=2,n/=2}for(var s=-1,c=0;a>n+c;c++)if(i(e,n+c)===i(t,-1===s?0:c-s)){if(-1===s&&(s=c),c-s+1===l)return(n+s)*o}else-1!==s&&(c-=c-s),s=-1;return-1}function w(e,t,n,r){n=Number(n)||0;var i=e.length-n;r?(r=Number(r),r>i&&(r=i)):r=i;var o=t.length;if(o%2!==0)throw new Error("Invalid hex string");r>o/2&&(r=o/2);for(var a=0;r>a;a++){var l=parseInt(t.substr(2*a,2),16);if(isNaN(l))return a;e[n+a]=l}return a}function k(e,t,n,r){return V(q(t,e.length-n),e,n,r)}function S(e,t,n,r){return V(G(t),e,n,r)}function C(e,t,n,r){return S(e,t,n,r)}function L(e,t,n,r){return V($(t),e,n,r)}function T(e,t,n,r){return V(Y(t,e.length-n),e,n,r)}function M(e,t,n){return 0===t&&n===e.length?X.fromByteArray(e):X.fromByteArray(e.slice(t,n))}function N(e,t,n){n=Math.min(e.length,n);for(var r=[],i=t;n>i;){var o=e[i],a=null,l=o>239?4:o>223?3:o>191?2:1;if(n>=i+l){var s,c,u,f;switch(l){case 1:128>o&&(a=o);break;case 2:s=e[i+1],128===(192&s)&&(f=(31&o)<<6|63&s,f>127&&(a=f));break;case 3:s=e[i+1],c=e[i+2],128===(192&s)&&128===(192&c)&&(f=(15&o)<<12|(63&s)<<6|63&c,f>2047&&(55296>f||f>57343)&&(a=f));break;case 4:s=e[i+1],c=e[i+2],u=e[i+3],128===(192&s)&&128===(192&c)&&128===(192&u)&&(f=(15&o)<<18|(63&s)<<12|(63&c)<<6|63&u,f>65535&&1114112>f&&(a=f))}}null===a?(a=65533,l=1):a>65535&&(a-=65536,r.push(a>>>10&1023|55296),a=56320|1023&a),r.push(a),i+=l}return A(r)}function A(e){var t=e.length;if(Q>=t)return String.fromCharCode.apply(String,e);for(var n="",r=0;t>r;)n+=String.fromCharCode.apply(String,e.slice(r,r+=Q));return n}function E(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;n>i;i++)r+=String.fromCharCode(127&e[i]);return r}function O(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;n>i;i++)r+=String.fromCharCode(e[i]);return r}function I(e,t,n){var r=e.length;(!t||0>t)&&(t=0),(!n||0>n||n>r)&&(n=r);for(var i="",o=t;n>o;o++)i+=U(e[o]);return i}function P(e,t,n){for(var r=e.slice(t,n),i="",o=0;o<r.length;o+=2)i+=String.fromCharCode(r[o]+256*r[o+1]);return i}function R(e,t,n){if(e%1!==0||0>e)throw new RangeError("offset is not uint");if(e+t>n)throw new RangeError("Trying to access beyond buffer length")}function D(e,t,n,r,i,o){if(!a.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||o>t)throw new RangeError('"value" argument is out of bounds');if(n+r>e.length)throw new RangeError("Index out of range")}function H(e,t,n,r){0>t&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-n,2);o>i;i++)e[n+i]=(t&255<<8*(r?i:1-i))>>>8*(r?i:1-i)}function W(e,t,n,r){0>t&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-n,4);o>i;i++)e[n+i]=t>>>8*(r?i:3-i)&255}function B(e,t,n,r,i,o){if(n+r>e.length)throw new RangeError("Index out of range");if(0>n)throw new RangeError("Index out of range")}function _(e,t,n,r,i){return i||B(e,t,n,4,3.4028234663852886e38,-3.4028234663852886e38),Z.write(e,t,n,r,23,4),n+4}function F(e,t,n,r,i){return i||B(e,t,n,8,1.7976931348623157e308,-1.7976931348623157e308),Z.write(e,t,n,r,52,8),n+8}function z(e){if(e=j(e).replace(ee,""),e.length<2)return"";for(;e.length%4!==0;)e+="=";return e}function j(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}function U(e){return 16>e?"0"+e.toString(16):e.toString(16)}function q(e,t){t=t||1/0;for(var n,r=e.length,i=null,o=[],a=0;r>a;a++){if(n=e.charCodeAt(a),n>55295&&57344>n){if(!i){if(n>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(56320>n){(t-=3)>-1&&o.push(239,191,189),i=n;continue}n=(i-55296<<10|n-56320)+65536}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,128>n){if((t-=1)<0)break;o.push(n)}else if(2048>n){if((t-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(65536>n){if((t-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(1114112>n))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function G(e){for(var t=[],n=0;n<e.length;n++)t.push(255&e.charCodeAt(n));return t}function Y(e,t){for(var n,r,i,o=[],a=0;a<e.length&&!((t-=2)<0);a++)n=e.charCodeAt(a),r=n>>8,i=n%256,o.push(i),o.push(r);return o}function $(e){return X.toByteArray(z(e))}function V(e,t,n,r){for(var i=0;r>i&&!(i+n>=t.length||i>=e.length);i++)t[i+n]=e[i];return i}function K(e){return e!==e}var X=e("base64-js"),Z=e("ieee754"),J=e("isarray");n.Buffer=a,n.SlowBuffer=g,n.INSPECT_MAX_BYTES=50,a.TYPED_ARRAY_SUPPORT=void 0!==t.TYPED_ARRAY_SUPPORT?t.TYPED_ARRAY_SUPPORT:r(),n.kMaxLength=i(),a.poolSize=8192,a._augment=function(e){return e.__proto__=a.prototype,e},a.from=function(e,t,n){return l(null,e,t,n)},a.TYPED_ARRAY_SUPPORT&&(a.prototype.__proto__=Uint8Array.prototype,a.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&a[Symbol.species]===a&&Object.defineProperty(a,Symbol.species,{value:null,configurable:!0})),a.alloc=function(e,t,n){return c(null,e,t,n)},a.allocUnsafe=function(e){return u(null,e)},a.allocUnsafeSlow=function(e){return u(null,e)},a.isBuffer=function(e){return!(null==e||!e._isBuffer)},a.compare=function(e,t){if(!a.isBuffer(e)||!a.isBuffer(t))throw new TypeError("Arguments must be Buffers");if(e===t)return 0;for(var n=e.length,r=t.length,i=0,o=Math.min(n,r);o>i;++i)if(e[i]!==t[i]){n=e[i],r=t[i];break}return r>n?-1:n>r?1:0},a.isEncoding=function(e){switch(String(e).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"raw":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},a.concat=function(e,t){if(!J(e))throw new TypeError('"list" argument must be an Array of Buffers');if(0===e.length)return a.alloc(0);var n;if(void 0===t)for(t=0,n=0;n<e.length;n++)t+=e[n].length;var r=a.allocUnsafe(t),i=0;for(n=0;n<e.length;n++){var o=e[n];if(!a.isBuffer(o))throw new TypeError('"list" argument must be an Array of Buffers');o.copy(r,i),i+=o.length}return r},a.byteLength=v,a.prototype._isBuffer=!0,a.prototype.swap16=function(){var e=this.length;if(e%2!==0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(var t=0;e>t;t+=2)x(this,t,t+1);return this},a.prototype.swap32=function(){var e=this.length;if(e%4!==0)throw new RangeError("Buffer size must be a multiple of 32-bits");for(var t=0;e>t;t+=4)x(this,t,t+3),x(this,t+1,t+2);return this},a.prototype.toString=function(){var e=0|this.length;return 0===e?"":0===arguments.length?N(this,0,e):y.apply(this,arguments)},a.prototype.equals=function(e){if(!a.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e?!0:0===a.compare(this,e)},a.prototype.inspect=function(){var e="",t=n.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,t).match(/.{2}/g).join(" "),this.length>t&&(e+=" ... ")),"<Buffer "+e+">"},a.prototype.compare=function(e,t,n,r,i){if(!a.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),0>t||n>e.length||0>r||i>this.length)throw new RangeError("out of range index");if(r>=i&&t>=n)return 0;if(r>=i)return-1;if(t>=n)return 1;if(t>>>=0,n>>>=0,r>>>=0,i>>>=0,this===e)return 0;for(var o=i-r,l=n-t,s=Math.min(o,l),c=this.slice(r,i),u=e.slice(t,n),f=0;s>f;++f)if(c[f]!==u[f]){o=c[f],l=u[f];break}return l>o?-1:o>l?1:0},a.prototype.indexOf=function(e,t,n){if("string"==typeof t?(n=t,t=0):t>2147483647?t=2147483647:-2147483648>t&&(t=-2147483648),t>>=0,0===this.length)return-1;if(t>=this.length)return-1;if(0>t&&(t=Math.max(this.length+t,0)),"string"==typeof e&&(e=a.from(e,n)),a.isBuffer(e))return 0===e.length?-1:b(this,e,t,n);if("number"==typeof e)return a.TYPED_ARRAY_SUPPORT&&"function"===Uint8Array.prototype.indexOf?Uint8Array.prototype.indexOf.call(this,e,t):b(this,[e],t,n);throw new TypeError("val must be string, number or Buffer")},a.prototype.includes=function(e,t,n){return-1!==this.indexOf(e,t,n)},a.prototype.write=function(e,t,n,r){if(void 0===t)r="utf8",n=this.length,t=0;else if(void 0===n&&"string"==typeof t)r=t,n=this.length,t=0;else{if(!isFinite(t))throw new Error("Buffer.write(string, encoding, offset[, length]) is no longer supported");t=0|t,isFinite(n)?(n=0|n,void 0===r&&(r="utf8")):(r=n,n=void 0)}var i=this.length-t;if((void 0===n||n>i)&&(n=i),e.length>0&&(0>n||0>t)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return w(this,e,t,n);case"utf8":case"utf-8":return k(this,e,t,n);case"ascii":return S(this,e,t,n);case"binary":return C(this,e,t,n);case"base64":return L(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return T(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},a.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var Q=4096;a.prototype.slice=function(e,t){var n=this.length;e=~~e,t=void 0===t?n:~~t,0>e?(e+=n,0>e&&(e=0)):e>n&&(e=n),0>t?(t+=n,0>t&&(t=0)):t>n&&(t=n),e>t&&(t=e);var r;if(a.TYPED_ARRAY_SUPPORT)r=this.subarray(e,t),r.__proto__=a.prototype;else{var i=t-e;r=new a(i,void 0);for(var o=0;i>o;o++)r[o]=this[o+e]}return r},a.prototype.readUIntLE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=this[e],i=1,o=0;++o<t&&(i*=256);)r+=this[e+o]*i;return r},a.prototype.readUIntBE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=this[e+--t],i=1;t>0&&(i*=256);)r+=this[e+--t]*i;return r},a.prototype.readUInt8=function(e,t){return t||R(e,1,this.length),this[e]},a.prototype.readUInt16LE=function(e,t){return t||R(e,2,this.length),this[e]|this[e+1]<<8},a.prototype.readUInt16BE=function(e,t){return t||R(e,2,this.length),this[e]<<8|this[e+1]},a.prototype.readUInt32LE=function(e,t){return t||R(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},a.prototype.readUInt32BE=function(e,t){return t||R(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},a.prototype.readIntLE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=this[e],i=1,o=0;++o<t&&(i*=256);)r+=this[e+o]*i;return i*=128,r>=i&&(r-=Math.pow(2,8*t)),r},a.prototype.readIntBE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=t,i=1,o=this[e+--r];r>0&&(i*=256);)o+=this[e+--r]*i;return i*=128,o>=i&&(o-=Math.pow(2,8*t)),o},a.prototype.readInt8=function(e,t){return t||R(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},a.prototype.readInt16LE=function(e,t){t||R(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},a.prototype.readInt16BE=function(e,t){t||R(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},a.prototype.readInt32LE=function(e,t){return t||R(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},a.prototype.readInt32BE=function(e,t){return t||R(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},a.prototype.readFloatLE=function(e,t){return t||R(e,4,this.length),Z.read(this,e,!0,23,4)},a.prototype.readFloatBE=function(e,t){return t||R(e,4,this.length),Z.read(this,e,!1,23,4)},a.prototype.readDoubleLE=function(e,t){return t||R(e,8,this.length),Z.read(this,e,!0,52,8)},a.prototype.readDoubleBE=function(e,t){return t||R(e,8,this.length),Z.read(this,e,!1,52,8)},a.prototype.writeUIntLE=function(e,t,n,r){if(e=+e,t=0|t,n=0|n,!r){var i=Math.pow(2,8*n)-1;D(this,e,t,n,i,0)}var o=1,a=0;for(this[t]=255&e;++a<n&&(o*=256);)this[t+a]=e/o&255;return t+n},a.prototype.writeUIntBE=function(e,t,n,r){if(e=+e,t=0|t,n=0|n,!r){var i=Math.pow(2,8*n)-1;D(this,e,t,n,i,0)}var o=n-1,a=1;for(this[t+o]=255&e;--o>=0&&(a*=256);)this[t+o]=e/a&255;return t+n},a.prototype.writeUInt8=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,1,255,0),a.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},a.prototype.writeUInt16LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,65535,0),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):H(this,e,t,!0),t+2},a.prototype.writeUInt16BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,65535,0),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):H(this,e,t,!1),t+2},a.prototype.writeUInt32LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,4294967295,0),a.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):W(this,e,t,!0),t+4},a.prototype.writeUInt32BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,4294967295,0),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):W(this,e,t,!1),t+4},a.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t=0|t,!r){var i=Math.pow(2,8*n-1);D(this,e,t,n,i-1,-i)}var o=0,a=1,l=0;for(this[t]=255&e;++o<n&&(a*=256);)0>e&&0===l&&0!==this[t+o-1]&&(l=1),this[t+o]=(e/a>>0)-l&255;return t+n},a.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t=0|t,!r){var i=Math.pow(2,8*n-1);D(this,e,t,n,i-1,-i)}var o=n-1,a=1,l=0;for(this[t+o]=255&e;--o>=0&&(a*=256);)0>e&&0===l&&0!==this[t+o+1]&&(l=1),this[t+o]=(e/a>>0)-l&255;return t+n},a.prototype.writeInt8=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,1,127,-128),a.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),0>e&&(e=255+e+1),this[t]=255&e,t+1},a.prototype.writeInt16LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,32767,-32768),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):H(this,e,t,!0),t+2},a.prototype.writeInt16BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,32767,-32768),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):H(this,e,t,!1),t+2},a.prototype.writeInt32LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,2147483647,-2147483648),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):W(this,e,t,!0),t+4},a.prototype.writeInt32BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,2147483647,-2147483648),0>e&&(e=4294967295+e+1),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):W(this,e,t,!1),t+4},a.prototype.writeFloatLE=function(e,t,n){return _(this,e,t,!0,n)},a.prototype.writeFloatBE=function(e,t,n){return _(this,e,t,!1,n)},a.prototype.writeDoubleLE=function(e,t,n){return F(this,e,t,!0,n)},a.prototype.writeDoubleBE=function(e,t,n){return F(this,e,t,!1,n)},a.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&n>r&&(r=n),r===n)return 0;if(0===e.length||0===this.length)return 0;if(0>t)throw new RangeError("targetStart out of bounds");if(0>n||n>=this.length)throw new RangeError("sourceStart out of bounds");if(0>r)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t<r-n&&(r=e.length-t+n);var i,o=r-n;if(this===e&&t>n&&r>t)for(i=o-1;i>=0;i--)e[i+t]=this[i+n];else if(1e3>o||!a.TYPED_ARRAY_SUPPORT)for(i=0;o>i;i++)e[i+t]=this[i+n];else Uint8Array.prototype.set.call(e,this.subarray(n,n+o),t);return o},a.prototype.fill=function(e,t,n,r){if("string"==typeof e){if("string"==typeof t?(r=t,t=0,n=this.length):"string"==typeof n&&(r=n,n=this.length),1===e.length){var i=e.charCodeAt(0);256>i&&(e=i)}if(void 0!==r&&"string"!=typeof r)throw new TypeError("encoding must be a string");if("string"==typeof r&&!a.isEncoding(r))throw new TypeError("Unknown encoding: "+r)}else"number"==typeof e&&(e=255&e);if(0>t||this.length<t||this.length<n)throw new RangeError("Out of range index");if(t>=n)return this;t>>>=0,n=void 0===n?this.length:n>>>0,e||(e=0);var o;if("number"==typeof e)for(o=t;n>o;o++)this[o]=e;else{var l=a.isBuffer(e)?e:q(new a(e,r).toString()),s=l.length;for(o=0;n-t>o;o++)this[o+t]=l[o%s]}return this};var ee=/[^+\/0-9A-Za-z-_]/g}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"base64-js":1,ieee754:15,isarray:16}],4:[function(e,t,n){"use strict";function r(e){return e=e||{},"function"!=typeof e.codeMirrorInstance||"function"!=typeof e.codeMirrorInstance.defineMode?void console.log("CodeMirror Spell Checker: You must provide an instance of CodeMirror via the option `codeMirrorInstance`"):(String.prototype.includes||(String.prototype.includes=function(){return-1!==String.prototype.indexOf.apply(this,arguments)}),void e.codeMirrorInstance.defineMode("spell-checker",function(t){if(!r.aff_loading){r.aff_loading=!0;var n=new XMLHttpRequest;n.open("GET","https://cdn.jsdelivr.net/codemirror.spell-checker/latest/en_US.aff",!0),n.onload=function(){4===n.readyState&&200===n.status&&(r.aff_data=n.responseText,r.num_loaded++,2==r.num_loaded&&(r.typo=new i("en_US",r.aff_data,r.dic_data,{platform:"any"})))},n.send(null)}if(!r.dic_loading){r.dic_loading=!0;var o=new XMLHttpRequest;o.open("GET","https://cdn.jsdelivr.net/codemirror.spell-checker/latest/en_US.dic",!0),o.onload=function(){4===o.readyState&&200===o.status&&(r.dic_data=o.responseText,r.num_loaded++,2==r.num_loaded&&(r.typo=new i("en_US",r.aff_data,r.dic_data,{platform:"any"})))},o.send(null)}var a='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~ ',l={token:function(e){var t=e.peek(),n="";if(a.includes(t))return e.next(),null;for(;null!=(t=e.peek())&&!a.includes(t);)n+=t,e.next();return r.typo&&!r.typo.check(n)?"spell-error":null}},s=e.codeMirrorInstance.getMode(t,t.backdrop||"text/plain");return e.codeMirrorInstance.overlayMode(s,l,!0)}))}var i=e("typo-js");r.num_loaded=0,r.aff_loading=!1,r.dic_loading=!1,r.aff_data="",r.dic_data="",r.typo,t.exports=r},{"typo-js":18}],5:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";function t(e){var t=e.getWrapperElement();e.state.fullScreenRestore={scrollTop:window.pageYOffset,scrollLeft:window.pageXOffset,width:t.style.width,height:t.style.height},t.style.width="",t.style.height="auto",t.className+=" CodeMirror-fullscreen",document.documentElement.style.overflow="hidden",e.refresh()}function n(e){var t=e.getWrapperElement();t.className=t.className.replace(/\s*CodeMirror-fullscreen\b/,""),document.documentElement.style.overflow="";var n=e.state.fullScreenRestore;t.style.width=n.width,t.style.height=n.height,window.scrollTo(n.scrollLeft,n.scrollTop),e.refresh()}e.defineOption("fullScreen",!1,function(r,i,o){o==e.Init&&(o=!1),!o!=!i&&(i?t(r):n(r))})})},{"../../lib/codemirror":10}],6:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){function t(e){e.state.placeholder&&(e.state.placeholder.parentNode.removeChild(e.state.placeholder),e.state.placeholder=null)}function n(e){t(e);var n=e.state.placeholder=document.createElement("pre");n.style.cssText="height: 0; overflow: visible",n.className="CodeMirror-placeholder";var r=e.getOption("placeholder");"string"==typeof r&&(r=document.createTextNode(r)),n.appendChild(r),e.display.lineSpace.insertBefore(n,e.display.lineSpace.firstChild)}function r(e){o(e)&&n(e)}function i(e){var r=e.getWrapperElement(),i=o(e);r.className=r.className.replace(" CodeMirror-empty","")+(i?" CodeMirror-empty":""),i?n(e):t(e)}function o(e){return 1===e.lineCount()&&""===e.getLine(0)}e.defineOption("placeholder","",function(n,o,a){var l=a&&a!=e.Init;if(o&&!l)n.on("blur",r),n.on("change",i),n.on("swapDoc",i),i(n);else if(!o&&l){n.off("blur",r),n.off("change",i),n.off("swapDoc",i),t(n);var s=n.getWrapperElement();s.className=s.className.replace(" CodeMirror-empty","")}o&&!n.hasFocus()&&r(n)})})},{"../../lib/codemirror":10}],7:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";var t=/^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))(\s*)/,n=/^(\s*)(>[> ]*|[*+-]|(\d+)[.)])(\s*)$/,r=/[*+-]\s/;e.commands.newlineAndIndentContinueMarkdownList=function(i){if(i.getOption("disableInput"))return e.Pass;for(var o=i.listSelections(),a=[],l=0;l<o.length;l++){var s=o[l].head,c=i.getStateAfter(s.line),u=c.list!==!1,f=0!==c.quote,h=i.getLine(s.line),d=t.exec(h);if(!o[l].empty()||!u&&!f||!d)return void i.execCommand("newlineAndIndent");if(n.test(h))i.replaceRange("",{line:s.line,ch:0},{line:s.line,ch:s.ch+1}),a[l]="\n";else{var p=d[1],m=d[5],g=r.test(d[2])||d[2].indexOf(">")>=0?d[2]:parseInt(d[3],10)+1+d[4];a[l]="\n"+p+g+m}}i.replaceSelections(a)}})},{"../../lib/codemirror":10}],8:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";e.overlayMode=function(t,n,r){return{startState:function(){return{base:e.startState(t),overlay:e.startState(n),basePos:0,baseCur:null,overlayPos:0,overlayCur:null,streamSeen:null}},copyState:function(r){return{base:e.copyState(t,r.base),overlay:e.copyState(n,r.overlay),basePos:r.basePos,baseCur:null,overlayPos:r.overlayPos,overlayCur:null}},token:function(e,i){return(e!=i.streamSeen||Math.min(i.basePos,i.overlayPos)<e.start)&&(i.streamSeen=e,i.basePos=i.overlayPos=e.start),e.start==i.basePos&&(i.baseCur=t.token(e,i.base),i.basePos=e.pos),e.start==i.overlayPos&&(e.pos=e.start,i.overlayCur=n.token(e,i.overlay),i.overlayPos=e.pos),e.pos=Math.min(i.basePos,i.overlayPos),null==i.overlayCur?i.baseCur:null!=i.baseCur&&i.overlay.combineTokens||r&&null==i.overlay.combineTokens?i.baseCur+" "+i.overlayCur:i.overlayCur},indent:t.indent&&function(e,n){return t.indent(e.base,n)},electricChars:t.electricChars,innerMode:function(e){return{state:e.base,mode:t}},blankLine:function(e){t.blankLine&&t.blankLine(e.base),n.blankLine&&n.blankLine(e.overlay)}}}})},{"../../lib/codemirror":10}],9:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";function t(e){e.operation(function(){a(e)})}function n(e){e.state.markedSelection.length&&e.operation(function(){i(e)})}function r(e,t,n,r){if(0!=c(t,n))for(var i=e.state.markedSelection,o=e.state.markedSelectionStyle,a=t.line;;){var u=a==t.line?t:s(a,0),f=a+l,h=f>=n.line,d=h?n:s(f,0),p=e.markText(u,d,{className:o});if(null==r?i.push(p):i.splice(r++,0,p),h)break;a=f}}function i(e){for(var t=e.state.markedSelection,n=0;n<t.length;++n)t[n].clear();t.length=0}function o(e){i(e);for(var t=e.listSelections(),n=0;n<t.length;n++)r(e,t[n].from(),t[n].to())}function a(e){if(!e.somethingSelected())return i(e);if(e.listSelections().length>1)return o(e);var t=e.getCursor("start"),n=e.getCursor("end"),a=e.state.markedSelection;if(!a.length)return r(e,t,n);var s=a[0].find(),u=a[a.length-1].find();if(!s||!u||n.line-t.line<l||c(t,u.to)>=0||c(n,s.from)<=0)return o(e);for(;c(t,s.from)>0;)a.shift().clear(),s=a[0].find();for(c(t,s.from)<0&&(s.to.line-t.line<l?(a.shift().clear(),r(e,t,s.to,0)):r(e,t,s.from,0));c(n,u.to)<0;)a.pop().clear(),u=a[a.length-1].find();c(n,u.to)>0&&(n.line-u.from.line<l?(a.pop().clear(),r(e,u.from,n)):r(e,u.to,n))}e.defineOption("styleSelectedText",!1,function(r,a,l){var s=l&&l!=e.Init;a&&!s?(r.state.markedSelection=[],r.state.markedSelectionStyle="string"==typeof a?a:"CodeMirror-selectedtext",o(r),r.on("cursorActivity",t),r.on("change",n)):!a&&s&&(r.off("cursorActivity",t),r.off("change",n),i(r),r.state.markedSelection=r.state.markedSelectionStyle=null)});var l=8,s=e.Pos,c=e.cmpPos})},{"../../lib/codemirror":10}],10:[function(t,n,r){!function(t){if("object"==typeof r&&"object"==typeof n)n.exports=t();else{if("function"==typeof e&&e.amd)return e([],t);(this||window).CodeMirror=t()}}(function(){"use strict";function e(n,r){if(!(this instanceof e))return new e(n,r);this.options=r=r?Wi(r):{},Wi(ea,r,!1),d(r);var i=r.value;"string"==typeof i&&(i=new Ca(i,r.mode,null,r.lineSeparator)),this.doc=i;var o=new e.inputStyles[r.inputStyle](this),a=this.display=new t(n,i,o);a.wrapper.CodeMirror=this,c(this),l(this),r.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),r.autofocus&&!Ao&&a.input.focus(),v(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:!1,cutIncoming:!1,selectingText:!1,draggingText:!1,highlight:new Ei,keySeq:null,specialChars:null};var s=this;xo&&11>bo&&setTimeout(function(){s.display.input.reset(!0)},20),jt(this),Ki(),bt(this),this.curOp.forceUpdate=!0,Xr(this,i),r.autofocus&&!Ao||s.hasFocus()?setTimeout(Bi(vn,this),20):yn(this);for(var u in ta)ta.hasOwnProperty(u)&&ta[u](this,r[u],na);k(this),r.finishInit&&r.finishInit(this);for(var f=0;f<aa.length;++f)aa[f](this);kt(this),wo&&r.lineWrapping&&"optimizelegibility"==getComputedStyle(a.lineDiv).textRendering&&(a.lineDiv.style.textRendering="auto")}function t(e,t,n){var r=this;this.input=n,r.scrollbarFiller=ji("div",null,"CodeMirror-scrollbar-filler"),r.scrollbarFiller.setAttribute("cm-not-content","true"),r.gutterFiller=ji("div",null,"CodeMirror-gutter-filler"),r.gutterFiller.setAttribute("cm-not-content","true"),r.lineDiv=ji("div",null,"CodeMirror-code"),r.selectionDiv=ji("div",null,null,"position: relative; z-index: 1"),r.cursorDiv=ji("div",null,"CodeMirror-cursors"),r.measure=ji("div",null,"CodeMirror-measure"),r.lineMeasure=ji("div",null,"CodeMirror-measure"),r.lineSpace=ji("div",[r.measure,r.lineMeasure,r.selectionDiv,r.cursorDiv,r.lineDiv],null,"position: relative; outline: none"),r.mover=ji("div",[ji("div",[r.lineSpace],"CodeMirror-lines")],null,"position: relative"),r.sizer=ji("div",[r.mover],"CodeMirror-sizer"),r.sizerWidth=null,r.heightForcer=ji("div",null,null,"position: absolute; height: "+Da+"px; width: 1px;"),r.gutters=ji("div",null,"CodeMirror-gutters"),r.lineGutter=null,r.scroller=ji("div",[r.sizer,r.heightForcer,r.gutters],"CodeMirror-scroll"),r.scroller.setAttribute("tabIndex","-1"),r.wrapper=ji("div",[r.scrollbarFiller,r.gutterFiller,r.scroller],"CodeMirror"),xo&&8>bo&&(r.gutters.style.zIndex=-1,r.scroller.style.paddingRight=0),wo||go&&Ao||(r.scroller.draggable=!0),e&&(e.appendChild?e.appendChild(r.wrapper):e(r.wrapper)),r.viewFrom=r.viewTo=t.first,r.reportedViewFrom=r.reportedViewTo=t.first,r.view=[],r.renderedView=null,r.externalMeasured=null,r.viewOffset=0,r.lastWrapHeight=r.lastWrapWidth=0,r.updateLineNumbers=null,r.nativeBarWidth=r.barHeight=r.barWidth=0,r.scrollbarsClipped=!1,r.lineNumWidth=r.lineNumInnerWidth=r.lineNumChars=null,r.alignWidgets=!1,r.cachedCharWidth=r.cachedTextHeight=r.cachedPaddingH=null, -r.maxLine=null,r.maxLineLength=0,r.maxLineChanged=!1,r.wheelDX=r.wheelDY=r.wheelStartX=r.wheelStartY=null,r.shift=!1,r.selForContextMenu=null,r.activeTouch=null,n.init(r)}function n(t){t.doc.mode=e.getMode(t.options,t.doc.modeOption),r(t)}function r(e){e.doc.iter(function(e){e.stateAfter&&(e.stateAfter=null),e.styles&&(e.styles=null)}),e.doc.frontier=e.doc.first,_e(e,100),e.state.modeGen++,e.curOp&&Dt(e)}function i(e){e.options.lineWrapping?(Ja(e.display.wrapper,"CodeMirror-wrap"),e.display.sizer.style.minWidth="",e.display.sizerWidth=null):(Za(e.display.wrapper,"CodeMirror-wrap"),h(e)),a(e),Dt(e),lt(e),setTimeout(function(){y(e)},100)}function o(e){var t=yt(e.display),n=e.options.lineWrapping,r=n&&Math.max(5,e.display.scroller.clientWidth/xt(e.display)-3);return function(i){if(kr(e.doc,i))return 0;var o=0;if(i.widgets)for(var a=0;a<i.widgets.length;a++)i.widgets[a].height&&(o+=i.widgets[a].height);return n?o+(Math.ceil(i.text.length/r)||1)*t:o+t}}function a(e){var t=e.doc,n=o(e);t.iter(function(e){var t=n(e);t!=e.height&&ei(e,t)})}function l(e){e.display.wrapper.className=e.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+e.options.theme.replace(/(^|\s)\s*/g," cm-s-"),lt(e)}function s(e){c(e),Dt(e),setTimeout(function(){w(e)},20)}function c(e){var t=e.display.gutters,n=e.options.gutters;Ui(t);for(var r=0;r<n.length;++r){var i=n[r],o=t.appendChild(ji("div",null,"CodeMirror-gutter "+i));"CodeMirror-linenumbers"==i&&(e.display.lineGutter=o,o.style.width=(e.display.lineNumWidth||1)+"px")}t.style.display=r?"":"none",u(e)}function u(e){var t=e.display.gutters.offsetWidth;e.display.sizer.style.marginLeft=t+"px"}function f(e){if(0==e.height)return 0;for(var t,n=e.text.length,r=e;t=mr(r);){var i=t.find(0,!0);r=i.from.line,n+=i.from.ch-i.to.ch}for(r=e;t=gr(r);){var i=t.find(0,!0);n-=r.text.length-i.from.ch,r=i.to.line,n+=r.text.length-i.to.ch}return n}function h(e){var t=e.display,n=e.doc;t.maxLine=Zr(n,n.first),t.maxLineLength=f(t.maxLine),t.maxLineChanged=!0,n.iter(function(e){var n=f(e);n>t.maxLineLength&&(t.maxLineLength=n,t.maxLine=e)})}function d(e){var t=Pi(e.gutters,"CodeMirror-linenumbers");-1==t&&e.lineNumbers?e.gutters=e.gutters.concat(["CodeMirror-linenumbers"]):t>-1&&!e.lineNumbers&&(e.gutters=e.gutters.slice(0),e.gutters.splice(t,1))}function p(e){var t=e.display,n=t.gutters.offsetWidth,r=Math.round(e.doc.height+qe(e.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:e.options.fixedGutter?n:0,docHeight:r,scrollHeight:r+Ye(e)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:n}}function m(e,t,n){this.cm=n;var r=this.vert=ji("div",[ji("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),i=this.horiz=ji("div",[ji("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");e(r),e(i),Ea(r,"scroll",function(){r.clientHeight&&t(r.scrollTop,"vertical")}),Ea(i,"scroll",function(){i.clientWidth&&t(i.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,xo&&8>bo&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")}function g(){}function v(t){t.display.scrollbars&&(t.display.scrollbars.clear(),t.display.scrollbars.addClass&&Za(t.display.wrapper,t.display.scrollbars.addClass)),t.display.scrollbars=new e.scrollbarModel[t.options.scrollbarStyle](function(e){t.display.wrapper.insertBefore(e,t.display.scrollbarFiller),Ea(e,"mousedown",function(){t.state.focused&&setTimeout(function(){t.display.input.focus()},0)}),e.setAttribute("cm-not-content","true")},function(e,n){"horizontal"==n?on(t,e):rn(t,e)},t),t.display.scrollbars.addClass&&Ja(t.display.wrapper,t.display.scrollbars.addClass)}function y(e,t){t||(t=p(e));var n=e.display.barWidth,r=e.display.barHeight;x(e,t);for(var i=0;4>i&&n!=e.display.barWidth||r!=e.display.barHeight;i++)n!=e.display.barWidth&&e.options.lineWrapping&&O(e),x(e,p(e)),n=e.display.barWidth,r=e.display.barHeight}function x(e,t){var n=e.display,r=n.scrollbars.update(t);n.sizer.style.paddingRight=(n.barWidth=r.right)+"px",n.sizer.style.paddingBottom=(n.barHeight=r.bottom)+"px",n.heightForcer.style.borderBottom=r.bottom+"px solid transparent",r.right&&r.bottom?(n.scrollbarFiller.style.display="block",n.scrollbarFiller.style.height=r.bottom+"px",n.scrollbarFiller.style.width=r.right+"px"):n.scrollbarFiller.style.display="",r.bottom&&e.options.coverGutterNextToScrollbar&&e.options.fixedGutter?(n.gutterFiller.style.display="block",n.gutterFiller.style.height=r.bottom+"px",n.gutterFiller.style.width=t.gutterWidth+"px"):n.gutterFiller.style.display=""}function b(e,t,n){var r=n&&null!=n.top?Math.max(0,n.top):e.scroller.scrollTop;r=Math.floor(r-Ue(e));var i=n&&null!=n.bottom?n.bottom:r+e.wrapper.clientHeight,o=ni(t,r),a=ni(t,i);if(n&&n.ensure){var l=n.ensure.from.line,s=n.ensure.to.line;o>l?(o=l,a=ni(t,ri(Zr(t,l))+e.wrapper.clientHeight)):Math.min(s,t.lastLine())>=a&&(o=ni(t,ri(Zr(t,s))-e.wrapper.clientHeight),a=s)}return{from:o,to:Math.max(a,o+1)}}function w(e){var t=e.display,n=t.view;if(t.alignWidgets||t.gutters.firstChild&&e.options.fixedGutter){for(var r=C(t)-t.scroller.scrollLeft+e.doc.scrollLeft,i=t.gutters.offsetWidth,o=r+"px",a=0;a<n.length;a++)if(!n[a].hidden){e.options.fixedGutter&&n[a].gutter&&(n[a].gutter.style.left=o);var l=n[a].alignable;if(l)for(var s=0;s<l.length;s++)l[s].style.left=o}e.options.fixedGutter&&(t.gutters.style.left=r+i+"px")}}function k(e){if(!e.options.lineNumbers)return!1;var t=e.doc,n=S(e.options,t.first+t.size-1),r=e.display;if(n.length!=r.lineNumChars){var i=r.measure.appendChild(ji("div",[ji("div",n)],"CodeMirror-linenumber CodeMirror-gutter-elt")),o=i.firstChild.offsetWidth,a=i.offsetWidth-o;return r.lineGutter.style.width="",r.lineNumInnerWidth=Math.max(o,r.lineGutter.offsetWidth-a)+1,r.lineNumWidth=r.lineNumInnerWidth+a,r.lineNumChars=r.lineNumInnerWidth?n.length:-1,r.lineGutter.style.width=r.lineNumWidth+"px",u(e),!0}return!1}function S(e,t){return String(e.lineNumberFormatter(t+e.firstLineNumber))}function C(e){return e.scroller.getBoundingClientRect().left-e.sizer.getBoundingClientRect().left}function L(e,t,n){var r=e.display;this.viewport=t,this.visible=b(r,e.doc,t),this.editorIsHidden=!r.wrapper.offsetWidth,this.wrapperHeight=r.wrapper.clientHeight,this.wrapperWidth=r.wrapper.clientWidth,this.oldDisplayWidth=$e(e),this.force=n,this.dims=P(e),this.events=[]}function T(e){var t=e.display;!t.scrollbarsClipped&&t.scroller.offsetWidth&&(t.nativeBarWidth=t.scroller.offsetWidth-t.scroller.clientWidth,t.heightForcer.style.height=Ye(e)+"px",t.sizer.style.marginBottom=-t.nativeBarWidth+"px",t.sizer.style.borderRightWidth=Ye(e)+"px",t.scrollbarsClipped=!0)}function M(e,t){var n=e.display,r=e.doc;if(t.editorIsHidden)return Wt(e),!1;if(!t.force&&t.visible.from>=n.viewFrom&&t.visible.to<=n.viewTo&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo)&&n.renderedView==n.view&&0==zt(e))return!1;k(e)&&(Wt(e),t.dims=P(e));var i=r.first+r.size,o=Math.max(t.visible.from-e.options.viewportMargin,r.first),a=Math.min(i,t.visible.to+e.options.viewportMargin);n.viewFrom<o&&o-n.viewFrom<20&&(o=Math.max(r.first,n.viewFrom)),n.viewTo>a&&n.viewTo-a<20&&(a=Math.min(i,n.viewTo)),Wo&&(o=br(e.doc,o),a=wr(e.doc,a));var l=o!=n.viewFrom||a!=n.viewTo||n.lastWrapHeight!=t.wrapperHeight||n.lastWrapWidth!=t.wrapperWidth;Ft(e,o,a),n.viewOffset=ri(Zr(e.doc,n.viewFrom)),e.display.mover.style.top=n.viewOffset+"px";var s=zt(e);if(!l&&0==s&&!t.force&&n.renderedView==n.view&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo))return!1;var c=Gi();return s>4&&(n.lineDiv.style.display="none"),R(e,n.updateLineNumbers,t.dims),s>4&&(n.lineDiv.style.display=""),n.renderedView=n.view,c&&Gi()!=c&&c.offsetHeight&&c.focus(),Ui(n.cursorDiv),Ui(n.selectionDiv),n.gutters.style.height=n.sizer.style.minHeight=0,l&&(n.lastWrapHeight=t.wrapperHeight,n.lastWrapWidth=t.wrapperWidth,_e(e,400)),n.updateLineNumbers=null,!0}function N(e,t){for(var n=t.viewport,r=!0;(r&&e.options.lineWrapping&&t.oldDisplayWidth!=$e(e)||(n&&null!=n.top&&(n={top:Math.min(e.doc.height+qe(e.display)-Ve(e),n.top)}),t.visible=b(e.display,e.doc,n),!(t.visible.from>=e.display.viewFrom&&t.visible.to<=e.display.viewTo)))&&M(e,t);r=!1){O(e);var i=p(e);Re(e),y(e,i),E(e,i)}t.signal(e,"update",e),e.display.viewFrom==e.display.reportedViewFrom&&e.display.viewTo==e.display.reportedViewTo||(t.signal(e,"viewportChange",e,e.display.viewFrom,e.display.viewTo),e.display.reportedViewFrom=e.display.viewFrom,e.display.reportedViewTo=e.display.viewTo)}function A(e,t){var n=new L(e,t);if(M(e,n)){O(e),N(e,n);var r=p(e);Re(e),y(e,r),E(e,r),n.finish()}}function E(e,t){e.display.sizer.style.minHeight=t.docHeight+"px",e.display.heightForcer.style.top=t.docHeight+"px",e.display.gutters.style.height=t.docHeight+e.display.barHeight+Ye(e)+"px"}function O(e){for(var t=e.display,n=t.lineDiv.offsetTop,r=0;r<t.view.length;r++){var i,o=t.view[r];if(!o.hidden){if(xo&&8>bo){var a=o.node.offsetTop+o.node.offsetHeight;i=a-n,n=a}else{var l=o.node.getBoundingClientRect();i=l.bottom-l.top}var s=o.line.height-i;if(2>i&&(i=yt(t)),(s>.001||-.001>s)&&(ei(o.line,i),I(o.line),o.rest))for(var c=0;c<o.rest.length;c++)I(o.rest[c])}}}function I(e){if(e.widgets)for(var t=0;t<e.widgets.length;++t)e.widgets[t].height=e.widgets[t].node.parentNode.offsetHeight}function P(e){for(var t=e.display,n={},r={},i=t.gutters.clientLeft,o=t.gutters.firstChild,a=0;o;o=o.nextSibling,++a)n[e.options.gutters[a]]=o.offsetLeft+o.clientLeft+i,r[e.options.gutters[a]]=o.clientWidth;return{fixedPos:C(t),gutterTotalWidth:t.gutters.offsetWidth,gutterLeft:n,gutterWidth:r,wrapperWidth:t.wrapper.clientWidth}}function R(e,t,n){function r(t){var n=t.nextSibling;return wo&&Eo&&e.display.currentWheelTarget==t?t.style.display="none":t.parentNode.removeChild(t),n}for(var i=e.display,o=e.options.lineNumbers,a=i.lineDiv,l=a.firstChild,s=i.view,c=i.viewFrom,u=0;u<s.length;u++){var f=s[u];if(f.hidden);else if(f.node&&f.node.parentNode==a){for(;l!=f.node;)l=r(l);var h=o&&null!=t&&c>=t&&f.lineNumber;f.changes&&(Pi(f.changes,"gutter")>-1&&(h=!1),D(e,f,c,n)),h&&(Ui(f.lineNumber),f.lineNumber.appendChild(document.createTextNode(S(e.options,c)))),l=f.node.nextSibling}else{var d=U(e,f,c,n);a.insertBefore(d,l)}c+=f.size}for(;l;)l=r(l)}function D(e,t,n,r){for(var i=0;i<t.changes.length;i++){var o=t.changes[i];"text"==o?_(e,t):"gutter"==o?z(e,t,n,r):"class"==o?F(t):"widget"==o&&j(e,t,r)}t.changes=null}function H(e){return e.node==e.text&&(e.node=ji("div",null,null,"position: relative"),e.text.parentNode&&e.text.parentNode.replaceChild(e.node,e.text),e.node.appendChild(e.text),xo&&8>bo&&(e.node.style.zIndex=2)),e.node}function W(e){var t=e.bgClass?e.bgClass+" "+(e.line.bgClass||""):e.line.bgClass;if(t&&(t+=" CodeMirror-linebackground"),e.background)t?e.background.className=t:(e.background.parentNode.removeChild(e.background),e.background=null);else if(t){var n=H(e);e.background=n.insertBefore(ji("div",null,t),n.firstChild)}}function B(e,t){var n=e.display.externalMeasured;return n&&n.line==t.line?(e.display.externalMeasured=null,t.measure=n.measure,n.built):Br(e,t)}function _(e,t){var n=t.text.className,r=B(e,t);t.text==t.node&&(t.node=r.pre),t.text.parentNode.replaceChild(r.pre,t.text),t.text=r.pre,r.bgClass!=t.bgClass||r.textClass!=t.textClass?(t.bgClass=r.bgClass,t.textClass=r.textClass,F(t)):n&&(t.text.className=n)}function F(e){W(e),e.line.wrapClass?H(e).className=e.line.wrapClass:e.node!=e.text&&(e.node.className="");var t=e.textClass?e.textClass+" "+(e.line.textClass||""):e.line.textClass;e.text.className=t||""}function z(e,t,n,r){if(t.gutter&&(t.node.removeChild(t.gutter),t.gutter=null),t.gutterBackground&&(t.node.removeChild(t.gutterBackground),t.gutterBackground=null),t.line.gutterClass){var i=H(t);t.gutterBackground=ji("div",null,"CodeMirror-gutter-background "+t.line.gutterClass,"left: "+(e.options.fixedGutter?r.fixedPos:-r.gutterTotalWidth)+"px; width: "+r.gutterTotalWidth+"px"),i.insertBefore(t.gutterBackground,t.text)}var o=t.line.gutterMarkers;if(e.options.lineNumbers||o){var i=H(t),a=t.gutter=ji("div",null,"CodeMirror-gutter-wrapper","left: "+(e.options.fixedGutter?r.fixedPos:-r.gutterTotalWidth)+"px");if(e.display.input.setUneditable(a),i.insertBefore(a,t.text),t.line.gutterClass&&(a.className+=" "+t.line.gutterClass),!e.options.lineNumbers||o&&o["CodeMirror-linenumbers"]||(t.lineNumber=a.appendChild(ji("div",S(e.options,n),"CodeMirror-linenumber CodeMirror-gutter-elt","left: "+r.gutterLeft["CodeMirror-linenumbers"]+"px; width: "+e.display.lineNumInnerWidth+"px"))),o)for(var l=0;l<e.options.gutters.length;++l){var s=e.options.gutters[l],c=o.hasOwnProperty(s)&&o[s];c&&a.appendChild(ji("div",[c],"CodeMirror-gutter-elt","left: "+r.gutterLeft[s]+"px; width: "+r.gutterWidth[s]+"px"))}}}function j(e,t,n){t.alignable&&(t.alignable=null);for(var r,i=t.node.firstChild;i;i=r){var r=i.nextSibling;"CodeMirror-linewidget"==i.className&&t.node.removeChild(i)}q(e,t,n)}function U(e,t,n,r){var i=B(e,t);return t.text=t.node=i.pre,i.bgClass&&(t.bgClass=i.bgClass),i.textClass&&(t.textClass=i.textClass),F(t),z(e,t,n,r),q(e,t,r),t.node}function q(e,t,n){if(G(e,t.line,t,n,!0),t.rest)for(var r=0;r<t.rest.length;r++)G(e,t.rest[r],t,n,!1)}function G(e,t,n,r,i){if(t.widgets)for(var o=H(n),a=0,l=t.widgets;a<l.length;++a){var s=l[a],c=ji("div",[s.node],"CodeMirror-linewidget");s.handleMouseEvents||c.setAttribute("cm-ignore-events","true"),Y(s,c,n,r),e.display.input.setUneditable(c),i&&s.above?o.insertBefore(c,n.gutter||n.text):o.appendChild(c),Ci(s,"redraw")}}function Y(e,t,n,r){if(e.noHScroll){(n.alignable||(n.alignable=[])).push(t);var i=r.wrapperWidth;t.style.left=r.fixedPos+"px",e.coverGutter||(i-=r.gutterTotalWidth,t.style.paddingLeft=r.gutterTotalWidth+"px"),t.style.width=i+"px"}e.coverGutter&&(t.style.zIndex=5,t.style.position="relative",e.noHScroll||(t.style.marginLeft=-r.gutterTotalWidth+"px"))}function $(e){return Bo(e.line,e.ch)}function V(e,t){return _o(e,t)<0?t:e}function K(e,t){return _o(e,t)<0?e:t}function X(e){e.state.focused||(e.display.input.focus(),vn(e))}function Z(e,t,n,r,i){var o=e.doc;e.display.shift=!1,r||(r=o.sel);var a=e.state.pasteIncoming||"paste"==i,l=o.splitLines(t),s=null;if(a&&r.ranges.length>1)if(Fo&&Fo.text.join("\n")==t){if(r.ranges.length%Fo.text.length==0){s=[];for(var c=0;c<Fo.text.length;c++)s.push(o.splitLines(Fo.text[c]))}}else l.length==r.ranges.length&&(s=Ri(l,function(e){return[e]}));for(var c=r.ranges.length-1;c>=0;c--){var u=r.ranges[c],f=u.from(),h=u.to();u.empty()&&(n&&n>0?f=Bo(f.line,f.ch-n):e.state.overwrite&&!a?h=Bo(h.line,Math.min(Zr(o,h.line).text.length,h.ch+Ii(l).length)):Fo&&Fo.lineWise&&Fo.text.join("\n")==t&&(f=h=Bo(f.line,0)));var d=e.curOp.updateInput,p={from:f,to:h,text:s?s[c%s.length]:l,origin:i||(a?"paste":e.state.cutIncoming?"cut":"+input")};Tn(e.doc,p),Ci(e,"inputRead",e,p)}t&&!a&&Q(e,t),Bn(e),e.curOp.updateInput=d,e.curOp.typing=!0,e.state.pasteIncoming=e.state.cutIncoming=!1}function J(e,t){var n=e.clipboardData&&e.clipboardData.getData("text/plain");return n?(e.preventDefault(),t.isReadOnly()||t.options.disableInput||At(t,function(){Z(t,n,0,null,"paste")}),!0):void 0}function Q(e,t){if(e.options.electricChars&&e.options.smartIndent)for(var n=e.doc.sel,r=n.ranges.length-1;r>=0;r--){var i=n.ranges[r];if(!(i.head.ch>100||r&&n.ranges[r-1].head.line==i.head.line)){var o=e.getModeAt(i.head),a=!1;if(o.electricChars){for(var l=0;l<o.electricChars.length;l++)if(t.indexOf(o.electricChars.charAt(l))>-1){a=Fn(e,i.head.line,"smart");break}}else o.electricInput&&o.electricInput.test(Zr(e.doc,i.head.line).text.slice(0,i.head.ch))&&(a=Fn(e,i.head.line,"smart"));a&&Ci(e,"electricInput",e,i.head.line)}}}function ee(e){for(var t=[],n=[],r=0;r<e.doc.sel.ranges.length;r++){var i=e.doc.sel.ranges[r].head.line,o={anchor:Bo(i,0),head:Bo(i+1,0)};n.push(o),t.push(e.getRange(o.anchor,o.head))}return{text:t,ranges:n}}function te(e){e.setAttribute("autocorrect","off"),e.setAttribute("autocapitalize","off"),e.setAttribute("spellcheck","false")}function ne(e){this.cm=e,this.prevInput="",this.pollingFast=!1,this.polling=new Ei,this.inaccurateSelection=!1,this.hasSelection=!1,this.composing=null}function re(){var e=ji("textarea",null,null,"position: absolute; padding: 0; width: 1px; height: 1em; outline: none"),t=ji("div",[e],null,"overflow: hidden; position: relative; width: 3px; height: 0px;");return wo?e.style.width="1000px":e.setAttribute("wrap","off"),No&&(e.style.border="1px solid black"),te(e),t}function ie(e){this.cm=e,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new Ei,this.gracePeriod=!1}function oe(e,t){var n=Qe(e,t.line);if(!n||n.hidden)return null;var r=Zr(e.doc,t.line),i=Xe(n,r,t.line),o=ii(r),a="left";if(o){var l=co(o,t.ch);a=l%2?"right":"left"}var s=nt(i.map,t.ch,a);return s.offset="right"==s.collapse?s.end:s.start,s}function ae(e,t){return t&&(e.bad=!0),e}function le(e,t,n){var r;if(t==e.display.lineDiv){if(r=e.display.lineDiv.childNodes[n],!r)return ae(e.clipPos(Bo(e.display.viewTo-1)),!0);t=null,n=0}else for(r=t;;r=r.parentNode){if(!r||r==e.display.lineDiv)return null;if(r.parentNode&&r.parentNode==e.display.lineDiv)break}for(var i=0;i<e.display.view.length;i++){var o=e.display.view[i];if(o.node==r)return se(o,t,n)}}function se(e,t,n){function r(t,n,r){for(var i=-1;i<(u?u.length:0);i++)for(var o=0>i?c.map:u[i],a=0;a<o.length;a+=3){var l=o[a+2];if(l==t||l==n){var s=ti(0>i?e.line:e.rest[i]),f=o[a]+r;return(0>r||l!=t)&&(f=o[a+(r?1:0)]),Bo(s,f)}}}var i=e.text.firstChild,o=!1;if(!t||!Va(i,t))return ae(Bo(ti(e.line),0),!0);if(t==i&&(o=!0,t=i.childNodes[n],n=0,!t)){var a=e.rest?Ii(e.rest):e.line;return ae(Bo(ti(a),a.text.length),o)}var l=3==t.nodeType?t:null,s=t;for(l||1!=t.childNodes.length||3!=t.firstChild.nodeType||(l=t.firstChild,n&&(n=l.nodeValue.length));s.parentNode!=i;)s=s.parentNode;var c=e.measure,u=c.maps,f=r(l,s,n);if(f)return ae(f,o);for(var h=s.nextSibling,d=l?l.nodeValue.length-n:0;h;h=h.nextSibling){if(f=r(h,h.firstChild,0))return ae(Bo(f.line,f.ch-d),o);d+=h.textContent.length}for(var p=s.previousSibling,d=n;p;p=p.previousSibling){if(f=r(p,p.firstChild,-1))return ae(Bo(f.line,f.ch+d),o);d+=h.textContent.length}}function ce(e,t,n,r,i){function o(e){return function(t){return t.id==e}}function a(t){if(1==t.nodeType){var n=t.getAttribute("cm-text");if(null!=n)return""==n&&(n=t.textContent.replace(/\u200b/g,"")),void(l+=n);var u,f=t.getAttribute("cm-marker");if(f){var h=e.findMarks(Bo(r,0),Bo(i+1,0),o(+f));return void(h.length&&(u=h[0].find())&&(l+=Jr(e.doc,u.from,u.to).join(c)))}if("false"==t.getAttribute("contenteditable"))return;for(var d=0;d<t.childNodes.length;d++)a(t.childNodes[d]);/^(pre|div|p)$/i.test(t.nodeName)&&(s=!0)}else if(3==t.nodeType){var p=t.nodeValue;if(!p)return;s&&(l+=c,s=!1),l+=p}}for(var l="",s=!1,c=e.doc.lineSeparator();a(t),t!=n;)t=t.nextSibling;return l}function ue(e,t){this.ranges=e,this.primIndex=t}function fe(e,t){this.anchor=e,this.head=t}function he(e,t){var n=e[t];e.sort(function(e,t){return _o(e.from(),t.from())}),t=Pi(e,n);for(var r=1;r<e.length;r++){var i=e[r],o=e[r-1];if(_o(o.to(),i.from())>=0){var a=K(o.from(),i.from()),l=V(o.to(),i.to()),s=o.empty()?i.from()==i.head:o.from()==o.head;t>=r&&--t,e.splice(--r,2,new fe(s?l:a,s?a:l))}}return new ue(e,t)}function de(e,t){return new ue([new fe(e,t||e)],0)}function pe(e,t){return Math.max(e.first,Math.min(t,e.first+e.size-1))}function me(e,t){if(t.line<e.first)return Bo(e.first,0);var n=e.first+e.size-1;return t.line>n?Bo(n,Zr(e,n).text.length):ge(t,Zr(e,t.line).text.length)}function ge(e,t){var n=e.ch;return null==n||n>t?Bo(e.line,t):0>n?Bo(e.line,0):e}function ve(e,t){return t>=e.first&&t<e.first+e.size}function ye(e,t){for(var n=[],r=0;r<t.length;r++)n[r]=me(e,t[r]);return n}function xe(e,t,n,r){if(e.cm&&e.cm.display.shift||e.extend){var i=t.anchor;if(r){var o=_o(n,i)<0;o!=_o(r,i)<0?(i=n,n=r):o!=_o(n,r)<0&&(n=r)}return new fe(i,n)}return new fe(r||n,n)}function be(e,t,n,r){Te(e,new ue([xe(e,e.sel.primary(),t,n)],0),r)}function we(e,t,n){for(var r=[],i=0;i<e.sel.ranges.length;i++)r[i]=xe(e,e.sel.ranges[i],t[i],null);var o=he(r,e.sel.primIndex);Te(e,o,n)}function ke(e,t,n,r){var i=e.sel.ranges.slice(0);i[t]=n,Te(e,he(i,e.sel.primIndex),r)}function Se(e,t,n,r){Te(e,de(t,n),r)}function Ce(e,t,n){var r={ranges:t.ranges,update:function(t){this.ranges=[];for(var n=0;n<t.length;n++)this.ranges[n]=new fe(me(e,t[n].anchor),me(e,t[n].head))},origin:n&&n.origin};return Pa(e,"beforeSelectionChange",e,r),e.cm&&Pa(e.cm,"beforeSelectionChange",e.cm,r),r.ranges!=t.ranges?he(r.ranges,r.ranges.length-1):t}function Le(e,t,n){var r=e.history.done,i=Ii(r);i&&i.ranges?(r[r.length-1]=t,Me(e,t,n)):Te(e,t,n)}function Te(e,t,n){Me(e,t,n),fi(e,e.sel,e.cm?e.cm.curOp.id:NaN,n)}function Me(e,t,n){(Ni(e,"beforeSelectionChange")||e.cm&&Ni(e.cm,"beforeSelectionChange"))&&(t=Ce(e,t,n));var r=n&&n.bias||(_o(t.primary().head,e.sel.primary().head)<0?-1:1);Ne(e,Ee(e,t,r,!0)),n&&n.scroll===!1||!e.cm||Bn(e.cm)}function Ne(e,t){t.equals(e.sel)||(e.sel=t,e.cm&&(e.cm.curOp.updateInput=e.cm.curOp.selectionChanged=!0,Mi(e.cm)),Ci(e,"cursorActivity",e))}function Ae(e){Ne(e,Ee(e,e.sel,null,!1),Wa)}function Ee(e,t,n,r){for(var i,o=0;o<t.ranges.length;o++){var a=t.ranges[o],l=t.ranges.length==e.sel.ranges.length&&e.sel.ranges[o],s=Ie(e,a.anchor,l&&l.anchor,n,r),c=Ie(e,a.head,l&&l.head,n,r);(i||s!=a.anchor||c!=a.head)&&(i||(i=t.ranges.slice(0,o)),i[o]=new fe(s,c))}return i?he(i,t.primIndex):t}function Oe(e,t,n,r,i){var o=Zr(e,t.line);if(o.markedSpans)for(var a=0;a<o.markedSpans.length;++a){var l=o.markedSpans[a],s=l.marker;if((null==l.from||(s.inclusiveLeft?l.from<=t.ch:l.from<t.ch))&&(null==l.to||(s.inclusiveRight?l.to>=t.ch:l.to>t.ch))){if(i&&(Pa(s,"beforeCursorEnter"),s.explicitlyCleared)){if(o.markedSpans){--a;continue}break}if(!s.atomic)continue;if(n){var c,u=s.find(0>r?1:-1);if((0>r?s.inclusiveRight:s.inclusiveLeft)&&(u=Pe(e,u,-r,u&&u.line==t.line?o:null)),u&&u.line==t.line&&(c=_o(u,n))&&(0>r?0>c:c>0))return Oe(e,u,t,r,i)}var f=s.find(0>r?-1:1);return(0>r?s.inclusiveLeft:s.inclusiveRight)&&(f=Pe(e,f,r,f.line==t.line?o:null)),f?Oe(e,f,t,r,i):null}}return t}function Ie(e,t,n,r,i){var o=r||1,a=Oe(e,t,n,o,i)||!i&&Oe(e,t,n,o,!0)||Oe(e,t,n,-o,i)||!i&&Oe(e,t,n,-o,!0);return a?a:(e.cantEdit=!0,Bo(e.first,0))}function Pe(e,t,n,r){return 0>n&&0==t.ch?t.line>e.first?me(e,Bo(t.line-1)):null:n>0&&t.ch==(r||Zr(e,t.line)).text.length?t.line<e.first+e.size-1?Bo(t.line+1,0):null:new Bo(t.line,t.ch+n)}function Re(e){e.display.input.showSelection(e.display.input.prepareSelection())}function De(e,t){for(var n=e.doc,r={},i=r.cursors=document.createDocumentFragment(),o=r.selection=document.createDocumentFragment(),a=0;a<n.sel.ranges.length;a++)if(t!==!1||a!=n.sel.primIndex){var l=n.sel.ranges[a];if(!(l.from().line>=e.display.viewTo||l.to().line<e.display.viewFrom)){var s=l.empty();(s||e.options.showCursorWhenSelecting)&&He(e,l.head,i),s||We(e,l,o)}}return r}function He(e,t,n){var r=dt(e,t,"div",null,null,!e.options.singleCursorHeightPerLine),i=n.appendChild(ji("div"," ","CodeMirror-cursor"));if(i.style.left=r.left+"px",i.style.top=r.top+"px",i.style.height=Math.max(0,r.bottom-r.top)*e.options.cursorHeight+"px",r.other){var o=n.appendChild(ji("div"," ","CodeMirror-cursor CodeMirror-secondarycursor"));o.style.display="",o.style.left=r.other.left+"px",o.style.top=r.other.top+"px",o.style.height=.85*(r.other.bottom-r.other.top)+"px"}}function We(e,t,n){function r(e,t,n,r){0>t&&(t=0),t=Math.round(t),r=Math.round(r),l.appendChild(ji("div",null,"CodeMirror-selected","position: absolute; left: "+e+"px; top: "+t+"px; width: "+(null==n?u-e:n)+"px; height: "+(r-t)+"px"))}function i(t,n,i){function o(n,r){return ht(e,Bo(t,n),"div",f,r)}var l,s,f=Zr(a,t),h=f.text.length;return eo(ii(f),n||0,null==i?h:i,function(e,t,a){var f,d,p,m=o(e,"left");if(e==t)f=m,d=p=m.left;else{if(f=o(t-1,"right"),"rtl"==a){var g=m;m=f,f=g}d=m.left,p=f.right}null==n&&0==e&&(d=c),f.top-m.top>3&&(r(d,m.top,null,m.bottom),d=c,m.bottom<f.top&&r(d,m.bottom,null,f.top)),null==i&&t==h&&(p=u),(!l||m.top<l.top||m.top==l.top&&m.left<l.left)&&(l=m),(!s||f.bottom>s.bottom||f.bottom==s.bottom&&f.right>s.right)&&(s=f),c+1>d&&(d=c),r(d,f.top,p-d,f.bottom)}),{start:l,end:s}}var o=e.display,a=e.doc,l=document.createDocumentFragment(),s=Ge(e.display),c=s.left,u=Math.max(o.sizerWidth,$e(e)-o.sizer.offsetLeft)-s.right,f=t.from(),h=t.to();if(f.line==h.line)i(f.line,f.ch,h.ch);else{var d=Zr(a,f.line),p=Zr(a,h.line),m=yr(d)==yr(p),g=i(f.line,f.ch,m?d.text.length+1:null).end,v=i(h.line,m?0:null,h.ch).start;m&&(g.top<v.top-2?(r(g.right,g.top,null,g.bottom),r(c,v.top,v.left,v.bottom)):r(g.right,g.top,v.left-g.right,g.bottom)),g.bottom<v.top&&r(c,g.bottom,null,v.top)}n.appendChild(l)}function Be(e){if(e.state.focused){var t=e.display;clearInterval(t.blinker);var n=!0;t.cursorDiv.style.visibility="",e.options.cursorBlinkRate>0?t.blinker=setInterval(function(){t.cursorDiv.style.visibility=(n=!n)?"":"hidden"},e.options.cursorBlinkRate):e.options.cursorBlinkRate<0&&(t.cursorDiv.style.visibility="hidden")}}function _e(e,t){e.doc.mode.startState&&e.doc.frontier<e.display.viewTo&&e.state.highlight.set(t,Bi(Fe,e))}function Fe(e){var t=e.doc;if(t.frontier<t.first&&(t.frontier=t.first),!(t.frontier>=e.display.viewTo)){var n=+new Date+e.options.workTime,r=sa(t.mode,je(e,t.frontier)),i=[];t.iter(t.frontier,Math.min(t.first+t.size,e.display.viewTo+500),function(o){if(t.frontier>=e.display.viewFrom){var a=o.styles,l=o.text.length>e.options.maxHighlightLength,s=Rr(e,o,l?sa(t.mode,r):r,!0);o.styles=s.styles;var c=o.styleClasses,u=s.classes;u?o.styleClasses=u:c&&(o.styleClasses=null);for(var f=!a||a.length!=o.styles.length||c!=u&&(!c||!u||c.bgClass!=u.bgClass||c.textClass!=u.textClass),h=0;!f&&h<a.length;++h)f=a[h]!=o.styles[h];f&&i.push(t.frontier),o.stateAfter=l?r:sa(t.mode,r)}else o.text.length<=e.options.maxHighlightLength&&Hr(e,o.text,r),o.stateAfter=t.frontier%5==0?sa(t.mode,r):null;return++t.frontier,+new Date>n?(_e(e,e.options.workDelay),!0):void 0}),i.length&&At(e,function(){for(var t=0;t<i.length;t++)Ht(e,i[t],"text")})}}function ze(e,t,n){for(var r,i,o=e.doc,a=n?-1:t-(e.doc.mode.innerMode?1e3:100),l=t;l>a;--l){if(l<=o.first)return o.first;var s=Zr(o,l-1);if(s.stateAfter&&(!n||l<=o.frontier))return l;var c=Fa(s.text,null,e.options.tabSize);(null==i||r>c)&&(i=l-1,r=c)}return i}function je(e,t,n){var r=e.doc,i=e.display;if(!r.mode.startState)return!0;var o=ze(e,t,n),a=o>r.first&&Zr(r,o-1).stateAfter;return a=a?sa(r.mode,a):ca(r.mode),r.iter(o,t,function(n){Hr(e,n.text,a);var l=o==t-1||o%5==0||o>=i.viewFrom&&o<i.viewTo;n.stateAfter=l?sa(r.mode,a):null,++o}),n&&(r.frontier=o),a}function Ue(e){return e.lineSpace.offsetTop}function qe(e){return e.mover.offsetHeight-e.lineSpace.offsetHeight}function Ge(e){if(e.cachedPaddingH)return e.cachedPaddingH;var t=qi(e.measure,ji("pre","x")),n=window.getComputedStyle?window.getComputedStyle(t):t.currentStyle,r={left:parseInt(n.paddingLeft),right:parseInt(n.paddingRight)};return isNaN(r.left)||isNaN(r.right)||(e.cachedPaddingH=r),r}function Ye(e){return Da-e.display.nativeBarWidth}function $e(e){return e.display.scroller.clientWidth-Ye(e)-e.display.barWidth}function Ve(e){return e.display.scroller.clientHeight-Ye(e)-e.display.barHeight}function Ke(e,t,n){var r=e.options.lineWrapping,i=r&&$e(e);if(!t.measure.heights||r&&t.measure.width!=i){var o=t.measure.heights=[];if(r){t.measure.width=i;for(var a=t.text.firstChild.getClientRects(),l=0;l<a.length-1;l++){var s=a[l],c=a[l+1];Math.abs(s.bottom-c.bottom)>2&&o.push((s.bottom+c.top)/2-n.top)}}o.push(n.bottom-n.top)}}function Xe(e,t,n){if(e.line==t)return{map:e.measure.map,cache:e.measure.cache};for(var r=0;r<e.rest.length;r++)if(e.rest[r]==t)return{map:e.measure.maps[r],cache:e.measure.caches[r]};for(var r=0;r<e.rest.length;r++)if(ti(e.rest[r])>n)return{map:e.measure.maps[r],cache:e.measure.caches[r],before:!0}}function Ze(e,t){t=yr(t);var n=ti(t),r=e.display.externalMeasured=new Pt(e.doc,t,n);r.lineN=n;var i=r.built=Br(e,r);return r.text=i.pre,qi(e.display.lineMeasure,i.pre),r}function Je(e,t,n,r){return tt(e,et(e,t),n,r)}function Qe(e,t){if(t>=e.display.viewFrom&&t<e.display.viewTo)return e.display.view[Bt(e,t)];var n=e.display.externalMeasured;return n&&t>=n.lineN&&t<n.lineN+n.size?n:void 0}function et(e,t){var n=ti(t),r=Qe(e,n);r&&!r.text?r=null:r&&r.changes&&(D(e,r,n,P(e)),e.curOp.forceUpdate=!0),r||(r=Ze(e,t));var i=Xe(r,t,n);return{line:t,view:r,rect:null,map:i.map,cache:i.cache,before:i.before,hasHeights:!1}}function tt(e,t,n,r,i){t.before&&(n=-1);var o,a=n+(r||"");return t.cache.hasOwnProperty(a)?o=t.cache[a]:(t.rect||(t.rect=t.view.text.getBoundingClientRect()),t.hasHeights||(Ke(e,t.view,t.rect),t.hasHeights=!0),o=rt(e,t,n,r),o.bogus||(t.cache[a]=o)),{left:o.left,right:o.right,top:i?o.rtop:o.top,bottom:i?o.rbottom:o.bottom}}function nt(e,t,n){for(var r,i,o,a,l=0;l<e.length;l+=3){var s=e[l],c=e[l+1];if(s>t?(i=0,o=1,a="left"):c>t?(i=t-s,o=i+1):(l==e.length-3||t==c&&e[l+3]>t)&&(o=c-s,i=o-1,t>=c&&(a="right")),null!=i){if(r=e[l+2],s==c&&n==(r.insertLeft?"left":"right")&&(a=n),"left"==n&&0==i)for(;l&&e[l-2]==e[l-3]&&e[l-1].insertLeft;)r=e[(l-=3)+2],a="left";if("right"==n&&i==c-s)for(;l<e.length-3&&e[l+3]==e[l+4]&&!e[l+5].insertLeft;)r=e[(l+=3)+2],a="right";break}}return{node:r,start:i,end:o,collapse:a,coverStart:s,coverEnd:c}}function rt(e,t,n,r){var i,o=nt(t.map,n,r),a=o.node,l=o.start,s=o.end,c=o.collapse;if(3==a.nodeType){for(var u=0;4>u;u++){for(;l&&zi(t.line.text.charAt(o.coverStart+l));)--l;for(;o.coverStart+s<o.coverEnd&&zi(t.line.text.charAt(o.coverStart+s));)++s;if(xo&&9>bo&&0==l&&s==o.coverEnd-o.coverStart)i=a.parentNode.getBoundingClientRect();else if(xo&&e.options.lineWrapping){var f=qa(a,l,s).getClientRects();i=f.length?f["right"==r?f.length-1:0]:qo}else i=qa(a,l,s).getBoundingClientRect()||qo;if(i.left||i.right||0==l)break;s=l,l-=1,c="right"}xo&&11>bo&&(i=it(e.display.measure,i))}else{l>0&&(c=r="right");var f;i=e.options.lineWrapping&&(f=a.getClientRects()).length>1?f["right"==r?f.length-1:0]:a.getBoundingClientRect()}if(xo&&9>bo&&!l&&(!i||!i.left&&!i.right)){var h=a.parentNode.getClientRects()[0];i=h?{left:h.left,right:h.left+xt(e.display),top:h.top,bottom:h.bottom}:qo}for(var d=i.top-t.rect.top,p=i.bottom-t.rect.top,m=(d+p)/2,g=t.view.measure.heights,u=0;u<g.length-1&&!(m<g[u]);u++);var v=u?g[u-1]:0,y=g[u],x={left:("right"==c?i.right:i.left)-t.rect.left,right:("left"==c?i.left:i.right)-t.rect.left,top:v,bottom:y};return i.left||i.right||(x.bogus=!0),e.options.singleCursorHeightPerLine||(x.rtop=d,x.rbottom=p),x}function it(e,t){if(!window.screen||null==screen.logicalXDPI||screen.logicalXDPI==screen.deviceXDPI||!Qi(e))return t;var n=screen.logicalXDPI/screen.deviceXDPI,r=screen.logicalYDPI/screen.deviceYDPI;return{left:t.left*n,right:t.right*n,top:t.top*r,bottom:t.bottom*r}}function ot(e){if(e.measure&&(e.measure.cache={},e.measure.heights=null,e.rest))for(var t=0;t<e.rest.length;t++)e.measure.caches[t]={}}function at(e){e.display.externalMeasure=null,Ui(e.display.lineMeasure);for(var t=0;t<e.display.view.length;t++)ot(e.display.view[t])}function lt(e){at(e),e.display.cachedCharWidth=e.display.cachedTextHeight=e.display.cachedPaddingH=null,e.options.lineWrapping||(e.display.maxLineChanged=!0),e.display.lineNumChars=null}function st(){return window.pageXOffset||(document.documentElement||document.body).scrollLeft}function ct(){return window.pageYOffset||(document.documentElement||document.body).scrollTop}function ut(e,t,n,r){if(t.widgets)for(var i=0;i<t.widgets.length;++i)if(t.widgets[i].above){var o=Lr(t.widgets[i]);n.top+=o,n.bottom+=o}if("line"==r)return n;r||(r="local");var a=ri(t);if("local"==r?a+=Ue(e.display):a-=e.display.viewOffset,"page"==r||"window"==r){var l=e.display.lineSpace.getBoundingClientRect();a+=l.top+("window"==r?0:ct());var s=l.left+("window"==r?0:st());n.left+=s,n.right+=s}return n.top+=a,n.bottom+=a,n}function ft(e,t,n){if("div"==n)return t;var r=t.left,i=t.top;if("page"==n)r-=st(), -i-=ct();else if("local"==n||!n){var o=e.display.sizer.getBoundingClientRect();r+=o.left,i+=o.top}var a=e.display.lineSpace.getBoundingClientRect();return{left:r-a.left,top:i-a.top}}function ht(e,t,n,r,i){return r||(r=Zr(e.doc,t.line)),ut(e,r,Je(e,r,t.ch,i),n)}function dt(e,t,n,r,i,o){function a(t,a){var l=tt(e,i,t,a?"right":"left",o);return a?l.left=l.right:l.right=l.left,ut(e,r,l,n)}function l(e,t){var n=s[t],r=n.level%2;return e==to(n)&&t&&n.level<s[t-1].level?(n=s[--t],e=no(n)-(n.level%2?0:1),r=!0):e==no(n)&&t<s.length-1&&n.level<s[t+1].level&&(n=s[++t],e=to(n)-n.level%2,r=!1),r&&e==n.to&&e>n.from?a(e-1):a(e,r)}r=r||Zr(e.doc,t.line),i||(i=et(e,r));var s=ii(r),c=t.ch;if(!s)return a(c);var u=co(s,c),f=l(c,u);return null!=al&&(f.other=l(c,al)),f}function pt(e,t){var n=0,t=me(e.doc,t);e.options.lineWrapping||(n=xt(e.display)*t.ch);var r=Zr(e.doc,t.line),i=ri(r)+Ue(e.display);return{left:n,right:n,top:i,bottom:i+r.height}}function mt(e,t,n,r){var i=Bo(e,t);return i.xRel=r,n&&(i.outside=!0),i}function gt(e,t,n){var r=e.doc;if(n+=e.display.viewOffset,0>n)return mt(r.first,0,!0,-1);var i=ni(r,n),o=r.first+r.size-1;if(i>o)return mt(r.first+r.size-1,Zr(r,o).text.length,!0,1);0>t&&(t=0);for(var a=Zr(r,i);;){var l=vt(e,a,i,t,n),s=gr(a),c=s&&s.find(0,!0);if(!s||!(l.ch>c.from.ch||l.ch==c.from.ch&&l.xRel>0))return l;i=ti(a=c.to.line)}}function vt(e,t,n,r,i){function o(r){var i=dt(e,Bo(n,r),"line",t,c);return l=!0,a>i.bottom?i.left-s:a<i.top?i.left+s:(l=!1,i.left)}var a=i-ri(t),l=!1,s=2*e.display.wrapper.clientWidth,c=et(e,t),u=ii(t),f=t.text.length,h=ro(t),d=io(t),p=o(h),m=l,g=o(d),v=l;if(r>g)return mt(n,d,v,1);for(;;){if(u?d==h||d==fo(t,h,1):1>=d-h){for(var y=p>r||g-r>=r-p?h:d,x=r-(y==h?p:g);zi(t.text.charAt(y));)++y;var b=mt(n,y,y==h?m:v,-1>x?-1:x>1?1:0);return b}var w=Math.ceil(f/2),k=h+w;if(u){k=h;for(var S=0;w>S;++S)k=fo(t,k,1)}var C=o(k);C>r?(d=k,g=C,(v=l)&&(g+=1e3),f=w):(h=k,p=C,m=l,f-=w)}}function yt(e){if(null!=e.cachedTextHeight)return e.cachedTextHeight;if(null==zo){zo=ji("pre");for(var t=0;49>t;++t)zo.appendChild(document.createTextNode("x")),zo.appendChild(ji("br"));zo.appendChild(document.createTextNode("x"))}qi(e.measure,zo);var n=zo.offsetHeight/50;return n>3&&(e.cachedTextHeight=n),Ui(e.measure),n||1}function xt(e){if(null!=e.cachedCharWidth)return e.cachedCharWidth;var t=ji("span","xxxxxxxxxx"),n=ji("pre",[t]);qi(e.measure,n);var r=t.getBoundingClientRect(),i=(r.right-r.left)/10;return i>2&&(e.cachedCharWidth=i),i||10}function bt(e){e.curOp={cm:e,viewChanged:!1,startHeight:e.doc.height,forceUpdate:!1,updateInput:null,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Yo},Go?Go.ops.push(e.curOp):e.curOp.ownsGroup=Go={ops:[e.curOp],delayedCallbacks:[]}}function wt(e){var t=e.delayedCallbacks,n=0;do{for(;n<t.length;n++)t[n].call(null);for(var r=0;r<e.ops.length;r++){var i=e.ops[r];if(i.cursorActivityHandlers)for(;i.cursorActivityCalled<i.cursorActivityHandlers.length;)i.cursorActivityHandlers[i.cursorActivityCalled++].call(null,i.cm)}}while(n<t.length)}function kt(e){var t=e.curOp,n=t.ownsGroup;if(n)try{wt(n)}finally{Go=null;for(var r=0;r<n.ops.length;r++)n.ops[r].cm.curOp=null;St(n)}}function St(e){for(var t=e.ops,n=0;n<t.length;n++)Ct(t[n]);for(var n=0;n<t.length;n++)Lt(t[n]);for(var n=0;n<t.length;n++)Tt(t[n]);for(var n=0;n<t.length;n++)Mt(t[n]);for(var n=0;n<t.length;n++)Nt(t[n])}function Ct(e){var t=e.cm,n=t.display;T(t),e.updateMaxLine&&h(t),e.mustUpdate=e.viewChanged||e.forceUpdate||null!=e.scrollTop||e.scrollToPos&&(e.scrollToPos.from.line<n.viewFrom||e.scrollToPos.to.line>=n.viewTo)||n.maxLineChanged&&t.options.lineWrapping,e.update=e.mustUpdate&&new L(t,e.mustUpdate&&{top:e.scrollTop,ensure:e.scrollToPos},e.forceUpdate)}function Lt(e){e.updatedDisplay=e.mustUpdate&&M(e.cm,e.update)}function Tt(e){var t=e.cm,n=t.display;e.updatedDisplay&&O(t),e.barMeasure=p(t),n.maxLineChanged&&!t.options.lineWrapping&&(e.adjustWidthTo=Je(t,n.maxLine,n.maxLine.text.length).left+3,t.display.sizerWidth=e.adjustWidthTo,e.barMeasure.scrollWidth=Math.max(n.scroller.clientWidth,n.sizer.offsetLeft+e.adjustWidthTo+Ye(t)+t.display.barWidth),e.maxScrollLeft=Math.max(0,n.sizer.offsetLeft+e.adjustWidthTo-$e(t))),(e.updatedDisplay||e.selectionChanged)&&(e.preparedSelection=n.input.prepareSelection(e.focus))}function Mt(e){var t=e.cm;null!=e.adjustWidthTo&&(t.display.sizer.style.minWidth=e.adjustWidthTo+"px",e.maxScrollLeft<t.doc.scrollLeft&&on(t,Math.min(t.display.scroller.scrollLeft,e.maxScrollLeft),!0),t.display.maxLineChanged=!1);var n=e.focus&&e.focus==Gi()&&(!document.hasFocus||document.hasFocus());e.preparedSelection&&t.display.input.showSelection(e.preparedSelection,n),(e.updatedDisplay||e.startHeight!=t.doc.height)&&y(t,e.barMeasure),e.updatedDisplay&&E(t,e.barMeasure),e.selectionChanged&&Be(t),t.state.focused&&e.updateInput&&t.display.input.reset(e.typing),n&&X(e.cm)}function Nt(e){var t=e.cm,n=t.display,r=t.doc;if(e.updatedDisplay&&N(t,e.update),null==n.wheelStartX||null==e.scrollTop&&null==e.scrollLeft&&!e.scrollToPos||(n.wheelStartX=n.wheelStartY=null),null==e.scrollTop||n.scroller.scrollTop==e.scrollTop&&!e.forceScroll||(r.scrollTop=Math.max(0,Math.min(n.scroller.scrollHeight-n.scroller.clientHeight,e.scrollTop)),n.scrollbars.setScrollTop(r.scrollTop),n.scroller.scrollTop=r.scrollTop),null==e.scrollLeft||n.scroller.scrollLeft==e.scrollLeft&&!e.forceScroll||(r.scrollLeft=Math.max(0,Math.min(n.scroller.scrollWidth-n.scroller.clientWidth,e.scrollLeft)),n.scrollbars.setScrollLeft(r.scrollLeft),n.scroller.scrollLeft=r.scrollLeft,w(t)),e.scrollToPos){var i=Rn(t,me(r,e.scrollToPos.from),me(r,e.scrollToPos.to),e.scrollToPos.margin);e.scrollToPos.isCursor&&t.state.focused&&Pn(t,i)}var o=e.maybeHiddenMarkers,a=e.maybeUnhiddenMarkers;if(o)for(var l=0;l<o.length;++l)o[l].lines.length||Pa(o[l],"hide");if(a)for(var l=0;l<a.length;++l)a[l].lines.length&&Pa(a[l],"unhide");n.wrapper.offsetHeight&&(r.scrollTop=t.display.scroller.scrollTop),e.changeObjs&&Pa(t,"changes",t,e.changeObjs),e.update&&e.update.finish()}function At(e,t){if(e.curOp)return t();bt(e);try{return t()}finally{kt(e)}}function Et(e,t){return function(){if(e.curOp)return t.apply(e,arguments);bt(e);try{return t.apply(e,arguments)}finally{kt(e)}}}function Ot(e){return function(){if(this.curOp)return e.apply(this,arguments);bt(this);try{return e.apply(this,arguments)}finally{kt(this)}}}function It(e){return function(){var t=this.cm;if(!t||t.curOp)return e.apply(this,arguments);bt(t);try{return e.apply(this,arguments)}finally{kt(t)}}}function Pt(e,t,n){this.line=t,this.rest=xr(t),this.size=this.rest?ti(Ii(this.rest))-n+1:1,this.node=this.text=null,this.hidden=kr(e,t)}function Rt(e,t,n){for(var r,i=[],o=t;n>o;o=r){var a=new Pt(e.doc,Zr(e.doc,o),o);r=o+a.size,i.push(a)}return i}function Dt(e,t,n,r){null==t&&(t=e.doc.first),null==n&&(n=e.doc.first+e.doc.size),r||(r=0);var i=e.display;if(r&&n<i.viewTo&&(null==i.updateLineNumbers||i.updateLineNumbers>t)&&(i.updateLineNumbers=t),e.curOp.viewChanged=!0,t>=i.viewTo)Wo&&br(e.doc,t)<i.viewTo&&Wt(e);else if(n<=i.viewFrom)Wo&&wr(e.doc,n+r)>i.viewFrom?Wt(e):(i.viewFrom+=r,i.viewTo+=r);else if(t<=i.viewFrom&&n>=i.viewTo)Wt(e);else if(t<=i.viewFrom){var o=_t(e,n,n+r,1);o?(i.view=i.view.slice(o.index),i.viewFrom=o.lineN,i.viewTo+=r):Wt(e)}else if(n>=i.viewTo){var o=_t(e,t,t,-1);o?(i.view=i.view.slice(0,o.index),i.viewTo=o.lineN):Wt(e)}else{var a=_t(e,t,t,-1),l=_t(e,n,n+r,1);a&&l?(i.view=i.view.slice(0,a.index).concat(Rt(e,a.lineN,l.lineN)).concat(i.view.slice(l.index)),i.viewTo+=r):Wt(e)}var s=i.externalMeasured;s&&(n<s.lineN?s.lineN+=r:t<s.lineN+s.size&&(i.externalMeasured=null))}function Ht(e,t,n){e.curOp.viewChanged=!0;var r=e.display,i=e.display.externalMeasured;if(i&&t>=i.lineN&&t<i.lineN+i.size&&(r.externalMeasured=null),!(t<r.viewFrom||t>=r.viewTo)){var o=r.view[Bt(e,t)];if(null!=o.node){var a=o.changes||(o.changes=[]);-1==Pi(a,n)&&a.push(n)}}}function Wt(e){e.display.viewFrom=e.display.viewTo=e.doc.first,e.display.view=[],e.display.viewOffset=0}function Bt(e,t){if(t>=e.display.viewTo)return null;if(t-=e.display.viewFrom,0>t)return null;for(var n=e.display.view,r=0;r<n.length;r++)if(t-=n[r].size,0>t)return r}function _t(e,t,n,r){var i,o=Bt(e,t),a=e.display.view;if(!Wo||n==e.doc.first+e.doc.size)return{index:o,lineN:n};for(var l=0,s=e.display.viewFrom;o>l;l++)s+=a[l].size;if(s!=t){if(r>0){if(o==a.length-1)return null;i=s+a[o].size-t,o++}else i=s-t;t+=i,n+=i}for(;br(e.doc,n)!=n;){if(o==(0>r?0:a.length-1))return null;n+=r*a[o-(0>r?1:0)].size,o+=r}return{index:o,lineN:n}}function Ft(e,t,n){var r=e.display,i=r.view;0==i.length||t>=r.viewTo||n<=r.viewFrom?(r.view=Rt(e,t,n),r.viewFrom=t):(r.viewFrom>t?r.view=Rt(e,t,r.viewFrom).concat(r.view):r.viewFrom<t&&(r.view=r.view.slice(Bt(e,t))),r.viewFrom=t,r.viewTo<n?r.view=r.view.concat(Rt(e,r.viewTo,n)):r.viewTo>n&&(r.view=r.view.slice(0,Bt(e,n)))),r.viewTo=n}function zt(e){for(var t=e.display.view,n=0,r=0;r<t.length;r++){var i=t[r];i.hidden||i.node&&!i.changes||++n}return n}function jt(e){function t(){i.activeTouch&&(o=setTimeout(function(){i.activeTouch=null},1e3),a=i.activeTouch,a.end=+new Date)}function n(e){if(1!=e.touches.length)return!1;var t=e.touches[0];return t.radiusX<=1&&t.radiusY<=1}function r(e,t){if(null==t.left)return!0;var n=t.left-e.left,r=t.top-e.top;return n*n+r*r>400}var i=e.display;Ea(i.scroller,"mousedown",Et(e,$t)),xo&&11>bo?Ea(i.scroller,"dblclick",Et(e,function(t){if(!Ti(e,t)){var n=Yt(e,t);if(n&&!Jt(e,t)&&!Gt(e.display,t)){Ma(t);var r=e.findWordAt(n);be(e.doc,r.anchor,r.head)}}})):Ea(i.scroller,"dblclick",function(t){Ti(e,t)||Ma(t)}),Do||Ea(i.scroller,"contextmenu",function(t){xn(e,t)});var o,a={end:0};Ea(i.scroller,"touchstart",function(t){if(!Ti(e,t)&&!n(t)){clearTimeout(o);var r=+new Date;i.activeTouch={start:r,moved:!1,prev:r-a.end<=300?a:null},1==t.touches.length&&(i.activeTouch.left=t.touches[0].pageX,i.activeTouch.top=t.touches[0].pageY)}}),Ea(i.scroller,"touchmove",function(){i.activeTouch&&(i.activeTouch.moved=!0)}),Ea(i.scroller,"touchend",function(n){var o=i.activeTouch;if(o&&!Gt(i,n)&&null!=o.left&&!o.moved&&new Date-o.start<300){var a,l=e.coordsChar(i.activeTouch,"page");a=!o.prev||r(o,o.prev)?new fe(l,l):!o.prev.prev||r(o,o.prev.prev)?e.findWordAt(l):new fe(Bo(l.line,0),me(e.doc,Bo(l.line+1,0))),e.setSelection(a.anchor,a.head),e.focus(),Ma(n)}t()}),Ea(i.scroller,"touchcancel",t),Ea(i.scroller,"scroll",function(){i.scroller.clientHeight&&(rn(e,i.scroller.scrollTop),on(e,i.scroller.scrollLeft,!0),Pa(e,"scroll",e))}),Ea(i.scroller,"mousewheel",function(t){an(e,t)}),Ea(i.scroller,"DOMMouseScroll",function(t){an(e,t)}),Ea(i.wrapper,"scroll",function(){i.wrapper.scrollTop=i.wrapper.scrollLeft=0}),i.dragFunctions={enter:function(t){Ti(e,t)||Aa(t)},over:function(t){Ti(e,t)||(tn(e,t),Aa(t))},start:function(t){en(e,t)},drop:Et(e,Qt),leave:function(t){Ti(e,t)||nn(e)}};var l=i.input.getField();Ea(l,"keyup",function(t){pn.call(e,t)}),Ea(l,"keydown",Et(e,hn)),Ea(l,"keypress",Et(e,mn)),Ea(l,"focus",Bi(vn,e)),Ea(l,"blur",Bi(yn,e))}function Ut(t,n,r){var i=r&&r!=e.Init;if(!n!=!i){var o=t.display.dragFunctions,a=n?Ea:Ia;a(t.display.scroller,"dragstart",o.start),a(t.display.scroller,"dragenter",o.enter),a(t.display.scroller,"dragover",o.over),a(t.display.scroller,"dragleave",o.leave),a(t.display.scroller,"drop",o.drop)}}function qt(e){var t=e.display;t.lastWrapHeight==t.wrapper.clientHeight&&t.lastWrapWidth==t.wrapper.clientWidth||(t.cachedCharWidth=t.cachedTextHeight=t.cachedPaddingH=null,t.scrollbarsClipped=!1,e.setSize())}function Gt(e,t){for(var n=wi(t);n!=e.wrapper;n=n.parentNode)if(!n||1==n.nodeType&&"true"==n.getAttribute("cm-ignore-events")||n.parentNode==e.sizer&&n!=e.mover)return!0}function Yt(e,t,n,r){var i=e.display;if(!n&&"true"==wi(t).getAttribute("cm-not-content"))return null;var o,a,l=i.lineSpace.getBoundingClientRect();try{o=t.clientX-l.left,a=t.clientY-l.top}catch(t){return null}var s,c=gt(e,o,a);if(r&&1==c.xRel&&(s=Zr(e.doc,c.line).text).length==c.ch){var u=Fa(s,s.length,e.options.tabSize)-s.length;c=Bo(c.line,Math.max(0,Math.round((o-Ge(e.display).left)/xt(e.display))-u))}return c}function $t(e){var t=this,n=t.display;if(!(Ti(t,e)||n.activeTouch&&n.input.supportsTouch())){if(n.shift=e.shiftKey,Gt(n,e))return void(wo||(n.scroller.draggable=!1,setTimeout(function(){n.scroller.draggable=!0},100)));if(!Jt(t,e)){var r=Yt(t,e);switch(window.focus(),ki(e)){case 1:t.state.selectingText?t.state.selectingText(e):r?Vt(t,e,r):wi(e)==n.scroller&&Ma(e);break;case 2:wo&&(t.state.lastMiddleDown=+new Date),r&&be(t.doc,r),setTimeout(function(){n.input.focus()},20),Ma(e);break;case 3:Do?xn(t,e):gn(t)}}}}function Vt(e,t,n){xo?setTimeout(Bi(X,e),0):e.curOp.focus=Gi();var r,i=+new Date;Uo&&Uo.time>i-400&&0==_o(Uo.pos,n)?r="triple":jo&&jo.time>i-400&&0==_o(jo.pos,n)?(r="double",Uo={time:i,pos:n}):(r="single",jo={time:i,pos:n});var o,a=e.doc.sel,l=Eo?t.metaKey:t.ctrlKey;e.options.dragDrop&&el&&!e.isReadOnly()&&"single"==r&&(o=a.contains(n))>-1&&(_o((o=a.ranges[o]).from(),n)<0||n.xRel>0)&&(_o(o.to(),n)>0||n.xRel<0)?Kt(e,t,n,l):Xt(e,t,n,r,l)}function Kt(e,t,n,r){var i=e.display,o=+new Date,a=Et(e,function(l){wo&&(i.scroller.draggable=!1),e.state.draggingText=!1,Ia(document,"mouseup",a),Ia(i.scroller,"drop",a),Math.abs(t.clientX-l.clientX)+Math.abs(t.clientY-l.clientY)<10&&(Ma(l),!r&&+new Date-200<o&&be(e.doc,n),wo||xo&&9==bo?setTimeout(function(){document.body.focus(),i.input.focus()},20):i.input.focus())});wo&&(i.scroller.draggable=!0),e.state.draggingText=a,i.scroller.dragDrop&&i.scroller.dragDrop(),Ea(document,"mouseup",a),Ea(i.scroller,"drop",a)}function Xt(e,t,n,r,i){function o(t){if(0!=_o(g,t))if(g=t,"rect"==r){for(var i=[],o=e.options.tabSize,a=Fa(Zr(c,n.line).text,n.ch,o),l=Fa(Zr(c,t.line).text,t.ch,o),s=Math.min(a,l),d=Math.max(a,l),p=Math.min(n.line,t.line),m=Math.min(e.lastLine(),Math.max(n.line,t.line));m>=p;p++){var v=Zr(c,p).text,y=za(v,s,o);s==d?i.push(new fe(Bo(p,y),Bo(p,y))):v.length>y&&i.push(new fe(Bo(p,y),Bo(p,za(v,d,o))))}i.length||i.push(new fe(n,n)),Te(c,he(h.ranges.slice(0,f).concat(i),f),{origin:"*mouse",scroll:!1}),e.scrollIntoView(t)}else{var x=u,b=x.anchor,w=t;if("single"!=r){if("double"==r)var k=e.findWordAt(t);else var k=new fe(Bo(t.line,0),me(c,Bo(t.line+1,0)));_o(k.anchor,b)>0?(w=k.head,b=K(x.from(),k.anchor)):(w=k.anchor,b=V(x.to(),k.head))}var i=h.ranges.slice(0);i[f]=new fe(me(c,b),w),Te(c,he(i,f),Ba)}}function a(t){var n=++y,i=Yt(e,t,!0,"rect"==r);if(i)if(0!=_o(i,g)){e.curOp.focus=Gi(),o(i);var l=b(s,c);(i.line>=l.to||i.line<l.from)&&setTimeout(Et(e,function(){y==n&&a(t)}),150)}else{var u=t.clientY<v.top?-20:t.clientY>v.bottom?20:0;u&&setTimeout(Et(e,function(){y==n&&(s.scroller.scrollTop+=u,a(t))}),50)}}function l(t){e.state.selectingText=!1,y=1/0,Ma(t),s.input.focus(),Ia(document,"mousemove",x),Ia(document,"mouseup",w),c.history.lastSelOrigin=null}var s=e.display,c=e.doc;Ma(t);var u,f,h=c.sel,d=h.ranges;if(i&&!t.shiftKey?(f=c.sel.contains(n),u=f>-1?d[f]:new fe(n,n)):(u=c.sel.primary(),f=c.sel.primIndex),Oo?t.shiftKey&&t.metaKey:t.altKey)r="rect",i||(u=new fe(n,n)),n=Yt(e,t,!0,!0),f=-1;else if("double"==r){var p=e.findWordAt(n);u=e.display.shift||c.extend?xe(c,u,p.anchor,p.head):p}else if("triple"==r){var m=new fe(Bo(n.line,0),me(c,Bo(n.line+1,0)));u=e.display.shift||c.extend?xe(c,u,m.anchor,m.head):m}else u=xe(c,u,n);i?-1==f?(f=d.length,Te(c,he(d.concat([u]),f),{scroll:!1,origin:"*mouse"})):d.length>1&&d[f].empty()&&"single"==r&&!t.shiftKey?(Te(c,he(d.slice(0,f).concat(d.slice(f+1)),0),{scroll:!1,origin:"*mouse"}),h=c.sel):ke(c,f,u,Ba):(f=0,Te(c,new ue([u],0),Ba),h=c.sel);var g=n,v=s.wrapper.getBoundingClientRect(),y=0,x=Et(e,function(e){ki(e)?a(e):l(e)}),w=Et(e,l);e.state.selectingText=w,Ea(document,"mousemove",x),Ea(document,"mouseup",w)}function Zt(e,t,n,r){try{var i=t.clientX,o=t.clientY}catch(t){return!1}if(i>=Math.floor(e.display.gutters.getBoundingClientRect().right))return!1;r&&Ma(t);var a=e.display,l=a.lineDiv.getBoundingClientRect();if(o>l.bottom||!Ni(e,n))return bi(t);o-=l.top-a.viewOffset;for(var s=0;s<e.options.gutters.length;++s){var c=a.gutters.childNodes[s];if(c&&c.getBoundingClientRect().right>=i){var u=ni(e.doc,o),f=e.options.gutters[s];return Pa(e,n,e,u,f,t),bi(t)}}}function Jt(e,t){return Zt(e,t,"gutterClick",!0)}function Qt(e){var t=this;if(nn(t),!Ti(t,e)&&!Gt(t.display,e)){Ma(e),xo&&($o=+new Date);var n=Yt(t,e,!0),r=e.dataTransfer.files;if(n&&!t.isReadOnly())if(r&&r.length&&window.FileReader&&window.File)for(var i=r.length,o=Array(i),a=0,l=function(e,r){if(!t.options.allowDropFileTypes||-1!=Pi(t.options.allowDropFileTypes,e.type)){var l=new FileReader;l.onload=Et(t,function(){var e=l.result;if(/[\x00-\x08\x0e-\x1f]{2}/.test(e)&&(e=""),o[r]=e,++a==i){n=me(t.doc,n);var s={from:n,to:n,text:t.doc.splitLines(o.join(t.doc.lineSeparator())),origin:"paste"};Tn(t.doc,s),Le(t.doc,de(n,Qo(s)))}}),l.readAsText(e)}},s=0;i>s;++s)l(r[s],s);else{if(t.state.draggingText&&t.doc.sel.contains(n)>-1)return t.state.draggingText(e),void setTimeout(function(){t.display.input.focus()},20);try{var o=e.dataTransfer.getData("Text");if(o){if(t.state.draggingText&&!(Eo?e.altKey:e.ctrlKey))var c=t.listSelections();if(Me(t.doc,de(n,n)),c)for(var s=0;s<c.length;++s)In(t.doc,"",c[s].anchor,c[s].head,"drag");t.replaceSelection(o,"around","paste"),t.display.input.focus()}}catch(e){}}}}function en(e,t){if(xo&&(!e.state.draggingText||+new Date-$o<100))return void Aa(t);if(!Ti(e,t)&&!Gt(e.display,t)&&(t.dataTransfer.setData("Text",e.getSelection()),t.dataTransfer.effectAllowed="copyMove",t.dataTransfer.setDragImage&&!Lo)){var n=ji("img",null,null,"position: fixed; left: 0; top: 0;");n.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",Co&&(n.width=n.height=1,e.display.wrapper.appendChild(n),n._top=n.offsetTop),t.dataTransfer.setDragImage(n,0,0),Co&&n.parentNode.removeChild(n)}}function tn(e,t){var n=Yt(e,t);if(n){var r=document.createDocumentFragment();He(e,n,r),e.display.dragCursor||(e.display.dragCursor=ji("div",null,"CodeMirror-cursors CodeMirror-dragcursors"),e.display.lineSpace.insertBefore(e.display.dragCursor,e.display.cursorDiv)),qi(e.display.dragCursor,r)}}function nn(e){e.display.dragCursor&&(e.display.lineSpace.removeChild(e.display.dragCursor),e.display.dragCursor=null)}function rn(e,t){Math.abs(e.doc.scrollTop-t)<2||(e.doc.scrollTop=t,go||A(e,{top:t}),e.display.scroller.scrollTop!=t&&(e.display.scroller.scrollTop=t),e.display.scrollbars.setScrollTop(t),go&&A(e),_e(e,100))}function on(e,t,n){(n?t==e.doc.scrollLeft:Math.abs(e.doc.scrollLeft-t)<2)||(t=Math.min(t,e.display.scroller.scrollWidth-e.display.scroller.clientWidth),e.doc.scrollLeft=t,w(e),e.display.scroller.scrollLeft!=t&&(e.display.scroller.scrollLeft=t),e.display.scrollbars.setScrollLeft(t))}function an(e,t){var n=Xo(t),r=n.x,i=n.y,o=e.display,a=o.scroller,l=a.scrollWidth>a.clientWidth,s=a.scrollHeight>a.clientHeight;if(r&&l||i&&s){if(i&&Eo&&wo)e:for(var c=t.target,u=o.view;c!=a;c=c.parentNode)for(var f=0;f<u.length;f++)if(u[f].node==c){e.display.currentWheelTarget=c;break e}if(r&&!go&&!Co&&null!=Ko)return i&&s&&rn(e,Math.max(0,Math.min(a.scrollTop+i*Ko,a.scrollHeight-a.clientHeight))),on(e,Math.max(0,Math.min(a.scrollLeft+r*Ko,a.scrollWidth-a.clientWidth))),(!i||i&&s)&&Ma(t),void(o.wheelStartX=null);if(i&&null!=Ko){var h=i*Ko,d=e.doc.scrollTop,p=d+o.wrapper.clientHeight;0>h?d=Math.max(0,d+h-50):p=Math.min(e.doc.height,p+h+50),A(e,{top:d,bottom:p})}20>Vo&&(null==o.wheelStartX?(o.wheelStartX=a.scrollLeft,o.wheelStartY=a.scrollTop,o.wheelDX=r,o.wheelDY=i,setTimeout(function(){if(null!=o.wheelStartX){var e=a.scrollLeft-o.wheelStartX,t=a.scrollTop-o.wheelStartY,n=t&&o.wheelDY&&t/o.wheelDY||e&&o.wheelDX&&e/o.wheelDX;o.wheelStartX=o.wheelStartY=null,n&&(Ko=(Ko*Vo+n)/(Vo+1),++Vo)}},200)):(o.wheelDX+=r,o.wheelDY+=i))}}function ln(e,t,n){if("string"==typeof t&&(t=ua[t],!t))return!1;e.display.input.ensurePolled();var r=e.display.shift,i=!1;try{e.isReadOnly()&&(e.state.suppressEdits=!0),n&&(e.display.shift=!1),i=t(e)!=Ha}finally{e.display.shift=r,e.state.suppressEdits=!1}return i}function sn(e,t,n){for(var r=0;r<e.state.keyMaps.length;r++){var i=ha(t,e.state.keyMaps[r],n,e);if(i)return i}return e.options.extraKeys&&ha(t,e.options.extraKeys,n,e)||ha(t,e.options.keyMap,n,e)}function cn(e,t,n,r){var i=e.state.keySeq;if(i){if(da(t))return"handled";Zo.set(50,function(){e.state.keySeq==i&&(e.state.keySeq=null,e.display.input.reset())}),t=i+" "+t}var o=sn(e,t,r);return"multi"==o&&(e.state.keySeq=t),"handled"==o&&Ci(e,"keyHandled",e,t,n),"handled"!=o&&"multi"!=o||(Ma(n),Be(e)),i&&!o&&/\'$/.test(t)?(Ma(n),!0):!!o}function un(e,t){var n=pa(t,!0);return n?t.shiftKey&&!e.state.keySeq?cn(e,"Shift-"+n,t,function(t){return ln(e,t,!0)})||cn(e,n,t,function(t){return("string"==typeof t?/^go[A-Z]/.test(t):t.motion)?ln(e,t):void 0}):cn(e,n,t,function(t){return ln(e,t)}):!1}function fn(e,t,n){return cn(e,"'"+n+"'",t,function(t){return ln(e,t,!0)})}function hn(e){var t=this;if(t.curOp.focus=Gi(),!Ti(t,e)){xo&&11>bo&&27==e.keyCode&&(e.returnValue=!1);var n=e.keyCode;t.display.shift=16==n||e.shiftKey;var r=un(t,e);Co&&(Jo=r?n:null,!r&&88==n&&!rl&&(Eo?e.metaKey:e.ctrlKey)&&t.replaceSelection("",null,"cut")),18!=n||/\bCodeMirror-crosshair\b/.test(t.display.lineDiv.className)||dn(t)}}function dn(e){function t(e){18!=e.keyCode&&e.altKey||(Za(n,"CodeMirror-crosshair"),Ia(document,"keyup",t),Ia(document,"mouseover",t))}var n=e.display.lineDiv;Ja(n,"CodeMirror-crosshair"),Ea(document,"keyup",t),Ea(document,"mouseover",t)}function pn(e){16==e.keyCode&&(this.doc.sel.shift=!1),Ti(this,e)}function mn(e){var t=this;if(!(Gt(t.display,e)||Ti(t,e)||e.ctrlKey&&!e.altKey||Eo&&e.metaKey)){var n=e.keyCode,r=e.charCode;if(Co&&n==Jo)return Jo=null,void Ma(e);if(!Co||e.which&&!(e.which<10)||!un(t,e)){var i=String.fromCharCode(null==r?n:r);fn(t,e,i)||t.display.input.onKeyPress(e)}}}function gn(e){e.state.delayingBlurEvent=!0,setTimeout(function(){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1,yn(e))},100)}function vn(e){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1),"nocursor"!=e.options.readOnly&&(e.state.focused||(Pa(e,"focus",e),e.state.focused=!0,Ja(e.display.wrapper,"CodeMirror-focused"),e.curOp||e.display.selForContextMenu==e.doc.sel||(e.display.input.reset(),wo&&setTimeout(function(){e.display.input.reset(!0)},20)),e.display.input.receivedFocus()),Be(e))}function yn(e){e.state.delayingBlurEvent||(e.state.focused&&(Pa(e,"blur",e),e.state.focused=!1,Za(e.display.wrapper,"CodeMirror-focused")),clearInterval(e.display.blinker),setTimeout(function(){e.state.focused||(e.display.shift=!1)},150))}function xn(e,t){Gt(e.display,t)||bn(e,t)||Ti(e,t,"contextmenu")||e.display.input.onContextMenu(t)}function bn(e,t){return Ni(e,"gutterContextMenu")?Zt(e,t,"gutterContextMenu",!1):!1}function wn(e,t){if(_o(e,t.from)<0)return e;if(_o(e,t.to)<=0)return Qo(t);var n=e.line+t.text.length-(t.to.line-t.from.line)-1,r=e.ch;return e.line==t.to.line&&(r+=Qo(t).ch-t.to.ch),Bo(n,r)}function kn(e,t){for(var n=[],r=0;r<e.sel.ranges.length;r++){var i=e.sel.ranges[r];n.push(new fe(wn(i.anchor,t),wn(i.head,t)))}return he(n,e.sel.primIndex)}function Sn(e,t,n){return e.line==t.line?Bo(n.line,e.ch-t.ch+n.ch):Bo(n.line+(e.line-t.line),e.ch)}function Cn(e,t,n){for(var r=[],i=Bo(e.first,0),o=i,a=0;a<t.length;a++){var l=t[a],s=Sn(l.from,i,o),c=Sn(Qo(l),i,o);if(i=l.to,o=c,"around"==n){var u=e.sel.ranges[a],f=_o(u.head,u.anchor)<0;r[a]=new fe(f?c:s,f?s:c)}else r[a]=new fe(s,s)}return new ue(r,e.sel.primIndex)}function Ln(e,t,n){var r={canceled:!1,from:t.from,to:t.to,text:t.text,origin:t.origin,cancel:function(){this.canceled=!0}};return n&&(r.update=function(t,n,r,i){t&&(this.from=me(e,t)),n&&(this.to=me(e,n)),r&&(this.text=r),void 0!==i&&(this.origin=i)}),Pa(e,"beforeChange",e,r),e.cm&&Pa(e.cm,"beforeChange",e.cm,r),r.canceled?null:{from:r.from,to:r.to,text:r.text,origin:r.origin}}function Tn(e,t,n){if(e.cm){if(!e.cm.curOp)return Et(e.cm,Tn)(e,t,n);if(e.cm.state.suppressEdits)return}if(!(Ni(e,"beforeChange")||e.cm&&Ni(e.cm,"beforeChange"))||(t=Ln(e,t,!0))){var r=Ho&&!n&&sr(e,t.from,t.to);if(r)for(var i=r.length-1;i>=0;--i)Mn(e,{from:r[i].from,to:r[i].to,text:i?[""]:t.text});else Mn(e,t)}}function Mn(e,t){if(1!=t.text.length||""!=t.text[0]||0!=_o(t.from,t.to)){var n=kn(e,t);ci(e,t,n,e.cm?e.cm.curOp.id:NaN),En(e,t,n,or(e,t));var r=[];Kr(e,function(e,n){n||-1!=Pi(r,e.history)||(xi(e.history,t),r.push(e.history)),En(e,t,null,or(e,t))})}}function Nn(e,t,n){if(!e.cm||!e.cm.state.suppressEdits){for(var r,i=e.history,o=e.sel,a="undo"==t?i.done:i.undone,l="undo"==t?i.undone:i.done,s=0;s<a.length&&(r=a[s],n?!r.ranges||r.equals(e.sel):r.ranges);s++);if(s!=a.length){for(i.lastOrigin=i.lastSelOrigin=null;r=a.pop(),r.ranges;){if(hi(r,l),n&&!r.equals(e.sel))return void Te(e,r,{clearRedo:!1});o=r}var c=[];hi(o,l),l.push({changes:c,generation:i.generation}),i.generation=r.generation||++i.maxGeneration;for(var u=Ni(e,"beforeChange")||e.cm&&Ni(e.cm,"beforeChange"),s=r.changes.length-1;s>=0;--s){var f=r.changes[s];if(f.origin=t,u&&!Ln(e,f,!1))return void(a.length=0);c.push(ai(e,f));var h=s?kn(e,f):Ii(a);En(e,f,h,lr(e,f)),!s&&e.cm&&e.cm.scrollIntoView({from:f.from,to:Qo(f)});var d=[];Kr(e,function(e,t){t||-1!=Pi(d,e.history)||(xi(e.history,f),d.push(e.history)),En(e,f,null,lr(e,f))})}}}}function An(e,t){if(0!=t&&(e.first+=t,e.sel=new ue(Ri(e.sel.ranges,function(e){return new fe(Bo(e.anchor.line+t,e.anchor.ch),Bo(e.head.line+t,e.head.ch))}),e.sel.primIndex),e.cm)){Dt(e.cm,e.first,e.first-t,t);for(var n=e.cm.display,r=n.viewFrom;r<n.viewTo;r++)Ht(e.cm,r,"gutter")}}function En(e,t,n,r){if(e.cm&&!e.cm.curOp)return Et(e.cm,En)(e,t,n,r);if(t.to.line<e.first)return void An(e,t.text.length-1-(t.to.line-t.from.line));if(!(t.from.line>e.lastLine())){if(t.from.line<e.first){var i=t.text.length-1-(e.first-t.from.line);An(e,i),t={from:Bo(e.first,0),to:Bo(t.to.line+i,t.to.ch),text:[Ii(t.text)],origin:t.origin}}var o=e.lastLine();t.to.line>o&&(t={from:t.from,to:Bo(o,Zr(e,o).text.length),text:[t.text[0]],origin:t.origin}),t.removed=Jr(e,t.from,t.to),n||(n=kn(e,t)),e.cm?On(e.cm,t,r):Yr(e,t,r),Me(e,n,Wa)}}function On(e,t,n){var r=e.doc,i=e.display,a=t.from,l=t.to,s=!1,c=a.line;e.options.lineWrapping||(c=ti(yr(Zr(r,a.line))),r.iter(c,l.line+1,function(e){return e==i.maxLine?(s=!0,!0):void 0})),r.sel.contains(t.from,t.to)>-1&&Mi(e),Yr(r,t,n,o(e)),e.options.lineWrapping||(r.iter(c,a.line+t.text.length,function(e){var t=f(e);t>i.maxLineLength&&(i.maxLine=e,i.maxLineLength=t,i.maxLineChanged=!0,s=!1)}),s&&(e.curOp.updateMaxLine=!0)),r.frontier=Math.min(r.frontier,a.line),_e(e,400);var u=t.text.length-(l.line-a.line)-1;t.full?Dt(e):a.line!=l.line||1!=t.text.length||Gr(e.doc,t)?Dt(e,a.line,l.line+1,u):Ht(e,a.line,"text");var h=Ni(e,"changes"),d=Ni(e,"change");if(d||h){var p={from:a,to:l,text:t.text,removed:t.removed,origin:t.origin};d&&Ci(e,"change",e,p),h&&(e.curOp.changeObjs||(e.curOp.changeObjs=[])).push(p)}e.display.selForContextMenu=null}function In(e,t,n,r,i){if(r||(r=n),_o(r,n)<0){var o=r;r=n,n=o}"string"==typeof t&&(t=e.splitLines(t)),Tn(e,{from:n,to:r,text:t,origin:i})}function Pn(e,t){if(!Ti(e,"scrollCursorIntoView")){var n=e.display,r=n.sizer.getBoundingClientRect(),i=null;if(t.top+r.top<0?i=!0:t.bottom+r.top>(window.innerHeight||document.documentElement.clientHeight)&&(i=!1),null!=i&&!Mo){var o=ji("div","",null,"position: absolute; top: "+(t.top-n.viewOffset-Ue(e.display))+"px; height: "+(t.bottom-t.top+Ye(e)+n.barHeight)+"px; left: "+t.left+"px; width: 2px;");e.display.lineSpace.appendChild(o),o.scrollIntoView(i),e.display.lineSpace.removeChild(o)}}}function Rn(e,t,n,r){null==r&&(r=0);for(var i=0;5>i;i++){var o=!1,a=dt(e,t),l=n&&n!=t?dt(e,n):a,s=Hn(e,Math.min(a.left,l.left),Math.min(a.top,l.top)-r,Math.max(a.left,l.left),Math.max(a.bottom,l.bottom)+r),c=e.doc.scrollTop,u=e.doc.scrollLeft;if(null!=s.scrollTop&&(rn(e,s.scrollTop),Math.abs(e.doc.scrollTop-c)>1&&(o=!0)),null!=s.scrollLeft&&(on(e,s.scrollLeft),Math.abs(e.doc.scrollLeft-u)>1&&(o=!0)),!o)break}return a}function Dn(e,t,n,r,i){var o=Hn(e,t,n,r,i);null!=o.scrollTop&&rn(e,o.scrollTop),null!=o.scrollLeft&&on(e,o.scrollLeft)}function Hn(e,t,n,r,i){var o=e.display,a=yt(e.display);0>n&&(n=0);var l=e.curOp&&null!=e.curOp.scrollTop?e.curOp.scrollTop:o.scroller.scrollTop,s=Ve(e),c={};i-n>s&&(i=n+s);var u=e.doc.height+qe(o),f=a>n,h=i>u-a;if(l>n)c.scrollTop=f?0:n;else if(i>l+s){var d=Math.min(n,(h?u:i)-s);d!=l&&(c.scrollTop=d)}var p=e.curOp&&null!=e.curOp.scrollLeft?e.curOp.scrollLeft:o.scroller.scrollLeft,m=$e(e)-(e.options.fixedGutter?o.gutters.offsetWidth:0),g=r-t>m;return g&&(r=t+m),10>t?c.scrollLeft=0:p>t?c.scrollLeft=Math.max(0,t-(g?0:10)):r>m+p-3&&(c.scrollLeft=r+(g?0:10)-m),c}function Wn(e,t,n){null==t&&null==n||_n(e),null!=t&&(e.curOp.scrollLeft=(null==e.curOp.scrollLeft?e.doc.scrollLeft:e.curOp.scrollLeft)+t),null!=n&&(e.curOp.scrollTop=(null==e.curOp.scrollTop?e.doc.scrollTop:e.curOp.scrollTop)+n)}function Bn(e){_n(e);var t=e.getCursor(),n=t,r=t;e.options.lineWrapping||(n=t.ch?Bo(t.line,t.ch-1):t,r=Bo(t.line,t.ch+1)),e.curOp.scrollToPos={from:n,to:r,margin:e.options.cursorScrollMargin,isCursor:!0}}function _n(e){var t=e.curOp.scrollToPos;if(t){e.curOp.scrollToPos=null;var n=pt(e,t.from),r=pt(e,t.to),i=Hn(e,Math.min(n.left,r.left),Math.min(n.top,r.top)-t.margin,Math.max(n.right,r.right),Math.max(n.bottom,r.bottom)+t.margin);e.scrollTo(i.scrollLeft,i.scrollTop)}}function Fn(e,t,n,r){var i,o=e.doc;null==n&&(n="add"),"smart"==n&&(o.mode.indent?i=je(e,t):n="prev");var a=e.options.tabSize,l=Zr(o,t),s=Fa(l.text,null,a);l.stateAfter&&(l.stateAfter=null);var c,u=l.text.match(/^\s*/)[0];if(r||/\S/.test(l.text)){if("smart"==n&&(c=o.mode.indent(i,l.text.slice(u.length),l.text),c==Ha||c>150)){if(!r)return;n="prev"}}else c=0,n="not";"prev"==n?c=t>o.first?Fa(Zr(o,t-1).text,null,a):0:"add"==n?c=s+e.options.indentUnit:"subtract"==n?c=s-e.options.indentUnit:"number"==typeof n&&(c=s+n),c=Math.max(0,c);var f="",h=0;if(e.options.indentWithTabs)for(var d=Math.floor(c/a);d;--d)h+=a,f+=" ";if(c>h&&(f+=Oi(c-h)),f!=u)return In(o,f,Bo(t,0),Bo(t,u.length),"+input"),l.stateAfter=null,!0;for(var d=0;d<o.sel.ranges.length;d++){var p=o.sel.ranges[d];if(p.head.line==t&&p.head.ch<u.length){var h=Bo(t,u.length);ke(o,d,new fe(h,h));break}}}function zn(e,t,n,r){var i=t,o=t;return"number"==typeof t?o=Zr(e,pe(e,t)):i=ti(t),null==i?null:(r(o,i)&&e.cm&&Ht(e.cm,i,n),o)}function jn(e,t){for(var n=e.doc.sel.ranges,r=[],i=0;i<n.length;i++){for(var o=t(n[i]);r.length&&_o(o.from,Ii(r).to)<=0;){var a=r.pop();if(_o(a.from,o.from)<0){o.from=a.from;break}}r.push(o)}At(e,function(){for(var t=r.length-1;t>=0;t--)In(e.doc,"",r[t].from,r[t].to,"+delete");Bn(e)})}function Un(e,t,n,r,i){function o(){var t=l+n;return t<e.first||t>=e.first+e.size?!1:(l=t,u=Zr(e,t))}function a(e){var t=(i?fo:ho)(u,s,n,!0);if(null==t){if(e||!o())return!1;s=i?(0>n?io:ro)(u):0>n?u.text.length:0}else s=t;return!0}var l=t.line,s=t.ch,c=n,u=Zr(e,l);if("char"==r)a();else if("column"==r)a(!0);else if("word"==r||"group"==r)for(var f=null,h="group"==r,d=e.cm&&e.cm.getHelper(t,"wordChars"),p=!0;!(0>n)||a(!p);p=!1){var m=u.text.charAt(s)||"\n",g=_i(m,d)?"w":h&&"\n"==m?"n":!h||/\s/.test(m)?null:"p";if(!h||p||g||(g="s"),f&&f!=g){0>n&&(n=1,a());break}if(g&&(f=g),n>0&&!a(!p))break}var v=Ie(e,Bo(l,s),t,c,!0);return _o(t,v)||(v.hitSide=!0),v}function qn(e,t,n,r){var i,o=e.doc,a=t.left;if("page"==r){var l=Math.min(e.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight);i=t.top+n*(l-(0>n?1.5:.5)*yt(e.display))}else"line"==r&&(i=n>0?t.bottom+3:t.top-3);for(;;){var s=gt(e,a,i);if(!s.outside)break;if(0>n?0>=i:i>=o.height){s.hitSide=!0;break}i+=5*n}return s}function Gn(t,n,r,i){e.defaults[t]=n,r&&(ta[t]=i?function(e,t,n){n!=na&&r(e,t,n)}:r)}function Yn(e){for(var t,n,r,i,o=e.split(/-(?!$)/),e=o[o.length-1],a=0;a<o.length-1;a++){var l=o[a];if(/^(cmd|meta|m)$/i.test(l))i=!0;else if(/^a(lt)?$/i.test(l))t=!0;else if(/^(c|ctrl|control)$/i.test(l))n=!0;else{ -if(!/^s(hift)$/i.test(l))throw new Error("Unrecognized modifier name: "+l);r=!0}}return t&&(e="Alt-"+e),n&&(e="Ctrl-"+e),i&&(e="Cmd-"+e),r&&(e="Shift-"+e),e}function $n(e){return"string"==typeof e?fa[e]:e}function Vn(e,t,n,r,i){if(r&&r.shared)return Kn(e,t,n,r,i);if(e.cm&&!e.cm.curOp)return Et(e.cm,Vn)(e,t,n,r,i);var o=new va(e,i),a=_o(t,n);if(r&&Wi(r,o,!1),a>0||0==a&&o.clearWhenEmpty!==!1)return o;if(o.replacedWith&&(o.collapsed=!0,o.widgetNode=ji("span",[o.replacedWith],"CodeMirror-widget"),r.handleMouseEvents||o.widgetNode.setAttribute("cm-ignore-events","true"),r.insertLeft&&(o.widgetNode.insertLeft=!0)),o.collapsed){if(vr(e,t.line,t,n,o)||t.line!=n.line&&vr(e,n.line,t,n,o))throw new Error("Inserting collapsed marker partially overlapping an existing one");Wo=!0}o.addToHistory&&ci(e,{from:t,to:n,origin:"markText"},e.sel,NaN);var l,s=t.line,c=e.cm;if(e.iter(s,n.line+1,function(e){c&&o.collapsed&&!c.options.lineWrapping&&yr(e)==c.display.maxLine&&(l=!0),o.collapsed&&s!=t.line&&ei(e,0),nr(e,new Qn(o,s==t.line?t.ch:null,s==n.line?n.ch:null)),++s}),o.collapsed&&e.iter(t.line,n.line+1,function(t){kr(e,t)&&ei(t,0)}),o.clearOnEnter&&Ea(o,"beforeCursorEnter",function(){o.clear()}),o.readOnly&&(Ho=!0,(e.history.done.length||e.history.undone.length)&&e.clearHistory()),o.collapsed&&(o.id=++ga,o.atomic=!0),c){if(l&&(c.curOp.updateMaxLine=!0),o.collapsed)Dt(c,t.line,n.line+1);else if(o.className||o.title||o.startStyle||o.endStyle||o.css)for(var u=t.line;u<=n.line;u++)Ht(c,u,"text");o.atomic&&Ae(c.doc),Ci(c,"markerAdded",c,o)}return o}function Kn(e,t,n,r,i){r=Wi(r),r.shared=!1;var o=[Vn(e,t,n,r,i)],a=o[0],l=r.widgetNode;return Kr(e,function(e){l&&(r.widgetNode=l.cloneNode(!0)),o.push(Vn(e,me(e,t),me(e,n),r,i));for(var s=0;s<e.linked.length;++s)if(e.linked[s].isParent)return;a=Ii(o)}),new ya(o,a)}function Xn(e){return e.findMarks(Bo(e.first,0),e.clipPos(Bo(e.lastLine())),function(e){return e.parent})}function Zn(e,t){for(var n=0;n<t.length;n++){var r=t[n],i=r.find(),o=e.clipPos(i.from),a=e.clipPos(i.to);if(_o(o,a)){var l=Vn(e,o,a,r.primary,r.primary.type);r.markers.push(l),l.parent=r}}}function Jn(e){for(var t=0;t<e.length;t++){var n=e[t],r=[n.primary.doc];Kr(n.primary.doc,function(e){r.push(e)});for(var i=0;i<n.markers.length;i++){var o=n.markers[i];-1==Pi(r,o.doc)&&(o.parent=null,n.markers.splice(i--,1))}}}function Qn(e,t,n){this.marker=e,this.from=t,this.to=n}function er(e,t){if(e)for(var n=0;n<e.length;++n){var r=e[n];if(r.marker==t)return r}}function tr(e,t){for(var n,r=0;r<e.length;++r)e[r]!=t&&(n||(n=[])).push(e[r]);return n}function nr(e,t){e.markedSpans=e.markedSpans?e.markedSpans.concat([t]):[t],t.marker.attachLine(e)}function rr(e,t,n){if(e)for(var r,i=0;i<e.length;++i){var o=e[i],a=o.marker,l=null==o.from||(a.inclusiveLeft?o.from<=t:o.from<t);if(l||o.from==t&&"bookmark"==a.type&&(!n||!o.marker.insertLeft)){var s=null==o.to||(a.inclusiveRight?o.to>=t:o.to>t);(r||(r=[])).push(new Qn(a,o.from,s?null:o.to))}}return r}function ir(e,t,n){if(e)for(var r,i=0;i<e.length;++i){var o=e[i],a=o.marker,l=null==o.to||(a.inclusiveRight?o.to>=t:o.to>t);if(l||o.from==t&&"bookmark"==a.type&&(!n||o.marker.insertLeft)){var s=null==o.from||(a.inclusiveLeft?o.from<=t:o.from<t);(r||(r=[])).push(new Qn(a,s?null:o.from-t,null==o.to?null:o.to-t))}}return r}function or(e,t){if(t.full)return null;var n=ve(e,t.from.line)&&Zr(e,t.from.line).markedSpans,r=ve(e,t.to.line)&&Zr(e,t.to.line).markedSpans;if(!n&&!r)return null;var i=t.from.ch,o=t.to.ch,a=0==_o(t.from,t.to),l=rr(n,i,a),s=ir(r,o,a),c=1==t.text.length,u=Ii(t.text).length+(c?i:0);if(l)for(var f=0;f<l.length;++f){var h=l[f];if(null==h.to){var d=er(s,h.marker);d?c&&(h.to=null==d.to?null:d.to+u):h.to=i}}if(s)for(var f=0;f<s.length;++f){var h=s[f];if(null!=h.to&&(h.to+=u),null==h.from){var d=er(l,h.marker);d||(h.from=u,c&&(l||(l=[])).push(h))}else h.from+=u,c&&(l||(l=[])).push(h)}l&&(l=ar(l)),s&&s!=l&&(s=ar(s));var p=[l];if(!c){var m,g=t.text.length-2;if(g>0&&l)for(var f=0;f<l.length;++f)null==l[f].to&&(m||(m=[])).push(new Qn(l[f].marker,null,null));for(var f=0;g>f;++f)p.push(m);p.push(s)}return p}function ar(e){for(var t=0;t<e.length;++t){var n=e[t];null!=n.from&&n.from==n.to&&n.marker.clearWhenEmpty!==!1&&e.splice(t--,1)}return e.length?e:null}function lr(e,t){var n=mi(e,t),r=or(e,t);if(!n)return r;if(!r)return n;for(var i=0;i<n.length;++i){var o=n[i],a=r[i];if(o&&a)e:for(var l=0;l<a.length;++l){for(var s=a[l],c=0;c<o.length;++c)if(o[c].marker==s.marker)continue e;o.push(s)}else a&&(n[i]=a)}return n}function sr(e,t,n){var r=null;if(e.iter(t.line,n.line+1,function(e){if(e.markedSpans)for(var t=0;t<e.markedSpans.length;++t){var n=e.markedSpans[t].marker;!n.readOnly||r&&-1!=Pi(r,n)||(r||(r=[])).push(n)}}),!r)return null;for(var i=[{from:t,to:n}],o=0;o<r.length;++o)for(var a=r[o],l=a.find(0),s=0;s<i.length;++s){var c=i[s];if(!(_o(c.to,l.from)<0||_o(c.from,l.to)>0)){var u=[s,1],f=_o(c.from,l.from),h=_o(c.to,l.to);(0>f||!a.inclusiveLeft&&!f)&&u.push({from:c.from,to:l.from}),(h>0||!a.inclusiveRight&&!h)&&u.push({from:l.to,to:c.to}),i.splice.apply(i,u),s+=u.length-1}}return i}function cr(e){var t=e.markedSpans;if(t){for(var n=0;n<t.length;++n)t[n].marker.detachLine(e);e.markedSpans=null}}function ur(e,t){if(t){for(var n=0;n<t.length;++n)t[n].marker.attachLine(e);e.markedSpans=t}}function fr(e){return e.inclusiveLeft?-1:0}function hr(e){return e.inclusiveRight?1:0}function dr(e,t){var n=e.lines.length-t.lines.length;if(0!=n)return n;var r=e.find(),i=t.find(),o=_o(r.from,i.from)||fr(e)-fr(t);if(o)return-o;var a=_o(r.to,i.to)||hr(e)-hr(t);return a?a:t.id-e.id}function pr(e,t){var n,r=Wo&&e.markedSpans;if(r)for(var i,o=0;o<r.length;++o)i=r[o],i.marker.collapsed&&null==(t?i.from:i.to)&&(!n||dr(n,i.marker)<0)&&(n=i.marker);return n}function mr(e){return pr(e,!0)}function gr(e){return pr(e,!1)}function vr(e,t,n,r,i){var o=Zr(e,t),a=Wo&&o.markedSpans;if(a)for(var l=0;l<a.length;++l){var s=a[l];if(s.marker.collapsed){var c=s.marker.find(0),u=_o(c.from,n)||fr(s.marker)-fr(i),f=_o(c.to,r)||hr(s.marker)-hr(i);if(!(u>=0&&0>=f||0>=u&&f>=0)&&(0>=u&&(s.marker.inclusiveRight&&i.inclusiveLeft?_o(c.to,n)>=0:_o(c.to,n)>0)||u>=0&&(s.marker.inclusiveRight&&i.inclusiveLeft?_o(c.from,r)<=0:_o(c.from,r)<0)))return!0}}}function yr(e){for(var t;t=mr(e);)e=t.find(-1,!0).line;return e}function xr(e){for(var t,n;t=gr(e);)e=t.find(1,!0).line,(n||(n=[])).push(e);return n}function br(e,t){var n=Zr(e,t),r=yr(n);return n==r?t:ti(r)}function wr(e,t){if(t>e.lastLine())return t;var n,r=Zr(e,t);if(!kr(e,r))return t;for(;n=gr(r);)r=n.find(1,!0).line;return ti(r)+1}function kr(e,t){var n=Wo&&t.markedSpans;if(n)for(var r,i=0;i<n.length;++i)if(r=n[i],r.marker.collapsed){if(null==r.from)return!0;if(!r.marker.widgetNode&&0==r.from&&r.marker.inclusiveLeft&&Sr(e,t,r))return!0}}function Sr(e,t,n){if(null==n.to){var r=n.marker.find(1,!0);return Sr(e,r.line,er(r.line.markedSpans,n.marker))}if(n.marker.inclusiveRight&&n.to==t.text.length)return!0;for(var i,o=0;o<t.markedSpans.length;++o)if(i=t.markedSpans[o],i.marker.collapsed&&!i.marker.widgetNode&&i.from==n.to&&(null==i.to||i.to!=n.from)&&(i.marker.inclusiveLeft||n.marker.inclusiveRight)&&Sr(e,t,i))return!0}function Cr(e,t,n){ri(t)<(e.curOp&&e.curOp.scrollTop||e.doc.scrollTop)&&Wn(e,null,n)}function Lr(e){if(null!=e.height)return e.height;var t=e.doc.cm;if(!t)return 0;if(!Va(document.body,e.node)){var n="position: relative;";e.coverGutter&&(n+="margin-left: -"+t.display.gutters.offsetWidth+"px;"),e.noHScroll&&(n+="width: "+t.display.wrapper.clientWidth+"px;"),qi(t.display.measure,ji("div",[e.node],null,n))}return e.height=e.node.parentNode.offsetHeight}function Tr(e,t,n,r){var i=new xa(e,n,r),o=e.cm;return o&&i.noHScroll&&(o.display.alignWidgets=!0),zn(e,t,"widget",function(t){var n=t.widgets||(t.widgets=[]);if(null==i.insertAt?n.push(i):n.splice(Math.min(n.length-1,Math.max(0,i.insertAt)),0,i),i.line=t,o&&!kr(e,t)){var r=ri(t)<e.scrollTop;ei(t,t.height+Lr(i)),r&&Wn(o,null,i.height),o.curOp.forceUpdate=!0}return!0}),i}function Mr(e,t,n,r){e.text=t,e.stateAfter&&(e.stateAfter=null),e.styles&&(e.styles=null),null!=e.order&&(e.order=null),cr(e),ur(e,n);var i=r?r(e):1;i!=e.height&&ei(e,i)}function Nr(e){e.parent=null,cr(e)}function Ar(e,t){if(e)for(;;){var n=e.match(/(?:^|\s+)line-(background-)?(\S+)/);if(!n)break;e=e.slice(0,n.index)+e.slice(n.index+n[0].length);var r=n[1]?"bgClass":"textClass";null==t[r]?t[r]=n[2]:new RegExp("(?:^|s)"+n[2]+"(?:$|s)").test(t[r])||(t[r]+=" "+n[2])}return e}function Er(t,n){if(t.blankLine)return t.blankLine(n);if(t.innerMode){var r=e.innerMode(t,n);return r.mode.blankLine?r.mode.blankLine(r.state):void 0}}function Or(t,n,r,i){for(var o=0;10>o;o++){i&&(i[0]=e.innerMode(t,r).mode);var a=t.token(n,r);if(n.pos>n.start)return a}throw new Error("Mode "+t.name+" failed to advance stream.")}function Ir(e,t,n,r){function i(e){return{start:f.start,end:f.pos,string:f.current(),type:o||null,state:e?sa(a.mode,u):u}}var o,a=e.doc,l=a.mode;t=me(a,t);var s,c=Zr(a,t.line),u=je(e,t.line,n),f=new ma(c.text,e.options.tabSize);for(r&&(s=[]);(r||f.pos<t.ch)&&!f.eol();)f.start=f.pos,o=Or(l,f,u),r&&s.push(i(!0));return r?s:i()}function Pr(e,t,n,r,i,o,a){var l=n.flattenSpans;null==l&&(l=e.options.flattenSpans);var s,c=0,u=null,f=new ma(t,e.options.tabSize),h=e.options.addModeClass&&[null];for(""==t&&Ar(Er(n,r),o);!f.eol();){if(f.pos>e.options.maxHighlightLength?(l=!1,a&&Hr(e,t,r,f.pos),f.pos=t.length,s=null):s=Ar(Or(n,f,r,h),o),h){var d=h[0].name;d&&(s="m-"+(s?d+" "+s:d))}if(!l||u!=s){for(;c<f.start;)c=Math.min(f.start,c+5e4),i(c,u);u=s}f.start=f.pos}for(;c<f.pos;){var p=Math.min(f.pos,c+5e4);i(p,u),c=p}}function Rr(e,t,n,r){var i=[e.state.modeGen],o={};Pr(e,t.text,e.doc.mode,n,function(e,t){i.push(e,t)},o,r);for(var a=0;a<e.state.overlays.length;++a){var l=e.state.overlays[a],s=1,c=0;Pr(e,t.text,l.mode,!0,function(e,t){for(var n=s;e>c;){var r=i[s];r>e&&i.splice(s,1,e,i[s+1],r),s+=2,c=Math.min(e,r)}if(t)if(l.opaque)i.splice(n,s-n,e,"cm-overlay "+t),s=n+2;else for(;s>n;n+=2){var o=i[n+1];i[n+1]=(o?o+" ":"")+"cm-overlay "+t}},o)}return{styles:i,classes:o.bgClass||o.textClass?o:null}}function Dr(e,t,n){if(!t.styles||t.styles[0]!=e.state.modeGen){var r=je(e,ti(t)),i=Rr(e,t,t.text.length>e.options.maxHighlightLength?sa(e.doc.mode,r):r);t.stateAfter=r,t.styles=i.styles,i.classes?t.styleClasses=i.classes:t.styleClasses&&(t.styleClasses=null),n===e.doc.frontier&&e.doc.frontier++}return t.styles}function Hr(e,t,n,r){var i=e.doc.mode,o=new ma(t,e.options.tabSize);for(o.start=o.pos=r||0,""==t&&Er(i,n);!o.eol();)Or(i,o,n),o.start=o.pos}function Wr(e,t){if(!e||/^\s*$/.test(e))return null;var n=t.addModeClass?ka:wa;return n[e]||(n[e]=e.replace(/\S+/g,"cm-$&"))}function Br(e,t){var n=ji("span",null,null,wo?"padding-right: .1px":null),r={pre:ji("pre",[n],"CodeMirror-line"),content:n,col:0,pos:0,cm:e,splitSpaces:(xo||wo)&&e.getOption("lineWrapping")};t.measure={};for(var i=0;i<=(t.rest?t.rest.length:0);i++){var o,a=i?t.rest[i-1]:t.line;r.pos=0,r.addToken=Fr,Ji(e.display.measure)&&(o=ii(a))&&(r.addToken=jr(r.addToken,o)),r.map=[];var l=t!=e.display.externalMeasured&&ti(a);qr(a,r,Dr(e,a,l)),a.styleClasses&&(a.styleClasses.bgClass&&(r.bgClass=$i(a.styleClasses.bgClass,r.bgClass||"")),a.styleClasses.textClass&&(r.textClass=$i(a.styleClasses.textClass,r.textClass||""))),0==r.map.length&&r.map.push(0,0,r.content.appendChild(Zi(e.display.measure))),0==i?(t.measure.map=r.map,t.measure.cache={}):((t.measure.maps||(t.measure.maps=[])).push(r.map),(t.measure.caches||(t.measure.caches=[])).push({}))}if(wo){var s=r.content.lastChild;(/\bcm-tab\b/.test(s.className)||s.querySelector&&s.querySelector(".cm-tab"))&&(r.content.className="cm-tab-wrap-hack")}return Pa(e,"renderLine",e,t.line,r.pre),r.pre.className&&(r.textClass=$i(r.pre.className,r.textClass||"")),r}function _r(e){var t=ji("span","•","cm-invalidchar");return t.title="\\u"+e.charCodeAt(0).toString(16),t.setAttribute("aria-label",t.title),t}function Fr(e,t,n,r,i,o,a){if(t){var l=e.splitSpaces?t.replace(/ {3,}/g,zr):t,s=e.cm.state.specialChars,c=!1;if(s.test(t))for(var u=document.createDocumentFragment(),f=0;;){s.lastIndex=f;var h=s.exec(t),d=h?h.index-f:t.length-f;if(d){var p=document.createTextNode(l.slice(f,f+d));xo&&9>bo?u.appendChild(ji("span",[p])):u.appendChild(p),e.map.push(e.pos,e.pos+d,p),e.col+=d,e.pos+=d}if(!h)break;if(f+=d+1," "==h[0]){var m=e.cm.options.tabSize,g=m-e.col%m,p=u.appendChild(ji("span",Oi(g),"cm-tab"));p.setAttribute("role","presentation"),p.setAttribute("cm-text"," "),e.col+=g}else if("\r"==h[0]||"\n"==h[0]){var p=u.appendChild(ji("span","\r"==h[0]?"␍":"","cm-invalidchar"));p.setAttribute("cm-text",h[0]),e.col+=1}else{var p=e.cm.options.specialCharPlaceholder(h[0]);p.setAttribute("cm-text",h[0]),xo&&9>bo?u.appendChild(ji("span",[p])):u.appendChild(p),e.col+=1}e.map.push(e.pos,e.pos+1,p),e.pos++}else{e.col+=t.length;var u=document.createTextNode(l);e.map.push(e.pos,e.pos+t.length,u),xo&&9>bo&&(c=!0),e.pos+=t.length}if(n||r||i||c||a){var v=n||"";r&&(v+=r),i&&(v+=i);var y=ji("span",[u],v,a);return o&&(y.title=o),e.content.appendChild(y)}e.content.appendChild(u)}}function zr(e){for(var t=" ",n=0;n<e.length-2;++n)t+=n%2?" ":" ";return t+=" "}function jr(e,t){return function(n,r,i,o,a,l,s){i=i?i+" cm-force-border":"cm-force-border";for(var c=n.pos,u=c+r.length;;){for(var f=0;f<t.length;f++){var h=t[f];if(h.to>c&&h.from<=c)break}if(h.to>=u)return e(n,r,i,o,a,l,s);e(n,r.slice(0,h.to-c),i,o,null,l,s),o=null,r=r.slice(h.to-c),c=h.to}}}function Ur(e,t,n,r){var i=!r&&n.widgetNode;i&&e.map.push(e.pos,e.pos+t,i),!r&&e.cm.display.input.needsContentAttribute&&(i||(i=e.content.appendChild(document.createElement("span"))),i.setAttribute("cm-marker",n.id)),i&&(e.cm.display.input.setUneditable(i),e.content.appendChild(i)),e.pos+=t}function qr(e,t,n){var r=e.markedSpans,i=e.text,o=0;if(r)for(var a,l,s,c,u,f,h,d=i.length,p=0,m=1,g="",v=0;;){if(v==p){s=c=u=f=l="",h=null,v=1/0;for(var y,x=[],b=0;b<r.length;++b){var w=r[b],k=w.marker;"bookmark"==k.type&&w.from==p&&k.widgetNode?x.push(k):w.from<=p&&(null==w.to||w.to>p||k.collapsed&&w.to==p&&w.from==p)?(null!=w.to&&w.to!=p&&v>w.to&&(v=w.to,c=""),k.className&&(s+=" "+k.className),k.css&&(l=(l?l+";":"")+k.css),k.startStyle&&w.from==p&&(u+=" "+k.startStyle),k.endStyle&&w.to==v&&(y||(y=[])).push(k.endStyle,w.to),k.title&&!f&&(f=k.title),k.collapsed&&(!h||dr(h.marker,k)<0)&&(h=w)):w.from>p&&v>w.from&&(v=w.from)}if(y)for(var b=0;b<y.length;b+=2)y[b+1]==v&&(c+=" "+y[b]);if(!h||h.from==p)for(var b=0;b<x.length;++b)Ur(t,0,x[b]);if(h&&(h.from||0)==p){if(Ur(t,(null==h.to?d+1:h.to)-p,h.marker,null==h.from),null==h.to)return;h.to==p&&(h=!1)}}if(p>=d)break;for(var S=Math.min(d,v);;){if(g){var C=p+g.length;if(!h){var L=C>S?g.slice(0,S-p):g;t.addToken(t,L,a?a+s:s,u,p+L.length==v?c:"",f,l)}if(C>=S){g=g.slice(S-p),p=S;break}p=C,u=""}g=i.slice(o,o=n[m++]),a=Wr(n[m++],t.cm.options)}}else for(var m=1;m<n.length;m+=2)t.addToken(t,i.slice(o,o=n[m]),Wr(n[m+1],t.cm.options))}function Gr(e,t){return 0==t.from.ch&&0==t.to.ch&&""==Ii(t.text)&&(!e.cm||e.cm.options.wholeLineUpdateBefore)}function Yr(e,t,n,r){function i(e){return n?n[e]:null}function o(e,n,i){Mr(e,n,i,r),Ci(e,"change",e,t)}function a(e,t){for(var n=e,o=[];t>n;++n)o.push(new ba(c[n],i(n),r));return o}var l=t.from,s=t.to,c=t.text,u=Zr(e,l.line),f=Zr(e,s.line),h=Ii(c),d=i(c.length-1),p=s.line-l.line;if(t.full)e.insert(0,a(0,c.length)),e.remove(c.length,e.size-c.length);else if(Gr(e,t)){var m=a(0,c.length-1);o(f,f.text,d),p&&e.remove(l.line,p),m.length&&e.insert(l.line,m)}else if(u==f)if(1==c.length)o(u,u.text.slice(0,l.ch)+h+u.text.slice(s.ch),d);else{var m=a(1,c.length-1);m.push(new ba(h+u.text.slice(s.ch),d,r)),o(u,u.text.slice(0,l.ch)+c[0],i(0)),e.insert(l.line+1,m)}else if(1==c.length)o(u,u.text.slice(0,l.ch)+c[0]+f.text.slice(s.ch),i(0)),e.remove(l.line+1,p);else{o(u,u.text.slice(0,l.ch)+c[0],i(0)),o(f,h+f.text.slice(s.ch),d);var m=a(1,c.length-1);p>1&&e.remove(l.line+1,p-1),e.insert(l.line+1,m)}Ci(e,"change",e,t)}function $r(e){this.lines=e,this.parent=null;for(var t=0,n=0;t<e.length;++t)e[t].parent=this,n+=e[t].height;this.height=n}function Vr(e){this.children=e;for(var t=0,n=0,r=0;r<e.length;++r){var i=e[r];t+=i.chunkSize(),n+=i.height,i.parent=this}this.size=t,this.height=n,this.parent=null}function Kr(e,t,n){function r(e,i,o){if(e.linked)for(var a=0;a<e.linked.length;++a){var l=e.linked[a];if(l.doc!=i){var s=o&&l.sharedHist;n&&!s||(t(l.doc,s),r(l.doc,e,s))}}}r(e,null,!0)}function Xr(e,t){if(t.cm)throw new Error("This document is already in use.");e.doc=t,t.cm=e,a(e),n(e),e.options.lineWrapping||h(e),e.options.mode=t.modeOption,Dt(e)}function Zr(e,t){if(t-=e.first,0>t||t>=e.size)throw new Error("There is no line "+(t+e.first)+" in the document.");for(var n=e;!n.lines;)for(var r=0;;++r){var i=n.children[r],o=i.chunkSize();if(o>t){n=i;break}t-=o}return n.lines[t]}function Jr(e,t,n){var r=[],i=t.line;return e.iter(t.line,n.line+1,function(e){var o=e.text;i==n.line&&(o=o.slice(0,n.ch)),i==t.line&&(o=o.slice(t.ch)),r.push(o),++i}),r}function Qr(e,t,n){var r=[];return e.iter(t,n,function(e){r.push(e.text)}),r}function ei(e,t){var n=t-e.height;if(n)for(var r=e;r;r=r.parent)r.height+=n}function ti(e){if(null==e.parent)return null;for(var t=e.parent,n=Pi(t.lines,e),r=t.parent;r;t=r,r=r.parent)for(var i=0;r.children[i]!=t;++i)n+=r.children[i].chunkSize();return n+t.first}function ni(e,t){var n=e.first;e:do{for(var r=0;r<e.children.length;++r){var i=e.children[r],o=i.height;if(o>t){e=i;continue e}t-=o,n+=i.chunkSize()}return n}while(!e.lines);for(var r=0;r<e.lines.length;++r){var a=e.lines[r],l=a.height;if(l>t)break;t-=l}return n+r}function ri(e){e=yr(e);for(var t=0,n=e.parent,r=0;r<n.lines.length;++r){var i=n.lines[r];if(i==e)break;t+=i.height}for(var o=n.parent;o;n=o,o=n.parent)for(var r=0;r<o.children.length;++r){var a=o.children[r];if(a==n)break;t+=a.height}return t}function ii(e){var t=e.order;return null==t&&(t=e.order=ll(e.text)),t}function oi(e){this.done=[],this.undone=[],this.undoDepth=1/0,this.lastModTime=this.lastSelTime=0,this.lastOp=this.lastSelOp=null,this.lastOrigin=this.lastSelOrigin=null,this.generation=this.maxGeneration=e||1}function ai(e,t){var n={from:$(t.from),to:Qo(t),text:Jr(e,t.from,t.to)};return di(e,n,t.from.line,t.to.line+1),Kr(e,function(e){di(e,n,t.from.line,t.to.line+1)},!0),n}function li(e){for(;e.length;){var t=Ii(e);if(!t.ranges)break;e.pop()}}function si(e,t){return t?(li(e.done),Ii(e.done)):e.done.length&&!Ii(e.done).ranges?Ii(e.done):e.done.length>1&&!e.done[e.done.length-2].ranges?(e.done.pop(),Ii(e.done)):void 0}function ci(e,t,n,r){var i=e.history;i.undone.length=0;var o,a=+new Date;if((i.lastOp==r||i.lastOrigin==t.origin&&t.origin&&("+"==t.origin.charAt(0)&&e.cm&&i.lastModTime>a-e.cm.options.historyEventDelay||"*"==t.origin.charAt(0)))&&(o=si(i,i.lastOp==r))){var l=Ii(o.changes);0==_o(t.from,t.to)&&0==_o(t.from,l.to)?l.to=Qo(t):o.changes.push(ai(e,t))}else{var s=Ii(i.done);for(s&&s.ranges||hi(e.sel,i.done),o={changes:[ai(e,t)],generation:i.generation},i.done.push(o);i.done.length>i.undoDepth;)i.done.shift(),i.done[0].ranges||i.done.shift()}i.done.push(n),i.generation=++i.maxGeneration,i.lastModTime=i.lastSelTime=a,i.lastOp=i.lastSelOp=r,i.lastOrigin=i.lastSelOrigin=t.origin,l||Pa(e,"historyAdded")}function ui(e,t,n,r){var i=t.charAt(0);return"*"==i||"+"==i&&n.ranges.length==r.ranges.length&&n.somethingSelected()==r.somethingSelected()&&new Date-e.history.lastSelTime<=(e.cm?e.cm.options.historyEventDelay:500)}function fi(e,t,n,r){var i=e.history,o=r&&r.origin;n==i.lastSelOp||o&&i.lastSelOrigin==o&&(i.lastModTime==i.lastSelTime&&i.lastOrigin==o||ui(e,o,Ii(i.done),t))?i.done[i.done.length-1]=t:hi(t,i.done),i.lastSelTime=+new Date,i.lastSelOrigin=o,i.lastSelOp=n,r&&r.clearRedo!==!1&&li(i.undone)}function hi(e,t){var n=Ii(t);n&&n.ranges&&n.equals(e)||t.push(e)}function di(e,t,n,r){var i=t["spans_"+e.id],o=0;e.iter(Math.max(e.first,n),Math.min(e.first+e.size,r),function(n){n.markedSpans&&((i||(i=t["spans_"+e.id]={}))[o]=n.markedSpans),++o})}function pi(e){if(!e)return null;for(var t,n=0;n<e.length;++n)e[n].marker.explicitlyCleared?t||(t=e.slice(0,n)):t&&t.push(e[n]);return t?t.length?t:null:e}function mi(e,t){var n=t["spans_"+e.id];if(!n)return null;for(var r=0,i=[];r<t.text.length;++r)i.push(pi(n[r]));return i}function gi(e,t,n){for(var r=0,i=[];r<e.length;++r){var o=e[r];if(o.ranges)i.push(n?ue.prototype.deepCopy.call(o):o);else{var a=o.changes,l=[];i.push({changes:l});for(var s=0;s<a.length;++s){var c,u=a[s];if(l.push({from:u.from,to:u.to,text:u.text}),t)for(var f in u)(c=f.match(/^spans_(\d+)$/))&&Pi(t,Number(c[1]))>-1&&(Ii(l)[f]=u[f],delete u[f])}}}return i}function vi(e,t,n,r){n<e.line?e.line+=r:t<e.line&&(e.line=t,e.ch=0)}function yi(e,t,n,r){for(var i=0;i<e.length;++i){var o=e[i],a=!0;if(o.ranges){o.copied||(o=e[i]=o.deepCopy(),o.copied=!0);for(var l=0;l<o.ranges.length;l++)vi(o.ranges[l].anchor,t,n,r),vi(o.ranges[l].head,t,n,r)}else{for(var l=0;l<o.changes.length;++l){var s=o.changes[l];if(n<s.from.line)s.from=Bo(s.from.line+r,s.from.ch),s.to=Bo(s.to.line+r,s.to.ch);else if(t<=s.to.line){a=!1;break}}a||(e.splice(0,i+1),i=0)}}}function xi(e,t){var n=t.from.line,r=t.to.line,i=t.text.length-(r-n)-1;yi(e.done,n,r,i),yi(e.undone,n,r,i)}function bi(e){return null!=e.defaultPrevented?e.defaultPrevented:0==e.returnValue}function wi(e){return e.target||e.srcElement}function ki(e){var t=e.which;return null==t&&(1&e.button?t=1:2&e.button?t=3:4&e.button&&(t=2)),Eo&&e.ctrlKey&&1==t&&(t=3),t}function Si(e,t,n){var r=e._handlers&&e._handlers[t];return n?r&&r.length>0?r.slice():Oa:r||Oa}function Ci(e,t){function n(e){return function(){e.apply(null,o)}}var r=Si(e,t,!1);if(r.length){var i,o=Array.prototype.slice.call(arguments,2);Go?i=Go.delayedCallbacks:Ra?i=Ra:(i=Ra=[],setTimeout(Li,0));for(var a=0;a<r.length;++a)i.push(n(r[a]))}}function Li(){var e=Ra;Ra=null;for(var t=0;t<e.length;++t)e[t]()}function Ti(e,t,n){return"string"==typeof t&&(t={type:t,preventDefault:function(){this.defaultPrevented=!0}}),Pa(e,n||t.type,e,t),bi(t)||t.codemirrorIgnore}function Mi(e){var t=e._handlers&&e._handlers.cursorActivity;if(t)for(var n=e.curOp.cursorActivityHandlers||(e.curOp.cursorActivityHandlers=[]),r=0;r<t.length;++r)-1==Pi(n,t[r])&&n.push(t[r])}function Ni(e,t){return Si(e,t).length>0}function Ai(e){e.prototype.on=function(e,t){Ea(this,e,t)},e.prototype.off=function(e,t){Ia(this,e,t)}}function Ei(){this.id=null}function Oi(e){for(;ja.length<=e;)ja.push(Ii(ja)+" ");return ja[e]}function Ii(e){return e[e.length-1]}function Pi(e,t){for(var n=0;n<e.length;++n)if(e[n]==t)return n;return-1}function Ri(e,t){for(var n=[],r=0;r<e.length;r++)n[r]=t(e[r],r);return n}function Di(){}function Hi(e,t){var n;return Object.create?n=Object.create(e):(Di.prototype=e,n=new Di),t&&Wi(t,n),n}function Wi(e,t,n){t||(t={});for(var r in e)!e.hasOwnProperty(r)||n===!1&&t.hasOwnProperty(r)||(t[r]=e[r]);return t}function Bi(e){var t=Array.prototype.slice.call(arguments,1);return function(){return e.apply(null,t)}}function _i(e,t){return t?t.source.indexOf("\\w")>-1&&Ya(e)?!0:t.test(e):Ya(e)}function Fi(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t])return!1;return!0}function zi(e){return e.charCodeAt(0)>=768&&$a.test(e)}function ji(e,t,n,r){var i=document.createElement(e);if(n&&(i.className=n),r&&(i.style.cssText=r),"string"==typeof t)i.appendChild(document.createTextNode(t));else if(t)for(var o=0;o<t.length;++o)i.appendChild(t[o]);return i}function Ui(e){for(var t=e.childNodes.length;t>0;--t)e.removeChild(e.firstChild);return e}function qi(e,t){return Ui(e).appendChild(t)}function Gi(){for(var e=document.activeElement;e&&e.root&&e.root.activeElement;)e=e.root.activeElement;return e}function Yi(e){return new RegExp("(^|\\s)"+e+"(?:$|\\s)\\s*")}function $i(e,t){for(var n=e.split(" "),r=0;r<n.length;r++)n[r]&&!Yi(n[r]).test(t)&&(t+=" "+n[r]);return t}function Vi(e){if(document.body.getElementsByClassName)for(var t=document.body.getElementsByClassName("CodeMirror"),n=0;n<t.length;n++){var r=t[n].CodeMirror;r&&e(r)}}function Ki(){Qa||(Xi(),Qa=!0)}function Xi(){var e;Ea(window,"resize",function(){null==e&&(e=setTimeout(function(){e=null,Vi(qt)},100))}),Ea(window,"blur",function(){Vi(yn)})}function Zi(e){if(null==Ka){var t=ji("span","");qi(e,ji("span",[t,document.createTextNode("x")])),0!=e.firstChild.offsetHeight&&(Ka=t.offsetWidth<=1&&t.offsetHeight>2&&!(xo&&8>bo))}var n=Ka?ji("span",""):ji("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");return n.setAttribute("cm-text",""),n}function Ji(e){if(null!=Xa)return Xa;var t=qi(e,document.createTextNode("AخA")),n=qa(t,0,1).getBoundingClientRect();if(!n||n.left==n.right)return!1;var r=qa(t,1,2).getBoundingClientRect();return Xa=r.right-n.right<3}function Qi(e){if(null!=il)return il;var t=qi(e,ji("span","x")),n=t.getBoundingClientRect(),r=qa(t,0,1).getBoundingClientRect();return il=Math.abs(n.left-r.left)>1}function eo(e,t,n,r){if(!e)return r(t,n,"ltr");for(var i=!1,o=0;o<e.length;++o){var a=e[o];(a.from<n&&a.to>t||t==n&&a.to==t)&&(r(Math.max(a.from,t),Math.min(a.to,n),1==a.level?"rtl":"ltr"),i=!0)}i||r(t,n,"ltr")}function to(e){return e.level%2?e.to:e.from}function no(e){return e.level%2?e.from:e.to}function ro(e){var t=ii(e);return t?to(t[0]):0}function io(e){var t=ii(e);return t?no(Ii(t)):e.text.length}function oo(e,t){var n=Zr(e.doc,t),r=yr(n);r!=n&&(t=ti(r));var i=ii(r),o=i?i[0].level%2?io(r):ro(r):0;return Bo(t,o)}function ao(e,t){for(var n,r=Zr(e.doc,t);n=gr(r);)r=n.find(1,!0).line,t=null;var i=ii(r),o=i?i[0].level%2?ro(r):io(r):r.text.length;return Bo(null==t?ti(r):t,o)}function lo(e,t){var n=oo(e,t.line),r=Zr(e.doc,n.line),i=ii(r);if(!i||0==i[0].level){var o=Math.max(0,r.text.search(/\S/)),a=t.line==n.line&&t.ch<=o&&t.ch;return Bo(n.line,a?0:o)}return n}function so(e,t,n){var r=e[0].level;return t==r?!0:n==r?!1:n>t}function co(e,t){al=null;for(var n,r=0;r<e.length;++r){var i=e[r];if(i.from<t&&i.to>t)return r;if(i.from==t||i.to==t){if(null!=n)return so(e,i.level,e[n].level)?(i.from!=i.to&&(al=n),r):(i.from!=i.to&&(al=r),n);n=r}}return n}function uo(e,t,n,r){if(!r)return t+n;do t+=n;while(t>0&&zi(e.text.charAt(t)));return t}function fo(e,t,n,r){var i=ii(e);if(!i)return ho(e,t,n,r);for(var o=co(i,t),a=i[o],l=uo(e,t,a.level%2?-n:n,r);;){if(l>a.from&&l<a.to)return l;if(l==a.from||l==a.to)return co(i,l)==o?l:(a=i[o+=n],n>0==a.level%2?a.to:a.from);if(a=i[o+=n],!a)return null;l=n>0==a.level%2?uo(e,a.to,-1,r):uo(e,a.from,1,r)}}function ho(e,t,n,r){var i=t+n;if(r)for(;i>0&&zi(e.text.charAt(i));)i+=n;return 0>i||i>e.text.length?null:i}var po=navigator.userAgent,mo=navigator.platform,go=/gecko\/\d/i.test(po),vo=/MSIE \d/.test(po),yo=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(po),xo=vo||yo,bo=xo&&(vo?document.documentMode||6:yo[1]),wo=/WebKit\//.test(po),ko=wo&&/Qt\/\d+\.\d+/.test(po),So=/Chrome\//.test(po),Co=/Opera\//.test(po),Lo=/Apple Computer/.test(navigator.vendor),To=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(po),Mo=/PhantomJS/.test(po),No=/AppleWebKit/.test(po)&&/Mobile\/\w+/.test(po),Ao=No||/Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(po),Eo=No||/Mac/.test(mo),Oo=/\bCrOS\b/.test(po),Io=/win/i.test(mo),Po=Co&&po.match(/Version\/(\d*\.\d*)/);Po&&(Po=Number(Po[1])),Po&&Po>=15&&(Co=!1,wo=!0);var Ro=Eo&&(ko||Co&&(null==Po||12.11>Po)),Do=go||xo&&bo>=9,Ho=!1,Wo=!1;m.prototype=Wi({update:function(e){var t=e.scrollWidth>e.clientWidth+1,n=e.scrollHeight>e.clientHeight+1,r=e.nativeBarWidth;if(n){this.vert.style.display="block",this.vert.style.bottom=t?r+"px":"0";var i=e.viewHeight-(t?r:0);this.vert.firstChild.style.height=Math.max(0,e.scrollHeight-e.clientHeight+i)+"px"}else this.vert.style.display="",this.vert.firstChild.style.height="0";if(t){this.horiz.style.display="block",this.horiz.style.right=n?r+"px":"0",this.horiz.style.left=e.barLeft+"px";var o=e.viewWidth-e.barLeft-(n?r:0);this.horiz.firstChild.style.width=e.scrollWidth-e.clientWidth+o+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&e.clientHeight>0&&(0==r&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:n?r:0,bottom:t?r:0}},setScrollLeft:function(e){this.horiz.scrollLeft!=e&&(this.horiz.scrollLeft=e),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz)},setScrollTop:function(e){this.vert.scrollTop!=e&&(this.vert.scrollTop=e),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert)},zeroWidthHack:function(){var e=Eo&&!To?"12px":"18px";this.horiz.style.height=this.vert.style.width=e,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new Ei,this.disableVert=new Ei},enableZeroWidthBar:function(e,t){function n(){var r=e.getBoundingClientRect(),i=document.elementFromPoint(r.left+1,r.bottom-1);i!=e?e.style.pointerEvents="none":t.set(1e3,n)}e.style.pointerEvents="auto",t.set(1e3,n)},clear:function(){var e=this.horiz.parentNode;e.removeChild(this.horiz),e.removeChild(this.vert)}},m.prototype),g.prototype=Wi({update:function(){return{bottom:0,right:0}},setScrollLeft:function(){},setScrollTop:function(){},clear:function(){}},g.prototype),e.scrollbarModel={"native":m,"null":g},L.prototype.signal=function(e,t){Ni(e,t)&&this.events.push(arguments)},L.prototype.finish=function(){for(var e=0;e<this.events.length;e++)Pa.apply(null,this.events[e])};var Bo=e.Pos=function(e,t){return this instanceof Bo?(this.line=e,void(this.ch=t)):new Bo(e,t)},_o=e.cmpPos=function(e,t){return e.line-t.line||e.ch-t.ch},Fo=null;ne.prototype=Wi({init:function(e){function t(e){if(!Ti(r,e)){if(r.somethingSelected())Fo={lineWise:!1,text:r.getSelections()},n.inaccurateSelection&&(n.prevInput="",n.inaccurateSelection=!1,o.value=Fo.text.join("\n"),Ua(o));else{if(!r.options.lineWiseCopyCut)return;var t=ee(r);Fo={lineWise:!0,text:t.text},"cut"==e.type?r.setSelections(t.ranges,null,Wa):(n.prevInput="",o.value=t.text.join("\n"),Ua(o))}"cut"==e.type&&(r.state.cutIncoming=!0)}}var n=this,r=this.cm,i=this.wrapper=re(),o=this.textarea=i.firstChild;e.wrapper.insertBefore(i,e.wrapper.firstChild),No&&(o.style.width="0px"),Ea(o,"input",function(){xo&&bo>=9&&n.hasSelection&&(n.hasSelection=null),n.poll()}),Ea(o,"paste",function(e){Ti(r,e)||J(e,r)||(r.state.pasteIncoming=!0,n.fastPoll())}),Ea(o,"cut",t),Ea(o,"copy",t),Ea(e.scroller,"paste",function(t){Gt(e,t)||Ti(r,t)||(r.state.pasteIncoming=!0,n.focus())}),Ea(e.lineSpace,"selectstart",function(t){Gt(e,t)||Ma(t)}),Ea(o,"compositionstart",function(){var e=r.getCursor("from");n.composing&&n.composing.range.clear(),n.composing={start:e,range:r.markText(e,r.getCursor("to"),{className:"CodeMirror-composing"})}}),Ea(o,"compositionend",function(){n.composing&&(n.poll(),n.composing.range.clear(),n.composing=null)})},prepareSelection:function(){var e=this.cm,t=e.display,n=e.doc,r=De(e);if(e.options.moveInputWithCursor){var i=dt(e,n.sel.primary().head,"div"),o=t.wrapper.getBoundingClientRect(),a=t.lineDiv.getBoundingClientRect();r.teTop=Math.max(0,Math.min(t.wrapper.clientHeight-10,i.top+a.top-o.top)),r.teLeft=Math.max(0,Math.min(t.wrapper.clientWidth-10,i.left+a.left-o.left))}return r},showSelection:function(e){var t=this.cm,n=t.display;qi(n.cursorDiv,e.cursors),qi(n.selectionDiv,e.selection),null!=e.teTop&&(this.wrapper.style.top=e.teTop+"px",this.wrapper.style.left=e.teLeft+"px")},reset:function(e){if(!this.contextMenuPending){var t,n,r=this.cm,i=r.doc;if(r.somethingSelected()){this.prevInput="";var o=i.sel.primary();t=rl&&(o.to().line-o.from().line>100||(n=r.getSelection()).length>1e3);var a=t?"-":n||r.getSelection();this.textarea.value=a,r.state.focused&&Ua(this.textarea),xo&&bo>=9&&(this.hasSelection=a)}else e||(this.prevInput=this.textarea.value="",xo&&bo>=9&&(this.hasSelection=null));this.inaccurateSelection=t}},getField:function(){return this.textarea},supportsTouch:function(){return!1},focus:function(){if("nocursor"!=this.cm.options.readOnly&&(!Ao||Gi()!=this.textarea))try{this.textarea.focus()}catch(e){}},blur:function(){this.textarea.blur()},resetPosition:function(){this.wrapper.style.top=this.wrapper.style.left=0; -},receivedFocus:function(){this.slowPoll()},slowPoll:function(){var e=this;e.pollingFast||e.polling.set(this.cm.options.pollInterval,function(){e.poll(),e.cm.state.focused&&e.slowPoll()})},fastPoll:function(){function e(){var r=n.poll();r||t?(n.pollingFast=!1,n.slowPoll()):(t=!0,n.polling.set(60,e))}var t=!1,n=this;n.pollingFast=!0,n.polling.set(20,e)},poll:function(){var e=this.cm,t=this.textarea,n=this.prevInput;if(this.contextMenuPending||!e.state.focused||nl(t)&&!n&&!this.composing||e.isReadOnly()||e.options.disableInput||e.state.keySeq)return!1;var r=t.value;if(r==n&&!e.somethingSelected())return!1;if(xo&&bo>=9&&this.hasSelection===r||Eo&&/[\uf700-\uf7ff]/.test(r))return e.display.input.reset(),!1;if(e.doc.sel==e.display.selForContextMenu){var i=r.charCodeAt(0);if(8203!=i||n||(n=""),8666==i)return this.reset(),this.cm.execCommand("undo")}for(var o=0,a=Math.min(n.length,r.length);a>o&&n.charCodeAt(o)==r.charCodeAt(o);)++o;var l=this;return At(e,function(){Z(e,r.slice(o),n.length-o,null,l.composing?"*compose":null),r.length>1e3||r.indexOf("\n")>-1?t.value=l.prevInput="":l.prevInput=r,l.composing&&(l.composing.range.clear(),l.composing.range=e.markText(l.composing.start,e.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},ensurePolled:function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},onKeyPress:function(){xo&&bo>=9&&(this.hasSelection=null),this.fastPoll()},onContextMenu:function(e){function t(){if(null!=a.selectionStart){var e=i.somethingSelected(),t=""+(e?a.value:"");a.value="⇚",a.value=t,r.prevInput=e?"":"",a.selectionStart=1,a.selectionEnd=t.length,o.selForContextMenu=i.doc.sel}}function n(){if(r.contextMenuPending=!1,r.wrapper.style.cssText=f,a.style.cssText=u,xo&&9>bo&&o.scrollbars.setScrollTop(o.scroller.scrollTop=s),null!=a.selectionStart){(!xo||xo&&9>bo)&&t();var e=0,n=function(){o.selForContextMenu==i.doc.sel&&0==a.selectionStart&&a.selectionEnd>0&&""==r.prevInput?Et(i,ua.selectAll)(i):e++<10?o.detectingSelectAll=setTimeout(n,500):o.input.reset()};o.detectingSelectAll=setTimeout(n,200)}}var r=this,i=r.cm,o=i.display,a=r.textarea,l=Yt(i,e),s=o.scroller.scrollTop;if(l&&!Co){var c=i.options.resetSelectionOnContextMenu;c&&-1==i.doc.sel.contains(l)&&Et(i,Te)(i.doc,de(l),Wa);var u=a.style.cssText,f=r.wrapper.style.cssText;r.wrapper.style.cssText="position: absolute";var h=r.wrapper.getBoundingClientRect();if(a.style.cssText="position: absolute; width: 30px; height: 30px; top: "+(e.clientY-h.top-5)+"px; left: "+(e.clientX-h.left-5)+"px; z-index: 1000; background: "+(xo?"rgba(255, 255, 255, .05)":"transparent")+"; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);",wo)var d=window.scrollY;if(o.input.focus(),wo&&window.scrollTo(null,d),o.input.reset(),i.somethingSelected()||(a.value=r.prevInput=" "),r.contextMenuPending=!0,o.selForContextMenu=i.doc.sel,clearTimeout(o.detectingSelectAll),xo&&bo>=9&&t(),Do){Aa(e);var p=function(){Ia(window,"mouseup",p),setTimeout(n,20)};Ea(window,"mouseup",p)}else setTimeout(n,50)}},readOnlyChanged:function(e){e||this.reset()},setUneditable:Di,needsContentAttribute:!1},ne.prototype),ie.prototype=Wi({init:function(e){function t(e){if(!Ti(r,e)){if(r.somethingSelected())Fo={lineWise:!1,text:r.getSelections()},"cut"==e.type&&r.replaceSelection("",null,"cut");else{if(!r.options.lineWiseCopyCut)return;var t=ee(r);Fo={lineWise:!0,text:t.text},"cut"==e.type&&r.operation(function(){r.setSelections(t.ranges,0,Wa),r.replaceSelection("",null,"cut")})}if(e.clipboardData&&!No)e.preventDefault(),e.clipboardData.clearData(),e.clipboardData.setData("text/plain",Fo.text.join("\n"));else{var n=re(),i=n.firstChild;r.display.lineSpace.insertBefore(n,r.display.lineSpace.firstChild),i.value=Fo.text.join("\n");var o=document.activeElement;Ua(i),setTimeout(function(){r.display.lineSpace.removeChild(n),o.focus()},50)}}}var n=this,r=n.cm,i=n.div=e.lineDiv;te(i),Ea(i,"paste",function(e){Ti(r,e)||J(e,r)}),Ea(i,"compositionstart",function(e){var t=e.data;if(n.composing={sel:r.doc.sel,data:t,startData:t},t){var i=r.doc.sel.primary(),o=r.getLine(i.head.line),a=o.indexOf(t,Math.max(0,i.head.ch-t.length));a>-1&&a<=i.head.ch&&(n.composing.sel=de(Bo(i.head.line,a),Bo(i.head.line,a+t.length)))}}),Ea(i,"compositionupdate",function(e){n.composing.data=e.data}),Ea(i,"compositionend",function(e){var t=n.composing;t&&(e.data==t.startData||/\u200b/.test(e.data)||(t.data=e.data),setTimeout(function(){t.handled||n.applyComposition(t),n.composing==t&&(n.composing=null)},50))}),Ea(i,"touchstart",function(){n.forceCompositionEnd()}),Ea(i,"input",function(){n.composing||!r.isReadOnly()&&n.pollContent()||At(n.cm,function(){Dt(r)})}),Ea(i,"copy",t),Ea(i,"cut",t)},prepareSelection:function(){var e=De(this.cm,!1);return e.focus=this.cm.state.focused,e},showSelection:function(e,t){e&&this.cm.display.view.length&&((e.focus||t)&&this.showPrimarySelection(),this.showMultipleSelections(e))},showPrimarySelection:function(){var e=window.getSelection(),t=this.cm.doc.sel.primary(),n=le(this.cm,e.anchorNode,e.anchorOffset),r=le(this.cm,e.focusNode,e.focusOffset);if(!n||n.bad||!r||r.bad||0!=_o(K(n,r),t.from())||0!=_o(V(n,r),t.to())){var i=oe(this.cm,t.from()),o=oe(this.cm,t.to());if(i||o){var a=this.cm.display.view,l=e.rangeCount&&e.getRangeAt(0);if(i){if(!o){var s=a[a.length-1].measure,c=s.maps?s.maps[s.maps.length-1]:s.map;o={node:c[c.length-1],offset:c[c.length-2]-c[c.length-3]}}}else i={node:a[0].measure.map[2],offset:0};try{var u=qa(i.node,i.offset,o.offset,o.node)}catch(f){}u&&(!go&&this.cm.state.focused?(e.collapse(i.node,i.offset),u.collapsed||e.addRange(u)):(e.removeAllRanges(),e.addRange(u)),l&&null==e.anchorNode?e.addRange(l):go&&this.startGracePeriod()),this.rememberSelection()}}},startGracePeriod:function(){var e=this;clearTimeout(this.gracePeriod),this.gracePeriod=setTimeout(function(){e.gracePeriod=!1,e.selectionChanged()&&e.cm.operation(function(){e.cm.curOp.selectionChanged=!0})},20)},showMultipleSelections:function(e){qi(this.cm.display.cursorDiv,e.cursors),qi(this.cm.display.selectionDiv,e.selection)},rememberSelection:function(){var e=window.getSelection();this.lastAnchorNode=e.anchorNode,this.lastAnchorOffset=e.anchorOffset,this.lastFocusNode=e.focusNode,this.lastFocusOffset=e.focusOffset},selectionInEditor:function(){var e=window.getSelection();if(!e.rangeCount)return!1;var t=e.getRangeAt(0).commonAncestorContainer;return Va(this.div,t)},focus:function(){"nocursor"!=this.cm.options.readOnly&&this.div.focus()},blur:function(){this.div.blur()},getField:function(){return this.div},supportsTouch:function(){return!0},receivedFocus:function(){function e(){t.cm.state.focused&&(t.pollSelection(),t.polling.set(t.cm.options.pollInterval,e))}var t=this;this.selectionInEditor()?this.pollSelection():At(this.cm,function(){t.cm.curOp.selectionChanged=!0}),this.polling.set(this.cm.options.pollInterval,e)},selectionChanged:function(){var e=window.getSelection();return e.anchorNode!=this.lastAnchorNode||e.anchorOffset!=this.lastAnchorOffset||e.focusNode!=this.lastFocusNode||e.focusOffset!=this.lastFocusOffset},pollSelection:function(){if(!this.composing&&!this.gracePeriod&&this.selectionChanged()){var e=window.getSelection(),t=this.cm;this.rememberSelection();var n=le(t,e.anchorNode,e.anchorOffset),r=le(t,e.focusNode,e.focusOffset);n&&r&&At(t,function(){Te(t.doc,de(n,r),Wa),(n.bad||r.bad)&&(t.curOp.selectionChanged=!0)})}},pollContent:function(){var e=this.cm,t=e.display,n=e.doc.sel.primary(),r=n.from(),i=n.to();if(r.line<t.viewFrom||i.line>t.viewTo-1)return!1;var o;if(r.line==t.viewFrom||0==(o=Bt(e,r.line)))var a=ti(t.view[0].line),l=t.view[0].node;else var a=ti(t.view[o].line),l=t.view[o-1].node.nextSibling;var s=Bt(e,i.line);if(s==t.view.length-1)var c=t.viewTo-1,u=t.lineDiv.lastChild;else var c=ti(t.view[s+1].line)-1,u=t.view[s+1].node.previousSibling;for(var f=e.doc.splitLines(ce(e,l,u,a,c)),h=Jr(e.doc,Bo(a,0),Bo(c,Zr(e.doc,c).text.length));f.length>1&&h.length>1;)if(Ii(f)==Ii(h))f.pop(),h.pop(),c--;else{if(f[0]!=h[0])break;f.shift(),h.shift(),a++}for(var d=0,p=0,m=f[0],g=h[0],v=Math.min(m.length,g.length);v>d&&m.charCodeAt(d)==g.charCodeAt(d);)++d;for(var y=Ii(f),x=Ii(h),b=Math.min(y.length-(1==f.length?d:0),x.length-(1==h.length?d:0));b>p&&y.charCodeAt(y.length-p-1)==x.charCodeAt(x.length-p-1);)++p;f[f.length-1]=y.slice(0,y.length-p),f[0]=f[0].slice(d);var w=Bo(a,d),k=Bo(c,h.length?Ii(h).length-p:0);return f.length>1||f[0]||_o(w,k)?(In(e.doc,f,w,k,"+input"),!0):void 0},ensurePolled:function(){this.forceCompositionEnd()},reset:function(){this.forceCompositionEnd()},forceCompositionEnd:function(){this.composing&&!this.composing.handled&&(this.applyComposition(this.composing),this.composing.handled=!0,this.div.blur(),this.div.focus())},applyComposition:function(e){this.cm.isReadOnly()?Et(this.cm,Dt)(this.cm):e.data&&e.data!=e.startData&&Et(this.cm,Z)(this.cm,e.data,0,e.sel)},setUneditable:function(e){e.contentEditable="false"},onKeyPress:function(e){e.preventDefault(),this.cm.isReadOnly()||Et(this.cm,Z)(this.cm,String.fromCharCode(null==e.charCode?e.keyCode:e.charCode),0)},readOnlyChanged:function(e){this.div.contentEditable=String("nocursor"!=e)},onContextMenu:Di,resetPosition:Di,needsContentAttribute:!0},ie.prototype),e.inputStyles={textarea:ne,contenteditable:ie},ue.prototype={primary:function(){return this.ranges[this.primIndex]},equals:function(e){if(e==this)return!0;if(e.primIndex!=this.primIndex||e.ranges.length!=this.ranges.length)return!1;for(var t=0;t<this.ranges.length;t++){var n=this.ranges[t],r=e.ranges[t];if(0!=_o(n.anchor,r.anchor)||0!=_o(n.head,r.head))return!1}return!0},deepCopy:function(){for(var e=[],t=0;t<this.ranges.length;t++)e[t]=new fe($(this.ranges[t].anchor),$(this.ranges[t].head));return new ue(e,this.primIndex)},somethingSelected:function(){for(var e=0;e<this.ranges.length;e++)if(!this.ranges[e].empty())return!0;return!1},contains:function(e,t){t||(t=e);for(var n=0;n<this.ranges.length;n++){var r=this.ranges[n];if(_o(t,r.from())>=0&&_o(e,r.to())<=0)return n}return-1}},fe.prototype={from:function(){return K(this.anchor,this.head)},to:function(){return V(this.anchor,this.head)},empty:function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch}};var zo,jo,Uo,qo={left:0,right:0,top:0,bottom:0},Go=null,Yo=0,$o=0,Vo=0,Ko=null;xo?Ko=-.53:go?Ko=15:So?Ko=-.7:Lo&&(Ko=-1/3);var Xo=function(e){var t=e.wheelDeltaX,n=e.wheelDeltaY;return null==t&&e.detail&&e.axis==e.HORIZONTAL_AXIS&&(t=e.detail),null==n&&e.detail&&e.axis==e.VERTICAL_AXIS?n=e.detail:null==n&&(n=e.wheelDelta),{x:t,y:n}};e.wheelEventPixels=function(e){var t=Xo(e);return t.x*=Ko,t.y*=Ko,t};var Zo=new Ei,Jo=null,Qo=e.changeEnd=function(e){return e.text?Bo(e.from.line+e.text.length-1,Ii(e.text).length+(1==e.text.length?e.from.ch:0)):e.to};e.prototype={constructor:e,focus:function(){window.focus(),this.display.input.focus()},setOption:function(e,t){var n=this.options,r=n[e];n[e]==t&&"mode"!=e||(n[e]=t,ta.hasOwnProperty(e)&&Et(this,ta[e])(this,t,r))},getOption:function(e){return this.options[e]},getDoc:function(){return this.doc},addKeyMap:function(e,t){this.state.keyMaps[t?"push":"unshift"]($n(e))},removeKeyMap:function(e){for(var t=this.state.keyMaps,n=0;n<t.length;++n)if(t[n]==e||t[n].name==e)return t.splice(n,1),!0},addOverlay:Ot(function(t,n){var r=t.token?t:e.getMode(this.options,t);if(r.startState)throw new Error("Overlays may not be stateful.");this.state.overlays.push({mode:r,modeSpec:t,opaque:n&&n.opaque}),this.state.modeGen++,Dt(this)}),removeOverlay:Ot(function(e){for(var t=this.state.overlays,n=0;n<t.length;++n){var r=t[n].modeSpec;if(r==e||"string"==typeof e&&r.name==e)return t.splice(n,1),this.state.modeGen++,void Dt(this)}}),indentLine:Ot(function(e,t,n){"string"!=typeof t&&"number"!=typeof t&&(t=null==t?this.options.smartIndent?"smart":"prev":t?"add":"subtract"),ve(this.doc,e)&&Fn(this,e,t,n)}),indentSelection:Ot(function(e){for(var t=this.doc.sel.ranges,n=-1,r=0;r<t.length;r++){var i=t[r];if(i.empty())i.head.line>n&&(Fn(this,i.head.line,e,!0),n=i.head.line,r==this.doc.sel.primIndex&&Bn(this));else{var o=i.from(),a=i.to(),l=Math.max(n,o.line);n=Math.min(this.lastLine(),a.line-(a.ch?0:1))+1;for(var s=l;n>s;++s)Fn(this,s,e);var c=this.doc.sel.ranges;0==o.ch&&t.length==c.length&&c[r].from().ch>0&&ke(this.doc,r,new fe(o,c[r].to()),Wa)}}}),getTokenAt:function(e,t){return Ir(this,e,t)},getLineTokens:function(e,t){return Ir(this,Bo(e),t,!0)},getTokenTypeAt:function(e){e=me(this.doc,e);var t,n=Dr(this,Zr(this.doc,e.line)),r=0,i=(n.length-1)/2,o=e.ch;if(0==o)t=n[2];else for(;;){var a=r+i>>1;if((a?n[2*a-1]:0)>=o)i=a;else{if(!(n[2*a+1]<o)){t=n[2*a+2];break}r=a+1}}var l=t?t.indexOf("cm-overlay "):-1;return 0>l?t:0==l?null:t.slice(0,l-1)},getModeAt:function(t){var n=this.doc.mode;return n.innerMode?e.innerMode(n,this.getTokenAt(t).state).mode:n},getHelper:function(e,t){return this.getHelpers(e,t)[0]},getHelpers:function(e,t){var n=[];if(!la.hasOwnProperty(t))return n;var r=la[t],i=this.getModeAt(e);if("string"==typeof i[t])r[i[t]]&&n.push(r[i[t]]);else if(i[t])for(var o=0;o<i[t].length;o++){var a=r[i[t][o]];a&&n.push(a)}else i.helperType&&r[i.helperType]?n.push(r[i.helperType]):r[i.name]&&n.push(r[i.name]);for(var o=0;o<r._global.length;o++){var l=r._global[o];l.pred(i,this)&&-1==Pi(n,l.val)&&n.push(l.val)}return n},getStateAfter:function(e,t){var n=this.doc;return e=pe(n,null==e?n.first+n.size-1:e),je(this,e+1,t)},cursorCoords:function(e,t){var n,r=this.doc.sel.primary();return n=null==e?r.head:"object"==typeof e?me(this.doc,e):e?r.from():r.to(),dt(this,n,t||"page")},charCoords:function(e,t){return ht(this,me(this.doc,e),t||"page")},coordsChar:function(e,t){return e=ft(this,e,t||"page"),gt(this,e.left,e.top)},lineAtHeight:function(e,t){return e=ft(this,{top:e,left:0},t||"page").top,ni(this.doc,e+this.display.viewOffset)},heightAtLine:function(e,t){var n,r=!1;if("number"==typeof e){var i=this.doc.first+this.doc.size-1;e<this.doc.first?e=this.doc.first:e>i&&(e=i,r=!0),n=Zr(this.doc,e)}else n=e;return ut(this,n,{top:0,left:0},t||"page").top+(r?this.doc.height-ri(n):0)},defaultTextHeight:function(){return yt(this.display)},defaultCharWidth:function(){return xt(this.display)},setGutterMarker:Ot(function(e,t,n){return zn(this.doc,e,"gutter",function(e){var r=e.gutterMarkers||(e.gutterMarkers={});return r[t]=n,!n&&Fi(r)&&(e.gutterMarkers=null),!0})}),clearGutter:Ot(function(e){var t=this,n=t.doc,r=n.first;n.iter(function(n){n.gutterMarkers&&n.gutterMarkers[e]&&(n.gutterMarkers[e]=null,Ht(t,r,"gutter"),Fi(n.gutterMarkers)&&(n.gutterMarkers=null)),++r})}),lineInfo:function(e){if("number"==typeof e){if(!ve(this.doc,e))return null;var t=e;if(e=Zr(this.doc,e),!e)return null}else{var t=ti(e);if(null==t)return null}return{line:t,handle:e,text:e.text,gutterMarkers:e.gutterMarkers,textClass:e.textClass,bgClass:e.bgClass,wrapClass:e.wrapClass,widgets:e.widgets}},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(e,t,n,r,i){var o=this.display;e=dt(this,me(this.doc,e));var a=e.bottom,l=e.left;if(t.style.position="absolute",t.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(t),o.sizer.appendChild(t),"over"==r)a=e.top;else if("above"==r||"near"==r){var s=Math.max(o.wrapper.clientHeight,this.doc.height),c=Math.max(o.sizer.clientWidth,o.lineSpace.clientWidth);("above"==r||e.bottom+t.offsetHeight>s)&&e.top>t.offsetHeight?a=e.top-t.offsetHeight:e.bottom+t.offsetHeight<=s&&(a=e.bottom),l+t.offsetWidth>c&&(l=c-t.offsetWidth)}t.style.top=a+"px",t.style.left=t.style.right="","right"==i?(l=o.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==i?l=0:"middle"==i&&(l=(o.sizer.clientWidth-t.offsetWidth)/2),t.style.left=l+"px"),n&&Dn(this,l,a,l+t.offsetWidth,a+t.offsetHeight)},triggerOnKeyDown:Ot(hn),triggerOnKeyPress:Ot(mn),triggerOnKeyUp:pn,execCommand:function(e){return ua.hasOwnProperty(e)?ua[e].call(null,this):void 0},triggerElectric:Ot(function(e){Q(this,e)}),findPosH:function(e,t,n,r){var i=1;0>t&&(i=-1,t=-t);for(var o=0,a=me(this.doc,e);t>o&&(a=Un(this.doc,a,i,n,r),!a.hitSide);++o);return a},moveH:Ot(function(e,t){var n=this;n.extendSelectionsBy(function(r){return n.display.shift||n.doc.extend||r.empty()?Un(n.doc,r.head,e,t,n.options.rtlMoveVisually):0>e?r.from():r.to()},_a)}),deleteH:Ot(function(e,t){var n=this.doc.sel,r=this.doc;n.somethingSelected()?r.replaceSelection("",null,"+delete"):jn(this,function(n){var i=Un(r,n.head,e,t,!1);return 0>e?{from:i,to:n.head}:{from:n.head,to:i}})}),findPosV:function(e,t,n,r){var i=1,o=r;0>t&&(i=-1,t=-t);for(var a=0,l=me(this.doc,e);t>a;++a){var s=dt(this,l,"div");if(null==o?o=s.left:s.left=o,l=qn(this,s,i,n),l.hitSide)break}return l},moveV:Ot(function(e,t){var n=this,r=this.doc,i=[],o=!n.display.shift&&!r.extend&&r.sel.somethingSelected();if(r.extendSelectionsBy(function(a){if(o)return 0>e?a.from():a.to();var l=dt(n,a.head,"div");null!=a.goalColumn&&(l.left=a.goalColumn),i.push(l.left);var s=qn(n,l,e,t);return"page"==t&&a==r.sel.primary()&&Wn(n,null,ht(n,s,"div").top-l.top),s},_a),i.length)for(var a=0;a<r.sel.ranges.length;a++)r.sel.ranges[a].goalColumn=i[a]}),findWordAt:function(e){var t=this.doc,n=Zr(t,e.line).text,r=e.ch,i=e.ch;if(n){var o=this.getHelper(e,"wordChars");(e.xRel<0||i==n.length)&&r?--r:++i;for(var a=n.charAt(r),l=_i(a,o)?function(e){return _i(e,o)}:/\s/.test(a)?function(e){return/\s/.test(e)}:function(e){return!/\s/.test(e)&&!_i(e)};r>0&&l(n.charAt(r-1));)--r;for(;i<n.length&&l(n.charAt(i));)++i}return new fe(Bo(e.line,r),Bo(e.line,i))},toggleOverwrite:function(e){null!=e&&e==this.state.overwrite||((this.state.overwrite=!this.state.overwrite)?Ja(this.display.cursorDiv,"CodeMirror-overwrite"):Za(this.display.cursorDiv,"CodeMirror-overwrite"),Pa(this,"overwriteToggle",this,this.state.overwrite))},hasFocus:function(){return this.display.input.getField()==Gi()},isReadOnly:function(){return!(!this.options.readOnly&&!this.doc.cantEdit)},scrollTo:Ot(function(e,t){null==e&&null==t||_n(this),null!=e&&(this.curOp.scrollLeft=e),null!=t&&(this.curOp.scrollTop=t)}),getScrollInfo:function(){var e=this.display.scroller;return{left:e.scrollLeft,top:e.scrollTop,height:e.scrollHeight-Ye(this)-this.display.barHeight,width:e.scrollWidth-Ye(this)-this.display.barWidth,clientHeight:Ve(this),clientWidth:$e(this)}},scrollIntoView:Ot(function(e,t){if(null==e?(e={from:this.doc.sel.primary().head,to:null},null==t&&(t=this.options.cursorScrollMargin)):"number"==typeof e?e={from:Bo(e,0),to:null}:null==e.from&&(e={from:e,to:null}),e.to||(e.to=e.from),e.margin=t||0,null!=e.from.line)_n(this),this.curOp.scrollToPos=e;else{var n=Hn(this,Math.min(e.from.left,e.to.left),Math.min(e.from.top,e.to.top)-e.margin,Math.max(e.from.right,e.to.right),Math.max(e.from.bottom,e.to.bottom)+e.margin);this.scrollTo(n.scrollLeft,n.scrollTop)}}),setSize:Ot(function(e,t){function n(e){return"number"==typeof e||/^\d+$/.test(String(e))?e+"px":e}var r=this;null!=e&&(r.display.wrapper.style.width=n(e)),null!=t&&(r.display.wrapper.style.height=n(t)),r.options.lineWrapping&&at(this);var i=r.display.viewFrom;r.doc.iter(i,r.display.viewTo,function(e){if(e.widgets)for(var t=0;t<e.widgets.length;t++)if(e.widgets[t].noHScroll){Ht(r,i,"widget");break}++i}),r.curOp.forceUpdate=!0,Pa(r,"refresh",this)}),operation:function(e){return At(this,e)},refresh:Ot(function(){var e=this.display.cachedTextHeight;Dt(this),this.curOp.forceUpdate=!0,lt(this),this.scrollTo(this.doc.scrollLeft,this.doc.scrollTop),u(this),(null==e||Math.abs(e-yt(this.display))>.5)&&a(this),Pa(this,"refresh",this)}),swapDoc:Ot(function(e){var t=this.doc;return t.cm=null,Xr(this,e),lt(this),this.display.input.reset(),this.scrollTo(e.scrollLeft,e.scrollTop),this.curOp.forceScroll=!0,Ci(this,"swapDoc",this,t),t}),getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Ai(e);var ea=e.defaults={},ta=e.optionHandlers={},na=e.Init={toString:function(){return"CodeMirror.Init"}};Gn("value","",function(e,t){e.setValue(t)},!0),Gn("mode",null,function(e,t){e.doc.modeOption=t,n(e)},!0),Gn("indentUnit",2,n,!0),Gn("indentWithTabs",!1),Gn("smartIndent",!0),Gn("tabSize",4,function(e){r(e),lt(e),Dt(e)},!0),Gn("lineSeparator",null,function(e,t){if(e.doc.lineSep=t,t){var n=[],r=e.doc.first;e.doc.iter(function(e){for(var i=0;;){var o=e.text.indexOf(t,i);if(-1==o)break;i=o+t.length,n.push(Bo(r,o))}r++});for(var i=n.length-1;i>=0;i--)In(e.doc,t,n[i],Bo(n[i].line,n[i].ch+t.length))}}),Gn("specialChars",/[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g,function(t,n,r){t.state.specialChars=new RegExp(n.source+(n.test(" ")?"":"| "),"g"),r!=e.Init&&t.refresh()}),Gn("specialCharPlaceholder",_r,function(e){e.refresh()},!0),Gn("electricChars",!0),Gn("inputStyle",Ao?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),Gn("rtlMoveVisually",!Io),Gn("wholeLineUpdateBefore",!0),Gn("theme","default",function(e){l(e),s(e)},!0),Gn("keyMap","default",function(t,n,r){var i=$n(n),o=r!=e.Init&&$n(r);o&&o.detach&&o.detach(t,i),i.attach&&i.attach(t,o||null)}),Gn("extraKeys",null),Gn("lineWrapping",!1,i,!0),Gn("gutters",[],function(e){d(e.options),s(e)},!0),Gn("fixedGutter",!0,function(e,t){e.display.gutters.style.left=t?C(e.display)+"px":"0",e.refresh()},!0),Gn("coverGutterNextToScrollbar",!1,function(e){y(e)},!0),Gn("scrollbarStyle","native",function(e){v(e),y(e),e.display.scrollbars.setScrollTop(e.doc.scrollTop),e.display.scrollbars.setScrollLeft(e.doc.scrollLeft)},!0),Gn("lineNumbers",!1,function(e){d(e.options),s(e)},!0),Gn("firstLineNumber",1,s,!0),Gn("lineNumberFormatter",function(e){return e},s,!0),Gn("showCursorWhenSelecting",!1,Re,!0),Gn("resetSelectionOnContextMenu",!0),Gn("lineWiseCopyCut",!0),Gn("readOnly",!1,function(e,t){"nocursor"==t?(yn(e),e.display.input.blur(),e.display.disabled=!0):e.display.disabled=!1,e.display.input.readOnlyChanged(t)}),Gn("disableInput",!1,function(e,t){t||e.display.input.reset()},!0),Gn("dragDrop",!0,Ut),Gn("allowDropFileTypes",null),Gn("cursorBlinkRate",530),Gn("cursorScrollMargin",0),Gn("cursorHeight",1,Re,!0),Gn("singleCursorHeightPerLine",!0,Re,!0),Gn("workTime",100),Gn("workDelay",100),Gn("flattenSpans",!0,r,!0),Gn("addModeClass",!1,r,!0),Gn("pollInterval",100),Gn("undoDepth",200,function(e,t){e.doc.history.undoDepth=t}),Gn("historyEventDelay",1250),Gn("viewportMargin",10,function(e){e.refresh()},!0),Gn("maxHighlightLength",1e4,r,!0),Gn("moveInputWithCursor",!0,function(e,t){t||e.display.input.resetPosition()}),Gn("tabindex",null,function(e,t){e.display.input.getField().tabIndex=t||""}),Gn("autofocus",null);var ra=e.modes={},ia=e.mimeModes={};e.defineMode=function(t,n){e.defaults.mode||"null"==t||(e.defaults.mode=t),arguments.length>2&&(n.dependencies=Array.prototype.slice.call(arguments,2)),ra[t]=n},e.defineMIME=function(e,t){ia[e]=t},e.resolveMode=function(t){if("string"==typeof t&&ia.hasOwnProperty(t))t=ia[t];else if(t&&"string"==typeof t.name&&ia.hasOwnProperty(t.name)){var n=ia[t.name];"string"==typeof n&&(n={name:n}),t=Hi(n,t),t.name=n.name}else if("string"==typeof t&&/^[\w\-]+\/[\w\-]+\+xml$/.test(t))return e.resolveMode("application/xml");return"string"==typeof t?{name:t}:t||{name:"null"}},e.getMode=function(t,n){var n=e.resolveMode(n),r=ra[n.name];if(!r)return e.getMode(t,"text/plain");var i=r(t,n);if(oa.hasOwnProperty(n.name)){var o=oa[n.name];for(var a in o)o.hasOwnProperty(a)&&(i.hasOwnProperty(a)&&(i["_"+a]=i[a]),i[a]=o[a])}if(i.name=n.name,n.helperType&&(i.helperType=n.helperType),n.modeProps)for(var a in n.modeProps)i[a]=n.modeProps[a];return i},e.defineMode("null",function(){return{token:function(e){e.skipToEnd()}}}),e.defineMIME("text/plain","null");var oa=e.modeExtensions={};e.extendMode=function(e,t){var n=oa.hasOwnProperty(e)?oa[e]:oa[e]={};Wi(t,n)},e.defineExtension=function(t,n){e.prototype[t]=n},e.defineDocExtension=function(e,t){Ca.prototype[e]=t},e.defineOption=Gn;var aa=[];e.defineInitHook=function(e){aa.push(e)};var la=e.helpers={};e.registerHelper=function(t,n,r){la.hasOwnProperty(t)||(la[t]=e[t]={_global:[]}),la[t][n]=r},e.registerGlobalHelper=function(t,n,r,i){e.registerHelper(t,n,i),la[t]._global.push({pred:r,val:i})};var sa=e.copyState=function(e,t){if(t===!0)return t;if(e.copyState)return e.copyState(t);var n={};for(var r in t){var i=t[r];i instanceof Array&&(i=i.concat([])),n[r]=i}return n},ca=e.startState=function(e,t,n){return e.startState?e.startState(t,n):!0};e.innerMode=function(e,t){for(;e.innerMode;){var n=e.innerMode(t);if(!n||n.mode==e)break;t=n.state,e=n.mode}return n||{mode:e,state:t}};var ua=e.commands={selectAll:function(e){e.setSelection(Bo(e.firstLine(),0),Bo(e.lastLine()),Wa)},singleSelection:function(e){e.setSelection(e.getCursor("anchor"),e.getCursor("head"),Wa)},killLine:function(e){jn(e,function(t){if(t.empty()){var n=Zr(e.doc,t.head.line).text.length;return t.head.ch==n&&t.head.line<e.lastLine()?{from:t.head,to:Bo(t.head.line+1,0)}:{from:t.head,to:Bo(t.head.line,n)}}return{from:t.from(),to:t.to()}})},deleteLine:function(e){jn(e,function(t){return{from:Bo(t.from().line,0),to:me(e.doc,Bo(t.to().line+1,0))}})},delLineLeft:function(e){jn(e,function(e){return{from:Bo(e.from().line,0),to:e.from()}})},delWrappedLineLeft:function(e){jn(e,function(t){var n=e.charCoords(t.head,"div").top+5,r=e.coordsChar({left:0,top:n},"div");return{from:r,to:t.from()}})},delWrappedLineRight:function(e){jn(e,function(t){var n=e.charCoords(t.head,"div").top+5,r=e.coordsChar({left:e.display.lineDiv.offsetWidth+100,top:n},"div");return{from:t.from(),to:r}})},undo:function(e){e.undo()},redo:function(e){e.redo()},undoSelection:function(e){e.undoSelection()},redoSelection:function(e){e.redoSelection()},goDocStart:function(e){e.extendSelection(Bo(e.firstLine(),0))},goDocEnd:function(e){e.extendSelection(Bo(e.lastLine()))},goLineStart:function(e){e.extendSelectionsBy(function(t){return oo(e,t.head.line)},{origin:"+move",bias:1})},goLineStartSmart:function(e){e.extendSelectionsBy(function(t){return lo(e,t.head)},{origin:"+move",bias:1})},goLineEnd:function(e){e.extendSelectionsBy(function(t){return ao(e,t.head.line)},{origin:"+move",bias:-1})},goLineRight:function(e){e.extendSelectionsBy(function(t){var n=e.charCoords(t.head,"div").top+5;return e.coordsChar({left:e.display.lineDiv.offsetWidth+100,top:n},"div")},_a)},goLineLeft:function(e){e.extendSelectionsBy(function(t){var n=e.charCoords(t.head,"div").top+5;return e.coordsChar({left:0,top:n},"div")},_a)},goLineLeftSmart:function(e){e.extendSelectionsBy(function(t){var n=e.charCoords(t.head,"div").top+5,r=e.coordsChar({left:0,top:n},"div");return r.ch<e.getLine(r.line).search(/\S/)?lo(e,t.head):r},_a)},goLineUp:function(e){e.moveV(-1,"line")},goLineDown:function(e){e.moveV(1,"line")},goPageUp:function(e){e.moveV(-1,"page")},goPageDown:function(e){e.moveV(1,"page")},goCharLeft:function(e){e.moveH(-1,"char")},goCharRight:function(e){e.moveH(1,"char")},goColumnLeft:function(e){e.moveH(-1,"column")},goColumnRight:function(e){e.moveH(1,"column")},goWordLeft:function(e){e.moveH(-1,"word")},goGroupRight:function(e){e.moveH(1,"group")},goGroupLeft:function(e){e.moveH(-1,"group")},goWordRight:function(e){e.moveH(1,"word")},delCharBefore:function(e){e.deleteH(-1,"char")},delCharAfter:function(e){e.deleteH(1,"char")},delWordBefore:function(e){e.deleteH(-1,"word")},delWordAfter:function(e){e.deleteH(1,"word")},delGroupBefore:function(e){e.deleteH(-1,"group")},delGroupAfter:function(e){e.deleteH(1,"group")},indentAuto:function(e){e.indentSelection("smart")},indentMore:function(e){e.indentSelection("add")},indentLess:function(e){e.indentSelection("subtract")},insertTab:function(e){e.replaceSelection(" ")},insertSoftTab:function(e){for(var t=[],n=e.listSelections(),r=e.options.tabSize,i=0;i<n.length;i++){var o=n[i].from(),a=Fa(e.getLine(o.line),o.ch,r);t.push(Oi(r-a%r))}e.replaceSelections(t)},defaultTab:function(e){e.somethingSelected()?e.indentSelection("add"):e.execCommand("insertTab")},transposeChars:function(e){At(e,function(){for(var t=e.listSelections(),n=[],r=0;r<t.length;r++){var i=t[r].head,o=Zr(e.doc,i.line).text;if(o)if(i.ch==o.length&&(i=new Bo(i.line,i.ch-1)),i.ch>0)i=new Bo(i.line,i.ch+1),e.replaceRange(o.charAt(i.ch-1)+o.charAt(i.ch-2),Bo(i.line,i.ch-2),i,"+transpose");else if(i.line>e.doc.first){var a=Zr(e.doc,i.line-1).text;a&&e.replaceRange(o.charAt(0)+e.doc.lineSeparator()+a.charAt(a.length-1),Bo(i.line-1,a.length-1),Bo(i.line,1),"+transpose")}n.push(new fe(i,i))}e.setSelections(n)})},newlineAndIndent:function(e){At(e,function(){for(var t=e.listSelections().length,n=0;t>n;n++){var r=e.listSelections()[n];e.replaceRange(e.doc.lineSeparator(),r.anchor,r.head,"+input"),e.indentLine(r.from().line+1,null,!0)}Bn(e)})},openLine:function(e){e.replaceSelection("\n","start")},toggleOverwrite:function(e){e.toggleOverwrite()}},fa=e.keyMap={};fa.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"},fa.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"},fa.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Alt-F":"goWordRight","Alt-B":"goWordLeft","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-D":"delWordAfter","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars","Ctrl-O":"openLine"},fa.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight","Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]},fa["default"]=Eo?fa.macDefault:fa.pcDefault,e.normalizeKeyMap=function(e){var t={};for(var n in e)if(e.hasOwnProperty(n)){var r=e[n];if(/^(name|fallthrough|(de|at)tach)$/.test(n))continue;if("..."==r){delete e[n];continue}for(var i=Ri(n.split(" "),Yn),o=0;o<i.length;o++){var a,l;o==i.length-1?(l=i.join(" "),a=r):(l=i.slice(0,o+1).join(" "),a="...");var s=t[l];if(s){if(s!=a)throw new Error("Inconsistent bindings for "+l)}else t[l]=a}delete e[n]}for(var c in t)e[c]=t[c];return e};var ha=e.lookupKey=function(e,t,n,r){t=$n(t);var i=t.call?t.call(e,r):t[e];if(i===!1)return"nothing";if("..."===i)return"multi";if(null!=i&&n(i))return"handled";if(t.fallthrough){if("[object Array]"!=Object.prototype.toString.call(t.fallthrough))return ha(e,t.fallthrough,n,r);for(var o=0;o<t.fallthrough.length;o++){var a=ha(e,t.fallthrough[o],n,r); -if(a)return a}}},da=e.isModifierKey=function(e){var t="string"==typeof e?e:ol[e.keyCode];return"Ctrl"==t||"Alt"==t||"Shift"==t||"Mod"==t},pa=e.keyName=function(e,t){if(Co&&34==e.keyCode&&e["char"])return!1;var n=ol[e.keyCode],r=n;return null==r||e.altGraphKey?!1:(e.altKey&&"Alt"!=n&&(r="Alt-"+r),(Ro?e.metaKey:e.ctrlKey)&&"Ctrl"!=n&&(r="Ctrl-"+r),(Ro?e.ctrlKey:e.metaKey)&&"Cmd"!=n&&(r="Cmd-"+r),!t&&e.shiftKey&&"Shift"!=n&&(r="Shift-"+r),r)};e.fromTextArea=function(t,n){function r(){t.value=c.getValue()}if(n=n?Wi(n):{},n.value=t.value,!n.tabindex&&t.tabIndex&&(n.tabindex=t.tabIndex),!n.placeholder&&t.placeholder&&(n.placeholder=t.placeholder),null==n.autofocus){var i=Gi();n.autofocus=i==t||null!=t.getAttribute("autofocus")&&i==document.body}if(t.form&&(Ea(t.form,"submit",r),!n.leaveSubmitMethodAlone)){var o=t.form,a=o.submit;try{var l=o.submit=function(){r(),o.submit=a,o.submit(),o.submit=l}}catch(s){}}n.finishInit=function(e){e.save=r,e.getTextArea=function(){return t},e.toTextArea=function(){e.toTextArea=isNaN,r(),t.parentNode.removeChild(e.getWrapperElement()),t.style.display="",t.form&&(Ia(t.form,"submit",r),"function"==typeof t.form.submit&&(t.form.submit=a))}},t.style.display="none";var c=e(function(e){t.parentNode.insertBefore(e,t.nextSibling)},n);return c};var ma=e.StringStream=function(e,t){this.pos=this.start=0,this.string=e,this.tabSize=t||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0};ma.prototype={eol:function(){return this.pos>=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){return this.pos<this.string.length?this.string.charAt(this.pos++):void 0},eat:function(e){var t=this.string.charAt(this.pos);if("string"==typeof e)var n=t==e;else var n=t&&(e.test?e.test(t):e(t));return n?(++this.pos,t):void 0},eatWhile:function(e){for(var t=this.pos;this.eat(e););return this.pos>t},eatSpace:function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},skipToEnd:function(){this.pos=this.string.length},skipTo:function(e){var t=this.string.indexOf(e,this.pos);return t>-1?(this.pos=t,!0):void 0},backUp:function(e){this.pos-=e},column:function(){return this.lastColumnPos<this.start&&(this.lastColumnValue=Fa(this.string,this.start,this.tabSize,this.lastColumnPos,this.lastColumnValue),this.lastColumnPos=this.start),this.lastColumnValue-(this.lineStart?Fa(this.string,this.lineStart,this.tabSize):0)},indentation:function(){return Fa(this.string,null,this.tabSize)-(this.lineStart?Fa(this.string,this.lineStart,this.tabSize):0)},match:function(e,t,n){if("string"!=typeof e){var r=this.string.slice(this.pos).match(e);return r&&r.index>0?null:(r&&t!==!1&&(this.pos+=r[0].length),r)}var i=function(e){return n?e.toLowerCase():e},o=this.string.substr(this.pos,e.length);return i(o)==i(e)?(t!==!1&&(this.pos+=e.length),!0):void 0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(e,t){this.lineStart+=e;try{return t()}finally{this.lineStart-=e}}};var ga=0,va=e.TextMarker=function(e,t){this.lines=[],this.type=t,this.doc=e,this.id=++ga};Ai(va),va.prototype.clear=function(){if(!this.explicitlyCleared){var e=this.doc.cm,t=e&&!e.curOp;if(t&&bt(e),Ni(this,"clear")){var n=this.find();n&&Ci(this,"clear",n.from,n.to)}for(var r=null,i=null,o=0;o<this.lines.length;++o){var a=this.lines[o],l=er(a.markedSpans,this);e&&!this.collapsed?Ht(e,ti(a),"text"):e&&(null!=l.to&&(i=ti(a)),null!=l.from&&(r=ti(a))),a.markedSpans=tr(a.markedSpans,l),null==l.from&&this.collapsed&&!kr(this.doc,a)&&e&&ei(a,yt(e.display))}if(e&&this.collapsed&&!e.options.lineWrapping)for(var o=0;o<this.lines.length;++o){var s=yr(this.lines[o]),c=f(s);c>e.display.maxLineLength&&(e.display.maxLine=s,e.display.maxLineLength=c,e.display.maxLineChanged=!0)}null!=r&&e&&this.collapsed&&Dt(e,r,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,e&&Ae(e.doc)),e&&Ci(e,"markerCleared",e,this),t&&kt(e),this.parent&&this.parent.clear()}},va.prototype.find=function(e,t){null==e&&"bookmark"==this.type&&(e=1);for(var n,r,i=0;i<this.lines.length;++i){var o=this.lines[i],a=er(o.markedSpans,this);if(null!=a.from&&(n=Bo(t?o:ti(o),a.from),-1==e))return n;if(null!=a.to&&(r=Bo(t?o:ti(o),a.to),1==e))return r}return n&&{from:n,to:r}},va.prototype.changed=function(){var e=this.find(-1,!0),t=this,n=this.doc.cm;e&&n&&At(n,function(){var r=e.line,i=ti(e.line),o=Qe(n,i);if(o&&(ot(o),n.curOp.selectionChanged=n.curOp.forceUpdate=!0),n.curOp.updateMaxLine=!0,!kr(t.doc,r)&&null!=t.height){var a=t.height;t.height=null;var l=Lr(t)-a;l&&ei(r,r.height+l)}})},va.prototype.attachLine=function(e){if(!this.lines.length&&this.doc.cm){var t=this.doc.cm.curOp;t.maybeHiddenMarkers&&-1!=Pi(t.maybeHiddenMarkers,this)||(t.maybeUnhiddenMarkers||(t.maybeUnhiddenMarkers=[])).push(this)}this.lines.push(e)},va.prototype.detachLine=function(e){if(this.lines.splice(Pi(this.lines,e),1),!this.lines.length&&this.doc.cm){var t=this.doc.cm.curOp;(t.maybeHiddenMarkers||(t.maybeHiddenMarkers=[])).push(this)}};var ga=0,ya=e.SharedTextMarker=function(e,t){this.markers=e,this.primary=t;for(var n=0;n<e.length;++n)e[n].parent=this};Ai(ya),ya.prototype.clear=function(){if(!this.explicitlyCleared){this.explicitlyCleared=!0;for(var e=0;e<this.markers.length;++e)this.markers[e].clear();Ci(this,"clear")}},ya.prototype.find=function(e,t){return this.primary.find(e,t)};var xa=e.LineWidget=function(e,t,n){if(n)for(var r in n)n.hasOwnProperty(r)&&(this[r]=n[r]);this.doc=e,this.node=t};Ai(xa),xa.prototype.clear=function(){var e=this.doc.cm,t=this.line.widgets,n=this.line,r=ti(n);if(null!=r&&t){for(var i=0;i<t.length;++i)t[i]==this&&t.splice(i--,1);t.length||(n.widgets=null);var o=Lr(this);ei(n,Math.max(0,n.height-o)),e&&At(e,function(){Cr(e,n,-o),Ht(e,r,"widget")})}},xa.prototype.changed=function(){var e=this.height,t=this.doc.cm,n=this.line;this.height=null;var r=Lr(this)-e;r&&(ei(n,n.height+r),t&&At(t,function(){t.curOp.forceUpdate=!0,Cr(t,n,r)}))};var ba=e.Line=function(e,t,n){this.text=e,ur(this,t),this.height=n?n(this):1};Ai(ba),ba.prototype.lineNo=function(){return ti(this)};var wa={},ka={};$r.prototype={chunkSize:function(){return this.lines.length},removeInner:function(e,t){for(var n=e,r=e+t;r>n;++n){var i=this.lines[n];this.height-=i.height,Nr(i),Ci(i,"delete")}this.lines.splice(e,t)},collapse:function(e){e.push.apply(e,this.lines)},insertInner:function(e,t,n){this.height+=n,this.lines=this.lines.slice(0,e).concat(t).concat(this.lines.slice(e));for(var r=0;r<t.length;++r)t[r].parent=this},iterN:function(e,t,n){for(var r=e+t;r>e;++e)if(n(this.lines[e]))return!0}},Vr.prototype={chunkSize:function(){return this.size},removeInner:function(e,t){this.size-=t;for(var n=0;n<this.children.length;++n){var r=this.children[n],i=r.chunkSize();if(i>e){var o=Math.min(t,i-e),a=r.height;if(r.removeInner(e,o),this.height-=a-r.height,i==o&&(this.children.splice(n--,1),r.parent=null),0==(t-=o))break;e=0}else e-=i}if(this.size-t<25&&(this.children.length>1||!(this.children[0]instanceof $r))){var l=[];this.collapse(l),this.children=[new $r(l)],this.children[0].parent=this}},collapse:function(e){for(var t=0;t<this.children.length;++t)this.children[t].collapse(e)},insertInner:function(e,t,n){this.size+=t.length,this.height+=n;for(var r=0;r<this.children.length;++r){var i=this.children[r],o=i.chunkSize();if(o>=e){if(i.insertInner(e,t,n),i.lines&&i.lines.length>50){for(var a=i.lines.length%25+25,l=a;l<i.lines.length;){var s=new $r(i.lines.slice(l,l+=25));i.height-=s.height,this.children.splice(++r,0,s),s.parent=this}i.lines=i.lines.slice(0,a),this.maybeSpill()}break}e-=o}},maybeSpill:function(){if(!(this.children.length<=10)){var e=this;do{var t=e.children.splice(e.children.length-5,5),n=new Vr(t);if(e.parent){e.size-=n.size,e.height-=n.height;var r=Pi(e.parent.children,e);e.parent.children.splice(r+1,0,n)}else{var i=new Vr(e.children);i.parent=e,e.children=[i,n],e=i}n.parent=e.parent}while(e.children.length>10);e.parent.maybeSpill()}},iterN:function(e,t,n){for(var r=0;r<this.children.length;++r){var i=this.children[r],o=i.chunkSize();if(o>e){var a=Math.min(t,o-e);if(i.iterN(e,a,n))return!0;if(0==(t-=a))break;e=0}else e-=o}}};var Sa=0,Ca=e.Doc=function(e,t,n,r){if(!(this instanceof Ca))return new Ca(e,t,n,r);null==n&&(n=0),Vr.call(this,[new $r([new ba("",null)])]),this.first=n,this.scrollTop=this.scrollLeft=0,this.cantEdit=!1,this.cleanGeneration=1,this.frontier=n;var i=Bo(n,0);this.sel=de(i),this.history=new oi(null),this.id=++Sa,this.modeOption=t,this.lineSep=r,this.extend=!1,"string"==typeof e&&(e=this.splitLines(e)),Yr(this,{from:i,to:i,text:e}),Te(this,de(i),Wa)};Ca.prototype=Hi(Vr.prototype,{constructor:Ca,iter:function(e,t,n){n?this.iterN(e-this.first,t-e,n):this.iterN(this.first,this.first+this.size,e)},insert:function(e,t){for(var n=0,r=0;r<t.length;++r)n+=t[r].height;this.insertInner(e-this.first,t,n)},remove:function(e,t){this.removeInner(e-this.first,t)},getValue:function(e){var t=Qr(this,this.first,this.first+this.size);return e===!1?t:t.join(e||this.lineSeparator())},setValue:It(function(e){var t=Bo(this.first,0),n=this.first+this.size-1;Tn(this,{from:t,to:Bo(n,Zr(this,n).text.length),text:this.splitLines(e),origin:"setValue",full:!0},!0),Te(this,de(t))}),replaceRange:function(e,t,n,r){t=me(this,t),n=n?me(this,n):t,In(this,e,t,n,r)},getRange:function(e,t,n){var r=Jr(this,me(this,e),me(this,t));return n===!1?r:r.join(n||this.lineSeparator())},getLine:function(e){var t=this.getLineHandle(e);return t&&t.text},getLineHandle:function(e){return ve(this,e)?Zr(this,e):void 0},getLineNumber:function(e){return ti(e)},getLineHandleVisualStart:function(e){return"number"==typeof e&&(e=Zr(this,e)),yr(e)},lineCount:function(){return this.size},firstLine:function(){return this.first},lastLine:function(){return this.first+this.size-1},clipPos:function(e){return me(this,e)},getCursor:function(e){var t,n=this.sel.primary();return t=null==e||"head"==e?n.head:"anchor"==e?n.anchor:"end"==e||"to"==e||e===!1?n.to():n.from()},listSelections:function(){return this.sel.ranges},somethingSelected:function(){return this.sel.somethingSelected()},setCursor:It(function(e,t,n){Se(this,me(this,"number"==typeof e?Bo(e,t||0):e),null,n)}),setSelection:It(function(e,t,n){Se(this,me(this,e),me(this,t||e),n)}),extendSelection:It(function(e,t,n){be(this,me(this,e),t&&me(this,t),n)}),extendSelections:It(function(e,t){we(this,ye(this,e),t)}),extendSelectionsBy:It(function(e,t){var n=Ri(this.sel.ranges,e);we(this,ye(this,n),t)}),setSelections:It(function(e,t,n){if(e.length){for(var r=0,i=[];r<e.length;r++)i[r]=new fe(me(this,e[r].anchor),me(this,e[r].head));null==t&&(t=Math.min(e.length-1,this.sel.primIndex)),Te(this,he(i,t),n)}}),addSelection:It(function(e,t,n){var r=this.sel.ranges.slice(0);r.push(new fe(me(this,e),me(this,t||e))),Te(this,he(r,r.length-1),n)}),getSelection:function(e){for(var t,n=this.sel.ranges,r=0;r<n.length;r++){var i=Jr(this,n[r].from(),n[r].to());t=t?t.concat(i):i}return e===!1?t:t.join(e||this.lineSeparator())},getSelections:function(e){for(var t=[],n=this.sel.ranges,r=0;r<n.length;r++){var i=Jr(this,n[r].from(),n[r].to());e!==!1&&(i=i.join(e||this.lineSeparator())),t[r]=i}return t},replaceSelection:function(e,t,n){for(var r=[],i=0;i<this.sel.ranges.length;i++)r[i]=e;this.replaceSelections(r,t,n||"+input")},replaceSelections:It(function(e,t,n){for(var r=[],i=this.sel,o=0;o<i.ranges.length;o++){var a=i.ranges[o];r[o]={from:a.from(),to:a.to(),text:this.splitLines(e[o]),origin:n}}for(var l=t&&"end"!=t&&Cn(this,r,t),o=r.length-1;o>=0;o--)Tn(this,r[o]);l?Le(this,l):this.cm&&Bn(this.cm)}),undo:It(function(){Nn(this,"undo")}),redo:It(function(){Nn(this,"redo")}),undoSelection:It(function(){Nn(this,"undo",!0)}),redoSelection:It(function(){Nn(this,"redo",!0)}),setExtending:function(e){this.extend=e},getExtending:function(){return this.extend},historySize:function(){for(var e=this.history,t=0,n=0,r=0;r<e.done.length;r++)e.done[r].ranges||++t;for(var r=0;r<e.undone.length;r++)e.undone[r].ranges||++n;return{undo:t,redo:n}},clearHistory:function(){this.history=new oi(this.history.maxGeneration)},markClean:function(){this.cleanGeneration=this.changeGeneration(!0)},changeGeneration:function(e){return e&&(this.history.lastOp=this.history.lastSelOp=this.history.lastOrigin=null),this.history.generation},isClean:function(e){return this.history.generation==(e||this.cleanGeneration)},getHistory:function(){return{done:gi(this.history.done),undone:gi(this.history.undone)}},setHistory:function(e){var t=this.history=new oi(this.history.maxGeneration);t.done=gi(e.done.slice(0),null,!0),t.undone=gi(e.undone.slice(0),null,!0)},addLineClass:It(function(e,t,n){return zn(this,e,"gutter"==t?"gutter":"class",function(e){var r="text"==t?"textClass":"background"==t?"bgClass":"gutter"==t?"gutterClass":"wrapClass";if(e[r]){if(Yi(n).test(e[r]))return!1;e[r]+=" "+n}else e[r]=n;return!0})}),removeLineClass:It(function(e,t,n){return zn(this,e,"gutter"==t?"gutter":"class",function(e){var r="text"==t?"textClass":"background"==t?"bgClass":"gutter"==t?"gutterClass":"wrapClass",i=e[r];if(!i)return!1;if(null==n)e[r]=null;else{var o=i.match(Yi(n));if(!o)return!1;var a=o.index+o[0].length;e[r]=i.slice(0,o.index)+(o.index&&a!=i.length?" ":"")+i.slice(a)||null}return!0})}),addLineWidget:It(function(e,t,n){return Tr(this,e,t,n)}),removeLineWidget:function(e){e.clear()},markText:function(e,t,n){return Vn(this,me(this,e),me(this,t),n,n&&n.type||"range")},setBookmark:function(e,t){var n={replacedWith:t&&(null==t.nodeType?t.widget:t),insertLeft:t&&t.insertLeft,clearWhenEmpty:!1,shared:t&&t.shared,handleMouseEvents:t&&t.handleMouseEvents};return e=me(this,e),Vn(this,e,e,n,"bookmark")},findMarksAt:function(e){e=me(this,e);var t=[],n=Zr(this,e.line).markedSpans;if(n)for(var r=0;r<n.length;++r){var i=n[r];(null==i.from||i.from<=e.ch)&&(null==i.to||i.to>=e.ch)&&t.push(i.marker.parent||i.marker)}return t},findMarks:function(e,t,n){e=me(this,e),t=me(this,t);var r=[],i=e.line;return this.iter(e.line,t.line+1,function(o){var a=o.markedSpans;if(a)for(var l=0;l<a.length;l++){var s=a[l];null!=s.to&&i==e.line&&e.ch>=s.to||null==s.from&&i!=e.line||null!=s.from&&i==t.line&&s.from>=t.ch||n&&!n(s.marker)||r.push(s.marker.parent||s.marker)}++i}),r},getAllMarks:function(){var e=[];return this.iter(function(t){var n=t.markedSpans;if(n)for(var r=0;r<n.length;++r)null!=n[r].from&&e.push(n[r].marker)}),e},posFromIndex:function(e){var t,n=this.first,r=this.lineSeparator().length;return this.iter(function(i){var o=i.text.length+r;return o>e?(t=e,!0):(e-=o,void++n)}),me(this,Bo(n,t))},indexFromPos:function(e){e=me(this,e);var t=e.ch;if(e.line<this.first||e.ch<0)return 0;var n=this.lineSeparator().length;return this.iter(this.first,e.line,function(e){t+=e.text.length+n}),t},copy:function(e){var t=new Ca(Qr(this,this.first,this.first+this.size),this.modeOption,this.first,this.lineSep);return t.scrollTop=this.scrollTop,t.scrollLeft=this.scrollLeft,t.sel=this.sel,t.extend=!1,e&&(t.history.undoDepth=this.history.undoDepth,t.setHistory(this.getHistory())),t},linkedDoc:function(e){e||(e={});var t=this.first,n=this.first+this.size;null!=e.from&&e.from>t&&(t=e.from),null!=e.to&&e.to<n&&(n=e.to);var r=new Ca(Qr(this,t,n),e.mode||this.modeOption,t,this.lineSep);return e.sharedHist&&(r.history=this.history),(this.linked||(this.linked=[])).push({doc:r,sharedHist:e.sharedHist}),r.linked=[{doc:this,isParent:!0,sharedHist:e.sharedHist}],Zn(r,Xn(this)),r},unlinkDoc:function(t){if(t instanceof e&&(t=t.doc),this.linked)for(var n=0;n<this.linked.length;++n){var r=this.linked[n];if(r.doc==t){this.linked.splice(n,1),t.unlinkDoc(this),Jn(Xn(this));break}}if(t.history==this.history){var i=[t.id];Kr(t,function(e){i.push(e.id)},!0),t.history=new oi(null),t.history.done=gi(this.history.done,i),t.history.undone=gi(this.history.undone,i)}},iterLinkedDocs:function(e){Kr(this,e)},getMode:function(){return this.mode},getEditor:function(){return this.cm},splitLines:function(e){return this.lineSep?e.split(this.lineSep):tl(e)},lineSeparator:function(){return this.lineSep||"\n"}}),Ca.prototype.eachLine=Ca.prototype.iter;var La="iter insert remove copy getEditor constructor".split(" ");for(var Ta in Ca.prototype)Ca.prototype.hasOwnProperty(Ta)&&Pi(La,Ta)<0&&(e.prototype[Ta]=function(e){return function(){return e.apply(this.doc,arguments)}}(Ca.prototype[Ta]));Ai(Ca);var Ma=e.e_preventDefault=function(e){e.preventDefault?e.preventDefault():e.returnValue=!1},Na=e.e_stopPropagation=function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0},Aa=e.e_stop=function(e){Ma(e),Na(e)},Ea=e.on=function(e,t,n){if(e.addEventListener)e.addEventListener(t,n,!1);else if(e.attachEvent)e.attachEvent("on"+t,n);else{var r=e._handlers||(e._handlers={}),i=r[t]||(r[t]=[]);i.push(n)}},Oa=[],Ia=e.off=function(e,t,n){if(e.removeEventListener)e.removeEventListener(t,n,!1);else if(e.detachEvent)e.detachEvent("on"+t,n);else for(var r=Si(e,t,!1),i=0;i<r.length;++i)if(r[i]==n){r.splice(i,1);break}},Pa=e.signal=function(e,t){var n=Si(e,t,!0);if(n.length)for(var r=Array.prototype.slice.call(arguments,2),i=0;i<n.length;++i)n[i].apply(null,r)},Ra=null,Da=30,Ha=e.Pass={toString:function(){return"CodeMirror.Pass"}},Wa={scroll:!1},Ba={origin:"*mouse"},_a={origin:"+move"};Ei.prototype.set=function(e,t){clearTimeout(this.id),this.id=setTimeout(t,e)};var Fa=e.countColumn=function(e,t,n,r,i){null==t&&(t=e.search(/[^\s\u00a0]/),-1==t&&(t=e.length));for(var o=r||0,a=i||0;;){var l=e.indexOf(" ",o);if(0>l||l>=t)return a+(t-o);a+=l-o,a+=n-a%n,o=l+1}},za=e.findColumn=function(e,t,n){for(var r=0,i=0;;){var o=e.indexOf(" ",r);-1==o&&(o=e.length);var a=o-r;if(o==e.length||i+a>=t)return r+Math.min(a,t-i);if(i+=o-r,i+=n-i%n,r=o+1,i>=t)return r}},ja=[""],Ua=function(e){e.select()};No?Ua=function(e){e.selectionStart=0,e.selectionEnd=e.value.length}:xo&&(Ua=function(e){try{e.select()}catch(t){}});var qa,Ga=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,Ya=e.isWordChar=function(e){return/\w/.test(e)||e>""&&(e.toUpperCase()!=e.toLowerCase()||Ga.test(e))},$a=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;qa=document.createRange?function(e,t,n,r){var i=document.createRange();return i.setEnd(r||e,n),i.setStart(e,t),i}:function(e,t,n){var r=document.body.createTextRange();try{r.moveToElementText(e.parentNode)}catch(i){return r}return r.collapse(!0),r.moveEnd("character",n),r.moveStart("character",t),r};var Va=e.contains=function(e,t){if(3==t.nodeType&&(t=t.parentNode),e.contains)return e.contains(t);do if(11==t.nodeType&&(t=t.host),t==e)return!0;while(t=t.parentNode)};xo&&11>bo&&(Gi=function(){try{return document.activeElement}catch(e){return document.body}});var Ka,Xa,Za=e.rmClass=function(e,t){var n=e.className,r=Yi(t).exec(n);if(r){var i=n.slice(r.index+r[0].length);e.className=n.slice(0,r.index)+(i?r[1]+i:"")}},Ja=e.addClass=function(e,t){var n=e.className;Yi(t).test(n)||(e.className+=(n?" ":"")+t)},Qa=!1,el=function(){if(xo&&9>bo)return!1;var e=ji("div");return"draggable"in e||"dragDrop"in e}(),tl=e.splitLines=3!="\n\nb".split(/\n/).length?function(e){for(var t=0,n=[],r=e.length;r>=t;){var i=e.indexOf("\n",t);-1==i&&(i=e.length);var o=e.slice(t,"\r"==e.charAt(i-1)?i-1:i),a=o.indexOf("\r");-1!=a?(n.push(o.slice(0,a)),t+=a+1):(n.push(o),t=i+1)}return n}:function(e){return e.split(/\r\n?|\n/)},nl=window.getSelection?function(e){try{return e.selectionStart!=e.selectionEnd}catch(t){return!1}}:function(e){try{var t=e.ownerDocument.selection.createRange()}catch(n){}return t&&t.parentElement()==e?0!=t.compareEndPoints("StartToEnd",t):!1},rl=function(){var e=ji("div");return"oncopy"in e?!0:(e.setAttribute("oncopy","return;"),"function"==typeof e.oncopy)}(),il=null,ol=e.keyNames={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",46:"Delete",59:";",61:"=",91:"Mod",92:"Mod",93:"Mod",106:"*",107:"=",109:"-",110:".",111:"/",127:"Delete",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63232:"Up",63233:"Down",63234:"Left",63235:"Right",63272:"Delete",63273:"Home",63275:"End",63276:"PageUp",63277:"PageDown",63302:"Insert"};!function(){for(var e=0;10>e;e++)ol[e+48]=ol[e+96]=String(e);for(var e=65;90>=e;e++)ol[e]=String.fromCharCode(e);for(var e=1;12>=e;e++)ol[e+111]=ol[e+63235]="F"+e}();var al,ll=function(){function e(e){return 247>=e?n.charAt(e):e>=1424&&1524>=e?"R":e>=1536&&1773>=e?r.charAt(e-1536):e>=1774&&2220>=e?"r":e>=8192&&8203>=e?"w":8204==e?"b":"L"}function t(e,t,n){this.level=e,this.from=t,this.to=n}var n="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",r="rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm",i=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,o=/[stwN]/,a=/[LRr]/,l=/[Lb1n]/,s=/[1n]/,c="L";return function(n){if(!i.test(n))return!1;for(var r,u=n.length,f=[],h=0;u>h;++h)f.push(r=e(n.charCodeAt(h)));for(var h=0,d=c;u>h;++h){var r=f[h];"m"==r?f[h]=d:d=r}for(var h=0,p=c;u>h;++h){var r=f[h];"1"==r&&"r"==p?f[h]="n":a.test(r)&&(p=r,"r"==r&&(f[h]="R"))}for(var h=1,d=f[0];u-1>h;++h){var r=f[h];"+"==r&&"1"==d&&"1"==f[h+1]?f[h]="1":","!=r||d!=f[h+1]||"1"!=d&&"n"!=d||(f[h]=d),d=r}for(var h=0;u>h;++h){var r=f[h];if(","==r)f[h]="N";else if("%"==r){for(var m=h+1;u>m&&"%"==f[m];++m);for(var g=h&&"!"==f[h-1]||u>m&&"1"==f[m]?"1":"N",v=h;m>v;++v)f[v]=g;h=m-1}}for(var h=0,p=c;u>h;++h){var r=f[h];"L"==p&&"1"==r?f[h]="L":a.test(r)&&(p=r)}for(var h=0;u>h;++h)if(o.test(f[h])){for(var m=h+1;u>m&&o.test(f[m]);++m);for(var y="L"==(h?f[h-1]:c),x="L"==(u>m?f[m]:c),g=y||x?"L":"R",v=h;m>v;++v)f[v]=g;h=m-1}for(var b,w=[],h=0;u>h;)if(l.test(f[h])){var k=h;for(++h;u>h&&l.test(f[h]);++h);w.push(new t(0,k,h))}else{var S=h,C=w.length;for(++h;u>h&&"L"!=f[h];++h);for(var v=S;h>v;)if(s.test(f[v])){v>S&&w.splice(C,0,new t(1,S,v));var L=v;for(++v;h>v&&s.test(f[v]);++v);w.splice(C,0,new t(2,L,v)),S=v}else++v;h>S&&w.splice(C,0,new t(1,S,h))}return 1==w[0].level&&(b=n.match(/^\s+/))&&(w[0].from=b[0].length,w.unshift(new t(0,0,b[0].length))),1==Ii(w).level&&(b=n.match(/\s+$/))&&(Ii(w).to-=b[0].length,w.push(new t(0,u-b[0].length,u))),2==w[0].level&&w.unshift(new t(1,w[0].to,w[0].to)),w[0].level!=Ii(w).level&&w.push(new t(w[0].level,u,u)),w}}();return e.version="5.15.2",e})},{}],11:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../markdown/markdown"),t("../../addon/mode/overlay")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../markdown/markdown","../../addon/mode/overlay"],i):i(CodeMirror)}(function(e){"use strict";var t=/^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i;e.defineMode("gfm",function(n,r){function i(e){return e.code=!1,null}var o=0,a={startState:function(){return{code:!1,codeBlock:!1,ateSpace:!1}},copyState:function(e){return{code:e.code,codeBlock:e.codeBlock,ateSpace:e.ateSpace}},token:function(e,n){if(n.combineTokens=null,n.codeBlock)return e.match(/^```+/)?(n.codeBlock=!1,null):(e.skipToEnd(),null);if(e.sol()&&(n.code=!1),e.sol()&&e.match(/^```+/))return e.skipToEnd(),n.codeBlock=!0,null;if("`"===e.peek()){e.next();var i=e.pos;e.eatWhile("`");var a=1+e.pos-i;return n.code?a===o&&(n.code=!1):(o=a,n.code=!0),null}if(n.code)return e.next(),null;if(e.eatSpace())return n.ateSpace=!0,null;if((e.sol()||n.ateSpace)&&(n.ateSpace=!1,r.gitHubSpice!==!1)){if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/))return n.combineTokens=!0,"link";if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/))return n.combineTokens=!0,"link"}return e.match(t)&&"]("!=e.string.slice(e.start-2,e.start)&&(0==e.start||/\W/.test(e.string.charAt(e.start-1)))?(n.combineTokens=!0,"link"):(e.next(),null)},blankLine:i},l={underscoresBreakWords:!1,taskLists:!0,fencedCodeBlocks:"```",strikethrough:!0};for(var s in r)l[s]=r[s];return l.name="markdown",e.overlayMode(e.getMode(n,l),a)},"markdown"),e.defineMIME("text/x-gfm","gfm")})},{"../../addon/mode/overlay":8,"../../lib/codemirror":10,"../markdown/markdown":12}],12:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../xml/xml"),t("../meta")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../xml/xml","../meta"],i):i(CodeMirror)}(function(e){"use strict";e.defineMode("markdown",function(t,n){function r(n){if(e.findModeByName){var r=e.findModeByName(n);r&&(n=r.mime||r.mimes[0])}var i=e.getMode(t,n);return"null"==i.name?null:i}function i(e,t,n){return t.f=t.inline=n,n(e,t)}function o(e,t,n){return t.f=t.block=n,n(e,t)}function a(e){return!e||!/\S/.test(e.string)}function l(e){return e.linkTitle=!1,e.em=!1,e.strong=!1,e.strikethrough=!1,e.quote=0,e.indentedCode=!1,k&&e.f==c&&(e.f=p,e.block=s),e.trailingSpace=0,e.trailingSpaceNewLine=!1,e.prevLine=e.thisLine,e.thisLine=null,null}function s(t,o){var l=t.sol(),s=o.list!==!1,c=o.indentedCode;o.indentedCode=!1,s&&(o.indentationDiff>=0?(o.indentationDiff<4&&(o.indentation-=o.indentationDiff),o.list=null):o.indentation>0?o.list=null:o.list=!1);var f=null;if(o.indentationDiff>=4)return t.skipToEnd(),c||a(o.prevLine)?(o.indentation-=4,o.indentedCode=!0,S.code):null;if(t.eatSpace())return null;if((f=t.match(A))&&f[1].length<=6)return o.header=f[1].length,n.highlightFormatting&&(o.formatting="header"),o.f=o.inline,h(o);if(!(a(o.prevLine)||o.quote||s||c)&&(f=t.match(E)))return o.header="="==f[0].charAt(0)?1:2,n.highlightFormatting&&(o.formatting="header"),o.f=o.inline,h(o);if(t.eat(">"))return o.quote=l?1:o.quote+1,n.highlightFormatting&&(o.formatting="quote"),t.eatSpace(),h(o);if("["===t.peek())return i(t,o,y);if(t.match(L,!0))return o.hr=!0,S.hr;if((a(o.prevLine)||s)&&(t.match(T,!1)||t.match(M,!1))){var d=null;for(t.match(T,!0)?d="ul":(t.match(M,!0),d="ol"),o.indentation=t.column()+t.current().length,o.list=!0;o.listStack&&t.column()<o.listStack[o.listStack.length-1];)o.listStack.pop();return o.listStack.push(o.indentation),n.taskLists&&t.match(N,!1)&&(o.taskList=!0),o.f=o.inline,n.highlightFormatting&&(o.formatting=["list","list-"+d]),h(o)}return n.fencedCodeBlocks&&(f=t.match(I,!0))?(o.fencedChars=f[1],o.localMode=r(f[2]),o.localMode&&(o.localState=e.startState(o.localMode)),o.f=o.block=u,n.highlightFormatting&&(o.formatting="code-block"),o.code=-1,h(o)):i(t,o,o.inline)}function c(t,n){var r=w.token(t,n.htmlState);if(!k){var i=e.innerMode(w,n.htmlState);("xml"==i.mode.name&&null===i.state.tagStart&&!i.state.context&&i.state.tokenize.isInText||n.md_inside&&t.current().indexOf(">")>-1)&&(n.f=p,n.block=s,n.htmlState=null)}return r}function u(e,t){return t.fencedChars&&e.match(t.fencedChars,!1)?(t.localMode=t.localState=null,t.f=t.block=f,null):t.localMode?t.localMode.token(e,t.localState):(e.skipToEnd(),S.code)}function f(e,t){e.match(t.fencedChars),t.block=s,t.f=p,t.fencedChars=null,n.highlightFormatting&&(t.formatting="code-block"),t.code=1;var r=h(t);return t.code=0,r}function h(e){var t=[];if(e.formatting){t.push(S.formatting),"string"==typeof e.formatting&&(e.formatting=[e.formatting]);for(var r=0;r<e.formatting.length;r++)t.push(S.formatting+"-"+e.formatting[r]),"header"===e.formatting[r]&&t.push(S.formatting+"-"+e.formatting[r]+"-"+e.header),"quote"===e.formatting[r]&&(!n.maxBlockquoteDepth||n.maxBlockquoteDepth>=e.quote?t.push(S.formatting+"-"+e.formatting[r]+"-"+e.quote):t.push("error"))}if(e.taskOpen)return t.push("meta"),t.length?t.join(" "):null;if(e.taskClosed)return t.push("property"),t.length?t.join(" "):null;if(e.linkHref?t.push(S.linkHref,"url"):(e.strong&&t.push(S.strong),e.em&&t.push(S.em),e.strikethrough&&t.push(S.strikethrough),e.linkText&&t.push(S.linkText),e.code&&t.push(S.code)),e.header&&t.push(S.header,S.header+"-"+e.header),e.quote&&(t.push(S.quote),!n.maxBlockquoteDepth||n.maxBlockquoteDepth>=e.quote?t.push(S.quote+"-"+e.quote):t.push(S.quote+"-"+n.maxBlockquoteDepth)),e.list!==!1){var i=(e.listStack.length-1)%3;i?1===i?t.push(S.list2):t.push(S.list3):t.push(S.list1)}return e.trailingSpaceNewLine?t.push("trailing-space-new-line"):e.trailingSpace&&t.push("trailing-space-"+(e.trailingSpace%2?"a":"b")),t.length?t.join(" "):null}function d(e,t){return e.match(O,!0)?h(t):void 0}function p(t,r){var i=r.text(t,r);if("undefined"!=typeof i)return i;if(r.list)return r.list=null,h(r);if(r.taskList){var a="x"!==t.match(N,!0)[1];return a?r.taskOpen=!0:r.taskClosed=!0,n.highlightFormatting&&(r.formatting="task"),r.taskList=!1,h(r)}if(r.taskOpen=!1,r.taskClosed=!1,r.header&&t.match(/^#+$/,!0))return n.highlightFormatting&&(r.formatting="header"), -h(r);var l=t.sol(),s=t.next();if(r.linkTitle){r.linkTitle=!1;var u=s;"("===s&&(u=")"),u=(u+"").replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1");var f="^\\s*(?:[^"+u+"\\\\]+|\\\\\\\\|\\\\.)"+u;if(t.match(new RegExp(f),!0))return S.linkHref}if("`"===s){var d=r.formatting;n.highlightFormatting&&(r.formatting="code"),t.eatWhile("`");var p=t.current().length;if(0==r.code)return r.code=p,h(r);if(p==r.code){var v=h(r);return r.code=0,v}return r.formatting=d,h(r)}if(r.code)return h(r);if("\\"===s&&(t.next(),n.highlightFormatting)){var y=h(r),x=S.formatting+"-escape";return y?y+" "+x:x}if("!"===s&&t.match(/\[[^\]]*\] ?(?:\(|\[)/,!1))return t.match(/\[[^\]]*\]/),r.inline=r.f=g,S.image;if("["===s&&t.match(/[^\]]*\](\(.*\)| ?\[.*?\])/,!1))return r.linkText=!0,n.highlightFormatting&&(r.formatting="link"),h(r);if("]"===s&&r.linkText&&t.match(/\(.*?\)| ?\[.*?\]/,!1)){n.highlightFormatting&&(r.formatting="link");var y=h(r);return r.linkText=!1,r.inline=r.f=g,y}if("<"===s&&t.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkInline}if("<"===s&&t.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkEmail}if("<"===s&&t.match(/^(!--|\w)/,!1)){var b=t.string.indexOf(">",t.pos);if(-1!=b){var k=t.string.substring(t.start,b);/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(k)&&(r.md_inside=!0)}return t.backUp(1),r.htmlState=e.startState(w),o(t,r,c)}if("<"===s&&t.match(/^\/\w*?>/))return r.md_inside=!1,"tag";var C=!1;if(!n.underscoresBreakWords&&"_"===s&&"_"!==t.peek()&&t.match(/(\w)/,!1)){var L=t.pos-2;if(L>=0){var T=t.string.charAt(L);"_"!==T&&T.match(/(\w)/,!1)&&(C=!0)}}if("*"===s||"_"===s&&!C)if(l&&" "===t.peek());else{if(r.strong===s&&t.eat(s)){n.highlightFormatting&&(r.formatting="strong");var v=h(r);return r.strong=!1,v}if(!r.strong&&t.eat(s))return r.strong=s,n.highlightFormatting&&(r.formatting="strong"),h(r);if(r.em===s){n.highlightFormatting&&(r.formatting="em");var v=h(r);return r.em=!1,v}if(!r.em)return r.em=s,n.highlightFormatting&&(r.formatting="em"),h(r)}else if(" "===s&&(t.eat("*")||t.eat("_"))){if(" "===t.peek())return h(r);t.backUp(1)}if(n.strikethrough)if("~"===s&&t.eatWhile(s)){if(r.strikethrough){n.highlightFormatting&&(r.formatting="strikethrough");var v=h(r);return r.strikethrough=!1,v}if(t.match(/^[^\s]/,!1))return r.strikethrough=!0,n.highlightFormatting&&(r.formatting="strikethrough"),h(r)}else if(" "===s&&t.match(/^~~/,!0)){if(" "===t.peek())return h(r);t.backUp(2)}return" "===s&&(t.match(/ +$/,!1)?r.trailingSpace++:r.trailingSpace&&(r.trailingSpaceNewLine=!0)),h(r)}function m(e,t){var r=e.next();if(">"===r){t.f=t.inline=p,n.highlightFormatting&&(t.formatting="link");var i=h(t);return i?i+=" ":i="",i+S.linkInline}return e.match(/^[^>]+/,!0),S.linkInline}function g(e,t){if(e.eatSpace())return null;var r=e.next();return"("===r||"["===r?(t.f=t.inline=v("("===r?")":"]",0),n.highlightFormatting&&(t.formatting="link-string"),t.linkHref=!0,h(t)):"error"}function v(e){return function(t,r){var i=t.next();if(i===e){r.f=r.inline=p,n.highlightFormatting&&(r.formatting="link-string");var o=h(r);return r.linkHref=!1,o}return t.match(P[e]),r.linkHref=!0,h(r)}}function y(e,t){return e.match(/^([^\]\\]|\\.)*\]:/,!1)?(t.f=x,e.next(),n.highlightFormatting&&(t.formatting="link"),t.linkText=!0,h(t)):i(e,t,p)}function x(e,t){if(e.match(/^\]:/,!0)){t.f=t.inline=b,n.highlightFormatting&&(t.formatting="link");var r=h(t);return t.linkText=!1,r}return e.match(/^([^\]\\]|\\.)+/,!0),S.linkText}function b(e,t){return e.eatSpace()?null:(e.match(/^[^\s]+/,!0),void 0===e.peek()?t.linkTitle=!0:e.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/,!0),t.f=t.inline=p,S.linkHref+" url")}var w=e.getMode(t,"text/html"),k="null"==w.name;void 0===n.highlightFormatting&&(n.highlightFormatting=!1),void 0===n.maxBlockquoteDepth&&(n.maxBlockquoteDepth=0),void 0===n.underscoresBreakWords&&(n.underscoresBreakWords=!0),void 0===n.taskLists&&(n.taskLists=!1),void 0===n.strikethrough&&(n.strikethrough=!1),void 0===n.tokenTypeOverrides&&(n.tokenTypeOverrides={});var S={header:"header",code:"comment",quote:"quote",list1:"variable-2",list2:"variable-3",list3:"keyword",hr:"hr",image:"tag",formatting:"formatting",linkInline:"link",linkEmail:"link",linkText:"link",linkHref:"string",em:"em",strong:"strong",strikethrough:"strikethrough"};for(var C in S)S.hasOwnProperty(C)&&n.tokenTypeOverrides[C]&&(S[C]=n.tokenTypeOverrides[C]);var L=/^([*\-_])(?:\s*\1){2,}\s*$/,T=/^[*\-+]\s+/,M=/^[0-9]+([.)])\s+/,N=/^\[(x| )\](?=\s)/,A=n.allowAtxHeaderWithoutSpace?/^(#+)/:/^(#+)(?: |$)/,E=/^ *(?:\={1,}|-{1,})\s*$/,O=/^[^#!\[\]*_\\<>` "'(~]+/,I=new RegExp("^("+(n.fencedCodeBlocks===!0?"~~~+|```+":n.fencedCodeBlocks)+")[ \\t]*([\\w+#-]*)"),P={")":/^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,"]":/^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\\]]|\\.)*\])*?(?=\])/},R={startState:function(){return{f:s,prevLine:null,thisLine:null,block:s,htmlState:null,indentation:0,inline:p,text:d,formatting:!1,linkText:!1,linkHref:!1,linkTitle:!1,code:0,em:!1,strong:!1,header:0,hr:!1,taskList:!1,list:!1,listStack:[],quote:0,trailingSpace:0,trailingSpaceNewLine:!1,strikethrough:!1,fencedChars:null}},copyState:function(t){return{f:t.f,prevLine:t.prevLine,thisLine:t.thisLine,block:t.block,htmlState:t.htmlState&&e.copyState(w,t.htmlState),indentation:t.indentation,localMode:t.localMode,localState:t.localMode?e.copyState(t.localMode,t.localState):null,inline:t.inline,text:t.text,formatting:!1,linkTitle:t.linkTitle,code:t.code,em:t.em,strong:t.strong,strikethrough:t.strikethrough,header:t.header,hr:t.hr,taskList:t.taskList,list:t.list,listStack:t.listStack.slice(0),quote:t.quote,indentedCode:t.indentedCode,trailingSpace:t.trailingSpace,trailingSpaceNewLine:t.trailingSpaceNewLine,md_inside:t.md_inside,fencedChars:t.fencedChars}},token:function(e,t){if(t.formatting=!1,e!=t.thisLine){var n=t.header||t.hr;if(t.header=0,t.hr=!1,e.match(/^\s*$/,!0)||n){if(l(t),!n)return null;t.prevLine=null}t.prevLine=t.thisLine,t.thisLine=e,t.taskList=!1,t.trailingSpace=0,t.trailingSpaceNewLine=!1,t.f=t.block;var r=e.match(/^\s*/,!0)[0].replace(/\t/g," ").length;if(t.indentationDiff=Math.min(r-t.indentation,4),t.indentation=t.indentation+t.indentationDiff,r>0)return null}return t.f(e,t)},innerMode:function(e){return e.block==c?{state:e.htmlState,mode:w}:e.localState?{state:e.localState,mode:e.localMode}:{state:e,mode:R}},blankLine:l,getType:h,fold:"markdown"};return R},"xml"),e.defineMIME("text/x-markdown","markdown")})},{"../../lib/codemirror":10,"../meta":13,"../xml/xml":14}],13:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../lib/codemirror")):"function"==typeof e&&e.amd?e(["../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";e.modeInfo=[{name:"APL",mime:"text/apl",mode:"apl",ext:["dyalog","apl"]},{name:"PGP",mimes:["application/pgp","application/pgp-keys","application/pgp-signature"],mode:"asciiarmor",ext:["pgp"]},{name:"ASN.1",mime:"text/x-ttcn-asn",mode:"asn.1",ext:["asn","asn1"]},{name:"Asterisk",mime:"text/x-asterisk",mode:"asterisk",file:/^extensions\.conf$/i},{name:"Brainfuck",mime:"text/x-brainfuck",mode:"brainfuck",ext:["b","bf"]},{name:"C",mime:"text/x-csrc",mode:"clike",ext:["c","h"]},{name:"C++",mime:"text/x-c++src",mode:"clike",ext:["cpp","c++","cc","cxx","hpp","h++","hh","hxx"],alias:["cpp"]},{name:"Cobol",mime:"text/x-cobol",mode:"cobol",ext:["cob","cpy"]},{name:"C#",mime:"text/x-csharp",mode:"clike",ext:["cs"],alias:["csharp"]},{name:"Clojure",mime:"text/x-clojure",mode:"clojure",ext:["clj","cljc","cljx"]},{name:"ClojureScript",mime:"text/x-clojurescript",mode:"clojure",ext:["cljs"]},{name:"Closure Stylesheets (GSS)",mime:"text/x-gss",mode:"css",ext:["gss"]},{name:"CMake",mime:"text/x-cmake",mode:"cmake",ext:["cmake","cmake.in"],file:/^CMakeLists.txt$/},{name:"CoffeeScript",mime:"text/x-coffeescript",mode:"coffeescript",ext:["coffee"],alias:["coffee","coffee-script"]},{name:"Common Lisp",mime:"text/x-common-lisp",mode:"commonlisp",ext:["cl","lisp","el"],alias:["lisp"]},{name:"Cypher",mime:"application/x-cypher-query",mode:"cypher",ext:["cyp","cypher"]},{name:"Cython",mime:"text/x-cython",mode:"python",ext:["pyx","pxd","pxi"]},{name:"Crystal",mime:"text/x-crystal",mode:"crystal",ext:["cr"]},{name:"CSS",mime:"text/css",mode:"css",ext:["css"]},{name:"CQL",mime:"text/x-cassandra",mode:"sql",ext:["cql"]},{name:"D",mime:"text/x-d",mode:"d",ext:["d"]},{name:"Dart",mimes:["application/dart","text/x-dart"],mode:"dart",ext:["dart"]},{name:"diff",mime:"text/x-diff",mode:"diff",ext:["diff","patch"]},{name:"Django",mime:"text/x-django",mode:"django"},{name:"Dockerfile",mime:"text/x-dockerfile",mode:"dockerfile",file:/^Dockerfile$/},{name:"DTD",mime:"application/xml-dtd",mode:"dtd",ext:["dtd"]},{name:"Dylan",mime:"text/x-dylan",mode:"dylan",ext:["dylan","dyl","intr"]},{name:"EBNF",mime:"text/x-ebnf",mode:"ebnf"},{name:"ECL",mime:"text/x-ecl",mode:"ecl",ext:["ecl"]},{name:"edn",mime:"application/edn",mode:"clojure",ext:["edn"]},{name:"Eiffel",mime:"text/x-eiffel",mode:"eiffel",ext:["e"]},{name:"Elm",mime:"text/x-elm",mode:"elm",ext:["elm"]},{name:"Embedded Javascript",mime:"application/x-ejs",mode:"htmlembedded",ext:["ejs"]},{name:"Embedded Ruby",mime:"application/x-erb",mode:"htmlembedded",ext:["erb"]},{name:"Erlang",mime:"text/x-erlang",mode:"erlang",ext:["erl"]},{name:"Factor",mime:"text/x-factor",mode:"factor",ext:["factor"]},{name:"FCL",mime:"text/x-fcl",mode:"fcl"},{name:"Forth",mime:"text/x-forth",mode:"forth",ext:["forth","fth","4th"]},{name:"Fortran",mime:"text/x-fortran",mode:"fortran",ext:["f","for","f77","f90"]},{name:"F#",mime:"text/x-fsharp",mode:"mllike",ext:["fs"],alias:["fsharp"]},{name:"Gas",mime:"text/x-gas",mode:"gas",ext:["s"]},{name:"Gherkin",mime:"text/x-feature",mode:"gherkin",ext:["feature"]},{name:"GitHub Flavored Markdown",mime:"text/x-gfm",mode:"gfm",file:/^(readme|contributing|history).md$/i},{name:"Go",mime:"text/x-go",mode:"go",ext:["go"]},{name:"Groovy",mime:"text/x-groovy",mode:"groovy",ext:["groovy","gradle"]},{name:"HAML",mime:"text/x-haml",mode:"haml",ext:["haml"]},{name:"Haskell",mime:"text/x-haskell",mode:"haskell",ext:["hs"]},{name:"Haskell (Literate)",mime:"text/x-literate-haskell",mode:"haskell-literate",ext:["lhs"]},{name:"Haxe",mime:"text/x-haxe",mode:"haxe",ext:["hx"]},{name:"HXML",mime:"text/x-hxml",mode:"haxe",ext:["hxml"]},{name:"ASP.NET",mime:"application/x-aspx",mode:"htmlembedded",ext:["aspx"],alias:["asp","aspx"]},{name:"HTML",mime:"text/html",mode:"htmlmixed",ext:["html","htm"],alias:["xhtml"]},{name:"HTTP",mime:"message/http",mode:"http"},{name:"IDL",mime:"text/x-idl",mode:"idl",ext:["pro"]},{name:"Jade",mime:"text/x-jade",mode:"jade",ext:["jade"]},{name:"Java",mime:"text/x-java",mode:"clike",ext:["java"]},{name:"Java Server Pages",mime:"application/x-jsp",mode:"htmlembedded",ext:["jsp"],alias:["jsp"]},{name:"JavaScript",mimes:["text/javascript","text/ecmascript","application/javascript","application/x-javascript","application/ecmascript"],mode:"javascript",ext:["js"],alias:["ecmascript","js","node"]},{name:"JSON",mimes:["application/json","application/x-json"],mode:"javascript",ext:["json","map"],alias:["json5"]},{name:"JSON-LD",mime:"application/ld+json",mode:"javascript",ext:["jsonld"],alias:["jsonld"]},{name:"JSX",mime:"text/jsx",mode:"jsx",ext:["jsx"]},{name:"Jinja2",mime:"null",mode:"jinja2"},{name:"Julia",mime:"text/x-julia",mode:"julia",ext:["jl"]},{name:"Kotlin",mime:"text/x-kotlin",mode:"clike",ext:["kt"]},{name:"LESS",mime:"text/x-less",mode:"css",ext:["less"]},{name:"LiveScript",mime:"text/x-livescript",mode:"livescript",ext:["ls"],alias:["ls"]},{name:"Lua",mime:"text/x-lua",mode:"lua",ext:["lua"]},{name:"Markdown",mime:"text/x-markdown",mode:"markdown",ext:["markdown","md","mkd"]},{name:"mIRC",mime:"text/mirc",mode:"mirc"},{name:"MariaDB SQL",mime:"text/x-mariadb",mode:"sql"},{name:"Mathematica",mime:"text/x-mathematica",mode:"mathematica",ext:["m","nb"]},{name:"Modelica",mime:"text/x-modelica",mode:"modelica",ext:["mo"]},{name:"MUMPS",mime:"text/x-mumps",mode:"mumps",ext:["mps"]},{name:"MS SQL",mime:"text/x-mssql",mode:"sql"},{name:"mbox",mime:"application/mbox",mode:"mbox",ext:["mbox"]},{name:"MySQL",mime:"text/x-mysql",mode:"sql"},{name:"Nginx",mime:"text/x-nginx-conf",mode:"nginx",file:/nginx.*\.conf$/i},{name:"NSIS",mime:"text/x-nsis",mode:"nsis",ext:["nsh","nsi"]},{name:"NTriples",mime:"text/n-triples",mode:"ntriples",ext:["nt"]},{name:"Objective C",mime:"text/x-objectivec",mode:"clike",ext:["m","mm"],alias:["objective-c","objc"]},{name:"OCaml",mime:"text/x-ocaml",mode:"mllike",ext:["ml","mli","mll","mly"]},{name:"Octave",mime:"text/x-octave",mode:"octave",ext:["m"]},{name:"Oz",mime:"text/x-oz",mode:"oz",ext:["oz"]},{name:"Pascal",mime:"text/x-pascal",mode:"pascal",ext:["p","pas"]},{name:"PEG.js",mime:"null",mode:"pegjs",ext:["jsonld"]},{name:"Perl",mime:"text/x-perl",mode:"perl",ext:["pl","pm"]},{name:"PHP",mime:"application/x-httpd-php",mode:"php",ext:["php","php3","php4","php5","phtml"]},{name:"Pig",mime:"text/x-pig",mode:"pig",ext:["pig"]},{name:"Plain Text",mime:"text/plain",mode:"null",ext:["txt","text","conf","def","list","log"]},{name:"PLSQL",mime:"text/x-plsql",mode:"sql",ext:["pls"]},{name:"PowerShell",mime:"application/x-powershell",mode:"powershell",ext:["ps1","psd1","psm1"]},{name:"Properties files",mime:"text/x-properties",mode:"properties",ext:["properties","ini","in"],alias:["ini","properties"]},{name:"ProtoBuf",mime:"text/x-protobuf",mode:"protobuf",ext:["proto"]},{name:"Python",mime:"text/x-python",mode:"python",ext:["BUILD","bzl","py","pyw"],file:/^(BUCK|BUILD)$/},{name:"Puppet",mime:"text/x-puppet",mode:"puppet",ext:["pp"]},{name:"Q",mime:"text/x-q",mode:"q",ext:["q"]},{name:"R",mime:"text/x-rsrc",mode:"r",ext:["r"],alias:["rscript"]},{name:"reStructuredText",mime:"text/x-rst",mode:"rst",ext:["rst"],alias:["rst"]},{name:"RPM Changes",mime:"text/x-rpm-changes",mode:"rpm"},{name:"RPM Spec",mime:"text/x-rpm-spec",mode:"rpm",ext:["spec"]},{name:"Ruby",mime:"text/x-ruby",mode:"ruby",ext:["rb"],alias:["jruby","macruby","rake","rb","rbx"]},{name:"Rust",mime:"text/x-rustsrc",mode:"rust",ext:["rs"]},{name:"SAS",mime:"text/x-sas",mode:"sas",ext:["sas"]},{name:"Sass",mime:"text/x-sass",mode:"sass",ext:["sass"]},{name:"Scala",mime:"text/x-scala",mode:"clike",ext:["scala"]},{name:"Scheme",mime:"text/x-scheme",mode:"scheme",ext:["scm","ss"]},{name:"SCSS",mime:"text/x-scss",mode:"css",ext:["scss"]},{name:"Shell",mime:"text/x-sh",mode:"shell",ext:["sh","ksh","bash"],alias:["bash","sh","zsh"],file:/^PKGBUILD$/},{name:"Sieve",mime:"application/sieve",mode:"sieve",ext:["siv","sieve"]},{name:"Slim",mimes:["text/x-slim","application/x-slim"],mode:"slim",ext:["slim"]},{name:"Smalltalk",mime:"text/x-stsrc",mode:"smalltalk",ext:["st"]},{name:"Smarty",mime:"text/x-smarty",mode:"smarty",ext:["tpl"]},{name:"Solr",mime:"text/x-solr",mode:"solr"},{name:"Soy",mime:"text/x-soy",mode:"soy",ext:["soy"],alias:["closure template"]},{name:"SPARQL",mime:"application/sparql-query",mode:"sparql",ext:["rq","sparql"],alias:["sparul"]},{name:"Spreadsheet",mime:"text/x-spreadsheet",mode:"spreadsheet",alias:["excel","formula"]},{name:"SQL",mime:"text/x-sql",mode:"sql",ext:["sql"]},{name:"Squirrel",mime:"text/x-squirrel",mode:"clike",ext:["nut"]},{name:"Swift",mime:"text/x-swift",mode:"swift",ext:["swift"]},{name:"sTeX",mime:"text/x-stex",mode:"stex"},{name:"LaTeX",mime:"text/x-latex",mode:"stex",ext:["text","ltx"],alias:["tex"]},{name:"SystemVerilog",mime:"text/x-systemverilog",mode:"verilog",ext:["v"]},{name:"Tcl",mime:"text/x-tcl",mode:"tcl",ext:["tcl"]},{name:"Textile",mime:"text/x-textile",mode:"textile",ext:["textile"]},{name:"TiddlyWiki ",mime:"text/x-tiddlywiki",mode:"tiddlywiki"},{name:"Tiki wiki",mime:"text/tiki",mode:"tiki"},{name:"TOML",mime:"text/x-toml",mode:"toml",ext:["toml"]},{name:"Tornado",mime:"text/x-tornado",mode:"tornado"},{name:"troff",mime:"text/troff",mode:"troff",ext:["1","2","3","4","5","6","7","8","9"]},{name:"TTCN",mime:"text/x-ttcn",mode:"ttcn",ext:["ttcn","ttcn3","ttcnpp"]},{name:"TTCN_CFG",mime:"text/x-ttcn-cfg",mode:"ttcn-cfg",ext:["cfg"]},{name:"Turtle",mime:"text/turtle",mode:"turtle",ext:["ttl"]},{name:"TypeScript",mime:"application/typescript",mode:"javascript",ext:["ts"],alias:["ts"]},{name:"Twig",mime:"text/x-twig",mode:"twig"},{name:"Web IDL",mime:"text/x-webidl",mode:"webidl",ext:["webidl"]},{name:"VB.NET",mime:"text/x-vb",mode:"vb",ext:["vb"]},{name:"VBScript",mime:"text/vbscript",mode:"vbscript",ext:["vbs"]},{name:"Velocity",mime:"text/velocity",mode:"velocity",ext:["vtl"]},{name:"Verilog",mime:"text/x-verilog",mode:"verilog",ext:["v"]},{name:"VHDL",mime:"text/x-vhdl",mode:"vhdl",ext:["vhd","vhdl"]},{name:"XML",mimes:["application/xml","text/xml"],mode:"xml",ext:["xml","xsl","xsd"],alias:["rss","wsdl","xsd"]},{name:"XQuery",mime:"application/xquery",mode:"xquery",ext:["xy","xquery"]},{name:"Yacas",mime:"text/x-yacas",mode:"yacas",ext:["ys"]},{name:"YAML",mime:"text/x-yaml",mode:"yaml",ext:["yaml","yml"],alias:["yml"]},{name:"Z80",mime:"text/x-z80",mode:"z80",ext:["z80"]},{name:"mscgen",mime:"text/x-mscgen",mode:"mscgen",ext:["mscgen","mscin","msc"]},{name:"xu",mime:"text/x-xu",mode:"mscgen",ext:["xu"]},{name:"msgenny",mime:"text/x-msgenny",mode:"mscgen",ext:["msgenny"]}];for(var t=0;t<e.modeInfo.length;t++){var n=e.modeInfo[t];n.mimes&&(n.mime=n.mimes[0])}e.findModeByMIME=function(t){t=t.toLowerCase();for(var n=0;n<e.modeInfo.length;n++){var r=e.modeInfo[n];if(r.mime==t)return r;if(r.mimes)for(var i=0;i<r.mimes.length;i++)if(r.mimes[i]==t)return r}},e.findModeByExtension=function(t){for(var n=0;n<e.modeInfo.length;n++){var r=e.modeInfo[n];if(r.ext)for(var i=0;i<r.ext.length;i++)if(r.ext[i]==t)return r}},e.findModeByFileName=function(t){for(var n=0;n<e.modeInfo.length;n++){var r=e.modeInfo[n];if(r.file&&r.file.test(t))return r}var i=t.lastIndexOf("."),o=i>-1&&t.substring(i+1,t.length);return o?e.findModeByExtension(o):void 0},e.findModeByName=function(t){t=t.toLowerCase();for(var n=0;n<e.modeInfo.length;n++){var r=e.modeInfo[n];if(r.name.toLowerCase()==t)return r;if(r.alias)for(var i=0;i<r.alias.length;i++)if(r.alias[i].toLowerCase()==t)return r}}})},{"../lib/codemirror":10}],14:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";var t={autoSelfClosers:{area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},implicitlyClosed:{dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},contextGrabbers:{dd:{dd:!0,dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}},doNotIndent:{pre:!0},allowUnquoted:!0,allowMissing:!0,caseFold:!0},n={autoSelfClosers:{},implicitlyClosed:{},contextGrabbers:{},doNotIndent:{},allowUnquoted:!1,allowMissing:!1,caseFold:!1};e.defineMode("xml",function(r,i){function o(e,t){function n(n){return t.tokenize=n,n(e,t)}var r=e.next();if("<"==r)return e.eat("!")?e.eat("[")?e.match("CDATA[")?n(s("atom","]]>")):null:e.match("--")?n(s("comment","-->")):e.match("DOCTYPE",!0,!0)?(e.eatWhile(/[\w\._\-]/),n(c(1))):null:e.eat("?")?(e.eatWhile(/[\w\._\-]/),t.tokenize=s("meta","?>"),"meta"):(T=e.eat("/")?"closeTag":"openTag",t.tokenize=a,"tag bracket");if("&"==r){var i;return i=e.eat("#")?e.eat("x")?e.eatWhile(/[a-fA-F\d]/)&&e.eat(";"):e.eatWhile(/[\d]/)&&e.eat(";"):e.eatWhile(/[\w\.\-:]/)&&e.eat(";"),i?"atom":"error"}return e.eatWhile(/[^&<]/),null}function a(e,t){var n=e.next();if(">"==n||"/"==n&&e.eat(">"))return t.tokenize=o,T=">"==n?"endTag":"selfcloseTag","tag bracket";if("="==n)return T="equals",null;if("<"==n){t.tokenize=o,t.state=d,t.tagName=t.tagStart=null;var r=t.tokenize(e,t);return r?r+" tag error":"tag error"}return/[\'\"]/.test(n)?(t.tokenize=l(n),t.stringStartCol=e.column(),t.tokenize(e,t)):(e.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/),"word")}function l(e){var t=function(t,n){for(;!t.eol();)if(t.next()==e){n.tokenize=a;break}return"string"};return t.isInAttribute=!0,t}function s(e,t){return function(n,r){for(;!n.eol();){if(n.match(t)){r.tokenize=o;break}n.next()}return e}}function c(e){return function(t,n){for(var r;null!=(r=t.next());){if("<"==r)return n.tokenize=c(e+1),n.tokenize(t,n);if(">"==r){if(1==e){n.tokenize=o;break}return n.tokenize=c(e-1),n.tokenize(t,n)}}return"meta"}}function u(e,t,n){this.prev=e.context,this.tagName=t,this.indent=e.indented,this.startOfLine=n,(S.doNotIndent.hasOwnProperty(t)||e.context&&e.context.noIndent)&&(this.noIndent=!0)}function f(e){e.context&&(e.context=e.context.prev)}function h(e,t){for(var n;;){if(!e.context)return;if(n=e.context.tagName,!S.contextGrabbers.hasOwnProperty(n)||!S.contextGrabbers[n].hasOwnProperty(t))return;f(e)}}function d(e,t,n){return"openTag"==e?(n.tagStart=t.column(),p):"closeTag"==e?m:d}function p(e,t,n){return"word"==e?(n.tagName=t.current(),M="tag",y):(M="error",p)}function m(e,t,n){if("word"==e){var r=t.current();return n.context&&n.context.tagName!=r&&S.implicitlyClosed.hasOwnProperty(n.context.tagName)&&f(n),n.context&&n.context.tagName==r||S.matchClosing===!1?(M="tag",g):(M="tag error",v)}return M="error",v}function g(e,t,n){return"endTag"!=e?(M="error",g):(f(n),d)}function v(e,t,n){return M="error",g(e,t,n)}function y(e,t,n){if("word"==e)return M="attribute",x;if("endTag"==e||"selfcloseTag"==e){var r=n.tagName,i=n.tagStart;return n.tagName=n.tagStart=null,"selfcloseTag"==e||S.autoSelfClosers.hasOwnProperty(r)?h(n,r):(h(n,r),n.context=new u(n,r,i==n.indented)),d}return M="error",y}function x(e,t,n){return"equals"==e?b:(S.allowMissing||(M="error"),y(e,t,n))}function b(e,t,n){return"string"==e?w:"word"==e&&S.allowUnquoted?(M="string",y):(M="error",y(e,t,n))}function w(e,t,n){return"string"==e?w:y(e,t,n)}var k=r.indentUnit,S={},C=i.htmlMode?t:n;for(var L in C)S[L]=C[L];for(var L in i)S[L]=i[L];var T,M;return o.isInText=!0,{startState:function(e){var t={tokenize:o,state:d,indented:e||0,tagName:null,tagStart:null,context:null};return null!=e&&(t.baseIndent=e),t},token:function(e,t){if(!t.tagName&&e.sol()&&(t.indented=e.indentation()),e.eatSpace())return null;T=null;var n=t.tokenize(e,t);return(n||T)&&"comment"!=n&&(M=null,t.state=t.state(T||n,e,t),M&&(n="error"==M?n+" error":M)),n},indent:function(t,n,r){var i=t.context;if(t.tokenize.isInAttribute)return t.tagStart==t.indented?t.stringStartCol+1:t.indented+k;if(i&&i.noIndent)return e.Pass;if(t.tokenize!=a&&t.tokenize!=o)return r?r.match(/^(\s*)/)[0].length:0;if(t.tagName)return S.multilineTagIndentPastTag!==!1?t.tagStart+t.tagName.length+2:t.tagStart+k*(S.multilineTagIndentFactor||1);if(S.alignCDATA&&/<!\[CDATA\[/.test(n))return 0;var l=n&&/^<(\/)?([\w_:\.-]*)/.exec(n);if(l&&l[1])for(;i;){if(i.tagName==l[2]){i=i.prev;break}if(!S.implicitlyClosed.hasOwnProperty(i.tagName))break;i=i.prev}else if(l)for(;i;){var s=S.contextGrabbers[i.tagName];if(!s||!s.hasOwnProperty(l[2]))break;i=i.prev}for(;i&&i.prev&&!i.startOfLine;)i=i.prev;return i?i.indent+k:t.baseIndent||0},electricInput:/<\/[\s\w:]+>$/,blockCommentStart:"<!--",blockCommentEnd:"-->",configuration:S.htmlMode?"html":"xml",helperType:S.htmlMode?"html":"xml",skipAttribute:function(e){e.state==b&&(e.state=y)}}}),e.defineMIME("text/xml","xml"),e.defineMIME("application/xml","xml"),e.mimeModes.hasOwnProperty("text/html")||e.defineMIME("text/html",{name:"xml",htmlMode:!0})})},{"../../lib/codemirror":10}],15:[function(e,t,n){n.read=function(e,t,n,r,i){var o,a,l=8*i-r-1,s=(1<<l)-1,c=s>>1,u=-7,f=n?i-1:0,h=n?-1:1,d=e[t+f];for(f+=h,o=d&(1<<-u)-1,d>>=-u,u+=l;u>0;o=256*o+e[t+f],f+=h,u-=8);for(a=o&(1<<-u)-1,o>>=-u,u+=r;u>0;a=256*a+e[t+f],f+=h,u-=8);if(0===o)o=1-c;else{if(o===s)return a?NaN:(d?-1:1)*(1/0);a+=Math.pow(2,r),o-=c}return(d?-1:1)*a*Math.pow(2,o-r)},n.write=function(e,t,n,r,i,o){var a,l,s,c=8*o-i-1,u=(1<<c)-1,f=u>>1,h=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,d=r?0:o-1,p=r?1:-1,m=0>t||0===t&&0>1/t?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(l=isNaN(t)?1:0,a=u):(a=Math.floor(Math.log(t)/Math.LN2),t*(s=Math.pow(2,-a))<1&&(a--,s*=2),t+=a+f>=1?h/s:h*Math.pow(2,1-f),t*s>=2&&(a++,s/=2),a+f>=u?(l=0,a=u):a+f>=1?(l=(t*s-1)*Math.pow(2,i),a+=f):(l=t*Math.pow(2,f-1)*Math.pow(2,i),a=0));i>=8;e[n+d]=255&l,d+=p,l/=256,i-=8);for(a=a<<i|l,c+=i;c>0;e[n+d]=255&a,d+=p,a/=256,c-=8);e[n+d-p]|=128*m}},{}],16:[function(e,t,n){var r={}.toString;t.exports=Array.isArray||function(e){return"[object Array]"==r.call(e)}},{}],17:[function(t,n,r){(function(t){(function(){function t(e){this.tokens=[],this.tokens.links={},this.options=e||h.defaults,this.rules=d.normal,this.options.gfm&&(this.options.tables?this.rules=d.tables:this.rules=d.gfm)}function i(e,t){if(this.options=t||h.defaults,this.links=e,this.rules=p.normal,this.renderer=this.options.renderer||new o,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=p.breaks:this.rules=p.gfm:this.options.pedantic&&(this.rules=p.pedantic)}function o(e){this.options=e||{}}function a(e){this.tokens=[],this.token=null,this.options=e||h.defaults,this.options.renderer=this.options.renderer||new o,this.renderer=this.options.renderer,this.renderer.options=this.options}function l(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function s(e){return e.replace(/&([#\w]+);/g,function(e,t){return t=t.toLowerCase(),"colon"===t?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function c(e,t){return e=e.source,t=t||"",function n(r,i){return r?(i=i.source||i,i=i.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,i),n):new RegExp(e,t)}}function u(){}function f(e){for(var t,n,r=1;r<arguments.length;r++){t=arguments[r];for(n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])}return e}function h(e,n,r){if(r||"function"==typeof n){r||(r=n,n=null),n=f({},h.defaults,n||{});var i,o,s=n.highlight,c=0;try{i=t.lex(e,n)}catch(u){return r(u)}o=i.length;var d=function(e){if(e)return n.highlight=s,r(e);var t;try{t=a.parse(i,n)}catch(o){e=o}return n.highlight=s,e?r(e):r(null,t)};if(!s||s.length<3)return d();if(delete n.highlight,!o)return d();for(;c<i.length;c++)!function(e){return"code"!==e.type?--o||d():s(e.text,e.lang,function(t,n){return t?d(t):null==n||n===e.text?--o||d():(e.text=n,e.escaped=!0,void(--o||d()))})}(i[c])}else try{return n&&(n=f({},h.defaults,n)),a.parse(t.lex(e,n),n)}catch(u){if(u.message+="\nPlease report this to https://github.com/chjj/marked.",(n||h.defaults).silent)return"<p>An error occured:</p><pre>"+l(u.message+"",!0)+"</pre>";throw u}}var d={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:u,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:u,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:u,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};d.bullet=/(?:[*+-]|\d+\.)/,d.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,d.item=c(d.item,"gm")(/bull/g,d.bullet)(),d.list=c(d.list)(/bull/g,d.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+d.def.source+")")(),d.blockquote=c(d.blockquote)("def",d.def)(),d._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b",d.html=c(d.html)("comment",/<!--[\s\S]*?-->/)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)(/tag/g,d._tag)(),d.paragraph=c(d.paragraph)("hr",d.hr)("heading",d.heading)("lheading",d.lheading)("blockquote",d.blockquote)("tag","<"+d._tag)("def",d.def)(),d.normal=f({},d),d.gfm=f({},d.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/}),d.gfm.paragraph=c(d.paragraph)("(?!","(?!"+d.gfm.fences.source.replace("\\1","\\2")+"|"+d.list.source.replace("\\1","\\3")+"|")(),d.tables=f({},d.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),t.rules=d,t.lex=function(e,n){var r=new t(n);return r.lex(e)},t.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},t.prototype.token=function(e,t,n){for(var r,i,o,a,l,s,c,u,f,e=e.replace(/^ +$/gm,"");e;)if((o=this.rules.newline.exec(e))&&(e=e.substring(o[0].length),o[0].length>1&&this.tokens.push({type:"space"})),o=this.rules.code.exec(e))e=e.substring(o[0].length),o=o[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?o:o.replace(/\n+$/,"")});else if(o=this.rules.fences.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"code",lang:o[2],text:o[3]||""});else if(o=this.rules.heading.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"heading",depth:o[1].length,text:o[2]});else if(t&&(o=this.rules.nptable.exec(e))){for(e=e.substring(o[0].length),s={type:"table",header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/\n$/,"").split("\n")},u=0;u<s.align.length;u++)/^ *-+: *$/.test(s.align[u])?s.align[u]="right":/^ *:-+: *$/.test(s.align[u])?s.align[u]="center":/^ *:-+ *$/.test(s.align[u])?s.align[u]="left":s.align[u]=null;for(u=0;u<s.cells.length;u++)s.cells[u]=s.cells[u].split(/ *\| */);this.tokens.push(s)}else if(o=this.rules.lheading.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"heading",depth:"="===o[2]?1:2,text:o[1]});else if(o=this.rules.hr.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"hr"});else if(o=this.rules.blockquote.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"blockquote_start"}),o=o[0].replace(/^ *> ?/gm,""),this.token(o,t,!0),this.tokens.push({type:"blockquote_end"});else if(o=this.rules.list.exec(e)){for(e=e.substring(o[0].length),a=o[2],this.tokens.push({type:"list_start",ordered:a.length>1}),o=o[0].match(this.rules.item),r=!1,f=o.length,u=0;f>u;u++)s=o[u],c=s.length,s=s.replace(/^ *([*+-]|\d+\.) +/,""),~s.indexOf("\n ")&&(c-=s.length,s=this.options.pedantic?s.replace(/^ {1,4}/gm,""):s.replace(new RegExp("^ {1,"+c+"}","gm"),"")),this.options.smartLists&&u!==f-1&&(l=d.bullet.exec(o[u+1])[0],a===l||a.length>1&&l.length>1||(e=o.slice(u+1).join("\n")+e,u=f-1)),i=r||/\n\n(?!\s*$)/.test(s),u!==f-1&&(r="\n"===s.charAt(s.length-1),i||(i=r)),this.tokens.push({type:i?"loose_item_start":"list_item_start"}),this.token(s,!1,n),this.tokens.push({type:"list_item_end"});this.tokens.push({type:"list_end"})}else if(o=this.rules.html.exec(e))e=e.substring(o[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&("pre"===o[1]||"script"===o[1]||"style"===o[1]),text:o[0]});else if(!n&&t&&(o=this.rules.def.exec(e)))e=e.substring(o[0].length),this.tokens.links[o[1].toLowerCase()]={href:o[2],title:o[3]};else if(t&&(o=this.rules.table.exec(e))){for(e=e.substring(o[0].length),s={type:"table", -header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u<s.align.length;u++)/^ *-+: *$/.test(s.align[u])?s.align[u]="right":/^ *:-+: *$/.test(s.align[u])?s.align[u]="center":/^ *:-+ *$/.test(s.align[u])?s.align[u]="left":s.align[u]=null;for(u=0;u<s.cells.length;u++)s.cells[u]=s.cells[u].replace(/^ *\| *| *\| *$/g,"").split(/ *\| */);this.tokens.push(s)}else if(t&&(o=this.rules.paragraph.exec(e)))e=e.substring(o[0].length),this.tokens.push({type:"paragraph",text:"\n"===o[1].charAt(o[1].length-1)?o[1].slice(0,-1):o[1]});else if(o=this.rules.text.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"text",text:o[0]});else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0));return this.tokens};var p={escape:/^\\([\\`*{}\[\]()#+\-.!_>])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:u,text:/^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/};p._inside=/(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/,p._href=/\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/,p.link=c(p.link)("inside",p._inside)("href",p._href)(),p.reflink=c(p.reflink)("inside",p._inside)(),p.normal=f({},p),p.pedantic=f({},p.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),p.gfm=f({},p.normal,{escape:c(p.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:c(p.text)("]|","~]|")("|","|https?://|")()}),p.breaks=f({},p.gfm,{br:c(p.br)("{2,}","*")(),text:c(p.gfm.text)("{2,}","*")()}),i.rules=p,i.output=function(e,t,n){var r=new i(t,n);return r.output(e)},i.prototype.output=function(e){for(var t,n,r,i,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=l(i[1]),r=n),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^<a /i.test(i[0])?this.inLink=!0:this.inLink&&/^<\/a>/i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):l(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,o+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(l(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),o+=this.renderer.text(l(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=l(i[1]),r=n,o+=this.renderer.link(r,null,n);return o},i.prototype.outputLink=function(e,t){var n=l(t.href),r=t.title?l(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,l(e[1]))},i.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014\/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014\/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},i.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;r>i;i++)t=e.charCodeAt(i),Math.random()>.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},o.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'<pre><code class="'+this.options.langPrefix+l(t,!0)+'">'+(n?e:l(e,!0))+"\n</code></pre>\n":"<pre><code>"+(n?e:l(e,!0))+"\n</code></pre>"},o.prototype.blockquote=function(e){return"<blockquote>\n"+e+"</blockquote>\n"},o.prototype.html=function(e){return e},o.prototype.heading=function(e,t,n){return"<h"+t+' id="'+this.options.headerPrefix+n.toLowerCase().replace(/[^\w]+/g,"-")+'">'+e+"</h"+t+">\n"},o.prototype.hr=function(){return this.options.xhtml?"<hr/>\n":"<hr>\n"},o.prototype.list=function(e,t){var n=t?"ol":"ul";return"<"+n+">\n"+e+"</"+n+">\n"},o.prototype.listitem=function(e){return"<li>"+e+"</li>\n"},o.prototype.paragraph=function(e){return"<p>"+e+"</p>\n"},o.prototype.table=function(e,t){return"<table>\n<thead>\n"+e+"</thead>\n<tbody>\n"+t+"</tbody>\n</table>\n"},o.prototype.tablerow=function(e){return"<tr>\n"+e+"</tr>\n"},o.prototype.tablecell=function(e,t){var n=t.header?"th":"td",r=t.align?"<"+n+' style="text-align:'+t.align+'">':"<"+n+">";return r+e+"</"+n+">\n"},o.prototype.strong=function(e){return"<strong>"+e+"</strong>"},o.prototype.em=function(e){return"<em>"+e+"</em>"},o.prototype.codespan=function(e){return"<code>"+e+"</code>"},o.prototype.br=function(){return this.options.xhtml?"<br/>":"<br>"},o.prototype.del=function(e){return"<del>"+e+"</del>"},o.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(s(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(i){return""}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:"))return""}var o='<a href="'+e+'"';return t&&(o+=' title="'+t+'"'),o+=">"+n+"</a>"},o.prototype.image=function(e,t,n){var r='<img src="'+e+'" alt="'+n+'"';return t&&(r+=' title="'+t+'"'),r+=this.options.xhtml?"/>":">"},o.prototype.text=function(e){return e},a.parse=function(e,t,n){var r=new a(t,n);return r.parse(e)},a.prototype.parse=function(e){this.inline=new i(e.links,this.options,this.renderer),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},a.prototype.next=function(){return this.token=this.tokens.pop()},a.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},a.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},a.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,i,o="",a="";for(n="",e=0;e<this.token.header.length;e++)r={header:!0,align:this.token.align[e]},n+=this.renderer.tablecell(this.inline.output(this.token.header[e]),{header:!0,align:this.token.align[e]});for(o+=this.renderer.tablerow(n),e=0;e<this.token.cells.length;e++){for(t=this.token.cells[e],n="",i=0;i<t.length;i++)n+=this.renderer.tablecell(this.inline.output(t[i]),{header:!1,align:this.token.align[i]});a+=this.renderer.tablerow(n)}return this.renderer.table(o,a);case"blockquote_start":for(var a="";"blockquote_end"!==this.next().type;)a+=this.tok();return this.renderer.blockquote(a);case"list_start":for(var a="",l=this.token.ordered;"list_end"!==this.next().type;)a+=this.tok();return this.renderer.list(a,l);case"list_item_start":for(var a="";"list_item_end"!==this.next().type;)a+="text"===this.token.type?this.parseText():this.tok();return this.renderer.listitem(a);case"loose_item_start":for(var a="";"list_item_end"!==this.next().type;)a+=this.tok();return this.renderer.listitem(a);case"html":var s=this.token.pre||this.options.pedantic?this.token.text:this.inline.output(this.token.text);return this.renderer.html(s);case"paragraph":return this.renderer.paragraph(this.inline.output(this.token.text));case"text":return this.renderer.paragraph(this.parseText())}},u.exec=u,h.options=h.setOptions=function(e){return f(h.defaults,e),h},h.defaults={gfm:!0,tables:!0,breaks:!1,pedantic:!1,sanitize:!1,sanitizer:null,mangle:!0,smartLists:!1,silent:!1,highlight:null,langPrefix:"lang-",smartypants:!1,headerPrefix:"",renderer:new o,xhtml:!1},h.Parser=a,h.parser=a.parse,h.Renderer=o,h.Lexer=t,h.lexer=t.lex,h.InlineLexer=i,h.inlineLexer=i.output,h.parse=h,"undefined"!=typeof n&&"object"==typeof r?n.exports=h:"function"==typeof e&&e.amd?e(function(){return h}):this.marked=h}).call(function(){return this||("undefined"!=typeof window?window:t)}())}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],18:[function(e,t,n){(function(n,r){"use strict";var i=function(e,t,n,i){if(i=i||{},this.dictionary=null,this.rules={},this.dictionaryTable={},this.compoundRules=[],this.compoundRuleCodes={},this.replacementTable=[],this.flags=i.flags||{},e){if(this.dictionary=e,"undefined"!=typeof window&&"chrome"in window&&"extension"in window.chrome&&"getURL"in window.chrome.extension)t||(t=this._readFile(chrome.extension.getURL("lib/typo/dictionaries/"+e+"/"+e+".aff"))),n||(n=this._readFile(chrome.extension.getURL("lib/typo/dictionaries/"+e+"/"+e+".dic")));else{if(i.dictionaryPath)var o=i.dictionaryPath;else if("undefined"!=typeof r)var o=r+"/dictionaries";else var o="./dictionaries";t||(t=this._readFile(o+"/"+e+"/"+e+".aff")),n||(n=this._readFile(o+"/"+e+"/"+e+".dic"))}this.rules=this._parseAFF(t),this.compoundRuleCodes={};for(var a=0,l=this.compoundRules.length;l>a;a++)for(var s=this.compoundRules[a],c=0,u=s.length;u>c;c++)this.compoundRuleCodes[s[c]]=[];"ONLYINCOMPOUND"in this.flags&&(this.compoundRuleCodes[this.flags.ONLYINCOMPOUND]=[]),this.dictionaryTable=this._parseDIC(n);for(var a in this.compoundRuleCodes)0==this.compoundRuleCodes[a].length&&delete this.compoundRuleCodes[a];for(var a=0,l=this.compoundRules.length;l>a;a++){for(var f=this.compoundRules[a],h="",c=0,u=f.length;u>c;c++){var d=f[c];h+=d in this.compoundRuleCodes?"("+this.compoundRuleCodes[d].join("|")+")":d}this.compoundRules[a]=new RegExp(h,"i")}}return this};i.prototype={load:function(e){for(var t in e)this[t]=e[t];return this},_readFile:function(t,r){if(r||(r="utf8"),"undefined"!=typeof XMLHttpRequest){var i=new XMLHttpRequest;return i.open("GET",t,!1),i.overrideMimeType&&i.overrideMimeType("text/plain; charset="+r),i.send(null),i.responseText}if("undefined"!=typeof e){var o=e("fs");try{if(o.existsSync(t)){var a=o.statSync(t),l=o.openSync(t,"r"),s=new n(a.size);return o.readSync(l,s,0,s.length,null),s.toString(r,0,s.length)}console.log("Path "+t+" does not exist.")}catch(c){return console.log(c),""}}},_parseAFF:function(e){var t={};e=this._removeAffixComments(e);for(var n=e.split("\n"),r=0,i=n.length;i>r;r++){var o=n[r],a=o.split(/\s+/),l=a[0];if("PFX"==l||"SFX"==l){for(var s=a[1],c=a[2],u=parseInt(a[3],10),f=[],h=r+1,d=r+1+u;d>h;h++){var o=n[h],p=o.split(/\s+/),m=p[2],g=p[3].split("/"),v=g[0];"0"===v&&(v="");var y=this.parseRuleCodes(g[1]),x=p[4],b={};b.add=v,y.length>0&&(b.continuationClasses=y),"."!==x&&("SFX"===l?b.match=new RegExp(x+"$"):b.match=new RegExp("^"+x)),"0"!=m&&("SFX"===l?b.remove=new RegExp(m+"$"):b.remove=m),f.push(b)}t[s]={type:l,combineable:"Y"==c,entries:f},r+=u}else if("COMPOUNDRULE"===l){for(var u=parseInt(a[1],10),h=r+1,d=r+1+u;d>h;h++){var o=n[h],p=o.split(/\s+/);this.compoundRules.push(p[1])}r+=u}else if("REP"===l){var p=o.split(/\s+/);3===p.length&&this.replacementTable.push([p[1],p[2]])}else this.flags[l]=a[1]}return t},_removeAffixComments:function(e){return e=e.replace(/#.*$/gm,""),e=e.replace(/^\s\s*/m,"").replace(/\s\s*$/m,""),e=e.replace(/\n{2,}/g,"\n"),e=e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")},_parseDIC:function(e){function t(e,t){e in r&&"object"==typeof r[e]||(r[e]=[]),r[e].push(t)}e=this._removeDicComments(e);for(var n=e.split("\n"),r={},i=1,o=n.length;o>i;i++){var a=n[i],l=a.split("/",2),s=l[0];if(l.length>1){var c=this.parseRuleCodes(l[1]);"NEEDAFFIX"in this.flags&&-1!=c.indexOf(this.flags.NEEDAFFIX)||t(s,c);for(var u=0,f=c.length;f>u;u++){var h=c[u],d=this.rules[h];if(d)for(var p=this._applyRule(s,d),m=0,g=p.length;g>m;m++){var v=p[m];if(t(v,[]),d.combineable)for(var y=u+1;f>y;y++){var x=c[y],b=this.rules[x];if(b&&b.combineable&&d.type!=b.type)for(var w=this._applyRule(v,b),k=0,S=w.length;S>k;k++){var C=w[k];t(C,[])}}}h in this.compoundRuleCodes&&this.compoundRuleCodes[h].push(s)}}else t(s.trim(),[])}return r},_removeDicComments:function(e){return e=e.replace(/^\t.*$/gm,"")},parseRuleCodes:function(e){if(!e)return[];if(!("FLAG"in this.flags))return e.split("");if("long"===this.flags.FLAG){for(var t=[],n=0,r=e.length;r>n;n+=2)t.push(e.substr(n,2));return t}return"num"===this.flags.FLAG?textCode.split(","):void 0},_applyRule:function(e,t){for(var n=t.entries,r=[],i=0,o=n.length;o>i;i++){var a=n[i];if(!a.match||e.match(a.match)){var l=e;if(a.remove&&(l=l.replace(a.remove,"")),"SFX"===t.type?l+=a.add:l=a.add+l,r.push(l),"continuationClasses"in a)for(var s=0,c=a.continuationClasses.length;c>s;s++){var u=this.rules[a.continuationClasses[s]];u&&(r=r.concat(this._applyRule(l,u)))}}}return r},check:function(e){var t=e.replace(/^\s\s*/,"").replace(/\s\s*$/,"");if(this.checkExact(t))return!0;if(t.toUpperCase()===t){var n=t[0]+t.substring(1).toLowerCase();if(this.hasFlag(n,"KEEPCASE"))return!1;if(this.checkExact(n))return!0}var r=t.toLowerCase();if(r!==t){if(this.hasFlag(r,"KEEPCASE"))return!1;if(this.checkExact(r))return!0}return!1},checkExact:function(e){var t=this.dictionaryTable[e];if("undefined"==typeof t){if("COMPOUNDMIN"in this.flags&&e.length>=this.flags.COMPOUNDMIN)for(var n=0,r=this.compoundRules.length;r>n;n++)if(e.match(this.compoundRules[n]))return!0;return!1}if("object"==typeof t){for(var n=0,r=t.length;r>n;n++)if(!this.hasFlag(e,"ONLYINCOMPOUND",t[n]))return!0;return!1}},hasFlag:function(e,t,n){if(t in this.flags){if("undefined"==typeof n)var n=Array.prototype.concat.apply([],this.dictionaryTable[e]);if(n&&-1!==n.indexOf(this.flags[t]))return!0}return!1},alphabet:"",suggest:function(e,t){function n(e){for(var t=[],n=0,r=e.length;r>n;n++){for(var i=e[n],o=[],a=0,l=i.length+1;l>a;a++)o.push([i.substring(0,a),i.substring(a,i.length)]);for(var s=[],a=0,l=o.length;l>a;a++){var u=o[a];u[1]&&s.push(u[0]+u[1].substring(1))}for(var f=[],a=0,l=o.length;l>a;a++){var u=o[a];u[1].length>1&&f.push(u[0]+u[1][1]+u[1][0]+u[1].substring(2))}for(var h=[],a=0,l=o.length;l>a;a++){var u=o[a];if(u[1])for(var d=0,p=c.alphabet.length;p>d;d++)h.push(u[0]+c.alphabet[d]+u[1].substring(1))}for(var m=[],a=0,l=o.length;l>a;a++){var u=o[a];if(u[1])for(var d=0,p=c.alphabet.length;p>d;d++)h.push(u[0]+c.alphabet[d]+u[1])}t=t.concat(s),t=t.concat(f),t=t.concat(h),t=t.concat(m)}return t}function r(e){for(var t=[],n=0;n<e.length;n++)c.check(e[n])&&t.push(e[n]);return t}function i(e){function i(e,t){return e[1]<t[1]?-1:1}for(var o=n([e]),a=n(o),l=r(o).concat(r(a)),s={},u=0,f=l.length;f>u;u++)l[u]in s?s[l[u]]+=1:s[l[u]]=1;var h=[];for(var u in s)h.push([u,s[u]]);h.sort(i).reverse();for(var d=[],u=0,f=Math.min(t,h.length);f>u;u++)c.hasFlag(h[u][0],"NOSUGGEST")||d.push(h[u][0]);return d}if(t||(t=5),this.check(e))return[];for(var o=0,a=this.replacementTable.length;a>o;o++){var l=this.replacementTable[o];if(-1!==e.indexOf(l[0])){var s=e.replace(l[0],l[1]);if(this.check(s))return[s]}}var c=this;return c.alphabet="abcdefghijklmnopqrstuvwxyz",i(e)}},"undefined"!=typeof t&&(t.exports=i)}).call(this,e("buffer").Buffer,"/node_modules/typo-js")},{buffer:3,fs:2}],19:[function(e,t,n){var r=e("codemirror");r.commands.tabAndIndentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentMore");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}},r.commands.shiftTabAndUnindentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentLess");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}}},{codemirror:10}],20:[function(e,t,n){"use strict";function r(e){return e=U?e.replace("Ctrl","Cmd"):e.replace("Cmd","Ctrl")}function i(e,t,n){e=e||{};var r=document.createElement("a");return t=void 0==t?!0:t,e.title&&t&&(r.title=a(e.title,e.action,n),U&&(r.title=r.title.replace("Ctrl","⌘"),r.title=r.title.replace("Alt","⌥"))),r.tabIndex=-1,r.className=e.className,r}function o(){var e=document.createElement("i");return e.className="separator",e.innerHTML="|",e}function a(e,t,n){var i,o=e;return t&&(i=Y(t),n[i]&&(o+=" ("+r(n[i])+")")),o}function l(e,t){t=t||e.getCursor("start");var n=e.getTokenAt(t);if(!n.type)return{};for(var r,i,o=n.type.split(" "),a={},l=0;l<o.length;l++)r=o[l],"strong"===r?a.bold=!0:"variable-2"===r?(i=e.getLine(t.line),/^\s*\d+\.\s/.test(i)?a["ordered-list"]=!0:a["unordered-list"]=!0):"atom"===r?a.quote=!0:"em"===r?a.italic=!0:"quote"===r?a.quote=!0:"strikethrough"===r?a.strikethrough=!0:"comment"===r?a.code=!0:"link"===r?a.link=!0:"tag"===r?a.image=!0:r.match(/^header(\-[1-6])?$/)&&(a[r.replace("header","heading")]=!0);return a}function s(e){var t=e.codemirror;t.setOption("fullScreen",!t.getOption("fullScreen")),t.getOption("fullScreen")?(V=document.body.style.overflow,document.body.style.overflow="hidden"):document.body.style.overflow=V;var n=t.getWrapperElement();/fullscreen/.test(n.previousSibling.className)?n.previousSibling.className=n.previousSibling.className.replace(/\s*fullscreen\b/,""):n.previousSibling.className+=" fullscreen";var r=e.toolbarElements.fullscreen;/active/.test(r.className)?r.className=r.className.replace(/\s*active\s*/g,""):r.className+=" active";var i=t.getWrapperElement().nextSibling;/editor-preview-active-side/.test(i.className)&&N(e)}function c(e){P(e,"bold",e.options.blockStyles.bold)}function u(e){P(e,"italic",e.options.blockStyles.italic)}function f(e){P(e,"strikethrough","~~")}function h(e){function t(e){if("object"!=typeof e)throw"fencing_line() takes a 'line' object (not a line number, or line text). Got: "+typeof e+": "+e;return e.styles&&e.styles[2]&&-1!==e.styles[2].indexOf("formatting-code-block")}function n(e){return e.state.base.base||e.state.base}function r(e,r,i,o,a){i=i||e.getLineHandle(r),o=o||e.getTokenAt({line:r,ch:1}),a=a||!!i.text&&e.getTokenAt({line:r,ch:i.text.length-1});var l=o.type?o.type.split(" "):[];return a&&n(a).indentedCode?"indented":-1===l.indexOf("comment")?!1:n(o).fencedChars||n(a).fencedChars||t(i)?"fenced":"single"}function i(e,t,n,r){var i=t.line+1,o=n.line+1,a=t.line!==n.line,l=r+"\n",s="\n"+r;a&&o++,a&&0===n.ch&&(s=r+"\n",o--),E(e,!1,[l,s]),e.setSelection({line:i,ch:0},{line:o,ch:0})}var o,a,l,s=e.options.blockStyles.code,c=e.codemirror,u=c.getCursor("start"),f=c.getCursor("end"),h=c.getTokenAt({line:u.line,ch:u.ch||1}),d=c.getLineHandle(u.line),p=r(c,u.line,d,h);if("single"===p){var m=d.text.slice(0,u.ch).replace("`",""),g=d.text.slice(u.ch).replace("`","");c.replaceRange(m+g,{line:u.line,ch:0},{line:u.line,ch:99999999999999}),u.ch--,u!==f&&f.ch--,c.setSelection(u,f),c.focus()}else if("fenced"===p)if(u.line!==f.line||u.ch!==f.ch){for(o=u.line;o>=0&&(d=c.getLineHandle(o),!t(d));o--);var v,y,x,b,w=c.getTokenAt({line:o,ch:1}),k=n(w).fencedChars;t(c.getLineHandle(u.line))?(v="",y=u.line):t(c.getLineHandle(u.line-1))?(v="",y=u.line-1):(v=k+"\n",y=u.line),t(c.getLineHandle(f.line))?(x="",b=f.line,0===f.ch&&(b+=1)):0!==f.ch&&t(c.getLineHandle(f.line+1))?(x="",b=f.line+1):(x=k+"\n",b=f.line+1),0===f.ch&&(b-=1),c.operation(function(){c.replaceRange(x,{line:b,ch:0},{line:b+(x?0:1),ch:0}),c.replaceRange(v,{line:y,ch:0},{line:y+(v?0:1),ch:0})}),c.setSelection({line:y+(v?1:0),ch:0},{line:b+(v?1:-1),ch:0}),c.focus()}else{var S=u.line;if(t(c.getLineHandle(u.line))&&("fenced"===r(c,u.line+1)?(o=u.line,S=u.line+1):(a=u.line,S=u.line-1)),void 0===o)for(o=S;o>=0&&(d=c.getLineHandle(o),!t(d));o--);if(void 0===a)for(l=c.lineCount(),a=S;l>a&&(d=c.getLineHandle(a),!t(d));a++);c.operation(function(){c.replaceRange("",{line:o,ch:0},{line:o+1,ch:0}),c.replaceRange("",{line:a-1,ch:0},{line:a,ch:0})}),c.focus()}else if("indented"===p){if(u.line!==f.line||u.ch!==f.ch)o=u.line,a=f.line,0===f.ch&&a--;else{for(o=u.line;o>=0;o--)if(d=c.getLineHandle(o),!d.text.match(/^\s*$/)&&"indented"!==r(c,o,d)){o+=1;break}for(l=c.lineCount(),a=u.line;l>a;a++)if(d=c.getLineHandle(a),!d.text.match(/^\s*$/)&&"indented"!==r(c,a,d)){a-=1;break}}var C=c.getLineHandle(a+1),L=C&&c.getTokenAt({line:a+1,ch:C.text.length-1}),T=L&&n(L).indentedCode;T&&c.replaceRange("\n",{line:a+1,ch:0});for(var M=o;a>=M;M++)c.indentLine(M,"subtract");c.focus()}else{var N=u.line===f.line&&u.ch===f.ch&&0===u.ch,A=u.line!==f.line;N||A?i(c,u,f,s):E(c,!1,["`","`"])}}function d(e){var t=e.codemirror;I(t,"quote")}function p(e){var t=e.codemirror;O(t,"smaller")}function m(e){var t=e.codemirror;O(t,"bigger")}function g(e){var t=e.codemirror;O(t,void 0,1)}function v(e){var t=e.codemirror;O(t,void 0,2)}function y(e){var t=e.codemirror;O(t,void 0,3)}function x(e){var t=e.codemirror;I(t,"unordered-list")}function b(e){var t=e.codemirror;I(t,"ordered-list")}function w(e){var t=e.codemirror;R(t)}function k(e){var t=e.codemirror,n=l(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.link),!i)?!1:void E(t,n.link,r.insertTexts.link,i)}function S(e){var t=e.codemirror,n=l(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.image),!i)?!1:void E(t,n.image,r.insertTexts.image,i)}function C(e){var t=e.codemirror,n=l(t),r=e.options;E(t,n.table,r.insertTexts.table)}function L(e){var t=e.codemirror,n=l(t),r=e.options;E(t,n.image,r.insertTexts.horizontalRule)}function T(e){var t=e.codemirror;t.undo(),t.focus()}function M(e){var t=e.codemirror;t.redo(),t.focus()}function N(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.nextSibling,i=e.toolbarElements["side-by-side"],o=!1;/editor-preview-active-side/.test(r.className)?(r.className=r.className.replace(/\s*editor-preview-active-side\s*/g,""),i.className=i.className.replace(/\s*active\s*/g,""),n.className=n.className.replace(/\s*CodeMirror-sided\s*/g," ")):(setTimeout(function(){t.getOption("fullScreen")||s(e),r.className+=" editor-preview-active-side"},1),i.className+=" active",n.className+=" CodeMirror-sided",o=!0);var a=n.lastChild;if(/editor-preview-active/.test(a.className)){a.className=a.className.replace(/\s*editor-preview-active\s*/g,"");var l=e.toolbarElements.preview,c=n.previousSibling;l.className=l.className.replace(/\s*active\s*/g,""),c.className=c.className.replace(/\s*disabled-for-preview*/g,"")}var u=function(){r.innerHTML=e.options.previewRender(e.value(),r)};t.sideBySideRenderingFunction||(t.sideBySideRenderingFunction=u),o?(r.innerHTML=e.options.previewRender(e.value(),r),t.on("update",t.sideBySideRenderingFunction)):t.off("update",t.sideBySideRenderingFunction),t.refresh()}function A(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.previousSibling,i=e.options.toolbar?e.toolbarElements.preview:!1,o=n.lastChild;o&&/editor-preview/.test(o.className)||(o=document.createElement("div"),o.className="editor-preview",n.appendChild(o)),/editor-preview-active/.test(o.className)?(o.className=o.className.replace(/\s*editor-preview-active\s*/g,""),i&&(i.className=i.className.replace(/\s*active\s*/g,""),r.className=r.className.replace(/\s*disabled-for-preview*/g,""))):(setTimeout(function(){o.className+=" editor-preview-active"},1),i&&(i.className+=" active",r.className+=" disabled-for-preview")),o.innerHTML=e.options.previewRender(e.value(),o);var a=t.getWrapperElement().nextSibling;/editor-preview-active-side/.test(a.className)&&N(e)}function E(e,t,n,r){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){var i,o=n[0],a=n[1],l=e.getCursor("start"),s=e.getCursor("end");r&&(a=a.replace("#url#",r)),t?(i=e.getLine(l.line),o=i.slice(0,l.ch),a=i.slice(l.ch),e.replaceRange(o+a,{line:l.line,ch:0})):(i=e.getSelection(),e.replaceSelection(o+i+a),l.ch+=o.length,l!==s&&(s.ch+=o.length)),e.setSelection(l,s),e.focus()}}function O(e,t,n){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var r=e.getCursor("start"),i=e.getCursor("end"),o=r.line;o<=i.line;o++)!function(r){var i=e.getLine(r),o=i.search(/[^#]/);i=void 0!==t?0>=o?"bigger"==t?"###### "+i:"# "+i:6==o&&"smaller"==t?i.substr(7):1==o&&"bigger"==t?i.substr(2):"bigger"==t?i.substr(1):"#"+i:1==n?0>=o?"# "+i:o==n?i.substr(o+1):"# "+i.substr(o+1):2==n?0>=o?"## "+i:o==n?i.substr(o+1):"## "+i.substr(o+1):0>=o?"### "+i:o==n?i.substr(o+1):"### "+i.substr(o+1),e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(o);e.focus()}}function I(e,t){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var n=l(e),r=e.getCursor("start"),i=e.getCursor("end"),o={quote:/^(\s*)\>\s+/,"unordered-list":/^(\s*)(\*|\-|\+)\s+/,"ordered-list":/^(\s*)\d+\.\s+/},a={quote:"> ","unordered-list":"* ","ordered-list":"1. "},s=r.line;s<=i.line;s++)!function(r){var i=e.getLine(r);i=n[t]?i.replace(o[t],"$1"):a[t]+i,e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(s);e.focus()}}function P(e,t,n,r){if(!/editor-preview-active/.test(e.codemirror.getWrapperElement().lastChild.className)){r="undefined"==typeof r?n:r;var i,o=e.codemirror,a=l(o),s=n,c=r,u=o.getCursor("start"),f=o.getCursor("end");a[t]?(i=o.getLine(u.line),s=i.slice(0,u.ch),c=i.slice(u.ch),"bold"==t?(s=s.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/,""),c=c.replace(/(\*\*|__)/,"")):"italic"==t?(s=s.replace(/(\*|_)(?![\s\S]*(\*|_))/,""),c=c.replace(/(\*|_)/,"")):"strikethrough"==t&&(s=s.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/,""),c=c.replace(/(\*\*|~~)/,"")),o.replaceRange(s+c,{line:u.line,ch:0},{line:u.line,ch:99999999999999}),"bold"==t||"strikethrough"==t?(u.ch-=2,u!==f&&(f.ch-=2)):"italic"==t&&(u.ch-=1,u!==f&&(f.ch-=1))):(i=o.getSelection(),"bold"==t?(i=i.split("**").join(""),i=i.split("__").join("")):"italic"==t?(i=i.split("*").join(""),i=i.split("_").join("")):"strikethrough"==t&&(i=i.split("~~").join("")),o.replaceSelection(s+i+c),u.ch+=n.length,f.ch=u.ch+i.length),o.setSelection(u,f),o.focus()}}function R(e){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className))for(var t,n=e.getCursor("start"),r=e.getCursor("end"),i=n.line;i<=r.line;i++)t=e.getLine(i),t=t.replace(/^[ ]*([# ]+|\*|\-|[> ]+|[0-9]+(.|\)))[ ]*/,""),e.replaceRange(t,{line:i,ch:0},{line:i,ch:99999999999999})}function D(e,t){for(var n in t)t.hasOwnProperty(n)&&(t[n]instanceof Array?e[n]=t[n].concat(e[n]instanceof Array?e[n]:[]):null!==t[n]&&"object"==typeof t[n]&&t[n].constructor===Object?e[n]=D(e[n]||{},t[n]):e[n]=t[n]);return e}function H(e){for(var t=1;t<arguments.length;t++)e=D(e,arguments[t]);return e}function W(e){var t=/[a-zA-Z0-9_\u0392-\u03c9\u0410-\u04F9]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g,n=e.match(t),r=0;if(null===n)return r;for(var i=0;i<n.length;i++)r+=n[i].charCodeAt(0)>=19968?n[i].length:1;return r}function B(e){e=e||{},e.parent=this;var t=!0;if(e.autoDownloadFontAwesome===!1&&(t=!1),e.autoDownloadFontAwesome!==!0)for(var n=document.styleSheets,r=0;r<n.length;r++)n[r].href&&n[r].href.indexOf("//maxcdn.bootstrapcdn.com/font-awesome/")>-1&&(t=!1);if(t){var i=document.createElement("link");i.rel="stylesheet",i.href="https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css",document.getElementsByTagName("head")[0].appendChild(i)}if(e.element)this.element=e.element;else if(null===e.element)return void console.log("SimpleMDE: Error. No element was found.");if(void 0===e.toolbar){e.toolbar=[];for(var o in K)K.hasOwnProperty(o)&&(-1!=o.indexOf("separator-")&&e.toolbar.push("|"),(K[o]["default"]===!0||e.showIcons&&e.showIcons.constructor===Array&&-1!=e.showIcons.indexOf(o))&&e.toolbar.push(o))}e.hasOwnProperty("status")||(e.status=["autosave","lines","words","cursor"]),e.previewRender||(e.previewRender=function(e){return this.parent.markdown(e)}),e.parsingConfig=H({highlightFormatting:!0},e.parsingConfig||{}),e.insertTexts=H({},X,e.insertTexts||{}),e.promptTexts=Z,e.blockStyles=H({},J,e.blockStyles||{}),e.shortcuts=H({},G,e.shortcuts||{}),void 0!=e.autosave&&void 0!=e.autosave.unique_id&&""!=e.autosave.unique_id&&(e.autosave.uniqueId=e.autosave.unique_id),this.options=e,this.render(),!e.initialValue||this.options.autosave&&this.options.autosave.foundSavedValue===!0||this.value(e.initialValue)}function _(){if("object"!=typeof localStorage)return!1;try{localStorage.setItem("smde_localStorage",1),localStorage.removeItem("smde_localStorage")}catch(e){return!1}return!0}var F=e("codemirror");e("codemirror/addon/edit/continuelist.js"),e("./codemirror/tablist"),e("codemirror/addon/display/fullscreen.js"),e("codemirror/mode/markdown/markdown.js"),e("codemirror/addon/mode/overlay.js"),e("codemirror/addon/display/placeholder.js"),e("codemirror/addon/selection/mark-selection.js"),e("codemirror/mode/gfm/gfm.js"),e("codemirror/mode/xml/xml.js");var z=e("codemirror-spell-checker"),j=e("marked"),U=/Mac/.test(navigator.platform),q={toggleBold:c,toggleItalic:u,drawLink:k,toggleHeadingSmaller:p,toggleHeadingBigger:m,drawImage:S,toggleBlockquote:d,toggleOrderedList:b,toggleUnorderedList:x,toggleCodeBlock:h,togglePreview:A,toggleStrikethrough:f,toggleHeading1:g,toggleHeading2:v,toggleHeading3:y,cleanBlock:w,drawTable:C,drawHorizontalRule:L,undo:T,redo:M,toggleSideBySide:N,toggleFullScreen:s},G={toggleBold:"Cmd-B",toggleItalic:"Cmd-I",drawLink:"Cmd-K",toggleHeadingSmaller:"Cmd-H",toggleHeadingBigger:"Shift-Cmd-H",cleanBlock:"Cmd-E",drawImage:"Cmd-Alt-I",toggleBlockquote:"Cmd-'",toggleOrderedList:"Cmd-Alt-L",toggleUnorderedList:"Cmd-L",toggleCodeBlock:"Cmd-Alt-C",togglePreview:"Cmd-P",toggleSideBySide:"F9",toggleFullScreen:"F11"},Y=function(e){for(var t in q)if(q[t]===e)return t;return null},$=function(){var e=!1;return function(t){(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(t)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(t.substr(0,4)))&&(e=!0); -}(navigator.userAgent||navigator.vendor||window.opera),e},V="",K={bold:{name:"bold",action:c,className:"fa fa-bold",title:"Bold","default":!0},italic:{name:"italic",action:u,className:"fa fa-italic",title:"Italic","default":!0},strikethrough:{name:"strikethrough",action:f,className:"fa fa-strikethrough",title:"Strikethrough"},heading:{name:"heading",action:p,className:"fa fa-header",title:"Heading","default":!0},"heading-smaller":{name:"heading-smaller",action:p,className:"fa fa-header fa-header-x fa-header-smaller",title:"Smaller Heading"},"heading-bigger":{name:"heading-bigger",action:m,className:"fa fa-header fa-header-x fa-header-bigger",title:"Bigger Heading"},"heading-1":{name:"heading-1",action:g,className:"fa fa-header fa-header-x fa-header-1",title:"Big Heading"},"heading-2":{name:"heading-2",action:v,className:"fa fa-header fa-header-x fa-header-2",title:"Medium Heading"},"heading-3":{name:"heading-3",action:y,className:"fa fa-header fa-header-x fa-header-3",title:"Small Heading"},"separator-1":{name:"separator-1"},code:{name:"code",action:h,className:"fa fa-code",title:"Code"},quote:{name:"quote",action:d,className:"fa fa-quote-left",title:"Quote","default":!0},"unordered-list":{name:"unordered-list",action:x,className:"fa fa-list-ul",title:"Generic List","default":!0},"ordered-list":{name:"ordered-list",action:b,className:"fa fa-list-ol",title:"Numbered List","default":!0},"clean-block":{name:"clean-block",action:w,className:"fa fa-eraser fa-clean-block",title:"Clean block"},"separator-2":{name:"separator-2"},link:{name:"link",action:k,className:"fa fa-link",title:"Create Link","default":!0},image:{name:"image",action:S,className:"fa fa-picture-o",title:"Insert Image","default":!0},table:{name:"table",action:C,className:"fa fa-table",title:"Insert Table"},"horizontal-rule":{name:"horizontal-rule",action:L,className:"fa fa-minus",title:"Insert Horizontal Line"},"separator-3":{name:"separator-3"},preview:{name:"preview",action:A,className:"fa fa-eye no-disable",title:"Toggle Preview","default":!0},"side-by-side":{name:"side-by-side",action:N,className:"fa fa-columns no-disable no-mobile",title:"Toggle Side by Side","default":!0},fullscreen:{name:"fullscreen",action:s,className:"fa fa-arrows-alt no-disable no-mobile",title:"Toggle Fullscreen","default":!0},"separator-4":{name:"separator-4"},guide:{name:"guide",action:"https://simplemde.com/markdown-guide",className:"fa fa-question-circle",title:"Markdown Guide","default":!0},"separator-5":{name:"separator-5"},undo:{name:"undo",action:T,className:"fa fa-undo no-disable",title:"Undo"},redo:{name:"redo",action:M,className:"fa fa-repeat no-disable",title:"Redo"}},X={link:["[","](#url#)"],image:[""],table:["","\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],horizontalRule:["","\n\n-----\n\n"]},Z={link:"URL for the link:",image:"URL of the image:"},J={bold:"**",code:"```",italic:"*"};B.prototype.markdown=function(e){if(j){var t={};return this.options&&this.options.renderingConfig&&this.options.renderingConfig.singleLineBreaks===!1?t.breaks=!1:t.breaks=!0,this.options&&this.options.renderingConfig&&this.options.renderingConfig.codeSyntaxHighlighting===!0&&window.hljs&&(t.highlight=function(e){return window.hljs.highlightAuto(e).value}),j.setOptions(t),j(e)}},B.prototype.render=function(e){if(e||(e=this.element||document.getElementsByTagName("textarea")[0]),!this._rendered||this._rendered!==e){this.element=e;var t=this.options,n=this,i={};for(var o in t.shortcuts)null!==t.shortcuts[o]&&null!==q[o]&&!function(e){i[r(t.shortcuts[e])]=function(){q[e](n)}}(o);i.Enter="newlineAndIndentContinueMarkdownList",i.Tab="tabAndIndentMarkdownList",i["Shift-Tab"]="shiftTabAndUnindentMarkdownList",i.Esc=function(e){e.getOption("fullScreen")&&s(n)},document.addEventListener("keydown",function(e){e=e||window.event,27==e.keyCode&&n.codemirror.getOption("fullScreen")&&s(n)},!1);var a,l;if(t.spellChecker!==!1?(a="spell-checker",l=t.parsingConfig,l.name="gfm",l.gitHubSpice=!1,z({codeMirrorInstance:F})):(a=t.parsingConfig,a.name="gfm",a.gitHubSpice=!1),this.codemirror=F.fromTextArea(e,{mode:a,backdrop:l,theme:"paper",tabSize:void 0!=t.tabSize?t.tabSize:2,indentUnit:void 0!=t.tabSize?t.tabSize:2,indentWithTabs:t.indentWithTabs!==!1,lineNumbers:!1,autofocus:t.autofocus===!0,extraKeys:i,lineWrapping:t.lineWrapping!==!1,allowDropFileTypes:["text/plain"],placeholder:t.placeholder||e.getAttribute("placeholder")||"",styleSelectedText:void 0!=t.styleSelectedText?t.styleSelectedText:!0}),t.forceSync===!0){var c=this.codemirror;c.on("change",function(){c.save()})}this.gui={},t.toolbar!==!1&&(this.gui.toolbar=this.createToolbar()),t.status!==!1&&(this.gui.statusbar=this.createStatusbar()),void 0!=t.autosave&&t.autosave.enabled===!0&&this.autosave(),this.gui.sideBySide=this.createSideBySide(),this._rendered=this.element;var u=this.codemirror;setTimeout(function(){u.refresh()}.bind(u),0)}},B.prototype.autosave=function(){if(_()){var e=this;if(void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to use the autosave feature");null!=e.element.form&&void 0!=e.element.form&&e.element.form.addEventListener("submit",function(){localStorage.removeItem("smde_"+e.options.autosave.uniqueId)}),this.options.autosave.loaded!==!0&&("string"==typeof localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&""!=localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&(this.codemirror.setValue(localStorage.getItem("smde_"+this.options.autosave.uniqueId)),this.options.autosave.foundSavedValue=!0),this.options.autosave.loaded=!0),localStorage.setItem("smde_"+this.options.autosave.uniqueId,e.value());var t=document.getElementById("autosaved");if(null!=t&&void 0!=t&&""!=t){var n=new Date,r=n.getHours(),i=n.getMinutes(),o="am",a=r;a>=12&&(a=r-12,o="pm"),0==a&&(a=12),i=10>i?"0"+i:i,t.innerHTML="Autosaved: "+a+":"+i+" "+o}this.autosaveTimeoutId=setTimeout(function(){e.autosave()},this.options.autosave.delay||1e4)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},B.prototype.clearAutosavedValue=function(){if(_()){if(void 0==this.options.autosave||void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to clear the autosave value");localStorage.removeItem("smde_"+this.options.autosave.uniqueId)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},B.prototype.createSideBySide=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.nextSibling;n&&/editor-preview-side/.test(n.className)||(n=document.createElement("div"),n.className="editor-preview-side",t.parentNode.insertBefore(n,t.nextSibling));var r=!1,i=!1;return e.on("scroll",function(e){if(r)return void(r=!1);i=!0;var t=e.getScrollInfo().height-e.getScrollInfo().clientHeight,o=parseFloat(e.getScrollInfo().top)/t,a=(n.scrollHeight-n.clientHeight)*o;n.scrollTop=a}),n.onscroll=function(){if(i)return void(i=!1);r=!0;var t=n.scrollHeight-n.clientHeight,o=parseFloat(n.scrollTop)/t,a=(e.getScrollInfo().height-e.getScrollInfo().clientHeight)*o;e.scrollTo(0,a)},n},B.prototype.createToolbar=function(e){if(e=e||this.options.toolbar,e&&0!==e.length){var t;for(t=0;t<e.length;t++)void 0!=K[e[t]]&&(e[t]=K[e[t]]);var n=document.createElement("div");n.className="editor-toolbar";var r=this,a={};for(r.toolbar=e,t=0;t<e.length;t++)if(("guide"!=e[t].name||r.options.toolbarGuideIcon!==!1)&&!(r.options.hideIcons&&-1!=r.options.hideIcons.indexOf(e[t].name)||("fullscreen"==e[t].name||"side-by-side"==e[t].name)&&$())){if("|"===e[t]){for(var s=!1,c=t+1;c<e.length;c++)"|"===e[c]||r.options.hideIcons&&-1!=r.options.hideIcons.indexOf(e[c].name)||(s=!0);if(!s)continue}!function(e){var t;t="|"===e?o():i(e,r.options.toolbarTips,r.options.shortcuts),e.action&&("function"==typeof e.action?t.onclick=function(t){t.preventDefault(),e.action(r)}:"string"==typeof e.action&&(t.href=e.action,t.target="_blank")),a[e.name||e]=t,n.appendChild(t)}(e[t])}r.toolbarElements=a;var u=this.codemirror;u.on("cursorActivity",function(){var e=l(u);for(var t in a)!function(t){var n=a[t];e[t]?n.className+=" active":"fullscreen"!=t&&"side-by-side"!=t&&(n.className=n.className.replace(/\s*active\s*/g,""))}(t)});var f=u.getWrapperElement();return f.parentNode.insertBefore(n,f),n}},B.prototype.createStatusbar=function(e){e=e||this.options.status;var t=this.options,n=this.codemirror;if(e&&0!==e.length){var r,i,o,a=[];for(r=0;r<e.length;r++)if(i=void 0,o=void 0,"object"==typeof e[r])a.push({className:e[r].className,defaultValue:e[r].defaultValue,onUpdate:e[r].onUpdate});else{var l=e[r];"words"===l?(o=function(e){e.innerHTML=W(n.getValue())},i=function(e){e.innerHTML=W(n.getValue())}):"lines"===l?(o=function(e){e.innerHTML=n.lineCount()},i=function(e){e.innerHTML=n.lineCount()}):"cursor"===l?(o=function(e){e.innerHTML="0:0"},i=function(e){var t=n.getCursor();e.innerHTML=t.line+":"+t.ch}):"autosave"===l&&(o=function(e){void 0!=t.autosave&&t.autosave.enabled===!0&&e.setAttribute("id","autosaved")}),a.push({className:l,defaultValue:o,onUpdate:i})}var s=document.createElement("div");for(s.className="editor-statusbar",r=0;r<a.length;r++){var c=a[r],u=document.createElement("span");u.className=c.className,"function"==typeof c.defaultValue&&c.defaultValue(u),"function"==typeof c.onUpdate&&this.codemirror.on("update",function(e,t){return function(){t.onUpdate(e)}}(u,c)),s.appendChild(u)}var f=this.codemirror.getWrapperElement();return f.parentNode.insertBefore(s,f.nextSibling),s}},B.prototype.value=function(e){return void 0===e?this.codemirror.getValue():(this.codemirror.getDoc().setValue(e),this)},B.toggleBold=c,B.toggleItalic=u,B.toggleStrikethrough=f,B.toggleBlockquote=d,B.toggleHeadingSmaller=p,B.toggleHeadingBigger=m,B.toggleHeading1=g,B.toggleHeading2=v,B.toggleHeading3=y,B.toggleCodeBlock=h,B.toggleUnorderedList=x,B.toggleOrderedList=b,B.cleanBlock=w,B.drawLink=k,B.drawImage=S,B.drawTable=C,B.drawHorizontalRule=L,B.undo=T,B.redo=M,B.togglePreview=A,B.toggleSideBySide=N,B.toggleFullScreen=s,B.prototype.toggleBold=function(){c(this)},B.prototype.toggleItalic=function(){u(this)},B.prototype.toggleStrikethrough=function(){f(this)},B.prototype.toggleBlockquote=function(){d(this)},B.prototype.toggleHeadingSmaller=function(){p(this)},B.prototype.toggleHeadingBigger=function(){m(this)},B.prototype.toggleHeading1=function(){g(this)},B.prototype.toggleHeading2=function(){v(this)},B.prototype.toggleHeading3=function(){y(this)},B.prototype.toggleCodeBlock=function(){h(this)},B.prototype.toggleUnorderedList=function(){x(this)},B.prototype.toggleOrderedList=function(){b(this)},B.prototype.cleanBlock=function(){w(this)},B.prototype.drawLink=function(){k(this)},B.prototype.drawImage=function(){S(this)},B.prototype.drawTable=function(){C(this)},B.prototype.drawHorizontalRule=function(){L(this)},B.prototype.undo=function(){T(this)},B.prototype.redo=function(){M(this)},B.prototype.togglePreview=function(){A(this)},B.prototype.toggleSideBySide=function(){N(this)},B.prototype.toggleFullScreen=function(){s(this)},B.prototype.isPreviewActive=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.lastChild;return/editor-preview-active/.test(n.className)},B.prototype.isSideBySideActive=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.nextSibling;return/editor-preview-active-side/.test(n.className)},B.prototype.isFullscreenActive=function(){var e=this.codemirror;return e.getOption("fullScreen")},B.prototype.getState=function(){var e=this.codemirror;return l(e)},B.prototype.toTextArea=function(){var e=this.codemirror,t=e.getWrapperElement();t.parentNode&&(this.gui.toolbar&&t.parentNode.removeChild(this.gui.toolbar),this.gui.statusbar&&t.parentNode.removeChild(this.gui.statusbar),this.gui.sideBySide&&t.parentNode.removeChild(this.gui.sideBySide)),e.toTextArea(),this.autosaveTimeoutId&&(clearTimeout(this.autosaveTimeoutId),this.autosaveTimeoutId=void 0,this.clearAutosavedValue())},t.exports=B},{"./codemirror/tablist":19,codemirror:10,"codemirror-spell-checker":4,"codemirror/addon/display/fullscreen.js":5,"codemirror/addon/display/placeholder.js":6,"codemirror/addon/edit/continuelist.js":7,"codemirror/addon/mode/overlay.js":8,"codemirror/addon/selection/mark-selection.js":9,"codemirror/mode/gfm/gfm.js":11,"codemirror/mode/markdown/markdown.js":12,"codemirror/mode/xml/xml.js":14,marked:17}]},{},[20])(20)}); diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html index 70426dc1..906fc577 100644 --- a/pydis_site/templates/base/base.html +++ b/pydis_site/templates/base/base.html @@ -1,6 +1,5 @@ {# Base template, with a few basic style definitions. #} {% load django_simple_bulma %} -{% load sekizai_tags %} {% load static %} <!DOCTYPE html> @@ -28,30 +27,15 @@ {# Font-awesome here is defined explicitly so that we can have Pro #} <script src="https://kit.fontawesome.com/ae6a3152d8.js"></script> - <script src="{% static "js/base/modal.js" %}"></script> <link rel="stylesheet" href="{% static "css/base/base.css" %}"> - <link rel="stylesheet" href="{% static "css/base/notification.css" %}"> {% block head %}{% endblock %} - {% render_block "css" %} </head> <body class="site"> <!-- Git hash for this release: {{ git_sha }} --> <main class="site-content"> - {% if messages %} - <div class="messages"> - {% for message in messages %} - <div class="notification {% if message.tags %}is-{{ message.tags }}{% endif %}"> - <button class="delete"></button> - - {{ message }} - </div> - {% endfor %} - </div> - {% endif %} - {% block content %} {{ block.super }} {% endblock %} @@ -61,6 +45,5 @@ {% include "base/footer.html" %} {% endblock %} -{% render_block "js" %} </body> </html> diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index bced8254..f19706cd 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -1,4 +1,3 @@ -{% load socialaccount %} {% load static %} <nav class="navbar is-primary" role="navigation" aria-label="main navigation"> @@ -62,67 +61,40 @@ More </a> <div class="navbar-dropdown"> - <a class="navbar-item" href="{% url 'wiki:get' path="resources/" %}"> + <a class="navbar-item" href="{% url "resources:index" %}"> Resources </a> - <a class="navbar-item" href="{% url 'wiki:get' path="resources/tools/" %}"> + <a class="navbar-item" href="{% url "resources:resources" category="tools" %}"> Tools </a> - <a class="navbar-item" href="{% url 'wiki:get' path="contributing/" %}"> + <a class="navbar-item" href="{% url "content:page_category" location="guides/pydis-guides/contributing"%}"> Contributing </a> + <a class="navbar-item" href="{% url "content:page_category" location="frequently-asked-questions" %}"> + FAQ + </a> <a class="navbar-item" href="{% url 'timeline' %}"> Timeline </a> - <a class="navbar-item" href="{% url 'wiki:get' path="frequently-asked-questions/" %}"> - FAQ - </a> - <a class="navbar-item" href="{% url 'wiki:get' path="rules/" %}"> + <a class="navbar-item" href="{% url "content:page_category" location="rules" %}"> Rules </a> - <a class="navbar-item" href="{% url 'wiki:get' path="code-of-conduct/" %}"> + <a class="navbar-item" href="{% url "content:page_category" location="code-of-conduct" %}"> Code of Conduct </a> - <a class="navbar-item" href="{% url 'wiki:get' path="privacy/" %}"> + <a class="navbar-item" href="{% url "content:page_category" location="privacy" %}"> Privacy </a> <hr class="navbar-divider"> <div class="navbar-item"> <strong>Events</strong> </div> - <a class="navbar-item" href="{% url 'wiki:get' path="code-jams/code-jam-7/" %}"> + <a class="navbar-item" href="{% url "events:page" path="code-jams/7" %}"> Most Recent: Code Jam 7 </a> - <a class="navbar-item" href="{% url 'wiki:get' path="events/" %}"> + <a class="navbar-item" href="{% url "events:index" %}"> All events </a> - <hr class="navbar-divider"> - - {% if not user.is_authenticated %} - {% get_providers as socialaccount_providers %} - - {% for provider in socialaccount_providers %} - {% if provider.id == "discord" %} - <a class="navbar-item" - href="{% provider_login_url provider.id process="login" scope=scope auth_params=auth_params %}" - >Login with {{ provider.name }}</a> - {% endif %} - {% endfor %} - {% else %} - <form method="post" action="{% url 'logout' %}"> - {% csrf_token %} - - <div class="field navbar-item is-paddingless is-fullwidth is-grouped"> - <button type="submit" class="button is-white is-inline is-fullwidth has-text-left is-size-navbar-menu has-text-grey-dark">Logout</button> - <a title="Settings" class="button is-white is-inline has-text-right is-size-navbar-menu has-text-grey-dark modal-button" data-target="account-modal"> - <span class="is-icon"> - <i class="fas fa-cog"></i> - </span> - </a> - </div> - </form> - {% endif %} - </div> </div> @@ -135,24 +107,3 @@ </div> </nav> - -{% if user.is_authenticated %} - <script defer type="text/javascript"> - // Script which loads and sets up the account settings modal. - // This script must be placed in a template, or rewritten to take the fetch - // URL as a function argument, in order to be used. - - "use strict"; - - // Create and prepend a new div for this modal - let element = document.createElement("div"); - document.body.prepend(element); - - fetch("{% url "account_settings" %}") // Fetch the URL - .then((response) => response.text()) // Read in the data stream as text - .then((text) => { - element.outerHTML = text; // Replace the div's HTML with the loaded modal HTML - setupModal(document.getElementById("account-modal")); // Set up the modal - }); - </script> -{% endif %} diff --git a/pydis_site/templates/content/base.html b/pydis_site/templates/content/base.html new file mode 100644 index 00000000..21895479 --- /dev/null +++ b/pydis_site/templates/content/base.html @@ -0,0 +1,41 @@ +{% 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"> + {# Article breadcrumb #} + <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> + {# Sub-Article dropdown for category pages #} + {% if subarticles %} + {% include "content/dropdown.html" %} + {% endif %} + </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/dropdown.html b/pydis_site/templates/content/dropdown.html new file mode 100644 index 00000000..d81e29dc --- /dev/null +++ b/pydis_site/templates/content/dropdown.html @@ -0,0 +1,17 @@ +<div class="dropdown is-pulled-right is-right" id="dropdown"> + <div class="dropdown-trigger"> + <a aria-haspopup="true" aria-controls="subarticle-menu"> + <span>Sub-Articles</span> + <span class="icon is-small"> + <i class="fas fa-angle-down" aria-hidden="true"></i> + </span> + </a> + </div> + <div class="dropdown-menu" id="subarticle-menu" role="menu"> + <div class="dropdown-content"> + {% for page in subarticles|dictsort:"name" %} + <a href="{{ page.path }}" class="dropdown-item">{{ page.name }}</a> + {% endfor %} + </div> + </div> +</div> diff --git a/pydis_site/templates/content/listing.html b/pydis_site/templates/content/listing.html new file mode 100644 index 00000000..ef0ef919 --- /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="{{ data.icon|default:"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.title }}</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|default:"fab 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..759286f6 --- /dev/null +++ b/pydis_site/templates/content/page.html @@ -0,0 +1,41 @@ +{% 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.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 or toc %} + <div class="columns is-variable is-8"> + <div class="column is-two-thirds"> + {{ page|safe }} + </div> + <div class="column"> + {% if toc %} + <div class="box"> + <p class="menu-label">Table of Contents</p> + <ul class="menu-list toc"> + {{ toc|safe }} + </ul> + </div> + {% endif %} + {% if relevant_links %} + <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> + {% endif %} + </div> + </div> + {% else %} + <div>{{ page|safe }}</div> + {% endif %} +{% endblock %} diff --git a/pydis_site/templates/events/_base.html b/pydis_site/templates/events/_base.html new file mode 100644 index 00000000..ff78d944 --- /dev/null +++ b/pydis_site/templates/events/_base.html @@ -0,0 +1,33 @@ +{% extends "base/base.html" %} +{% load static %} + +{% block head %} + <link rel="stylesheet" href="{% static "css/events/base.css" %}"> + <link rel="stylesheet" + href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.4.0/styles/atom-one-dark.min.css"> + <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.4.0/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> + {% block breadcrumb %}{% endblock %} + </ul> + </nav> + </div> + </section> + + <section class="section"> + <div class="container"> + <div class="content"> + <h1>{% block title %}{% endblock %}</h1> + <div>{% block event_base_content %}{% endblock %}</div> + </div> + </div> + </section> +{% endblock %} diff --git a/pydis_site/templates/events/base.html b/pydis_site/templates/events/base.html new file mode 100644 index 00000000..c9a963e7 --- /dev/null +++ b/pydis_site/templates/events/base.html @@ -0,0 +1,7 @@ +{% extends "events/_base.html" %} + +{% block event_base_content %} +<div> + {% block event_content %}{% endblock %} +</div> +{% endblock %} diff --git a/pydis_site/templates/events/base_sidebar.html b/pydis_site/templates/events/base_sidebar.html new file mode 100644 index 00000000..8ce6ad65 --- /dev/null +++ b/pydis_site/templates/events/base_sidebar.html @@ -0,0 +1,12 @@ +{% extends "events/base.html" %} + +{% block event_base_content %} +<div class="columns is-variable is-8"> + <div class="column is-two-thirds"> + {% block event_content %}{% endblock %} + </div> + <div class="column"> + {% block sidebar %}{% endblock %} + </div> +</div> +{% endblock %} diff --git a/pydis_site/templates/events/index.html b/pydis_site/templates/events/index.html new file mode 100644 index 00000000..024e7fdc --- /dev/null +++ b/pydis_site/templates/events/index.html @@ -0,0 +1,123 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Events{% endblock %} + +{% block breadcrumb %} + <li class="is-active"><a href="#">Events</a></li> +{% endblock %} + +{% block event_content %} + <div class="box"> + <h2 class="title is-4">Code Jams</h2> + <p>Each year, we organize a Winter Code Jam and a Summer Code Jam. During these events, members of our community will work together in teams to create something amazing using a technology we picked for them. One such technology that was picked for the Winter Code Jam 2020 was Kivy, a cross-platform GUI framework.</p> + <p>To help fuel the creative process, we provide a specific theme, like <strong>Ancient Technology</strong> or <strong>This App Hates You</strong>. At the end of the Code Jam, the projects are judged by Python Discord server staff members and guest judges from the larger Python community. The judges will consider creativity, code quality, teamwork, and adherence to the theme.</p> + <p>If you want to read more about Code Jams, visit our <a href="{% url "events:page" path="code-jams" %}">Code Jam info page</a> or watch this video showcasing the best projects created during the <strong>Winter Code Jam 2020: Ancient Technology</strong>:</p> + <iframe width="560" height="315" src="https://www.youtube.com/embed/8fbZsGrqBzo" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> + </div> + + <div class="box"> + <h2 class="title is-4">Game Jam</h2> + <div class="columns is-3" style="--columnGap: 0.75rem;"> + <div class="column"> + <p> + The Game Jam is similar to our Code Jams, but smaller in scope. Instead of having to complete a qualifier + and being teamed up with random strangers, members of our community can just sign-up individually or pair up + with whoever they like. + </p> + <p> + The participants will have ten days to create a game using the technology we've selected, and drawing + inspiration from a provided theme. After the event, a panel of judges will play all the games and select a + winner. The top 5 will featured in a special video on our <a href="https://www.youtube.com/channel/UCQsrA4xo6jvdgsJZhKaBL6w">YouTube channel</a>. + </p> + <p> + The <a class="has-text-link" href="{% url "events:page" path="game-jams/2020" %}">first edition of the Game Jam</a> ran from + <strong>April 17, 2020 to April 26, 2020</strong>. + </p> + </div> + <div class="column is-3"> + <img src="https://user-images.githubusercontent.com/33516116/77593036-5fb09780-6eeb-11ea-9feb-336b2e5e23de.png" style="border-radius: 10px;" alt=""> + </div> + </div> + </div> + + <div class="box"> + <h2 class="title is-4">Hacktoberfest</h2> + <div class="columns is-3" style="--columnGap: 0.75rem;"> + <div class="column"> + <p> + This event revolves around the annual <a href="https://hacktoberfest.digitalocean.com/">Hacktoberfest + event</a> organized by Digital Ocean. In addition to promoting Hacktoberfest in our community and supporting + those who choose to take their first steps into the world of open source, we will also ease our members into + contributing to open source by starting a low-entry, beginner-friendly open source project where we will + guide our members through the open source process in a safe environment. + </p> + <p> + The exact form this event will take has not been decided yet, but we'll make sure to keep you updated in + our community announcements! + </p> + </div> + <div class="column is-3"> + <img src="https://raw.githubusercontent.com/python-discord/branding/master/seasonal/halloween/hacktoberfest/2020/animated_server_icon.gif" style="border-radius: 10px;" alt=""> + </div> + </div> + </div> + + <div class="box"> + <h2 class="title is-4">Advent of Code</h2> + <div class="columns is-3" style="--columnGap: 0.75rem;"> + <div class="column"> + <p> + Each year, many of our members take part of an online coding competition called + <a href="https://adventofcode.com/">Advent of Code</a> that takes place in December. Advent of Code is an + Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved + in any programming language you like, including Python. + </p> + <p> + During the event, we will open a special discussion channel in which our members can discuss the puzzles + and compare their solutions. We will also open a private leaderboard and may even reward the best scoring + members on that board with prizes! + </p> + <p> + However, this event isn't purely competitive. You can also join in just to see how far you'll get, to + challenge yourself, as a way of learning Python, or just for the fun of it. In 2019, almost 200 members + signed up for our private leaderboard, but many more took on the challenge without focusing on the + competitive aspect. + </p> + </div> + <div class="column is-3"> + <img src="https://raw.githubusercontent.com/python-discord/branding/master/seasonal/christmas/2019/festive_256.gif" style="border-radius: 10px;" alt=""> + </div> + </div> + </div> + + <div class="box"> + <h2 class="title is-4">PyWeek</h2> + <div class="columns is-3" style="--columnGap: 0.75rem;"> + <div class="column"> + <p> + For the past 15 years, <a href="https://pyweek.org">PyWeek</a> has been running a bi-annual game jam for the + Python language. As of 2020, we are excited to say we are officially partnered with PyWeek to co-run these + events. + </p> + <p> + During each PyWeek event, we open a special discussion channel in which our members can discuss their + submissions, meet other participants, and talk to PyWeek staff. The PyWeek organizer, + Daniel Pope (<a href="https://twitter.com/lordmauve">@lordmauve</a>) will be present during the entire event to answer + questions and post announcements and information in our community. + </p> + <p> + Unlike our other events, the <strong>community</strong> will select the winner from all the submissions + during PyWeek. We may release YouTube content showcasing the best submissions after the events are finished. + </p> + </div> + <div class="column is-3"> + <img src="https://pyweek.readthedocs.io/en/latest/_static/pyweek.svg" style="border-radius: 10px;" alt=""> + </div> + </div> + </div> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/upcoming-event.html" %} + {% include "events/sidebar/events-list.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/1.html b/pydis_site/templates/events/pages/code-jams/1.html new file mode 100644 index 00000000..bde4e0b4 --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/1.html @@ -0,0 +1,82 @@ +{% extends "events/base.html" %} + +{% block title %}Code Jam 1: Snakes{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#">Code Jam 1: Snakes</a></li> +{% endblock %} + +{% block event_content %} + <p> + The theme of the code jam was <strong>snakes</strong> Participants were assigned a random partner, + and used Git to submit code to us. Staff members reviewed the code both during and after the code jam, + and provided suggestions for changes and improvements. + The best submissions were implemented into our community bot. + Winners recieved a special Code Jam Champion title. + </p> + + <h2 id="task-description" class="title is-4"><a href="#task-description">Task description</a></h2> + <p>Here is the original task description which was posted on the code-jam-1 GitHub repo:</p> + + <blockquote> + <p> + For this code jam, your task will be to create a Snake cog for a Discord.py rewrite bot. + <a href="https://discordpy.readthedocs.io/en/rewrite/">You can find the documentation for Discord.py rewrite here</a>. + The best cog commands will be added to the official Python Discord bot and made available to everyone on the server. + The overall best cog will be awarded custom Code Jam Champion roles, + but the best commands from the teams who did not win will also be added to our bot, + and any users who write something that ends up in the bot will be awarded Contributor roles on the server. + </p> + <p> + Your initial task will be to write <code>get_snek()</code>. This is the minimum requirement for this contest, + and everyone must do it. <code>get_snek()</code> should be a method that goes online and fetches information about a snake. + If you run it without providing an argument, it should fetch information about a random snake, + including the name of the snake, a picture of the snake, and various information about it. + Is it venomous? Where can it be found? What information you choose to get is up to you. + </p> + <p> + <code>get_snek()</code> should also take an optional argument name, which should be a string that represents the name of a snake. + For example, the call <code>get_snek('cobra')</code> should get information about a cobra. name should be case insensitive. + </p> + <p> + If <code>get_snek('Python')</code> is called, the method should instead return information about the programming language, + but making sure to return the same type of information as for all the other snakes. + Fill in this information in any way you want, try to have some fun with it. + </p> + <p> + Once you have finished <code>get_snek()</code>, you should make at least two bot commands. + The first command, <code>get()</code>, should simply call <code>get_snek()</code> with whatever arguments the user provided, + and then make a nice embed that it returns to Discord. + For example, if the user in the Discord channel says bot.snakes.get('anaconda'), + the bot should post an embed that shows a picture of an anaconda and some information about the snake. + </p> + <p> + The second command is entirely up to you. You can choose to use <code>get_snek</code> for this command as well, + or you can come up with something entirely different. + The only requirement is that it is snake related in some way or other. + Here is your chance to be creative. It is these commands that will win or lose you this code jam. + The best original ideas for these commands will walk away with the victory. + </p> + </blockquote> + + <h2 id="result" class="title is-4"><a href="#result">Result</a></h2> + <p><strong>The Winning Team: Team 23, kel and Momo!</strong></p> + <p> + These two experts worked together to create what can only be described as a flawless submission. + The staff were unable to find a single thing to complain about in the 1100 lines that were committed. + Here are some of the features they created for their snake cog: + </p> + <ul> + <li>A fully functional implementation of Snakes and Ladders</li> + <li>A feature that uses perlin noise to draw a random snake, and posts the picture in the chat.</li> + <li>Hatch your very own baby snake!</li> + <li>Snakify your post history with a Markov chain</li> + <li>Rattlesnake sound effects in the voice channels</li> + </ul> + <p> + These features, as well as the best ones from the other teams, + have been implemented into our community bot. Use <code>.snakes</code> in <code>#sir-lancebot-commands</code> to play with it. + </p> +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/2.html b/pydis_site/templates/events/pages/code-jams/2.html new file mode 100644 index 00000000..602a1c66 --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/2.html @@ -0,0 +1,72 @@ +{% extends "events/base.html" %} + +{% block title %}Code Jam 2: Mythological API{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#">Code Jam 2: Mythological API</a></li> +{% endblock %} + +{% block event_content %} + <p> + The theme for this code jam will be <strong>mythology</strong>. + That means you're going to be creating a <strong>RESTful API in Flask with a mythology theme</strong>. + </p> + <p> + For example, you might create the The Mimír API, + an API used to look up information about norse gods and godesses. + </p> + + <pre><code class="lang-json">{ + "name": "Kvasir", + "parents": "Born from the saliva of the Æsir and the Vanir", + "type": "A god of wisdom and knowledge" +}</code></pre> + + <p> + The API must respond with json data, but this is not limited to text. + It could respond with bytedata, with a URL to an image, a video or some audio, + or with any other form of data you deem interesting. + </p> + <p> + The API should accept at least a GET or a POST request with JSON data. + You are allowed to use any third party libraries you want. + </p> + <p> + Remember, creativity counts. Try to have fun with it. + We're not necessarily looking for dead serious solutions only, + and it's okay if your solution is only tangentially related to the theme, + so long as there's a relationship of some sort. + </p> + + <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> + <p>The winner of the second code jam is <strong>Defiant Sails</strong>, with Momo, WrongEnd, and SharpBit! Congratulations!</p> + + <p> + They've written a phenomenal API with OAuth authentication, a multiplayer RPG, + and a feature to procedurally generate a random mythologically themed story. + This story can be played as a video, + where they use Text-To-Speech to read the story out loud while epic music plays in the background. + The music is different for each play, and the audio is visualized in a video. + </p> + <p> + The team will receive the grand prize of 12-month PyCharm Pro licenses, + and will also receive the honorary Code Jam Champions role. + </p> + <hr> + <p> + In second place with another really excellent submission, we have the team <strong>Rude Interests</strong>, + consisting of Lord Bisk, Runew0lf and Nix. They've got procedurally generated rune images, + procedural mythologies with gods with procedural relationships, descriptions, genders, and names. + A super fun idea which nearly ended up taking the first place. + </p> + <hr> + <p> + In third place, we have <strong>Overconfident Ideas</strong>, with Martmists, Casterly and eivl. + They did relationship trees with a number of different visualization options, + a fantastic myth lookup feature to find a great deal of details about a mythological figure, + a mythology trivia feature, and a markov chain string generator. + It also had a very comprehensive framework and it was clear that a great deal of work had gone into it. + </p> +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/3.html b/pydis_site/templates/events/pages/code-jams/3.html new file mode 100644 index 00000000..0bd293db --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/3.html @@ -0,0 +1,81 @@ +{% extends "events/base.html" %} + +{% block title %}Code Jam 3: Games!{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#">Code Jam 3: Games!</a></li> +{% endblock %} + +{% block event_content %} + <p> + The theme for code jam 3 was creating a <strong>game</strong> with the theme <strong>the world ends in 10 minutes</strong>. + The teams of three could live out their creativity here - any form of game was fine, + as long as they fit the theme. The jam started at <strong>Monday, the 22nd October 2018, 12:00 PM UTC</strong>. + </p> + + <h2 id="task-description" class="title is-4"><a href="#task-description">Task Description</a></h2> + <p>The <a href="https://github.com/python-discord/code-jam-3">original task description</a> was as follows:</p> + <blockquote> + <p> + This task is a little more freeform than the other code jams we've had so far - + we're happy to accept games in any form, as long as they fit that theme. + You may use PyGame, Kivy, Curses/Unicurses, Pyxel, web frameworks like Flask, + or anything else your team desires. + Please provide instructions on how to set up and run your game within the README where necessary. + Remember that teamwork is paramount - You will need to work together. + For this jam, we've assigned a leader for each team based on their responses to the application form. + Remember to listen to your leader, and communicate with the rest of your team! + </p> + </blockquote> + + <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> + <p> + The <strong>winning team</strong> for this jam was Certain Horses, consisting of Wattle, Scragly, and Mark. + They made a 2D platformer where the player had to collect lemons in order to earn points. + Below the player was an ocean of lemon juice that caused instant death if you touched it. + </p> + <p> + Every time you collected a lemon, the lemon juice water level would raise up, + so you had to be tactical about which lemons you could collect + without dying and which of the branching paths you'd have to select to avoid the sea of lemon juice rushing up to meet you. + </p> + <p> + You can play the game by yourself here: + <a href="https://github.com/MarkKoz/code-jam-3">https://github.com/MarkKoz/code-jam-3</a> + </p> + + <h2 id="runner-up-1" class="title is-4"><a href="#runner-up-1">Runner up 1: Successful Toads</a></h2> + <p> + The <strong>first runner up</strong> for this jam was team Successful Toads, + whose code can be found at <a href="https://gitlab.com/biskette/code-jam-3/">https://gitlab.com/biskette/code-jam-3/</a>. + They wrote a flask app game which was about deciding who to let into the last nuclear bunker on the planet. + Like Tinder, but for nuclear holocaust gatekeepers. + The game had procedurally generated graphics which were absolutely hilarious, + and you would swipe right or left to decide whether or not to let someone into your bunker. + Each person had certain traits which could either save or destroy your bunker. + </p> + <p> + In order to win, you had to balance stuff like medical expertise with combat experience + and make sure that your bunker would survive into the post-apocalypse. + </p> + + <h2 id="runner-up-2" class="title is-4"><a href="#runner-up-2">Runner up 2: Misty Hats</a></h2> + <p> + The second runner up for this jam was team Misty Hats, + with code available at <a href="https://gitlab.com/JannesJ/code-jam-3">https://gitlab.com/JannesJ/code-jam-3</a>. + They made an excellent shoot-em-up in the style of Gradius, + with a long intro cinematic, lots of original graphics assets, + and a whole bunch of different power-ups. Other features included: + </p> + <ul> + <li><p>Fighter enemy: A tiny spaceship that will follow you and try to take you down.</p></li> + <li><p>Pythonic mines: Space mines shaped like Python logos that damage you if you touch them.</p></li> + <li><p>Defensive Structures: Semi-Stationary defensive structures that will shoot you on sight</p></li> + <li><p>8 different power-ups: Extra damage, armor, hp, shield, double shot.. and more!</p></li> + <li><p>Timers: A ten minute timer and small timers for temporary power ups.</p></li> + <li><p>Different size/color blaster projectiles</p></li> + <li><p>Wave system</p></li> + </ul> +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/4.html b/pydis_site/templates/events/pages/code-jams/4.html new file mode 100644 index 00000000..f3d750bc --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/4.html @@ -0,0 +1,93 @@ +{% extends "events/base.html" %} + +{% block title %}Code Jam 4: This Apps Hates You{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#">Code Jam 4: This Apps Hates You</a></li> +{% endblock %} + +{% block event_content %} + <p> + The theme for code jam 4 was creating a <strong>GUI application</strong> with the theme <strong>this app hates you</strong>. + 19 randomly assembled teams worked on this task for three days from the 22nd of February, 00:00 UTC. + </p> + + <h2 id="task-description" class="title is-4"><a href="#task-description">Task Description</a></h2> + <p> + The original task description, + as found in the <a href="https://github.com/python-discord/code-jam-4">repository</a> was as follows: + </p> + <blockquote> + <p> + The theme for this code jam will be <strong>This app hates you!</strong>. + You will be creating an application using a GUI library of your choice in Python. + The application must serve a real purpose, but must also fit the theme. + </p> + <p> + Here are a couple of examples of what we mean by an application that "serves a real purpose but also fits the theme": + </p> + <ul> + <li><p>A calculator app that calculates the right answers, but represents the answer in a way that's completely impractical.</p></li> + <li> + <p> + An image resizer where you have to specify which part of the image to resize, + specify how much force to apply to the resize operation in newtons, + and then manually resize the image by turning a crank. + </p> + </li> + <li> + <p> + An alarm clock app that plays a very loud sound effect every 5 minutes reminding you that your alarm will ring in 6 hours. + The closer it gets to the 6 hour mark, the lower the volume of the sound effect. + When the time is up, the sound effect is virtually inaudible. + </p> + </li> + </ul> + </blockquote> + + <h2 id="judging-stream" class="title is-4"><a href="#judging-stream">Judging stream</a></h2> + <p> + If you want to watch the original code jam judging stream, + you can find it on YouTube - all of the submissions are showcased in the stream. + The winning project is showcased at around the 40 minute mark. + </p> + <iframe width="560" height="315" src="https://www.youtube.com/embed/TlU6GPGbSuY?start=805" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> + + <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> + <p> + The <strong>winning team</strong> for this code jam was the team <strong>Cool Crocodiles</strong>, + consisting of Runew0lf, gl4cial, and syntaxaire. + They created a text editor called CrocPad++ featuring an infuriating troubleshooter that would pop up every five or so characters, + sound effects when you type, the ability to insert symbols by turning a dial, + a theme "for blind users", and many more features. + </p> + + <h2 id="runner-up-1" class="title is-4"><a href="#runner-up-1">Runner up 1: Team Enthusiastic Electricians</a></h2> + <p> + The <strong>first runner up</strong> for this code jam was the team <strong>Enthusiastic Electricians</strong>, + who made a paint tool called <i>ArtiQule</i> where the pencil point would break, + colors would blend together on your palette, and the paint brush would drip onto the canvas. + </p> + + <h2 id="runner-up-2" class="title is-4"><a href="#runner-up-2">Runner up 2: Team Slithering Snacks</a></h2> + <p> + The <strong>second runner up</strong> for this code jam was the team <strong>Slithering Snacks</strong>. + They ade a media player where you had to fill out a CAPTCHA every time you wanted to load a file, + register an account, and confirm your password to log in by typing it with all the characters <i>in alphabetic order</i>. + </p> + + <h2 id="honorable-mentions" class="title is-4"><a href="#honorable-mentions">Honorable mentions</a></h2> + <p>While they didn't make it on top of the ladder, the following submissions have stood out exceptionally:</p> + <ul> + <li><p>Team <strong>Blue Buckets</strong> won best code quality with their <i>Tinder for Cats</i>.</p></li> + <li> + <p> + Team <strong>Overjoyed <a href="https://wiki.teamfortress.com/wiki/Otolaryngologist%27s_Mirror">Otolaryngologists</a></strong> + won best looking UI with their sleek <i>Minesweeper</i> game where you had to press each tile up to 100 times to break it and only got one flag. + </p> + </li> + <li><p>Team <strong>High Houses</strong> won best idea with an <i>on-screen keyboard</i> where you only got some of the keys and additional keys had to be unlocked by gaining XP, leveling up, and getting loot boxes.</p></li> + </ul> +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/5.html b/pydis_site/templates/events/pages/code-jams/5.html new file mode 100644 index 00000000..ba6928c7 --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/5.html @@ -0,0 +1,80 @@ +{% extends "events/base.html" %} + +{% block title %}Code Jam 5: Climate Change{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#"></a>Code Jam 5: Climate Change</li> +{% endblock %} + +{% block event_content %} + <p> + The theme for code jam 5 was <strong>climate change</strong>. + Similar to <a href="https://pythondiscord.com/pages/code-jams/code-jam-3/">code jam 3</a>, + teams could live out their creativity here, as long as it fits the theme. + The code jam started on July 20, 2019 at 12 PM UTC, + and with more than 117 sign-ups and 27 teams competing, + this was our biggest code jam ever (so far)! + </p> + + <h2 id="task-description" class="title is-4"><a href="#task-description">Task Description</a></h2> + <p>The original task description, as found in <a href="https://github.com/python-discord/code-jam-5">the repository</a>, was as follows:</p> + <blockquote> + <p> + Your theme for this code jam is <strong>climate change</strong>. + </p> + <p> + Because this is a free-for-all, you are free to make anything you want, + as long as it fits this theme. + We'd love if you created something that might help raise awareness, + but first and foremost, we want you to create something <i>fun</i>. + </p> + </blockquote> + + <h2 id="judging-stream" class="title is-4"><a href="#judging-stream">Judging stream</a></h2> + <p> + If you want to watch the original code jam judging stream, + you can find it on YouTube - all of the submissions are showcased in the stream. + The winning project is showcased at around the 30 minute mark. + </p> + <iframe width="560" height="315" src="https://www.youtube.com/embed/drBKNU73Ss4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> + + <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> + <p> + While it was a difficult decision, the winners were Team <strong>Combined Crusader</strong>, + consisting of Makusu, Mahabama, and missingfragment. + They made a really addicting <i>Climate Clicker game</i> that surprised us for being extremely responsive, + having beautiful, original art assets, and, most of all, being really fun to play. + The code quality of this project was good and they used Pythonic techniques like the LRU cache to make the game behave extremely snappy. + </p> + <p> + You can check out their entry in the + <a href="https://github.com/python-discord/code-jam-5/tree/master/combined_crusaders">repository</a> + and watch us doing so in the <a href="https://www.youtube.com/watch?v=drBKNU73Ss4&t=1815s">livestream</a>. + </p> + + <h2 id="runner-up-1" class="title is-4"><a href="#runner-up-1">Runner up 1: Various Vipers</a></h2> + <p> + Team <strong>Various Vipers</strong> produced a game consisting of various mini-games that had an excellent graphical user interface. + The fact that the project had accompanying game design documents showed how well-organized this project was. + Combined with good code quality, this thematic project deserves the second place in this code jam. + </p> + <p> + Feel free to check out their submission in the code jam <a href="https://github.com/python-discord/code-jam-5/tree/master/various_vipers">repository</a> + and watch us inspecting it in the <a href="https://www.youtube.com/watch?time_continue=1&v=drBKNU73Ss4&feature=emb_logo">livestream</a>. + </p> + + <h2 id="runner-up-2" class="title is-4"><a href="#runner-up-2">Runner up 2: Gentle Gnomes</a></h2> + <p> + The <strong>Gentle Gnomes</strong> ended up in the third spot. + Their web app allowed the user to search for a location and would then show climate-related statistics, + plots as well as projections for the future for the specified location. + The code quality of this project was outstanding and the web app was extremely useful and interesting. + </p> + <p> + Again, you can view their submission on the + <a href="https://github.com/python-discord/code-jam-5/tree/master/gentle_gnomes">repository</a> + or view it live on the <a href="https://www.youtube.com/watch?v=drBKNU73Ss4&t=7800s">livestream</a>. + </p> +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/6/_index.html b/pydis_site/templates/events/pages/code-jams/6/_index.html new file mode 100644 index 00000000..256914b6 --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/6/_index.html @@ -0,0 +1,93 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Winter Code Jam 2020: Ancient Technology{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#"></a>Winter Code Jam 2020: Ancient Technology</li> +{% endblock %} + +{% block event_content %} + <p> + The theme for the Winter Code Jam 2020, the sixth Python Discord Code Jam, + was utilizing the <strong>Kivy Framework</strong> to create <strong>Ancient Technology</strong>. + The jam lasted for 9 days from January 17, 2020 to January 26, 2020 with 16 teams competing against each other. + </p> + + <h2 id="task-description" class="title is-4"><a href="#task-description">Task Description</a></h2> + <p> + The original task description, as found in the <a href="https://github.com/python-discord/code-jam-6">repository</a>, was as follows: + </p> + <blockquote> + <p>By popular choice, the theme for this code jam is <strong>Ancient Technology</strong>.</p> + <p> + What you do with this theme or how you interpret it is up to you, + but it will be your task to come up with something fun using this theme. + </p> + </blockquote> + + <h2 id="judging-stream" class="title is-4"><a href="#judging-stream">Judging Stream</a></h2> + <p> + If you want to watch the original code jam judging stream, + you can find it on YouTube - all of the submissions are showcased in the stream. + The winning project is showcased at the <a href="https://youtu.be/I97L_Y3rhvc?t=13710">03:48:30</a> timestamp. + </p> + <iframe width="560" height="315" src="https://www.youtube.com/embed/I97L_Y3rhvc" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> + + <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> + <p> + The <strong>winning team</strong> for this code jam was team <strong>Tactless Tricksters</strong>, + consisting of GoBig87, Kan, sposker, DomicidalManiac, and Inventor. + They created an incredible morse code chatting app, with a plethora of different features. + Morse code training, morse code audio encoding/decoding, audio input sensitivity calibration, + and a morse code messaging system are all features of this project. + The beautiful and intuitive UI paired with all these features was what won them this jam. + With the given amount of time during this jam, this was quite the impressive project. + </p> + <p> + You can check out their entry in the + <a href="https://github.com/python-discord/code-jam-6/tree/master/tactless-tricksters">repository</a> + and watch us doing so in the <a href="https://youtu.be/I97L_Y3rhvc?t=13710">livestream</a>. + The top 3 winning teams' projects are also displayed on the <a href="https://kivy.org#gallery">Kivy Gallery</a>. + </p> + + <h2 id="runner-up-1" class="title is-4"><a href="#runner-up-1">Runner up 1: Circumstancial Champions</a></h2> + <p> + The <strong>Circumstantial Champions</strong> (salt-die, david987, & Music) + created a game in which players go back in time to smash and carve out rocks, + just like cavemen. The combination of their execellent graphical interface, animations, + and satisfying sounds made this one of the most polished projects in the jam. + </p> + <p> + Check out their submission in the code jam + <a href="https://github.com/python-discord/code-jam-6/tree/master/circumstantial-companions">repository</a> + and view it live on the <a href="https://youtu.be/I97L_Y3rhvc?t=2854">livestream</a>. + </p> + + <h2 id="runner-up-2" class="title is-4"><a href="#runner-up-2">Runner up 2: Inquisitive Investigators</a></h2> + <p> + Team <strong>Inquisitive Investigators</strong> (f1re & Monika) ended up in third place with their retro TUI file explorer. + The simple, easy-to-use file explorer along with their built-in terminal, text editor, + and photo viewer made this a very feature packed project. + </p> + <p> + Feel free to view their submission on the + <a href="https://github.com/python-discord/code-jam-6/tree/master/inquisitive-investigators">repository</a> + and watch us review it on the <a href="https://youtu.be/I97L_Y3rhvc?t=6545">livestream</a>. + </p> + + <img src="https://raw.githubusercontent.com/python-discord/code-jam-6/master/ancient%20tech.png?token=AAQAKVPQ55SEFWYYLYO5YV26ETLTC" alt="Code Jam Banner" style="max-width:100%;"> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/code-jams/6.html" %} + <div class="box"> + <p class="menu-label">Relevant Links</p> + <ul class="menu-list"> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/6/rules" %}">Rules</a></li> + <li><a class="has-text-link" href="https://github.com/python-discord/code-jam-6-qualifier">Qualifier</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams" %}">About Code Jams</a></li> + </ul> + </div> +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/6/rules.html b/pydis_site/templates/events/pages/code-jams/6/rules.html new file mode 100644 index 00000000..dfb1586f --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/6/rules.html @@ -0,0 +1,80 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Rules{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li><a href="{% url "events:page" path="code-jams/6" %}">Winter Code Jam 2020: Ancient Technology</a></li> + <li class="is-active"><a href="#">Rules</a></li> +{% endblock %} + +{% block event_content %} + <ol> + <li> + <p> + The majority of your project must be Python. + We will use the GitHub language details for this, + so just <strong>make sure your repo contains at least 51% Python</strong>, and you'll be okay. + </p> + </li> + <li> + <p> + Your solution must use the Kivy framework. + It is not permitted to work around this by e.g. using Kivy as a wrapper for another framework. + </p> + </li> + <li> + <p> + Your solution should be platform agnostic. + For example, if you use filepaths in your submission, + use <code>pathlib</code> to create platform agnostic Path objects instead of hardcoding the paths. + </p> + </li> + <li> + <p> + Your project must be feasible to run and simple to set up <strong>on a desktop computer</strong> + - which means you should almost certainly use some sort of dependency manager, + like <code>pipenv</code>, <code>poetry</code>, or a strictly pinned <code>requirements.txt</code>. + </p> + </li> + <li> + <p> + You must get contributions from every member of your team, + if you have an issue with someone on your team please contact a member of the administration team. + These contributions do not necessarily have to be code, + for example it's absolutely fine for someone to contribute management, documentation, graphics or audio. + <strong> + Team members that do not contribute will be removed from the Code Jam, + and will not receive their share of any prizes the team may win. + They may also be barred from entering future events. + </strong>. + </p> + </li> + <li> + <p>You must use GitHub as source control.</p> + </li> + <li> + <p> + All code must be written and committed within the time constrictions of the jam + <ul><li>Late commits may be reverted, so make sure you leave enough time to bug test your program</li></ul> + </p> + </li> + </ol> + + <blockquote> + Please note that our regular <a href="/pages/rules/">community rules</a> and <a href="/pages/code-of-conduct/">code of conduct</a> + also apply during the event and that we reserve the right to make changes to these rules at any time. + </blockquote> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/code-jams/6.html" %} + <div class="box"> + <p class="menu-label">Relevant Links</p> + <ul class="menu-list"> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/6" %}">Winter Code Jam 2020</a></li> + <li><a class="has-text-link" href="https://github.com/python-discord/code-jam-6-qualifier">Code Jam Qualifier</a></li> + </ul> + </div> +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/7/_index.html b/pydis_site/templates/events/pages/code-jams/7/_index.html new file mode 100644 index 00000000..1f0b0909 --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/7/_index.html @@ -0,0 +1,85 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Summer Code Jam 2020: Early Internet{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#">Summer Code Jam 2020: Early Internet</a></li> +{% endblock %} + +{% block event_content %} + <p> + For Python Discord's 7<sup>th</sup> biannual code jam, contestants utilized the <strong>Django</strong> framework + to create a web application. The theme for this event was <strong>Early Internet</strong>, lasting from + July 31<sup>st</sup> to August 9<sup>th</sup>. This was our largest event ever, with over 50 teams competing + for prizes from our prize pool along with the illustrious role of <code>Code Jam Champions</code> on our server. + </p> + + <h3 id="task-description"><a href="#task-description">Task Description</a></h3> + <p> + The original task description, as found in the <a href="https://github.com/python-discord/summer-code-jam-2020">repository</a>, was as follows: + </p> + <blockquote> + <strong>The theme for this code jam is Early Internet.</strong> + <p> + What you do with this theme or how you interpret it is up to you, but it will be your task to come up with something fun using this theme. + </p> + + <strong>The chosen technology for this game jam is <a href="https://www.djangoproject.com/">Django</a>.</strong> + <p> + Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. It comes "batteries included" with a slew of incredibly polished features, including database migrations, an excellent CRM, access control, security features to prevent you from making expensive mistakes, and + <a href="https://docs.djangoproject.com/en/3.0/">probably the best documentation on the internet</a>. + </p> + </blockquote> + + <h3 id="judging-stream"><a href="#judging-stream">Judging Stream</a></h3> + <p> + If you want to watch the original code jam judging stream, you can find it on YouTube - all of the submissions are showcased in the stream. The winning project is showcased at the <a href="https://youtu.be/OFtm8f2iu6c?t=6443">01:47:23</a> timestamp. + </p> + + <iframe width="560" height="315" src="https://www.youtube.com/embed/OFtm8f2iu6c" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> + + <h3 id="results"><a href="#results">Results</a></h3> + <p> + The <strong>winning team</strong> for this code jam was the <strong>Juicy Jaguars</strong>, consisting of charlotte, joshuqa, and dang. + They created an incredible Windows 95 web application, named Web95. It included a plethora of features, such as movable windows, + a start menu, a recreation of Internet Explorer, different themes & wallpapers, and a couple of fun easter eggs. The killer feature of this project was Internet Explorer. It processed the fonts, images, and colors of each webpage visited to make the experience feel as if + it were from the 90s! This was an incredibly novel idea, which was executed beautifully. + </p> + <p> + Check out their submission in our <a href="https://github.com/python-discord/summer-code-jam-2020/tree/master/juicy-jaguars">repository</a> + and watch us review it on the <a href="https://youtu.be/OFtm8f2iu6c?t=6443">livestream</a>! + </p> + + <h3 id="runner-up-1"><a href="#runner-up-1">Runner up 1: Annoyed Alligators</a></h3> + <p> + The <strong>Annoyed Alligators</strong> (i..kun, gkrou, SurvivingOnNaps, corner, and pykam) created a social media platform which is controllable by + a command line interface, named SoCommandLine Media (SoCL). Users can signup to create posts and view what others have posted already, all via the + command line. It evens supports direct messaging other users and getting the latest news articles for you to read! + </p> + <p> + Have a look at their submission in our <a href="https://github.com/python-discord/summer-code-jam-2020/tree/master/annoyed-alligators">repository</a> + and view it on the <a href="https://youtu.be/OFtm8f2iu6c?t=2200">livestream</a>! + </p> + + <h3 id="runner-up-2"><a href="#runner-up-2">Runner up 2: Mysterious Mice</a></h3> + <p> + The <strong>Mysterious Mice</strong> (rryyaann, onatoko, n0remac, and venturafranklin) created a social media site for NASA's rovers, named SpaceBook. It included profiles and photos of each rover, showed what the weather was like on Mars, and even had a text-based adventure game in a terminal! The beautiful, retro UI along with the animated space background made it a pleasure to use. + </p> + <p> + Feel free to check out their submission in our <a href="https://github.com/python-discord/summer-code-jam-2020/tree/master/mysterious-mice">repository</a> + and watch us use it on the <a href="https://youtu.be/OFtm8f2iu6c?t=7624">livestream</a>! + </p> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/code-jams/7.html" %} + <div class="box"> + <p class="menu-label">Relevant Links</p> + <ul class="menu-list"> + <!-- Add other items when they become available --> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/7/rules" %}">Rules</a></li> + </ul> + </div> +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/7/rules.html b/pydis_site/templates/events/pages/code-jams/7/rules.html new file mode 100644 index 00000000..5979fbe6 --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/7/rules.html @@ -0,0 +1,68 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Rules{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li><a href="{% url "events:page" path="code-jams/7" %}">Summer Code Jam 2020</a></li> + <li class="is-active"><a href="#">Rules</a></li> +{% endblock %} + +{% block event_content %} + <ol> + <li><p>Your solution must use the Django framework. It is not permitted to circumvent this rule by e.g. using Django as a wrapper for another framework.</p></li> + <li><p>Your solution should be platform agnostic. For example, if you use filepaths in your submission, use <code>pathlib</code> to create platform agnostic Path objects instead of hardcoding the paths.</p></li> + <li><p>Your project should be a web app which can be run in a <em>standard web browser</em>.</p></li> + <li> + <p> + You must document precisely how to install and run your project. + This should be as easy as possible, which means you should consider using dependency managers like <code>pipenv</code>, and <code>npm</code>. + We would also encourage you to use <code>docker</code> and <code>docker-compose</code> to containerize your project, but this isn't a requirement. + </p> + </li> + <li> + You must get contributions from every member of your team, if you have an issue with someone on your team please contact a member of the administration team. + These contributions do not necessarily have to be code, for example it's absolutely fine for someone to contribute management, documentation, graphics or audio. + <strong> + Team members that do not contribute will be removed from the Code Jam, and will not receive their share of any prizes the team may win. + They may also be barred from entering future events. + </strong> + </li> + <li><p>You must use GitHub as source control.</p></li> + <li> + <p> + All code and assets must be compatible with the <a href="https://en.wikipedia.org/wiki/MIT_License">MIT license</a>. + This is because we will be merging your submission into our <code>summer-code-jam-2020</code> repo at the end of the jam, + and this repo is licensed with the MIT license. + <strong>Projects that include assets that are incompatible with this license may be disqualified.</strong> + </p> + </li> + <li><p>All code must be written and committed within the time constrictions of the jam. Late commits may be reverted, so make sure you leave enough time to bug test your program.</p></li> + <li> + <p> + Use English as the main language for your project, including names, comments, documentation, and commit messages. + The text displayed in your web application should also be in English, + although you are allowed to provide the user with options for internationalisation and translation. + </p> + </li> + </ol> + + <!-- Change links after migrating them is done. --> + <blockquote> + Please note that our regular + <a href="/pages/rules">community rules</a> and <a href="/pages/code-of-conduct">code of conduct</a> + also apply during the event and that we reserve the right to make changes to these rules at any time. + </blockquote> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/code-jams/7.html" %} + <div class="box"> + <p class="menu-label">Relevant Links</p> + <ul class="menu-list"> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/7" %}">Summer Code Jam 2020</a></li> + <li><a class="has-text-link" href="https://github.com/python-discord/summer-code-jam-2020-qualifier/">Code Jam Qualifier</a></li> + </ul> + </div> +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/_index.html b/pydis_site/templates/events/pages/code-jams/_index.html new file mode 100644 index 00000000..fcbfa4d9 --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/_index.html @@ -0,0 +1,72 @@ +{% extends "events/base_sidebar.html" %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li class="is-active"><a href="#">Code Jams</a></li> +{% endblock %} + +{% block title %}Code Jams{% endblock %} + +{% block event_content %} + <p> + If you've been around the server for a while, or you just happened to join at the right time, + you may have heard of something known as a Code Jam. + This page will help answer some questions you may have about them. + </p> + + <h2 class="title is-4" id="what-is-code-jam"><a href="#what-is-code-jam">What is a Code Jam?</a></h2> + <p> + This is the question you might be asking yourself right now. + A Code Jam is a chance to create something with a team. + In each jam, you are paired up with a group of other users just like yourself who will then be given a type of program to make and a theme to help guide it. + You then have a little over a week's time to create the best project you can. + </p> + + <h2 class="title is-4" id="how-do-i-join"><a href="#how-do-i-join">How do I join?</a></h2> + <p> + Before each Code Jam, we open up a Qualifier. + The Qualifier is a challenge program that you have to write yourself and has to meet certain criteria in order to, well, qualify. + The kinds of qualifiers can vary from something like making an RPG inventory layout to making a password generator that meets certain conditions. + </p> + + <h2 class="title is-4" id="how-often-do-these-happen"><a href="#how-often-do-these-happen">How often do these happen?</a></h2> + <p> + Our Code Jams happen twice a year. We have a Winter Jam and a Summer Jam. + </p> + + <h2 class="title is-4" id="what-happens-if-i-have-to-drop-out"><a href="#what-happens-if-i-have-to-drop-out">What happens if I have to drop out?</a></h2> + <p> + This does happen from time to time. + Life gets in the way, we find out that relative passed away or your significant other RSVP'd you to a wedding you didn't know about until a week before. + Ideally we'd like to know about dropouts before the jam starts. + But if something does come up, please make sure to communicate that fact to any of our Event Coordinators, Moderators, or Admins. + The worst thing you could do is simply abandon your team, and doing so may hurt your chances for participating in future jams. + </p> + + <h2 class="title is-4" id="how-experienced-do-i-need-to-be-to-join-a-code-jam"> + <a href="#how-experienced-do-i-need-to-be-to-join-a-code-jam">How experienced do I need to be to join a Code Jam?</a> + </h2> + <p> + This is a question that we get asked a lot but is very difficult to answer. + Creating something fun with a team can be a great experience for almost everyone, + including relative beginners and more experienced developers. + That said, to make it a fun experience for everyone in the team, + we do require participants to complete a qualifier task so we can ensure that everyone + who enters the jam knows enough of the basics to activily participate in the development process. + </p> + <p> + Participating in a Code Jam often turns out to be a great learning experience. + Not only do you typically get to use packages and libraries you haven't touched before, + it will also increase your experience with developing an application in a team. + If you have never really done this before, + then you'll notice that collaborating with others adds a whole new dimension to the development process. + Even if you do have a lot of experience with working in teams, + having to collaborate with relative strangers of varying skill levels may provide a different experience than what you're used to. + </p> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/code-jams/upcoming-code-jam.html" %} + {% include "events/sidebar/code-jams/previous-code-jams.html" %} + {% include "events/sidebar/code-jams/useful-information.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/judging.html b/pydis_site/templates/events/pages/code-jams/judging.html new file mode 100644 index 00000000..6c6531d9 --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/judging.html @@ -0,0 +1,110 @@ +{% extends "events/base_sidebar.html" %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#">How does judging work?</a></li> +{% endblock %} + +{% block title %}How does judging work?{% endblock %} + +{% block event_content %} + <p> + Here at Python Discord, judging the code jam is a process that continues throughout the entire event. + This document seeks to detail exactly what the judges will consider when attempting to determine the winner of the code jam. + </p> + <p> + Because programming is full of subtleties and judging is not an exact science, this document will inevitably fall short of being an exact guide for how to win, but it should at least give you an idea of what you should be mindful of as a participant. + </p> + + <h2 class="title is-4" id="style"><a href="#style">Style</a></h2> + <p> + Python is a language in which code style matters more than what someone might be used to coming from other languages. + We believe that good code is code that is easy to read, pleasant to work with, + and self-documenting. Here are some of the things we'll consider when judging your submission: + </p> + <ul> + <li> + Do variables have good names that make it obvious what they are going to be used for? + </li> + <li> + Are files organized in a way that makes the project easy to navigate? + </li> + <li> + Does the code follow the conventions of <a href="https://www.python.org/dev/peps/pep-0008/">the official Python style guide</a>? + </li> + <li> + Does your project have good git commit messages that explain not only what you were doing, but why? + </li> + <li> + Are there enough comments in the code? + A good rule of thumb for this is to make sure it's easy to <em>skim through the code</em>. + Another programmer should not have to read every line of code in a section in order to determine what it does. + </li> + <li> + Does your code use blank lines and block comments in order to split up large chunks of code into smaller, logically separated sections? + </li> + <li> + Are your comments and docstrings free of poor formulations and typos? + </li> + </ul> + <p> + If you can answer yes to all of the above, you will probably score well on style. + Using a linter (such as <code>flake8</code>) can be an easy way to greatly improve your style. + </p> + <p> + If your submission contains other languages than Python, + <em>please keep in mind that we will only review the Python code</em>. + This means that it is a good idea for you to solve as much of your submission as possible using Python. + For example, if you're making a webapp with a Python backend, make sure you solve all the interesting stuff in the backend, not in the frontend. + </p> + + <h2 class="title is-4" id="originality"><a href="#originality">Originality</a></h2> + <p> + In order to level the playing field a bit, we've decided that this is the most important factor of all in our code jams. + If your idea is an exceptionally good one and you are a beginner, + we will cut you some slack on style and execution. + You may be able to walk away victorious even though the other teams have far more experienced Python professionals. + It is therefore crucial that you consider <em>what to do more</em> carefully than exactly <em>how to do it</em>. + </p> + <p> + That said, experienced Python developers will always have an advantage, + so don't expect to win with a lazy or ugly implementation just because your idea is brilliant. + </p> + + <h2 class="title is-4" id="execution"><a href="#execution">Execution</a></h2> + <p> + An important factor is whether your solution is the best solution to the problem you are solving. + If your solution is overly convoluted, this will count against you in our evaluation. + </p> + <p> + While the code jam is ongoing, staff will be available to you both inside your team channel and in the common code jam channels. + Staff may drop by your team channel to make helpful suggestions, answer questions, or just ask you how it's going. + Feel free to grab a staff member and ask a question at any point if you're unsure about something. + </p> + <p> + Our talented team of Helpers will also be available during the code jam, + so if there is a part of your code you're unsure about, feel free to bring it to a help channel. + We will be happy to provide advice on how to improve it. + Do not, however, post your entire program; help requests should be bite-sized. + </p> + + <h2 class="title is-4" id="teamwork"><a href="#teamwork">Teamwork</a></h2> + <p> + During the code jam, you will be assigned teammates. It is crucial that you find a way to work with your teammates, + as we will be evaluating your ability to work as a team. + Team members who constantly bicker, complain about each other, + or fail to divide up tasks so that everyone can contribute will be penalized when we evaluate the code jam. + </p> + <p> + This does not, however, mean that a team that has a "bad egg" will automatically be unable to win the code-jam. + If a team member refuses to play well with the rest of the team or does not participate in the jam, + we will usually remove that person from the team and attempt to find a new teammate for the team to replace them. + This will also lead to an infraction against the offending team member, + which may make it impossible for them to join some (or all) future code jams. + </p> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/code-jams/useful-information.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/pull-request.html b/pydis_site/templates/events/pages/code-jams/pull-request.html new file mode 100644 index 00000000..c276fd12 --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/pull-request.html @@ -0,0 +1,235 @@ +{% extends "events/base_sidebar.html" %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#">Opening a Pull Request</a></li> +{% endblock %} + +{% block title %}Opening a Pull Request{% endblock %} + +{% block event_content %} + <p> + When you start out with a jam we ask your team leader to fork the code jam brief repository to their account for your to work on your code. + </p> + <p> + In this guide we're going to go over: + </p> + <ul> + <li> + <a href="#forking-the-repository">Forking the repository</a> + </li> + <li> + <a href="#opening-a-pull-request">Opening a pull request back to the code jam brief on python-discord</a> + </li> + <li> + <a href="#adding-collaborators">Adding collaborators</a> + </li> + <li> + <a href="#adding-a-github-webhook">Adding a GitHub webhook to your teams Discord channel</a> + </li> + </ul> + + <h2 class="title is-4" id="forking-the-repository"><a href="#forking-the-repository">Forking the repository</a></h2> + <p> + When you browse to the code jam brief repository you will be presented with the project root. + </p> + <p> + You want to fork the repository to your account using the button shown in the image below: + </p> + <p> + <img alt="fork button" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/fork-button.png"> + </p> + <p> + If you see a dialog like this one, just select your user. + </p> + <p> + <img alt="where to fork" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/where-to-fork.png"> + </p> + <p> + Once you've done this you'll be redirected to a page like this: + </p> + <p> + <img alt="example fork" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/my-fork.png"> + </p> + <p> + And that's it! You've forked the repo! + </p> + + <h2 class="title is-4" id="opening-a-pull-request"><a href="#opening-a-pull-request">Opening a Pull Request</a></h2> + <p> + Before we can open a Pull Request you need to have something to actually compare against the brief repo. + </p> + <p> + In the actual jam you should fill in the Project information section of the README.md, but for now I'm just going to add a line to the README. + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/commit.png"> + </p> + <p> + Once you've made your commit, navigate back to the project root where you will see this button above your commit bar: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/pull-request-button.png"> + </p> + <p> + You should click on this pull request button. When you do you will see something like this, just click the Create pull request button: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/compare-changes.png"> + </p> + <p> + Next up you'll see a form like this: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/pull-request-form.png"> + </p> + <p> + You should fill it in like so: + </p> + <li> + <strong>Title</strong> should be your team's name + </li> + <li> + <strong>Description</strong> does not have to be filled in + </li> + <li> + Make sure to <strong>select the "allow edits from maintainers" option</strong> + </li> + <li> + Make sure to open a pull request and not a draft pull request + </li> + <p> + That's it! You've opened your pull request and should see something like this: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/pull-request.png"> + </p> + + <h2 class="title is-4" id="adding-collaborators"><a href="#adding-collaborators">Adding collaborators</a></h2> + <p> + Obviously your fork is no good if your team members cannot make edits to it. + To allow your team mates to edit you need to add them to the project as collaborators. + </p> + <p> + We're going to start off by heading to our fork of the repository (this is the one with your username in it, not python-discord) and head to the settings page. + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/settings-page.png"> + </p> + <p> + Click on collaborators. GitHub will ask you to enter your password now for security. Enter it and proceed. + </p> + <p> + You should see a page like this: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/collabs-page.png"> + </p> + <p> + Add your team members using this form and use the copy invite button to copy an invite link they can then use. + You should send this to them in your team channel on Discord. + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/invites.png"> + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/invited-discord.png"> + </p> + <p> + Once your team mates have joined you'll see this: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/accepted.png"> + </p> + <p> + That's all! Your teammates have access to the repository now! + </p> + + <h2 class="title is-4" id="adding-a-github-webhook"><a href="#adding-a-github-webhook">Add a GitHub webhook to Discord</a></h2> + <p> + It may be useful to keep up on your GitHub repository updates right from your Discord channel. + For that reason, we've assigned all team leaders webhook creation permissions inside your team channel. + </p> + <h3 class="title is-5">Webhook creation</h3> + <p> + To make use of this we're going to start out by making a webhook inside the channel. + </p> + <p> + To do this go to your team channel and click on the gear that appears when you mouse-over it. + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/channel-gear.png"> + </p> + <p> + Navigate to the Webhooks section of the settings page and click <code>Create Webhook</code>. + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/webhook-creation-page.png"> + </p> + <p> + You'll see something like this: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/webhook-modal.png"> + </p> + <p> + You can edit the name to 'GitHub' or anything, but GitHub will do this anyway. + </p> + <p> + Copy the link to the clipboard, you will get something like this: + </p> + <p> + <code>https://canary.discordapp.com/api/webhooks/548221675087462410/h7lkQ7cX5-95J8CLMNCGXTEBrWOmXYFWv7N4MqsFtV-D8F0W8Rf3rWj4dPAEFGspm7EZ</code> + </p> + <p> + By default this is not configured to respond to GitHub payloads so you will need to add a <code>/github</code> to the end of the URL. + </p> + <p> + Your new URL should look like: + </p> + <p> + <code>https://canary.discordapp.com/api/webhooks/548221675087462410/h7lkQ7cX5-95J8CLMNCGXTEBrWOmXYFWv7N4MqsFtV-D8F0W8Rf3rWj4dPAEFGspm7EZ/github</code> + </p> + <h3 class="title is-5">Adding to GitHub</h3> + <p> + Next up we will head over to our GitHub project settings (on our fork) and go to the webhooks section: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/webhooks-settings.png"> + </p> + <p> + Click on the Add Webhook button and paste in your URL to the <code>Payload URL</code> section. + </p> + <p> + Set the <code>Content Type</code> to <code>application/json</code> otherwise Discord cannot interpret your request. + </p> + <p> + You can select either just the push or everything depending on how much information you want. + </p> + <p> + Make sure <code>Active</code> is ticked and click <code>Add webhook</code>. + </p> + <p> + And that's it! You've added a Discord webhook to your GitHub repository! + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/added.png"> + </p> + <p> + As you can see here, my teammate has commited a change to our fork: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/aperture-commit.png"> + </p> + <p> + In our Discord channel, you can see the following: + </p> + <p> + <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/webhook-channel.png"> + </p> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/code-jams/useful-information.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/using-git.html b/pydis_site/templates/events/pages/code-jams/using-git.html new file mode 100644 index 00000000..19c4a08f --- /dev/null +++ b/pydis_site/templates/events/pages/code-jams/using-git.html @@ -0,0 +1,61 @@ +{% extends "events/base_sidebar.html" %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> + <li class="is-active"><a href="#">How to use git</a></li> +{% endblock %} + +{% block title %}How to use git{% endblock %} + +{% block event_content %} + <p> + We require all participants to use Git for source control. + This means that you will need to learn a bit of Git to participate in the code jam, + if you're not already familiar with it. Git is incredibly useful, + and an essential skill to master if you ever want to write code out there in the real world. + If you don't yet have Git installed... + </p> + + <ul> + <li> + <strong>Windows</strong>: Install git using <a href="https://gitforwindows.org/">Git for Windows</a> - you can leave all of the options at their defaults, if you wish + </li> + <li> + <strong>Mac</strong>: Install git using <a href="https://brew.sh/">Homebrew</a> + </li> + <li> + <strong>Linux</strong>: Install git using your Linux distro's package manager + </li> + </ul> + + <p> + In order to make the learning process easier, we've included some Git learning resources below... + </p> + <ul> + <li> + Roger Dudler has <a href="https://rogerdudler.github.io/git-guide/">an excellent crash</a> course that will show you the most important commands. This can be used as a cheat sheet once you're starting to actually use Git. + </li> + <li> + If you'd like a wordier introduction, Prithaj Nath (who participated in our first code jam) wrote a more in-depth guide called <a href="https://medium.com/@prithajnath/getting-started-with-git-7aae82dd3599">Let's Git Started!</a> for <a href="https://medium.com">medium.com</a> that might be worth a read. + </li> + <li> + The ever-excellent Corey Schafer has a 30-minute YouTube tutorial called <a href="https://www.youtube.com/watch?v=HVsySz-h9r4">Git Tutorial for Beginners</a> which should teach you everything you need to know to participate in our jams. + </li> + </ul> + + <p> + Optionally, certain IDEs and editors will be able to handle Git for you, + and tools like GitKraken are also permitted. + Make sure you familiarize yourself with the client of your choice before the code jam starts + so you don't need to spend time learning this while the clock is running. + </p> + <p> + For more information on GUI clients you can use, + <a href="https://about.gitlab.com/applications/#gui-git-clients">please see this page</a>. + </p> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/code-jams/useful-information.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/_index.html b/pydis_site/templates/events/pages/game-jams/2020/_index.html new file mode 100644 index 00000000..136c9f4d --- /dev/null +++ b/pydis_site/templates/events/pages/game-jams/2020/_index.html @@ -0,0 +1,104 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Game Jam 2020: Three of a Kind{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> + <li class="is-active"><a href="#">Game Jam 2020: Three of a Kind</a></li> +{% endblock %} + +{% block event_content %} + <p> + During the first ever Python Discord Game Jam, + 23 teams signed up to create a game using the <a href="https://arcade.academy/">Python Arcade Library</a>. + Each team consisted of 1-3 people and teammates were chosen by the participants themselves, + unlike in our <a href="{% url "events:page" path="code-jams" %}">Code Jams</a>. + The Game Jam ran from <strong>April 17, 12:00 UTC to April 26, 18:00 UTC</strong>. + </p> + + <h2 class="title is-4" id="task-description"><a href="#task-description">Task Description</a></h2> + <blockquote> + <p>The theme for this game jam is <strong>Three-of-a-kind</strong>.</p> + <p> + What you do with this theme or how you interpret it is up to you, + but it will be your task to come up with something fun using this theme. + </p> + </blockquote> + + <h2 class="title is-4" id="judging-stream"><a href="#judging-stream">Judging Stream and Interview</a></h2> + <p> + The original Game Jam live stream is shown below, + where the top ten submissions were reviewed and showcased. + There was also an interview with <strong>Paul Craven</strong>, + the creator of the <a href="https://arcade.academy/">Python Arcade Library</a>, + where he answered many questions about his origin/backstory, + the development of the Arcade library, and general programming. + </p> + <iframe width="560" height="315" src="https://www.youtube.com/embed/KkLXMvKfEgs" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> + + <blockquote> + <p> + You can also see the top ten submissions shown + <a href="https://arcade.academy/game_jam_2020.html">here</a> + on the Arcade website and all submissions were submitted + <a href="https://github.com/python-discord/game-jam-2020">here</a> to the repository. + </p> + </blockquote> + + <h2 class="title is-4" id="results"><a href="#results">Results</a></h2> + <p> + The <strong>winning team</strong> for our first ever Game Jam was team <strong>Score AAA</strong>, + consisting of one person, <strong>EmberDuck</strong>. He created a very polished game, + focusing in on one core idea and just nailing it perfectly. + This is a game where the player controls <strong>3</strong> different characters which need to jump over obstacles. + The concept itself may seem simple, but EmberDuck kept iterating on the idea, making it better and better, + taking it to the point of perfection. The graphics were phenomenal, the UI was beautiful and intuitive, and the animations were just fantastic. + The parallax effect in the background was mesmerizing, which was an element that was unique and different from other submissions. + Taking this project to another level, all art assets for this submission were made by EmberDuck himself. + This was an incredible project, especially for a one-man-team. <strong>Congratulations EmberDuck!</strong> + </p> + + <p> + Check out their submission + <a href="https://github.com/python-discord/game-jam-2020/tree/master/Finalists/Score_AAA/">here</a> + and watch it being played live <a href="https://youtu.be/KkLXMvKfEgs?t=7600">here</a>! + </p> + + <h2 class="title is-4" id="runner-up-1"><a href="#runner-up-1">Runner Up 1: Gamer Gang</a></h2> + <p> + The <strong>Gamer Gang</strong> (SansPapyrus683, Blue Bird, & SimplyNarwell) created a platformer game where the player controls + <strong>3</strong> different slimes to get through 6 different levels. + The slimes can either move together in a stack, or the player can split them apart to control each of them one by one. + The physics in this game was a nice addition and tied the game together nicely. + The graphics in this game were quite beautiful and the UI complemented it well. + This game scored quite high on many factors, and was a very fun game overall. Excellent job! + </p> + <p> + Feel free to look at their submission + <a href="https://github.com/python-discord/game-jam-2020/tree/master/Finalists/gamer_gang/">here</a> + and watch it being played on stream <a href="https://youtu.be/KkLXMvKfEgs?t=3812">here</a>! + </p> + + <h2 class="title is-4" id="runner-up-2"><a href="#runner-up-2">Runner Up 2: Monkeys and Frogs on Fire</a></h2> + <p> + Team <strong>Monkeys and Frogs on Fire</strong> (BrainDead, F4zi, & f1re) + created a dungeon-crawler type of game where the player can switch between + <strong>3</strong> different wizards, each having their own special abilities. + The red wizard has great strength, while the green wizard has a lot of defense and the blue wizard is quick and fast. + A unique and exceptional feature of this game was the backend server which allowed a player to register/login. + The goal of this game was to survive as long as you can to get the highest score, + which is then uploaded to the server and can be seen on the leaderboard. + This game had a crisp and smooth UI along with nice graphics and fantastic animations, + bringing this game together. Phenomenol work! + </p> + <p> + Have a look at this submission <a href="https://github.com/Den4200/game-jam-2020">here</a> + and watch this game played live on stream <a href="https://youtu.be/KkLXMvKfEgs?t=5188">here</a>! + </p> + <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png" alt=""> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/game-jams/2020.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/judging.html b/pydis_site/templates/events/pages/game-jams/2020/judging.html new file mode 100644 index 00000000..1ec836ac --- /dev/null +++ b/pydis_site/templates/events/pages/game-jams/2020/judging.html @@ -0,0 +1,68 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Judging{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> + <li><a href="{% url "events:page" path="game-jams/2020" %}">Game Jam 2020: Three of a Kind</a></li> + <li class="is-active"><a href="#">Judging</a></li> +{% endblock %} + +{% block event_content %} + <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png"> + <p> + The game jam has a greatly simplified judging process compared to the <a href="/events/code-jams/">Code Jam events</a>. We will not be evaluating your code quality, your commit history, or your teamwork. We will instead evaluate your project based on a few simple factors, which are outlined in this document. + </p> + + + <!-- Novelty --> + <h2>Creativity & Novelty Factor</h2> + <p> + The theme and the tech that we provide are provided as limitations, because we believe that <em>limitations breed creativity</em>. One of the big factors we consider when we judge these projects is the novelty factor. If you've created something truly unique, you've got a leg up on someone who made a very polished version of something that's very familiar. This means that even if you're not an expert programmer or experienced game dev, the right idea (and that ideas' adherence to the theme) might still net you a win! + </p> + <p> + Please try to spend some time coming up with the most fun and novel idea you can before diving into your actual programming. If you're not sure how, try googling for <strong>game dev ideation techniques</strong>. Professional game developers frequently make use of these techniques in order to help spin the wheels of creativity. + </p> + <p> + For example, you can try to write down a bunch of different adjectives (like "yellow" or "terrified") on red notes, nouns (like "lemon" or "snowman") on blue notes, and modifiers (like "in space!" or "the floor is lava!") on yellow notes. Then select a random note from each color, put them together in different orders and think about how you can create a game idea around it that fits the theme. + </p> + + <!-- Execution--> + <h2>Execution</h2> + <p> + We will of course be evaluating the execution. How well does the game perform? How is the user experience? Is it fun? Is it easy to play? Does it look good, sound good, and feel good? The performance and interaction are probably the most important factors, but polish is nice, too! + </p> + <p> + We're going to try to do this in an objective way, but stuff like fun is always going to be a somewhat subjective thing to evaluate, and for that reason, this is not the single most critical criteria in the judging process, like it might have been in other game jams. To be more precise - A really fun and novel idea that ticks all the other boxes but has somewhat mediocre execution will still have a very good chance of doing well in this event. + </p> + + <!-- Adherence--> + <h2>Theme Adherence</h2> + <p>Your submission must adhere to the theme we will present on the day the jam starts. There are different degrees of adherence here, and quite a lot of room to be creative, but a deeply thematic submission will be scored higher than a barely thematic submission, and a submission that does not adhere to the theme at all <strong>may be disqualified</strong>. + </p> + <p> + We love seeing multiple <em>layers</em> of thematic adherence - For example, your theme could play a part in your team name, your art style, your story, your sound effects, and of course, in the gameplay. You might even try to inject your theme into your documentation! + </p> + + <!-- Documentation via README.md--> + <h2>Documentation</h2> + <p>We require that you make the project easy to comprehend, easy to install, and easy to run. To ensure this, your submission must contain a <code>README.md</code> in your team folder which clearly documents how to interact with it. + </p> + <p> + First of all, you need to include instructions on exactly how to get your game running. We require that you use a <strong>dependency manager</strong> and encourage you to make a <strong>click-and-play option</strong>, and these should be documented in this section. Please see <a href="/events/game-jam-2020/technical-requirements/">our technical requirements page</a> for more information on this. Remember, we have to judge dozens of submissions, so if every submission can be set up and run by just doing <code>pipenv install</code> and then <code>pipenv run start</code>, it will make our job significantly easier. + </p> + <p> + The readme should also contain information about the game, screenshots, a logo if you got it, your team name, instructions on how to play and anything else that helps provide a high-level overview over the game. This is also extremely helpful when we are judging so many projects, so that we can easily see which is which by looking through readmes. For an example of an excellent readme, check out <a href="https://github.com/python-discord/code-jam-6/blob/master/amphibian-alchemists/README.md">this readme created by the Amphibian Alchemists team during Code Jam 6</a>. + </p> + + <!-- Team size--> + <h2>Team size</h2> + <p> + For this game jam, we are allowing you to sign up either by yourself, or with one or two friends. But, of course, it wouldn't be fair if we judged all entries the same regardless of team size - so we will be expecting slightly more from a team of 3 than from a solo team. To be more precise, we will not be expecting <em>three times as much</em> - working in a team has its own set of unique setbacks as well as advantages, and two developers does not equal 200% the output of one - but we will be expecting a full team to be able to deliver more content in ten days than a single person can. + </p> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/game-jams/2020.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/project-setup.html b/pydis_site/templates/events/pages/game-jams/2020/project-setup.html new file mode 100644 index 00000000..962cd556 --- /dev/null +++ b/pydis_site/templates/events/pages/game-jams/2020/project-setup.html @@ -0,0 +1,57 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Project Setup{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> + <li><a href="{% url "events:page" path="game-jams/2020" %}">Game Jam 2020: Three of a Kind</a></li> + <li class="is-active"><a href="#">Project Setup</a></li> +{% endblock %} + +{% block event_content %} + <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png"> + <br></br> + <p> + The first step is to fork our repository: <a href="https://github.com/python-discord/game-jam-2020/">python-discord/game-jam-2020</a>. You can learn how to do this in our + <a href="https://pythondiscord.com/pages/guides/pydis-guides/contributing/forking-repository/">contributing guidelines</a>. + </p> + + <p> + Alternatively, GitHub has a <a href="https://help.github.com/en/github/getting-started-with-github/fork-a-repo">detailed suite of tutorials explaining forking</a> as well as other technical terms that you may come across. If you get + stuck at any point, you are always welcome to ask for help at Python Discord. + </p> + + <p>Once the fork is created, each team will make a directory for their project. The directory should be named after the team. For example, team <strong>Precise Rabbits</strong> will set up a directory as follows:</p> + + <pre><code>game-jam-2020/ + └── precise-rabbits/ + └── game/ + ├── __init__.py + ├── __main__.py + └── rabbits.py + ├── config.json + ├── Pipfile + ├── Pipfile.lock + └── README.md</code></pre> + + <p><strong>Do not make any changes to other teams' project directories.</strong></p> + + <p> + Each team will then develop their game within their fork. You are encouraged to use git to its full potential - feature branches will help you keep your commit history clean, and prevent annoying conflicts. Feel free to refer to + <a href="https://pythondiscord.com/pages/guides/pydis-guides/contributing/working-with-git/">our guidelines on the topic</a>, or any other resources that you may find useful. Version control is an important aspect of software development, and code jams are + a great opportunity to improve skills in the area. + </p> + + <p> + Once the project is finished, a pull request will be opened against the upstream repository. All of your projects will be merged to the upstream repository, which means that you will be credited as a contributor to an open source + project. The final version <strong>must be committed by the jam's deadline</strong>. Any changes beyond this point will not be considered in the judging process. + </p> + + <p>Please ensure that your final version is well tested, and satisfies our <a href="https://pythondiscord.com/events/game-jams/2020/technical-requirements/">technical requirements</a>.</p> + +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/game-jams/2020.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/rules.html b/pydis_site/templates/events/pages/game-jams/2020/rules.html new file mode 100644 index 00000000..33eccf63 --- /dev/null +++ b/pydis_site/templates/events/pages/game-jams/2020/rules.html @@ -0,0 +1,31 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Game Jam Rules{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> + <li><a href="{% url "events:page" path="game-jams/2020" %}">Game Jam 2020: Three of a Kind</a></li> + <li class="is-active"><a href="#">Game Jam Rules</a></li> +{% endblock %} + +{% block event_content %} + <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png"> + <ol> + <li>You may enter individually or with up to two friends.</li> + <li>All members of your team must be members of our Discord community.</li> + <li>Your project must use the <a href="https://arcade.academy/">Python Arcade Library</a>. It is not permitted to work around this by, e.g., using the Python Arcade Libary as a wrapper for another framework.</li> + <li>The majority of your project must be written in Python.</li> + <li>Your project must be feasible to run and simple to set up on a <strong>desktop computer</strong>.</li> + <li>You are allowed to use existing assets, like images and sound effects, as long as the licenses of those assets permit it. Typically, this means that the assets are licensed under an OSI-approved or Creative Commons license, or is in the public domain.</li> + <li>All projects should start from scratch and all code must be written within the time constrictions of the jam.</li> + <li>You retain all copyrights to entries you submit. By submitting your entry, you grant a transferrable, irrevocable license to redistribute, copy and run your entry without modification, and to distribute screenshots and other forms of gameplay captures of the entry, provided no fee is charged.</li> + </ol> + <blockquote> + Please note that our regular <a href="/pages/rules/">community rules</a> and <a href="/pages/code-of-conduct/">code of conduct</a> also apply during the event and that we reserve the right to make changes to these rules at any time. + </blockquote> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/game-jams/2020.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/technical-requirements.html b/pydis_site/templates/events/pages/game-jams/2020/technical-requirements.html new file mode 100644 index 00000000..4a44e7db --- /dev/null +++ b/pydis_site/templates/events/pages/game-jams/2020/technical-requirements.html @@ -0,0 +1,41 @@ +{% extends "events/base_sidebar.html" %} + +{% block title %}Technical Requirements{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> + <li><a href="{% url "events:page" path="game-jams/2020" %}">Game Jam 2020: Three of a Kind</a></li> + <li class="is-active"><a href="#">Technical Requirements</a></li> +{% endblock %} + +{% block event_content %} + <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png"> + + <br></br> + + <p>The technological theme of this game jam is <a href="https://arcade.academy/">Arcade</a>. As per <a href="https://pythondiscord.com/events/game-jams/2020/rules/">rule 3</a>, please ensure that you use Arcade as your project's main framework. Your project can use other dependencies, as long as they do not violate rule 3. If you're unsure about a potential dependency, consult us in the discussion channel.</p> + + <h2>Dependency management</h2> + + <p>Participating teams are asked to provide a <em>click-and-play</em> option to launch their project. There should be no complicated process to install dependencies, or otherwise prepare the environment, before the project can be started. This will allow us to spend more time evaluating the actual game. Ideally, projects will make use of <a href="https://github.com/pypa/pipenv">Pipenv</a> to both install dependencies and launch the game, but it is also acceptable to solve this with other dependency managers, or even with a <code>requirements.txt</code> file and a great readme. <strong>Ideally, it should only take a judge 2-3 commands to get your game ready to run, and these should be clearly documented in your readme.</strong></p> + + <p>Working with Pipenv is not difficult, and existing Python Discord projects can be used for reference. For example, <a href="https://github.com/python-discord/seasonalbot/">Seasonalbot's</a> <code>Pipfile</code> and <code>Pipfile.lock</code> list all necessary dependencies and provide a run script. As a result, a virtual environment with all necessary dependencies can be conveniently created with <code>pipenv install</code>, and the project can be launched with <code>pipenv run start</code>.</p> + + <p>In simple terms, emulating this workflow would be the ideal way to handle dependency management for this game jam.</p> + + <p>If you're unfamiliar with Pipenv, we recommend Corey Schafer's excellent video as a learning resource.</p> + + <iframe width="560" height="315" src="https://www.youtube.com/embed/zDYL22QNiWk" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> + + </br> + </br> + + <h2>Target platform</h2> + + <p>All submissions will be evaluated on Windows 10. This does not necessarily mean that all development must take place on Windows, but all submissions should be functional and well tested on the target platform. This is also something to keep in mind in the initial stages of development - make sure you don't accidentally build your project around a dependency that isn't available on Windows.</p> +{% endblock %} + +{% block sidebar %} + {% include "events/sidebar/game-jams/2020.html" %} +{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/_index.html b/pydis_site/templates/events/pages/game-jams/_index.html new file mode 100644 index 00000000..85048cf1 --- /dev/null +++ b/pydis_site/templates/events/pages/game-jams/_index.html @@ -0,0 +1,12 @@ +{% extends "events/base.html" %} + +{% block title %}Game Jams{% endblock %} + +{% block breadcrumb %} + <li><a href="{% url "events:index" %}">Events</a></li> + <li class="is-active"><a href="#">Game Jams</a></li> +{% endblock %} + +{% block event_content %} + <p>Sorry, this page is not ready yet.</p> +{% endblock %} diff --git a/pydis_site/templates/events/sidebar/code-jams/6.html b/pydis_site/templates/events/sidebar/code-jams/6.html new file mode 100644 index 00000000..95a1f247 --- /dev/null +++ b/pydis_site/templates/events/sidebar/code-jams/6.html @@ -0,0 +1,6 @@ +<div class="box"> + <img src="https://raw.githubusercontent.com/python-discord/branding/master/events/winer_code_jam_2020/code%20jam%206%20-%20website%20banner.png" alt=""> + <p class="menu-label">Sponsors:</p> + <p><a href="https://do.co/devmeetup" target="_new"><img src="https://d9hhrg4mnvzow.cloudfront.net/try.digitalocean.com/developer-cloud/3f06d1cb-do-logo-blue-svg.svg" style="width: 100%;" alt=""></a></p> + <p><a href="https://jetbrains.com" target="_new"><img src="https://pythondiscord.com/static/images/sponsors/jetbrains.png" alt=""></a></p> +</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/7.html b/pydis_site/templates/events/sidebar/code-jams/7.html new file mode 100644 index 00000000..d4615c2a --- /dev/null +++ b/pydis_site/templates/events/sidebar/code-jams/7.html @@ -0,0 +1,12 @@ +{% load static %} + +<div class="box"> + <img src="https://raw.githubusercontent.com/python-discord/branding/master/events/summer_code_jam_2020/summer%20cj%202020%20discord%20banner.png" alt="Summer Code Jam 2020"> + <p class="menu-label">Sponsors</p> + <a href="https://www.djangoproject.com/" target="_blank"> + <img src="https://static.djangoproject.com/img/logos/django-logo-positive.png" alt="Django"> + </a> + <a href="https://jetbrains.com" target="_blank"> + <img src="{% static "images/sponsors/jetbrains.png" %}" alt="JetBrains"> + </a> +</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html b/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html new file mode 100644 index 00000000..154f6ac4 --- /dev/null +++ b/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html @@ -0,0 +1,11 @@ +<div class="box"> + <p class="menu-label">Previous Code Jams</p> + <ul class="menu-list"> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/6" %}">Code Jam 6: Ancient Technology</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/5" %}">Code Jam 5: Climate Change</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/4" %}">Code Jam 4: This App Hates You</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/3" %}">Code Jam 3: Games!</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/2" %}">Code Jam 2: Mythology API</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/1" %}">Code Jam 1: Snake Bot</a></li> + </ul> +</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/upcoming-code-jam.html b/pydis_site/templates/events/sidebar/code-jams/upcoming-code-jam.html new file mode 100644 index 00000000..914a9545 --- /dev/null +++ b/pydis_site/templates/events/sidebar/code-jams/upcoming-code-jam.html @@ -0,0 +1,8 @@ +{% load static %} + +<div class="box"> + <p class="menu-label">Upcoming Code Jam</p> + <a href="{% url "events:page" path="code-jams/7" %}"> + <img src="{% static "images/events/summer_code_jam_2020.png" %}" alt=""> + </a> +</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/useful-information.html b/pydis_site/templates/events/sidebar/code-jams/useful-information.html new file mode 100644 index 00000000..c4e665e6 --- /dev/null +++ b/pydis_site/templates/events/sidebar/code-jams/useful-information.html @@ -0,0 +1,8 @@ +<div class="box"> + <p class="menu-label">Useful information for code jam participants</p> + <ul class="menu-list"> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/using-git" %}">How to use git</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/judging" %}">How does judging work?</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/pull-request" %}">Opening a Pull Request</a></li> + </ul> +</div> diff --git a/pydis_site/templates/events/sidebar/events-list.html b/pydis_site/templates/events/sidebar/events-list.html new file mode 100644 index 00000000..327b0e77 --- /dev/null +++ b/pydis_site/templates/events/sidebar/events-list.html @@ -0,0 +1,10 @@ +<div class="box"> + <p class="menu-label">Event Calendar 2020</p> + <ul class="menu-list"> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/6" %}">January 17-January 26: Winter Code Jam</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="game-jams/2020" %}">April 17-April 26: Game Jam</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="code-jams/7" %}">July 31-August 9: Summer Code Jam</a></li> + <li><a class="has-text-black" style="cursor: default;">October: Hacktoberfest</a></li> + <li><a class="has-text-black" style="cursor: default;">December: Advent of Code</a></li> + </ul> +</div> diff --git a/pydis_site/templates/events/sidebar/game-jams/2020.html b/pydis_site/templates/events/sidebar/game-jams/2020.html new file mode 100644 index 00000000..30042d2b --- /dev/null +++ b/pydis_site/templates/events/sidebar/game-jams/2020.html @@ -0,0 +1,14 @@ +<div class="box"> + <p class="menu-label">Partners</p> + <p><a href="https://arcade.academy/" target="_new"><img src="https://i.imgur.com/CtY3Siv.png" style="width: 100%;" alt=""></a></p> +</div> + +<div class="box"> + <p class="menu-label">Relevant Links</p> + <ul class="menu-list"> + <li><a class="has-text-link" href="{% url "events:page" path="game-jams/2020/rules" %}">Rules</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="game-jams/2020/project-setup" %}">Project Setup</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="game-jams/2020/technical-requirements" %}">Technical Requirements</a></li> + <li><a class="has-text-link" href="{% url "events:page" path="game-jams/2020/judging" %}">Judging</a></li> + </ul> +</div> diff --git a/pydis_site/templates/events/sidebar/upcoming-event.html b/pydis_site/templates/events/sidebar/upcoming-event.html new file mode 100644 index 00000000..5c1d925a --- /dev/null +++ b/pydis_site/templates/events/sidebar/upcoming-event.html @@ -0,0 +1,8 @@ +{% load static %} + +<div class="box"> + <p class="menu-label">Upcoming Event</p> + <a href="{% url "events:page" path="code-jams/7" %}"> + <img src="{% static "images/events/summer_code_jam_2020.png" %}" alt=""> + </a> +</div> diff --git a/pydis_site/templates/events/test-pages/my-event/_index.html b/pydis_site/templates/events/test-pages/my-event/_index.html new file mode 100644 index 00000000..1a859fdd --- /dev/null +++ b/pydis_site/templates/events/test-pages/my-event/_index.html @@ -0,0 +1 @@ +{% extends "events/base.html" %} diff --git a/pydis_site/templates/events/test-pages/my-event/subpage.html b/pydis_site/templates/events/test-pages/my-event/subpage.html new file mode 100644 index 00000000..1a859fdd --- /dev/null +++ b/pydis_site/templates/events/test-pages/my-event/subpage.html @@ -0,0 +1 @@ +{% extends "events/base.html" %} diff --git a/pydis_site/templates/home/account/delete.html b/pydis_site/templates/home/account/delete.html deleted file mode 100644 index 0d44e32a..00000000 --- a/pydis_site/templates/home/account/delete.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends 'base/base.html' %} -{% load static %} - -{% block title %}Delete Account{% endblock %} - -{% block content %} - {% include "base/navbar.html" %} - - <section class="section content"> - <div class="container"> - <h2 class="is-size-2 has-text-centered">Account Deletion</h2> - - <div class="columns is-centered"> - <div class="column is-half-desktop is-full-tablet is-full-mobile"> - - <article class="message is-danger"> - <div class="message-body"> - <p> - You have requested to delete the account with username - <strong><span class="has-text-dark is-family-monospace">{{ user.username }}</span></strong>. - </p> - - <p> - Please note that this <strong>cannot be undone</strong>. - </p> - - <p> - To verify that you'd like to remove your account, please type your username into the box below. - </p> - </div> - </article> - </div> - </div> - - <div class="columns is-centered"> - <div class="column is-half-desktop is-full-tablet is-full-mobile"> - <form method="post"> - {% csrf_token %} - <label for="id_username" class="label requiredField">Username</label> - <input id="id_username" class="input" type="text" required name="username"> - <input style="margin-top: 1em;" type="submit" value="I understand, delete my account" class="button is-primary"> - </form> - </div> - </div> - </div> - </section> -{% endblock %} diff --git a/pydis_site/templates/home/account/settings.html b/pydis_site/templates/home/account/settings.html deleted file mode 100644 index ed59b052..00000000 --- a/pydis_site/templates/home/account/settings.html +++ /dev/null @@ -1,136 +0,0 @@ -{% load socialaccount %} - -{# This template is just for a modal, which is actually inserted into the navbar #} -{# template. Take a look at `navbar.html` to see how it's inserted. #} - -<div class="modal" id="account-modal"> - <div class="modal-background"></div> - <div class="modal-card"> - <div class="modal-card-head"> - <div class="modal-card-title">Settings for {{ user.username }}</div> - - {% if groups %} - <div> - {% for group in groups %} - <span class="tag is-primary">{{ group.name }}</span> - {% endfor %} - </div> - {% else %} - <span class="tag is-dark">No groups</span> - {% endif %} - </div> - <div class="modal-card-body"> - <h3 class="title">Connections</h3> - <div class="columns"> - {% if discord_provider is not None %} - <div class="column"> - <div class="box"> - {% if not discord %} - <div class="media"> - <div class="media-left"> - <div class="image"> - <i class="fab fa-discord fa-3x has-text-primary"></i> - </div> - </div> - <div class="media-content"> - <div class="title is-5">Discord</div> - <div class="subtitle is-6">Not connected</div> - </div> - </div> - <div> - <br /> - <a class="button is-primary" href="{% provider_login_url "discord" process="connect" %}"> - <span class="icon"> - <i class="fad fa-link"></i> - </span> - <span>Connect</span> - </a> - </div> - {% else %} - <div class="media"> - <div class="media-left"> - <div class="image"> - <i class="fab fa-discord fa-3x has-text-primary"></i> - </div> - </div> - <div class="media-content"> - <div class="title is-5">Discord</div> - <div class="subtitle is-6">{{ user.username }}</div> - </div> - </div> - <div> - <br /> - <button class="button" disabled> - <span class="icon"> - <i class="fas fa-check"></i> - </span> - <span>Connected</span> - </button> - </div> - {% endif %} - </div> - </div> - {% endif %} - - {% if github_provider is not None %} - <div class="column"> - <div class="box"> - {% if not github %} - <div class="media"> - <div class="media-left"> - <div class="image"> - <i class="fab fa-github fa-3x"></i> - </div> - </div> - <div class="media-content"> - <div class="title is-5">GitHub</div> - <div class="subtitle is-6">Not connected</div> - </div> - </div> - <div> - <br /> - <a class="button is-primary" href="{% provider_login_url "github" process="connect" %}"> - <span class="icon"> - <i class="fad fa-link"></i> - </span> - <span>Connect</span> - </a> - </div> - {% else %} - <div class="media"> - <div class="media-left"> - <div class="image"> - <i class="fab fa-github fa-3x"></i> - </div> - </div> - <div class="media-content"> - <div class="title is-5">GitHub</div> - <div class="subtitle is-6">{{ github.extra_data.name }}</div> - </div> - </div> - <div> - <form method="post" action="{% url "account_settings" %}" type="submit"> - {% csrf_token %} - - <input type="hidden" name="provider" value="github" /> - - <br /> - <button type="submit" class="button is-danger"> - <span class="icon"> - <i class="fas fa-times"></i> - </span> - <span>Disconnect</span> - </button> - </form> - </div> - {% endif %} - </div> - </div> - {% endif %} - </div> - </div> - <div class="modal-card-foot"> - <a class="button is-danger" href="{% url "account_delete" %}">Delete Account</a> - </div> - </div> -</div> diff --git a/pydis_site/templates/resources/resource_box.html b/pydis_site/templates/resources/resource_box.html new file mode 100644 index 00000000..af7c8d65 --- /dev/null +++ b/pydis_site/templates/resources/resource_box.html @@ -0,0 +1,22 @@ +{% load as_icon %} + +<div class="box" style="max-width: 800px;"> + {% if 'title_url' in resource %} + <a href="{{ resource.title_url }}"> + {% include "resources/resource_box_header.html" %} + </a> + {% else %} + {% include "resources/resource_box_header.html" %} + {% endif %} + + <p class="is-italic">{{ resource.description|safe }}</p> + + {# Icons #} + {% for icon in resource.urls %} + <span class="icon is-size-4 is-medium" style="margin: 5px;"> + <a href="{{ icon.url }}"> + <i class="{{ icon.icon|as_icon }} is-size-3 resource-icon is-{{ icon.color }}"></i> + </a> + </span> + {% endfor %} +</div> diff --git a/pydis_site/templates/resources/resource_box_header.html b/pydis_site/templates/resources/resource_box_header.html new file mode 100644 index 00000000..84e1a79b --- /dev/null +++ b/pydis_site/templates/resources/resource_box_header.html @@ -0,0 +1,24 @@ +{% load as_icon %} + +{# Icon section #} +{% if 'icon_image' in resource %} + <img src="{{ resource.icon_image }}" alt="" style="height: {{ resource.icon_size|default:30 }}px;"> +{% elif 'title_icon' in resource %} + <span class="icon is-size-4 is-medium"> + <i class="{{ resource.title_icon|as_icon }} is-size-3 resource-icon has-icon-padding is-{{ resource.title_icon_color }}"></i> + </span> +{% elif 'default_icon' in category_info %} + <span class="icon is-size-4 is-medium"> + <i class="{{ category_info.default_icon|as_icon }} is-size-3 resource-icon has-icon-padding is-{{ category_info.default_icon_color }}"></i> + </span> +{% endif %} + +{# Title section #} +<span class="is-size-4 has-text-weight-bold"> + {% if 'title_image' in resource %} + <img src="{{ resource.title_image }}" alt="" style="height: 50px; {{ resource.title_image_style }}"> + {% endif %} + {% if 'name' in resource %} + {{ resource.name }} + {% endif %} +</span> diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html new file mode 100644 index 00000000..f1f487cf --- /dev/null +++ b/pydis_site/templates/resources/resources.html @@ -0,0 +1,90 @@ +{% extends 'base/base.html' %} +{% load static %} + +{% block title %}Resources{% endblock %} +{% block head %} + <link rel="stylesheet" href="{% static "css/resources/resources.css" %}"> +{% endblock %} + +{% block content %} + {% include "base/navbar.html" %} + + <section class="section"> + <div class="container"> + <div class="content"> + <h1>Resources</h1> + + <div class="tile is-ancestor"> + <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> + </article> + </a> + + <div class="tile is-vertical is-9"> + <div class="tile"> + <a class="tile is-8 is-parent" href="{% url "resources:resources" category="reading" %}"> + <article class="tile is-child box hero is-black" id="readingBlock"> + <p class="title is-size-1"><i class="fad fa-book-alt" aria-hidden="true"></i> Read</p> + <p class="subtitle is-size-4">Lovingly curated books to explore</p> + </article> + </a> + + <div class="tile"> + <a class="tile is-parent" href="{% url "resources:resources" category="videos" %}"> + <article class="tile is-child box hero is-danger is-bold"> + <p class="title is-size-1"><i class="fad fa-video" aria-hidden="true"></i> Watch</p> + <p class="subtitle is-size-4">Visually engaging</p> + </article> + </a> + </div> + </div> + + <div class="tile"> + <a class="tile is-parent" href="{% url "resources:resources" category="interactive" %}"> + <article class="tile is-child box hero is-black" id="interactiveBlock"> + <p class="title is-size-1"><i class="fad fa-code" aria-hidden="true"></i> Try</p> + <p class="subtitle is-size-4">Interactively discover the possibilities</p> + </article> + </a> + <a class="tile is-8 is-parent" href="{% url "resources:resources" category="courses" %}"> + <article class="tile is-child box hero is-success is-bold"> + <p class="title is-size-1"><i class="fad fa-graduation-cap" aria-hidden="true"></i> Learn</p> + <p class="subtitle is-size-4">Structured courses with clear goals</p> + </article> + </a> + </div> + </div> + </div> + + <div class="tile is-ancestor"> + <div class="tile is-vertical is-9"> + <div class="tile"> + <a class="tile is-8 is-parent" href="{% url "resources:resources" category="communities" %}"> + <article class="tile is-child box hero is-black" id="communitiesBlock"> + <p class="title is-size-1"><i class="fad fa-users" aria-hidden="true"></i> Communities</p> + <p class="subtitle is-size-4">Some of our best friends</p> + </article> + </a> + <div class="tile"> + <a class="tile is-parent" href="{% url "resources:resources" category="podcasts" %}"> + <article class="tile is-child box hero is-black" id="podcastsBlock"> + <p class="title is-size-1"><i class="fad fa-podcast" aria-hidden="true"></i> Listen</p> + <p class="subtitle is-size-4">Regular podcasts to follow</p> + </article> + </a> + </div> + </div> + </div> + <a class="tile is-parent" href="{% url "resources:resources" category="tools" %}"> + <article class="tile is-child box hero is-dark"> + <p class="title is-size-1"><i class="fad fa-tools" aria-hidden="true"></i> Tools</p> + <p class="subtitle is-size-4">Things we love to use</p> + </article> + </a> + </div> + </div> + </div> + </section> +{% endblock %} diff --git a/pydis_site/templates/resources/resources_list.html b/pydis_site/templates/resources/resources_list.html new file mode 100644 index 00000000..e2be3cb7 --- /dev/null +++ b/pydis_site/templates/resources/resources_list.html @@ -0,0 +1,52 @@ +{% extends "base/base.html" %} +{% load as_icon %} +{% load static %} + +{% block title %}{{ category_info.name }}{% endblock %} +{% block head %} + <link rel="stylesheet" href="{% static "css/resources/resources_list.css" %}"> +{% endblock %} + +{% block content %} + {% include "base/navbar.html" %} + + <section class="section breadcrumb-section"> + <div class="container"> + <nav class="breadcrumb is-pulled-left" aria-label="breadcrumbs"> + <ul> + <li><a href="{% url "resources:index" %}">Resources</a></li> + <li class="is-active"><a href="#">{{ category_info.name }}</a></li> + </ul> + </nav> + </div> + </section> + + <section class="section"> + <div class="container"> + <div class="content"> + <h1>{{ category_info.name }}</h1> + <p>{{ category_info.description|safe }}</p> + <div> + {% for resource in resources|dictsort:"position" %} + {% include "resources/resource_box.html" %} + {% endfor %} + + {% for subcategory in subcategories|dictsort:"category_info.position" %} + <h2 id="{{ subcategory.category_info.raw_name }}"> + <a href="{% url "resources:resources" category=category_info.raw_name %}#{{ subcategory.category_info.raw_name }}"> + {{ subcategory.category_info.name }} + </a> + </h2> + <p>{{ subcategory.category_info.description|safe }}</p> + + {% for resource in subcategory.resources|dictsort:"position" %} + {% with category_info=subcategory.category_info %} + {% include "resources/resource_box.html" %} + {% endwith %} + {% endfor %} + {% endfor %} + </div> + </div> + </div> + </section> +{% endblock %} diff --git a/pydis_site/templates/wiki/article.html b/pydis_site/templates/wiki/article.html deleted file mode 100644 index 7f2b3af6..00000000 --- a/pydis_site/templates/wiki/article.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "wiki/base.html" %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}{{ article.current_revision.title }}{% endblock %} - -{% block wiki_navbar %} - {% include "wiki/includes/article_menu.html" %} -{% endblock %} - -{% block wiki_breadcrumbs %} - {% include "wiki/includes/breadcrumbs.html" %} -{% endblock %} - -{% block wiki_contents %} - <div id="article-container"> - <h1 id="article-title"> - {{ article.current_revision.title }} - </h1> - <div> - {% block wiki_contents_tab %} - {% endblock %} - </div> - </div> -{% endblock %} diff --git a/pydis_site/templates/wiki/base.html b/pydis_site/templates/wiki/base.html deleted file mode 100644 index 846492ab..00000000 --- a/pydis_site/templates/wiki/base.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base/base.html" %} -{% load static %} -{% load wiki_tags %} - -{% block title %}{% block wiki_pagetitle %}{% endblock %}{% block wiki_site_title %}{% endblock %}{% endblock %} - -{% block head %} - {{ block.super }} - - <script src="{% static "wiki/js/jquery-3.4.1.min.js" %}" type="text/javascript"></script> - <script src="{% static "wiki/js/core.js" %}" type="text/javascript"></script> - <script src="{% static "js/wiki/simplemde.min.js" %}" type="text/javascript"></script> - - <link rel="stylesheet" href="{% static "css/wiki/simplemde.min.css" %}"> - <link rel="stylesheet" href="{% static "css/wiki/simplemde-fixes.css" %}"> - <link rel="stylesheet" href="{% static "css/wiki/style.css" %}"> - <link rel="stylesheet" href="{% static "css/pygments/darcula.css" %}"> - <link rel="stylesheet" href="{% static "css/base/notification.css" %}"> -{% endblock %} - -{% block content %} - {% block site_navbar %} - {% include "base/navbar.html" %} - {% endblock %} - - {% block wiki_navbar %}{% endblock %} - - {% block wiki_breadcrumbs %} - {% include "wiki/includes/breadcrumbs.html" %} - {% endblock %} - - {% block wiki_body %} - <section class="section"> - <div class="container"> - <div class="content"> - {% block wiki_contents %}{% endblock %} - </div> - </div> - </section> - {% endblock %} - - <script src="{% static "js/wiki/dropdown.js" %}" type="text/javascript"></script> - <script src="{% static "js/wiki/load_editor.js" %}" type="text/javascript"></script> -{% endblock %} diff --git a/pydis_site/templates/wiki/create.html b/pydis_site/templates/wiki/create.html deleted file mode 100644 index 3fbba969..00000000 --- a/pydis_site/templates/wiki/create.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "wiki/base.html" %} -{% load sekizai_tags %} -{% load static %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}Add new article{% endblock %} - -{% block wiki_contents %} - {% addtoblock "js" %} - <script type="text/javascript" src="{% static "admin/js/urlify.js" %}"></script> - - {% if not create_form.slug.value %} - <script type="text/javascript" src="{% static "js/wiki/create.js" %}"></script> - {% endif %} - {% endaddtoblock %} - - {% include "wiki/includes/editormedia.html" %} - <h1 class="page-header">Add new article</h1> - - <form method="POST" class="form-horizontal"> - {% wiki_form create_form %} - - <div class="field is-horizontal is-grouped"> - <div class="control"> - <a href="{% url 'wiki:get' path=parent_urlpath.path %}" class="button is-light"> - <span class="icon"> - <i class="fa fa-arrow-left"></i> - </span> - <span>Go back</span> - </a> - </div> - <div class="control"> - <button type="submit" name="save_changes" class="button is-primary"> - <span class="icon"> - <i class="fa fa-plus"></i> - </span> - <span>Create Article</span> - </button> - </div> - </div> - </form> -{% endblock %} diff --git a/pydis_site/templates/wiki/create_root.html b/pydis_site/templates/wiki/create_root.html deleted file mode 100644 index 2d09089d..00000000 --- a/pydis_site/templates/wiki/create_root.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "wiki/base.html" %} -{% load static %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}{Create root article{% endblock %} - -{% block head %} - {{ block.super }} - - {% for js in editor.Media.js %} - <script type="text/javascript" src="{% static js %}"></script> - {% endfor %} - - {% for media, srcs in editor.Media.css.items %} - {% for src in srcs %} - <link rel="stylesheet" media="{{ media }}" href="{% static src %}" /> - {% endfor %} - {% endfor %} -{% endblock %} - -{% block wiki_contents %} - <h2 class="title is-2">Create First Article</h2> - - <p> - Please create the root article. This article will be available at the root of your wiki, - so consider creating a landing page here. - </p> - <p> - Please note that, to begin with, this article may only be modified by wiki administrators. - Once it's been created, you may edit the permissions and set up plugins, metadata, and so on. - </p> - - <form method="POST" class="form-horizontal"> - {% wiki_form form %} - <div class="field is-horizontal"> - <div class="field-body"> - <div class="control"> - <button type="submit" class="button is-primary" name="save_changes"> - <span class="icon"> - <i class="fas fa-plus"></i> - </span> - <span>Create</span> - </button> - </div> - </div> - </div> - </form> -{% endblock %} diff --git a/pydis_site/templates/wiki/delete.html b/pydis_site/templates/wiki/delete.html deleted file mode 100644 index bb7a7966..00000000 --- a/pydis_site/templates/wiki/delete.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends "wiki/base.html" %} -{% load static %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}Delete Article{% endblock %} - -{% block wiki_contents %} - <h1 class="title is-1">Delete "{{ article.current_revision.title }}"</h1> - - {% if cannot_delete_root %} - <article class="message is-warning"> - <div class="message-body"> - <p> - You cannot delete the root article. - </p> - - <a href="{% url 'wiki:get' path=urlpath.path article_id=article.id %}" class="button is-primary"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Go back</span> - </a> - </div> - </article> - {% else %} - {% if cannot_delete_children %} - <article class="message is-warning"> - <div class="message-body"> - <p> - You do not have permission to delete articles that have children. - </p> - - <a href="{% url 'wiki:get' path=urlpath.path article_id=article.id %}" class="button is-primary"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Go back</span> - </a> - </div> - </article> - {% endif %} - - {% if delete_children %} - <div class="message is-danger"> - <div class="message-body"> - <p> - You are deleting an article with children. If you delete this article, then its children will also be deleted. - </p> - <p> - If you choose to purge this article, note that its children will also be purged. - </p> - </div> - </div> - - <h2 class="title is-2">Child articles</h2> - - <ul> - {% for child in delete_children %} - <li><a href="{% url 'wiki:get' article_id=child.article.id %}" target="_blank">{{ child.article }}</a></li> - {% endfor %} - - {% if delete_children_more %} - <li><em>"...and more!"</em></li> - {% endif %} - </ul> - - {% endif %} - - {% if not cannot_delete_children %} - <p>Please confirm that you would like to delete this article.</p> - - <form method="POST"> - {% wiki_form delete_form %} - - <a href="{% url 'wiki:get' path=urlpath.path article_id=article.id %}" class="button is-white"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Go back</span> - </a> - <button type="submit" name="save_changes" class="button is-danger"> - <span class="icon"> - <i class="fas fa-trash"></i> - </span> - <span>Delete Article</span> - </button> - </form> - {% endif %} - {% endif %} -{% endblock %} diff --git a/pydis_site/templates/wiki/deleted.html b/pydis_site/templates/wiki/deleted.html deleted file mode 100644 index cdde2c47..00000000 --- a/pydis_site/templates/wiki/deleted.html +++ /dev/null @@ -1,62 +0,0 @@ -{% extends "wiki/base.html" %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}Article deleted{% endblock %} - -{% block wiki_contents %} - <article class="message is-warning"> - <div class="message-header"> - <p>Article deleted</p> - </div> - <div class="message-body"> - <p> - The article you were looking for has been deleted. - </p> - </div> - </article> - - {% if not article.current_revision.locked or article|can_delete:user %} - <h2 class="title is-2"> - Restore Article - </h2> - - <p> - To restore this article and any child articles, click the restore button below. - </p> - <a href="?restore=1" class="button is-primary"> - <span class="icon"> - <i class="fa fa-sync"></i> - </span> - <span>Restore</span> - </a> - {% endif %} - - {% if article|can_moderate:user %} - <h2 class="title is-2"> - Purge Article - </h2> - - <p> - To permanently remove this article and any child articles, click the purge button below. This will - allow you to free up the slugs assigned to these articles, so that they may be used for other - articles. - </p> - <p> - Please note: <span class="has-text-danger">This action cannot be undone</span>. - </p> - - <form method="POST" class="form form-inline"> - {% csrf_token %} - {% wiki_form purge_form %} - <button class="button is-danger" type="submit"> - <span class="icon"> - <i class="fas fa-trash"></i> - </span> - <span> - Purge - </span> - </button> - </form> - {% endif %} - -{% endblock %} diff --git a/pydis_site/templates/wiki/deleted_list.html b/pydis_site/templates/wiki/deleted_list.html deleted file mode 100644 index 1a8d203c..00000000 --- a/pydis_site/templates/wiki/deleted_list.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends "wiki/base.html" %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}Deleted Articles{% endblock %} - -{% block wiki_contents %} - <h1 class="title is-1">Deleted Articles</h1> - - {% if deleted_articles %} - <table class="table table-striped"> - <thead> - <tr> - <th>Page Title</th> - <th>Date Deleted</th> - <th>Restore Article</th> - </tr> - </thead> - - <tbody> - {% for article in deleted_articles %} - <tr> - <td> - <a href="{{article.get_absolute_url}}">{{ article }}</a> - </td> - <td> - {{article.modified}} - </td> - <td> - <a href="{% url 'wiki:deleted' article_id=article.id %}?restore=1" class="button is-primary"> - <span class="icon"> - <i class="fas fa-sync"></i> - </span> - <span>Restore</span> - </a> - </td> - </tr> - {% endfor %} - </tbody> - </table> - {% else %} - <p> - No deleted articles to display. - </p> - {% endif %} -{% endblock %} diff --git a/pydis_site/templates/wiki/dir.html b/pydis_site/templates/wiki/dir.html deleted file mode 100644 index 5a30de7b..00000000 --- a/pydis_site/templates/wiki/dir.html +++ /dev/null @@ -1,103 +0,0 @@ -{% extends "wiki/article.html" %} -{% load humanize %} -{% load i18n %} -{% load wiki_extra %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}Listing articles in {{ article.current_revision.title }}{% endblock %} - -{% block wiki_contents_tab %} - {% url 'wiki:dir' urlpath.path as self_url %} - - <form class="form-search directory-toolbar"> - <div class="is-pulled-right"> - {% render_field filter_form.query render_labels=False %} - </div> - - <div class="field is-grouped"> - <div class="field-body"> - <div class="control"> - {% if urlpath.parent %} - <a href="{% url 'wiki:dir' path=urlpath.parent.path %}" class="button"> - <span class="icon"> - <i class="fas fa-arrow-up"></i> - </span> - <span>Up one level</span> - </a> - {% endif %} - </div> - <div class="control"> - <a href="{% url 'wiki:create' path=urlpath.path %}" class="button is-primary"> - <span class="icon"> - <i class="fas fa-plus"></i> - </span> - <span>Add Article</span> - </a> - </div> - </div> - </div> - </form> - - <br /> - - <p> - {% with paginator.object_list.count as cnt %} - {% blocktrans with urlpath.path as path and cnt|pluralize:_("article,articles") as articles_plur and cnt|pluralize:_("is,are") as articles_plur_verb trimmed %} - Browsing <strong><a href="{{ self_url }}">/{{ path }}</a></strong>. There {{ articles_plur_verb }} <strong>{{ cnt }} {{ articles_plur }}</strong> in this level. - {% endblocktrans %} - {% endwith %} - </p> - - {% include "wiki/includes/pagination.html" %} - - <table class="table is-striped"> - <tr> - <th>Title</th> - <th>Slug</th> - <th>Last modified</th> - </tr> - - {% for urlpath in directory %} - <tr> - <td> - <a href="{% url 'wiki:get' path=urlpath.path %}">{{ urlpath.article.current_revision.title }}</a> - - <a href="{% url 'wiki:dir' path=urlpath.path %}" class="list-children"> - <span class="icon"> - <i class="fas fa-arrow-right"></i> - </span> - </a> - - {% if urlpath.article.current_revision.deleted %} - <span class="icon tooltip" data-tooltip="This article has been deleted"> - <i class="fas fa-trash"></i> - </span> - {% endif %} - - {% if urlpath.article.current_revision.locked %} - <span class="icon tooltip" data-tooltip="This article is locked"> - <i class="fas fa-lock"></i> - </span> - {% endif %} - </td> - - <td> - {{ urlpath.slug }} - </td> - - <td> - {{ urlpath.article.current_revision.created|naturaltime }} - </td> - </tr> - - {% empty %} - <tr> - <td colspan="3"> - <em>There are no articles at this level</em> - </td> - </tr> - {% endfor %} - </table> - - {% include "wiki/includes/pagination.html" %} -{% endblock %} diff --git a/pydis_site/templates/wiki/edit.html b/pydis_site/templates/wiki/edit.html deleted file mode 100644 index c378362a..00000000 --- a/pydis_site/templates/wiki/edit.html +++ /dev/null @@ -1,95 +0,0 @@ -{% extends "wiki/article.html" %} -{% load static %} -{% load wiki_tags %} - -{% block wiki_pagetitle %} - Edit: {{ article.current_revision.title }} -{% endblock %} - -{% block wiki_contents_tab %} - <div class="columns"> - <div class="column is-two-thirds"> - <form method="POST" class="form-horizontal" id="article_edit_form"> - {% with edit_form as form %} - {% include "wiki/includes/editor.html" %} - {% endwith %} - - <button class="button is-light" type="submit" name="preview" value="1" id="id_preview" - formaction="{% url 'wiki:preview' path=urlpath.path article_id=article.id %}" - formtarget="previewWindow" - > - <span class="icon"> - <i class="fas fa-eye"></i> - </span> - <span>Preview</span> - </button> - <button class="button is-primary" type="submit" name="save" value="1" id="id_save"> - <span class="icon"> - <i class="fas fa-save"></i> - </span> - <span>Save Changes</span> - </button> - - <div class="is-pulled-right"> - {% if user.is_authenticated and urlpath.path %} - <a href="{% url 'wiki:move' path=urlpath.path article_id=article.id %}" class="button is-warning "> - <span class="icon"> - <i class="fas fa-random"></i> - </span> - <span>Move Article</span> - </a> - {% endif %} - {% if article|can_delete:user %} - <a href="{% url 'wiki:delete' path=urlpath.path article_id=article.id %}" class="button is-danger"> - <span class="icon"> - <i class="fas fa-trash"></i> - </span> - <span>Delete Article</span> - </a> - {% endif %} - </div> - - <br /> - <br /> - </form> - </div> - - <div class="column is-one-third" id="wiki-edit-sidebar"> - {% include "wiki/includes/editor_sidebar.html" %} - </div> - </div> - - <div class="modal" id="previewModal"> - <div class="modal-background"></div> - <div class="modal-card" style="height: 80%; width: 80%;"> - <div class="modal-card-head"> - <p class="modal-card-title">Article Preview</p> - </div> - <div class="modal-card-body" style="padding: 0; overflow: hidden;"> - <iframe name="previewWindow" frameborder="0" style="width: 100%; height: 100%;"></iframe> - </div> - <div class="modal-card-foot"> - <button class="button is-light" aria-label="close"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Back</span> - </button> - <a class="button is-primary" id="id_preview_save_changes"> - <span class="icon"> - <i class="fas fa-save"></i> - </span> - <span>Save Changes</span> - </a> - </div> - </div> - </div> - - <script src="{% static "js/wiki/modal.js" %}" type="text/javascript"></script> - <script src="{% static "js/wiki/edit.js" %}" type="text/javascript"></script> - <script type="text/javascript"> - $(document).ready(function() { - $("#id_revision").val('{{ article.current_revision.id }}'); - }); - </script> -{% endblock %} diff --git a/pydis_site/templates/wiki/error.html b/pydis_site/templates/wiki/error.html deleted file mode 100644 index d7ee70fd..00000000 --- a/pydis_site/templates/wiki/error.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "wiki/base.html" %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}{% if article %}{{ article.current_revision.title }}{% else %}Error{% endif %}{% endblock %} - -{% block wiki_breadcrumbs %} - {% include "wiki/includes/breadcrumbs.html" %} -{% endblock %} - -{% block wiki_contents %} - {% if error_type == "ancestors_missing" %} - <h1 class="title">Not Found</h1> - - <article class="message is-primary"> - <div class="message-body"> - <p> - We were unable to find that page. If you think this was in error, please let us know! - </p> - <a href="{% url 'wiki:get' path='' %}" class="button is-primary"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Back to homepage</span> - </a> - </div> - </article> - {% else %} - <h1 class="title">Error</h1> - - <article class="message is-danger"> - <div class="message-body"> - <p> - {% if not error_msg %} - Unfortunately, an error occurred while retrieving that page. Please let us know! - {% else %} - {{ error_msg }} - {% endif %} - </p> - - {% if article %} - <a href="{% url 'wiki:get' path=urlpath.path article_id=article.id %}" class="button is-primary"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Back to {{ article.current_revision.title }}</span> - </a> - {% endif %} - </div> - </article> - {% endif %} -{% endblock %} diff --git a/pydis_site/templates/wiki/forms/fields/boolean.html b/pydis_site/templates/wiki/forms/fields/boolean.html deleted file mode 100644 index 9a8470be..00000000 --- a/pydis_site/templates/wiki/forms/fields/boolean.html +++ /dev/null @@ -1,49 +0,0 @@ -<div id="div_{{ field.auto_id }}" class="field is-horizontal{% if field.errors %} is-danger{% endif %}"> - {% if render_labels %} - <div class="field-label"> - <label class="label {% if field.errors %} is-danger{% endif %}"> - {{ field.label | safe }} {% if field.field.required %}<span class="asterisk has-text-danger">*</span>{% endif %} - </label> - - {% if field.help_text %} - <p id="hint_{{ field.auto_id }}" class="help has-text-grey"> - {{ field.help_text|safe }} - </p> - {% endif %} - </div> - {% endif %} - - <div class="field-body"> - <div class="field"> - <div class="control"> - <input class="switch is-rounded{% if field.errors %} is-danger{% endif %}" - type="checkbox" - id="{{ field.auto_id }}" - name="{{ field.name }}" - {% if field.value %}checked="checked"{% endif %} - - {% if field.required %}required{% endif %} - {% if field.max_length %}maxlength="{{ field.max_length }}"{% endif %} - {% if field.min_length %}minlength="{{ field.min_length }}"{% endif %} - {% if field.empty_value %}placeholder="{{ field.empty_value }}"{% endif %} - /> - - <label for="{{ field.auto_id }}" class="label {% if field.errors %} is-danger{% endif %}"> - {% if not render_labels %} - {{ field.label | safe }} {% if field.field.required %}<span class="asterisk has-text-danger">*</span>{% endif %} - {% else %} - - {% endif %} - </label> - </div> - - {% if field.errors %} - <p class="help is-danger"> - {% for error in field.errors %} - <span id="error_{{ forloop.counter }}_{{ field.auto_id }}">{{ error }}</span><br /> - {% endfor %} - </p> - {% endif %} - </div> - </div> -</div> diff --git a/pydis_site/templates/wiki/forms/fields/char.html b/pydis_site/templates/wiki/forms/fields/char.html deleted file mode 100644 index 06e9f1b7..00000000 --- a/pydis_site/templates/wiki/forms/fields/char.html +++ /dev/null @@ -1,57 +0,0 @@ -<div id="div_{{ field.auto_id }}" class="field is-horizontal{% if field.errors %} is-danger{% endif %}"> - {% if render_labels and not is_markitup %} - <div class="field-label"> - {% if field.label %} - <label for="{{ field.auto_id }}" class="label {% if field.errors %} is-danger{% endif %}"> - {{ field.label | safe }} {% if field.field.required %}<span class="asterisk has-text-danger">*</span>{% endif %} - </label> - {% endif %} - - {% if field.help_text %} - <p id="hint_{{ field.auto_id }}" class="help has-text-grey"> - {{ field.help_text|safe }} - </p> - {% endif %} - </div> - {% endif %} - - <div class="field-body"{% if is_markitup %} style="display: block; width: 100%;"{% endif %}> - <div class="field"> - <div class="control"> - {% if is_markitup %} - <textarea class="simple-mde textarea is-family-monospace{% if field.errors %} is-danger{% endif %}" - id="{{ field.auto_id }}" - name="{{ field.name }}" - - {% if field.required %}required{% endif %} - {% if field.max_length %}maxlength="{{ field.max_length }}"{% endif %} - {% if field.min_length %}minlength="{{ field.min_length }}"{% endif %} - {% if field.field.widget.attrs.placeholder %}placeholder="{{ field.field.widget.attrs.placeholderplaceholder }}"{% endif %} - {% if field.empty_value %}placeholder="{{ field.empty_value }}"{% endif %} - >{% if field.value %}{{ field.value }}{% endif %}</textarea> - {% else %} - <input class="input{% if field.errors %} is-danger{% endif %}" - type="text" - id="{{ field.auto_id }}" - name="{{ field.name }}" - - {% if field.required %}required{% endif %} - {% if field.max_length %}maxlength="{{ field.max_length }}"{% endif %} - {% if field.min_length %}minlength="{{ field.min_length }}"{% endif %} - {% if field.field.widget.attrs.placeholder %}placeholder="{{ field.field.widget.attrs.placeholder }}"{% endif %} - {% if field.empty_value %}placeholder="{{ field.empty_value }}"{% endif %} - {% if field.value %}value="{{ field.value }}"{% endif %} - /> - {% endif %} - </div> - - {% if field.errors %} - <p class="help is-danger"> - {% for error in field.errors %} - <span id="error_{{ forloop.counter }}_{{ field.auto_id }}">{{ error }}</span><br /> - {% endfor %} - </p> - {% endif %} - </div> - </div> -</div> diff --git a/pydis_site/templates/wiki/forms/fields/image.html b/pydis_site/templates/wiki/forms/fields/image.html deleted file mode 100644 index ce5402be..00000000 --- a/pydis_site/templates/wiki/forms/fields/image.html +++ /dev/null @@ -1,53 +0,0 @@ -<div id="div_{{ field.auto_id }}" class="field is-horizontal{% if field.errors %} is-danger{% endif %}"> - {% if render_labels %} - <div class="field-label"> - {% if field.label %} - <label for="{{ field.auto_id }}" class="label {% if field.errors %} is-danger{% endif %}"> - {{ field.label | safe }} {% if field.field.required %}<span class="asterisk has-text-danger">*</span>{% endif %} - </label> - {% endif %} - - {% if field.help_text %} - <p id="hint_{{ field.auto_id }}" class="help has-text-grey"> - {{ field.help_text|safe }} - </p> - {% endif %} - </div> - {% endif %} - - <div class="field-body"> - <div class="field"> - <div class="control"> - <div class="file has-name is-fullwidth"> - <label class="file-label"> - <input class="file-input" - type="file" - id="{{ field.auto_id }}" - name="{{ field.name }}" - accept="image/*" - - {% if field.required %}required{% endif %} - /> - <span class="file-cta"> - <span class="file-icon"> - <i class="fas fa-image"></i> - </span> - <span class="file-label"> - Choose - </span> - </span> - <span class="file-name"></span> - </label> - </div> - </div> - - {% if field.errors %} - <p class="help is-danger"> - {% for error in field.errors %} - <span id="error_{{ forloop.counter }}_{{ field.auto_id }}">{{ error }}</span><br /> - {% endfor %} - </p> - {% endif %} - </div> - </div> -</div> diff --git a/pydis_site/templates/wiki/forms/fields/in_place_render.html b/pydis_site/templates/wiki/forms/fields/in_place_render.html deleted file mode 100644 index 8591f547..00000000 --- a/pydis_site/templates/wiki/forms/fields/in_place_render.html +++ /dev/null @@ -1,33 +0,0 @@ -<div id="div_{{ field.auto_id }}" class="field is-horizontal{% if field.errors %} is-danger{% endif %}"> - {% if render_labels %} - <div class="field-label"> - {% if field.label %} - <label for="{{ field.auto_id }}" class="label {% if field.errors %} is-danger{% endif %}"> - IN PLACE RENDER: {{ field.label | safe }} {% if field.field.required %}<span class="asterisk has-text-danger">*</span>{% endif %} - </label> - {% endif %} - - {% if field.help_text %} - <p id="hint_{{ field.auto_id }}" class="help has-text-grey"> - {{ field.help_text|safe }} - </p> - {% endif %} - </div> - {% endif %} - - <div class="field-body"> - <div class="field"> - <div class="control"> - {{ field }} - </div> - - {% if field.errors %} - <p class="help is-danger"> - {% for error in field.errors %} - <span id="error_{{ forloop.counter }}_{{ field.auto_id }}">{{ error }}</span><br /> - {% endfor %} - </p> - {% endif %} - </div> - </div> -</div> diff --git a/pydis_site/templates/wiki/forms/fields/model_choice.html b/pydis_site/templates/wiki/forms/fields/model_choice.html deleted file mode 100644 index 58c55e04..00000000 --- a/pydis_site/templates/wiki/forms/fields/model_choice.html +++ /dev/null @@ -1,71 +0,0 @@ -{% load wiki_extra %} - -<div id="div_{{ field.auto_id }}" class="field is-horizontal{% if field.errors %} is-danger{% endif %}"> - {% if render_labels %} - <div class="field-label"> - {% if field.label %} - <label for="{{ field.auto_id }}" class="label {% if field.errors %} is-danger{% endif %}"> - {{ field.label | safe }} {% if field.field.required %}<span class="asterisk has-text-danger">*</span>{% endif %} - </label> - {% endif %} - - {% if field.help_text %} - <p id="hint_{{ field.auto_id }}" class="help has-text-grey"> - {{ field.help_text|safe }} - </p> - {% endif %} - </div> - {% endif %} - - <div class="field-body"> - <div class="field"> - <div class="control"> - <div class="select"> - {% get_field_options field %} - - <select {% if not options %}disabled{% endif %} - id="{{ field.auto_id }}" - name="{{ field.name }}" - - {% if field.required %}required{% endif %} - > - {% if options %} - {% for group_name, group_choices, group_index in options %} - {% if group_name %} - <optgroup label="{{ group_name }}"> - {% endif %} - {% for option in group_choices %} - <option {% if option.value == field.value %}selected{% endif %} - value="{{ option.value|stringformat:'s' }}" - > - {{ option.label }} - </option> - {% endfor %} - {% if group_name %} - </optgroup> - {% endif %} - {% endfor %} - {% else %} - <option value=""> - {% if field.field.empty_label %} - {{ field.field.empty_label }} - {% else %} - N/A - {% endif %} - </option> - {% endif %} - </select> - </div> - - </div> - - {% if field.errors %} - <p class="help is-danger"> - {% for error in field.errors %} - <span id="error_{{ forloop.counter }}_{{ field.auto_id }}">{{ error }}</span><br /> - {% endfor %} - </p> - {% endif %} - </div> - </div> -</div> diff --git a/pydis_site/templates/wiki/forms/fields/wiki_slug_render.html b/pydis_site/templates/wiki/forms/fields/wiki_slug_render.html deleted file mode 100644 index ff5c8528..00000000 --- a/pydis_site/templates/wiki/forms/fields/wiki_slug_render.html +++ /dev/null @@ -1,48 +0,0 @@ -{% load wiki_extra %} - -<div id="div_{{ field.auto_id }}" class="field is-horizontal{% if field.errors %} is-danger{% endif %}"> - {% if render_labels %} - <div class="field-label"> - {% if field.label %} - <label for="{{ field.auto_id }}" class="label {% if field.errors %} is-danger{% endif %}"> - {{ field.label | safe }} {% if field.field.required %}<span class="asterisk has-text-danger">*</span>{% endif %} - </label> - {% endif %} - - {% if field.help_text %} - <p id="hint_{{ field.auto_id }}" class="help has-text-grey"> - {{ field.help_text|safe }} - </p> - {% endif %} - </div> - {% endif %} - - <div class="field-body"> - <div class="field has-addons"> - <div class="control"> - <a class="button is-static">{{ field.form.urlpath_parent | render_urlpath }}</a> - </div> - <div class="control is-expanded"> - <input class="input{% if field.errors %} is-danger{% endif %}" - type="text" - id="{{ field.auto_id }}" - name="{{ field.name }}" - - {% if field.required %}required{% endif %} - {% if field.max_length %}maxlength="{{ field.max_length }}"{% endif %} - {% if field.min_length %}minlength="{{ field.min_length }}"{% endif %} - {% if field.empty_value %}placeholder="{{ field.empty_value }}"{% endif %} - {% if field.value %}value="{{ field.value }}"{% endif %} - > - </div> - - {% if field.errors %} - <p class="help is-danger"> - {% for error in field.errors %} - <span id="error_{{ forloop.counter }}_{{ field.auto_id }}">{{ error }}</span><br /> - {% endfor %} - </p> - {% endif %} - </div> - </div> -</div> diff --git a/pydis_site/templates/wiki/history.html b/pydis_site/templates/wiki/history.html deleted file mode 100644 index ee297bdd..00000000 --- a/pydis_site/templates/wiki/history.html +++ /dev/null @@ -1,126 +0,0 @@ -{% extends "wiki/article.html" %} -{% load sekizai_tags %} -{% load static %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}History: {{ article.current_revision.title }}{% endblock %} - -{% block wiki_contents_tab %} - {% include "wiki/includes/modals.html" %} - - {% addtoblock "js" %} - <script type="text/javascript" src="{% static "wiki/js/diffview.js" %}"></script> - <script type="text/javascript" src="{% static "wiki/js/diff.js" %}"></script> - {% endaddtoblock %} - - <p> - Click each revision to see a list of edited lines. Click the Preview - button to see how the article looked at this stage. At the bottom of - this page, you can change to a particular revision or merge an old - revision with the current one. - </p> - - {% include "wiki/includes/pagination.html" %} - - <form method="GET" id="historyForm"> - <table class="table is-striped"> - <thead> - <tr> - <th>Updated</th> - <th>Summary</th> - <th class="has-text-right">Actions</th> - </tr> - </thead> - <tbody> - {% for revision in revisions %} - <tr {% if revision == article.current_revision %}class="is-selected"{% endif %}> - <td> - {% include "wiki/includes/revision_info.html" with current_revision=article.current_revision %} - </td> - <td> - {% if revision.user_message %} - {{ revision.user_message }} - {% elif revision.automatic_log %} - {{ revision.automatic_log }} - {% else %} - <em>No summary</em> - {% endif %} - </td> - <td class="has-text-right"> - {% if revision == article.current_revision %} - <a class="button is-static has-text-grey"> - <span class="icon"> - <i class="fas fa-eye"></i> - </span> - <span>Preview</span> - </a> - <a class="button is-static has-text-grey"> - <span class="icon"> - <i class="fas fa-sync"></i> - </span> - <span>Switch</span> - </a> - {% else %} - <button type="submit" class="button" onclick="showPreviewModal('{{ revision.id }}', '{% url 'wiki:preview_revision' article.id %}', '{% url 'wiki:change_revision' path=urlpath.path article_id=article.id revision_id=revision.id %}'); event.preventDefault();"> - <span class="icon"> - <i class="fas fa-eye"></i> - </span> - <span>Preview</span> - </button> - <a class="button is-primary" href="{% url 'wiki:change_revision' path=urlpath.path article_id=article.id revision_id=revision.id %}"> - <span class="icon"> - <i class="fas fa-sync"></i> - </span> - <span>Switch</span> - </a> - {% endif %} - </td> - </tr> - {% endfor %} - </tbody> - </table> - - {% include "wiki/includes/pagination.html" %} - - <input type="hidden" name="r" value="" id="r" /> - - <div class="modal" id="previewModal"> - <div class="modal-background"></div> - <div class="modal-card" style="height: 80%; width: 80%;"> - <div class="modal-card-head"> - <p class="modal-card-title">Revision Preview</p> - </div> - <div class="modal-card-body" style="padding: 0; overflow: hidden;"> - <iframe name="previewWindow" id="previewWindow" frameborder="0" style="width: 100%; height: 100%;"></iframe> - </div> - <div class="modal-card-foot"> - <button class="button is-light" aria-label="close"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Back</span> - </button> - - {% if article|can_write:user %} - <a href="#" class="button is-primary switch-to-revision"> - <span class="icon"> - <i class="fas fa-sync"></i> - </span> - <span>Switch to this version</span> - </a> - {% else %} - <a class="button is-static"> - <span class="icon"> - <i class="fas fa-check"></i> - </span> - <span>Switch to this version</span> - </a> - {% endif %} - </div> - </div> - </div> - </form> - - <script src="{% static "js/wiki/modal.js" %}" type="text/javascript"></script> - <script src="{% static "js/wiki/history.js" %}" type="text/javascript"></script> -{% endblock %} diff --git a/pydis_site/templates/wiki/includes/article_menu.html b/pydis_site/templates/wiki/includes/article_menu.html deleted file mode 100644 index 966e8120..00000000 --- a/pydis_site/templates/wiki/includes/article_menu.html +++ /dev/null @@ -1,78 +0,0 @@ -{% load wiki_tags %} - -{% if article|can_write:user %} - {% with selected_tab as selected %} - <nav class="navbar is-light" role="navigation" aria-label="wiki navigation" id="wikiNavbar"> - <div class="container"> - <div class="navbar-brand"> - {% if not user.is_anonymous %} - <a class="navbar-item{% if selected == "settings" %} is-active{% endif %}" - href="{% url 'wiki:settings' article_id=article.id path=urlpath.path %}" - > - <span class="icon"> - <i class="fas fa-wrench"></i> - </span> - - <span class="is-hidden-mobile"> Settings</span> - </a> - {% endif %} - - {% for plugin in article_tabs %} - <a class="navbar-item{% if selected == plugin.slug %} is-active{% endif %}" - href="{% url 'wiki:plugin' slug=plugin.slug article_id=article.id path=urlpath.path %}" - > - <span class="icon"> - <i class="{{ plugin.article_tab.1 }}"></i> - </span> - - <span class="is-hidden-mobile"> {{ plugin.article_tab.0 }}</span> - </a> - {% endfor %} - - <a class="navbar-item{% if selected == "history" %} is-active{% endif %}" - href="{% url 'wiki:history' article_id=article.id path=urlpath.path %}" - > - <span class="icon"> - <i class="fas fa-clock"></i> - </span> - - <span class="is-hidden-mobile"> Changes</span> - </a> - - {% if article|can_write:user and not article.current_revision.locked %} - <a class="navbar-item{% if selected == "edit" %} is-active{% endif %}" - href="{% url 'wiki:edit' article_id=article.id path=urlpath.path %}" - > - <span class="icon"> - <i class="fas fa-edit"></i> - </span> - - <span class="is-hidden-mobile"> Edit</span> - </a> - {% else %} - <a class="navbar-item{% if selected == "source" %} is-active{% endif %}" - href="{% url 'wiki:source' article_id=article.id path=urlpath.path %}" - > - <span class="icon"> - <i class="fas fa-book-spells"></i> - </span> - - <span class="is-hidden-mobile"> View Source</span> - </a> - {% endif %} - - <a class="navbar-item{% if selected == "view" %} is-active{% endif %}" - href="{% url 'wiki:get' article_id=article.id path=urlpath.path %}" - > - <span class="icon"> - <i class="fas fa-book-open"></i> - </span> - - <span class="is-hidden-mobile"> View</span> - </a> - </div> - - </div> - </nav> - {% endwith %} -{% endif %} diff --git a/pydis_site/templates/wiki/includes/breadcrumbs.html b/pydis_site/templates/wiki/includes/breadcrumbs.html deleted file mode 100644 index 1b268e11..00000000 --- a/pydis_site/templates/wiki/includes/breadcrumbs.html +++ /dev/null @@ -1,95 +0,0 @@ -{% load wiki_tags %} - -{% if urlpath and article %} - <section class="breadcrumb-section section"> - <div class="container"> - <nav class="breadcrumb is-pulled-left" aria-label="breadcrumbs"> - <ul> - {% for ancestor in urlpath.cached_ancestors %} - {% if forloop.first and not article|can_write:user %} - {# Continue, we don't want to show the root element #} - {% else %} - <li> - <a href="{% url 'wiki:get' path=ancestor.path %}">{{ ancestor.article.current_revision.title }}</a> - </li> - {% endif %} - {% endfor %} - - <li class="is-active"> - <a href="{% url 'wiki:get' path=article.path %}">{{ article.current_revision.title }}</a> - </li> - </ul> - </nav> - - {% if article|can_write:user %} - <div class="dropdown is-pulled-right is-right"> - <div class="dropdown-trigger"> - <a aria-haspopup="true" aria-controls="sub-article-dropdown"> - <span>Sub-Articles</span> - <span class="icon"> - <i class="fas fa-angle-down"></i> - </span> - </a> - </div> - <div class="dropdown-menu" id="sub-article-dropdown" role="menu"> - <div class="dropdown-content"> - {% if children_slice %} - {% for child in children_slice %} - <a class="dropdown-item" href="{% url 'wiki:get' path=child.path %}"> - {{ child.article.current_revision.title }} - </a> - {% endfor %} - - {% if children_slice_more %} - <a class="dropdown-item" href="{% url 'wiki:dir' path=urlpath.path %}"> - ...and more. - </a> - {% endif %} - - <hr class="dropdown-divider"> - {% endif %} - - <a class="dropdown-item" href="{% url 'wiki:dir' path=urlpath.path %}"> - Browse other articles - </a> - </div> - </div> - </div> - - {% if request.user.is_authenticated %} - <div class="dropdown is-pulled-right is-right"> - <div class="dropdown-trigger"> - <a aria-haspopup="true" aria-controls="sub-article-dropdown"> - <span>Create Article</span> - <span class="icon"> - <i class="fas fa-angle-down"></i> - </span> - - </a> - </div> - <div class="dropdown-menu" id="sub-article-dropdown" role="menu"> - <div class="dropdown-content"> - {% if urlpath.parent %} - <a class="dropdown-item" href="{% url 'wiki:create' path=urlpath.parent.path %}"> - <span class="icon"> - <i class="fas fa-arrow-right"></i> - </span> - <span>At current level</span> - </a> - {% endif %} - - <a class="dropdown-item" href="{% url 'wiki:create' path=urlpath.path %}"> - <span class="icon"> - <i class="fas fa-arrow-down"></i> - </span> - <span>Below current level</span> - </a> - </div> - </div> - </div> - {% endif %} - {% endif %} - - </div> - </section> -{% endif %} diff --git a/pydis_site/templates/wiki/includes/editor.html b/pydis_site/templates/wiki/includes/editor.html deleted file mode 100644 index 6eb6cd45..00000000 --- a/pydis_site/templates/wiki/includes/editor.html +++ /dev/null @@ -1,4 +0,0 @@ -{% load wiki_tags %} -{% include "wiki/includes/editormedia.html" %} - -{% wiki_form form %} diff --git a/pydis_site/templates/wiki/includes/editor_sidebar.html b/pydis_site/templates/wiki/includes/editor_sidebar.html deleted file mode 100644 index 45ac87a1..00000000 --- a/pydis_site/templates/wiki/includes/editor_sidebar.html +++ /dev/null @@ -1,38 +0,0 @@ -{% load static %} - -<section class="accordions"> - {% for plugin, plugin_form in sidebar %} - <article class="accordion is-primary"> - <div class="accordion-header toggle"> - <p> - {% if plugin.sidebar.icon_class %} - <span class="icon"> - {% if plugin.sidebar.icon_class == "fa-picture-o" %} - <i class="fas fa-images"></i> - {% else %} - <i class="fas {{ plugin.sidebar.icon_class }}"></i> - {% endif %} - </span> - {% endif %} - - {{ plugin.sidebar.headline }} - </p> - - </div> - <div class="accordion-body"> - <div class="accordion-content"> - {% if plugin.sidebar.template %} - {% with plugin_form as form %} - <form method="POST" class="form-horizontal sidebar-form" action="?f={{ plugin_form.form_id }}" enctype="multipart/form-data"> - {% csrf_token %} - {% include plugin.sidebar.template %} - </form> - {% endwith %} - {% endif %} - </div> - </div> - </article> - {% endfor %} -</section> - -<script type="application/javascript" src="{% static "js/wiki/editor_sidebar.js" %}"></script> diff --git a/pydis_site/templates/wiki/includes/editormedia.html b/pydis_site/templates/wiki/includes/editormedia.html deleted file mode 100644 index c10fbef8..00000000 --- a/pydis_site/templates/wiki/includes/editormedia.html +++ /dev/null @@ -1,17 +0,0 @@ -{% load sekizai_tags %} -{% load static %} - -{% addtoblock "js" %} - <script type="text/javascript" src="{% static "wiki/js/editor.js" %}"></script> - {% for js in editor.Media.js %} - <script type="text/javascript" src="{% static js %}"></script> - {% endfor %} -{% endaddtoblock %} - -{% addtoblock "css" %} - {% for media, srcs in editor.Media.css.items %} - {% for src in srcs %} - <link rel="stylesheet" media="{{ media }}" href="{% static src %}" /> - {% endfor %} - {% endfor %} -{% endaddtoblock %} diff --git a/pydis_site/templates/wiki/includes/form.html b/pydis_site/templates/wiki/includes/form.html deleted file mode 100644 index 4ea08de4..00000000 --- a/pydis_site/templates/wiki/includes/form.html +++ /dev/null @@ -1,16 +0,0 @@ -{% load sekizai_tags %} -{% csrf_token %} - -{% include "wiki/includes/formerrors.html" %} - -{% addtoblock "js" %} - {{ form.media.js }} -{% endaddtoblock %} - -{% addtoblock "css" %} - {{ form.media.css }} -{% endaddtoblock %} - -{% for field in form %} - {% include "wiki/includes/formfield.html" %} -{% endfor %} diff --git a/pydis_site/templates/wiki/includes/formerrors.html b/pydis_site/templates/wiki/includes/formerrors.html deleted file mode 100644 index c6df2637..00000000 --- a/pydis_site/templates/wiki/includes/formerrors.html +++ /dev/null @@ -1,15 +0,0 @@ -{% if form.non_field_errors %} - <article class="message is-danger"> - {% if form_error_title %} - <div class="message-header"> - <p>{{ form_error_title }}</p> - </div> - {% endif %} - - <div class="message-body"> - {% for error_message in form.non_field_errors %} - <p>{{ error_message }}</p> - {% endfor %} - </div> - </article> -{% endif %} diff --git a/pydis_site/templates/wiki/includes/formfield.html b/pydis_site/templates/wiki/includes/formfield.html deleted file mode 100644 index 8c42cfbf..00000000 --- a/pydis_site/templates/wiki/includes/formfield.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load wiki_extra %} - -{% if field.is_hidden %} - {{ field }} -{% else %} - {% render_field field render_labels %} -{% endif %} diff --git a/pydis_site/templates/wiki/includes/pagination.html b/pydis_site/templates/wiki/includes/pagination.html deleted file mode 100644 index 35dac538..00000000 --- a/pydis_site/templates/wiki/includes/pagination.html +++ /dev/null @@ -1,27 +0,0 @@ -{% load i18n %} - -{% if is_paginated %} - <nav class="pagination" role="navigation" aria-label="pagination"> - {% if page_obj.has_previous %} - <a class="pagination-previous" href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ page_obj.previous_page_number }}{% if appended_key %}&{{ appended_key }}={{ appended_value }}{% endif %}">Previous</a> - {% else %} - <a class="pagination-previous tooltip" data-tooltip="This is the first page" disabled>Previous</a> - {% endif %} - - {% if page_obj.has_next %} - <a class="pagination-next" href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ page_obj.next_page_number }}{% if appended_key %}&{{ appended_key }}={{ appended_value }}{% endif %}">Next</a> - {% else %} - <a class="pagination-next tooltip" data-tooltip="This is the last page" disabled>Older</a> - {% endif %} - - <ul class="pagination-list"> - {% for pc in paginator.page_range %} - {% if pc == 0 %} - <li><span class="pagination-ellipsis">…</span></li> - {% else %} - <li><a class="pagination-link{% if pc == page_obj.number %} is-current{% endif %}" href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ pc }}{% if appended_key %}&{{ appended_key }}={{ appended_value }}{% endif %}">{{ pc }}</a></li> - {% endif %} - {% endfor %} - </ul> - </nav> -{% endif %} diff --git a/pydis_site/templates/wiki/includes/render.html b/pydis_site/templates/wiki/includes/render.html deleted file mode 100644 index c0334d98..00000000 --- a/pydis_site/templates/wiki/includes/render.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load sekizai_tags %} -{% load static %} - -{% addtoblock "js" %} - <script type="text/javascript" src="{% static "wiki/js/article.js" %}"></script> -{% endaddtoblock %} - -<div class="wiki-article"> - {{ content|default:"" }} -</div> - -{% for plugin in plugins %} - {% if plugin.RenderMedia.css %} - {% addtoblock "css" %} - {% for media, url in plugin.RenderMedia.css.items %} - <link rel="stylesheet" href="{% static url %}" /> - {% endfor %} - {% endaddtoblock %} - {% endif %} - - {% if plugin.RenderMedia.js %} - {% addtoblock "js" %} - {% for url in plugin.RenderMedia.js %} - <script type="text/javascript" src="{% static url %}"></script> - {% endfor %} - {% endaddtoblock %} - {% endif %} -{% endfor %} diff --git a/pydis_site/templates/wiki/includes/revision_info.html b/pydis_site/templates/wiki/includes/revision_info.html deleted file mode 100644 index f2964034..00000000 --- a/pydis_site/templates/wiki/includes/revision_info.html +++ /dev/null @@ -1,24 +0,0 @@ -{% comment %} - This reusable code is shared between different templates and different inheritors of - BaseRevision. -{% endcomment %} - -{% load wiki_tags %} - -{% if not hidedate %}{{ revision.created }}{% endif %} {% if not hidenumber %}(#{{ revision.revision_number }}) by{% endif %} {% if revision.user %}{{ revision.user }}{% else %}{% if article|can_moderate:user %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}anonymous (IP logged){% endif %}{% endif %} - -{% if revision.deleted %} - <span class="tag is-danger">deleted</span> -{% endif %} - -{% if revision.previous_revision.deleted and not revision.deleted %} - <span class="tag is-success">restored</span> -{% endif %} - -{% if revision.locked %} - <span class="tag is-danger">locked</span> -{% endif %} - -{% if revision.previous_revision.locked and not revision.locked %} - <span class="tag is-success">unlocked</span> -{% endif %} diff --git a/pydis_site/templates/wiki/includes/searchresult.html b/pydis_site/templates/wiki/includes/searchresult.html deleted file mode 100644 index 897ee4a6..00000000 --- a/pydis_site/templates/wiki/includes/searchresult.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load humanize %} - -<tr> - <td> - {% for urlpath in article.urlpath_set.all %} - <a href="{% url 'wiki:get' path=urlpath.path %}"> - {{ article.current_revision.title }} - <br /> - <small class="has-text-grey">Slug: /{{ urlpath.path }}</small> - </a> - {% empty %} - <a href="{% url 'wiki:get' article_id=article.id %}"> - {{ article.current_revision.title }} - </a> - {% endfor %} - - {% if article.current_revision.deleted %} - <span class="icon"> - <i class="fas fa-trash"></i> - </span> - {% endif %} - - {% if article.current_revision.locked %} - <span class="icon"> - <i class="fas fa-lock"></i> - </span> - {% endif %} - </td> - - <td class="has-text-right"> - {{ article.current_revision.created|naturaltime }} - </td> -</tr> diff --git a/pydis_site/templates/wiki/move.html b/pydis_site/templates/wiki/move.html deleted file mode 100644 index a3f7a5d8..00000000 --- a/pydis_site/templates/wiki/move.html +++ /dev/null @@ -1,72 +0,0 @@ -{% extends "wiki/article.html" %} -{% load i18n %} -{% load sekizai_tags %} -{% load static %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}Move: {{ article.current_revision.title }}{% endblock %} - -{% block wiki_contents_tab %} - <article class="message is-warning"> - <div class="message-body"> - {% if urlpath.get_descendants %} - {% blocktrans count cnt=urlpath.get_descendants.count trimmed %} - <p> - Please note that this article has {{ cnt }} child article. If you - decide to move this article, then links to any child articles will - not be updated. - </p> - {% plural %} - <p> - Please note that this article has {{ cnt }} child articles. If you - decide to move this article, then links to any child articles will - not be updated. - </p> - {% endblocktrans %} - {% endif %} - <p> - Remember: Any links to this article will not be automatically updated. You - may leave behind a redirect page by specifying that option below, but these - are temporary - so it's better to update the links directly. - </p> - </div> - </article> - - <form method="POST" class="form-horizontal" id="article_move_form"> - {% wiki_form form %} - - {# Not gonna lie, I have no idea what this is or what it's for #} - <div class="btn-group" id="dest_selector"> - <a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#"> - <span class="dest_selector_title"></span> - <span class="caret"></span> - </a> - - <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> - {% with current_path=root_path %}{% include "wiki/includes/move_tree.html" %}{% endwith %} - </ul> - </div> - - <a href="{% url 'wiki:get' path=urlpath.path article_id=article.id %}" class="button is-white"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Go back</span> - </a> - - <button class="button is-warning" type="submit" name="move" id="id_move"> - <span class="icon"> - <i class="fas fa-random"></i> - </span> - <span>Move article</span> - </button> - </form> - - {% addtoblock "js" %} - <script type="text/javascript" src="{% static "js/wiki/move.js" %}"></script> - <script type="text/javascript"> - $('#id_slug').val('{{ urlpath.slug }}'); - select_path('{{urlpath.parent.pk}}', '{{urlpath.parent}}'); - </script> - {% endaddtoblock %} -{% endblock %} diff --git a/pydis_site/templates/wiki/permission_denied.html b/pydis_site/templates/wiki/permission_denied.html deleted file mode 100644 index 58394c8f..00000000 --- a/pydis_site/templates/wiki/permission_denied.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "wiki/base.html" %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}Permission Denied{% endblock %} - -{% block wiki_contents %} - <article class="message is-danger"> - <div class="message-header"> - <p>Permission Denied</p> - </div> - - <div class="message-body"> - <p> - Sorry, you don't have permission to access this page. - </p> - {% if article.current_revision.locked %} - <p>This article is locked for editing.</p> - {% endif %} - {% if not read_denied %} - <a href="{% url 'wiki:get' article_id=article.id path=urlpath.path %}" class="button is-primary"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Back to article</span> - </a> - {% elif urlpath.parent %} - <a href="{% url 'wiki:get' path=urlpath.parent.path %}" class="button is-primary"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Back to article</span> - </a> - {% endif %} - </div> - </article> -{% endblock %} diff --git a/pydis_site/templates/wiki/plugins/images/index.html b/pydis_site/templates/wiki/plugins/images/index.html deleted file mode 100644 index a76703aa..00000000 --- a/pydis_site/templates/wiki/plugins/images/index.html +++ /dev/null @@ -1,171 +0,0 @@ -{% extends "wiki/article.html" %} -{% load humanize %} -{% load wiki_tags %} -{% load wiki_thumbnails %} - -{# TODO: This page needs re-styling, but it's functional for now so we're not touching it until after wiki completion #} - -{% block wiki_pagetitle %}Images: {{ article.current_revision.title }}{% endblock %} - -{% block wiki_contents_tab %} - <p>The following images are available for this article. Copy the markdown tag to directly refer to an image from the article text.</p> - - <p> - <a href="{% url 'wiki:edit' path=urlpath.path article_id=article.id %}" class="button is-white"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Back to edit page</span> - </a> - </p> - - {% include "wiki/includes/pagination.html" %} - - <table class="table is-fullwidth"> - {% for image in images %} - {% with image.current_revision.imagerevision as revision %} - <thead> - <tr> - <th>{{ revision.get_filename|default:_("No filename") }}</th> - <th>Tag</th> - <th colspan="3">Updated</th> - <th>Size</th> - </tr> - </thead> - - <tbody> - <tr> - <td rowspan="3"> - <figure class="image is-128x128"> - {% thumbnail revision.image "128x128" as thumb %} - <a href="{{ revision.image.url }}"> - <img src="{{ thumb.url }}" alt="{{ revision.get_filename }}" title="{{ revision.get_filename }}" /> - </a> - {% endthumbnail %} - </figure> - - {% if image|can_write:user %} - {% if revision.deleted %} - <a class="button is-success" title="Upload and publish new image" href="{% url 'wiki:images_add_revision' path=urlpath.path article_id=article.id image_id=image.id %}"> - <span class="icon"> - <i class="fas fa-upload"></i> - </span> - <span>Upload</span> - </a> - <a class="button is-primary" title="Restore deleted image" href="{% url 'wiki:images_restore' path=urlpath.path article_id=article.id image_id=image.id %}"> - <span class="icon"> - <i class="fas fa-sync"></i> - </span> - <span>Restore</span> - </a> - {% else %} - <a class="button is-success" title="Upload replacement image" href="{% url 'wiki:images_add_revision' path=urlpath.path article_id=article.id image_id=image.id %}"> - <span class="icon"> - <i class="fas fa-upload"></i> - </span> - <span>Upload</span> - </a> - <a class="button is-warning" title="Remove image" href="{% url 'wiki:images_delete' path=urlpath.path article_id=article.id image_id=image.id %}"> - <span class="icon"> - <i class="fas fa-times"></i> - </span> - <span>Remove</span> - </a> - {% endif %} - {% if article|can_moderate:user %} - <a class="button is-danger" href="{% url 'wiki:images_purge' path=urlpath.path article_id=article.id image_id=image.id %}"> - <span class="icon"> - <i class="fas fa-trash"></i> - </span> - <span>Delete</span> - </a> - {% endif %} - {% endif %} - </td> - - <td> - <code>[image:{{ image.id }}]</code> - </td> - - <td colspan="3"> - {% include "wiki/includes/revision_info.html" %} - </td> - - <td> - {{ revision.get_size|filesizeformat }}<br />{{ revision.width }}x{{ revision.height }} pixels - </td> - </tr> - - <tr> - <th colspan="5"> - History - </th> - </tr> - - <tr> - <td class="is-paddingless" colspan="5"> - <table class="table is-striped"> - <thead> - <tr> - <th></th> - <th>Updated</th> - <th>Size</th> - <th>Dimensions</th> - <th></th> - </tr> - </thead> - <tbody> - {% for old_revision in image.revision_set.all %} - <tr {% if forloop.first %}class="is-selected"{% endif %}> - <td> - <figure class="image"> - {% thumbnail old_revision.imagerevision.image "50x50" crop="center" as thumb %} - <a href="{{ old_revision.imagerevision.image.url }}"> - <img src="{{ thumb.url }}" alt="{{ revision.imagerevision.get_filename }}" /> - </a> - {% endthumbnail %} - </figure> - </td> - - <td> - {% include "wiki/includes/revision_info.html" with current_revision=image.current_revision revision=old_revision %} - </td> - - <td> - {{ old_revision.imagerevision.get_size|filesizeformat }} - </td> - - <td> - {{ old_revision.imagerevision.width }}x{{ old_revision.imagerevision.height }} pixels - </td> - - <td> - {% if image|can_write:user and old_revision != image.current_revision %} - <a class="button is-primary" href="{% url 'wiki:images_set_revision' path=urlpath.path article_id=article.id image_id=image.id rev_id=old_revision.id %}"> - <span class="icon"> - <i class="fas fa-sync"></i> - </span> - <span>Restore</span> - </a> - {% else %} - <a class="button is-static has-text-grey"> - <span class="icon"> - <i class="fas fa-times"></i> - </span> - <span>Current</span> - </a> - {% endif %} - </td> - </tr> - {% endfor %} - </tbody> - </table> - </td> - </tr> - </tbody> - {% endwith %} - {% endfor %} - </table> - - {% include "wiki/includes/pagination.html" %} -{% endblock %} diff --git a/pydis_site/templates/wiki/plugins/images/purge.html b/pydis_site/templates/wiki/plugins/images/purge.html deleted file mode 100644 index 3b514e4c..00000000 --- a/pydis_site/templates/wiki/plugins/images/purge.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "wiki/article.html" %} -{% load wiki_tags %} -{% load wiki_thumbnails %} - -{% block wiki_pagetitle %}Purge image: {{ image }}{% endblock %} - -{% block wiki_contents_tab %} - <div class="columns"> - <div class="column is-two-thirds"> - <p> - Purge image: Completely remove image file and all revisions - </p> - <form method="POST" class="form-horizontal" enctype="multipart/form-data"> - {% wiki_form form %} - - <a href="{% url 'wiki:images_index' path=urlpath.path article_id=article.id %}" class="button is-white"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Go Back</span> - </a> - - <button class="button is-danger" type="submit"> - <span class="icon"> - <i class="fas fa-trash"></i> - </span> - <span>Permanently remove</span> - </button> - </form> - </div> - - <div class="column is-one-third"> - {% thumbnail image.current_revision.imagerevision.image "250x250" as thumb %} - <figure class="image"> - <a href="{{ image.current_revision.imagerevision.image.url }}"> - <img src="{{ thumb.url }}" alt="{{ revision.get_filename }}" title="{{ revision.get_filename }}" /> - </a> - </figure> - {% endthumbnail %} - </div> - </div> -{% endblock %} diff --git a/pydis_site/templates/wiki/plugins/images/render.html b/pydis_site/templates/wiki/plugins/images/render.html deleted file mode 100644 index d14b3de4..00000000 --- a/pydis_site/templates/wiki/plugins/images/render.html +++ /dev/null @@ -1,25 +0,0 @@ -{% load wiki_thumbnails %}{% comment %} - This template is used for the markdown extension that renders images and captions. - - NB! Watch out for line breaks, markdown might add <br />s and <p>s. -{% endcomment %}{% with image.current_revision.imagerevision as revision %}{% spaceless %} -<figure - class="image{% if align %} is-pulled-{{ align }}{% endif %}" - {% if width %}style="width: {{ width }}px;" {% endif %} -> - <a href="{{ revision.image.url }}"> - {% if size %} - {% thumbnail revision.image size upscale=False as thumb %} - <img src="{{ thumb.url }}" alt="{{ revision.get_filename }}" /> - {% empty %} - <figcaption> - <em>Image not found</em> - </figcaption> - {% endthumbnail %} - {% else %} - <img src="{{ revision.image.url }}" alt="{{ revision.get_filename }}" /> - {% endif %} - </a> - <figcaption>{{ caption|safe }}</figcaption> -</figure> -{% endspaceless %}{% endwith %} diff --git a/pydis_site/templates/wiki/plugins/images/revision_add.html b/pydis_site/templates/wiki/plugins/images/revision_add.html deleted file mode 100644 index eb872eab..00000000 --- a/pydis_site/templates/wiki/plugins/images/revision_add.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "wiki/article.html" %} -{% load wiki_tags %} -{% load wiki_thumbnails %} - -{% block wiki_pagetitle %}Replace Image: {{ image }}{% endblock %} - -{% block wiki_contents_tab %} - <div class="columns"> - <div class="column is-two-thirds"> - <p> - Upload an image to replace the current one. - </p> - - <form method="POST" class="form-horizontal" enctype="multipart/form-data"> - {% wiki_form form %} - - <a href="{% url 'wiki:images_index' path=urlpath.path article_id=article.id %}" class="button is-white"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Go Back</span> - </a> - - <button class="button is-primary" type="submit"> - <span class="icon"> - <i class="fas fa-upload"></i> - </span> - <span>Upload</span> - </button> - </form> - </div> - - <div class="column is-one-third"> - {% thumbnail image.current_revision.imagerevision.image "250x250" as thumb %} - <figure class="image"> - <a href="{{ image.current_revision.imagerevision.image.url }}"> - <img src="{{ thumb.url }}" alt="{{ revision.get_filename }}" title="{{ revision.get_filename }}" /> - </a> - </figure> - {% endthumbnail %} - </div> - </div> -{% endblock %} diff --git a/pydis_site/templates/wiki/plugins/images/sidebar.html b/pydis_site/templates/wiki/plugins/images/sidebar.html deleted file mode 100644 index b29ef240..00000000 --- a/pydis_site/templates/wiki/plugins/images/sidebar.html +++ /dev/null @@ -1,206 +0,0 @@ -{% load static %} -{% load wiki_images_tags %} -{% load wiki_tags %} -{% load wiki_thumbnails %} - -{% with article|images_for_article as images %} - {% if article|images_can_add:user %} - {% include "wiki/includes/formerrors.html" %} - - {# Include the hidden fields #} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - - {% for field in form.visible_fields %} - {% include "wiki/includes/formfield.html" with render_labels=False %} - {% endfor %} - - <button type="button" onClick="add_image(this.form)" name="{{ plugin.slug }}_save" value="1" class="button is-primary is-fullwidth"> - <span class="icon"> - <i class="fas fa-upload"></i> - </span> - <span>Upload</span> - </button> - - <br /> - {% endif %} - - {% for image in images %} - {% with image.current_revision.imagerevision as revision %} - {% thumbnail revision.image "100x100" crop="center" as thumb %} - <div class="columns"> - <div class="column is-half"> - <img src="{{ thumb.url }}" alt="{{ revision.get_filename }}" title="{{ revision.get_filename }}" /> - </div> - <div class="column is-half has-text-right"> - <div class="tags is-right"> - <span class="tag is-dark is-medium">Image ID: {{ image.id }}</span> - </div> - - <p> - <a class="button is-primary" title="Insert image" href="javascript:void(insert_image({{ image.id }}))"> - <span class="icon"> - <i class="fa fa-paste"></i> - </span> - </a> - - {% if image|can_write:user %} - <a class="button is-primary" title="Replace" href="{% url 'wiki:images_add_revision' path=urlpath.path article_id=article.id image_id=image.id %}"> - <span class="icon"> - <i class="fas fa-upload"></i> - </span> - </a> - {% endif %} - </p> - </div> - </div> - {% endthumbnail %} - {% endwith %} - - {% empty %} - <div class="has-text-centered"> - No images found for this article. - </div> - <br /> - {% endfor %} - - <p> - <a class="button is-primary is-fullwidth" href="{% url 'wiki:images_index' path=urlpath.path article_id=article.id %}"> - <span>Manage Images</span> - <span class="icon"> - <i class="fas fa-arrow-right"></i> - </span> - </a> - </p> - - <h4 class="title is-4">How to use images</h4> - - <p> - Images are local to the article, and may only be used in the article they are - uploaded to. Images may be replaced by clicking the upload button next to it - above, but note that image revisions are kept and can be found on the - <a href="{% url 'wiki:images_index' path=urlpath.path article_id=article.id %}">Manage Images page</a>. - </p> - - <p> - To make use of images in an article, use the <code>image</code> wiki tag in - your Markdown. These tags take some arguments for customisation, and you can - also include a caption on the next line, indented by four spaces. Note that - the <code>align</code> and <code>size</code> options are optional. - </p> - <p> - Syntax: <code>[image:ID align:x size:y]</code> - </p> - <p> - The ID to use is the image ID shown next to the image in the list above. - You can click on the insert button if you'd like to insert an image into the - editor without manually typing the tag. - </p> - - <h5 class="title is-5">Example tag</h5> - - <pre> - [image:1 align:left size:orig] - Python Discord logo - </pre> - - <h5 class="title is-5">Options for align</h5> - - <div class="tags"> - <span class="tag is-primary">left</span> - <span class="tag is-primary">right</span> - </div> - - <h5 class="title is-5">Options for size</h5> - - <div class="tags"> - <span class="tag is-primary">small</span> - <span class="tag is-primary">medium</span> - <span class="tag is-primary">large</span> - <span class="tag is-primary">orig</span> - <span class="tag is-primary">default</span> - </div> -{% endwith %} - -<div class="modal" id="imgModal"> - <div class="modal-background"></div> - <div class="modal-card" style="height: 80%; width: 80%;"> - <div class="modal-card-head"> - <p class="modal-card-title">Insert Image</p> - </div> - <div class="modal-card-body" style="padding: 0; overflow: hidden;"> - <section class="section"> - <input type="hidden" value="0" id="img_id" /> - <div class="field is-horizontal"> - <div class="field-label"> - <label for="img_align">Image alignment</label> - </div> - <div class="field-body"> - <div class="control has-icons-left is-fullwidth"> - <div class="select is-fullwidth"> - <select id="img_align"> - <option selected value="">Default</option> - <option value="left">Left</option> - <option value="right">Right</option> - </select> - </div> - - <span class="icon is-left"> - <i class="fas fa-align-center"></i> - </span> - </div> - </div> - </div> - - <div class="field is-horizontal"> - <div class="field-label"> - <label for="img_size">Image size</label> - </div> - <div class="field-body"> - <div class="control has-icons-left is-fullwidth"> - <div class="select is-fullwidth"> - <select id="img_size"> - <option selected value="">Default</option> - <option value="orig">Original</option> - - <option value="small">Small</option> - <option value="medium">Medium</option> - <option value="large">Large</option> - </select> - </div> - <span class="icon is-left"> - <i class="fas fa-expand-arrows"></i> - </span> - </div> - </div> - </div> - - <div class="field is-horizontal"> - <div class="field-label"> - <label for="img_caption">Image caption</label> - </div> - <div class="field-body"> - <input class="input" type="text" placeholder="Enter Caption" id="img_caption" /> - </div> - </div> - </section> - </div> - <div class="modal-card-foot"> - <button class="button is-light" aria-label="close"> - <span class="icon"> - <i class="fas fa-arrow-left"></i> - </span> - <span>Back</span> - </button> - <button class="button is-primary" id="id_image_insert"> - <span class="icon"> - <i class="fas fa-paste"></i> - </span> - <span>Insert</span> - </button> - </div> - </div> -</div> - -<script type="text/javascript" src="{% static "js/wiki/image_sidebar.js" %}"></script> diff --git a/pydis_site/templates/wiki/plugins/links/sidebar.html b/pydis_site/templates/wiki/plugins/links/sidebar.html deleted file mode 100644 index 4fdbac72..00000000 --- a/pydis_site/templates/wiki/plugins/links/sidebar.html +++ /dev/null @@ -1,56 +0,0 @@ -{% load sekizai_tags %} -{% load static %} - -<h4 class="title is-4">Link to another wiki page</h4> - -<p> - Type in something from another wiki page's title and auto-complete will help you create a tag for your wiki link. Tags for links look like this:<br /> -</p> - -<pre>[Title of link](wiki:ArticleSlug)</pre> - -{# We do this to prevent accidental form submission - this isn't _really_ a form #} -<input type="submit" disabled style="display: none" aria-hidden="true" /> - -<div class="field"> - <div class="control has-icons-left"> - <div class="dropdown"> - <div class="dropdown-trigger"> - <input class="input" type="text" id="page_title_input" value="" placeholder="Search: 3+ chars" aria-haspopup="true" aria-controls="page_title_menu"> - </div> - <div class="dropdown-menu" id="page_title_menu" role="menu"></div> - </div> - - <span class="icon is-small is-left"> - <i class="fas fa-search"></i> - </span> - </div> -</div> - -<div class="is-divider"></div> - -<h4 class="title is-4">An external link</h4> - -<p> - You can link to another website simply by inserting an address example.com or http://example.com or by using the markdown syntax:<br /> -</p> - -<pre>[Clickable text](http://example.com)</pre> - -{% addtoblock "js" %} - {% comment %} - So, for whatever reason, bulmahead doesn't have a LICENSE file. There is one in - the package.json, but that isn't a standard most projects adhere to - so I've - declined to include it within the project directly. - - The package.json states MIT - but there is no prose or license - text available for the project itself. - {% endcomment %} - - <script src="https://cdn.rawgit.com/mattmezza/bulmahead/master/dist/bulmahead.bundle.js"></script> - <script src="{% static "js/wiki/links_sidebar.js" %}"></script> - - <script type="text/javascript"> - setFetchURL("{% url 'wiki:links_query_urlpath' path=urlpath.path article_id=article.id %}"); - </script> -{% endaddtoblock %} diff --git a/pydis_site/templates/wiki/preview_inline.html b/pydis_site/templates/wiki/preview_inline.html deleted file mode 100644 index a01c963a..00000000 --- a/pydis_site/templates/wiki/preview_inline.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "wiki/base.html" %} -{% load sekizai_tags %} -{% load static %} -{% load wiki_tags %} - -{# We make these empty so they don't appear in the preview #} -{% block site_navbar %}{% endblock %} -{% block site_footer %}{% endblock %} -{% block wiki_breadcrumbs %}{% endblock %} - -{% block wiki_body %} - {% if revision %} - <div class="container"> - <div class="message"> - <div class="message-header"> - <p>Previewing Revision</p> - </div> - <div class="message-body"> - {% include "wiki/includes/revision_info.html" %} - </div> - </div> - </div> - {% endif %} - - {% if merge %} - <div class="alert alert-info"> - <strong>Previewing merge between:</strong> - {% include "wiki/includes/revision_info.html" with revision=merge1 %} - <strong>and</strong> - {% include "wiki/includes/revision_info.html" with revision=merge2 %} - </div> - - {% if merge1.deleted %} - <div class="alert alert-danger"> - <strong>You cannot merge with a deleted revision</strong> - </div> - {% endif %} - {% endif %} - - {% if revision and revision.deleted %} - <div class="warning"> - <strong>This revision has been deleted.</strong> - <p>Restoring to this revision will mark the article as deleted.</p> - </div> - {% else %} - <section class="section"> - <div class="container"> - <div class="content"> - <h1 class="title">{{ article.current_revision.title }}</h1> - - {% wiki_render article content %} - </div> - </div> - </section> - - {% for plugin in plugins %} - {% if plugin.RenderMedia.css %} - {% addtoblock "css" %} - {% for media, url in plugin.RenderMedia.css.items %} - <link rel="stylesheet" href="{% static url %}" /> - {% endfor %} - {% endaddtoblock %} - {% endif %} - {% if plugin.RenderMedia.js %} - {% addtoblock "js" %} - {% for url in plugin.RenderMedia.js %} - <script type="text/javascript" src="{% static url %}"></script> - {% endfor %} - {% endaddtoblock %} - {% endif %} - {% endfor %} - {% endif %} -{% endblock %} diff --git a/pydis_site/templates/wiki/root_missing.html b/pydis_site/templates/wiki/root_missing.html deleted file mode 100644 index 31ef2eab..00000000 --- a/pydis_site/templates/wiki/root_missing.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "wiki/create_root.html" %} -{% load wiki_tags %} - -{% block wiki_contents %} - <div class="columns"> - <div class="column is-two-thirds is-offset-2"> - <article class="message is-primary"> - <div class="message-header"> - <p>No articles found</p> - </div> - <div class="message-body"> - <p> - It appears that there are no articles available on this wiki. - </p> - - {% if not user.is_superuser %} - <p> - To get started, please login with a superuser account. - </p> - - {% login_url as wiki_login_url %} - - {% if wiki_login_url %} - <div class="has-text-centered"> - <a href="{{ wiki_login_url }}" class="button is-primary">Login</a> - </div> - {% endif %} - {% else %} - <p> - Let's get started - click below to create the first article, or to read up on the - <code>django-wiki</code> documentation. - </p> - - <a class="button is-primary" href="{% url 'wiki:root_create' %}">Create Article</a> - <a class="button is-info" href="http://django-wiki.readthedocs.org/">Documentation</a> - {% endif %} - </div> - </article> - </div> - </div> -{% endblock %} diff --git a/pydis_site/templates/wiki/search.html b/pydis_site/templates/wiki/search.html deleted file mode 100644 index 154b6934..00000000 --- a/pydis_site/templates/wiki/search.html +++ /dev/null @@ -1,64 +0,0 @@ -{% extends "wiki/base.html" %} -{% load i18n %} -{% load wiki_tags %} - - -{% block wiki_pagetitle %}Search results for: "{{ search_query }}"{% endblock %} - -{% block wiki_contents %} - <h1 class="page-header">Search results for: "{{ search_query }}"</h1> - - <form class="form-search directory-toolbar"> - <p> - {% if urlpath %} - Searching: <strong>in {{ urlpath.article }}</strong> - {% else %} - Searching: <strong>everywhere</strong> - {% endif %} - </p> - - <div class="field has-addons"> - <div class="control has-icons-left is-expanded"> - <input type="search" class="input" name="q" value="{{ search_query }}" /> - <span class="icon is-left"> - <i class="fas fa-search"></i> - </span> - </div> - <div class="control"> - <button class="button is-primary" type="submit"> - Search - </button> - </div> - </div> - - <p> - {% blocktrans with paginator.object_list.count as cnt %} - Your search returned <strong>{{ cnt }}</strong> results. - {% endblocktrans %} - </p> - </form> - - {% include "wiki/includes/pagination.html" %} - - <table class="table is-striped"> - <tr> - <th>Title</th> - <th class="has-text-right">Last modified</th> - </tr> - - {% for article in articles %} - {% block wiki_search_loop %} - {% include "wiki/includes/searchresult.html" %} - {% endblock %} - - {% empty %} - <tr> - <td colspan="2"> - <em>No articles were found for that search query.</em> - </td> - </tr> - {% endfor %} - </table> - - {% include "wiki/includes/pagination.html" %} -{% endblock %} diff --git a/pydis_site/templates/wiki/settings.html b/pydis_site/templates/wiki/settings.html deleted file mode 100644 index e291621d..00000000 --- a/pydis_site/templates/wiki/settings.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "wiki/article.html" %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}Settings: {{ article.current_revision.title }}{% endblock %} - -{% block wiki_contents_tab %} - {% for form in forms %} - <form method="POST" class="form-horizontal" action="?f={{form.action}}"> - <h3 class="page-header">{{ form.settings_form_headline }}</h3> - - {% wiki_form form %} - - <div class="field is-horizontal"> - <div class="field-label"> - {# Empty for spacing #} - </div> - <div class="field-body"> - <div class="control"> - <button type="submit" class="button is-primary" name="save"> - <span class="icon"> - <i class="fas fa-save"></i> - </span> - <span>Save Changes</span> - </button> - </div> - </div> - </div> - </form> - {% endfor %} -{% endblock %} diff --git a/pydis_site/templates/wiki/source.html b/pydis_site/templates/wiki/source.html deleted file mode 100644 index a6611233..00000000 --- a/pydis_site/templates/wiki/source.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "wiki/article.html" %} -{% load wiki_tags %} - -{% block wiki_pagetitle %}Source of "{{ article.current_revision.title }}"{% endblock %} - -{% block wiki_contents_tab %} - {% if article.current_revision.locked %} - <p>This article is currently locked for editing.</p> - {% endif %} - - <pre class="is-fullwidth"> -{{ article.current_revision.content }} - </pre> -{% endblock %} diff --git a/pydis_site/tests/test_utils_account.py b/pydis_site/tests/test_utils_account.py deleted file mode 100644 index 6f8338b4..00000000 --- a/pydis_site/tests/test_utils_account.py +++ /dev/null @@ -1,139 +0,0 @@ -from unittest.mock import patch - -from allauth.exceptions import ImmediateHttpResponse -from allauth.socialaccount.models import SocialAccount, SocialLogin -from django.contrib.auth.models import User -from django.contrib.messages.storage.base import BaseStorage -from django.http import HttpRequest -from django.test import RequestFactory, TestCase - -from pydis_site.apps.api.models import Role, User as DiscordUser -from pydis_site.utils.account import AccountAdapter, SocialAccountAdapter - - -class AccountUtilsTests(TestCase): - def setUp(self): - # Create the user - self.django_user = User.objects.create(username="user") - - # Create the roles - developers_role = Role.objects.create( - id=1, - name="Developers", - colour=0, - permissions=0, - position=1 - ) - everyone_role = Role.objects.create( - id=0, - name="@everyone", - colour=0, - permissions=0, - position=0 - ) - - # Create the social accounts - self.discord_account = SocialAccount.objects.create( - user=self.django_user, provider="discord", uid=0 - ) - self.discord_account_one_role = SocialAccount.objects.create( - user=self.django_user, provider="discord", uid=1 - ) - self.discord_account_two_roles = SocialAccount.objects.create( - user=self.django_user, provider="discord", uid=2 - ) - self.discord_account_not_present = SocialAccount.objects.create( - user=self.django_user, provider="discord", uid=3 - ) - self.github_account = SocialAccount.objects.create( - user=self.django_user, provider="github", uid=0 - ) - - # Create DiscordUsers - self.discord_user = DiscordUser.objects.create( - id=0, - name="user", - discriminator=0 - ) - - self.discord_user_role = DiscordUser.objects.create( - id=1, - name="user present", - discriminator=0, - roles=[everyone_role.id] - ) - - self.discord_user_two_roles = DiscordUser.objects.create( - id=2, - name="user with both roles", - discriminator=0, - roles=[everyone_role.id, developers_role.id] - ) - - self.request_factory = RequestFactory() - - def test_account_adapter(self): - """Test that our Allauth account adapter functions correctly.""" - adapter = AccountAdapter() - - self.assertFalse(adapter.is_open_for_signup(HttpRequest())) - - def test_social_account_adapter_signup(self): - """Test that our Allauth social account adapter correctly handles signups.""" - adapter = SocialAccountAdapter() - - discord_login = SocialLogin(account=self.discord_account) - discord_login_role = SocialLogin(account=self.discord_account_one_role) - discord_login_not_present = SocialLogin(account=self.discord_account_not_present) - discord_login_two_roles = SocialLogin(account=self.discord_account_two_roles) - - github_login = SocialLogin(account=self.github_account) - - messages_request = self.request_factory.get("/") - messages_request._messages = BaseStorage(messages_request) - - with patch("pydis_site.utils.account.reverse") as mock_reverse: - with patch("pydis_site.utils.account.redirect") as mock_redirect: - with self.assertRaises(ImmediateHttpResponse): - adapter.is_open_for_signup(messages_request, github_login) - - with self.assertRaises(ImmediateHttpResponse): - adapter.is_open_for_signup(messages_request, discord_login) - - with self.assertRaises(ImmediateHttpResponse): - adapter.is_open_for_signup(messages_request, discord_login_role) - - with self.assertRaises(ImmediateHttpResponse): - adapter.is_open_for_signup(messages_request, discord_login_not_present) - - self.assertTrue( - adapter.is_open_for_signup(messages_request, discord_login_two_roles) - ) - - self.assertEqual(len(messages_request._messages._queued_messages), 4) - self.assertEqual(mock_redirect.call_count, 4) - self.assertEqual(mock_reverse.call_count, 4) - - def test_social_account_adapter_populate(self): - """Test that our Allauth social account adapter correctly handles data population.""" - adapter = SocialAccountAdapter() - - discord_login = SocialLogin( - account=self.discord_account, - user=self.django_user - ) - discord_login.account.extra_data["discriminator"] = "0000" - - discord_user = adapter.populate_user( - self.request_factory.get("/"), discord_login, - {"username": "user"} - ) - self.assertEqual(discord_user.username, "user#0000") - self.assertEqual(discord_user.first_name, "user#0000") - - discord_login.account.provider = "not_discord" - not_discord_user = adapter.populate_user( - self.request_factory.get("/"), discord_login, - {"username": "user"} - ) - self.assertEqual(not_discord_user.username, "user") diff --git a/pydis_site/utils/account.py b/pydis_site/utils/account.py deleted file mode 100644 index b4e41198..00000000 --- a/pydis_site/utils/account.py +++ /dev/null @@ -1,79 +0,0 @@ -from typing import Any, Dict - -from allauth.account.adapter import DefaultAccountAdapter -from allauth.exceptions import ImmediateHttpResponse -from allauth.socialaccount.adapter import DefaultSocialAccountAdapter -from allauth.socialaccount.models import SocialLogin -from django.contrib.auth.models import User as DjangoUser -from django.contrib.messages import ERROR, add_message -from django.http import HttpRequest -from django.shortcuts import redirect -from django.urls import reverse - -from pydis_site.apps.api.models import User as DiscordUser - -ERROR_CONNECT_DISCORD = ("You must login with Discord before connecting another account. " - "Your account details have not been saved.") -ERROR_JOIN_DISCORD = ("Please join the Discord server and verify that you accept the rules and " - "privacy policy.") - - -class AccountAdapter(DefaultAccountAdapter): - """An Allauth account adapter that prevents signups via form submission.""" - - def is_open_for_signup(self, request: HttpRequest) -> bool: - """ - Checks whether or not the site is open for signups. - - We override this to always return False so that users may never sign up using - Allauth's signup form endpoints, to be on the safe side - since we only want users - to sign up using their Discord account. - """ - return False - - -class SocialAccountAdapter(DefaultSocialAccountAdapter): - """An Allauth SocialAccount adapter that prevents signups via non-Discord connections.""" - - def is_open_for_signup(self, request: HttpRequest, social_login: SocialLogin) -> bool: - """ - Checks whether or not the site is open for signups. - - We override this method in order to prevent users from creating a new account using - a non-Discord connection, as we require this connection for our users. - """ - if social_login.account.provider != "discord": - add_message(request, ERROR, ERROR_CONNECT_DISCORD) - - raise ImmediateHttpResponse(redirect(reverse("home"))) - - try: - user = DiscordUser.objects.get(id=int(social_login.account.uid)) - except DiscordUser.DoesNotExist: - add_message(request, ERROR, ERROR_JOIN_DISCORD) - - raise ImmediateHttpResponse(redirect(reverse("home"))) - - if len(user.roles) <= 1: - add_message(request, ERROR, ERROR_JOIN_DISCORD) - - raise ImmediateHttpResponse(redirect(reverse("home"))) - - return True - - def populate_user(self, request: HttpRequest, - social_login: SocialLogin, - data: Dict[str, Any]) -> DjangoUser: - """ - Method used to populate a Django User with data. - - We override this so that the Django user is created with the username#discriminator, - instead of just the username, as Django users must have unique usernames. For display - purposes, we also set the `name` key, which is used for `first_name` in the database. - """ - if social_login.account.provider == "discord": - discriminator = social_login.account.extra_data["discriminator"] - data["username"] = f"{data['username']}#{discriminator:0>4}" - data["name"] = data["username"] - - return super().populate_user(request, social_login, data) diff --git a/pydis_site/utils/views.py b/pydis_site/utils/views.py deleted file mode 100644 index c9803bd6..00000000 --- a/pydis_site/utils/views.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.contrib import messages -from django.http import HttpRequest -from django.views.generic import RedirectView - - -class MessageRedirectView(RedirectView): - """ - Redirects to another URL, also setting a message using the Django Messages framework. - - This is based on Django's own `RedirectView` and works the same way, but takes two additional - parameters. - - * `message`: Set to the message content you wish to display. - * `message_level`: Set to one of the message levels from the Django messages framework. This - parameter defaults to `messages.INFO`. - """ - - message: str = "" - message_level: int = messages.INFO - - def get(self, request: HttpRequest, *args, **kwargs) -> None: - """Called upon a GET request.""" - messages.add_message(request, self.message_level, self.message) - - return super().get(request, *args, **kwargs) |