From 6a309c4238971ff7a1c7b5ec35936a057b912cbb Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Sep 2020 10:49:32 +0300 Subject: Create CSS file for all Guides app pages --- pydis_site/static/css/guides/guide.css | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 pydis_site/static/css/guides/guide.css (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/guides/guide.css b/pydis_site/static/css/guides/guide.css new file mode 100644 index 00000000..fa7a0ba5 --- /dev/null +++ b/pydis_site/static/css/guides/guide.css @@ -0,0 +1,7 @@ +.breadcrumb-section { + padding: 1rem; +} + +i.has-icon-padding { + padding: 0 10px 25px 0; +} -- cgit v1.2.3 From cae8eb732bce6fc879f853eebf43f4f7553349b6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 22 Sep 2020 20:48:58 +0300 Subject: Create CSS for resources index --- pydis_site/static/css/resources/resources.css | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 pydis_site/static/css/resources/resources.css (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css new file mode 100644 index 00000000..025b28c6 --- /dev/null +++ b/pydis_site/static/css/resources/resources.css @@ -0,0 +1,33 @@ +.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%); +} + +.breadcrumb-section { + padding: 1rem; +} -- cgit v1.2.3 From e26897e3f7c0d0af3950e1d1d60fd3800fc13683 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 4 Oct 2020 15:33:44 +0200 Subject: Remove wiki templates and static. --- LICENSE-SimpleMDE | 21 --- pydis_site/static/css/pygments/darcula.css | 91 --------- pydis_site/static/css/pygments/friendly.css | 71 ------- pydis_site/static/css/pygments/monokai.css | 72 ------- pydis_site/static/css/wiki/simplemde-fixes.css | 7 - pydis_site/static/css/wiki/simplemde.min.css | 7 - pydis_site/static/css/wiki/style.css | 89 --------- .../images/wiki/contributing/fork_button.png | Bin 30391 -> 0 bytes .../static/images/wiki/contributing/fork_url.png | Bin 17151 -> 0 bytes .../static/images/wiki/contributing/fork_user.png | Bin 10574 -> 0 bytes .../images/wiki/contributing/pycharm_branch.png | Bin 33963 -> 0 bytes .../images/wiki/contributing/pycharm_checkout.png | Bin 37692 -> 0 bytes .../images/wiki/contributing/pycharm_commit.png | Bin 178593 -> 0 bytes .../wiki/contributing/pycharm_commit_button.png | Bin 6049 -> 0 bytes .../wiki/contributing/pycharm_interpreter.png | Bin 52028 -> 0 bytes .../images/wiki/contributing/pycharm_pipenv.png | Bin 35997 -> 0 bytes .../wiki/contributing/pycharm_pipenv_success.png | Bin 119232 -> 0 bytes .../images/wiki/contributing/pycharm_pull.png | Bin 33441 -> 0 bytes .../images/wiki/contributing/pycharm_push.png | Bin 143689 -> 0 bytes .../images/wiki/contributing/pycharm_remotes.png | Bin 142273 -> 0 bytes .../wiki/contributing/pycharm_test_clone.png | Bin 16919 -> 0 bytes .../images/wiki/contributing/pycharm_upstream.png | Bin 14863 -> 0 bytes pydis_site/static/js/wiki/create.js | 13 -- pydis_site/static/js/wiki/dropdown.js | 35 ---- pydis_site/static/js/wiki/edit.js | 58 ------ pydis_site/static/js/wiki/editor_sidebar.js | 1 - pydis_site/static/js/wiki/history.js | 12 -- pydis_site/static/js/wiki/image_sidebar.js | 32 ---- pydis_site/static/js/wiki/links_sidebar.js | 30 --- pydis_site/static/js/wiki/load_editor.js | 96 ---------- pydis_site/static/js/wiki/modal.js | 14 -- pydis_site/static/js/wiki/move.js | 8 - pydis_site/static/js/wiki/simplemde.min.js | 15 -- pydis_site/templates/wiki/article.html | 24 --- pydis_site/templates/wiki/base.html | 44 ----- pydis_site/templates/wiki/create.html | 42 ----- pydis_site/templates/wiki/create_root.html | 48 ----- pydis_site/templates/wiki/delete.html | 90 --------- pydis_site/templates/wiki/deleted.html | 62 ------- pydis_site/templates/wiki/deleted_list.html | 45 ----- pydis_site/templates/wiki/dir.html | 103 ----------- pydis_site/templates/wiki/edit.html | 95 ---------- pydis_site/templates/wiki/error.html | 51 ----- .../templates/wiki/forms/fields/boolean.html | 49 ----- pydis_site/templates/wiki/forms/fields/char.html | 57 ------ pydis_site/templates/wiki/forms/fields/image.html | 53 ------ .../wiki/forms/fields/in_place_render.html | 33 ---- .../templates/wiki/forms/fields/model_choice.html | 71 ------- .../wiki/forms/fields/wiki_slug_render.html | 48 ----- pydis_site/templates/wiki/history.html | 126 ------------- .../templates/wiki/includes/article_menu.html | 78 -------- .../templates/wiki/includes/breadcrumbs.html | 95 ---------- pydis_site/templates/wiki/includes/editor.html | 4 - .../templates/wiki/includes/editor_sidebar.html | 38 ---- .../templates/wiki/includes/editormedia.html | 17 -- pydis_site/templates/wiki/includes/form.html | 16 -- pydis_site/templates/wiki/includes/formerrors.html | 15 -- pydis_site/templates/wiki/includes/formfield.html | 7 - pydis_site/templates/wiki/includes/messages.html | 0 pydis_site/templates/wiki/includes/pagination.html | 27 --- pydis_site/templates/wiki/includes/render.html | 28 --- .../templates/wiki/includes/revision_info.html | 24 --- .../templates/wiki/includes/searchresult.html | 33 ---- pydis_site/templates/wiki/move.html | 72 ------- pydis_site/templates/wiki/permission_denied.html | 36 ---- .../templates/wiki/plugins/images/index.html | 171 ----------------- .../templates/wiki/plugins/images/purge.html | 42 ----- .../templates/wiki/plugins/images/render.html | 25 --- .../wiki/plugins/images/revision_add.html | 43 ----- .../templates/wiki/plugins/images/sidebar.html | 206 --------------------- .../templates/wiki/plugins/links/sidebar.html | 56 ------ pydis_site/templates/wiki/preview_inline.html | 73 -------- pydis_site/templates/wiki/root_missing.html | 41 ---- pydis_site/templates/wiki/search.html | 64 ------- pydis_site/templates/wiki/settings.html | 30 --- pydis_site/templates/wiki/source.html | 14 -- 76 files changed, 2968 deletions(-) delete mode 100644 LICENSE-SimpleMDE delete mode 100644 pydis_site/static/css/pygments/darcula.css delete mode 100644 pydis_site/static/css/pygments/friendly.css delete mode 100644 pydis_site/static/css/pygments/monokai.css delete mode 100644 pydis_site/static/css/wiki/simplemde-fixes.css delete mode 100644 pydis_site/static/css/wiki/simplemde.min.css delete mode 100644 pydis_site/static/css/wiki/style.css delete mode 100644 pydis_site/static/images/wiki/contributing/fork_button.png delete mode 100644 pydis_site/static/images/wiki/contributing/fork_url.png delete mode 100644 pydis_site/static/images/wiki/contributing/fork_user.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_branch.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_checkout.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_commit.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_commit_button.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_interpreter.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_pipenv.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_pipenv_success.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_pull.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_push.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_remotes.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_test_clone.png delete mode 100644 pydis_site/static/images/wiki/contributing/pycharm_upstream.png delete mode 100644 pydis_site/static/js/wiki/create.js delete mode 100644 pydis_site/static/js/wiki/dropdown.js delete mode 100644 pydis_site/static/js/wiki/edit.js delete mode 100644 pydis_site/static/js/wiki/editor_sidebar.js delete mode 100644 pydis_site/static/js/wiki/history.js delete mode 100644 pydis_site/static/js/wiki/image_sidebar.js delete mode 100644 pydis_site/static/js/wiki/links_sidebar.js delete mode 100644 pydis_site/static/js/wiki/load_editor.js delete mode 100644 pydis_site/static/js/wiki/modal.js delete mode 100644 pydis_site/static/js/wiki/move.js delete mode 100644 pydis_site/static/js/wiki/simplemde.min.js delete mode 100644 pydis_site/templates/wiki/article.html delete mode 100644 pydis_site/templates/wiki/base.html delete mode 100644 pydis_site/templates/wiki/create.html delete mode 100644 pydis_site/templates/wiki/create_root.html delete mode 100644 pydis_site/templates/wiki/delete.html delete mode 100644 pydis_site/templates/wiki/deleted.html delete mode 100644 pydis_site/templates/wiki/deleted_list.html delete mode 100644 pydis_site/templates/wiki/dir.html delete mode 100644 pydis_site/templates/wiki/edit.html delete mode 100644 pydis_site/templates/wiki/error.html delete mode 100644 pydis_site/templates/wiki/forms/fields/boolean.html delete mode 100644 pydis_site/templates/wiki/forms/fields/char.html delete mode 100644 pydis_site/templates/wiki/forms/fields/image.html delete mode 100644 pydis_site/templates/wiki/forms/fields/in_place_render.html delete mode 100644 pydis_site/templates/wiki/forms/fields/model_choice.html delete mode 100644 pydis_site/templates/wiki/forms/fields/wiki_slug_render.html delete mode 100644 pydis_site/templates/wiki/history.html delete mode 100644 pydis_site/templates/wiki/includes/article_menu.html delete mode 100644 pydis_site/templates/wiki/includes/breadcrumbs.html delete mode 100644 pydis_site/templates/wiki/includes/editor.html delete mode 100644 pydis_site/templates/wiki/includes/editor_sidebar.html delete mode 100644 pydis_site/templates/wiki/includes/editormedia.html delete mode 100644 pydis_site/templates/wiki/includes/form.html delete mode 100644 pydis_site/templates/wiki/includes/formerrors.html delete mode 100644 pydis_site/templates/wiki/includes/formfield.html delete mode 100644 pydis_site/templates/wiki/includes/messages.html delete mode 100644 pydis_site/templates/wiki/includes/pagination.html delete mode 100644 pydis_site/templates/wiki/includes/render.html delete mode 100644 pydis_site/templates/wiki/includes/revision_info.html delete mode 100644 pydis_site/templates/wiki/includes/searchresult.html delete mode 100644 pydis_site/templates/wiki/move.html delete mode 100644 pydis_site/templates/wiki/permission_denied.html delete mode 100644 pydis_site/templates/wiki/plugins/images/index.html delete mode 100644 pydis_site/templates/wiki/plugins/images/purge.html delete mode 100644 pydis_site/templates/wiki/plugins/images/render.html delete mode 100644 pydis_site/templates/wiki/plugins/images/revision_add.html delete mode 100644 pydis_site/templates/wiki/plugins/images/sidebar.html delete mode 100644 pydis_site/templates/wiki/plugins/links/sidebar.html delete mode 100644 pydis_site/templates/wiki/preview_inline.html delete mode 100644 pydis_site/templates/wiki/root_missing.html delete mode 100644 pydis_site/templates/wiki/search.html delete mode 100644 pydis_site/templates/wiki/settings.html delete mode 100644 pydis_site/templates/wiki/source.html (limited to 'pydis_site/static/css') 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. 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 - * @author Chris Bracco - * @author Zeno Rocha - */ - -.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/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/wiki/contributing/fork_button.png b/pydis_site/static/images/wiki/contributing/fork_button.png deleted file mode 100644 index 955c93dd..00000000 Binary files a/pydis_site/static/images/wiki/contributing/fork_button.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/fork_url.png b/pydis_site/static/images/wiki/contributing/fork_url.png deleted file mode 100644 index 57a8e421..00000000 Binary files a/pydis_site/static/images/wiki/contributing/fork_url.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/fork_user.png b/pydis_site/static/images/wiki/contributing/fork_user.png deleted file mode 100644 index b8317cab..00000000 Binary files a/pydis_site/static/images/wiki/contributing/fork_user.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_branch.png b/pydis_site/static/images/wiki/contributing/pycharm_branch.png deleted file mode 100644 index d9cde97a..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_branch.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_checkout.png b/pydis_site/static/images/wiki/contributing/pycharm_checkout.png deleted file mode 100644 index d679a467..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_checkout.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_commit.png b/pydis_site/static/images/wiki/contributing/pycharm_commit.png deleted file mode 100644 index 03223688..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_commit.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_commit_button.png b/pydis_site/static/images/wiki/contributing/pycharm_commit_button.png deleted file mode 100644 index ae2745b0..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_commit_button.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_interpreter.png b/pydis_site/static/images/wiki/contributing/pycharm_interpreter.png deleted file mode 100644 index cb757f84..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_interpreter.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_pipenv.png b/pydis_site/static/images/wiki/contributing/pycharm_pipenv.png deleted file mode 100644 index feb9b1d7..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_pipenv.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_pipenv_success.png b/pydis_site/static/images/wiki/contributing/pycharm_pipenv_success.png deleted file mode 100644 index 03fa3244..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_pipenv_success.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_pull.png b/pydis_site/static/images/wiki/contributing/pycharm_pull.png deleted file mode 100644 index f82725fa..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_pull.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_push.png b/pydis_site/static/images/wiki/contributing/pycharm_push.png deleted file mode 100644 index a21a18b1..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_push.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_remotes.png b/pydis_site/static/images/wiki/contributing/pycharm_remotes.png deleted file mode 100644 index b2e4239c..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_remotes.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_test_clone.png b/pydis_site/static/images/wiki/contributing/pycharm_test_clone.png deleted file mode 100644 index 6410b847..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_test_clone.png and /dev/null differ diff --git a/pydis_site/static/images/wiki/contributing/pycharm_upstream.png b/pydis_site/static/images/wiki/contributing/pycharm_upstream.png deleted file mode 100644 index 70c60ea2..00000000 Binary files a/pydis_site/static/images/wiki/contributing/pycharm_upstream.png and /dev/null differ 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 @@ -// 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;at;++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?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=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;oe)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>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;nt;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+=" ... ")),""},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;++o0&&(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=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=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;++oe&&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-tn&&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=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")>=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)=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;n1)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=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.line0&&(n.line-u.from.linebo&&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;fbo&&(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;at.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.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.viewFroma&&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;rbo){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=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;ibo&&(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;l1)if(Fo&&Fo.text.join("\n")==t){if(r.ranges.length%Fo.text.length==0){s=[];for(var c=0;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-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;ri?c.map:u[i],a=0;ai?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=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.linen?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=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.display.viewTo||l.to().linet&&(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.bottoms.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.top0?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)){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&&hn?(_e(e,e.options.workDelay),!0):void 0}),i.length&&At(e,function(){for(var t=0;ta;--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&&o2&&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;rn)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=n.lineN&&tt?(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(;lu;u++){for(;l&&zi(t.line.text.charAt(o.coverStart+l));)--l;for(;o.coverStart+sbo&&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;un.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:ag)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=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.maxScrollLefto;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&&nt)&&(i.updateLineNumbers=t),e.curOp.viewChanged=!0,t>=i.viewTo)Wo&&br(e.doc,t)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=i.lineN&&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;rt)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.viewFromn&&(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;r400}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=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.linev.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=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;sa.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;fh?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;rbo&&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=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=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;re.lastLine())){if(t.from.lineo&&(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=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+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;a0||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=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=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.from0&&l)for(var f=0;ff;++f)p.push(m);p.push(s)}return p}function ar(e){for(var t=0;t0)){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=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;io;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.pose.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(;cc;){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;nc&&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;bp||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=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;mn;++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;tt||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;rt){e=i;continue e}t-=o,n+=i.chunkSize()}return n}while(!e.lines);for(var r=0;rt)break;t-=l}return n+r}function ri(e){e=yr(e);for(var t=0,n=e.parent,r=0;r1&&!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-1&&(Ii(l)[f]=u[f],delete u[f])}}}return i}function vi(e,t,n,r){n0?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;a0}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-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;o0;--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;r2&&!(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;ot||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;rt)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&&l0==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=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.linet.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=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;nn&&(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]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;oi&&(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;a0&&l(n.charAt(r-1));)--r;for(;i.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.line0)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=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){return this.post},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.lastColumnPos0?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;oe.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;in;++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;re;++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;ne){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=e){if(i.insertInner(e,t,n),i.lines&&i.lines.length>50){for(var a=i.lines.length%25+25,l=a;l10);e.parent.maybeSpill()}},iterN:function(e,t,n){for(var r=0;re){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=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.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=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;re?(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.linet&&(t=e.from),null!=e.to&&e.tol||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()")>-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.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-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")):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&&/$/,blockCommentStart:"",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<>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<>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<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,"'")}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;rAn error occured:

"+l(u.message+"",!0)+"
";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:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\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",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/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 ?/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])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^|^<\/?\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]+?(?=[\\?(?:\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&&/^/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?'
'+(n?e:l(e,!0))+"\n
\n":"
"+(n?e:l(e,!0))+"\n
"},o.prototype.blockquote=function(e){return"
\n"+e+"
\n"},o.prototype.html=function(e){return e},o.prototype.heading=function(e,t,n){return"'+e+"\n"},o.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"},o.prototype.list=function(e,t){var n=t?"ol":"ul";return"<"+n+">\n"+e+"\n"},o.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},o.prototype.paragraph=function(e){return"

    "+e+"

    \n"},o.prototype.table=function(e,t){return"\n\n"+e+"\n\n"+t+"\n
    \n"},o.prototype.tablerow=function(e){return"\n"+e+"\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"},o.prototype.strong=function(e){return""+e+""},o.prototype.em=function(e){return""+e+""},o.prototype.codespan=function(e){return""+e+""},o.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},o.prototype.del=function(e){return""+e+""},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='
    "},o.prototype.image=function(e,t,n){var r=''+n+'":">"},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;ea;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;nu;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=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=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-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:["![](","#url#)"],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 -

    - {{ article.current_revision.title }} -

    -
    - {% block wiki_contents_tab %} - {% endblock %} -
    - -{% 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 }} - - - - - - - - - - -{% 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 %} -
    -
    -
    - {% block wiki_contents %}{% endblock %} -
    -
    -
    - {% endblock %} - - - -{% 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" %} - - - {% if not create_form.slug.value %} - - {% endif %} - {% endaddtoblock %} - - {% include "wiki/includes/editormedia.html" %} -

    Add new article

    - -
    - {% wiki_form create_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 %} - - {% endfor %} - - {% for media, srcs in editor.Media.css.items %} - {% for src in srcs %} - - {% endfor %} - {% endfor %} -{% endblock %} - -{% block wiki_contents %} -

    Create First Article

    - -

    - Please create the root article. This article will be available at the root of your wiki, - so consider creating a landing page here. -

    -

    - 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. -

    - -
    - {% wiki_form 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 %} -

    Delete "{{ article.current_revision.title }}"

    - - {% if cannot_delete_root %} - - {% else %} - {% if cannot_delete_children %} -
    -
    -

    - You do not have permission to delete articles that have children. -

    - - - - - - Go back - -
    -
    - {% endif %} - - {% if delete_children %} -
    -
    -

    - You are deleting an article with children. If you delete this article, then its children will also be deleted. -

    -

    - If you choose to purge this article, note that its children will also be purged. -

    -
    -
    - -

    Child articles

    - -
      - {% for child in delete_children %} -
    • {{ child.article }}
    • - {% endfor %} - - {% if delete_children_more %} -
    • "...and more!"
    • - {% endif %} -
    - - {% endif %} - - {% if not cannot_delete_children %} -

    Please confirm that you would like to delete this article.

    - -
    - {% wiki_form delete_form %} - - - - - - Go back - - -
    - {% 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 deleted

    -
    -
    -

    - The article you were looking for has been deleted. -

    -
    -
    - - {% if not article.current_revision.locked or article|can_delete:user %} -

    - Restore Article -

    - -

    - To restore this article and any child articles, click the restore button below. -

    - - - - - Restore - - {% endif %} - - {% if article|can_moderate:user %} -

    - Purge Article -

    - -

    - 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. -

    -

    - Please note: This action cannot be undone. -

    - -
    - {% csrf_token %} - {% wiki_form purge_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 %} -

    Deleted Articles

    - - {% if deleted_articles %} - - - - - - - - - - - {% for article in deleted_articles %} - - - - - - {% endfor %} - -
    Page TitleDate DeletedRestore Article
    - {{ article }} - - {{article.modified}} - - - - - - Restore - -
    - {% else %} -

    - No deleted articles to display. -

    - {% 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 %} - - - -
    - -

    - {% 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 /{{ path }}. There {{ articles_plur_verb }} {{ cnt }} {{ articles_plur }} in this level. - {% endblocktrans %} - {% endwith %} -

    - - {% include "wiki/includes/pagination.html" %} - - - - - - - - - {% for urlpath in directory %} - - - - - - - - - {% empty %} - - - - {% endfor %} -
    TitleSlugLast modified
    - {{ urlpath.article.current_revision.title }} - - - - - - - - {% if urlpath.article.current_revision.deleted %} - - - - {% endif %} - - {% if urlpath.article.current_revision.locked %} - - - - {% endif %} - - {{ urlpath.slug }} - - {{ urlpath.article.current_revision.created|naturaltime }} -
    - There are no articles at this level -
    - - {% 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 %} -
    -
    -
    - {% with edit_form as form %} - {% include "wiki/includes/editor.html" %} - {% endwith %} - - - - -
    - {% if user.is_authenticated and urlpath.path %} - - - - - Move Article - - {% endif %} - {% if article|can_delete:user %} - - - - - Delete Article - - {% endif %} -
    - -
    -
    -
    -
    - -
    - {% include "wiki/includes/editor_sidebar.html" %} -
    -
    - - - - - - -{% 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" %} -

    Not Found

    - - - {% else %} -

    Error

    - - - {% 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 @@ -
    - {% if render_labels %} -
    - - - {% if field.help_text %} -

    - {{ field.help_text|safe }} -

    - {% endif %} -
    - {% endif %} - -
    -
    -
    - - - -
    - - {% if field.errors %} -

    - {% for error in field.errors %} - {{ error }}
    - {% endfor %} -

    - {% endif %} -
    -
    -
    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 @@ -
    - {% if render_labels and not is_markitup %} -
    - {% if field.label %} - - {% endif %} - - {% if field.help_text %} -

    - {{ field.help_text|safe }} -

    - {% endif %} -
    - {% endif %} - -
    -
    -
    - {% if is_markitup %} - - {% else %} - - {% endif %} -
    - - {% if field.errors %} -

    - {% for error in field.errors %} - {{ error }}
    - {% endfor %} -

    - {% endif %} -
    -
    -
    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 @@ -
    - {% if render_labels %} -
    - {% if field.label %} - - {% endif %} - - {% if field.help_text %} -

    - {{ field.help_text|safe }} -

    - {% endif %} -
    - {% endif %} - -
    -
    -
    -
    - -
    -
    - - {% if field.errors %} -

    - {% for error in field.errors %} - {{ error }}
    - {% endfor %} -

    - {% endif %} -
    -
    -
    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 @@ -
    - {% if render_labels %} -
    - {% if field.label %} - - {% endif %} - - {% if field.help_text %} -

    - {{ field.help_text|safe }} -

    - {% endif %} -
    - {% endif %} - -
    -
    -
    - {{ field }} -
    - - {% if field.errors %} -

    - {% for error in field.errors %} - {{ error }}
    - {% endfor %} -

    - {% endif %} -
    -
    -
    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 %} - -
    - {% if render_labels %} -
    - {% if field.label %} - - {% endif %} - - {% if field.help_text %} -

    - {{ field.help_text|safe }} -

    - {% endif %} -
    - {% endif %} - -
    -
    -
    -
    - {% get_field_options field %} - - -
    - -
    - - {% if field.errors %} -

    - {% for error in field.errors %} - {{ error }}
    - {% endfor %} -

    - {% endif %} -
    -
    -
    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 %} - -
    - {% if render_labels %} -
    - {% if field.label %} - - {% endif %} - - {% if field.help_text %} -

    - {{ field.help_text|safe }} -

    - {% endif %} -
    - {% endif %} - -
    -
    - -
    - -
    - - {% if field.errors %} -

    - {% for error in field.errors %} - {{ error }}
    - {% endfor %} -

    - {% endif %} -
    -
    -
    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" %} - - - {% endaddtoblock %} - -

    - 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. -

    - - {% include "wiki/includes/pagination.html" %} - -
    - - - - - - - - - - {% for revision in revisions %} - - - - - - {% endfor %} - -
    UpdatedSummaryActions
    - {% include "wiki/includes/revision_info.html" with current_revision=article.current_revision %} - - {% if revision.user_message %} - {{ revision.user_message }} - {% elif revision.automatic_log %} - {{ revision.automatic_log }} - {% else %} - No summary - {% endif %} - - {% if revision == article.current_revision %} - - - - - Preview - - - - - - Switch - - {% else %} - - - - - - Switch - - {% endif %} -
    - - {% include "wiki/includes/pagination.html" %} - - - - -
    - - - -{% 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 %} - - {% 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 %} - -{% 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 %} - -
    - {% for plugin, plugin_form in sidebar %} -
    -
    -

    - {% if plugin.sidebar.icon_class %} - - {% if plugin.sidebar.icon_class == "fa-picture-o" %} - - {% else %} - - {% endif %} - - {% endif %} - - {{ plugin.sidebar.headline }} -

    - -
    -
    -
    - {% if plugin.sidebar.template %} - {% with plugin_form as form %} - - {% endwith %} - {% endif %} -
    -
    -
    - {% endfor %} -
    - - 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" %} - - {% for js in editor.Media.js %} - - {% endfor %} -{% endaddtoblock %} - -{% addtoblock "css" %} - {% for media, srcs in editor.Media.css.items %} - {% for src in srcs %} - - {% 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 %} -
    - {% if form_error_title %} -
    -

    {{ form_error_title }}

    -
    - {% endif %} - -
    - {% for error_message in form.non_field_errors %} -

    {{ error_message }}

    - {% endfor %} -
    -
    -{% 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/messages.html b/pydis_site/templates/wiki/includes/messages.html deleted file mode 100644 index e69de29b..00000000 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 %} - -{% 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" %} - -{% endaddtoblock %} - -
    - {{ content|default:"" }} -
    - -{% for plugin in plugins %} - {% if plugin.RenderMedia.css %} - {% addtoblock "css" %} - {% for media, url in plugin.RenderMedia.css.items %} - - {% endfor %} - {% endaddtoblock %} - {% endif %} - - {% if plugin.RenderMedia.js %} - {% addtoblock "js" %} - {% for url in plugin.RenderMedia.js %} - - {% 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 %} - deleted -{% endif %} - -{% if revision.previous_revision.deleted and not revision.deleted %} - restored -{% endif %} - -{% if revision.locked %} - locked -{% endif %} - -{% if revision.previous_revision.locked and not revision.locked %} - unlocked -{% 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 %} - - - - {% for urlpath in article.urlpath_set.all %} - - {{ article.current_revision.title }} -
    - Slug: /{{ urlpath.path }} -
    - {% empty %} - - {{ article.current_revision.title }} - - {% endfor %} - - {% if article.current_revision.deleted %} - - - - {% endif %} - - {% if article.current_revision.locked %} - - - - {% endif %} - - - - {{ article.current_revision.created|naturaltime }} - - 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 %} -
    -
    - {% if urlpath.get_descendants %} - {% blocktrans count cnt=urlpath.get_descendants.count trimmed %} -

    - 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. -

    - {% plural %} -

    - 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. -

    - {% endblocktrans %} - {% endif %} -

    - 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. -

    -
    -
    - -
    - {% wiki_form form %} - - {# Not gonna lie, I have no idea what this is or what it's for #} -
    - - - - - - -
    - - - - - - Go back - - - -
    - - {% addtoblock "js" %} - - - {% 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 %} -
    -
    -

    Permission Denied

    -
    - -
    -

    - Sorry, you don't have permission to access this page. -

    - {% if article.current_revision.locked %} -

    This article is locked for editing.

    - {% endif %} - {% if not read_denied %} - - - - - Back to article - - {% elif urlpath.parent %} - - - - - Back to article - - {% endif %} -
    -
    -{% 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 %} -

    The following images are available for this article. Copy the markdown tag to directly refer to an image from the article text.

    - -

    - - - - - Back to edit page - -

    - - {% include "wiki/includes/pagination.html" %} - - - {% for image in images %} - {% with image.current_revision.imagerevision as revision %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% endwith %} - {% endfor %} -
    {{ revision.get_filename|default:_("No filename") }}TagUpdatedSize
    -
    - {% thumbnail revision.image "128x128" as thumb %} - - {{ revision.get_filename }} - - {% endthumbnail %} -
    - - {% if image|can_write:user %} - {% if revision.deleted %} - - - - - Upload - - - - - - Restore - - {% else %} - - - - - Upload - - - - - - Remove - - {% endif %} - {% if article|can_moderate:user %} - - - - - Delete - - {% endif %} - {% endif %} -
    - [image:{{ image.id }}] - - {% include "wiki/includes/revision_info.html" %} - - {{ revision.get_size|filesizeformat }}
    {{ revision.width }}x{{ revision.height }} pixels -
    - History -
    - - - - - - - - - - - - {% for old_revision in image.revision_set.all %} - - - - - - - - - - - - {% endfor %} - -
    UpdatedSizeDimensions
    -
    - {% thumbnail old_revision.imagerevision.image "50x50" crop="center" as thumb %} - - {{ revision.imagerevision.get_filename }} - - {% endthumbnail %} -
    -
    - {% include "wiki/includes/revision_info.html" with current_revision=image.current_revision revision=old_revision %} - - {{ old_revision.imagerevision.get_size|filesizeformat }} - - {{ old_revision.imagerevision.width }}x{{ old_revision.imagerevision.height }} pixels - - {% if image|can_write:user and old_revision != image.current_revision %} - - - - - Restore - - {% else %} - - - - - Current - - {% endif %} -
    -
    - - {% 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 %} -
    -
    -

    - Purge image: Completely remove image file and all revisions -

    -
    - {% wiki_form form %} - - - - - - Go Back - - - -
    -
    - -
    - {% thumbnail image.current_revision.imagerevision.image "250x250" as thumb %} -
    - - {{ revision.get_filename }} - -
    - {% endthumbnail %} -
    -
    -{% 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
    s and

    s. -{% endcomment %}{% with image.current_revision.imagerevision as revision %}{% spaceless %} -

    - - {% if size %} - {% thumbnail revision.image size upscale=False as thumb %} - {{ revision.get_filename }} - {% empty %} -
    - Image not found -
    - {% endthumbnail %} - {% else %} - {{ revision.get_filename }} - {% endif %} -
    -
    {{ caption|safe }}
    -
    -{% 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 %} -
    -
    -

    - Upload an image to replace the current one. -

    - -
    - {% wiki_form form %} - - - - - - Go Back - - - -
    -
    - -
    - {% thumbnail image.current_revision.imagerevision.image "250x250" as thumb %} -
    - - {{ revision.get_filename }} - -
    - {% endthumbnail %} -
    -
    -{% 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 %} - - - -
    - {% endif %} - - {% for image in images %} - {% with image.current_revision.imagerevision as revision %} - {% thumbnail revision.image "100x100" crop="center" as thumb %} -
    -
    - {{ revision.get_filename }} -
    -
    -
    - Image ID: {{ image.id }} -
    - -

    - - - - - - - {% if image|can_write:user %} - - - - - - {% endif %} -

    -
    -
    - {% endthumbnail %} - {% endwith %} - - {% empty %} -
    - No images found for this article. -
    -
    - {% endfor %} - -

    - - Manage Images - - - - -

    - -

    How to use images

    - -

    - 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 - Manage Images page. -

    - -

    - To make use of images in an article, use the image 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 align and size options are optional. -

    -

    - Syntax: [image:ID align:x size:y] -

    -

    - 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. -

    - -
    Example tag
    - -
    -  [image:1 align:left size:orig]
    -      Python Discord logo
    -  
    - -
    Options for align
    - -
    - left - right -
    - -
    Options for size
    - -
    - small - medium - large - orig - default -
    -{% endwith %} - - - - 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 %} - -

    Link to another wiki page

    - -

    - 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:
    -

    - -
    [Title of link](wiki:ArticleSlug)
    - -{# We do this to prevent accidental form submission - this isn't _really_ a form #} - - -
    -
    - - - - - -
    -
    - -
    - -

    An external link

    - -

    - You can link to another website simply by inserting an address example.com or http://example.com or by using the markdown syntax:
    -

    - -
    [Clickable text](http://example.com)
    - -{% 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 %} - - - - - -{% 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 %} -
    -
    -
    -

    Previewing Revision

    -
    -
    - {% include "wiki/includes/revision_info.html" %} -
    -
    -
    - {% endif %} - - {% if merge %} -
    - Previewing merge between: - {% include "wiki/includes/revision_info.html" with revision=merge1 %} - and - {% include "wiki/includes/revision_info.html" with revision=merge2 %} -
    - - {% if merge1.deleted %} -
    - You cannot merge with a deleted revision -
    - {% endif %} - {% endif %} - - {% if revision and revision.deleted %} -
    - This revision has been deleted. -

    Restoring to this revision will mark the article as deleted.

    -
    - {% else %} -
    -
    -
    -

    {{ article.current_revision.title }}

    - - {% wiki_render article content %} -
    -
    -
    - - {% for plugin in plugins %} - {% if plugin.RenderMedia.css %} - {% addtoblock "css" %} - {% for media, url in plugin.RenderMedia.css.items %} - - {% endfor %} - {% endaddtoblock %} - {% endif %} - {% if plugin.RenderMedia.js %} - {% addtoblock "js" %} - {% for url in plugin.RenderMedia.js %} - - {% 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 %} -
    -
    -
    -
    -

    No articles found

    -
    -
    -

    - It appears that there are no articles available on this wiki. -

    - - {% if not user.is_superuser %} -

    - To get started, please login with a superuser account. -

    - - {% login_url as wiki_login_url %} - - {% if wiki_login_url %} -
    - Login -
    - {% endif %} - {% else %} -

    - Let's get started - click below to create the first article, or to read up on the - django-wiki documentation. -

    - - Create Article - Documentation - {% endif %} -
    -
    -
    -
    -{% 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 %} -

    Search results for: "{{ search_query }}"

    - - - - {% include "wiki/includes/pagination.html" %} - - - - - - - - {% for article in articles %} - {% block wiki_search_loop %} - {% include "wiki/includes/searchresult.html" %} - {% endblock %} - - {% empty %} - - - - {% endfor %} -
    TitleLast modified
    - No articles were found for that search query. -
    - - {% 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 %} -
    - - - {% wiki_form form %} - -
    -
    - {# Empty for spacing #} -
    -
    -
    - -
    -
    -
    -
    - {% 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 %} -

    This article is currently locked for editing.

    - {% endif %} - -
    -{{ article.current_revision.content }}
    -  
    -{% endblock %} -- cgit v1.2.3 From aff3a89c3cec04eda096e8f27115e36108ee6286 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 4 Oct 2020 18:55:33 +0300 Subject: Change guides system to content system As this system will be used for more than just guides, I had to do some refactoring to match this system with plans. Basically now there isn't guides, but articles instead. --- pydis_site/apps/content/__init__.py | 0 pydis_site/apps/content/apps.py | 7 ++ pydis_site/apps/content/migrations/__init__.py | 0 .../content/resources/content/guides/_info.yml | 2 + .../content/guides/how-to-write-a-guide.md | 63 +++++++++++ pydis_site/apps/content/tests/__init__.py | 0 .../content/tests/test_content/category/_info.yml | 2 + .../content/tests/test_content/category/test3.md | 5 + pydis_site/apps/content/tests/test_content/test.md | 11 ++ .../apps/content/tests/test_content/test2.md | 5 + pydis_site/apps/content/tests/test_utils.py | 122 +++++++++++++++++++++ pydis_site/apps/content/tests/test_views.py | 104 ++++++++++++++++++ pydis_site/apps/content/urls.py | 11 ++ pydis_site/apps/content/utils.py | 77 +++++++++++++ pydis_site/apps/content/views/__init__.py | 5 + pydis_site/apps/content/views/article.py | 50 +++++++++ pydis_site/apps/content/views/articles.py | 14 +++ pydis_site/apps/content/views/category.py | 18 +++ pydis_site/apps/guides/__init__.py | 0 pydis_site/apps/guides/apps.py | 7 -- pydis_site/apps/guides/migrations/__init__.py | 0 .../resources/guides/how-to-write-a-guide.md | 63 ----------- pydis_site/apps/guides/tests/__init__.py | 0 .../guides/tests/test_guides/category/_info.yml | 2 - .../guides/tests/test_guides/category/test3.md | 5 - pydis_site/apps/guides/tests/test_guides/test.md | 11 -- pydis_site/apps/guides/tests/test_guides/test2.md | 5 - pydis_site/apps/guides/tests/test_utils.py | 122 --------------------- pydis_site/apps/guides/tests/test_views.py | 104 ------------------ pydis_site/apps/guides/urls.py | 11 -- pydis_site/apps/guides/utils.py | 77 ------------- pydis_site/apps/guides/views/__init__.py | 5 - pydis_site/apps/guides/views/category.py | 18 --- pydis_site/apps/guides/views/guide.py | 48 -------- pydis_site/apps/guides/views/guides.py | 14 --- pydis_site/apps/home/urls.py | 2 +- pydis_site/settings.py | 2 +- pydis_site/static/css/content/articles.css | 7 ++ pydis_site/static/css/guides/guide.css | 7 -- pydis_site/templates/content/article.html | 61 +++++++++++ pydis_site/templates/content/articles.html | 53 +++++++++ pydis_site/templates/content/category.html | 44 ++++++++ pydis_site/templates/guides/category.html | 44 -------- pydis_site/templates/guides/guide.html | 61 ----------- pydis_site/templates/guides/guides.html | 53 --------- 45 files changed, 663 insertions(+), 659 deletions(-) create mode 100644 pydis_site/apps/content/__init__.py create mode 100644 pydis_site/apps/content/apps.py create mode 100644 pydis_site/apps/content/migrations/__init__.py create mode 100644 pydis_site/apps/content/resources/content/guides/_info.yml create mode 100644 pydis_site/apps/content/resources/content/guides/how-to-write-a-guide.md create mode 100644 pydis_site/apps/content/tests/__init__.py create mode 100644 pydis_site/apps/content/tests/test_content/category/_info.yml create mode 100644 pydis_site/apps/content/tests/test_content/category/test3.md create mode 100644 pydis_site/apps/content/tests/test_content/test.md create mode 100644 pydis_site/apps/content/tests/test_content/test2.md create mode 100644 pydis_site/apps/content/tests/test_utils.py create mode 100644 pydis_site/apps/content/tests/test_views.py create mode 100644 pydis_site/apps/content/urls.py create mode 100644 pydis_site/apps/content/utils.py create mode 100644 pydis_site/apps/content/views/__init__.py create mode 100644 pydis_site/apps/content/views/article.py create mode 100644 pydis_site/apps/content/views/articles.py create mode 100644 pydis_site/apps/content/views/category.py delete mode 100644 pydis_site/apps/guides/__init__.py delete mode 100644 pydis_site/apps/guides/apps.py delete mode 100644 pydis_site/apps/guides/migrations/__init__.py delete mode 100644 pydis_site/apps/guides/resources/guides/how-to-write-a-guide.md delete mode 100644 pydis_site/apps/guides/tests/__init__.py delete mode 100644 pydis_site/apps/guides/tests/test_guides/category/_info.yml delete mode 100644 pydis_site/apps/guides/tests/test_guides/category/test3.md delete mode 100644 pydis_site/apps/guides/tests/test_guides/test.md delete mode 100644 pydis_site/apps/guides/tests/test_guides/test2.md delete mode 100644 pydis_site/apps/guides/tests/test_utils.py delete mode 100644 pydis_site/apps/guides/tests/test_views.py delete mode 100644 pydis_site/apps/guides/urls.py delete mode 100644 pydis_site/apps/guides/utils.py delete mode 100644 pydis_site/apps/guides/views/__init__.py delete mode 100644 pydis_site/apps/guides/views/category.py delete mode 100644 pydis_site/apps/guides/views/guide.py delete mode 100644 pydis_site/apps/guides/views/guides.py create mode 100644 pydis_site/static/css/content/articles.css delete mode 100644 pydis_site/static/css/guides/guide.css create mode 100644 pydis_site/templates/content/article.html create mode 100644 pydis_site/templates/content/articles.html create mode 100644 pydis_site/templates/content/category.html delete mode 100644 pydis_site/templates/guides/category.html delete mode 100644 pydis_site/templates/guides/guide.html delete mode 100644 pydis_site/templates/guides/guides.html (limited to 'pydis_site/static/css') diff --git a/pydis_site/apps/content/__init__.py b/pydis_site/apps/content/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pydis_site/apps/content/apps.py b/pydis_site/apps/content/apps.py new file mode 100644 index 00000000..1e300a48 --- /dev/null +++ b/pydis_site/apps/content/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ContentConfig(AppConfig): + """Django AppConfig for content app.""" + + name = 'content' diff --git a/pydis_site/apps/content/migrations/__init__.py b/pydis_site/apps/content/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pydis_site/apps/content/resources/content/guides/_info.yml b/pydis_site/apps/content/resources/content/guides/_info.yml new file mode 100644 index 00000000..8a38271d --- /dev/null +++ b/pydis_site/apps/content/resources/content/guides/_info.yml @@ -0,0 +1,2 @@ +name: Guides +description: Python and PyDis guides. \ No newline at end of file diff --git a/pydis_site/apps/content/resources/content/guides/how-to-write-a-guide.md b/pydis_site/apps/content/resources/content/guides/how-to-write-a-guide.md new file mode 100644 index 00000000..072c2538 --- /dev/null +++ b/pydis_site/apps/content/resources/content/guides/how-to-write-a-guide.md @@ -0,0 +1,63 @@ +Title: How to Write a Guide +ShortDescription: Learn how to write a guide for this website +Contributors: ks129 + +When you are interested about how to write guide for this site (like this), then you can learn about it here. +PyDis use Markdown files for guides, but these files have some small differences from standard Markdown (like defining HTML IDs and classes). + +## [Getting Started](#getting-started){: id="getting-started" } +First, you have to have a good idea, that match with PyDis theme. We can't accept guides like *How to bake a cake*, +*How to lose weigth*. These doesn't match with PyDis theme and will be declined. Most of guides theme should be server and Python, but there can be some exceptions, when they are connected with PyDis. +Best way to find out is your idea good is to discuss about it in #dev-core channel. There can other peoples give their opinion about your idea. Even better, open issue in site repository first, then PyDis staff can see it and approve/decline this idea. +It's good idea to wait for staff decision before starting to write guide to avoid case when you write a long long guide, but then this don't get approved. + +To start with contributing, you should read [how to contribute to site](https://pythondiscord.com/pages/contributing/site/). +You should also read our [Git workflow](https://pythondiscord.com/pages/contributing/working-with-git/), because you need to push your guide to GitHub. + +## [Creating a File](#creating-a-file){: id="creating-a-file" } +All guides is located at `site` repository, in `pydis_site/apps/guides/resources/guides`. Under this is root level guides (.md files) and categories (directories). Learn more about categories in [categories section](#categories). + +At this point, you will need your guide name for filename. Replace all your guide name spaces with `-` and make all lowercase. Save this as `.md` (Markdown) file. This name (without Markdown extension) is path of guide in URL. + +## [Markdown Metadata](#markdown-metadata){: id="markdown-metadata" } +Guide files have some required metadata, like title, contributors, description, relevant pages. Metadata is first thing in file, YAML-like key-value pairs: + +```md +Title: My Guide +ShortDescription: This is my short description. +Contributors: person1 + person2 + person3 +RelevantLinks: url1 + url2 + url3 +RelevantLinkValues: Text for url1 + Text for url2 + Text for url3 + +Here comes content of guide... +``` + +You can read more about Markdown metadata [here](https://python-markdown.github.io/extensions/meta_data/). + +### Fields +- **Name:** Easily-readable name for your guide. +- **Short Description:** Small, 1-2 line description that describe what your guide explain. +- **Contributors:** All who have contributed to this guide. One person per-line, and they **have to be at same level**. When you edit guide, add your name to there. +- **Relevant Links and Values:** Links that will be shown at right side. Both key's values have to be at same level, just like for contributors field. + +## [Content](#content){: id="content" } +For content, mostly you can use standard markdown, but there is a few addition that is available. + +### HTML classes and IDs +To provide HTML classes and/or IDs, this use `{: id="myid" class="class1 class2" }`. When using it at header, place this **right after** title, no space between them. For mutliline items, place them next line after end of block. You can read more about it [here](https://python-markdown.github.io/extensions/attr_list/). + +## [Categories](#categories){: id="categories" } +To have some systematic sorting of guides, site support guides categories. Currently this system support only 1 level of categories. Categories live at `site` repo in `pydis_site/apps/guides/resources/guides` subdirectories. Directory name is path of category in URL. Inside category directory, there is 1 file required: `_info.yml`. This file need 2 key-value pairs defined: + +```yml +name: Category name +description: Category description +``` + +Then all Markdown files in this folder will be under this category. diff --git a/pydis_site/apps/content/tests/__init__.py b/pydis_site/apps/content/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pydis_site/apps/content/tests/test_content/category/_info.yml b/pydis_site/apps/content/tests/test_content/category/_info.yml new file mode 100644 index 00000000..8311509d --- /dev/null +++ b/pydis_site/apps/content/tests/test_content/category/_info.yml @@ -0,0 +1,2 @@ +name: My Category +description: My Description diff --git a/pydis_site/apps/content/tests/test_content/category/test3.md b/pydis_site/apps/content/tests/test_content/category/test3.md new file mode 100644 index 00000000..bdde6188 --- /dev/null +++ b/pydis_site/apps/content/tests/test_content/category/test3.md @@ -0,0 +1,5 @@ +Title: Test 3 +ShortDescription: Testing 3 +Contributors: user3 + +This is too test content, but in category. diff --git a/pydis_site/apps/content/tests/test_content/test.md b/pydis_site/apps/content/tests/test_content/test.md new file mode 100644 index 00000000..7a917899 --- /dev/null +++ b/pydis_site/apps/content/tests/test_content/test.md @@ -0,0 +1,11 @@ +Title: Test +ShortDescription: Testing +Contributors: user +RelevantLinks: https://pythondiscord.com/pages/resources/guides/asking-good-questions/ + https://pythondiscord.com/pages/resources/guides/help-channels/ + https://pythondiscord.com/pages/code-of-conduct/ +RelevantLinkValues: Asking Good Questions + Help Channel Guide + Code of Conduct + +This is test content. diff --git a/pydis_site/apps/content/tests/test_content/test2.md b/pydis_site/apps/content/tests/test_content/test2.md new file mode 100644 index 00000000..f0852356 --- /dev/null +++ b/pydis_site/apps/content/tests/test_content/test2.md @@ -0,0 +1,5 @@ +Title: Test 2 +ShortDescription: Testing 2 +Contributors: user2 + +This is too test content. \ No newline at end of file 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..82e1ac5f --- /dev/null +++ b/pydis_site/apps/content/tests/test_utils.py @@ -0,0 +1,122 @@ +import os +from unittest.mock import patch + +from django.conf import settings +from django.http import Http404 +from django.test import TestCase +from markdown import Markdown + +from pydis_site.apps.content import utils + +BASE_PATH = os.path.join(settings.BASE_DIR, "pydis_site", "apps", "content", "tests", "test_content") + + +class TestGetBasePath(TestCase): + def test_get_base_path(self): + """Test does function return content base path.""" + self.assertEqual( + utils._get_base_path(), + os.path.join(settings.BASE_DIR, "pydis_site", "apps", "content", "resources", "content") + ) + + +class TestGetCategory(TestCase): + def test_get_category_successfully(self): + """Check does this get right data from category data file.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + result = utils.get_category("category") + + self.assertEqual(result, {"name": "My Category", "description": "My Description"}) + + def test_get_category_not_exists(self): + """Check does this raise 404 error when category don't exists.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + with self.assertRaises(Http404): + utils.get_category("invalid") + + def test_get_category_not_directory(self): + """Check does this raise 404 error when category isn't directory.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + with self.assertRaises(Http404): + utils.get_category("test.md") + + +class TestGetCategories(TestCase): + def test_get_categories(self): + """Check does this return test content categories.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + result = utils.get_categories() + + self.assertEqual(result, {"category": {"name": "My Category", "description": "My Description"}}) + + +class TestGetArticles(TestCase): + def test_get_all_root_articles(self): + """Check does this return all root level testing content.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + result = utils.get_articles() + + for case in ["test", "test2"]: + with self.subTest(guide=case): + md = Markdown(extensions=['meta']) + with open(os.path.join(BASE_PATH, f"{case}.md")) as f: + md.convert(f.read()) + + self.assertIn(case, result) + self.assertEqual(md.Meta, result[case]) + + def test_get_all_category_articles(self): + """Check does this return all category testing content.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + result = utils.get_articles("category") + + md = Markdown(extensions=['meta']) + with open(os.path.join(BASE_PATH, "category", "test3.md")) as f: + md.convert(f.read()) + + self.assertIn("test3", result) + self.assertEqual(md.Meta, result["test3"]) + + +class TestGetArticle(TestCase): + def test_get_root_article_success(self): + """Check does this return article HTML and metadata when root article exist.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + result = utils.get_article("test", None) + + md = Markdown(extensions=['meta', 'attr_list', 'fenced_code']) + + with open(os.path.join(BASE_PATH, "test.md")) as f: + html = md.convert(f.read()) + + self.assertEqual(result, {"article": html, "metadata": md.Meta}) + + def test_get_root_article_dont_exist(self): + """Check does this raise Http404 when root article don't exist.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + with self.assertRaises(Http404): + utils.get_article("invalid", None) + + def test_get_category_article_success(self): + """Check does this return article HTML and metadata when category guide exist.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + result = utils.get_article("test3", "category") + + md = Markdown(extensions=['meta', 'attr_list', 'fenced_code']) + + with open(os.path.join(BASE_PATH, "category", "test3.md")) as f: + html = md.convert(f.read()) + + self.assertEqual(result, {"article": html, "metadata": md.Meta}) + + def test_get_category_article_dont_exist(self): + """Check does this raise Http404 when category article don't exist.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + with self.assertRaises(Http404): + utils.get_article("invalid", "category") + + def test_get_category_article_category_dont_exist(self): + """Check does this raise Http404 when category don't exist.""" + with patch("pydis_site.apps.content.utils._get_base_path", return_value=BASE_PATH): + with self.assertRaises(Http404): + utils.get_article("some-guide", "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..98054534 --- /dev/null +++ b/pydis_site/apps/content/tests/test_views.py @@ -0,0 +1,104 @@ +from unittest.mock import patch + +from django.http import Http404 +from django.test import TestCase +from django_hosts.resolvers import reverse + + +class TestGuidesIndexView(TestCase): + @patch("pydis_site.apps.content.views.articles.get_articles") + @patch("pydis_site.apps.content.views.articles.get_categories") + def test_articles_index_return_200(self, get_categories_mock, get_articles_mock): + """Check that content index return HTTP code 200.""" + get_categories_mock.return_value = {} + get_articles_mock.return_value = {} + + url = reverse('content:content') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + get_articles_mock.assert_called_once() + get_categories_mock.assert_called_once() + + +class TestGuideView(TestCase): + @patch("pydis_site.apps.content.views.article.os.path.getmtime") + @patch("pydis_site.apps.content.views.article.get_article") + @patch("pydis_site.apps.content.views.article.get_category") + def test_guide_return_code_200(self, get_category_mock, get_article_mock, get_time_mock): + get_article_mock.return_value = {"guide": "test", "metadata": {}} + + url = reverse("content:article", args=["test-guide"]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + get_category_mock.assert_not_called() + get_article_mock.assert_called_once_with("test-guide", None) + + @patch("pydis_site.apps.content.views.article.os.path.getmtime") + @patch("pydis_site.apps.content.views.article.get_article") + @patch("pydis_site.apps.content.views.article.get_category") + def test_guide_return_404(self, get_category_mock, get_article_mock, get_time_mock): + """Check that return code is 404 when invalid article provided.""" + get_article_mock.side_effect = Http404("Article not found.") + + url = reverse("content:article", args=["invalid-guide"]) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + get_article_mock.assert_called_once_with("invalid-guide", None) + get_category_mock.assert_not_called() + + +class TestCategoryView(TestCase): + @patch("pydis_site.apps.content.views.category.get_category") + @patch("pydis_site.apps.content.views.category.get_articles") + def test_valid_category_code_200(self, get_articles_mock, get_category_mock): + """Check that return code is 200 when visiting valid category.""" + get_category_mock.return_value = {"name": "test", "description": "test"} + get_articles_mock.return_value = {} + + url = reverse("content:category", args=["category"]) + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + get_articles_mock.assert_called_once_with("category") + get_category_mock.assert_called_once_with("category") + + @patch("pydis_site.apps.content.views.category.get_category") + @patch("pydis_site.apps.content.views.category.get_articles") + def test_invalid_category_code_404(self, get_articles_mock, get_category_mock): + """Check that return code is 404 when trying to visit invalid category.""" + get_category_mock.side_effect = Http404("Category not found.") + + url = reverse("content:category", args=["invalid-category"]) + response = self.client.get(url) + + self.assertEqual(response.status_code, 404) + get_category_mock.assert_called_once_with("invalid-category") + get_articles_mock.assert_not_called() + + +class TestCategoryGuidesView(TestCase): + @patch("pydis_site.apps.content.views.article.os.path.getmtime") + @patch("pydis_site.apps.content.views.article.get_article") + @patch("pydis_site.apps.content.views.article.get_category") + def test_valid_category_article_code_200(self, get_category_mock, get_article_mock, get_time_mock): + """Check that return code is 200 when visiting valid category article.""" + get_article_mock.return_value = {"guide": "test", "metadata": {}} + + url = reverse("content:category_article", args=["category", "test3"]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + get_article_mock.assert_called_once_with("test3", "category") + get_category_mock.assert_called_once_with("category") + + @patch("pydis_site.apps.content.views.article.os.path.getmtime") + @patch("pydis_site.apps.content.views.article.get_article") + @patch("pydis_site.apps.content.views.article.get_category") + def test_invalid_category_article_code_404(self, get_category_mock, get_article_mock, get_time_mock): + """Check that return code is 200 when trying to visit invalid category article.""" + get_article_mock.side_effect = Http404("Article not found.") + + url = reverse("content:category_article", args=["category", "invalid"]) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + get_article_mock.assert_called_once_with("invalid", "category") + get_category_mock.assert_not_called() diff --git a/pydis_site/apps/content/urls.py b/pydis_site/apps/content/urls.py new file mode 100644 index 00000000..ae9b1e57 --- /dev/null +++ b/pydis_site/apps/content/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from . import views + +app_name = "content" +urlpatterns = [ + path("", views.ArticlesView.as_view(), name='content'), + path("category//", views.CategoryView.as_view(), name='category'), + path("category///", views.ArticleView.as_view(), name='category_article'), + path("/", views.ArticleView.as_view(), name='article') +] diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py new file mode 100644 index 00000000..57905a69 --- /dev/null +++ b/pydis_site/apps/content/utils.py @@ -0,0 +1,77 @@ +import os +from typing import Dict, Optional, Union + +import yaml +from django.conf import settings +from django.http import Http404 +from markdown import Markdown + + +def _get_base_path() -> str: + """Have extra function for base path getting for testability.""" + return os.path.join(settings.BASE_DIR, "pydis_site", "apps", "content", "resources", "content") + + +def get_category(category: str) -> Dict[str, str]: + """Load category information by name from _info.yml.""" + path = os.path.join(_get_base_path(), category) + if not os.path.exists(path) or not os.path.isdir(path): + raise Http404("Category not found.") + + with open(os.path.join(path, "_info.yml")) as f: + return yaml.safe_load(f.read()) + + +def get_categories() -> Dict[str, Dict]: + """Get all categories information.""" + base_path = _get_base_path() + categories = {} + + for name in os.listdir(base_path): + if os.path.isdir(os.path.join(base_path, name)): + categories[name] = get_category(name) + + return categories + + +def get_articles(category: Optional[str] = None) -> Dict[str, Dict]: + """Get all root content when category is not specified. Otherwise get all this category content.""" + if category is None: + base_dir = _get_base_path() + else: + base_dir = os.path.join(_get_base_path(), category) + + articles = {} + + for filename in os.listdir(base_dir): + full_path = os.path.join(base_dir, filename) + if os.path.isfile(full_path) and filename.endswith(".md"): + md = Markdown(extensions=['meta']) + with open(full_path) as f: + md.convert(f.read()) + + articles[os.path.splitext(filename)[0]] = md.Meta + + return articles + + +def get_article(article: str, category: Optional[str]) -> Dict[str, Union[str, Dict]]: + """Get one specific article. When category is specified, get it from there.""" + if category is None: + base_path = _get_base_path() + else: + base_path = os.path.join(_get_base_path(), category) + + if not os.path.exists(base_path) or not os.path.isdir(base_path): + raise Http404("Category not found.") + + article_path = os.path.join(base_path, f"{article}.md") + if not os.path.exists(article_path) or not os.path.isfile(article_path): + raise Http404("Article not found.") + + md = Markdown(extensions=['meta', 'attr_list', 'fenced_code']) + + with open(article_path) as f: + html = md.convert(f.read()) + + return {"article": html, "metadata": md.Meta} diff --git a/pydis_site/apps/content/views/__init__.py b/pydis_site/apps/content/views/__init__.py new file mode 100644 index 00000000..b50d487b --- /dev/null +++ b/pydis_site/apps/content/views/__init__.py @@ -0,0 +1,5 @@ +from .category import CategoryView +from .article import ArticleView +from .articles import ArticlesView + +__all__ = ["ArticleView", "ArticlesView", "CategoryView"] diff --git a/pydis_site/apps/content/views/article.py b/pydis_site/apps/content/views/article.py new file mode 100644 index 00000000..b34ca3ee --- /dev/null +++ b/pydis_site/apps/content/views/article.py @@ -0,0 +1,50 @@ +import os +from datetime import datetime +from typing import Optional + +from django.conf import settings +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponse +from django.shortcuts import render +from django.views import View + +from pydis_site.apps.content.utils import get_category, get_article + + +class ArticleView(View): + """Shows specific guide page.""" + + def get(self, request: WSGIRequest, article: str, category: Optional[str] = None) -> HttpResponse: + """Collect guide content and display it. When guide don't exist, return 404.""" + article_result = get_article(article, category) + + if category is not None: + path = os.path.join( + settings.BASE_DIR, "pydis_site", "apps", "content", "resources", "content", category, f"{article}.md" + ) + else: + path = os.path.join( + settings.BASE_DIR, "pydis_site", "apps", "content", "resources", "content", f"{article}.md" + ) + + if category is not None: + category_data = get_category(category) + category_data["raw_name"] = category + else: + category_data = {"name": None, "raw_name": None} + + return render( + request, + "content/article.html", + { + "article": article_result, + "last_modified": datetime.fromtimestamp(os.path.getmtime(path)).strftime("%dth %B %Y"), + "category_data": category_data, + "relevant_links": { + link: value for link, value in zip( + article_result["metadata"].get("relevantlinks", []), + article_result["metadata"].get("relevantlinkvalues", []) + ) + } + } + ) diff --git a/pydis_site/apps/content/views/articles.py b/pydis_site/apps/content/views/articles.py new file mode 100644 index 00000000..ff945a19 --- /dev/null +++ b/pydis_site/apps/content/views/articles.py @@ -0,0 +1,14 @@ +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponse +from django.shortcuts import render +from django.views import View + +from pydis_site.apps.content.utils import get_categories, get_articles + + +class ArticlesView(View): + """Shows all content and categories.""" + + def get(self, request: WSGIRequest) -> HttpResponse: + """Shows all content and categories.""" + return render(request, "content/articles.html", {"content": get_articles(), "categories": get_categories()}) diff --git a/pydis_site/apps/content/views/category.py b/pydis_site/apps/content/views/category.py new file mode 100644 index 00000000..62e80a47 --- /dev/null +++ b/pydis_site/apps/content/views/category.py @@ -0,0 +1,18 @@ +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponse +from django.shortcuts import render +from django.views import View + +from pydis_site.apps.content.utils import get_category, get_articles + + +class CategoryView(View): + """Handles content category page.""" + + def get(self, request: WSGIRequest, category: str) -> HttpResponse: + """Handles page that displays category content.""" + return render( + request, + "content/category.html", + {"category_info": get_category(category), "content": get_articles(category), "category_name": category} + ) diff --git a/pydis_site/apps/guides/__init__.py b/pydis_site/apps/guides/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pydis_site/apps/guides/apps.py b/pydis_site/apps/guides/apps.py deleted file mode 100644 index 8dfa4f65..00000000 --- a/pydis_site/apps/guides/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class GuidesConfig(AppConfig): - """Django AppConfig for guides app.""" - - name = 'guides' diff --git a/pydis_site/apps/guides/migrations/__init__.py b/pydis_site/apps/guides/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pydis_site/apps/guides/resources/guides/how-to-write-a-guide.md b/pydis_site/apps/guides/resources/guides/how-to-write-a-guide.md deleted file mode 100644 index 072c2538..00000000 --- a/pydis_site/apps/guides/resources/guides/how-to-write-a-guide.md +++ /dev/null @@ -1,63 +0,0 @@ -Title: How to Write a Guide -ShortDescription: Learn how to write a guide for this website -Contributors: ks129 - -When you are interested about how to write guide for this site (like this), then you can learn about it here. -PyDis use Markdown files for guides, but these files have some small differences from standard Markdown (like defining HTML IDs and classes). - -## [Getting Started](#getting-started){: id="getting-started" } -First, you have to have a good idea, that match with PyDis theme. We can't accept guides like *How to bake a cake*, -*How to lose weigth*. These doesn't match with PyDis theme and will be declined. Most of guides theme should be server and Python, but there can be some exceptions, when they are connected with PyDis. -Best way to find out is your idea good is to discuss about it in #dev-core channel. There can other peoples give their opinion about your idea. Even better, open issue in site repository first, then PyDis staff can see it and approve/decline this idea. -It's good idea to wait for staff decision before starting to write guide to avoid case when you write a long long guide, but then this don't get approved. - -To start with contributing, you should read [how to contribute to site](https://pythondiscord.com/pages/contributing/site/). -You should also read our [Git workflow](https://pythondiscord.com/pages/contributing/working-with-git/), because you need to push your guide to GitHub. - -## [Creating a File](#creating-a-file){: id="creating-a-file" } -All guides is located at `site` repository, in `pydis_site/apps/guides/resources/guides`. Under this is root level guides (.md files) and categories (directories). Learn more about categories in [categories section](#categories). - -At this point, you will need your guide name for filename. Replace all your guide name spaces with `-` and make all lowercase. Save this as `.md` (Markdown) file. This name (without Markdown extension) is path of guide in URL. - -## [Markdown Metadata](#markdown-metadata){: id="markdown-metadata" } -Guide files have some required metadata, like title, contributors, description, relevant pages. Metadata is first thing in file, YAML-like key-value pairs: - -```md -Title: My Guide -ShortDescription: This is my short description. -Contributors: person1 - person2 - person3 -RelevantLinks: url1 - url2 - url3 -RelevantLinkValues: Text for url1 - Text for url2 - Text for url3 - -Here comes content of guide... -``` - -You can read more about Markdown metadata [here](https://python-markdown.github.io/extensions/meta_data/). - -### Fields -- **Name:** Easily-readable name for your guide. -- **Short Description:** Small, 1-2 line description that describe what your guide explain. -- **Contributors:** All who have contributed to this guide. One person per-line, and they **have to be at same level**. When you edit guide, add your name to there. -- **Relevant Links and Values:** Links that will be shown at right side. Both key's values have to be at same level, just like for contributors field. - -## [Content](#content){: id="content" } -For content, mostly you can use standard markdown, but there is a few addition that is available. - -### HTML classes and IDs -To provide HTML classes and/or IDs, this use `{: id="myid" class="class1 class2" }`. When using it at header, place this **right after** title, no space between them. For mutliline items, place them next line after end of block. You can read more about it [here](https://python-markdown.github.io/extensions/attr_list/). - -## [Categories](#categories){: id="categories" } -To have some systematic sorting of guides, site support guides categories. Currently this system support only 1 level of categories. Categories live at `site` repo in `pydis_site/apps/guides/resources/guides` subdirectories. Directory name is path of category in URL. Inside category directory, there is 1 file required: `_info.yml`. This file need 2 key-value pairs defined: - -```yml -name: Category name -description: Category description -``` - -Then all Markdown files in this folder will be under this category. diff --git a/pydis_site/apps/guides/tests/__init__.py b/pydis_site/apps/guides/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pydis_site/apps/guides/tests/test_guides/category/_info.yml b/pydis_site/apps/guides/tests/test_guides/category/_info.yml deleted file mode 100644 index 8311509d..00000000 --- a/pydis_site/apps/guides/tests/test_guides/category/_info.yml +++ /dev/null @@ -1,2 +0,0 @@ -name: My Category -description: My Description diff --git a/pydis_site/apps/guides/tests/test_guides/category/test3.md b/pydis_site/apps/guides/tests/test_guides/category/test3.md deleted file mode 100644 index bdde6188..00000000 --- a/pydis_site/apps/guides/tests/test_guides/category/test3.md +++ /dev/null @@ -1,5 +0,0 @@ -Title: Test 3 -ShortDescription: Testing 3 -Contributors: user3 - -This is too test content, but in category. diff --git a/pydis_site/apps/guides/tests/test_guides/test.md b/pydis_site/apps/guides/tests/test_guides/test.md deleted file mode 100644 index 7a917899..00000000 --- a/pydis_site/apps/guides/tests/test_guides/test.md +++ /dev/null @@ -1,11 +0,0 @@ -Title: Test -ShortDescription: Testing -Contributors: user -RelevantLinks: https://pythondiscord.com/pages/resources/guides/asking-good-questions/ - https://pythondiscord.com/pages/resources/guides/help-channels/ - https://pythondiscord.com/pages/code-of-conduct/ -RelevantLinkValues: Asking Good Questions - Help Channel Guide - Code of Conduct - -This is test content. diff --git a/pydis_site/apps/guides/tests/test_guides/test2.md b/pydis_site/apps/guides/tests/test_guides/test2.md deleted file mode 100644 index f0852356..00000000 --- a/pydis_site/apps/guides/tests/test_guides/test2.md +++ /dev/null @@ -1,5 +0,0 @@ -Title: Test 2 -ShortDescription: Testing 2 -Contributors: user2 - -This is too test content. \ No newline at end of file diff --git a/pydis_site/apps/guides/tests/test_utils.py b/pydis_site/apps/guides/tests/test_utils.py deleted file mode 100644 index e7448be6..00000000 --- a/pydis_site/apps/guides/tests/test_utils.py +++ /dev/null @@ -1,122 +0,0 @@ -import os -from unittest.mock import patch - -from django.conf import settings -from django.http import Http404 -from django.test import TestCase -from markdown import Markdown - -from pydis_site.apps.guides import utils - -BASE_PATH = os.path.join(settings.BASE_DIR, "pydis_site", "apps", "guides", "tests", "test_guides") - - -class TestGetBasePath(TestCase): - def test_get_base_path(self): - """Test does function return guides base path.""" - self.assertEqual( - utils._get_base_path(), - os.path.join(settings.BASE_DIR, "pydis_site", "apps", "guides", "resources", "guides") - ) - - -class TestGetCategory(TestCase): - def test_get_category_successfully(self): - """Check does this get right data from category data file.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - result = utils.get_category("category") - - self.assertEqual(result, {"name": "My Category", "description": "My Description"}) - - def test_get_category_not_exists(self): - """Check does this raise 404 error when category don't exists.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - with self.assertRaises(Http404): - utils.get_category("invalid") - - def test_get_category_not_directory(self): - """Check does this raise 404 error when category isn't directory.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - with self.assertRaises(Http404): - utils.get_category("test.md") - - -class TestGetCategories(TestCase): - def test_get_categories(self): - """Check does this return test guides categories.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - result = utils.get_categories() - - self.assertEqual(result, {"category": {"name": "My Category", "description": "My Description"}}) - - -class TestGetGuides(TestCase): - def test_get_all_root_guides(self): - """Check does this return all root level testing guides.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - result = utils.get_guides() - - for case in ["test", "test2"]: - with self.subTest(guide=case): - md = Markdown(extensions=['meta']) - with open(os.path.join(BASE_PATH, f"{case}.md")) as f: - md.convert(f.read()) - - self.assertIn(case, result) - self.assertEqual(md.Meta, result[case]) - - def test_get_all_category_guides(self): - """Check does this return all category testing guides.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - result = utils.get_guides("category") - - md = Markdown(extensions=['meta']) - with open(os.path.join(BASE_PATH, "category", "test3.md")) as f: - md.convert(f.read()) - - self.assertIn("test3", result) - self.assertEqual(md.Meta, result["test3"]) - - -class TestGetGuide(TestCase): - def test_get_root_guide_success(self): - """Check does this return guide HTML and metadata when root guide exist.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - result = utils.get_guide("test", None) - - md = Markdown(extensions=['meta', 'attr_list', 'fenced_code']) - - with open(os.path.join(BASE_PATH, "test.md")) as f: - html = md.convert(f.read()) - - self.assertEqual(result, {"guide": html, "metadata": md.Meta}) - - def test_get_root_guide_dont_exist(self): - """Check does this raise Http404 when root guide don't exist.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - with self.assertRaises(Http404): - result = utils.get_guide("invalid", None) - - def test_get_category_guide_success(self): - """Check does this return guide HTML and metadata when category guide exist.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - result = utils.get_guide("test3", "category") - - md = Markdown(extensions=['meta', 'attr_list', 'fenced_code']) - - with open(os.path.join(BASE_PATH, "category", "test3.md")) as f: - html = md.convert(f.read()) - - self.assertEqual(result, {"guide": html, "metadata": md.Meta}) - - def test_get_category_guide_dont_exist(self): - """Check does this raise Http404 when category guide don't exist.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - with self.assertRaises(Http404): - result = utils.get_guide("invalid", "category") - - def test_get_category_guide_category_dont_exist(self): - """Check does this raise Http404 when category don't exist.""" - with patch("pydis_site.apps.guides.utils._get_base_path", return_value=BASE_PATH): - with self.assertRaises(Http404): - result = utils.get_guide("some-guide", "invalid") diff --git a/pydis_site/apps/guides/tests/test_views.py b/pydis_site/apps/guides/tests/test_views.py deleted file mode 100644 index e3945136..00000000 --- a/pydis_site/apps/guides/tests/test_views.py +++ /dev/null @@ -1,104 +0,0 @@ -from unittest.mock import patch - -from django.http import Http404 -from django.test import TestCase -from django_hosts.resolvers import reverse - - -class TestGuidesIndexView(TestCase): - @patch("pydis_site.apps.guides.views.guides.get_guides") - @patch("pydis_site.apps.guides.views.guides.get_categories") - def test_guides_index_return_200(self, get_categories_mock, get_guides_mock): - """Check that guides index return HTTP code 200.""" - get_categories_mock.return_value = {} - get_guides_mock.return_value = {} - - url = reverse('guide:guides') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - get_guides_mock.assert_called_once() - get_categories_mock.assert_called_once() - - -class TestGuideView(TestCase): - @patch("pydis_site.apps.guides.views.guide.os.path.getmtime") - @patch("pydis_site.apps.guides.views.guide.get_guide") - @patch("pydis_site.apps.guides.views.guide.get_category") - def test_guide_return_code_200(self, get_category_mock, get_guide_mock, get_time_mock): - get_guide_mock.return_value = {"guide": "test", "metadata": {}} - - url = reverse("guide:guide", args=["test-guide"]) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - get_category_mock.assert_not_called() - get_guide_mock.assert_called_once_with("test-guide", None) - - @patch("pydis_site.apps.guides.views.guide.os.path.getmtime") - @patch("pydis_site.apps.guides.views.guide.get_guide") - @patch("pydis_site.apps.guides.views.guide.get_category") - def test_guide_return_404(self, get_category_mock, get_guide_mock, get_time_mock): - """Check that return code is 404 when invalid guide provided.""" - get_guide_mock.side_effect = Http404("Guide not found.") - - url = reverse("guide:guide", args=["invalid-guide"]) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - get_guide_mock.assert_called_once_with("invalid-guide", None) - get_category_mock.assert_not_called() - - -class TestCategoryView(TestCase): - @patch("pydis_site.apps.guides.views.category.get_category") - @patch("pydis_site.apps.guides.views.category.get_guides") - def test_valid_category_code_200(self, get_guides_mock, get_category_mock): - """Check that return code is 200 when visiting valid category.""" - get_category_mock.return_value = {"name": "test", "description": "test"} - get_guides_mock.return_value = {} - - url = reverse("guide:category", args=["category"]) - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - get_guides_mock.assert_called_once_with("category") - get_category_mock.assert_called_once_with("category") - - @patch("pydis_site.apps.guides.views.category.get_category") - @patch("pydis_site.apps.guides.views.category.get_guides") - def test_invalid_category_code_404(self, get_guides_mock, get_category_mock): - """Check that return code is 404 when trying to visit invalid category.""" - get_category_mock.side_effect = Http404("Category not found.") - - url = reverse("guide:category", args=["invalid-category"]) - response = self.client.get(url) - - self.assertEqual(response.status_code, 404) - get_category_mock.assert_called_once_with("invalid-category") - get_guides_mock.assert_not_called() - - -class TestCategoryGuidesView(TestCase): - @patch("pydis_site.apps.guides.views.guide.os.path.getmtime") - @patch("pydis_site.apps.guides.views.guide.get_guide") - @patch("pydis_site.apps.guides.views.guide.get_category") - def test_valid_category_guide_code_200(self, get_category_mock, get_guide_mock, get_time_mock): - """Check that return code is 200 when visiting valid category article.""" - get_guide_mock.return_value = {"guide": "test", "metadata": {}} - - url = reverse("guide:category_guide", args=["category", "test3"]) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - get_guide_mock.assert_called_once_with("test3", "category") - get_category_mock.assert_called_once_with("category") - - @patch("pydis_site.apps.guides.views.guide.os.path.getmtime") - @patch("pydis_site.apps.guides.views.guide.get_guide") - @patch("pydis_site.apps.guides.views.guide.get_category") - def test_invalid_category_guide_code_404(self, get_category_mock, get_guide_mock, get_time_mock): - """Check that return code is 200 when trying to visit invalid category article.""" - get_guide_mock.side_effect = Http404("Guide not found.") - - url = reverse("guide:category_guide", args=["category", "invalid"]) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - get_guide_mock.assert_called_once_with("invalid", "category") - get_category_mock.assert_not_called() diff --git a/pydis_site/apps/guides/urls.py b/pydis_site/apps/guides/urls.py deleted file mode 100644 index 69641638..00000000 --- a/pydis_site/apps/guides/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path - -from . import views - -app_name = "guides" -urlpatterns = [ - path("", views.GuidesView.as_view(), name='guides'), - path("category//", views.CategoryView.as_view(), name='category'), - path("category///", views.GuideView.as_view(), name='category_guide'), - path("/", views.GuideView.as_view(), name='guide') -] diff --git a/pydis_site/apps/guides/utils.py b/pydis_site/apps/guides/utils.py deleted file mode 100644 index c6f668f7..00000000 --- a/pydis_site/apps/guides/utils.py +++ /dev/null @@ -1,77 +0,0 @@ -import os -from typing import Dict, Optional, Union - -import yaml -from django.conf import settings -from django.http import Http404 -from markdown import Markdown - - -def _get_base_path() -> str: - """Have extra function for base path getting for testability.""" - return os.path.join(settings.BASE_DIR, "pydis_site", "apps", "guides", "resources", "guides") - - -def get_category(category: str) -> Dict[str, str]: - """Load category information by name from _info.yml.""" - path = os.path.join(_get_base_path(), category) - if not os.path.exists(path) or not os.path.isdir(path): - raise Http404("Category not found.") - - with open(os.path.join(path, "_info.yml")) as f: - return yaml.safe_load(f.read()) - - -def get_categories() -> Dict[str, Dict]: - """Get all categories information.""" - base_path = _get_base_path() - categories = {} - - for name in os.listdir(base_path): - if os.path.isdir(os.path.join(base_path, name)): - categories[name] = get_category(name) - - return categories - - -def get_guides(category: Optional[str] = None) -> Dict[str, Dict]: - """Get all root guides when category is not specified. Otherwise get all this category guides.""" - if category is None: - base_dir = _get_base_path() - else: - base_dir = os.path.join(_get_base_path(), category) - - guides = {} - - for filename in os.listdir(base_dir): - full_path = os.path.join(base_dir, filename) - if os.path.isfile(full_path) and filename.endswith(".md"): - md = Markdown(extensions=['meta']) - with open(full_path) as f: - md.convert(f.read()) - - guides[os.path.splitext(filename)[0]] = md.Meta - - return guides - - -def get_guide(guide: str, category: Optional[str]) -> Dict[str, Union[str, Dict]]: - """Get one specific guide. When category is specified, get it from there.""" - if category is None: - base_path = _get_base_path() - else: - base_path = os.path.join(_get_base_path(), category) - - if not os.path.exists(base_path) or not os.path.isdir(base_path): - raise Http404("Category not found.") - - guide_path = os.path.join(base_path, f"{guide}.md") - if not os.path.exists(guide_path) or not os.path.isfile(guide_path): - raise Http404("Guide not found.") - - md = Markdown(extensions=['meta', 'attr_list', 'fenced_code']) - - with open(guide_path) as f: - html = md.convert(f.read()) - - return {"guide": html, "metadata": md.Meta} diff --git a/pydis_site/apps/guides/views/__init__.py b/pydis_site/apps/guides/views/__init__.py deleted file mode 100644 index 17a244c1..00000000 --- a/pydis_site/apps/guides/views/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .category import CategoryView -from .guide import GuideView -from .guides import GuidesView - -__all__ = ["GuideView", "GuidesView", "CategoryView"] diff --git a/pydis_site/apps/guides/views/category.py b/pydis_site/apps/guides/views/category.py deleted file mode 100644 index 33e8c97b..00000000 --- a/pydis_site/apps/guides/views/category.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.core.handlers.wsgi import WSGIRequest -from django.http import HttpResponse -from django.shortcuts import render -from django.views import View - -from pydis_site.apps.guides.utils import get_category, get_guides - - -class CategoryView(View): - """Handles guides category page.""" - - def get(self, request: WSGIRequest, category: str) -> HttpResponse: - """Handles page that displays category guides.""" - return render( - request, - "guides/category.html", - {"category_info": get_category(category), "guides": get_guides(category), "category_name": category} - ) diff --git a/pydis_site/apps/guides/views/guide.py b/pydis_site/apps/guides/views/guide.py deleted file mode 100644 index bcd68bc4..00000000 --- a/pydis_site/apps/guides/views/guide.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -from datetime import datetime -from typing import Optional - -from django.conf import settings -from django.core.handlers.wsgi import WSGIRequest -from django.http import HttpResponse -from django.shortcuts import render -from django.views import View - -from pydis_site.apps.guides.utils import get_category, get_guide - - -class GuideView(View): - """Shows specific guide page.""" - - def get(self, request: WSGIRequest, guide: str, category: Optional[str] = None) -> HttpResponse: - """Collect guide content and display it. When guide don't exist, return 404.""" - guide_result = get_guide(guide, category) - - if category is not None: - path = os.path.join( - settings.BASE_DIR, "pydis_site", "apps", "guides", "resources", "guides", category, f"{guide}.md" - ) - else: - path = os.path.join(settings.BASE_DIR, "pydis_site", "apps", "guides", "resources", "guides", f"{guide}.md") - - if category is not None: - category_data = get_category(category) - category_data["raw_name"] = category - else: - category_data = {"name": None, "raw_name": None} - - return render( - request, - "guides/guide.html", - { - "guide": guide_result, - "last_modified": datetime.fromtimestamp(os.path.getmtime(path)).strftime("%dth %B %Y"), - "category_data": category_data, - "relevant_links": { - link: value for link, value in zip( - guide_result["metadata"].get("relevantlinks", []), - guide_result["metadata"].get("relevantlinkvalues", []) - ) - } - } - ) diff --git a/pydis_site/apps/guides/views/guides.py b/pydis_site/apps/guides/views/guides.py deleted file mode 100644 index bb8b565e..00000000 --- a/pydis_site/apps/guides/views/guides.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.core.handlers.wsgi import WSGIRequest -from django.http import HttpResponse -from django.shortcuts import render -from django.views import View - -from pydis_site.apps.guides.utils import get_categories, get_guides - - -class GuidesView(View): - """Shows all guides and categories.""" - - def get(self, request: WSGIRequest) -> HttpResponse: - """Shows all guides and categories.""" - return render(request, "guides/guides.html", {"guides": get_guides(), "categories": get_categories()}) diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index 06d62352..e49fd4e0 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -39,5 +39,5 @@ urlpatterns = [ path('admin/', admin.site.urls), path('notifications/', include('django_nyt.urls')), - path('guides/', include('pydis_site.apps.guides.urls', namespace='guide')), + path('content/', include('pydis_site.apps.content.urls', namespace='content')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 6aff0fc1..aaac3bfe 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -91,7 +91,7 @@ INSTALLED_APPS = [ 'pydis_site.apps.api', 'pydis_site.apps.home', 'pydis_site.apps.staff', - 'pydis_site.apps.guides', + 'pydis_site.apps.content', 'django.contrib.admin', 'django.contrib.auth', diff --git a/pydis_site/static/css/content/articles.css b/pydis_site/static/css/content/articles.css new file mode 100644 index 00000000..fa7a0ba5 --- /dev/null +++ b/pydis_site/static/css/content/articles.css @@ -0,0 +1,7 @@ +.breadcrumb-section { + padding: 1rem; +} + +i.has-icon-padding { + padding: 0 10px 25px 0; +} diff --git a/pydis_site/static/css/guides/guide.css b/pydis_site/static/css/guides/guide.css deleted file mode 100644 index fa7a0ba5..00000000 --- a/pydis_site/static/css/guides/guide.css +++ /dev/null @@ -1,7 +0,0 @@ -.breadcrumb-section { - padding: 1rem; -} - -i.has-icon-padding { - padding: 0 10px 25px 0; -} diff --git a/pydis_site/templates/content/article.html b/pydis_site/templates/content/article.html new file mode 100644 index 00000000..de6cd28d --- /dev/null +++ b/pydis_site/templates/content/article.html @@ -0,0 +1,61 @@ +{% extends 'base/base.html' %} +{% load static %} + +{% block title %}{{ metadata.title|first }}{% endblock %} +{% block head %} + + + + + + + +{% endblock %} + +{% block content %} + {% include "base/navbar.html" %} + + + +
    +
    +
    +

    {{ article.metadata.title|first }}

    +
    +
    + {{ article.article|safe }} +

    + Last modified: {{ last_modified }}
    + Contributors: {{ article.metadata.contributors|join:", " }} +

    +
    +
    + {% if relevant_links|length > 0 %} +
    + + +
    + {% endif %} +
    +
    +
    +
    +
    + +{% endblock %} diff --git a/pydis_site/templates/content/articles.html b/pydis_site/templates/content/articles.html new file mode 100644 index 00000000..6fea66e5 --- /dev/null +++ b/pydis_site/templates/content/articles.html @@ -0,0 +1,53 @@ +{% extends 'base/base.html' %} +{% load static %} + +{% block title %}Guides{% endblock %} +{% block head %} + +{% endblock %} + +{% block content %} + {% include "base/navbar.html" %} + + + +
    +
    +
    +

    Articles

    + {% for article, data in content.items %} +
    + + + + + {{ data.title.0 }} + +

    {{ data.shortdescription.0 }}

    +
    + {% endfor %} + {% for category, data in categories.items %} +
    + + + + + + + {{ data.name }} + +

    {{ data.description }}

    +
    + {% endfor %} +
    +
    +
    +{% endblock %} diff --git a/pydis_site/templates/content/category.html b/pydis_site/templates/content/category.html new file mode 100644 index 00000000..61e20c43 --- /dev/null +++ b/pydis_site/templates/content/category.html @@ -0,0 +1,44 @@ +{% extends 'base/base.html' %} +{% load static %} + +{% block title %}{{ category_info.name }}{% endblock %} +{% block head %} + + + + +{% endblock %} + +{% block content %} + {% include "base/navbar.html" %} + + + +
    +
    +
    +

    {{ category_info.name }}

    + {% for article, data in content.items %} +
    + + + + + {{ data.title.0 }} + +

    {{ data.shortdescription.0 }}

    +
    + {% endfor %} +
    +
    +
    +{% endblock %} diff --git a/pydis_site/templates/guides/category.html b/pydis_site/templates/guides/category.html deleted file mode 100644 index f3a8c2ce..00000000 --- a/pydis_site/templates/guides/category.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'base/base.html' %} -{% load static %} - -{% block title %}{{ category_info.name }}{% endblock %} -{% block head %} - - - - -{% endblock %} - -{% block content %} - {% include "base/navbar.html" %} - - - -
    -
    -
    -

    {{ category_info.name }}

    - {% for guide, data in guides.items %} -
    - - - - - {{ data.title.0 }} - -

    {{ data.shortdescription.0 }}

    -
    - {% endfor %} -
    -
    -
    -{% endblock %} diff --git a/pydis_site/templates/guides/guide.html b/pydis_site/templates/guides/guide.html deleted file mode 100644 index 97fc8262..00000000 --- a/pydis_site/templates/guides/guide.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends 'base/base.html' %} -{% load static %} - -{% block title %}{{ metadata.title|first }}{% endblock %} -{% block head %} - - - - - - - -{% endblock %} - -{% block content %} - {% include "base/navbar.html" %} - - - -
    -
    -
    -

    {{ guide.metadata.title|first }}

    -
    -
    - {{ guide.guide|safe }} -

    - Last modified: {{ last_modified }}
    - Contributors: {{ guide.metadata.contributors|join:", " }} -

    -
    -
    - {% if relevant_links|length > 0 %} -
    - - -
    - {% endif %} -
    -
    -
    -
    -
    - -{% endblock %} diff --git a/pydis_site/templates/guides/guides.html b/pydis_site/templates/guides/guides.html deleted file mode 100644 index 0e6f2073..00000000 --- a/pydis_site/templates/guides/guides.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends 'base/base.html' %} -{% load static %} - -{% block title %}Guides{% endblock %} -{% block head %} - -{% endblock %} - -{% block content %} - {% include "base/navbar.html" %} - - - -
    -
    -
    -

    Guides

    - {% for guide, data in guides.items %} -
    - - - - - {{ data.title.0 }} - -

    {{ data.shortdescription.0 }}

    -
    - {% endfor %} - {% for category, data in categories.items %} -
    - - - - - - - {{ data.name }} - -

    {{ data.description }}

    -
    - {% endfor %} -
    -
    -
    -{% endblock %} -- cgit v1.2.3 From a2357c087239cfd5bcdd7c77e1d3bdee25ea833b Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 4 Oct 2020 20:04:53 +0200 Subject: Remove files related to django-allauth. --- .mdlrc | 1 - pydis_site/apps/home/apps.py | 38 -- pydis_site/apps/home/forms/__init__.py | 0 pydis_site/apps/home/forms/account_deletion.py | 10 - pydis_site/apps/home/signals.py | 314 -------------- pydis_site/apps/home/tests/test_signal_listener.py | 458 --------------------- pydis_site/apps/home/views/account/__init__.py | 4 - pydis_site/apps/home/views/account/delete.py | 37 -- pydis_site/apps/home/views/account/settings.py | 59 --- pydis_site/apps/staff/admin.py | 6 - pydis_site/apps/staff/models/__init__.py | 3 - pydis_site/apps/staff/models/role_mapping.py | 31 -- pydis_site/static/css/base/notification.css | 99 ----- pydis_site/static/js/base/modal.js | 100 ----- pydis_site/templates/home/account/delete.html | 47 --- pydis_site/templates/home/account/settings.html | 136 ------ pydis_site/tests/__init__.py | 0 pydis_site/tests/test_utils_account.py | 139 ------- pydis_site/utils/account.py | 79 ---- pydis_site/utils/views.py | 25 -- 20 files changed, 1586 deletions(-) delete mode 100644 .mdlrc delete mode 100644 pydis_site/apps/home/apps.py delete mode 100644 pydis_site/apps/home/forms/__init__.py delete mode 100644 pydis_site/apps/home/forms/account_deletion.py delete mode 100644 pydis_site/apps/home/signals.py delete mode 100644 pydis_site/apps/home/tests/test_signal_listener.py delete mode 100644 pydis_site/apps/home/views/account/__init__.py delete mode 100644 pydis_site/apps/home/views/account/delete.py delete mode 100644 pydis_site/apps/home/views/account/settings.py delete mode 100644 pydis_site/apps/staff/admin.py delete mode 100644 pydis_site/apps/staff/models/__init__.py delete mode 100644 pydis_site/apps/staff/models/role_mapping.py delete mode 100644 pydis_site/static/css/base/notification.css delete mode 100644 pydis_site/static/js/base/modal.js delete mode 100644 pydis_site/templates/home/account/delete.html delete mode 100644 pydis_site/templates/home/account/settings.html delete mode 100644 pydis_site/tests/__init__.py delete mode 100644 pydis_site/tests/test_utils_account.py delete mode 100644 pydis_site/utils/account.py delete mode 100644 pydis_site/utils/views.py (limited to 'pydis_site/static/css') 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/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/__init__.py b/pydis_site/apps/home/forms/__init__.py deleted file mode 100644 index e69de29b..00000000 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/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/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/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/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/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/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/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/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" %} - -
    -
    -

    Account Deletion

    - -
    -
    - -
    -
    -

    - You have requested to delete the account with username - {{ user.username }}. -

    - -

    - Please note that this cannot be undone. -

    - -

    - To verify that you'd like to remove your account, please type your username into the box below. -

    -
    -
    -
    -
    - -
    -
    -
    - {% csrf_token %} - - - -
    -
    -
    -
    -
    -{% 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. #} - - diff --git a/pydis_site/tests/__init__.py b/pydis_site/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 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) -- cgit v1.2.3 From 2535d2285e1b6f130425ca43e1d22f11528c5017 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 8 Oct 2020 19:02:20 +0300 Subject: Remove resources index breadcrumb CSS --- pydis_site/static/css/resources/resources.css | 4 ---- 1 file changed, 4 deletions(-) (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css index 025b28c6..cf4cb472 100644 --- a/pydis_site/static/css/resources/resources.css +++ b/pydis_site/static/css/resources/resources.css @@ -27,7 +27,3 @@ #podcastsBlock { background-image: linear-gradient(141deg, #232382 0%, #30309c 71%, #4343ad 100%); } - -.breadcrumb-section { - padding: 1rem; -} -- cgit v1.2.3 From 2a502d06a251868eb9c2fb41bf736c11182161b8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 26 Oct 2020 19:44:27 +0200 Subject: Create CSS file for events pages --- pydis_site/static/css/events/base.css | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pydis_site/static/css/events/base.css (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/events/base.css b/pydis_site/static/css/events/base.css new file mode 100644 index 00000000..b4f233f7 --- /dev/null +++ b/pydis_site/static/css/events/base.css @@ -0,0 +1,3 @@ +.breadcrumb-section { + padding: 1rem; +} -- cgit v1.2.3 From cdd1e7b9bcabf8acd5c6c5974c89d36704a52f73 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 14 Nov 2020 11:46:46 +0200 Subject: Create CSS for resources list --- pydis_site/static/css/resources/resources_list.css | 75 ++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 pydis_site/static/css/resources/resources_list.css (limited to 'pydis_site/static/css') 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..735421fa --- /dev/null +++ b/pydis_site/static/css/resources/resources_list.css @@ -0,0 +1,75 @@ +.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; +} + +.payment-free, .payment-optional { + color: dimgray; +} + +.payment-free:hover, .payment-optional:hover { + color: gray; +} + +.payment-paid { + color: darkgoldenrod; +} + +.payment-paid:hover { + color: goldenrod; +} + +.tooltip:hover:before { + display: block; +} -- cgit v1.2.3 From e4e5268122650679f6106b6241109ebc2d930d52 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 13 Mar 2021 19:59:50 +0000 Subject: master => main --- .github/workflows/build.yaml | 2 +- .github/workflows/deploy.yaml | 2 +- .github/workflows/lint-test.yaml | 2 +- .github/workflows/sentry-release.yml | 4 ++-- CONTRIBUTING.md | 6 +++--- README.md | 14 +++++++------- pydis_site/static/css/error_pages.css | 2 +- pydis_site/templates/home/timeline.html | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) (limited to 'pydis_site/static/css') diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d113cff7..873bcda4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,7 +4,7 @@ on: workflow_run: workflows: ["Lint & Test"] branches: - - master + - main types: - completed diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index ff2652fd..8abf2bfb 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,7 +4,7 @@ on: workflow_run: workflows: ["Build"] branches: - - master + - main types: - completed diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml index 397c2085..9e3d331d 100644 --- a/.github/workflows/lint-test.yaml +++ b/.github/workflows/lint-test.yaml @@ -3,7 +3,7 @@ name: Lint & Test on: push: branches: - - master + - main pull_request: diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml index 01ed1daf..a3df5b1a 100644 --- a/.github/workflows/sentry-release.yml +++ b/.github/workflows/sentry-release.yml @@ -3,14 +3,14 @@ name: Create Sentry release on: push: branches: - - master + - main jobs: createSentryRelease: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@master + uses: actions/checkout@main - name: Create a Sentry.io release uses: tclindner/sentry-releases-action@v1.2.0 env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de682a31..84a59d54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to one of Our Projects -Our projects are open-source and are automatically deployed whenever commits are pushed to the `master` branch on each repository, so we've created a set of guidelines in order to keep everything clean and in working order. +Our projects are open-source and are automatically deployed whenever commits are pushed to the `main` branch on each repository, so we've created a set of guidelines in order to keep everything clean and in working order. Note that contributions may be rejected on the basis of a contributor failing to follow these guidelines. @@ -8,7 +8,7 @@ Note that contributions may be rejected on the basis of a contributor failing to 1. **No force-pushes** or modifying the Git history in any way. 2. If you have direct access to the repository, **create a branch for your changes** and create a pull request for that branch. If not, create a branch on a fork of the repository and create a pull request from there. - * It's common practice for a repository to reject direct pushes to `master`, so make branching a habit! + * It's common practice for a repository to reject direct pushes to `main`, so make branching a habit! * If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This gives permission for maintainers to commit changes directly to your fork, speeding up the review process. 3. **Adhere to the prevailing code style**, which we enforce using [`flake8`](http://flake8.pycqa.org/en/latest/index.html) and [`pre-commit`](https://pre-commit.com/). * Run `flake8` and `pre-commit` against your code [**before** you push it](https://soundcloud.com/lemonsaurusrex/lint-before-you-push). Your commit will be rejected by the build server if it fails to lint. @@ -18,7 +18,7 @@ Note that contributions may be rejected on the basis of a contributor failing to * Avoid making minor commits for fixing typos or linting errors. Since you've already set up a `pre-commit` hook to run the linting pipeline before a commit, you shouldn't be committing linting issues anyway. * 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/) 5. **Avoid frequent pushes to the main repository**. This goes for PRs opened against your fork as well. Our test build pipelines are triggered every time a push to the repository (or PR) is made. Try to batch your commits until you've finished working for that session, or you've reached a point where collaborators need your commits to continue their own work. This also provides you the opportunity to amend commits for minor changes rather than having to commit them on their own because you've already pushed. - * This includes merging master into your branch. Try to leave merging from master for after your PR passes review; a maintainer will bring your PR up to date before merging. Exceptions to this include: resolving merge conflicts, needing something that was pushed to master for your branch, or something was pushed to master that could potentionally affect the functionality of what you're writing. + * This includes merging main into your branch. Try to leave merging from main for after your PR passes review; a maintainer will bring your PR up to date before merging. Exceptions to this include: resolving merge conflicts, needing something that was pushed to main for your branch, or something was pushed to main that could potentionally affect the functionality of what you're writing. 6. **Don't fight the framework**. Every framework has its flaws, but the frameworks we've picked out have been carefully chosen for their particular merits. If you can avoid it, please resist reimplementing swathes of framework logic - the work has already been done for you! 7. If someone is working on an issue or pull request, **do not open your own pull request for the same task**. Instead, collaborate with the author(s) of the existing pull request. Duplicate PRs opened without communicating with the other author(s) and/or PyDis staff will be closed. Communication is key, and there's no point in two separate implementations of the same thing. * One option is to fork the other contributor's repository and submit your changes to their branch with your own pull request. We suggest following these guidelines when interacting with their repository as well. diff --git a/README.md b/README.md index a1eeb9fb..f88c2cf7 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ If you happen to run into issues with setup, please don't hesitate to open an is If you're looking to contribute or play around with the code, take a look at [the wiki][8] or the [`docs` directory](docs). If you're looking for things to do, check out [our issues][9]. -[1]: https://github.com/python-discord/site/workflows/Lint%20&%20Test/badge.svg?branch=master -[2]: https://github.com/python-discord/site/actions?query=workflow%3A%22Lint+%26+Test%22+branch%3Amaster -[3]: https://github.com/python-discord/site/workflows/Build%20&%20Deploy/badge.svg?branch=master -[4]: https://github.com/python-discord/site/actions?query=workflow%3A%22Build+%26+Deploy%22+branch%3Amaster -[5]: https://coveralls.io/repos/github/python-discord/site/badge.svg?branch=master -[6]: https://coveralls.io/github/python-discord/site?branch=master +[1]: https://github.com/python-discord/site/workflows/Lint%20&%20Test/badge.svg?branch=main +[2]: https://github.com/python-discord/site/actions?query=workflow%3A%22Lint+%26+Test%22+branch%3Amain +[3]: https://github.com/python-discord/site/workflows/Build%20&%20Deploy/badge.svg?branch=main +[4]: https://github.com/python-discord/site/actions?query=workflow%3A%22Build+%26+Deploy%22+branch%3Amain +[5]: https://coveralls.io/repos/github/python-discord/site/badge.svg?branch=main +[6]: https://coveralls.io/github/python-discord/site?branch=main [7]: https://pythondiscord.com [8]: https://pythondiscord.com/pages/contributing/site/ [9]: https://github.com/python-discord/site/issues -[10]: https://raw.githubusercontent.com/python-discord/branding/master/logos/badge/badge_github.svg +[10]: https://raw.githubusercontent.com/python-discord/branding/main/logos/badge/badge_github.svg [11]: https://discord.gg/python diff --git a/pydis_site/static/css/error_pages.css b/pydis_site/static/css/error_pages.css index 77bb7e2b..ee41fa5c 100644 --- a/pydis_site/static/css/error_pages.css +++ b/pydis_site/static/css/error_pages.css @@ -4,7 +4,7 @@ html { body { background-color: #7289DA; - background-image: url("https://raw.githubusercontent.com/python-discord/branding/master/logos/banner_pattern/banner_pattern.svg"); + background-image: url("https://raw.githubusercontent.com/python-discord/branding/main/logos/banner_pattern/banner_pattern.svg"); background-size: 128px; font-family: "Hind", "Helvetica", "Arial", sans-serif; display: flex; diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index f3c58fc2..ece2e4e5 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -54,7 +54,7 @@

    Our logo is born. Thanks @Aperture!

    + src="https://raw.githubusercontent.com/python-discord/branding/main/logos/logo_banner/logo_site_banner.svg">

    -- cgit v1.2.3 From 15e301d7a539e15b473d896c24609bc10f6ff3d8 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Mon, 22 Mar 2021 22:58:04 +0800 Subject: Style
     tag backgrounds the same as .
    
    This makes it consistent with the django-wiki version of the code
    blocks, which looks neater.
    ---
     pydis_site/static/css/events/base.css | 9 +++++++++
     1 file changed, 9 insertions(+)
    
    (limited to 'pydis_site/static/css')
    
    diff --git a/pydis_site/static/css/events/base.css b/pydis_site/static/css/events/base.css
    index b4f233f7..266bca1d 100644
    --- a/pydis_site/static/css/events/base.css
    +++ b/pydis_site/static/css/events/base.css
    @@ -1,3 +1,12 @@
     .breadcrumb-section {
         padding: 1rem;
     }
    +
    +pre {
    +    /*
    +     * Style it the same as the  tag, since highlight.js does not style
    +     * backgrounds of 
     tags but bulma does, resulting in a weird off-white
    +     * border.
    +     */
    +    background-color: #282c34;
    +}
    -- 
    cgit v1.2.3
    
    
    From 3d32585d8433252cc78115a744a8117c7210f3ad Mon Sep 17 00:00:00 2001
    From: kosayoda 
    Date: Tue, 23 Mar 2021 16:00:07 +0800
    Subject: Style 
     tag backgrounds same as .
    
    This makes it consistent with the django-wiki version of the code
    blocks, which looks neater.
    ---
     pydis_site/static/css/content/articles.css | 9 +++++++++
     1 file changed, 9 insertions(+)
    
    (limited to 'pydis_site/static/css')
    
    diff --git a/pydis_site/static/css/content/articles.css b/pydis_site/static/css/content/articles.css
    index fa7a0ba5..f46d6b15 100644
    --- a/pydis_site/static/css/content/articles.css
    +++ b/pydis_site/static/css/content/articles.css
    @@ -5,3 +5,12 @@
     i.has-icon-padding {
         padding: 0 10px 25px 0;
     }
    +
    +pre {
    +    /*
    +     * Style it the same as the  tag, since highlight.js does not style
    +     * backgrounds of 
     tags but bulma does, resulting in a weird off-white
    +     * border.
    +     */
    +    background-color: #282c34;
    +}
    -- 
    cgit v1.2.3
    
    
    From 7e05e56d7e650adbaf2e8f552408368d1ff55521 Mon Sep 17 00:00:00 2001
    From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com>
    Date: Tue, 23 Mar 2021 13:09:46 +0300
    Subject: Centers Sponsor Logos
    
    Center justifies sponsor logos on the home page to make wrapping look cleaner.
    ---
     pydis_site/static/css/home/index.css | 6 +++++-
     1 file changed, 5 insertions(+), 1 deletion(-)
    
    (limited to 'pydis_site/static/css')
    
    diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css
    index 58ca8888..106feb7c 100644
    --- a/pydis_site/static/css/home/index.css
    +++ b/pydis_site/static/css/home/index.css
    @@ -208,7 +208,11 @@ h1 {
         padding-bottom: 3rem;
     }
     
    +#sponsors .columns {
    +    justify-content: center;
    +}
    +
     #sponsors img {
         height: 5rem;
    -    margin-right: 2rem;
    +    margin: auto 1rem;
     }
    -- 
    cgit v1.2.3
    
    
    From 367ba267c4dbc6d406922f80bc4337ee9a0139a0 Mon Sep 17 00:00:00 2001
    From: kosayoda 
    Date: Tue, 23 Mar 2021 18:24:56 +0800
    Subject: Rename `articles` to `pages`.
    
    Articles was a good name, but we want an `articles` category in
    the future. `/pages/guides/` and `/pages/articles/` are clearer in name
    than `/articles/guides/` and `/articles/articles/`.
    ---
     pydis_site/apps/content/tests/test_utils.py       |  70 ++++++------
     pydis_site/apps/content/tests/test_views.py       | 127 +++++++++++-----------
     pydis_site/apps/content/urls.py                   |   4 +-
     pydis_site/apps/content/utils.py                  |  36 +++---
     pydis_site/apps/content/views/__init__.py         |   6 +-
     pydis_site/apps/content/views/article_category.py |  69 ------------
     pydis_site/apps/content/views/articles.py         |  16 ---
     pydis_site/apps/content/views/page_category.py    |  69 ++++++++++++
     pydis_site/apps/content/views/pages.py            |  16 +++
     pydis_site/apps/events/urls.py                    |   4 +-
     pydis_site/apps/events/views/__init__.py          |   4 +-
     pydis_site/apps/events/views/page.py              |   2 +-
     pydis_site/apps/home/urls.py                      |   2 +-
     pydis_site/settings.py                            |   2 +-
     pydis_site/static/css/content/articles.css        |  16 ---
     pydis_site/static/css/content/page.css            |  16 +++
     pydis_site/templates/content/article.html         |  58 ----------
     pydis_site/templates/content/listing.html         |  20 ++--
     pydis_site/templates/content/page.html            |  58 ++++++++++
     pydis_site/templates/resources/resources.html     |   2 +-
     20 files changed, 299 insertions(+), 298 deletions(-)
     delete mode 100644 pydis_site/apps/content/views/article_category.py
     delete mode 100644 pydis_site/apps/content/views/articles.py
     create mode 100644 pydis_site/apps/content/views/page_category.py
     create mode 100644 pydis_site/apps/content/views/pages.py
     delete mode 100644 pydis_site/static/css/content/articles.css
     create mode 100644 pydis_site/static/css/content/page.css
     delete mode 100644 pydis_site/templates/content/article.html
     create mode 100644 pydis_site/templates/content/page.html
    
    (limited to 'pydis_site/static/css')
    
    diff --git a/pydis_site/apps/content/tests/test_utils.py b/pydis_site/apps/content/tests/test_utils.py
    index 26b2bba9..e1f1b92a 100644
    --- a/pydis_site/apps/content/tests/test_utils.py
    +++ b/pydis_site/apps/content/tests/test_utils.py
    @@ -13,20 +13,20 @@ BASE_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "content", "tests", "t
     
     
     class TestGetCategory(TestCase):
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    +    @override_settings(PAGES_PATH=BASE_PATH)
         def test_get_category_successfully(self):
             """Check does this get right data from category data file."""
             result = utils.get_category(["category"])
     
             self.assertEqual(result, {"name": "My Category", "description": "My Description"})
     
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    +    @override_settings(PAGES_PATH=BASE_PATH)
         def test_get_category_not_exists(self):
             """Check does this raise 404 error when category don't exists."""
             with self.assertRaises(Http404):
                 utils.get_category(["invalid"])
     
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    +    @override_settings(PAGES_PATH=BASE_PATH)
         def test_get_category_not_directory(self):
             """Check does this raise 404 error when category isn't directory."""
             with self.assertRaises(Http404):
    @@ -34,7 +34,7 @@ class TestGetCategory(TestCase):
     
     
     class TestGetCategories(TestCase):
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    +    @override_settings(PAGES_PATH=BASE_PATH)
         @patch("pydis_site.apps.content.utils.get_category")
         def test_get_categories(self, get_category_mock):
             """Check does this return test content categories."""
    @@ -47,7 +47,7 @@ class TestGetCategories(TestCase):
                 result, {"category": {"name": "My Category", "description": "My Description"}}
             )
     
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    +    @override_settings(PAGES_PATH=BASE_PATH)
         def test_get_categories_root_path(self):
             """Check does this doesn't call joinpath when getting root categories."""
             result = utils.get_categories()
    @@ -55,7 +55,7 @@ class TestGetCategories(TestCase):
                 result, {"category": {"name": "My Category", "description": "My Description"}}
             )
     
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    +    @override_settings(PAGES_PATH=BASE_PATH)
         def test_get_categories_in_category(self):
             """Check does this call joinpath when getting subcategories."""
             result = utils.get_categories(["category"])
    @@ -64,11 +64,11 @@ class TestGetCategories(TestCase):
             )
     
     
    -class TestGetArticles(TestCase):
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_get_all_root_articles(self):
    +class TestGetPages(TestCase):
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_get_all_root_pages(self):
             """Check does this return all root level testing content."""
    -        result = utils.get_articles()
    +        result = utils.get_pages()
     
             for case in ["test", "test2"]:
                 with self.subTest(guide=case):
    @@ -77,10 +77,10 @@ class TestGetArticles(TestCase):
                     self.assertIn(case, result)
                     self.assertEqual(md.metadata, result[case])
     
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_get_all_category_articles(self):
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_get_all_category_pages(self):
             """Check does this return all category testing content."""
    -        result = utils.get_articles(["category"])
    +        result = utils.get_pages(["category"])
     
             md = markdown(BASE_PATH.joinpath("category", "test3.md").read_text(), extras=["metadata"])
     
    @@ -88,11 +88,11 @@ class TestGetArticles(TestCase):
             self.assertEqual(md.metadata, result["test3"])
     
     
    -class TestGetArticle(TestCase):
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_get_root_article_success(self):
    -        """Check does this return article HTML and metadata when root article exist."""
    -        result = utils.get_article(["test"])
    +class TestGetPage(TestCase):
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_get_root_page_success(self):
    +        """Check does this return page HTML and metadata when root page exist."""
    +        result = utils.get_page(["test"])
     
             md = markdown(
                 BASE_PATH.joinpath("test.md").read_text(),
    @@ -107,18 +107,18 @@ class TestGetArticle(TestCase):
                 ]
             )
     
    -        self.assertEqual(result, {"article": str(md), "metadata": md.metadata})
    +        self.assertEqual(result, {"page": str(md), "metadata": md.metadata})
     
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_get_root_article_dont_exist(self):
    -        """Check does this raise Http404 when root article don't exist."""
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_get_root_page_dont_exist(self):
    +        """Check does this raise Http404 when root page don't exist."""
             with self.assertRaises(Http404):
    -            utils.get_article(["invalid"])
    +            utils.get_page(["invalid"])
     
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_get_category_article_success(self):
    -        """Check does this return article HTML and metadata when category guide exist."""
    -        result = utils.get_article(["category", "test3"])
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_get_category_page_success(self):
    +        """Check does this return page HTML and metadata when category guide exist."""
    +        result = utils.get_page(["category", "test3"])
     
             md = markdown(
                 BASE_PATH.joinpath("category", "test3.md").read_text(),
    @@ -133,16 +133,16 @@ class TestGetArticle(TestCase):
                 ]
             )
     
    -        self.assertEqual(result, {"article": str(md), "metadata": md.metadata})
    +        self.assertEqual(result, {"page": str(md), "metadata": md.metadata})
     
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_get_category_article_dont_exist(self):
    -        """Check does this raise Http404 when category article don't exist."""
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_get_category_page_dont_exist(self):
    +        """Check does this raise Http404 when category page don't exist."""
             with self.assertRaises(Http404):
    -            utils.get_article(["category", "invalid"])
    +            utils.get_page(["category", "invalid"])
     
    -    @patch("pydis_site.settings.ARTICLES_PATH", new=BASE_PATH)
    -    def test_get_category_article_category_dont_exist(self):
    +    @patch("pydis_site.settings.PAGES_PATH", new=BASE_PATH)
    +    def test_get_category_page_category_dont_exist(self):
             """Check does this raise Http404 when category don't exist."""
             with self.assertRaises(Http404):
    -            utils.get_article(["invalid", "some-guide"])
    +            utils.get_page(["invalid", "some-guide"])
    diff --git a/pydis_site/apps/content/tests/test_views.py b/pydis_site/apps/content/tests/test_views.py
    index 55bfb8ea..5ce18afb 100644
    --- a/pydis_site/apps/content/tests/test_views.py
    +++ b/pydis_site/apps/content/tests/test_views.py
    @@ -6,143 +6,144 @@ from django.http import Http404
     from django.test import RequestFactory, TestCase, override_settings
     from django_hosts.resolvers import reverse
     
    -from pydis_site.apps.content.views import ArticleOrCategoryView
    +from pydis_site.apps.content.views import PageOrCategoryView
     
     BASE_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "content", "tests", "test_content")
     
     
    -class TestArticlesIndexView(TestCase):
    -    @patch("pydis_site.apps.content.views.articles.get_articles")
    -    @patch("pydis_site.apps.content.views.articles.get_categories")
    -    def test_articles_index_return_200(self, get_categories_mock, get_articles_mock):
    +class TestPagesIndexView(TestCase):
    +    @patch("pydis_site.apps.content.views.pages.get_pages")
    +    @patch("pydis_site.apps.content.views.pages.get_categories")
    +    def test_pages_index_return_200(self, get_categories_mock, get_page_mock):
             """Check that content index return HTTP code 200."""
             get_categories_mock.return_value = {}
    -        get_articles_mock.return_value = {}
    +        get_page_mock.return_value = {}
     
    -        url = reverse('content:articles')
    +        url = reverse('content:pages')
             response = self.client.get(url)
             self.assertEqual(response.status_code, 200)
    -        get_articles_mock.assert_called_once()
    +        get_page_mock.assert_called_once()
             get_categories_mock.assert_called_once()
     
     
    -class TestArticleOrCategoryView(TestCase):
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_article")
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_category")
    -    def test_article_return_code_200(self, get_category_mock, get_article_mock):
    -        get_article_mock.return_value = {"guide": "test", "metadata": {}}
    +class TestPageOrCategoryView(TestCase):
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_page")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_category")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_github_information")
    +    def test_page_return_code_200(self, get_category_mock, get_page_mock):
    +        get_page_mock.return_value = {"guide": "test", "metadata": {}}
     
    -        url = reverse("content:article_category", args=["test2"])
    +        url = reverse("content:page_category", args=["test2"])
             response = self.client.get(url)
             self.assertEqual(response.status_code, 200)
             get_category_mock.assert_not_called()
    -        get_article_mock.assert_called_once()
    +        get_page_mock.assert_called_once()
     
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_article")
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_category")
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_article_return_404(self, get_category_mock, get_article_mock):
    -        """Check that return code is 404 when invalid article provided."""
    -        get_article_mock.side_effect = Http404("Article not found.")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_page")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_category")
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_page_return_404(self, get_category_mock, get_page_mock):
    +        """Check that return code is 404 when invalid page provided."""
    +        get_page_mock.side_effect = Http404("Page not found.")
     
    -        url = reverse("content:article_category", args=["invalid-guide"])
    +        url = reverse("content:page_category", args=["invalid-guide"])
             response = self.client.get(url)
             self.assertEqual(response.status_code, 404)
    -        get_article_mock.assert_not_called()
    +        get_page_mock.assert_not_called()
             get_category_mock.assert_not_called()
     
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_category")
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_articles")
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_categories")
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_category")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_pages")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_categories")
    +    @override_settings(PAGES_PATH=BASE_PATH)
         def test_valid_category_code_200(
                 self,
                 get_categories_mock,
    -            get_articles_mock,
    +            get_pages_mock,
                 get_category_mock
         ):
             """Check that return code is 200 when visiting valid category."""
             get_category_mock.return_value = {"name": "test", "description": "test"}
    -        get_articles_mock.return_value = {}
    +        get_pages_mock.return_value = {}
     
    -        url = reverse("content:article_category", args=["category"])
    +        url = reverse("content:page_category", args=["category"])
             response = self.client.get(url)
     
             self.assertEqual(response.status_code, 200)
    -        get_articles_mock.assert_called_once()
    +        get_pages_mock.assert_called_once()
             get_category_mock.assert_called_once()
             get_categories_mock.assert_called_once()
     
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_category")
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_articles")
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_categories")
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_category")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_pages")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_categories")
    +    @override_settings(PAGES_PATH=BASE_PATH)
         def test_invalid_category_code_404(
                 self,
                 get_categories_mock,
    -            get_articles_mock,
    +            get_pages_mock,
                 get_category_mock
         ):
             """Check that return code is 404 when trying to visit invalid category."""
             get_category_mock.side_effect = Http404("Category not found.")
     
    -        url = reverse("content:article_category", args=["invalid-category"])
    +        url = reverse("content:page_category", args=["invalid-category"])
             response = self.client.get(url)
     
             self.assertEqual(response.status_code, 404)
             get_category_mock.assert_not_called()
    -        get_articles_mock.assert_not_called()
    +        get_pages_mock.assert_not_called()
             get_categories_mock.assert_not_called()
     
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_article")
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_category")
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_valid_category_article_code_200(
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_page")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_category")
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_valid_category_page_code_200(
                 self,
                 get_category_mock,
    -            get_article_mock
    +            get_page_mock
         ):
    -        """Check that return code is 200 when visiting valid category article."""
    -        get_article_mock.return_value = {"guide": "test", "metadata": {}}
    +        """Check that return code is 200 when visiting valid category page."""
    +        get_page_mock.return_value = {"guide": "test", "metadata": {}}
     
    -        url = reverse("content:article_category", args=["category/test3"])
    +        url = reverse("content:page_category", args=["category/test3"])
             response = self.client.get(url)
             self.assertEqual(response.status_code, 200)
    -        get_article_mock.assert_called_once()
    +        get_page_mock.assert_called_once()
             self.assertEqual(get_category_mock.call_count, 2)
     
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_article")
    -    @patch("pydis_site.apps.content.views.article_category.utils.get_category")
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_invalid_category_article_code_404(
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_page")
    +    @patch("pydis_site.apps.content.views.page_category.utils.get_category")
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_invalid_category_page_code_404(
                 self,
                 get_category_mock,
    -            get_article_mock
    +            get_page_mock
         ):
    -        """Check that return code is 200 when trying to visit invalid category article."""
    -        get_article_mock.side_effect = Http404("Article not found.")
    +        """Check that return code is 200 when trying to visit invalid category page."""
    +        get_page_mock.side_effect = Http404("Page not found.")
     
    -        url = reverse("content:article_category", args=["category/invalid"])
    +        url = reverse("content:page_category", args=["category/invalid"])
             response = self.client.get(url)
             self.assertEqual(response.status_code, 404)
    -        get_article_mock.assert_not_called()
    +        get_page_mock.assert_not_called()
             get_category_mock.assert_not_called()
     
    -    @override_settings(ARTICLES_PATH=BASE_PATH)
    -    def test_article_category_template_names(self):
    -        """Check that this return category, article template or raise Http404."""
    +    @override_settings(PAGES_PATH=BASE_PATH)
    +    def test_page_category_template_names(self):
    +        """Check that this return category, page template or raise Http404."""
             factory = RequestFactory()
             cases = [
                 {"location": "category", "output": ["content/listing.html"]},
    -            {"location": "test", "output": ["content/article.html"]},
    +            {"location": "test", "output": ["content/page.html"]},
                 {"location": "invalid", "output": None, "raises": Http404}
             ]
     
             for case in cases:
                 with self.subTest(location=case["location"], output=case["output"]):
    -                request = factory.get(f"/articles/{case['location']}")
    -                instance = ArticleOrCategoryView()
    +                request = factory.get(f"/pages/{case['location']}")
    +                instance = PageOrCategoryView()
                     instance.request = request
                     instance.kwargs = {"location": case["location"]}
     
    diff --git a/pydis_site/apps/content/urls.py b/pydis_site/apps/content/urls.py
    index 49a5a2ef..1406f672 100644
    --- a/pydis_site/apps/content/urls.py
    +++ b/pydis_site/apps/content/urls.py
    @@ -4,6 +4,6 @@ from . import views
     
     app_name = "content"
     urlpatterns = [
    -    path("", views.ArticlesView.as_view(), name='articles'),
    -    path("/", views.ArticleOrCategoryView.as_view(), name='article_category'),
    +    path("", views.PagesView.as_view(), name='pages'),
    +    path("/", views.PageOrCategoryView.as_view(), name='page_category'),
     ]
    diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py
    index 305d26b5..db502a71 100644
    --- a/pydis_site/apps/content/utils.py
    +++ b/pydis_site/apps/content/utils.py
    @@ -9,7 +9,7 @@ from markdown2 import markdown
     
     def get_category(path: List[str]) -> Dict[str, str]:
         """Load category information by name from _info.yml."""
    -    path = settings.ARTICLES_PATH.joinpath(*path)
    +    path = settings.PAGES_PATH.joinpath(*path)
         if not path.exists() or not path.is_dir():
             raise Http404("Category not found.")
     
    @@ -20,10 +20,10 @@ def get_categories(path: Optional[List[str]] = None) -> Dict[str, Dict]:
         """Get all categories information."""
         categories = {}
         if path is None:
    -        categories_path = settings.ARTICLES_PATH
    +        categories_path = settings.PAGES_PATH
             path = []
         else:
    -        categories_path = settings.ARTICLES_PATH.joinpath(*path)
    +        categories_path = settings.PAGES_PATH.joinpath(*path)
     
         for name in categories_path.iterdir():
             if name.is_dir():
    @@ -32,34 +32,34 @@ def get_categories(path: Optional[List[str]] = None) -> Dict[str, Dict]:
         return categories
     
     
    -def get_articles(path: Optional[List[str]] = None) -> Dict[str, Dict]:
    -    """Get all root or category articles."""
    +def get_pages(path: Optional[List[str]] = None) -> Dict[str, Dict]:
    +    """Get all root or category pages."""
         if path is None:
    -        base_dir = settings.ARTICLES_PATH
    +        base_dir = settings.PAGES_PATH
         else:
    -        base_dir = settings.ARTICLES_PATH.joinpath(*path)
    +        base_dir = settings.PAGES_PATH.joinpath(*path)
     
    -    articles = {}
    +    pages = {}
     
         for item in base_dir.iterdir():
             if item.is_file() and item.name.endswith(".md"):
                 md = markdown(item.read_text(), extras=["metadata"])
    -            articles[os.path.splitext(item.name)[0]] = md.metadata
    +            pages[os.path.splitext(item.name)[0]] = md.metadata
     
    -    return articles
    +    return pages
     
     
    -def get_article(path: List[str]) -> Dict[str, Union[str, Dict]]:
    -    """Get one specific article. When category is specified, get it from there."""
    -    article_path = settings.ARTICLES_PATH.joinpath(*path[:-1])
    +def get_page(path: List[str]) -> Dict[str, Union[str, Dict]]:
    +    """Get one specific page. When category is specified, get it from there."""
    +    page_path = settings.PAGES_PATH.joinpath(*path[:-1])
     
         # We need to include extension MD
    -    article_path = article_path.joinpath(f"{path[-1]}.md")
    -    if not article_path.exists() or not article_path.is_file():
    -        raise Http404("Article not found.")
    +    page_path = page_path.joinpath(f"{path[-1]}.md")
    +    if not page_path.exists() or not page_path.is_file():
    +        raise Http404("Page not found.")
     
         html = markdown(
    -        article_path.read_text(),
    +        page_path.read_text(),
             extras=[
                 "metadata",
                 "fenced-code-blocks",
    @@ -71,4 +71,4 @@ def get_article(path: List[str]) -> Dict[str, Union[str, Dict]]:
             ]
         )
     
    -    return {"article": str(html), "metadata": html.metadata}
    +    return {"page": str(html), "metadata": html.metadata}
    diff --git a/pydis_site/apps/content/views/__init__.py b/pydis_site/apps/content/views/__init__.py
    index f92660d6..740d98e9 100644
    --- a/pydis_site/apps/content/views/__init__.py
    +++ b/pydis_site/apps/content/views/__init__.py
    @@ -1,4 +1,4 @@
    -from .article_category import ArticleOrCategoryView
    -from .articles import ArticlesView
    +from .page_category import PageOrCategoryView
    +from .pages import PagesView
     
    -__all__ = ["ArticleOrCategoryView", "ArticlesView"]
    +__all__ = ["PageOrCategoryView", "PagesView"]
    diff --git a/pydis_site/apps/content/views/article_category.py b/pydis_site/apps/content/views/article_category.py
    deleted file mode 100644
    index 2a407b99..00000000
    --- a/pydis_site/apps/content/views/article_category.py
    +++ /dev/null
    @@ -1,69 +0,0 @@
    -import typing as t
    -
    -from django.conf import settings
    -from django.http import Http404
    -from django.views.generic import TemplateView
    -
    -from pydis_site.apps.content import utils
    -
    -
    -class ArticleOrCategoryView(TemplateView):
    -    """Handles article and category pages."""
    -
    -    def get_template_names(self) -> t.List[str]:
    -        """Checks does this use article template or listing template."""
    -        location = self.kwargs["location"].split("/")
    -        full_location = settings.ARTICLES_PATH.joinpath(*location)
    -
    -        if full_location.is_dir():
    -            template_name = "content/listing.html"
    -        elif full_location.with_suffix(".md").is_file():
    -            template_name = "content/article.html"
    -        else:
    -            raise Http404
    -
    -        return [template_name]
    -
    -    def get_context_data(self, **kwargs) -> t.Dict[str, t.Any]:
    -        """Assign proper context variables based on what resource user requests."""
    -        context = super().get_context_data(**kwargs)
    -
    -        location: list = self.kwargs["location"].split("/")
    -        full_location = settings.ARTICLES_PATH.joinpath(*location)
    -
    -        if full_location.is_dir():
    -            context["category_info"] = utils.get_category(location)
    -            context["content"] = utils.get_articles(location)
    -            context["categories"] = utils.get_categories(location)
    -            # Add trailing slash here to simplify template
    -            context["path"] = "/".join(location) + "/"
    -            context["in_category"] = True
    -        elif full_location.with_suffix(".md").is_file():
    -            article_result = utils.get_article(location)
    -
    -            if len(location) > 1:
    -                context["category_data"] = utils.get_category(location[:-1])
    -                context["category_data"]["raw_name"] = location[:-1][-1]
    -            else:
    -                context["category_data"] = {"name": None, "raw_name": None}
    -
    -            context["article"] = article_result
    -            context["relevant_links"] = article_result["metadata"].get("relevant_links", {})
    -        else:
    -            raise Http404
    -
    -        location.pop()
    -        breadcrumb_items = []
    -        while len(location):
    -            breadcrumb_items.insert(
    -                0,
    -                {
    -                    "name": utils.get_category(location)["name"],
    -                    "path": "/".join(location)
    -                }
    -            )
    -            location.pop()
    -
    -        context["breadcrumb_items"] = breadcrumb_items
    -
    -        return context
    diff --git a/pydis_site/apps/content/views/articles.py b/pydis_site/apps/content/views/articles.py
    deleted file mode 100644
    index 999002d0..00000000
    --- a/pydis_site/apps/content/views/articles.py
    +++ /dev/null
    @@ -1,16 +0,0 @@
    -from django.views.generic import TemplateView
    -
    -from pydis_site.apps.content.utils import get_articles, get_categories
    -
    -
    -class ArticlesView(TemplateView):
    -    """Shows all content and categories."""
    -
    -    template_name = "content/listing.html"
    -
    -    def get_context_data(self, **kwargs) -> dict:
    -        """Add articles and categories data to template context."""
    -        context = super().get_context_data(**kwargs)
    -        context["content"] = get_articles()
    -        context["categories"] = get_categories()
    -        return context
    diff --git a/pydis_site/apps/content/views/page_category.py b/pydis_site/apps/content/views/page_category.py
    new file mode 100644
    index 00000000..f00a79ee
    --- /dev/null
    +++ b/pydis_site/apps/content/views/page_category.py
    @@ -0,0 +1,69 @@
    +import typing as t
    +
    +from django.conf import settings
    +from django.http import Http404
    +from django.views.generic import TemplateView
    +
    +from pydis_site.apps.content import utils
    +
    +
    +class PageOrCategoryView(TemplateView):
    +    """Handles pages and page categories."""
    +
    +    def get_template_names(self) -> t.List[str]:
    +        """Checks does this use page template or listing template."""
    +        location = self.kwargs["location"].split("/")
    +        full_location = settings.PAGES_PATH.joinpath(*location)
    +
    +        if full_location.is_dir():
    +            template_name = "content/listing.html"
    +        elif full_location.with_suffix(".md").is_file():
    +            template_name = "content/page.html"
    +        else:
    +            raise Http404
    +
    +        return [template_name]
    +
    +    def get_context_data(self, **kwargs) -> t.Dict[str, t.Any]:
    +        """Assign proper context variables based on what resource user requests."""
    +        context = super().get_context_data(**kwargs)
    +
    +        location: list = self.kwargs["location"].split("/")
    +        full_location = settings.PAGES_PATH.joinpath(*location)
    +
    +        if full_location.is_dir():
    +            context["category_info"] = utils.get_category(location)
    +            context["content"] = utils.get_pages(location)
    +            context["categories"] = utils.get_categories(location)
    +            # Add trailing slash here to simplify template
    +            context["path"] = "/".join(location) + "/"
    +            context["in_category"] = True
    +        elif full_location.with_suffix(".md").is_file():
    +            page_result = utils.get_page(location)
    +
    +            if len(location) > 1:
    +                context["category_data"] = utils.get_category(location[:-1])
    +                context["category_data"]["raw_name"] = location[:-1][-1]
    +            else:
    +                context["category_data"] = {"name": None, "raw_name": None}
    +
    +            context["page"] = page_result
    +            context["relevant_links"] = page_result["metadata"].get("relevant_links", {})
    +        else:
    +            raise Http404
    +
    +        location.pop()
    +        breadcrumb_items = []
    +        while len(location):
    +            breadcrumb_items.insert(
    +                0,
    +                {
    +                    "name": utils.get_category(location)["name"],
    +                    "path": "/".join(location)
    +                }
    +            )
    +            location.pop()
    +
    +        context["breadcrumb_items"] = breadcrumb_items
    +
    +        return context
    diff --git a/pydis_site/apps/content/views/pages.py b/pydis_site/apps/content/views/pages.py
    new file mode 100644
    index 00000000..11ac0eeb
    --- /dev/null
    +++ b/pydis_site/apps/content/views/pages.py
    @@ -0,0 +1,16 @@
    +from django.views.generic import TemplateView
    +
    +from pydis_site.apps.content.utils import get_pages, get_categories
    +
    +
    +class PagesView(TemplateView):
    +    """Shows all pages and categories."""
    +
    +    template_name = "content/listing.html"
    +
    +    def get_context_data(self, **kwargs) -> dict:
    +        """Add page and category data to template context."""
    +        context = super().get_context_data(**kwargs)
    +        context["content"] = get_pages()
    +        context["categories"] = get_categories()
    +        return context
    diff --git a/pydis_site/apps/events/urls.py b/pydis_site/apps/events/urls.py
    index 9a65cf1f..601451da 100644
    --- a/pydis_site/apps/events/urls.py
    +++ b/pydis_site/apps/events/urls.py
    @@ -1,9 +1,9 @@
     from django.urls import path
     
    -from pydis_site.apps.events.views import IndexView, PageView
    +from pydis_site.apps.events.views import IndexView, PagesView
     
     app_name = "events"
     urlpatterns = [
         path("", IndexView.as_view(), name="index"),
    -    path("/", PageView.as_view(), name="page"),
    +    path("/", PagesView.as_view(), name="page"),
     ]
    diff --git a/pydis_site/apps/events/views/__init__.py b/pydis_site/apps/events/views/__init__.py
    index 8a107e2f..6188f723 100644
    --- a/pydis_site/apps/events/views/__init__.py
    +++ b/pydis_site/apps/events/views/__init__.py
    @@ -1,4 +1,4 @@
     from .index import IndexView
    -from .page import PageView
    +from .page import PagesView
     
    -__all__ = ["IndexView", "PageView"]
    +__all__ = ["IndexView", "PagesView"]
    diff --git a/pydis_site/apps/events/views/page.py b/pydis_site/apps/events/views/page.py
    index f4c37aeb..fbedd4e8 100644
    --- a/pydis_site/apps/events/views/page.py
    +++ b/pydis_site/apps/events/views/page.py
    @@ -5,7 +5,7 @@ from django.http import Http404
     from django.views.generic import TemplateView
     
     
    -class PageView(TemplateView):
    +class PagesView(TemplateView):
         """Handles event pages showing."""
     
         def get_template_names(self) -> List[str]:
    diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py
    index bd7c0625..3c716875 100644
    --- a/pydis_site/apps/home/urls.py
    +++ b/pydis_site/apps/home/urls.py
    @@ -8,6 +8,6 @@ urlpatterns = [
         path('', HomeView.as_view(), name='home'),
         path('admin/', admin.site.urls),
         path('resources/', include('pydis_site.apps.resources.urls')),
    -    path('articles/', include('pydis_site.apps.content.urls')),
    +    path('pages/', include('pydis_site.apps.content.urls')),
         path('events/', include('pydis_site.apps.events.urls', namespace='events')),
     ]
    diff --git a/pydis_site/settings.py b/pydis_site/settings.py
    index d509d980..0b4d9fd1 100644
    --- a/pydis_site/settings.py
    +++ b/pydis_site/settings.py
    @@ -287,4 +287,4 @@ SITE_REPOSITORY_OWNER = "python-discord"
     SITE_REPOSITORY_NAME = "site"
     SITE_REPOSITORY_BRANCH = "master"
     
    -ARTICLES_PATH = Path(BASE_DIR, "pydis_site", "apps", "content", "resources", "content")
    +PAGES_PATH = Path(BASE_DIR, "pydis_site", "apps", "content", "resources", "content")
    diff --git a/pydis_site/static/css/content/articles.css b/pydis_site/static/css/content/articles.css
    deleted file mode 100644
    index f46d6b15..00000000
    --- a/pydis_site/static/css/content/articles.css
    +++ /dev/null
    @@ -1,16 +0,0 @@
    -.breadcrumb-section {
    -    padding: 1rem;
    -}
    -
    -i.has-icon-padding {
    -    padding: 0 10px 25px 0;
    -}
    -
    -pre {
    -    /*
    -     * Style it the same as the  tag, since highlight.js does not style
    -     * backgrounds of 
     tags but bulma does, resulting in a weird off-white
    -     * border.
    -     */
    -    background-color: #282c34;
    -}
    diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css
    new file mode 100644
    index 00000000..f46d6b15
    --- /dev/null
    +++ b/pydis_site/static/css/content/page.css
    @@ -0,0 +1,16 @@
    +.breadcrumb-section {
    +    padding: 1rem;
    +}
    +
    +i.has-icon-padding {
    +    padding: 0 10px 25px 0;
    +}
    +
    +pre {
    +    /*
    +     * Style it the same as the  tag, since highlight.js does not style
    +     * backgrounds of 
     tags but bulma does, resulting in a weird off-white
    +     * border.
    +     */
    +    background-color: #282c34;
    +}
    diff --git a/pydis_site/templates/content/article.html b/pydis_site/templates/content/article.html
    deleted file mode 100644
    index 69d01a8d..00000000
    --- a/pydis_site/templates/content/article.html
    +++ /dev/null
    @@ -1,58 +0,0 @@
    -{% extends 'base/base.html' %}
    -{% load static %}
    -
    -{% block title %}{{ article.metadata.title }}{% endblock %}
    -{% block head %}
    -  
    -  
    -  
    -  
    -  
    -  
    -  
    -{% endblock %}
    -
    -{% block content %}
    -    {% include "base/navbar.html" %}
    -
    -    
    -
    -    
    -
    -
    -

    {{ article.metadata.title }}

    -
    -
    - {{ article.article|safe }} -
    -
    - {% if relevant_links|length > 0 %} -
    - - -
    - {% endif %} -
    -
    -
    -
    -
    - -{% endblock %} diff --git a/pydis_site/templates/content/listing.html b/pydis_site/templates/content/listing.html index 8c06bccc..39eae1c2 100644 --- a/pydis_site/templates/content/listing.html +++ b/pydis_site/templates/content/listing.html @@ -1,12 +1,12 @@ {% extends 'base/base.html' %} {% load static %} -{% block title %}{{ category_info.name|default:"Articles" }}{% endblock %} +{% block title %}{{ category_info.name|default:"Pages" }}{% endblock %} {% block head %} - + - + {% endblock %} {% block content %} @@ -17,12 +17,12 @@
    @@ -31,7 +31,7 @@
    -

    {{ category_info.name|default:"Articles" }}

    +

    {{ category_info.name|default:"Pages" }}

    {% for category, data in categories.items %}
    @@ -39,18 +39,18 @@ - + {{ data.name }}

    {{ data.description }}

    {% endfor %} - {% for article, data in content.items %} + {% for page, data in content.items %}
    - + {{ data.title }}

    {{ data.short_description }}

    diff --git a/pydis_site/templates/content/page.html b/pydis_site/templates/content/page.html new file mode 100644 index 00000000..3b0ebb5f --- /dev/null +++ b/pydis_site/templates/content/page.html @@ -0,0 +1,58 @@ +{% extends 'base/base.html' %} +{% load static %} + +{% block title %}{{ page.metadata.title }}{% endblock %} +{% block head %} + + + + + + + +{% endblock %} + +{% block content %} + {% include "base/navbar.html" %} + + + +
    +
    +
    +

    {{ page.metadata.title }}

    +
    +
    + {{ page.page|safe }} +
    +
    + {% if relevant_links|length > 0 %} +
    + + +
    + {% endif %} +
    +
    +
    +
    +
    + +{% endblock %} diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html index 2dc88a8c..491bc55e 100644 --- a/pydis_site/templates/resources/resources.html +++ b/pydis_site/templates/resources/resources.html @@ -15,7 +15,7 @@

    Resources

    - +

    Guides

    Made by us, for you

    -- cgit v1.2.3 From 20737ab238719ce7a2df78b0ca5101884f28db0d Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 23 Mar 2021 13:42:14 +0300 Subject: Centers Sponsor Title --- pydis_site/static/css/home/index.css | 2 ++ 1 file changed, 2 insertions(+) (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index 106feb7c..808c0517 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -206,6 +206,8 @@ h1 { #sponsors .hero-body { padding-top: 2rem; padding-bottom: 3rem; + + text-align: center; } #sponsors .columns { -- cgit v1.2.3 From c7c571ae404fe6f0ffba53713d86c14090366f0b Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 23 Mar 2021 13:43:00 +0300 Subject: Reduces The Width Of The Sponsors Tab Reduces the width of the sponsor tab to reduce the chance of one logo ending up on a new line by itself. --- pydis_site/static/css/home/index.css | 2 ++ 1 file changed, 2 insertions(+) (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index 808c0517..6cfbf69f 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -212,6 +212,8 @@ h1 { #sponsors .columns { justify-content: center; + margin: auto; + max-width: 80%; } #sponsors img { -- cgit v1.2.3 From 6ccec0d866c44cd7f9789a396ef6ec6cd2cd5df8 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Wed, 24 Mar 2021 19:55:16 +0800 Subject: Replace `markdown2` with `markdown` and `python-frontmatter`. This allows us to properly escape codeblocks within markdown, permalink to headers on a page, and decouples getting metadata from a file and getting generated HTML from the Markdown content. --- Pipfile | 3 +- Pipfile.lock | 43 ++++++++++++++-- .../pydis-guides/how-to-contribute-a-page.md | 59 +++++++++++++++++++++- pydis_site/apps/content/utils.py | 32 ++++++------ pydis_site/apps/content/views/page_category.py | 15 +++--- pydis_site/static/css/content/page.css | 29 ++++++++--- pydis_site/templates/content/base.html | 2 +- pydis_site/templates/content/page.html | 4 +- 8 files changed, 146 insertions(+), 41 deletions(-) (limited to 'pydis_site/static/css') diff --git a/Pipfile b/Pipfile index 8af6e3ce..c2b87ab4 100644 --- a/Pipfile +++ b/Pipfile @@ -18,7 +18,8 @@ pyyaml = "~=5.1" pyuwsgi = {version = "~=2.0", sys_platform = "!='win32'"} sentry-sdk = "~=0.14" gitpython = "~=3.1.7" -markdown2 = "~=2.3.9" +markdown = "~=3.3.4" +python-frontmatter = "~=1.0" [dev-packages] coverage = "~=5.0" diff --git a/Pipfile.lock b/Pipfile.lock index 3dfb7ec6..4f8bd0d3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "893ab5cc0b64f7bb87c0304c9fb6eb24b1856c40286fbbdb1a2bf4a0d7a6d39f" + "sha256": "a338a377b64a5d25bf925646c97d932a2d1c783fc8f40d91b6a9ab8f30c2b14e" }, "pipfile-spec": 6, "requires": { @@ -118,6 +118,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, + "importlib-metadata": { + "hashes": [ + "sha256:742add720a20d0467df2f444ae41704000f50e1234f46174b51f9c6031a1bd71", + "sha256:b74159469b464a99cb8cc3e21973e4d96e05d3024d337313fedb618a6e86e6f4" + ], + "markers": "python_version < '3.8'", + "version": "==3.7.3" + }, "libsass": { "hashes": [ "sha256:1521d2a8d4b397c6ec90640a1f6b5529077035efc48ef1c2e53095544e713d1b", @@ -136,13 +144,13 @@ ], "version": "==0.20.1" }, - "markdown2": { + "markdown": { "hashes": [ - "sha256:85956d8119fa6378156fef65545d66705a842819d2e1b50379a2b9d2aaa17cf0", - "sha256:fef148e5fd68d4532286c3e2943e9d2c076a8ad781b0a70a9d599a0ffe91652d" + "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49", + "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c" ], "index": "pypi", - "version": "==2.3.10" + "version": "==3.3.4" }, "psycopg2-binary": { "hashes": [ @@ -185,6 +193,14 @@ "index": "pypi", "version": "==2.8.6" }, + "python-frontmatter": { + "hashes": [ + "sha256:766ae75f1b301ffc5fe3494339147e0fd80bc3deff3d7590a93991978b579b08", + "sha256:e98152e977225ddafea6f01f40b4b0f1de175766322004c826ca99842d19a7cd" + ], + "index": "pypi", + "version": "==1.0.0" + }, "pytz": { "hashes": [ "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", @@ -288,6 +304,15 @@ "markers": "python_version >= '3.5'", "version": "==0.4.1" }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + ], + "markers": "python_version < '3.8'", + "version": "==3.7.4.3" + }, "urllib3": { "hashes": [ "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", @@ -303,6 +328,14 @@ ], "index": "pypi", "version": "==5.2.0" + }, + "zipp": { + "hashes": [ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.1" } }, "develop": { 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 index 8967e7da..12969ba2 100644 --- 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 @@ -69,18 +69,75 @@ You can learn more about Markdown metadata [here](https://github.com/trentm/pyth Apart from standard Markdown, certain additions are available: +### Abbreviations +HTML `` 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 HTML +generated from YAML 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 | -| Big item | Small 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 diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py index 0faf722c..11d2b792 100644 --- a/pydis_site/apps/content/utils.py +++ b/pydis_site/apps/content/utils.py @@ -1,9 +1,11 @@ from pathlib import Path -from typing import Dict, Union +from typing import Dict, Tuple +import frontmatter +import markdown import yaml from django.http import Http404 -from markdown2 import markdown +from markdown.extensions.toc import TocExtension def get_category(path: Path) -> Dict[str, str]: @@ -31,29 +33,25 @@ def get_category_pages(path: Path) -> Dict[str, Dict]: for item in path.glob("*.md"): if item.is_file(): - md = markdown(item.read_text(), extras=["metadata"]) - pages[item.stem] = md.metadata + pages[item.stem] = frontmatter.load(item) return pages -def get_page(path: Path) -> Dict[str, Union[str, Dict]]: +def get_page(path: Path) -> Tuple[str, Dict]: """Get one specific page.""" if not path.is_file(): raise Http404("Page not found.") - html = markdown( - path.read_text(encoding="utf-8"), - extras=[ - "metadata", - "fenced-code-blocks", - "highlightjs-lang", - "header-ids", - "strike", - "target-blank-links", - "tables", - "task_list" + metadata, content = frontmatter.parse(path.read_text(encoding="utf-8")) + html = markdown.markdown( + content, + extensions=[ + "extra", + # Empty string for marker to disable text searching for [TOC] + # By using a metadata key instead, we save time on long markdown documents + TocExtension(title="Table of Contents:", permalink=True, marker="") ] ) - return {"content": str(html), "metadata": html.metadata} + return str(html), metadata diff --git a/pydis_site/apps/content/views/page_category.py b/pydis_site/apps/content/views/page_category.py index 91aed7f0..4a2ed2d6 100644 --- a/pydis_site/apps/content/views/page_category.py +++ b/pydis_site/apps/content/views/page_category.py @@ -35,18 +35,19 @@ class PageOrCategoryView(TemplateView): if self.full_location.is_dir(): context["categories"] = utils.get_categories(self.full_location) + context["pages"] = utils.get_category_pages(self.full_location) + category = utils.get_category(self.full_location) - context["category_info"] = category context["page_title"] = category["name"] context["page_description"] = category["description"] - context["pages"] = utils.get_category_pages(self.full_location) + context["path"] = f"{self.location}/" # Add trailing slash here to simplify template elif self.full_location.with_suffix(".md").is_file(): - page_result = utils.get_page(self.full_location.with_suffix(".md")) - context["page"] = page_result - context["page_title"] = page_result["metadata"]["title"] - context["page_description"] = page_result["metadata"]["description"] - context["relevant_links"] = page_result["metadata"].get("relevant_links", {}) + page, metadata = utils.get_page(self.full_location.with_suffix(".md")) + context["page"] = page + context["page_title"] = metadata["title"] + context["page_description"] = metadata["description"] + context["relevant_links"] = metadata.get("relevant_links", {}) else: raise Http404 diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css index f46d6b15..57d7472b 100644 --- a/pydis_site/static/css/content/page.css +++ b/pydis_site/static/css/content/page.css @@ -6,11 +6,26 @@ i.has-icon-padding { padding: 0 10px 25px 0; } -pre { - /* - * Style it the same as the tag, since highlight.js does not style - * backgrounds of
     tags but bulma does, resulting in a weird off-white
    -     * border.
    -     */
    -    background-color: #282c34;
    +/*
    + * Move padding padding from 
     tag to hljs  tags so the padding
    + * space is colored the same as the background of hljs  blocks.
    + */
    +.content pre {
    +    padding: 0;
    +}
    +
    +code.hljs {
    +    padding: 1.75em 2em;
    +}
    +
    +/*
    + * Show header permalink on hover.
    + */
    +.headerlink {
    +    display: none;
    +    padding-left: 0.5em;
    +}
    +
    +:is(h1, h2, h3, h4, h5, h6):hover > .headerlink {
    +    display: inline;
     }
    diff --git a/pydis_site/templates/content/base.html b/pydis_site/templates/content/base.html
    index 1508dfb3..19eec5d4 100644
    --- a/pydis_site/templates/content/base.html
    +++ b/pydis_site/templates/content/base.html
    @@ -3,7 +3,7 @@
     
     {% block title %}{{ page_title }}{% endblock %}
     {% block head %}
    -
    +    
         
         
         
    diff --git a/pydis_site/templates/content/page.html b/pydis_site/templates/content/page.html
    index 5e820c26..06d74208 100644
    --- a/pydis_site/templates/content/page.html
    +++ b/pydis_site/templates/content/page.html
    @@ -12,7 +12,7 @@
         {% if relevant_links|length > 0 %}
             
    - {{ page.content|safe }} + {{ page|safe }}
    @@ -26,6 +26,6 @@
    {% else %} -
    {{ page.content|safe }}
    +
    {{ page|safe }}
    {% endif %} {% endblock %} -- cgit v1.2.3 From c244fea3bc7c9560b3ff5cd4bd40f9e19cf62ae1 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 24 Mar 2021 22:46:05 +0100 Subject: Remove the 100K user banner. --- pydis_site/static/css/home/index.css | 4 ++++ pydis_site/templates/home/index.html | 8 +------- 2 files changed, 5 insertions(+), 7 deletions(-) (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index 6cfbf69f..ee6f6e4c 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -45,6 +45,10 @@ h1 { box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); } +#wave-hero-centered { + margin: auto auto; +} + #wave-hero-right img{ border-radius: 10px; box-shadow: 0 1px 6px rgba(0,0,0,0.16), 0 1px 6px rgba(0,0,0,0.23); diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index c35af2aa..f3470a83 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -29,7 +29,7 @@
    {# Embedded Welcome video #} -
    +
    - - {# Right side content #} -
    - 100K members! -
    -
    -- cgit v1.2.3 From 8b68a5a4ce7ff33d76d10a980087048a73764b29 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Mon, 29 Mar 2021 15:25:53 +0800 Subject: Document additional markdown features. Adds image caption capabilities and documents missing HTML extensions available. --- .../pydis-guides/how-to-contribute-a-page.md | 26 ++++++++++++++++++++++ pydis_site/static/css/content/page.css | 20 +++++++++++++++++ 2 files changed, 46 insertions(+) (limited to 'pydis_site/static/css') 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 index 303a2c0f..67666428 100644 --- 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 @@ -159,3 +159,29 @@ 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)). +**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 +![Summer Code Jam 2020](/static/images/events/summer_code_jam_2020.png){: width="400" } +*Summmer Code Jam 2020 banner with event information.* +``` + +**Output:** + +![Summer Code Jam 2020](/static/images/events/summer_code_jam_2020.png){: 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. diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css index 57d7472b..97b297b2 100644 --- a/pydis_site/static/css/content/page.css +++ b/pydis_site/static/css/content/page.css @@ -29,3 +29,23 @@ code.hljs { :is(h1, h2, h3, h4, h5, h6):hover > .headerlink { display: inline; } + +/* + * Display tags immediately following 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: + * + * ![Title](Source) + * *This is my caption.* + * + */ +img + em { + /* Place the caption on it's own line */ + display: block; + white-space: pre; + + /* Style */ + font-size: .875em; +} -- cgit v1.2.3 From 69f33290b8bd12b95bb6c620a9a1422a5d11b798 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Mon, 29 Mar 2021 17:51:10 +0800 Subject: Allow adding a table of contents to a page. --- .../pydis-guides/how-to-contribute-a-page.md | 2 ++ pydis_site/apps/content/utils.py | 12 +++++++--- pydis_site/apps/content/views/page_category.py | 1 + pydis_site/static/css/content/page.css | 19 +++++++++++++++ pydis_site/templates/content/page.html | 28 +++++++++++++++------- 5 files changed, 50 insertions(+), 12 deletions(-) (limited to 'pydis_site/static/css') 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 index 67666428..c2d9d975 100644 --- 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 @@ -5,6 +5,7 @@ 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. @@ -80,6 +81,7 @@ Pages, which include guides, articles, and other static content,... ### Optional Fields - **icon:** Icon for the category entry for the page. Default: `fab fa-python` - **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. ## Extended Markdown diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py index 726c991f..d6886ce2 100644 --- a/pydis_site/apps/content/utils.py +++ b/pydis_site/apps/content/utils.py @@ -44,14 +44,20 @@ def get_page(path: Path) -> Tuple[str, Dict]: raise Http404("Page not found.") metadata, content = frontmatter.parse(path.read_text(encoding="utf-8")) - html = markdown.markdown( - content, + 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(title="Table of Contents:", permalink=True, marker="") + 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/page_category.py b/pydis_site/apps/content/views/page_category.py index 711d6a56..a995d2a1 100644 --- a/pydis_site/apps/content/views/page_category.py +++ b/pydis_site/apps/content/views/page_category.py @@ -67,6 +67,7 @@ class PageOrCategoryView(TemplateView): "page_title": metadata["title"], "page_description": metadata["description"], "relevant_links": metadata.get("relevant_links", {}), + "toc": metadata.get("toc") } @staticmethod diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css index 97b297b2..3ac41d1b 100644 --- a/pydis_site/static/css/content/page.css +++ b/pydis_site/static/css/content/page.css @@ -49,3 +49,22 @@ img + em { /* 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
      items */ +.toc > ul > li { + list-style-type: disc; +} diff --git a/pydis_site/templates/content/page.html b/pydis_site/templates/content/page.html index 06d74208..45aa8221 100644 --- a/pydis_site/templates/content/page.html +++ b/pydis_site/templates/content/page.html @@ -9,20 +9,30 @@ {% endblock %} {% block page_content %} - {% if relevant_links|length > 0 %} + {% if relevant_links or toc %} {% else %} -- cgit v1.2.3 From 01b6ad7b3d5e9313833c621d65b7c6cf3835aedb Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 30 Mar 2021 13:16:38 +0300 Subject: Remove resources payments-related CSS --- pydis_site/static/css/resources/resources_list.css | 20 -------------------- 1 file changed, 20 deletions(-) (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/resources/resources_list.css b/pydis_site/static/css/resources/resources_list.css index 735421fa..33129c87 100644 --- a/pydis_site/static/css/resources/resources_list.css +++ b/pydis_site/static/css/resources/resources_list.css @@ -53,23 +53,3 @@ i.resource-icon.is-black { i.has-icon-padding { padding: 0 10px 25px 0; } - -.payment-free, .payment-optional { - color: dimgray; -} - -.payment-free:hover, .payment-optional:hover { - color: gray; -} - -.payment-paid { - color: darkgoldenrod; -} - -.payment-paid:hover { - color: goldenrod; -} - -.tooltip:hover:before { - display: block; -} -- cgit v1.2.3 From 348ee615ebd62a3449a6d0a8426aba2d3029333b Mon Sep 17 00:00:00 2001 From: kosayoda Date: Mon, 19 Apr 2021 21:32:26 +0800 Subject: Increase padding between text and images in lists. This has a caveat in that there may be additional padding if the first thing in a list item is an image, but it is very unlikely in practice, so it's not handled for now. --- .../content/resources/guides/pydis-guides/contributing/issues.md | 6 +++--- pydis_site/static/css/content/page.css | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) (limited to 'pydis_site/static/css') 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 index 52da95ce..9151e5e3 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md @@ -18,13 +18,13 @@ Often, an Issue ticket already exists within the scope of what you might be cons If you don't see one existing, then: -Click the `Issues` tab in a repository:
      +1. Click the `Issues` tab in a repository:
      ![Repository Issues Tab](/static/images/content/contributing/github_issues_tab.png) -Click `New Issue`
      +2. Click `New Issue`:
      ![New Issues Button](/static/images/content/contributing/github_new_issue.png) -Enter the title and description for your issue, then click `Submit new issue`
      +3. Enter the title and description for your issue, then click `Submit new issue`:
      ![Sample Issue](/static/images/content/contributing/github_sample_issue.png){: width="600" } ## What should I put as a title? diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css index 3ac41d1b..d831f86d 100644 --- a/pydis_site/static/css/content/page.css +++ b/pydis_site/static/css/content/page.css @@ -68,3 +68,12 @@ ul.menu-list.toc { .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; +} -- cgit v1.2.3 From a75ec754241a69fe5b5b190a5f50d76a36ba7d11 Mon Sep 17 00:00:00 2001 From: Bast <52266665+bast0006@users.noreply.github.com> Date: Thu, 13 May 2021 15:11:05 -0700 Subject: Fix default margin on error pages This removes the scrollbar from them --- pydis_site/static/css/error_pages.css | 1 + 1 file changed, 1 insertion(+) (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/error_pages.css b/pydis_site/static/css/error_pages.css index ee41fa5c..e59e2a54 100644 --- a/pydis_site/static/css/error_pages.css +++ b/pydis_site/static/css/error_pages.css @@ -11,6 +11,7 @@ body { align-items: center; justify-content: center; height: 100%; + margin: 0; } h1, -- cgit v1.2.3 From 274efc3ec73e2bcfee9cd93b26f737ee68fd4638 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Fri, 14 May 2021 13:58:56 +0800 Subject: Merge branch main into dewikification --- .coveragerc | 2 + .dockerignore | 6 +- .flake8 | 2 +- .github/CODEOWNERS | 22 +- .github/review-policy.yml | 3 + .github/workflows/build.yaml | 58 + .github/workflows/codeql-analysis.yml | 32 - .github/workflows/deploy.yaml | 46 + .github/workflows/lint-test.yaml | 142 + .github/workflows/sentry-release.yml | 6 +- .github/workflows/status_embed.yaml | 78 + .pre-commit-config.yaml | 2 +- CODE_OF_CONDUCT.md | 3 + CONTRIBUTING.md | 125 +- Dockerfile | 32 + LICENSE | 2 +- Pipfile | 9 +- Pipfile.lock | 285 +- README.md | 29 +- SECURITY.md | 3 + azure-pipelines.yml | 102 - docker-compose.yml | 6 +- docker/Dockerfile | 32 - docker/uwsgi.ini | 38 - docs/deployment.md | 146 - manage.py | 24 +- postgres/init.sql | 146 + pydis_site/apps/api/admin.py | 108 +- pydis_site/apps/api/dblogger.py | 22 - .../apps/api/migrations/0064_delete_logentry.py | 16 + .../api/migrations/0066_merge_20201003_0730.py | 14 + .../0067_add_voice_ban_infraction_type.py | 18 + .../api/migrations/0068_split_nomination_tables.py | 75 + .../0069_documentationlink_validators.py | 25 + pydis_site/apps/api/models/__init__.py | 2 +- pydis_site/apps/api/models/bot/__init__.py | 2 +- .../apps/api/models/bot/documentation_link.py | 17 +- pydis_site/apps/api/models/bot/infraction.py | 3 +- pydis_site/apps/api/models/bot/metricity.py | 132 + pydis_site/apps/api/models/bot/nomination.py | 52 +- pydis_site/apps/api/models/log_entry.py | 55 - pydis_site/apps/api/pagination.py | 49 + pydis_site/apps/api/serializers.py | 139 +- pydis_site/apps/api/tests/test_dblogger.py | 27 - .../apps/api/tests/test_documentation_links.py | 15 +- pydis_site/apps/api/tests/test_infractions.py | 30 + pydis_site/apps/api/tests/test_models.py | 16 +- pydis_site/apps/api/tests/test_nominations.py | 118 +- pydis_site/apps/api/tests/test_users.py | 337 +- pydis_site/apps/api/urls.py | 2 - pydis_site/apps/api/views.py | 5 +- pydis_site/apps/api/viewsets/__init__.py | 1 - pydis_site/apps/api/viewsets/bot/infraction.py | 21 +- pydis_site/apps/api/viewsets/bot/nomination.py | 142 +- pydis_site/apps/api/viewsets/bot/user.py | 192 +- pydis_site/apps/api/viewsets/log_entry.py | 36 - .../0002_auto_now_on_repository_metadata.py | 18 + pydis_site/apps/home/models/repository_metadata.py | 15 +- .../apps/home/tests/mock_github_api_response.json | 2 +- .../apps/home/tests/test_repodata_helpers.py | 42 +- pydis_site/apps/home/urls.py | 3 +- pydis_site/apps/home/views/__init__.py | 4 +- pydis_site/apps/home/views/home.py | 160 +- pydis_site/apps/redirect/tests.py | 7 +- .../resources/reading/books/effective_python.yaml | 5 +- pydis_site/constants.py | 6 +- pydis_site/hosts.py | 3 + pydis_site/settings.py | 39 +- pydis_site/static/css/base/base.css | 76 +- pydis_site/static/css/error_pages.css | 67 + pydis_site/static/css/home/index.css | 239 +- pydis_site/static/css/home/timeline.css | 3823 ++++++++++++++++++++ pydis_site/static/images/events/100k.png | Bin 0 -> 210477 bytes pydis_site/static/images/frontpage/welcome.jpg | Bin 0 -> 51725 bytes pydis_site/static/images/navbar/discord.svg | 165 + .../static/images/navbar/navbar_discordjoin.svg | 81 - pydis_site/static/images/sponsors/adafruit.png | Bin 11705 -> 0 bytes pydis_site/static/images/sponsors/notion.png | Bin 0 -> 38207 bytes pydis_site/static/images/sponsors/streamyard.png | Bin 0 -> 86678 bytes .../static/images/timeline/cd-icon-location.svg | 4 + .../static/images/timeline/cd-icon-movie.svg | 4 + .../static/images/timeline/cd-icon-picture.svg | 72 + pydis_site/static/images/waves/wave_dark.svg | 73 + pydis_site/static/images/waves/wave_white.svg | 77 + pydis_site/static/js/timeline/main.js | 104 + pydis_site/templates/404.html | 34 + pydis_site/templates/500.html | 29 + pydis_site/templates/base/base.html | 1 - pydis_site/templates/base/footer.html | 2 +- pydis_site/templates/base/navbar.html | 17 +- pydis_site/templates/home/index.html | 190 +- pydis_site/templates/home/timeline.html | 693 ++++ 92 files changed, 7813 insertions(+), 1294 deletions(-) create mode 100644 .github/review-policy.yml create mode 100644 .github/workflows/build.yaml delete mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/deploy.yaml create mode 100644 .github/workflows/lint-test.yaml create mode 100644 .github/workflows/status_embed.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Dockerfile create mode 100644 SECURITY.md delete mode 100644 azure-pipelines.yml delete mode 100644 docker/Dockerfile delete mode 100644 docker/uwsgi.ini delete mode 100644 docs/deployment.md create mode 100644 postgres/init.sql delete mode 100644 pydis_site/apps/api/dblogger.py create mode 100644 pydis_site/apps/api/migrations/0064_delete_logentry.py create mode 100644 pydis_site/apps/api/migrations/0066_merge_20201003_0730.py create mode 100644 pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py create mode 100644 pydis_site/apps/api/migrations/0068_split_nomination_tables.py create mode 100644 pydis_site/apps/api/migrations/0069_documentationlink_validators.py create mode 100644 pydis_site/apps/api/models/bot/metricity.py delete mode 100644 pydis_site/apps/api/models/log_entry.py create mode 100644 pydis_site/apps/api/pagination.py delete mode 100644 pydis_site/apps/api/tests/test_dblogger.py delete mode 100644 pydis_site/apps/api/viewsets/log_entry.py create mode 100644 pydis_site/apps/home/migrations/0002_auto_now_on_repository_metadata.py create mode 100644 pydis_site/static/css/error_pages.css create mode 100644 pydis_site/static/css/home/timeline.css create mode 100644 pydis_site/static/images/events/100k.png create mode 100644 pydis_site/static/images/frontpage/welcome.jpg create mode 100644 pydis_site/static/images/navbar/discord.svg delete mode 100644 pydis_site/static/images/navbar/navbar_discordjoin.svg delete mode 100644 pydis_site/static/images/sponsors/adafruit.png create mode 100644 pydis_site/static/images/sponsors/notion.png create mode 100644 pydis_site/static/images/sponsors/streamyard.png create mode 100755 pydis_site/static/images/timeline/cd-icon-location.svg create mode 100755 pydis_site/static/images/timeline/cd-icon-movie.svg create mode 100755 pydis_site/static/images/timeline/cd-icon-picture.svg create mode 100644 pydis_site/static/images/waves/wave_dark.svg create mode 100644 pydis_site/static/images/waves/wave_white.svg create mode 100644 pydis_site/static/js/timeline/main.js create mode 100644 pydis_site/templates/404.html create mode 100644 pydis_site/templates/500.html create mode 100644 pydis_site/templates/home/timeline.html (limited to 'pydis_site/static/css') diff --git a/.coveragerc b/.coveragerc index e2511cb9..b4a9bbe4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,9 +11,11 @@ omit = */admin.py */apps.py */urls.py + pydis_site/apps/api/models/bot/metricity.py pydis_site/wsgi.py pydis_site/settings.py pydis_site/utils/resources.py + pydis_site/apps/home/views/home.py [report] fail_under = 100 diff --git a/.dockerignore b/.dockerignore index 61fa291a..2a1f68e7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ .cache .coverage .coveragerc +.git .github .gitignore .gitlab @@ -15,11 +16,8 @@ pydis_site/apps/home/tests pydis_site/apps/staff/tests CHANGELOG.md CONTRIBUTING.md -docker -!docker/uwsgi.ini -!docker/wheels docker-compose.yml -Dockerfile.local +Dockerfile docs htmlcov LICENSE diff --git a/.flake8 b/.flake8 index bcd26d9e..6690af3e 100644 --- a/.flake8 +++ b/.flake8 @@ -3,7 +3,7 @@ max-line-length=100 docstring-convention=all import-order-style=pycharm application_import_names=pydis_site -exclude=__pycache__, venv, .venv, **/migrations/** +exclude=__pycache__, venv, .venv, **/migrations/**, .cache/ ignore= B311,W503,E226,S311,T000 # Missing Docstrings diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cf5f1590..009b21c4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,21 @@ -* @python-discord/core-developers +# Infractions API +pydis_site/apps/api/models/bot/infraction.py @MarkKoz +pydis_site/apps/api/viewsets/bot/infraction.py @MarkKoz + +# Home app +pydis_site/apps/home/** @ks129 + +# Django ORM +**/migrations/** @Akarys42 +**/models/** @Akarys42 @Den4200 + +# CI & Docker +.github/workflows/** @MarkKoz @Akarys42 @SebastiaanZ @Den4200 @ks129 +Dockerfile @MarkKoz @Akarys42 @Den4200 +docker-compose.yml @MarkKoz @Akarys42 @Den4200 + +# Tools +Pipfile* @Akarys42 + +# Metricity +pydis_site/apps/api/models/bot/metricity.py @jb3 diff --git a/.github/review-policy.yml b/.github/review-policy.yml new file mode 100644 index 00000000..421b30f8 --- /dev/null +++ b/.github/review-policy.yml @@ -0,0 +1,3 @@ +remote: python-discord/.github +path: review-policies/core-developers.yml +ref: main diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..ab7321de --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,58 @@ +name: Build + +on: + workflow_run: + workflows: ["Lint & Test"] + branches: + - main + types: + - completed + +jobs: + build: + name: Build Docker Image + if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' + runs-on: ubuntu-latest + + steps: + # Create a commit SHA-based tag for the container repositories + - name: Create SHA Container Tag + id: sha_tag + run: | + tag=$(cut -c 1-7 <<< $GITHUB_SHA) + echo "::set-output name=tag::$tag" + + - name: Checkout code + uses: actions/checkout@v2 + + # The current version (v2) of Docker's build-push action uses + # buildx, which comes with BuildKit features that help us speed + # up our builds using additional cache features. Buildx also + # has a lot of other features that are not as relevant to us. + # + # See https://github.com/docker/build-push-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Github Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Build the container, including an inline cache manifest to + # allow us to use the registry as a cache source. + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/python-discord/site:latest + cache-to: type=inline + tags: | + ghcr.io/python-discord/site:latest + ghcr.io/python-discord/site:${{ steps.sha_tag.outputs.tag }} + build-args: | + git_sha=${{ github.sha }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 8760b35e..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: "Code scanning - action" - -on: - push: - pull_request: - schedule: - - cron: '0 12 * * *' - -jobs: - CodeQL-Build: - - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - fetch-depth: 2 - - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: python - - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 00000000..efc08040 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,46 @@ +name: Deploy + +on: + workflow_run: + workflows: ["Build"] + branches: + - main + types: + - completed + +jobs: + deploy: + if: github.event.workflow_run.conclusion == 'success' + name: Deploy to Kubernetes Cluster + runs-on: ubuntu-latest + environment: production + + steps: + - name: Create SHA Container Tag + id: sha_tag + run: | + tag=$(cut -c 1-7 <<< $GITHUB_SHA) + echo "::set-output name=tag::$tag" + + # Check out the private Kubernetes repository for the + # deployment.yaml file using a GitHub Personal Access + # Token to get access. + - name: Checkout code + uses: actions/checkout@v2 + with: + repository: python-discord/kubernetes + token: ${{ secrets.REPO_TOKEN }} + + - name: Authenticate with Kubernetes + uses: azure/k8s-set-context@v1 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBECONFIG }} + + - name: Deploy to Kubernetes + uses: Azure/k8s-deploy@v1 + with: + manifests: | + site/deployment.yaml + images: 'ghcr.io/python-discord/site:${{ steps.sha_tag.outputs.tag }}' + kubectl-version: 'latest' diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml new file mode 100644 index 00000000..9e3d331d --- /dev/null +++ b/.github/workflows/lint-test.yaml @@ -0,0 +1,142 @@ +name: Lint & Test + +on: + push: + branches: + - main + pull_request: + + +jobs: + lint-test: + runs-on: ubuntu-latest + env: + # Configure pip to cache dependencies and do a user install + PIP_NO_CACHE_DIR: false + PIP_USER: 1 + + # Hide the graphical elements from pipenv's output + PIPENV_HIDE_EMOJIS: 1 + PIPENV_NOSPIN: 1 + + # Make sure pipenv does not try reuse an environment it's running in + PIPENV_IGNORE_VIRTUALENVS: 1 + + # Specify explicit paths for python dependencies and the pre-commit + # environment so we know which directories to cache + PYTHONUSERBASE: ${{ github.workspace }}/.cache/py-user-base + PRE_COMMIT_HOME: ${{ github.workspace }}/.cache/pre-commit-cache + + steps: + - name: Add custom PYTHONUSERBASE to PATH + run: echo '${{ env.PYTHONUSERBASE }}/bin/' >> $GITHUB_PATH + + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup python + id: python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + + # Start the database early to give it a chance to get ready before + # we start running tests. + - name: Run database using docker-compose + run: docker-compose run -d -p 7777:5432 --name pydis_web postgres + + # This step caches our Python dependencies. To make sure we + # only restore a cache when the dependencies, the python version, + # the runner operating system, and the dependency location haven't + # changed, we create a cache key that is a composite of those states. + # + # Only when the context is exactly the same, we will restore the cache. + - name: Python Dependency Caching + uses: actions/cache@v2 + id: python_cache + with: + path: ${{ env.PYTHONUSERBASE }} + key: "python-0-${{ runner.os }}-${{ env.PYTHONUSERBASE }}-\ + ${{ steps.python.outputs.python-version }}-\ + ${{ hashFiles('./Pipfile', './Pipfile.lock') }}" + + # Install our dependencies if we did not restore a dependency cache + - name: Install dependencies using pipenv + if: steps.python_cache.outputs.cache-hit != 'true' + run: | + pip install pipenv + pipenv install --dev --deploy --system + + # This step caches our pre-commit environment. To make sure we + # do create a new environment when our pre-commit setup changes, + # we create a cache key based on relevant factors. + - name: Pre-commit Environment Caching + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: "precommit-0-${{ runner.os }}-${{ env.PRE_COMMIT_HOME }}-\ + ${{ steps.python.outputs.python-version }}-\ + ${{ hashFiles('./.pre-commit-config.yaml') }}" + + # We will not run `flake8` here, as we will use a separate flake8 + # action. As pre-commit does not support user installs, we set + # PIP_USER=0 to not do a user install. + - name: Run pre-commit hooks + run: export PIP_USER=0; SKIP=flake8 pre-commit run --all-files + + # Run flake8 and have it format the linting errors in the format of + # the GitHub Workflow command to register error annotations. This + # means that our flake8 output is automatically added as an error + # annotation to both the run result and in the "Files" tab of a + # pull request. + # + # Format used: + # ::error file={filename},line={line},col={col}::{message} + - name: Run flake8 + run: "flake8 \ + --format='::error file=%(path)s,line=%(row)d,col=%(col)d::\ + [flake8] %(code)s: %(text)s'" + + - name: Migrations and run tests with coverage.py + run: | + python manage.py makemigrations --check + python manage.py migrate + coverage run manage.py test --no-input + coverage report -m + env: + CI: True + DATABASE_URL: postgres://pysite:pysite@localhost:7777/pysite + METRICITY_DB_URL: postgres://pysite:pysite@localhost:7777/metricity + + # This step will publish the coverage reports coveralls.io and + # print a "job" link in the output of the GitHub Action + - name: Publish coverage report to coveralls.io + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: coveralls + + - name: Tear down docker-compose containers + run: docker-compose stop + if: ${{ always() }} + + # Prepare the Pull Request Payload artifact. If this fails, we + # we fail silently using the `continue-on-error` option. It's + # nice if this succeeds, but if it fails for any reason, it + # does not mean that our lint-test checks failed. + - name: Prepare Pull Request Payload artifact + id: prepare-artifact + if: always() && github.event_name == 'pull_request' + continue-on-error: true + run: cat $GITHUB_EVENT_PATH | jq '.pull_request' > pull_request_payload.json + + # This only makes sense if the previous step succeeded. To + # get the original outcome of the previous step before the + # `continue-on-error` conclusion is applied, we use the + # `.outcome` value. This step also fails silently. + - name: Upload a Build Artifact + if: always() && steps.prepare-artifact.outcome == 'success' + continue-on-error: true + uses: actions/upload-artifact@v2 + with: + name: pull-request-payload + path: pull_request_payload.json diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml index 87c85277..a3df5b1a 100644 --- a/.github/workflows/sentry-release.yml +++ b/.github/workflows/sentry-release.yml @@ -3,14 +3,14 @@ name: Create Sentry release on: push: branches: - - master + - main jobs: createSentryRelease: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@master + uses: actions/checkout@main - name: Create a Sentry.io release uses: tclindner/sentry-releases-action@v1.2.0 env: @@ -20,4 +20,4 @@ jobs: with: tagName: ${{ github.sha }} environment: production - releaseNamePrefix: pydis-site@ + releaseNamePrefix: site@ diff --git a/.github/workflows/status_embed.yaml b/.github/workflows/status_embed.yaml new file mode 100644 index 00000000..b6a71b88 --- /dev/null +++ b/.github/workflows/status_embed.yaml @@ -0,0 +1,78 @@ +name: Status Embed + +on: + workflow_run: + workflows: + - Lint & Test + - Build + - Deploy + types: + - completed + +jobs: + status_embed: + # We need to send a status embed whenever the workflow + # sequence we're running terminates. There are a number + # of situations in which that happens: + # + # 1. We reach the end of the Deploy workflow, without + # it being skipped. + # + # 2. A `pull_request` triggered a Lint & Test workflow, + # as the sequence always terminates with one run. + # + # 3. If any workflow ends in failure or was cancelled. + if: >- + (github.event.workflow_run.name == 'Deploy' && github.event.workflow_run.conclusion != 'skipped') || + github.event.workflow_run.event == 'pull_request' || + github.event.workflow_run.conclusion == 'failure' || + github.event.workflow_run.conclusion == 'cancelled' + name: Send Status Embed to Discord + runs-on: ubuntu-latest + + steps: + # A workflow_run event does not contain all the information + # we need for a PR embed. That's why we upload an artifact + # with that information in the Lint workflow. + - name: Get Pull Request Information + id: pr_info + if: github.event.workflow_run.event == 'pull_request' + run: | + curl -s -H "Authorization: token $GITHUB_TOKEN" ${{ github.event.workflow_run.artifacts_url }} > artifacts.json + DOWNLOAD_URL=$(cat artifacts.json | jq -r '.artifacts[] | select(.name == "pull-request-payload") | .archive_download_url') + [ -z "$DOWNLOAD_URL" ] && exit 1 + wget --quiet --header="Authorization: token $GITHUB_TOKEN" -O pull_request_payload.zip $DOWNLOAD_URL || exit 2 + unzip -p pull_request_payload.zip > pull_request_payload.json + [ -s pull_request_payload.json ] || exit 3 + echo "::set-output name=pr_author_login::$(jq -r '.user.login // empty' pull_request_payload.json)" + echo "::set-output name=pr_number::$(jq -r '.number // empty' pull_request_payload.json)" + echo "::set-output name=pr_title::$(jq -r '.title // empty' pull_request_payload.json)" + echo "::set-output name=pr_source::$(jq -r '.head.label // empty' pull_request_payload.json)" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Send an informational status embed to Discord instead of the + # standard embeds that Discord sends. This embed will contain + # more information and we can fine tune when we actually want + # to send an embed. + - name: GitHub Actions Status Embed for Discord + uses: SebastiaanZ/github-status-embed-for-discord@v0.2.1 + with: + # Our GitHub Actions webhook + webhook_id: '784184528997842985' + webhook_token: ${{ secrets.GHA_WEBHOOK_TOKEN }} + + # Workflow information + workflow_name: ${{ github.event.workflow_run.name }} + run_id: ${{ github.event.workflow_run.id }} + run_number: ${{ github.event.workflow_run.run_number }} + status: ${{ github.event.workflow_run.conclusion }} + actor: ${{ github.actor }} + repository: ${{ github.repository }} + ref: ${{ github.ref }} + sha: ${{ github.event.workflow_run.head_sha }} + + pr_author_login: ${{ steps.pr_info.outputs.pr_author_login }} + pr_number: ${{ steps.pr_info.outputs.pr_number }} + pr_title: ${{ steps.pr_info.outputs.pr_title }} + pr_source: ${{ steps.pr_info.outputs.pr_source }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be57904e..a66bf97c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,6 @@ repos: name: Flake8 description: This hook runs flake8 within our project's pipenv environment. entry: pipenv run flake8 - language: python + language: system types: [python] require_serial: true diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..57ccd80e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +The Python Discord Code of Conduct can be found [on our website](https://pydis.com/coc). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de682a31..f20b5316 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,124 +1,3 @@ -# Contributing to one of Our Projects +# Contributing Guidelines -Our projects are open-source and are automatically deployed whenever commits are pushed to the `master` branch on each repository, so we've created a set of guidelines in order to keep everything clean and in working order. - -Note that contributions may be rejected on the basis of a contributor failing to follow these guidelines. - -## Rules - -1. **No force-pushes** or modifying the Git history in any way. -2. If you have direct access to the repository, **create a branch for your changes** and create a pull request for that branch. If not, create a branch on a fork of the repository and create a pull request from there. - * It's common practice for a repository to reject direct pushes to `master`, so make branching a habit! - * If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This gives permission for maintainers to commit changes directly to your fork, speeding up the review process. -3. **Adhere to the prevailing code style**, which we enforce using [`flake8`](http://flake8.pycqa.org/en/latest/index.html) and [`pre-commit`](https://pre-commit.com/). - * Run `flake8` and `pre-commit` against your code [**before** you push it](https://soundcloud.com/lemonsaurusrex/lint-before-you-push). Your commit will be rejected by the build server if it fails to lint. - * [Git Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) are a powerful git feature for executing custom scripts when certain important git actions occur. The pre-commit hook is the first hook executed during the commit process and can be used to check the code being committed & abort the commit if issues, such as linting failures, are detected. While git hooks can seem daunting to configure, the `pre-commit` framework abstracts this process away from you and is provided as a dev dependency for this project. Run `pipenv run precommit` when setting up the project and you'll never have to worry about committing code that fails linting. -4. **Make great commits**. A well structured git log is key to a project's maintainability; it efficiently 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. - * Avoid making minor commits for fixing typos or linting errors. Since you've already set up a `pre-commit` hook to run the linting pipeline before a commit, you shouldn't be committing linting issues anyway. - * 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/) -5. **Avoid frequent pushes to the main repository**. This goes for PRs opened against your fork as well. Our test build pipelines are triggered every time a push to the repository (or PR) is made. Try to batch your commits until you've finished working for that session, or you've reached a point where collaborators need your commits to continue their own work. This also provides you the opportunity to amend commits for minor changes rather than having to commit them on their own because you've already pushed. - * This includes merging master into your branch. Try to leave merging from master for after your PR passes review; a maintainer will bring your PR up to date before merging. Exceptions to this include: resolving merge conflicts, needing something that was pushed to master for your branch, or something was pushed to master that could potentionally affect the functionality of what you're writing. -6. **Don't fight the framework**. Every framework has its flaws, but the frameworks we've picked out have been carefully chosen for their particular merits. If you can avoid it, please resist reimplementing swathes of framework logic - the work has already been done for you! -7. If someone is working on an issue or pull request, **do not open your own pull request for the same task**. Instead, collaborate with the author(s) of the existing pull request. Duplicate PRs opened without communicating with the other author(s) and/or PyDis staff will be closed. Communication is key, and there's no point in two separate implementations of the same thing. - * One option is to fork the other contributor's repository and submit your changes to their branch with your own pull request. We suggest following these guidelines when interacting with their repository as well. - * The author(s) of inactive PRs and claimed issues will be be pinged after a week of inactivity for an update. Continued inactivity may result in the issue being released back to the community and/or PR closure. -8. **Work as a team** and collaborate wherever possible. Keep things friendly and help each other out - these are shared projects and nobody likes to have their feet trodden on. -9. All API changes **must be validated against the bot** before PRing. Please don't leave this for reviewers to discover. Guides for setting up the developer environment can be [found below](#developer-environment). -10. All static content, such as images or audio, **must be licensed for open public use**. - * Static content must be hosted by a service designed to do so. Failing to do so is known as "leeching" and is frowned upon, as it generates extra bandwidth costs to the host without providing benefit. It would be best if appropriately licensed content is added to the repository itself so it can be served by PyDis' infrastructure. - -Above all, the needs of our community should come before the wants of an individual. Work together, build solutions to problems and try to do so in a way that people can learn from easily. Abuse of our trust may result in the loss of your Contributor role. - -## Changes to this Arrangement - -All projects evolve over time, and this contribution guide is no different. This document is open to pull requests or changes by contributors. If you believe you have something valuable to add or change, please don't hesitate to do so in a PR. - -## Supplemental Information -### Developer Environment -Instructions for setting up environments for both the site and the bot can be found on the PyDis Wiki: - * [Site](https://pythondiscord.com/pages/contributing/site/) - * [Bot](https://pythondiscord.com/pages/contributing/bot/) - -When pulling down changes from GitHub, remember to sync your environment using `pipenv sync --dev` to ensure you're using the most up-to-date versions the project's dependencies. - -### 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: - -```py -import typing as t - - -def foo(input_1: int, input_2: t.Dict[str, str]) -> bool: - ... -``` - -Tells us that `foo` accepts an `int` and a `dict`, with `str` keys and values, and returns a `bool`. - -All function declarations should be type hinted in code contributed to the PyDis organization. - -For more information, see *[PEP 483](https://www.python.org/dev/peps/pep-0483/) - The Theory of Type Hints* and Python's documentation for the [`typing`](https://docs.python.org/3/library/typing.html) module. - -### AutoDoc Formatting Directives -Many documentation packages provide support for automatic documentation generation from the codebase's docstrings. These tools utilize special formatting directives to enable richer formatting in the generated documentation. - -For example: - -```py -import typing as t - - -def foo(bar: int, baz: t.Optional[t.Dict[str, str]] = None) -> bool: - """ - Does some things with some stuff. - - :param bar: Some input - :param baz: Optional, some dictionary with string keys and values - - :return: Some boolean - """ - ... -``` - -Since PyDis does not utilize automatic documentation generation, use of this syntax should not be used in code contributed to the organization. Should the purpose and type of the input variables not be easily discernable from the variable name and type annotation, a prose explanation can be used. Explicit references to variables, functions, classes, etc. should be wrapped with backticks (`` ` ``). - -For example, the above docstring would become: - -```py -import typing as t - - -def foo(bar: int, baz: t.Optional[t.Dict[str, str]] = None) -> bool: - """ - Does some things with some stuff. - - This function takes an index, `bar` and checks for its presence in the database `baz`, passed as a dictionary. Returns `False` if `baz` is not passed. - """ - ... -``` - -### Logging Levels -The project currently defines [`logging`](https://docs.python.org/3/library/logging.html) 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. -* **DEBUG:** These events should add context to what's happening in a development setup to make it easier to follow what's going while working 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 have not caused a failure. - * **NOTE:** Events at this logging level and higher should be reserved for events that require the attention of the DevOps team. -* **ERROR:** These events have caused a failure in a specific part of the application and require urgent attention. -* **CRITICAL:** These events have caused the whole application to fail and require immediate intervention. - -Ensure that log messages are succinct. Should you want to pass additional useful information that would otherwise make the log message overly verbose the `logging` module accepts an `extra` kwarg, which can be used to pass a dictionary. This is used to populate the `__dict__` of the `LogRecord` created for the logging event with user-defined attributes that can be accessed by a log handler. Additional information and caveats may be found [in Python's `logging` documentation](https://docs.python.org/3/library/logging.html#logging.Logger.debug). - -### Work in Progress (WIP) PRs -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 WIP. 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. - -As stated earlier, **ensure that "Allow edits from maintainers" is checked**. This gives permission for maintainers to commit changes directly to your fork, speeding up the review process. - -## Footnotes - -This document was inspired by the [Glowstone contribution guidelines](https://github.com/GlowstoneMC/Glowstone/blob/dev/docs/CONTRIBUTING.md). +The Contributing Guidelines for Python Discord projects can be found [on our website](https://pydis.com/contributing.md). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5d8ba5da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.8-slim-buster + +# Allow service to handle stops gracefully +STOPSIGNAL SIGQUIT + +# Set pip to have cleaner logs and no saved cache +ENV PIP_NO_CACHE_DIR=false \ + PIPENV_HIDE_EMOJIS=1 \ + PIPENV_NOSPIN=1 + +# Install pipenv +RUN pip install -U pipenv + +# Copy the project files into working directory +WORKDIR /app + +# Copy dependency files +COPY Pipfile Pipfile.lock ./ + +# Install project dependencies +RUN pipenv install --system --deploy + +# Copy project code +COPY . . + +# Set Git SHA environment variable +ARG git_sha="development" +ENV GIT_SHA=$git_sha + +# Run web server through custom manager +ENTRYPOINT ["python", "manage.py"] +CMD ["run"] diff --git a/LICENSE b/LICENSE index 15f00946..dd39e18d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Python Discord +Copyright (c) 2018-2020 Python Discord Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Pipfile b/Pipfile index 9ec75e84..ca467c9e 100644 --- a/Pipfile +++ b/Pipfile @@ -9,14 +9,13 @@ django-environ = "~=0.4.5" django-filter = "~=2.1.0" django-hosts = "~=4.0" djangorestframework = "~=3.11.0" -djangorestframework-bulk = "~=0.2.1" psycopg2-binary = "~=2.8" django-simple-bulma = "~=2.1" whitenoise = "~=5.0" requests = "~=2.21" pyyaml = "~=5.1" -pyuwsgi = {version = "~=2.0", sys_platform = "!='win32'"} -sentry-sdk = "~=0.14" +gunicorn = "~=20.0.4" +sentry-sdk = "~=0.19" gitpython = "~=3.1.7" markdown = "~=3.3.4" python-frontmatter = "~=1.0" @@ -35,11 +34,11 @@ flake8-todo = "~=0.7" mccabe = "~=0.6.1" pep8-naming = "~=0.9" pre-commit = "~=2.1" -unittest-xml-reporting = "~=3.0" pyfakefs = "~=4.4.0" +coveralls = "~=2.1" [requires] -python_version = "3.7" +python_version = "3.8" [scripts] start = "python manage.py run --debug" diff --git a/Pipfile.lock b/Pipfile.lock index 8a81b34d..ab363c10 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "a3ca73db3d86d7e8e7cc833ea4fe6aa464332d061c6319bf0c83ee4cb9ed7a27" + "sha256": "44f16cc8889bf8e8fc7ab0cbf9a7b95ac14da10341326e6a9961e24a5fa5df4c" }, "pipfile-spec": 6, "requires": { - "python_version": "3.7" + "python_version": "3.8" }, "sources": [ { @@ -18,11 +18,11 @@ "default": { "asgiref": { "hashes": [ - "sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17", - "sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0" + "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", + "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" ], - "markers": "python_version >= '3.5'", - "version": "==3.3.1" + "markers": "python_version >= '3.6'", + "version": "==3.3.4" }, "certifi": { "hashes": [ @@ -41,11 +41,11 @@ }, "django": { "hashes": [ - "sha256:2afe4900667bcceac792fa34b4fb25448c4fd950d8b32c5508b3442c4b10442a", - "sha256:6f13c3e8109236129c49d65a42fbf30c928e66b05ca6862246061b9343ecbaf2" + "sha256:9bc7aa619ed878fedba62ce139abe663a147dccfd20e907725ec11e02a1ca225", + "sha256:d58d8394036db75a81896037d757357e79406e8f68816c3e8a28721c1d9d4c11" ], "index": "pypi", - "version": "==3.0.13" + "version": "==3.0.14" }, "django-environ": { "hashes": [ @@ -73,11 +73,11 @@ }, "django-simple-bulma": { "hashes": [ - "sha256:048d957a4c2a3c37c461082d07c473e72abef2f706a619bc069a78bef98c84ac", - "sha256:0894e247b7f6e88432894f10e7c051fba867df5cd645e42de2b94892dacaeeba" + "sha256:38530d787b2b6a091b480f7cbb8841a3b3709733ebfa5e543ae339c3d8fece03", + "sha256:dfc34839e050d5e4749498806ed7ee8c77794021efa54ab224a2de925c644fea" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.2.0" }, "djangorestframework": { "hashes": [ @@ -87,13 +87,6 @@ "index": "pypi", "version": "==3.11.2" }, - "djangorestframework-bulk": { - "hashes": [ - "sha256:39230d8379acebd86d313df6c9150cafecb636eae1d097c30a26389ab9fee5b1" - ], - "index": "pypi", - "version": "==0.2.1" - }, "gitdb": { "hashes": [ "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", @@ -104,11 +97,19 @@ }, "gitpython": { "hashes": [ - "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b", - "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61" + "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135", + "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e" + ], + "index": "pypi", + "version": "==3.1.17" + }, + "gunicorn": { + "hashes": [ + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" ], "index": "pypi", - "version": "==3.1.14" + "version": "==20.0.4" }, "idna": { "hashes": [ @@ -118,14 +119,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, - "importlib-metadata": { - "hashes": [ - "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a", - "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe" - ], - "markers": "python_version < '3.8'", - "version": "==3.10.0" - }, "libsass": { "hashes": [ "sha256:1521d2a8d4b397c6ec90640a1f6b5529077035efc48ef1c2e53095544e713d1b", @@ -208,27 +201,6 @@ ], "version": "==2021.1" }, - "pyuwsgi": { - "hashes": [ - "sha256:0bd14517398f494d828d77a9bf72b5a6cbef0112e1cc05e9a0080fa8828ccfa0", - "sha256:149675b2e020b0e833e8b871a545751ca346cbfed85c8fd2b320a01d40dc3d8f", - "sha256:285e263a9094389f13cfdefd033a4e99fbed3ad120dba9ac5093846cc03ac5ab", - "sha256:297d1d0b8c472374b12eda7f17a9f5de67cf516612e42b71a7636afb9d1e3974", - "sha256:5439f0f3ef5d6bf1622f341662d04c1d92b88889db40b295419e5fe75a7c7d45", - "sha256:56ecda11e873b2eb937b33d2999766322eebfa82ee5b26a2196a335c4e786186", - "sha256:66a9751f28abf348e0ddccadc4ded47623f2d35cf9609c87b57909d55a4cdc15", - "sha256:890e7e863cb61c8369b6bcfa5d6f323753aaeec2cfaba16741f119c79b964aa7", - "sha256:90e4235020048456ad867aefc383cdf5528b7f6e327555ceec579c428a828759", - "sha256:94d4287b155aa789ce4b6f671c981f7d6c58fc3113330e2f29ac7926cb854645", - "sha256:a425f562f382a097ca49df26b70d47d12f0cf0abf233610f3f58b1f7f780355e", - "sha256:ac79dead0685beab5ecfe0926426849a44c5572528f89bb17f6ecf5eb561024e", - "sha256:bddeb8df77010d0f842068765a0b3155fdcfd847f14bc1ba89fc7e37914a13d2", - "sha256:dac4a04dc0f69d641dba984e83214d2c2cc098496c5d5585e7d3f4e7a9190f84" - ], - "index": "pypi", - "markers": "sys_platform != 'win32'", - "version": "==2.0.19.1.post0" - }, "pyyaml": { "hashes": [ "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", @@ -282,11 +254,11 @@ }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.15.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" }, "smmap": { "hashes": [ @@ -304,15 +276,6 @@ "markers": "python_version >= '3.5'", "version": "==0.4.1" }, - "typing-extensions": { - "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" - ], - "markers": "python_version < '3.8'", - "version": "==3.7.4.3" - }, "urllib3": { "hashes": [ "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", @@ -328,14 +291,6 @@ ], "index": "pypi", "version": "==5.2.0" - }, - "zipp": { - "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" - ], - "markers": "python_version >= '3.6'", - "version": "==3.4.1" } }, "develop": { @@ -348,11 +303,11 @@ }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "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": [ @@ -361,6 +316,13 @@ ], "version": "==1.7.0" }, + "certifi": { + "hashes": [ + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + ], + "version": "==2020.12.5" + }, "cfgv": { "hashes": [ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", @@ -369,6 +331,14 @@ "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, + "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" + }, "coverage": { "hashes": [ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", @@ -427,6 +397,14 @@ "index": "pypi", "version": "==5.5" }, + "coveralls": { + "hashes": [ + "sha256:2301a19500b06649d2ec4f2858f9c69638d7699a4c63027c5d53daba666147cc", + "sha256:b990ba1f7bc4288e63340be0433698c1efe8217f78c689d254c2540af3d38617" + ], + "index": "pypi", + "version": "==2.2.0" + }, "distlib": { "hashes": [ "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", @@ -434,6 +412,12 @@ ], "version": "==0.3.1" }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, "filelock": { "hashes": [ "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", @@ -443,19 +427,19 @@ }, "flake8": { "hashes": [ - "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff", - "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0" + "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", + "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" ], "index": "pypi", - "version": "==3.9.0" + "version": "==3.9.2" }, "flake8-annotations": { "hashes": [ - "sha256:40a4d504cdf64126ea0bdca39edab1608bc6d515e96569b7e7c3c59c84f66c36", - "sha256:eabbfb2dd59ae0e9835f509f930e79cd99fa4ff1026fe6ca073503a57407037c" + "sha256:0d6cd2e770b5095f09689c9d84cc054c51b929c41a68969ea1beb4b825cac515", + "sha256:d10c4638231f8a50c0a597c4efce42bd7b7d85df4f620a0ddaca526138936a4f" ], "index": "pypi", - "version": "==2.6.1" + "version": "==2.6.2" }, "flake8-bandit": { "hashes": [ @@ -505,11 +489,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": [ @@ -528,27 +512,27 @@ }, "gitpython": { "hashes": [ - "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b", - "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61" + "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135", + "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e" ], "index": "pypi", - "version": "==3.1.14" + "version": "==3.1.17" }, "identify": { "hashes": [ - "sha256:43cb1965e84cdd247e875dec6d13332ef5be355ddc16776396d98089b9053d87", - "sha256:c7c0f590526008911ccc5ceee6ed7b085cbc92f7b6591d0ee5913a130ad64034" + "sha256:9bcc312d4e2fa96c7abebcdfb1119563b511b5e3985ac52f60d9116277865b2e", + "sha256:ad9f3fa0c2316618dc4d840f627d474ab6de106392a4f00221820200f490f5a8" ], "markers": "python_full_version >= '3.6.1'", - "version": "==2.2.2" + "version": "==2.2.4" }, - "importlib-metadata": { + "idna": { "hashes": [ - "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a", - "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version < '3.8'", - "version": "==3.10.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "mccabe": { "hashes": [ @@ -560,18 +544,18 @@ }, "nodeenv": { "hashes": [ - "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", - "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c" + "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b", + "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7" ], - "version": "==1.5.0" + "version": "==1.6.0" }, "pbr": { "hashes": [ - "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", - "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" + "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", + "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" ], "markers": "python_version >= '2.6'", - "version": "==5.5.1" + "version": "==5.6.0" }, "pep8-naming": { "hashes": [ @@ -583,11 +567,11 @@ }, "pre-commit": { "hashes": [ - "sha256:94c82f1bf5899d56edb1d926732f4e75a7df29a0c8c092559c77420c9d62428b", - "sha256:de55c5c72ce80d79106e48beb1b54104d16495ce7f95b0c7b13d4784193a00af" + "sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712", + "sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9" ], "index": "pypi", - "version": "==2.11.1" + "version": "==2.12.1" }, "pycodestyle": { "hashes": [ @@ -656,13 +640,21 @@ "index": "pypi", "version": "==5.4.1" }, + "requests": { + "hashes": [ + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + ], + "index": "pypi", + "version": "==2.25.1" + }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.15.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" }, "smmap": { "hashes": [ @@ -692,77 +684,24 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==0.10.2" }, - "typed-ast": { - "hashes": [ - "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", - "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", - "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", - "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", - "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", - "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", - "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", - "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", - "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", - "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", - "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", - "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", - "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", - "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", - "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", - "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", - "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", - "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", - "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", - "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", - "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", - "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", - "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", - "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", - "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", - "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", - "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", - "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", - "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", - "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" - ], - "markers": "python_version < '3.8'", - "version": "==1.4.2" - }, - "typing-extensions": { - "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" - ], - "markers": "python_version < '3.8'", - "version": "==3.7.4.3" - }, - "unittest-xml-reporting": { - "hashes": [ - "sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca", - "sha256:984cebba69e889401bfe3adb9088ca376b3a1f923f0590d005126c1bffd1a695" - ], - "index": "pypi", - "version": "==3.0.4" - }, - "virtualenv": { + "urllib3": { "hashes": [ - "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107", - "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c" + "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", + "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.4.3" + "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" }, - "zipp": { + "virtualenv": { "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + "sha256:307a555cf21e1550885c82120eccaf5acedf42978fd362d32ba8410f9593f543", + "sha256:72cf267afc04bf9c86ec932329b7e94db6a0331ae9847576daaa7ca3c86b29a4" ], - "markers": "python_version >= '3.6'", - "version": "==3.4.1" + "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/README.md b/README.md index 616f2edc..f88c2cf7 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,25 @@ # Python Discord: Site -[![Discord](https://img.shields.io/static/v1?label=Python%20Discord&logo=discord&message=%3E95k%20members&color=%237289DA&logoColor=white)](https://discord.gg/2B963hn) -[![Build Status](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Site?branchName=master)](https://dev.azure.com/python-discord/Python%20Discord/_build/latest?definitionId=2&branchName=master) -[![Tests](https://img.shields.io/azure-devops/tests/python-discord/Python%20Discord/2?compact_message)](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Site?branchName=master) -[![Coverage](https://img.shields.io/azure-devops/coverage/python-discord/Python%20Discord/2/master)](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Site?branchName=master) -[![License](https://img.shields.io/github/license/python-discord/site)](LICENSE) -[![Status](https://img.shields.io/website?url=https%3A%2F%2Fpythondiscord.com)][1] +[![Discord][10]][11] +[![Lint & Test][1]][2] +[![Build & Deploy][3]][4] +[![Coverage Status][5]][6] +[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) -This is all of the code that is responsible for maintaining [our website][1] and all of its subdomains. +This is all of the code that is responsible for maintaining [our website][7] and all of its subdomains. The website is built on Django and should be simple to set up and get started with. If you happen to run into issues with setup, please don't hesitate to open an issue! -If you're looking to contribute or play around with the code, take a look at [the wiki][2] or the [`docs` directory](docs). If you're looking for things to do, check out [our issues][3]. +If you're looking to contribute or play around with the code, take a look at [the wiki][8] or the [`docs` directory](docs). If you're looking for things to do, check out [our issues][9]. -[1]: https://pythondiscord.com -[2]: https://pythondiscord.com/pages/contributing/site/ -[3]: https://github.com/python-discord/site/issues +[1]: https://github.com/python-discord/site/workflows/Lint%20&%20Test/badge.svg?branch=main +[2]: https://github.com/python-discord/site/actions?query=workflow%3A%22Lint+%26+Test%22+branch%3Amain +[3]: https://github.com/python-discord/site/workflows/Build%20&%20Deploy/badge.svg?branch=main +[4]: https://github.com/python-discord/site/actions?query=workflow%3A%22Build+%26+Deploy%22+branch%3Amain +[5]: https://coveralls.io/repos/github/python-discord/site/badge.svg?branch=main +[6]: https://coveralls.io/github/python-discord/site?branch=main +[7]: https://pythondiscord.com +[8]: https://pythondiscord.com/pages/contributing/site/ +[9]: https://github.com/python-discord/site/issues +[10]: https://raw.githubusercontent.com/python-discord/branding/main/logos/badge/badge_github.svg +[11]: https://discord.gg/python diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..fa5a88a3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security Notice + +The Security Notice for Python Discord projects can be found [on our website](https://pydis.com/security.md). diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index f273dad3..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,102 +0,0 @@ -# https://aka.ms/yaml - -jobs: - - job: test - displayName: 'Test & Lint' - pool: - vmImage: ubuntu-16.04 - - variables: - PIP_CACHE_DIR: .cache/pip - PRE_COMMIT_HOME: $(Pipeline.Workspace)/pre-commit-cache - - steps: - - task: UsePythonVersion@0 - displayName: 'Set Python Version' - name: PythonVersion - inputs: - versionSpec: '3.7.x' - addToPath: true - - - task: DockerCompose@0 - displayName: 'Setup Database' - inputs: - action: Run a specific service - dockerComposeFile: docker-compose.yml - projectName: pydis_web - serviceName: postgres - ports: '7777:5432' - - - script: | - pip install pipenv - pipenv install --dev --system - pip install flake8-formatter-junit-xml - displayName: 'Install Project Environment' - - # Create an executable shell script which replaces the original pipenv binary. - # The shell script ignores the first argument and executes the rest of the args as a command. - # It makes the `pipenv run flake8` command in the pre-commit hook work by circumventing - # pipenv entirely, which is too dumb to know it should use the system interpreter rather than - # creating a new venv. - - script: | - printf '%s\n%s' '#!/bin/bash' '"${@:2}"' > $(PythonVersion.pythonLocation)/bin/pipenv \ - && chmod +x $(PythonVersion.pythonLocation)/bin/pipenv - displayName: 'Mock pipenv binary' - - - task: Cache@2 - displayName: 'Restore pre-commit environment' - inputs: - key: pre-commit | "$(PythonVersion.pythonLocation)" | .pre-commit-config.yaml - restoreKeys: | - pre-commit | "$(PythonVersion.pythonLocation)" - path: $(PRE_COMMIT_HOME) - - # flake8 runs so it can generate the XML output. pre-commit will run it again to show stdout. - # flake8 standalone runs first to avoid any fixes pre-commit hooks may make. - - script: flake8 --format junit-xml --output-file TEST-lint.xml; pre-commit run --all-files - displayName: 'Run pre-commit hooks' - - - script: | - python3 manage.py makemigrations --check - python3 manage.py migrate - coverage run \ - manage.py test \ - --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner \ - --no-input - env: - CI: azure - DATABASE_URL: postgres://pysite:pysite@localhost:7777/pysite - displayName: 'Run Tests' - - - script: coverage report -m && coverage xml - displayName: 'Generate Coverage Reports' - - - task: PublishTestResults@2 - condition: succeededOrFailed() - displayName: 'Publish Test & Linting Results' - inputs: - testResultsFiles: '**/TEST-*.xml' - testRunTitle: 'Site Test Results' - - - task: PublishCodeCoverageResults@1 - displayName: 'Publish Coverage Results' - condition: succeededOrFailed() - inputs: - codeCoverageTool: Cobertura - summaryFileLocation: '**/coverage.xml' - - - job: build - displayName: 'Build & Push Container' - dependsOn: test - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranch'], 'refs/heads/master')) - - steps: - - task: Docker@2 - displayName: 'Build & Push Container' - inputs: - containerRegistry: 'DockerHub' - repository: 'pythondiscord/site' - command: 'buildAndPush' - Dockerfile: 'docker/Dockerfile' - buildContext: '.' - tags: 'latest' diff --git a/docker-compose.yml b/docker-compose.yml index 73d2ff85..1f49f1f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,11 +18,13 @@ services: POSTGRES_DB: pysite POSTGRES_PASSWORD: pysite POSTGRES_USER: pysite + volumes: + - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql web: build: context: . - dockerfile: docker/Dockerfile + dockerfile: Dockerfile command: ["run", "--debug"] networks: default: @@ -40,8 +42,10 @@ services: - staticfiles:/var/www/static environment: DATABASE_URL: postgres://pysite:pysite@postgres:5432/pysite + METRICITY_DB_URL: postgres://pysite:pysite@postgres:5432/metricity SECRET_KEY: suitable-for-development-only STATIC_ROOT: /var/www/static + DEBUG: 1 volumes: staticfiles: diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 97cb73d5..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM python:3.7-slim - -# Allow service to handle stops gracefully -STOPSIGNAL SIGQUIT - -# Set pip to have cleaner logs and no saved cache -ENV PIP_NO_CACHE_DIR=false \ - PIPENV_HIDE_EMOJIS=1 \ - PIPENV_NOSPIN=1 - -# Install git -RUN apt-get -y update \ - && apt-get install -y \ - git \ - && rm -rf /var/lib/apt/lists/* - -# Create non-root user. -RUN useradd --system --shell /bin/false --uid 1500 pysite - -# Install pipenv -RUN pip install -U pipenv - -# Copy the project files into working directory -WORKDIR /app -COPY . . - -# Install project dependencies -RUN pipenv install --system --deploy - -# Run web server through custom manager -ENTRYPOINT ["python", "manage.py"] -CMD ["run"] diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini deleted file mode 100644 index 3f35258c..00000000 --- a/docker/uwsgi.ini +++ /dev/null @@ -1,38 +0,0 @@ -[uwsgi] -### Exposed ports -# uWSGI protocol socket -socket = :4000 - -### File settings -# WSGI application -wsgi = pydis_site.wsgi:application -# Directory to move into at startup -chdir = /app - -### Concurrency options -# Run a master to supervise the workers -master = true -# Keep a minimum of 1 worker -cheaper = 1 -# Allow a maximum of 4 workers -workers = 4 -# Automatically set up meanginful process names -auto-procname = true -# Prefix process names with `pydis_site : ` -procname-prefix-spaced = pydis_site : - -### Worker options -# Kill workers if they take more than 30 seconds to respond. -harakiri = 30 - -### Startup settings -# Exit if we can't load the app -need-app = true -# `setuid` to an unprivileged user -uid = 1500 -# Do not use multiple interpreters -single-interpreter = true - -### Hook setup -# Gracefully kill workers on `SIGQUIT` -hook-master-start = unix_signal:3 gracefully_kill_them_all diff --git a/docs/deployment.md b/docs/deployment.md deleted file mode 100644 index e561b5d0..00000000 --- a/docs/deployment.md +++ /dev/null @@ -1,146 +0,0 @@ -# Deployment -The default Dockerfile should do a good job at running a solid web server that -automatically adjusts its worker count based on traffic. This is managed by -uWSGI. You need to configure the `DATABASE_URL` and `SECRET_KEY` variables. If -you want to deploy to a different host than the default, configure the -`ALLOWED_HOSTS` variable. - -## Static file hosting -You can either collect the static files in the container and use uWSGI to host -them, or put them on your host and manage them through a web server running on -the host like nginx. - -## Database migrations -To bring the schema up-to-date, first stop an existing database container, then -start a container that just runs the migrations and exits, and then starts the -main container off the new container again. - -## Ansible task -An example Ansible task to deploy the site is shown below, it should read fairly -humanly and give you a rough idea of steps needed to deploy the site. - -```yml -- name: ensure the `{{ pysite_pg_username }}` postgres user exists - become: yes - become_user: postgres - postgresql_user: - name: "{{ pysite_pg_username }}" - password: "{{ pysite_pg_password }}" - when: pysite_pg_host == 'localhost' - -- name: ensure the `{{ pysite_pg_database }}` postgres database exists - become: yes - become_user: postgres - postgresql_db: - name: "{{ pysite_pg_database }}" - owner: "{{ pysite_pg_username }}" - when: pysite_pg_host == 'localhost' - -- name: ensure the `{{ pysite_hub_repository }}` image is up-to-date - become: yes - docker_image: - name: "{{ pysite_hub_repository }}" - force: yes - -- name: ensure the nginx HTTP vhosts are up-to-date - become: yes - template: - src: "nginx/{{ item.key }}.http.conf.j2" - dest: "/etc/nginx/sites-available/{{ item.value }}.http.conf" - with_dict: "{{ pysite_domains }}" - notify: reload nginx - -- name: ensure the nginx HTTPS vhosts are up-to-date - become: yes - template: - src: "nginx/{{ item.key }}.https.conf.j2" - dest: "/etc/nginx/sites-available/{{ item.value }}.https.conf" - with_dict: "{{ pysite_domains }}" - notify: reload nginx - -- name: ensure the nginx HTTP vhosts are symlinked to `/etc/nginx/sites-enabled` - become: yes - file: - src: /etc/nginx/sites-available/{{ item.value }}.http.conf - dest: /etc/nginx/sites-enabled/{{ item.value }}.http.conf - state: link - with_dict: "{{ pysite_domains }}" - notify: reload nginx - -- name: ensure we have HTTPS certificates - include_role: - name: thefinn93.letsencrypt - vars: - letsencrypt_cert_domains: "{{ pysite_domains | dict2items | map(attribute='value') | list }}" - letsencrypt_email: "webmaster@example.com" - letsencrypt_renewal_command_args: '--renew-hook "systemctl restart nginx"' - letsencrypt_webroot_path: /var/www/_letsencrypt - -- name: ensure the nginx HTTPS vhosts are symlinked to `/etc/nginx/sites-enabled` - become: yes - file: - src: /etc/nginx/sites-available/{{ item.value }}.https.conf - dest: /etc/nginx/sites-enabled/{{ item.value }}.https.conf - state: link - with_dict: "{{ pysite_domains }}" - notify: reload nginx - -- name: ensure the web container is absent - become: yes - docker_container: - name: pysite - state: absent - -- name: ensure the `{{ pysite_static_file_dir }}` directory exists - become: yes - file: - path: "{{ pysite_static_file_dir }}" - state: directory - owner: root - group: root - -- name: collect static files - become: yes - docker_container: - image: "{{ pysite_hub_repository }}" - name: pysite-static-file-writer - command: python manage.py collectstatic --noinput - detach: no - cleanup: yes - network_mode: host - env: - DATABASE_URL: "{{ pysite_pg_database_url }}" - SECRET_KEY: "me-dont-need-no-secret-key" - STATIC_ROOT: "/html" - volumes: - - "/var/www/pythondiscord.com:/html" - -- name: ensure the database schema is up-to-date - become: yes - docker_container: - image: "{{ pysite_hub_repository }}" - name: pysite-migrator - detach: no - cleanup: yes - command: python manage.py migrate - network_mode: host - env: - DATABASE_URL: "postgres://{{ pysite_pg_username }}:{{ pysite_pg_password }}@{{ pysite_pg_host }}/{{ pysite_pg_database }}" - SECRET_KEY: "me-dont-need-no-secret-key" - -- name: ensure the website container is started - become: yes - docker_container: - image: "{{ pysite_hub_repository }}" - name: pysite - network_mode: host - restart: yes - restart_policy: unless-stopped - ports: - - "127.0.0.1:4000:4000" - env: - ALLOWED_HOSTS: "{{ pysite_domains | dict2items | map(attribute='value') | join(',') }}" - DATABASE_URL: "postgres://{{ pysite_pg_username }}:{{ pysite_pg_password }}@{{ pysite_pg_host }}/{{ pysite_pg_database }}" - PARENT_HOST: pysite.example.com - SECRET_KEY: "{{ pysite_secret_key }}" -``` diff --git a/manage.py b/manage.py index d4748a3a..71af23c4 100755 --- a/manage.py +++ b/manage.py @@ -10,7 +10,6 @@ import django from django.contrib.auth import get_user_model from django.core.management import call_command, execute_from_command_line - DEFAULT_ENVS = { "DJANGO_SETTINGS_MODULE": "pydis_site.settings", "SUPER_USERNAME": "admin", @@ -156,10 +155,25 @@ class SiteManager: call_command("runserver", "0.0.0.0:8000") return - import pyuwsgi - - # Run uwsgi for production server - pyuwsgi.run(["--ini", "docker/uwsgi.ini"]) + # Import gunicorn only if we aren't in debug mode. + import gunicorn.app.wsgiapp + + # Patch the arguments for gunicorn + sys.argv = [ + "gunicorn", + "--preload", + "-b", "0.0.0.0:8000", + "pydis_site.wsgi:application", + "--threads", "8", + "-w", "2", + "--max-requests", "1000", + "--max-requests-jitter", "50", + "--statsd-host", "graphite.default.svc.cluster.local:8125", + "--statsd-prefix", "site", + ] + + # Run gunicorn for the production server. + gunicorn.app.wsgiapp.run() def main() -> None: diff --git a/postgres/init.sql b/postgres/init.sql new file mode 100644 index 00000000..190a705c --- /dev/null +++ b/postgres/init.sql @@ -0,0 +1,146 @@ +CREATE DATABASE metricity; + +\c metricity; + +CREATE TABLE users ( + id varchar, + joined_at timestamp, + primary key(id) +); + +INSERT INTO users VALUES ( + 0, + current_timestamp +); + +INSERT INTO users VALUES ( + 1, + current_timestamp +); + +CREATE TABLE channels ( + id varchar, + name varchar, + primary key(id) +); + +INSERT INTO channels VALUES( + '267659945086812160', + 'python-general' +); + +INSERT INTO channels VALUES( + '11', + 'help-apple' +); + +INSERT INTO channels VALUES( + '12', + 'help-cherry' +); + +INSERT INTO channels VALUES( + '21', + 'ot0-hello' +); + +INSERT INTO channels VALUES( + '22', + 'ot1-world' +); + +INSERT INTO channels VALUES( + '31', + 'voice-chat-0' +); + +INSERT INTO channels VALUES( + '32', + 'code-help-voice-0' +); + +INSERT INTO channels VALUES( + '1234', + 'zebra' +); + +CREATE TABLE messages ( + id varchar, + author_id varchar references users(id), + is_deleted boolean, + created_at timestamp, + channel_id varchar references channels(id), + primary key(id) +); + +INSERT INTO messages VALUES( + 0, + 0, + false, + now(), + '267659945086812160' +); + +INSERT INTO messages VALUES( + 1, + 0, + false, + now() + INTERVAL '10 minutes,', + '1234' +); + +INSERT INTO messages VALUES( + 2, + 0, + false, + now(), + '11' +); + +INSERT INTO messages VALUES( + 3, + 0, + false, + now(), + '12' +); + +INSERT INTO messages VALUES( + 4, + 1, + false, + now(), + '21' +); + +INSERT INTO messages VALUES( + 5, + 1, + false, + now(), + '22' +); + +INSERT INTO messages VALUES( + 6, + 1, + false, + now(), + '31' +); + +INSERT INTO messages VALUES( + 7, + 1, + false, + now(), + '32' +); + +INSERT INTO messages VALUES( + 8, + 1, + true, + now(), + '32' +); diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 5093e605..449e660e 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -14,7 +14,6 @@ from .models import ( DeletedMessage, DocumentationLink, Infraction, - LogEntry, MessageDeletionContext, Nomination, OffTopicChannelName, @@ -22,6 +21,7 @@ from .models import ( Role, User ) +from .models.bot.nomination import NominationEntry admin.site.site_header = "Python Discord | Administration" admin.site.site_title = "Python Discord" @@ -120,33 +120,6 @@ class InfractionAdmin(admin.ModelAdmin): return False -@admin.register(LogEntry) -class LogEntryAdmin(admin.ModelAdmin): - """Allows viewing logs in the Django Admin without allowing edits.""" - - actions = None - list_display = ('timestamp', 'level', 'message') - fieldsets = ( - ('Overview', {'fields': ('timestamp', 'application', 'logger_name')}), - ('Metadata', {'fields': ('level', 'module', 'line')}), - ('Contents', {'fields': ('message',)}) - ) - list_filter = ('level', 'timestamp') - search_fields = ('message',) - - def has_add_permission(self, request: HttpRequest) -> bool: - """Deny manual LogEntry creation.""" - return False - - def has_change_permission(self, *args) -> bool: - """Prevent editing from django admin.""" - return False - - def has_delete_permission(self, request: HttpRequest, obj: Optional[LogEntry] = None) -> bool: - """Deny LogEntry deletion.""" - return False - - @admin.register(DeletedMessage) class DeletedMessageAdmin(admin.ModelAdmin): """Admin formatting for the DeletedMessage model.""" @@ -246,7 +219,7 @@ class NominationActorFilter(admin.SimpleListFilter): def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]: """Selectable values for viewer to filter by.""" - actor_ids = Nomination.objects.order_by().values_list("actor").distinct() + actor_ids = NominationEntry.objects.order_by().values_list("actor").distinct() actors = User.objects.filter(id__in=actor_ids) return ((a.id, a.username) for a in actors) @@ -254,7 +227,10 @@ class NominationActorFilter(admin.SimpleListFilter): """Query to filter the list of Users against.""" if not self.value(): return - return queryset.filter(actor__id=self.value()) + nomination_ids = NominationEntry.objects.filter( + actor__id=self.value() + ).values_list("nomination_id").distinct() + return queryset.filter(id__in=nomination_ids) @admin.register(Nomination) @@ -264,9 +240,6 @@ class NominationAdmin(admin.ModelAdmin): search_fields = ( "user__name", "user__id", - "actor__name", - "actor__id", - "reason", "end_reason" ) @@ -275,27 +248,25 @@ class NominationAdmin(admin.ModelAdmin): list_display = ( "user", "active", - "reason", - "actor", + "reviewed" ) fields = ( "user", "active", - "actor", - "reason", "inserted_at", "ended_at", - "end_reason" + "end_reason", + "reviewed" ) - # only allow reason fields to be edited. + # only allow end reason field to be edited. readonly_fields = ( "user", "active", - "actor", "inserted_at", - "ended_at" + "ended_at", + "reviewed" ) def has_add_permission(self, *args) -> bool: @@ -303,6 +274,61 @@ class NominationAdmin(admin.ModelAdmin): return False +class NominationEntryActorFilter(admin.SimpleListFilter): + """Actor Filter for NominationEntry Admin list page.""" + + title = "Actor" + parameter_name = "actor" + + def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]: + """Selectable values for viewer to filter by.""" + actor_ids = NominationEntry.objects.order_by().values_list("actor").distinct() + actors = User.objects.filter(id__in=actor_ids) + return ((a.id, a.username) for a in actors) + + def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: + """Query to filter the list of Users against.""" + if not self.value(): + return + return queryset.filter(actor__id=self.value()) + + +@admin.register(NominationEntry) +class NominationEntryAdmin(admin.ModelAdmin): + """Admin formatting for the NominationEntry model.""" + + search_fields = ( + "actor__name", + "actor__id", + "reason", + ) + + list_filter = (NominationEntryActorFilter,) + + list_display = ( + "nomination", + "actor", + ) + + fields = ( + "nomination", + "actor", + "reason", + "inserted_at", + ) + + # only allow reason field to be edited + readonly_fields = ( + "nomination", + "actor", + "inserted_at", + ) + + def has_add_permission(self, request: HttpRequest) -> bool: + """Disable adding new nomination entry from admin.""" + return False + + @admin.register(OffTopicChannelName) class OffTopicChannelNameAdmin(admin.ModelAdmin): """Admin formatting for the OffTopicChannelName model.""" diff --git a/pydis_site/apps/api/dblogger.py b/pydis_site/apps/api/dblogger.py deleted file mode 100644 index 4b4e3a9d..00000000 --- a/pydis_site/apps/api/dblogger.py +++ /dev/null @@ -1,22 +0,0 @@ -from logging import LogRecord, StreamHandler - - -class DatabaseLogHandler(StreamHandler): - """Logs entries into the database.""" - - def emit(self, record: LogRecord) -> None: - """Write the given `record` into the database.""" - # This import needs to be deferred due to Django's application - # registry instantiation logic loading this handler before the - # application is ready. - from pydis_site.apps.api.models.log_entry import LogEntry - - entry = LogEntry( - application='site', - logger_name=record.name, - level=record.levelname.lower(), - module=record.module, - line=record.lineno, - message=self.format(record) - ) - entry.save() diff --git a/pydis_site/apps/api/migrations/0064_delete_logentry.py b/pydis_site/apps/api/migrations/0064_delete_logentry.py new file mode 100644 index 00000000..a5f344d1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0064_delete_logentry.py @@ -0,0 +1,16 @@ +# Generated by Django 3.0.9 on 2020-10-03 06:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0063_Allow_blank_or_null_for_nomination_reason'), + ] + + operations = [ + migrations.DeleteModel( + name='LogEntry', + ), + ] diff --git a/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py b/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py new file mode 100644 index 00000000..298416db --- /dev/null +++ b/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.9 on 2020-10-03 07:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0064_delete_logentry'), + ('api', '0065_auto_20200919_2033'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py b/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py new file mode 100644 index 00000000..9a940ff4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.10 on 2020-10-10 16:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0066_merge_20201003_0730'), + ] + + operations = [ + migrations.AlterField( + model_name='infraction', + name='type', + field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The type of the infraction.', max_length=9), + ), + ] diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py new file mode 100644 index 00000000..79825ed7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py @@ -0,0 +1,75 @@ +# Generated by Django 3.0.11 on 2021-02-21 15:32 + +from django.apps.registry import Apps +from django.db import backends, migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +import django.db.models.deletion +import pydis_site.apps.api.models.mixins + + +def migrate_nominations(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + Nomination = apps.get_model("api", "Nomination") + NominationEntry = apps.get_model("api", "NominationEntry") + + for nomination in Nomination.objects.all(): + nomination_entry = NominationEntry( + nomination=nomination, + actor=nomination.actor, + reason=nomination.reason, + inserted_at=nomination.inserted_at + ) + nomination_entry.save() + + +def unmigrate_nominations(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + Nomination = apps.get_model("api", "Nomination") + NominationEntry = apps.get_model("api", "NominationEntry") + + for entry in NominationEntry.objects.all(): + nomination = Nomination.objects.get(pk=entry.nomination.id) + nomination.actor = entry.actor + nomination.reason = entry.reason + nomination.inserted_at = entry.inserted_at + + nomination.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0067_add_voice_ban_infraction_type'), + ] + + operations = [ + migrations.CreateModel( + name='NominationEntry', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reason', models.TextField(blank=True, help_text='Why the actor nominated this user.', default="")), + ('inserted_at', + models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination entry.')), + ('actor', models.ForeignKey(help_text='The staff member that nominated this user.', + on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set', + to='api.User')), + ('nomination', models.ForeignKey(help_text='The nomination this entry belongs to.', + on_delete=django.db.models.deletion.CASCADE, to='api.Nomination', + related_name='entries')), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + options={'ordering': ('-inserted_at',), 'verbose_name_plural': 'nomination entries'} + ), + migrations.RunPython(migrate_nominations, unmigrate_nominations), + migrations.RemoveField( + model_name='nomination', + name='actor', + ), + migrations.RemoveField( + model_name='nomination', + name='reason', + ), + migrations.AddField( + model_name='nomination', + name='reviewed', + field=models.BooleanField(default=False, help_text='Whether a review was made.'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0069_documentationlink_validators.py b/pydis_site/apps/api/migrations/0069_documentationlink_validators.py new file mode 100644 index 00000000..347c0e1a --- /dev/null +++ b/pydis_site/apps/api/migrations/0069_documentationlink_validators.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.11 on 2021-03-26 18:21 + +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.documentation_link + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0068_split_nomination_tables'), + ] + + operations = [ + migrations.AlterField( + model_name='documentationlink', + name='base_url', + field=models.URLField(help_text='The base URL from which documentation will be available for this project. Used to generate links to various symbols within this package.', validators=[pydis_site.apps.api.models.bot.documentation_link.ends_with_slash_validator]), + ), + migrations.AlterField( + model_name='documentationlink', + name='package', + field=models.CharField(help_text='The Python package name that this documentation link belongs to.', max_length=50, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(message='Package names can only consist of lowercase a-z letters, digits, and underscores.', regex='^[a-z0-9_]+$')]), + ), + ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index e3f928e1..fd5bf220 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -8,10 +8,10 @@ from .bot import ( Message, MessageDeletionContext, Nomination, + NominationEntry, OffensiveMessage, OffTopicChannelName, Reminder, Role, User ) -from .log_entry import LogEntry diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 1673b434..ac864de3 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -6,7 +6,7 @@ from .documentation_link import DocumentationLink from .infraction import Infraction from .message import Message from .message_deletion_context import MessageDeletionContext -from .nomination import Nomination +from .nomination import Nomination, NominationEntry from .off_topic_channel_name import OffTopicChannelName from .offensive_message import OffensiveMessage from .reminder import Reminder diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py index 2a0ce751..3dcc71fc 100644 --- a/pydis_site/apps/api/models/bot/documentation_link.py +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -1,7 +1,20 @@ +from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator from django.db import models from pydis_site.apps.api.models.mixins import ModelReprMixin +package_name_validator = RegexValidator( + regex=r"^[a-z0-9_]+$", + message="Package names can only consist of lowercase a-z letters, digits, and underscores." +) + + +def ends_with_slash_validator(string: str) -> None: + """Raise a ValidationError if `string` does not end with a slash.""" + if not string.endswith("/"): + raise ValidationError("The entered URL must end with a slash.") + class DocumentationLink(ModelReprMixin, models.Model): """A documentation link used by the `!docs` command of the bot.""" @@ -9,13 +22,15 @@ class DocumentationLink(ModelReprMixin, models.Model): package = models.CharField( primary_key=True, max_length=50, + validators=(package_name_validator,), help_text="The Python package name that this documentation link belongs to." ) base_url = models.URLField( help_text=( "The base URL from which documentation will be available for this project. " "Used to generate links to various symbols within this package." - ) + ), + validators=(ends_with_slash_validator,) ) inventory_url = models.URLField( help_text="The URL at which the Sphinx inventory is available for this package." diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 7660cbba..60c1e8dd 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -15,7 +15,8 @@ class Infraction(ModelReprMixin, models.Model): ("mute", "Mute"), ("kick", "Kick"), ("ban", "Ban"), - ("superstar", "Superstar") + ("superstar", "Superstar"), + ("voice_ban", "Voice Ban"), ) inserted_at = models.DateTimeField( default=timezone.now, diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py new file mode 100644 index 00000000..5daa5c66 --- /dev/null +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -0,0 +1,132 @@ +from typing import List, Tuple + +from django.db import connections + +BLOCK_INTERVAL = 10 * 60 # 10 minute blocks + +EXCLUDE_CHANNELS = [ + "267659945086812160", # Bot commands + "607247579608121354" # SeasonalBot commands +] + + +class NotFound(Exception): + """Raised when an entity cannot be found.""" + + pass + + +class Metricity: + """Abstraction for a connection to the metricity database.""" + + def __init__(self): + self.cursor = connections['metricity'].cursor() + + def __enter__(self): + return self + + def __exit__(self, *_): + self.cursor.close() + + def user(self, user_id: str) -> dict: + """Query a user's data.""" + # TODO: Swap this back to some sort of verified at date + columns = ["joined_at"] + query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'" + self.cursor.execute(query, [user_id]) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return dict(zip(columns, values)) + + def total_messages(self, user_id: str) -> int: + """Query total number of messages for a user.""" + self.cursor.execute( + """ + SELECT + COUNT(*) + FROM messages + WHERE + author_id = '%s' + AND NOT is_deleted + AND NOT %s::varchar[] @> ARRAY[channel_id] + """, + [user_id, EXCLUDE_CHANNELS] + ) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return values[0] + + def total_message_blocks(self, user_id: str) -> int: + """ + Query number of 10 minute blocks during which the user has been active. + + This metric prevents users from spamming to achieve the message total threshold. + """ + self.cursor.execute( + """ + SELECT + COUNT(*) + FROM ( + SELECT + (floor((extract('epoch' from created_at) / %s )) * %s) AS interval + FROM messages + WHERE + author_id='%s' + AND NOT is_deleted + AND NOT %s::varchar[] @> ARRAY[channel_id] + GROUP BY interval + ) block_query; + """, + [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id, EXCLUDE_CHANNELS] + ) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return values[0] + + def top_channel_activity(self, user_id: str) -> List[Tuple[str, int]]: + """ + Query the top three channels in which the user is most active. + + Help channels are grouped under "the help channels", + and off-topic channels are grouped under "off-topic". + """ + self.cursor.execute( + """ + SELECT + CASE + WHEN channels.name ILIKE 'help-%%' THEN 'the help channels' + WHEN channels.name ILIKE 'ot%%' THEN 'off-topic' + WHEN channels.name ILIKE '%%voice%%' THEN 'voice chats' + ELSE channels.name + END, + COUNT(1) + FROM + messages + LEFT JOIN channels ON channels.id = messages.channel_id + WHERE + author_id = '%s' AND NOT messages.is_deleted + GROUP BY + 1 + ORDER BY + 2 DESC + LIMIT + 3; + """, + [user_id] + ) + + values = self.cursor.fetchall() + + if not values: + raise NotFound() + + return values diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 11b9e36e..221d8534 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -5,23 +5,12 @@ from pydis_site.apps.api.models.mixins import ModelReprMixin class Nomination(ModelReprMixin, models.Model): - """A helper nomination created by staff.""" + """A general helper nomination information created by staff.""" active = models.BooleanField( default=True, help_text="Whether this nomination is still relevant." ) - actor = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text="The staff member that nominated this user.", - related_name='nomination_set' - ) - reason = models.TextField( - help_text="Why this user was nominated.", - null=True, - blank=True - ) user = models.ForeignKey( User, on_delete=models.CASCADE, @@ -42,6 +31,10 @@ class Nomination(ModelReprMixin, models.Model): help_text="When the nomination was ended.", null=True ) + reviewed = models.BooleanField( + default=False, + help_text="Whether a review was made." + ) def __str__(self): """Representation that makes the target and state of the nomination immediately evident.""" @@ -52,3 +45,38 @@ class Nomination(ModelReprMixin, models.Model): """Set the ordering of nominations to most recent first.""" ordering = ("-inserted_at",) + + +class NominationEntry(ModelReprMixin, models.Model): + """A nomination entry created by a single staff member.""" + + nomination = models.ForeignKey( + Nomination, + on_delete=models.CASCADE, + help_text="The nomination this entry belongs to.", + related_name="entries" + ) + actor = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The staff member that nominated this user.", + related_name='nomination_set' + ) + reason = models.TextField( + help_text="Why the actor nominated this user.", + default="", + blank=True + ) + inserted_at = models.DateTimeField( + auto_now_add=True, + help_text="The creation date of this nomination entry." + ) + + class Meta: + """Meta options for NominationEntry model.""" + + verbose_name_plural = "nomination entries" + + # Set default ordering here to latest first + # so we don't need to define it everywhere + ordering = ("-inserted_at",) diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py deleted file mode 100644 index 752cd2ca..00000000 --- a/pydis_site/apps/api/models/log_entry.py +++ /dev/null @@ -1,55 +0,0 @@ -from django.db import models -from django.utils import timezone - -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class LogEntry(ModelReprMixin, models.Model): - """A log entry generated by one of the PyDis applications.""" - - application = models.CharField( - max_length=20, - help_text="The application that generated this log entry.", - choices=( - ('bot', 'Bot'), - ('seasonalbot', 'Seasonalbot'), - ('site', 'Website') - ) - ) - logger_name = models.CharField( - max_length=100, - help_text="The name of the logger that generated this log entry." - ) - timestamp = models.DateTimeField( - default=timezone.now, - help_text="The date and time when this entry was created." - ) - level = models.CharField( - max_length=8, # 'critical' - choices=( - ('debug', 'Debug'), - ('info', 'Info'), - ('warning', 'Warning'), - ('error', 'Error'), - ('critical', 'Critical') - ), - help_text=( - "The logger level at which this entry was emitted. The levels " - "correspond to the Python `logging` levels." - ) - ) - module = models.CharField( - max_length=100, - help_text="The fully qualified path of the module generating this log line." - ) - line = models.PositiveSmallIntegerField( - help_text="The line at which the log line was emitted." - ) - message = models.TextField( - help_text="The textual content of the log line." - ) - - class Meta: - """Customizes the default generated plural name to valid English.""" - - verbose_name_plural = 'Log entries' diff --git a/pydis_site/apps/api/pagination.py b/pydis_site/apps/api/pagination.py new file mode 100644 index 00000000..2a325460 --- /dev/null +++ b/pydis_site/apps/api/pagination.py @@ -0,0 +1,49 @@ +import typing + +from rest_framework.pagination import LimitOffsetPagination +from rest_framework.response import Response + + +class LimitOffsetPaginationExtended(LimitOffsetPagination): + """ + Extend LimitOffsetPagination to customise the default response. + + For example: + + ## Default response + >>> { + ... "count": 1, + ... "next": None, + ... "previous": None, + ... "results": [{ + ... "id": 6, + ... "inserted_at": "2021-01-26T21:13:35.477879Z", + ... "expires_at": None, + ... "active": False, + ... "user": 1, + ... "actor": 2, + ... "type": "warning", + ... "reason": null, + ... "hidden": false + ... }] + ... } + + ## Required response + >>> [{ + ... "id": 6, + ... "inserted_at": "2021-01-26T21:13:35.477879Z", + ... "expires_at": None, + ... "active": False, + ... "user": 1, + ... "actor": 2, + ... "type": "warning", + ... "reason": None, + ... "hidden": False + ... }] + """ + + default_limit = 100 + + def get_paginated_response(self, data: typing.Any) -> Response: + """Override to skip metadata i.e. `count`, `next`, and `previous`.""" + return Response(data) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 90bd6f91..f47bedca 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,7 +1,16 @@ """Converters from Django models to data interchange formats and back.""" -from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError +from django.db.models.query import QuerySet +from django.db.utils import IntegrityError +from rest_framework.exceptions import NotFound +from rest_framework.serializers import ( + IntegerField, + ListSerializer, + ModelSerializer, + PrimaryKeyRelatedField, + ValidationError +) +from rest_framework.settings import api_settings from rest_framework.validators import UniqueTogetherValidator -from rest_framework_bulk import BulkSerializerMixin from .models import ( BotSetting, @@ -9,9 +18,9 @@ from .models import ( DocumentationLink, FilterList, Infraction, - LogEntry, MessageDeletionContext, Nomination, + NominationEntry, OffTopicChannelName, OffensiveMessage, Reminder, @@ -159,7 +168,7 @@ class InfractionSerializer(ModelSerializer): raise ValidationError({'expires_at': [f'{infr_type} infractions cannot expire.']}) hidden = attrs.get('hidden') - if hidden and infr_type in ('superstar', 'warning'): + if hidden and infr_type in ('superstar', 'warning', 'voice_ban'): raise ValidationError({'hidden': [f'{infr_type} infractions cannot be hidden.']}) if not hidden and infr_type in ('note', ): @@ -191,19 +200,6 @@ class ExpandedInfractionSerializer(InfractionSerializer): return ret -class LogEntrySerializer(ModelSerializer): - """A class providing (de-)serialization of `LogEntry` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = LogEntry - fields = ( - 'application', 'logger_name', 'timestamp', - 'level', 'module', 'line', 'message' - ) - - class OffTopicChannelNameSerializer(ModelSerializer): """A class providing (de-)serialization of `OffTopicChannelName` instances.""" @@ -249,27 +245,130 @@ class RoleSerializer(ModelSerializer): fields = ('id', 'name', 'colour', 'permissions', 'position') -class UserSerializer(BulkSerializerMixin, ModelSerializer): +class UserListSerializer(ListSerializer): + """List serializer for User model to handle bulk updates.""" + + def create(self, validated_data: list) -> list: + """Override create method to optimize django queries.""" + new_users = [] + seen = set() + + for user_dict in validated_data: + if user_dict["id"] in seen: + raise ValidationError( + {"id": [f"User with ID {user_dict['id']} given multiple times."]} + ) + seen.add(user_dict["id"]) + new_users.append(User(**user_dict)) + + User.objects.bulk_create(new_users, ignore_conflicts=True) + return [] + + def update(self, queryset: QuerySet, validated_data: list) -> list: + """ + Override update method to support bulk updates. + + ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update + """ + object_ids = set() + + for data in validated_data: + try: + if data["id"] in object_ids: + # If request data contains users with same ID. + raise ValidationError( + {"id": [f"User with ID {data['id']} given multiple times."]} + ) + except KeyError: + # If user ID not provided in request body. + raise ValidationError( + {"id": ["This field is required."]} + ) + object_ids.add(data["id"]) + + # filter queryset + filtered_instances = queryset.filter(id__in=object_ids) + + instance_mapping = {user.id: user for user in filtered_instances} + + updated = [] + fields_to_update = set() + for user_data in validated_data: + for key in user_data: + fields_to_update.add(key) + + try: + user = instance_mapping[user_data["id"]] + except KeyError: + raise NotFound({"detail": f"User with id {user_data['id']} not found."}) + + user.__dict__.update(user_data) + updated.append(user) + + fields_to_update.remove("id") + + if not fields_to_update: + # Raise ValidationError when only id field is given. + raise ValidationError( + {api_settings.NON_FIELD_ERRORS_KEY: ["Insufficient data provided."]} + ) + + User.objects.bulk_update(updated, fields_to_update) + return updated + + +class UserSerializer(ModelSerializer): """A class providing (de-)serialization of `User` instances.""" + # ID field must be explicitly set as the default id field is read-only. + id = IntegerField(min_value=0) + class Meta: """Metadata defined for the Django REST Framework.""" model = User fields = ('id', 'name', 'discriminator', 'roles', 'in_guild') depth = 1 + list_serializer_class = UserListSerializer + + def create(self, validated_data: dict) -> User: + """Override create method to catch IntegrityError.""" + try: + return super().create(validated_data) + except IntegrityError: + raise ValidationError({"id": ["User with ID already present."]}) + + +class NominationEntrySerializer(ModelSerializer): + """A class providing (de-)serialization of `NominationEntry` instances.""" + + # We need to define it here, because we don't want that nomination ID + # return inside nomination response entry, because ID is already available + # as top-level field. Queryset is required if field is not read only. + nomination = PrimaryKeyRelatedField( + queryset=Nomination.objects.all(), + write_only=True + ) + + class Meta: + """Metadata defined for the Django REST framework.""" + + model = NominationEntry + fields = ('nomination', 'actor', 'reason', 'inserted_at') class NominationSerializer(ModelSerializer): """A class providing (de-)serialization of `Nomination` instances.""" + entries = NominationEntrySerializer(many=True, read_only=True) + class Meta: """Metadata defined for the Django REST Framework.""" model = Nomination fields = ( - 'id', 'active', 'actor', 'reason', 'user', - 'inserted_at', 'end_reason', 'ended_at') + 'id', 'active', 'user', 'inserted_at', 'end_reason', 'ended_at', 'reviewed', 'entries' + ) class OffensiveMessageSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/tests/test_dblogger.py b/pydis_site/apps/api/tests/test_dblogger.py deleted file mode 100644 index bb19f297..00000000 --- a/pydis_site/apps/api/tests/test_dblogger.py +++ /dev/null @@ -1,27 +0,0 @@ -import logging -from datetime import datetime - -from django.test import TestCase - -from ..dblogger import DatabaseLogHandler -from ..models import LogEntry - - -class DatabaseLogHandlerTests(TestCase): - def test_logs_to_database(self): - module_basename = __name__.split('.')[-1] - logger = logging.getLogger(__name__) - logger.handlers = [DatabaseLogHandler()] - logger.warning("I am a test case!") - - # Ensure we only have a single record in the database - # after the logging call above. - [entry] = LogEntry.objects.all() - - self.assertEqual(entry.application, 'site') - self.assertEqual(entry.logger_name, __name__) - self.assertIsInstance(entry.timestamp, datetime) - self.assertEqual(entry.level, 'warning') - self.assertEqual(entry.module, module_basename) - self.assertIsInstance(entry.line, int) - self.assertEqual(entry.message, "I am a test case!") diff --git a/pydis_site/apps/api/tests/test_documentation_links.py b/pydis_site/apps/api/tests/test_documentation_links.py index e560a2fd..39fb08f3 100644 --- a/pydis_site/apps/api/tests/test_documentation_links.py +++ b/pydis_site/apps/api/tests/test_documentation_links.py @@ -60,7 +60,7 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase): def setUpTestData(cls): cls.doc_link = DocumentationLink.objects.create( package='testpackage', - base_url='https://example.com', + base_url='https://example.com/', inventory_url='https://example.com' ) @@ -108,6 +108,17 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase): self.assertEqual(response.status_code, 400) + def test_create_invalid_package_name_returns_400(self): + test_cases = ("InvalidPackage", "invalid package", "i\u0150valid") + for case in test_cases: + with self.subTest(package_name=case): + body = self.doc_json.copy() + body['package'] = case + url = reverse('bot:documentationlink-list', host='api') + response = self.client.post(url, data=body) + + self.assertEqual(response.status_code, 400) + class DocumentationLinkCreationTests(APISubdomainTestCase): def setUp(self): @@ -115,7 +126,7 @@ class DocumentationLinkCreationTests(APISubdomainTestCase): self.body = { 'package': 'example', - 'base_url': 'https://example.com', + 'base_url': 'https://example.com/', 'inventory_url': 'https://docs.example.com' } diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py index 93ef8171..82b497aa 100644 --- a/pydis_site/apps/api/tests/test_infractions.py +++ b/pydis_site/apps/api/tests/test_infractions.py @@ -512,6 +512,36 @@ class CreationTests(APISubdomainTestCase): ) +class InfractionDeletionTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create( + id=9876, + name='Unknown user', + discriminator=9876, + ) + + cls.warning = Infraction.objects.create( + user_id=cls.user.id, + actor_id=cls.user.id, + type='warning', + active=False + ) + + def test_delete_unknown_infraction_returns_404(self): + url = reverse('bot:infraction-detail', args=('something',), host='api') + response = self.client.delete(url) + + self.assertEqual(response.status_code, 404) + + def test_delete_known_infraction_returns_204(self): + url = reverse('bot:infraction-detail', args=(self.warning.id,), host='api') + response = self.client.delete(url) + + self.assertEqual(response.status_code, 204) + self.assertRaises(Infraction.DoesNotExist, Infraction.objects.get, id=self.warning.id) + + class ExpandedTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index 853e6621..66052e01 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -10,6 +10,7 @@ from pydis_site.apps.api.models import ( Message, MessageDeletionContext, Nomination, + NominationEntry, OffTopicChannelName, OffensiveMessage, Reminder, @@ -37,17 +38,11 @@ class StringDunderMethodTests(SimpleTestCase): def setUp(self): self.nomination = Nomination( id=123, - actor=User( - id=9876, - name='Mr. Hemlock', - discriminator=6666, - ), user=User( id=9876, name="Hemlock's Cat", discriminator=7777, ), - reason="He purrrrs like the best!", ) self.objects = ( @@ -135,6 +130,15 @@ class StringDunderMethodTests(SimpleTestCase): ), content="oh no", expiration=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc) + ), + NominationEntry( + nomination_id=self.nomination.id, + actor=User( + id=9876, + name='Mr. Hemlock', + discriminator=6666, + ), + reason="He purrrrs like the best!", ) ) diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index b37135f8..9cefbd8f 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -3,7 +3,7 @@ from datetime import datetime as dt, timedelta, timezone from django_hosts.resolvers import reverse from .base import APISubdomainTestCase -from ..models import Nomination, User +from ..models import Nomination, NominationEntry, User class CreationTests(APISubdomainTestCase): @@ -14,6 +14,11 @@ class CreationTests(APISubdomainTestCase): name='joe dart', discriminator=1111, ) + cls.user2 = User.objects.create( + id=9876, + name='Who?', + discriminator=1234 + ) def test_accepts_valid_data(self): url = reverse('bot:nomination-list', host='api') @@ -27,17 +32,39 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) nomination = Nomination.objects.get(id=response.json()['id']) + nomination_entry = NominationEntry.objects.get( + nomination_id=nomination.id, + actor_id=self.user.id + ) self.assertAlmostEqual( nomination.inserted_at, dt.now(timezone.utc), delta=timedelta(seconds=2) ) self.assertEqual(nomination.user.id, data['user']) - self.assertEqual(nomination.actor.id, data['actor']) - self.assertEqual(nomination.reason, data['reason']) + self.assertEqual(nomination_entry.reason, data['reason']) self.assertEqual(nomination.active, True) - def test_returns_400_on_second_active_nomination(self): + def test_returns_200_on_second_active_nomination_by_different_user(self): + url = reverse('bot:nomination-list', host='api') + first_data = { + 'actor': self.user.id, + 'reason': 'Joe Dart on Fender Bass', + 'user': self.user.id, + } + second_data = { + 'actor': self.user2.id, + 'reason': 'Great user', + 'user': self.user.id + } + + response1 = self.client.post(url, data=first_data) + self.assertEqual(response1.status_code, 201) + + response2 = self.client.post(url, data=second_data) + self.assertEqual(response2.status_code, 201) + + def test_returns_400_on_second_active_nomination_by_existing_nominator(self): url = reverse('bot:nomination-list', host='api') data = { 'actor': self.user.id, @@ -51,7 +78,7 @@ class CreationTests(APISubdomainTestCase): response2 = self.client.post(url, data=data) self.assertEqual(response2.status_code, 400) self.assertEqual(response2.json(), { - 'active': ['There can only be one active nomination.'] + 'actor': ['This actor has already endorsed this nomination.'] }) def test_returns_400_for_missing_user(self): @@ -189,30 +216,40 @@ class NominationTests(APISubdomainTestCase): ) cls.active_nomination = Nomination.objects.create( - user=cls.user, + user=cls.user + ) + cls.active_nomination_entry = NominationEntry.objects.create( + nomination=cls.active_nomination, actor=cls.user, reason="He's pretty funky" ) cls.inactive_nomination = Nomination.objects.create( user=cls.user, - actor=cls.user, - reason="He's pretty funky", active=False, end_reason="His neck couldn't hold the funk", ended_at="5018-11-20T15:52:00+00:00" ) + cls.inactive_nomination_entry = NominationEntry.objects.create( + nomination=cls.inactive_nomination, + actor=cls.user, + reason="He's pretty funky" + ) - def test_returns_200_update_reason_on_active(self): + def test_returns_200_update_reason_on_active_with_actor(self): url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') data = { - 'reason': "He's one funky duck" + 'reason': "He's one funky duck", + 'actor': self.user.id } response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 200) - nomination = Nomination.objects.get(id=response.json()['id']) - self.assertEqual(nomination.reason, data['reason']) + nomination_entry = NominationEntry.objects.get( + nomination_id=response.json()['id'], + actor_id=self.user.id + ) + self.assertEqual(nomination_entry.reason, data['reason']) def test_returns_400_on_frozen_field_update(self): url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') @@ -241,14 +278,18 @@ class NominationTests(APISubdomainTestCase): def test_returns_200_update_reason_on_inactive(self): url = reverse('bot:nomination-detail', args=(self.inactive_nomination.id,), host='api') data = { - 'reason': "He's one funky duck" + 'reason': "He's one funky duck", + 'actor': self.user.id } response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 200) - nomination = Nomination.objects.get(id=response.json()['id']) - self.assertEqual(nomination.reason, data['reason']) + nomination_entry = NominationEntry.objects.get( + nomination_id=response.json()['id'], + actor_id=self.user.id + ) + self.assertEqual(nomination_entry.reason, data['reason']) def test_returns_200_update_end_reason_on_inactive(self): url = reverse('bot:nomination-detail', args=(self.inactive_nomination.id,), host='api') @@ -442,3 +483,50 @@ class NominationTests(APISubdomainTestCase): infractions = response.json() self.assertEqual(len(infractions), 2) + + def test_patch_nomination_set_reviewed_of_active_nomination(self): + url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') + data = {'reviewed': True} + + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 200) + + def test_patch_nomination_set_reviewed_of_inactive_nomination(self): + url = reverse('api:nomination-detail', args=(self.inactive_nomination.id,), host='api') + data = {'reviewed': True} + + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), { + 'reviewed': ['This field cannot be set if the nomination is inactive.'] + }) + + def test_patch_nomination_set_reviewed_and_end(self): + url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') + data = {'reviewed': True, 'active': False, 'end_reason': "What?"} + + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), { + 'reviewed': ['This field cannot be set while you are ending a nomination.'] + }) + + def test_modifying_reason_without_actor(self): + url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') + data = {'reason': 'That is my reason!'} + + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), { + 'actor': ['This field is required when editing the reason.'] + }) + + def test_modifying_reason_with_unknown_actor(self): + url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') + data = {'reason': 'That is my reason!', 'actor': 90909090909090} + + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), { + 'actor': ["The actor doesn't exist or has not nominated the user."] + }) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index a02fce8a..c43b916a 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -1,7 +1,11 @@ +from unittest.mock import patch + +from django.core.exceptions import ObjectDoesNotExist from django_hosts.resolvers import reverse from .base import APISubdomainTestCase from ..models import Role, User +from ..models.bot.metricity import NotFound class UnauthedUserAPITests(APISubdomainTestCase): @@ -45,6 +49,13 @@ class CreationTests(APISubdomainTestCase): position=1 ) + cls.user = User.objects.create( + id=11, + name="Name doesn't matter.", + discriminator=1122, + in_guild=True + ) + def test_accepts_valid_data(self): url = reverse('bot:user-list', host='api') data = { @@ -89,7 +100,7 @@ class CreationTests(APISubdomainTestCase): response = self.client.post(url, data=data) self.assertEqual(response.status_code, 201) - self.assertEqual(response.json(), data) + self.assertEqual(response.json(), []) def test_returns_400_for_unknown_role_id(self): url = reverse('bot:user-list', host='api') @@ -115,6 +126,176 @@ class CreationTests(APISubdomainTestCase): response = self.client.post(url, data=data) self.assertEqual(response.status_code, 400) + def test_returns_400_for_user_recreation(self): + """Return 201 if User is already present in database as it skips User creation.""" + url = reverse('bot:user-list', host='api') + data = [{ + 'id': 11, + 'name': 'You saw nothing.', + 'discriminator': 112, + 'in_guild': True + }] + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 201) + + def test_returns_400_for_duplicate_request_users(self): + """Return 400 if 2 Users with same ID is passed in the request data.""" + url = reverse('bot:user-list', host='api') + data = [ + { + 'id': 11, + 'name': 'You saw nothing.', + 'discriminator': 112, + 'in_guild': True + }, + { + 'id': 11, + 'name': 'You saw nothing part 2.', + 'discriminator': 1122, + 'in_guild': False + } + ] + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) + + def test_returns_400_for_existing_user(self): + """Returns 400 if user is already present in DB.""" + url = reverse('bot:user-list', host='api') + data = { + 'id': 11, + 'name': 'You saw nothing part 3.', + 'discriminator': 1122, + 'in_guild': True + } + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) + + +class MultiPatchTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.role_developer = Role.objects.create( + id=159, + name="Developer", + colour=2, + permissions=0b01010010101, + position=10, + ) + cls.user_1 = User.objects.create( + id=1, + name="Patch test user 1.", + discriminator=1111, + in_guild=True + ) + cls.user_2 = User.objects.create( + id=2, + name="Patch test user 2.", + discriminator=2222, + in_guild=True + ) + + def test_multiple_users_patch(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "id": 1, + "name": "User 1 patched!", + "discriminator": 1010, + "roles": [self.role_developer.id], + "in_guild": False + }, + { + "id": 2, + "name": "User 2 patched!" + } + ] + + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()[0], data[0]) + + user_2 = User.objects.get(id=2) + self.assertEqual(user_2.name, data[1]["name"]) + + def test_returns_400_for_missing_user_id(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "name": "I am ghost user!", + "discriminator": 1010, + "roles": [self.role_developer.id], + "in_guild": False + }, + { + "name": "patch me? whats my id?" + } + ] + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + + def test_returns_404_for_not_found_user(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "id": 1, + "name": "User 1 patched again!!!", + "discriminator": 1010, + "roles": [self.role_developer.id], + "in_guild": False + }, + { + "id": 22503405, + "name": "User unknown not patched!" + } + ] + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 404) + + def test_returns_400_for_bad_data(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "id": 1, + "in_guild": "Catch me!" + }, + { + "id": 2, + "discriminator": "find me!" + } + ] + + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + + def test_returns_400_for_insufficient_data(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "id": 1, + }, + { + "id": 2, + } + ] + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + + def test_returns_400_for_duplicate_request_users(self): + """Return 400 if 2 Users with same ID is passed in the request data.""" + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + 'id': 1, + 'name': 'You saw nothing.', + }, + { + 'id': 1, + 'name': 'You saw nothing part 2.', + } + ] + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + class UserModelTests(APISubdomainTestCase): @classmethod @@ -170,3 +351,157 @@ class UserModelTests(APISubdomainTestCase): def test_correct_username_formatting(self): """Tests the username property with both name and discriminator formatted together.""" self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") + + +class UserPaginatorTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + users = [] + for i in range(1, 10_001): + users.append(User( + id=i, + name=f"user{i}", + discriminator=1111, + in_guild=True + )) + cls.users = User.objects.bulk_create(users) + + def test_returns_single_page_response(self): + url = reverse("bot:user-list", host="api") + response = self.client.get(url).json() + self.assertIsNone(response["next_page_no"]) + self.assertIsNone(response["previous_page_no"]) + + def test_returns_next_page_number(self): + User.objects.create( + id=10_001, + name="user10001", + discriminator=1111, + in_guild=True + ) + url = reverse("bot:user-list", host="api") + response = self.client.get(url).json() + self.assertEqual(2, response["next_page_no"]) + + def test_returns_previous_page_number(self): + User.objects.create( + id=10_001, + name="user10001", + discriminator=1111, + in_guild=True + ) + url = reverse("bot:user-list", host="api") + response = self.client.get(url, {"page": 2}).json() + self.assertEqual(1, response["previous_page_no"]) + + +class UserMetricityTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + User.objects.create( + id=0, + name="Test user", + discriminator=1, + in_guild=True, + ) + + def test_get_metricity_data(self): + # Given + joined_at = "foo" + total_messages = 1 + total_blocks = 1 + self.mock_metricity_user(joined_at, total_messages, total_blocks, []) + + # When + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), { + "joined_at": joined_at, + "total_messages": total_messages, + "voice_banned": False, + "activity_blocks": total_blocks + }) + + def test_no_metricity_user(self): + # Given + self.mock_no_metricity_user() + + # When + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 404) + + def test_no_metricity_user_for_review(self): + # Given + self.mock_no_metricity_user() + + # When + url = reverse('bot:user-metricity-review-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 404) + + def test_metricity_voice_banned(self): + cases = [ + {'exception': None, 'voice_banned': True}, + {'exception': ObjectDoesNotExist, 'voice_banned': False}, + ] + + self.mock_metricity_user("foo", 1, 1, [["bar", 1]]) + + for case in cases: + with self.subTest(exception=case['exception'], voice_banned=case['voice_banned']): + with patch("pydis_site.apps.api.viewsets.bot.user.Infraction.objects.get") as p: + p.side_effect = case['exception'] + + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["voice_banned"], case["voice_banned"]) + + def test_metricity_review_data(self): + # Given + joined_at = "foo" + total_messages = 10 + total_blocks = 1 + channel_activity = [["bar", 4], ["buzz", 6]] + self.mock_metricity_user(joined_at, total_messages, total_blocks, channel_activity) + + # When + url = reverse('bot:user-metricity-review-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), { + "joined_at": joined_at, + "top_channel_activity": channel_activity, + "total_messages": total_messages + }) + + def mock_metricity_user(self, joined_at, total_messages, total_blocks, top_channel_activity): + patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") + self.metricity = patcher.start() + self.addCleanup(patcher.stop) + self.metricity = self.metricity.return_value.__enter__.return_value + self.metricity.user.return_value = dict(joined_at=joined_at) + self.metricity.total_messages.return_value = total_messages + self.metricity.total_message_blocks.return_value = total_blocks + self.metricity.top_channel_activity.return_value = top_channel_activity + + def mock_no_metricity_user(self): + patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") + self.metricity = patcher.start() + self.addCleanup(patcher.stop) + self.metricity = self.metricity.return_value.__enter__.return_value + self.metricity.user.side_effect = NotFound() + self.metricity.total_messages.side_effect = NotFound() + self.metricity.total_message_blocks.side_effect = NotFound() + self.metricity.top_channel_activity.side_effect = NotFound() diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 4dbf93db..2e1ef0b4 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -8,7 +8,6 @@ from .viewsets import ( DocumentationLinkViewSet, FilterListViewSet, InfractionViewSet, - LogEntryViewSet, NominationViewSet, OffTopicChannelNameViewSet, OffensiveMessageViewSet, @@ -71,7 +70,6 @@ urlpatterns = ( # # from django_hosts.resolvers import reverse path('bot/', include((bot_router.urls, 'api'), namespace='bot')), - path('logs', LogEntryViewSet.as_view({'post': 'create'}), name='logs'), path('healthcheck', HealthcheckView.as_view(), name='healthcheck'), path('rules', RulesView.as_view(), name='rules') ) diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py index 7ac56641..0d126051 100644 --- a/pydis_site/apps/api/views.py +++ b/pydis_site/apps/api/views.py @@ -135,8 +135,9 @@ class RulesView(APIView): ), ( "Do not provide or request help on projects that may break laws, " - "breach terms of services, be considered malicious/inappropriate " - "or be for graded coursework/exams." + "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." ), ( "No spamming or unapproved advertising, including requests for paid work. " diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index dfbb880d..f133e77f 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -12,4 +12,3 @@ from .bot import ( RoleViewSet, UserViewSet ) -from .log_entry import LogEntryViewSet diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index edec0a1e..bd512ddd 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -5,6 +5,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.mixins import ( CreateModelMixin, + DestroyModelMixin, ListModelMixin, RetrieveModelMixin ) @@ -12,13 +13,20 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from pydis_site.apps.api.models.bot.infraction import Infraction +from pydis_site.apps.api.pagination import LimitOffsetPaginationExtended from pydis_site.apps.api.serializers import ( ExpandedInfractionSerializer, InfractionSerializer ) -class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): +class InfractionViewSet( + CreateModelMixin, + RetrieveModelMixin, + ListModelMixin, + GenericViewSet, + DestroyModelMixin +): """ View providing CRUD operations on infractions for Discord users. @@ -31,6 +39,8 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge - **active** `bool`: whether the infraction is still active - **actor__id** `int`: snowflake of the user which applied the infraction - **hidden** `bool`: whether the infraction is a shadow infraction + - **limit** `int`: number of results return per page (default 100) + - **offset** `int`: the initial index from which to return the results (default 0) - **search** `str`: regular expression applied to the infraction's reason - **type** `str`: the type of the infraction - **user__id** `int`: snowflake of the user to which the infraction was applied @@ -39,6 +49,7 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge Invalid query parameters are ignored. #### Response format + Response is paginated but the result is returned without any pagination metadata. >>> [ ... { ... 'id': 5, @@ -108,6 +119,13 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge - 400: if a field in the request body is invalid or disallowed - 404: if an infraction with the given `id` could not be found + ### DELETE /bot/infractions/ + Delete the infraction with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a infraction with the given `id` does not exist + ### Expanded routes All routes support expansion of `user` and `actor` in responses. To use an expanded route, append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`. @@ -119,6 +137,7 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge serializer_class = InfractionSerializer queryset = Infraction.objects.all() + pagination_class = LimitOffsetPaginationExtended filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) filter_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type') search_fields = ('$reason',) diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index cf6e262f..144daab0 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -14,8 +14,8 @@ from rest_framework.mixins import ( from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet -from pydis_site.apps.api.models.bot import Nomination -from pydis_site.apps.api.serializers import NominationSerializer +from pydis_site.apps.api.models.bot import Nomination, NominationEntry +from pydis_site.apps.api.serializers import NominationEntrySerializer, NominationSerializer class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): @@ -29,7 +29,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge #### Query parameters - **active** `bool`: whether the nomination is still active - - **actor__id** `int`: snowflake of the user who nominated the user - **user__id** `int`: snowflake of the user who received the nomination - **ordering** `str`: comma-separated sequence of fields to order the returned results @@ -40,12 +39,18 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge ... { ... 'id': 1, ... 'active': false, - ... 'actor': 336843820513755157, - ... 'reason': 'They know how to explain difficult concepts', ... 'user': 336843820513755157, ... 'inserted_at': '2019-04-25T14:02:37.775587Z', ... 'end_reason': 'They were helpered after a staff-vote', - ... 'ended_at': '2019-04-26T15:12:22.123587Z' + ... 'ended_at': '2019-04-26T15:12:22.123587Z', + ... 'entries': [ + ... { + ... 'actor': 336843820513755157, + ... 'reason': 'They know how to explain difficult concepts', + ... 'inserted_at': '2019-04-25T14:02:37.775587Z' + ... } + ... ], + ... 'reviewed': true ... } ... ] @@ -59,12 +64,18 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge >>> { ... 'id': 1, ... 'active': true, - ... 'actor': 336843820513755157, - ... 'reason': 'They know how to explain difficult concepts', ... 'user': 336843820513755157, ... 'inserted_at': '2019-04-25T14:02:37.775587Z', ... 'end_reason': 'They were helpered after a staff-vote', - ... 'ended_at': '2019-04-26T15:12:22.123587Z' + ... 'ended_at': '2019-04-26T15:12:22.123587Z', + ... 'entries': [ + ... { + ... 'actor': 336843820513755157, + ... 'reason': 'They know how to explain difficult concepts', + ... 'inserted_at': '2019-04-25T14:02:37.775587Z' + ... } + ... ], + ... 'reviewed': false ... } ### Status codes @@ -75,8 +86,9 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge Create a new, active nomination returns the created nominations. The `user`, `reason` and `actor` fields are required and the `user` and `actor` need to know by the site. Providing other valid fields - is not allowed and invalid fields are ignored. A `user` is only - allowed one active nomination at a time. + is not allowed and invalid fields are ignored. If `user` already has an + active nomination, a new nomination entry will be created and assigned to the + active nomination. #### Request body >>> { @@ -91,7 +103,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge #### Status codes - 201: returned on success - 400: returned on failure for one of the following reasons: - - A user already has an active nomination; - The `user` or `actor` are unknown to the site; - The request contained a field that cannot be set at creation. @@ -102,16 +113,18 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge 1. Updating the `reason` of `active` nomination; 2. Ending an `active` nomination; 3. Updating the `end_reason` or `reason` field of an `inactive` nomination. + 4. Updating `reviewed` field of `active` nomination. While the response format and status codes are the same for all three operations (see below), the request bodies vary depending on the operation. For all operations it holds that providing other valid fields is not allowed and invalid fields are ignored. - ### 1. Updating the `reason` of `active` nomination + ### 1. Updating the `reason` of `active` nomination. The `actor` field is required. #### Request body >>> { ... 'reason': 'He would make a great helper', + ... 'actor': 409107086526644234 ... } #### Response format @@ -133,24 +146,35 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge See operation 1 for the response format and status codes. ### 3. Updating the `end_reason` or `reason` field of an `inactive` nomination. + Actor field is required when updating reason. #### Request body >>> { ... 'reason': 'Updated reason for this nomination', + ... 'actor': 409107086526644234, ... 'end_reason': 'Updated end_reason for this nomination', ... } Note: The request body may contain either or both fields. + See operation 1 for the response format and status codes. + + ### 4. Setting nomination `reviewed` + + #### Request body + >>> { + ... 'reviewed': True + ... } + See operation 1 for the response format and status codes. """ serializer_class = NominationSerializer queryset = Nomination.objects.all() filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) - filter_fields = ('user__id', 'actor__id', 'active') - frozen_fields = ('id', 'actor', 'inserted_at', 'user', 'ended_at') - frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at') + filter_fields = ('user__id', 'active') + frozen_fields = ('id', 'inserted_at', 'user', 'ended_at') + frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at', 'reviewed') def create(self, request: HttpRequest, *args, **kwargs) -> Response: """ @@ -163,19 +187,50 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge raise ValidationError({field: ['This field cannot be set at creation.']}) user_id = request.data.get("user") - if Nomination.objects.filter(active=True, user__id=user_id).exists(): - raise ValidationError({'active': ['There can only be one active nomination.']}) + nomination_filter = Nomination.objects.filter(active=True, user__id=user_id) + + if not nomination_filter.exists(): + serializer = NominationSerializer( + data=ChainMap( + request.data, + {"active": True} + ) + ) + serializer.is_valid(raise_exception=True) + nomination = Nomination.objects.create(**serializer.validated_data) - serializer = self.get_serializer( - data=ChainMap( - request.data, - {"active": True} + # The serializer will truncate and get rid of excessive data + entry_serializer = NominationEntrySerializer( + data=ChainMap(request.data, {"nomination": nomination.id}) ) + entry_serializer.is_valid(raise_exception=True) + NominationEntry.objects.create(**entry_serializer.validated_data) + + data = NominationSerializer(nomination).data + + headers = self.get_success_headers(data) + return Response(data, status=status.HTTP_201_CREATED, headers=headers) + + entry_serializer = NominationEntrySerializer( + data=ChainMap(request.data, {"nomination": nomination_filter[0].id}) ) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + entry_serializer.is_valid(raise_exception=True) + + # Don't allow a user to create many nomination entries in a single nomination + if NominationEntry.objects.filter( + nomination_id=nomination_filter[0].id, + actor__id=entry_serializer.validated_data["actor"].id + ).exists(): + raise ValidationError( + {'actor': ['This actor has already endorsed this nomination.']} + ) + + NominationEntry.objects.create(**entry_serializer.validated_data) + + data = NominationSerializer(nomination_filter[0]).data + + headers = self.get_success_headers(data) + return Response(data, status=status.HTTP_201_CREATED, headers=headers) def partial_update(self, request: HttpRequest, *args, **kwargs) -> Response: """ @@ -203,7 +258,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge elif instance.active and not data['active']: # 2. We're ending an active nomination. - if 'reason' in data: + if 'reason' in request.data: raise ValidationError( {'reason': ['This field cannot be set when ending a nomination.']} ) @@ -213,6 +268,11 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge {'end_reason': ['This field is required when ending a nomination.']} ) + if 'reviewed' in request.data: + raise ValidationError( + {'reviewed': ['This field cannot be set while you are ending a nomination.']} + ) + instance.ended_at = timezone.now() elif 'active' in data: @@ -221,6 +281,34 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge {'active': ['This field can only be used to end a nomination']} ) + # This is actually covered, but for some reason coverage don't think so. + elif 'reviewed' in request.data: # pragma: no cover + # 4. We are altering the reviewed state of the nomination. + if not instance.active: + raise ValidationError( + {'reviewed': ['This field cannot be set if the nomination is inactive.']} + ) + + if 'reason' in request.data: + if 'actor' not in request.data: + raise ValidationError( + {'actor': ['This field is required when editing the reason.']} + ) + + entry_filter = NominationEntry.objects.filter( + nomination_id=instance.id, + actor__id=request.data['actor'] + ) + + if not entry_filter.exists(): + raise ValidationError( + {'actor': ["The actor doesn't exist or has not nominated the user."]} + ) + + entry = entry_filter[0] + entry.reason = request.data['reason'] + entry.save() + serializer.save() return Response(serializer.data) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 9571b3d7..25722f5a 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,21 +1,67 @@ +import typing +from collections import OrderedDict + +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.pagination import PageNumberPagination +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet -from rest_framework_bulk import BulkCreateModelMixin +from pydis_site.apps.api.models.bot.infraction import Infraction +from pydis_site.apps.api.models.bot.metricity import Metricity, NotFound from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer -class UserViewSet(BulkCreateModelMixin, ModelViewSet): +class UserListPagination(PageNumberPagination): + """Custom pagination class for the User Model.""" + + page_size = 10000 + page_size_query_param = "page_size" + + def get_next_page_number(self) -> typing.Optional[int]: + """Get the next page number.""" + if not self.page.has_next(): + return None + page_number = self.page.next_page_number() + return page_number + + def get_previous_page_number(self) -> typing.Optional[int]: + """Get the previous page number.""" + if not self.page.has_previous(): + return None + + page_number = self.page.previous_page_number() + return page_number + + def get_paginated_response(self, data: list) -> Response: + """Override method to send modified response.""" + return Response(OrderedDict([ + ('count', self.page.paginator.count), + ('next_page_no', self.get_next_page_number()), + ('previous_page_no', self.get_previous_page_number()), + ('results', data) + ])) + + +class UserViewSet(ModelViewSet): """ View providing CRUD operations on Discord users through the bot. ## Routes ### GET /bot/users - Returns all users currently known. + Returns all users currently known with pagination. #### Response format - >>> [ - ... { + >>> { + ... 'count': 95000, + ... 'next_page_no': "2", + ... 'previous_page_no': None, + ... 'results': [ + ... { ... 'id': 409107086526644234, ... 'name': "Python", ... 'discriminator': 4329, @@ -26,8 +72,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): ... 458226699344019457 ... ], ... 'in_guild': True - ... } - ... ] + ... }, + ... ] + ... } + + #### Optional Query Parameters + - page_size: number of Users in one page, defaults to 10,000 + - page: page number #### Status codes - 200: returned on success @@ -53,9 +104,41 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): - 200: returned on success - 404: if a user with the given `snowflake` could not be found + ### GET /bot/users//metricity_data + Gets metricity data for a single user by ID. + + #### Response format + >>> { + ... "joined_at": "2020-10-06T21:54:23.540766", + ... "total_messages": 2, + ... "voice_banned": False, + ... "activity_blocks": 1 + ...} + + #### Status codes + - 200: returned on success + - 404: if a user with the given `snowflake` could not be found + + ### GET /bot/users//metricity_review_data + Gets metricity data for a single user's review by ID. + + #### Response format + >>> { + ... 'joined_at': '2020-08-26T08:09:43.507000', + ... 'top_channel_activity': [['off-topic', 15], + ... ['talent-pool', 4], + ... ['defcon', 2]], + ... 'total_messages': 22 + ... } + + #### Status codes + - 200: returned on success + - 404: if a user with the given `snowflake` could not be found + ### POST /bot/users Adds a single or multiple new users. The roles attached to the user(s) must be roles known by the site. + Users that already exist in the database will be skipped. #### Request body >>> { @@ -67,11 +150,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): ... } Alternatively, request users can be POSTed as a list of above objects, - in which case multiple users will be created at once. + in which case multiple users will be created at once. In this case, + the response is an empty list. #### Status codes - 201: returned on success - 400: if one of the given roles does not exist, or one of the given fields is invalid + - 400: if multiple user objects with the same id are given ### PUT /bot/users/ Update the user with the given `snowflake`. @@ -109,6 +194,34 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): - 400: if the request body was invalid, see response body for details - 404: if the user with the given `snowflake` could not be found + ### BULK PATCH /bot/users/bulk_patch + Update users with the given `ids` and `details`. + `id` field and at least one other field is mandatory. + + #### Request body + >>> [ + ... { + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool + ... }, + ... { + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool + ... }, + ... ] + + #### Status codes + - 200: returned on success + - 400: if the request body was invalid, see response body for details + - 400: if multiple user objects with the same id are given + - 404: if the user with the given id does not exist + ### DELETE /bot/users/ Deletes the user with the given `snowflake`. @@ -118,4 +231,65 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): """ serializer_class = UserSerializer - queryset = User.objects + queryset = User.objects.all().order_by("id") + pagination_class = UserListPagination + + def get_serializer(self, *args, **kwargs) -> ModelSerializer: + """Set Serializer many attribute to True if request body contains a list.""" + if isinstance(kwargs.get('data', {}), list): + kwargs['many'] = True + + return super().get_serializer(*args, **kwargs) + + @action(detail=False, methods=["PATCH"], name='user-bulk-patch') + def bulk_patch(self, request: Request) -> Response: + """Update multiple User objects in a single request.""" + serializer = self.get_serializer( + instance=self.get_queryset(), + data=request.data, + many=True, + partial=True + ) + + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data, status=status.HTTP_200_OK) + + @action(detail=True) + def metricity_data(self, request: Request, pk: str = None) -> Response: + """Request handler for metricity_data endpoint.""" + user = self.get_object() + + try: + Infraction.objects.get(user__id=user.id, active=True, type="voice_ban") + except ObjectDoesNotExist: + voice_banned = False + else: + voice_banned = True + + with Metricity() as metricity: + try: + data = metricity.user(user.id) + data["total_messages"] = metricity.total_messages(user.id) + data["voice_banned"] = voice_banned + data["activity_blocks"] = metricity.total_message_blocks(user.id) + return Response(data, status=status.HTTP_200_OK) + except NotFound: + return Response(dict(detail="User not found in metricity"), + status=status.HTTP_404_NOT_FOUND) + + @action(detail=True) + def metricity_review_data(self, request: Request, pk: str = None) -> Response: + """Request handler for metricity_review_data endpoint.""" + user = self.get_object() + + with Metricity() as metricity: + try: + data = metricity.user(user.id) + data["total_messages"] = metricity.total_messages(user.id) + data["top_channel_activity"] = metricity.top_channel_activity(user.id) + return Response(data, status=status.HTTP_200_OK) + except NotFound: + return Response(dict(detail="User not found in metricity"), + status=status.HTTP_404_NOT_FOUND) diff --git a/pydis_site/apps/api/viewsets/log_entry.py b/pydis_site/apps/api/viewsets/log_entry.py deleted file mode 100644 index 9108a4fa..00000000 --- a/pydis_site/apps/api/viewsets/log_entry.py +++ /dev/null @@ -1,36 +0,0 @@ -from rest_framework.mixins import CreateModelMixin -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.log_entry import LogEntry -from pydis_site.apps.api.serializers import LogEntrySerializer - - -class LogEntryViewSet(CreateModelMixin, GenericViewSet): - """ - View supporting the creation of log entries in the database for viewing via the log browser. - - ## Routes - ### POST /logs - Create a new log entry. - - #### Request body - >>> { - ... 'application': str, # 'bot' | 'seasonalbot' | 'site' - ... 'logger_name': str, # such as 'bot.cogs.moderation' - ... 'timestamp': Optional[str], # from `datetime.utcnow().isoformat()` - ... 'level': str, # 'debug' | 'info' | 'warning' | 'error' | 'critical' - ... 'module': str, # such as 'pydis_site.apps.api.serializers' - ... 'line': int, # > 0 - ... 'message': str, # textual formatted content of the logline - ... } - - #### Status codes - - 201: returned on success - - 400: if the request body has invalid fields, see the response for details - - ## Authentication - Requires a API token. - """ - - queryset = LogEntry.objects.all() - serializer_class = LogEntrySerializer diff --git a/pydis_site/apps/home/migrations/0002_auto_now_on_repository_metadata.py b/pydis_site/apps/home/migrations/0002_auto_now_on_repository_metadata.py new file mode 100644 index 00000000..7e78045b --- /dev/null +++ b/pydis_site/apps/home/migrations/0002_auto_now_on_repository_metadata.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.11 on 2020-12-21 22:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='repositorymetadata', + name='last_updated', + field=models.DateTimeField(auto_now=True, help_text='The date and time this data was last fetched.'), + ), + ] diff --git a/pydis_site/apps/home/models/repository_metadata.py b/pydis_site/apps/home/models/repository_metadata.py index 92d2404d..00a83cd7 100644 --- a/pydis_site/apps/home/models/repository_metadata.py +++ b/pydis_site/apps/home/models/repository_metadata.py @@ -1,32 +1,31 @@ from django.db import models -from django.utils import timezone class RepositoryMetadata(models.Model): """Information about one of our repos fetched from the GitHub API.""" last_updated = models.DateTimeField( - default=timezone.now, - help_text="The date and time this data was last fetched." + help_text="The date and time this data was last fetched.", + auto_now=True, ) repo_name = models.CharField( primary_key=True, max_length=40, - help_text="The full name of the repo, e.g. python-discord/site" + help_text="The full name of the repo, e.g. python-discord/site", ) description = models.CharField( max_length=400, - help_text="The description of the repo." + help_text="The description of the repo.", ) forks = models.IntegerField( - help_text="The number of forks of this repo" + help_text="The number of forks of this repo", ) stargazers = models.IntegerField( - help_text="The number of stargazers for this repo" + help_text="The number of stargazers for this repo", ) language = models.CharField( max_length=20, - help_text="The primary programming language used for this repo." + help_text="The primary programming language used for this repo.", ) def __str__(self): diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json index 10be4f99..ddbffed8 100644 --- a/pydis_site/apps/home/tests/mock_github_api_response.json +++ b/pydis_site/apps/home/tests/mock_github_api_response.json @@ -35,7 +35,7 @@ "forks_count": 31 }, { - "full_name": "python-discord/seasonalbot", + "full_name": "python-discord/sir-lancebot", "description": "test", "stargazers_count": 97, "language": "Python", diff --git a/pydis_site/apps/home/tests/test_repodata_helpers.py b/pydis_site/apps/home/tests/test_repodata_helpers.py index 77b1a68d..5634bc9b 100644 --- a/pydis_site/apps/home/tests/test_repodata_helpers.py +++ b/pydis_site/apps/home/tests/test_repodata_helpers.py @@ -123,10 +123,38 @@ class TestRepositoryMetadataHelpers(TestCase): mock_get.return_value.json.return_value = ['garbage'] metadata = self.home_view._get_repo_data() - self.assertEquals(len(metadata), len(self.home_view.repos)) - for item in metadata: - with self.subTest(item=item): - self.assertEqual(item.description, "Not available.") - self.assertEqual(item.forks, 999) - self.assertEqual(item.stargazers, 999) - self.assertEqual(item.language, "Python") + self.assertEquals(len(metadata), 0) + + def test_cleans_up_stale_metadata(self): + """Tests that we clean up stale metadata when we start the HomeView.""" + repo_data = RepositoryMetadata( + repo_name="python-discord/INVALID", + description="testrepo", + forks=42, + stargazers=42, + language="English", + last_updated=timezone.now() - timedelta(seconds=HomeView.repository_cache_ttl + 1), + ) + repo_data.save() + self.home_view.__init__() + cached_repos = RepositoryMetadata.objects.all() + cached_names = [repo.repo_name for repo in cached_repos] + + self.assertNotIn("python-discord/INVALID", cached_names) + + def test_dont_clean_up_unstale_metadata(self): + """Tests that we don't clean up good metadata when we start the HomeView.""" + repo_data = RepositoryMetadata( + repo_name="python-discord/site", + description="testrepo", + forks=42, + stargazers=42, + language="English", + last_updated=timezone.now() - timedelta(seconds=HomeView.repository_cache_ttl + 1), + ) + repo_data.save() + self.home_view.__init__() + cached_repos = RepositoryMetadata.objects.all() + cached_names = [repo.repo_name for repo in cached_repos] + + self.assertIn("python-discord/site", cached_names) diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index e475c491..1e2af8f3 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.urls import include, path -from .views import HomeView +from .views import HomeView, timeline app_name = 'home' urlpatterns = [ @@ -11,4 +11,5 @@ urlpatterns = [ 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"), ] diff --git a/pydis_site/apps/home/views/__init__.py b/pydis_site/apps/home/views/__init__.py index 971d73a3..28cc4d65 100644 --- a/pydis_site/apps/home/views/__init__.py +++ b/pydis_site/apps/home/views/__init__.py @@ -1,3 +1,3 @@ -from .home import HomeView +from .home import HomeView, timeline -__all__ = ["HomeView"] +__all__ = ["HomeView", "timeline"] diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py index 3b5cd5ac..e77772fb 100644 --- a/pydis_site/apps/home/views/home.py +++ b/pydis_site/apps/home/views/home.py @@ -1,4 +1,4 @@ -import datetime +import logging from typing import Dict, List import requests @@ -10,11 +10,13 @@ from django.views import View from pydis_site.apps.home.models import RepositoryMetadata +log = logging.getLogger(__name__) + class HomeView(View): """The main landing page for the website.""" - github_api = "https://api.github.com/users/python-discord/repos" + github_api = "https://api.github.com/users/python-discord/repos?per_page=100" repository_cache_ttl = 3600 # Which of our GitHub repos should be displayed on the front page, and in which order? @@ -22,82 +24,98 @@ class HomeView(View): "python-discord/site", "python-discord/bot", "python-discord/snekbox", - "python-discord/seasonalbot", + "python-discord/sir-lancebot", "python-discord/metricity", "python-discord/django-simple-bulma", ] + def __init__(self): + """Clean up stale RepositoryMetadata.""" + RepositoryMetadata.objects.exclude(repo_name__in=self.repos).delete() + def _get_api_data(self) -> Dict[str, Dict[str, str]]: - """Call the GitHub API and get information about our repos.""" - repo_dict: Dict[str, dict] = {repo_name: {} for repo_name in self.repos} + """ + Call the GitHub API and get information about our repos. + + If we're unable to get that info for any reason, return an empty dict. + """ + repo_dict = {} # Fetch the data from the GitHub API api_data: List[dict] = requests.get(self.github_api).json() # Process the API data into our dict for repo in api_data: - full_name = repo["full_name"] - - if full_name in self.repos: - repo_dict[full_name] = { - "full_name": repo["full_name"], - "description": repo["description"], - "language": repo["language"], - "forks_count": repo["forks_count"], - "stargazers_count": repo["stargazers_count"], - } + try: + full_name = repo["full_name"] + + if full_name in self.repos: + repo_dict[full_name] = { + "full_name": repo["full_name"], + "description": repo["description"], + "language": repo["language"], + "forks_count": repo["forks_count"], + "stargazers_count": repo["stargazers_count"], + } + # Something is not right about the API data we got back from GitHub. + except (TypeError, ConnectionError, KeyError) as e: + log.error( + "Unable to parse the GitHub repository metadata from response!", + extra={ + 'api_data': api_data, + 'error': e + } + ) + continue return repo_dict def _get_repo_data(self) -> List[RepositoryMetadata]: """Build a list of RepositoryMetadata objects that we can use to populate the front page.""" - # Try to get site data from the cache - try: - repo_data = RepositoryMetadata.objects.get(repo_name="python-discord/site") + database_repositories = [] - # If the data is stale, we should refresh it. - if (timezone.now() - repo_data.last_updated).seconds > self.repository_cache_ttl: + # First, let's see if we have any metadata cached. + cached_data = RepositoryMetadata.objects.all() - # Try to get new data from the API. If it fails, return the cached data. - try: - api_repositories = self._get_api_data() - except (TypeError, ConnectionError): - return RepositoryMetadata.objects.all() - database_repositories = [] - - # Update or create all RepoData objects in self.repos - for repo_name, api_data in api_repositories.items(): - try: - repo_data = RepositoryMetadata.objects.get(repo_name=repo_name) - repo_data.description = api_data["description"] - repo_data.language = api_data["language"] - repo_data.forks = api_data["forks_count"] - repo_data.stargazers = api_data["stargazers_count"] - except RepositoryMetadata.DoesNotExist: - repo_data = RepositoryMetadata( - repo_name=api_data["full_name"], - description=api_data["description"], - forks=api_data["forks_count"], - stargazers=api_data["stargazers_count"], - language=api_data["language"], - ) - repo_data.save() - database_repositories.append(repo_data) - return database_repositories - - # Otherwise, if the data is fresher than 2 minutes old, we should just return it. - else: - return RepositoryMetadata.objects.all() + # If we don't, we have to create some! + if not cached_data: - # If this is raised, the database has no repodata at all, we will create them all. - except RepositoryMetadata.DoesNotExist: - database_repositories = [] - try: - # Get new data from API - api_repositories = self._get_api_data() + # Try to get new data from the API. If it fails, we'll return an empty list. + # In this case, we simply don't display our projects on the site. + api_repositories = self._get_api_data() + + # Create all the repodata records in the database. + for api_data in api_repositories.values(): + repo_data = RepositoryMetadata( + repo_name=api_data["full_name"], + description=api_data["description"], + forks=api_data["forks_count"], + stargazers=api_data["stargazers_count"], + language=api_data["language"], + ) + + repo_data.save() + database_repositories.append(repo_data) + + return database_repositories + + # If the data is stale, we should refresh it. + if (timezone.now() - cached_data[0].last_updated).seconds > self.repository_cache_ttl: + # Try to get new data from the API. If it fails, return the cached data. + api_repositories = self._get_api_data() + + if not api_repositories: + return RepositoryMetadata.objects.all() - # Create all the repodata records in the database. - for api_data in api_repositories.values(): + # Update or create all RepoData objects in self.repos + for repo_name, api_data in api_repositories.items(): + try: + repo_data = RepositoryMetadata.objects.get(repo_name=repo_name) + repo_data.description = api_data["description"] + repo_data.language = api_data["language"] + repo_data.forks = api_data["forks_count"] + repo_data.stargazers = api_data["stargazers_count"] + except RepositoryMetadata.DoesNotExist: repo_data = RepositoryMetadata( repo_name=api_data["full_name"], description=api_data["description"], @@ -105,24 +123,20 @@ class HomeView(View): stargazers=api_data["stargazers_count"], language=api_data["language"], ) - repo_data.save() - database_repositories.append(repo_data) - except TypeError: - for repo_name in self.repos: - repo_data = RepositoryMetadata( - last_updated=timezone.now() - datetime.timedelta(minutes=50), - repo_name=repo_name, - description="Not available.", - forks=999, - stargazers=999, - language="Python", - ) - repo_data.save() - database_repositories.append(repo_data) - + repo_data.save() + database_repositories.append(repo_data) return database_repositories + # Otherwise, if the data is fresher than 2 minutes old, we should just return it. + else: + return RepositoryMetadata.objects.all() + def get(self, request: WSGIRequest) -> HttpResponse: """Collect repo data and render the homepage view.""" repo_data = self._get_repo_data() return render(request, "home/index.html", {"repo_data": repo_data}) + + +def timeline(request: WSGIRequest) -> HttpResponse: + """Render timeline view.""" + return render(request, 'home/timeline.html') diff --git a/pydis_site/apps/redirect/tests.py b/pydis_site/apps/redirect/tests.py index c145ecda..fce2642f 100644 --- a/pydis_site/apps/redirect/tests.py +++ b/pydis_site/apps/redirect/tests.py @@ -40,11 +40,14 @@ class RedirectTests(TestCase): if data.get("prefix_redirect", False): expected_args = ( "".join( - tuple(data.get("redirect_arguments", ())) + TESTING_ARGUMENTS.get(name, ()) + tuple(data.get("redirect_arguments", ())) + + TESTING_ARGUMENTS.get(name, ()) ), ) else: - expected_args = TESTING_ARGUMENTS.get(name, ()) + tuple(data.get("redirect_arguments", ())) + expected_args = ( + TESTING_ARGUMENTS.get(name, ()) + tuple(data.get("redirect_arguments", ())) + ) self.assertEqual(1, len(resp.redirect_chain)) self.assertRedirects( diff --git a/pydis_site/apps/resources/resources/reading/books/effective_python.yaml b/pydis_site/apps/resources/resources/reading/books/effective_python.yaml index 1d4124cc..becd0578 100644 --- a/pydis_site/apps/resources/resources/reading/books/effective_python.yaml +++ b/pydis_site/apps/resources/resources/reading/books/effective_python.yaml @@ -1,4 +1,4 @@ -description: A book that gives 59 best practices for writing excellent Python. Great +description: A book that gives 90 best practices for writing excellent Python. Great for intermediates. name: Effective Python position: 3 @@ -7,8 +7,9 @@ urls: url: https://effectivepython.com/ color: teal - icon: branding/amazon - url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134034287 + url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134853989 color: amazon-orange + title: Amazon - icon: branding/github url: https://github.com/bslatkin/effectivepython color: black diff --git a/pydis_site/constants.py b/pydis_site/constants.py index 0b76694a..c7ab5db0 100644 --- a/pydis_site/constants.py +++ b/pydis_site/constants.py @@ -1,5 +1,3 @@ -import git +import os -# Git SHA -repo = git.Repo(search_parent_directories=True) -GIT_SHA = repo.head.object.hexsha +GIT_SHA = os.environ.get("GIT_SHA", "development") diff --git a/pydis_site/hosts.py b/pydis_site/hosts.py index 898e8cdc..5a837a8b 100644 --- a/pydis_site/hosts.py +++ b/pydis_site/hosts.py @@ -4,7 +4,10 @@ from django_hosts import host, patterns host_patterns = patterns( '', host(r'admin', 'pydis_site.apps.admin.urls', name="admin"), + # External API ingress (over the net) host(r'api', 'pydis_site.apps.api.urls', name='api'), + # Internal API ingress (cluster local) + host(r'pydis-api', 'pydis_site.apps.api.urls', name='internal_api'), host(r'staff', 'pydis_site.apps.staff.urls', name='staff'), host(r'.*', 'pydis_site.apps.home.urls', name=settings.DEFAULT_HOST) ) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 65bd8e7a..2fd16241 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -23,14 +23,14 @@ from pydis_site.constants import GIT_SHA env = environ.Env( DEBUG=(bool, False), - SITE_SENTRY_DSN=(str, "") + SITE_DSN=(str, "") ) sentry_sdk.init( - dsn=env('SITE_SENTRY_DSN'), + dsn=env('SITE_DSN'), integrations=[DjangoIntegration()], send_default_pii=True, - release=f"pydis-site@{GIT_SHA}" + release=f"site@{GIT_SHA}" ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -42,21 +42,7 @@ DEBUG = env('DEBUG') # SECURITY WARNING: keep the secret key used in production secret! if DEBUG: - ALLOWED_HOSTS = env.list( - 'ALLOWED_HOSTS', - default=[ - 'pythondiscord.local', - 'api.pythondiscord.local', - 'admin.pythondiscord.local', - 'staff.pythondiscord.local', - '0.0.0.0', # noqa: S104 - 'localhost', - 'web', - 'api.web', - 'admin.web', - 'staff.web' - ] - ) + ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) SECRET_KEY = "yellow polkadot bikini" # noqa: S105 elif 'CI' in os.environ: @@ -71,10 +57,7 @@ else: 'admin.pythondiscord.com', 'api.pythondiscord.com', 'staff.pythondiscord.com', - 'pydis.com', - 'api.pydis.com', - 'admin.pydis.com', - 'staff.pydis.com', + 'pydis-api.default.svc.cluster.local', ] ) SECRET_KEY = env('SECRET_KEY') @@ -147,7 +130,8 @@ WSGI_APPLICATION = 'pydis_site.wsgi.application' # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { - 'default': env.db() + 'default': env.db(), + 'metricity': env.db('METRICITY_DB_URL'), } # Password validation @@ -232,14 +216,11 @@ LOGGING = { 'handlers': { 'console': { 'class': 'logging.StreamHandler' - }, - 'database': { - 'class': 'pydis_site.apps.api.dblogger.DatabaseLogHandler' } }, 'loggers': { 'django': { - 'handlers': ['console', 'database'], + 'handlers': ['console'], 'propagate': True, 'level': env( 'LOG_LEVEL', @@ -279,6 +260,9 @@ BULMA_SETTINGS = { "footer-padding": "1rem 1.5rem 1rem", "tooltip-max-width": "30rem", }, + "extensions": [ + "bulma-navbar-burger", + ], } # Information about site repository @@ -292,4 +276,5 @@ 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") +# 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 dc7c504d..830dad59 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -12,42 +12,69 @@ main.site-content { flex: 1; } -div.card.has-equal-height { +.card.has-equal-height { height: 100%; display: flex; flex-direction: column; } -.navbar-item.is-fullsize { - padding: 0; +.navbar { + padding-right: 0.8em; } -.navbar-item.is-fullsize img { - max-height: 4.75rem; +.navbar-item .navbar-link { + padding-left: 1.5em; + padding-right: 2.5em; +} + +.navbar-link:not(.is-arrowless)::after { + right: 1.125em; + margin-top: -0.455em; } .navbar-item.has-no-highlight:hover { background-color: transparent; } -.navbar-item.has-left-margin-1 { - margin-left: 1rem; +#navbar-banner { + background-color: transparent; } -.navbar-item.has-left-margin-2 { - margin-left: 2rem; +#navbar-banner img { + max-height: 3rem; } -.navbar-item.has-left-margin-3 { - margin-left: 3rem; +#discord-btn a { + color: transparent; + background-image: url(../../images/navbar/discord.svg); + background-size: 200%; + background-position: 100% 50%; + background-repeat: no-repeat; + padding-left: 2.5rem; + padding-right: 2.5rem; + background-color: #697ec4ff; + margin-left: 0.5rem; + transition: all 0.2s cubic-bezier(.25,.8,.25,1); + overflow: hidden; } -#navbar-banner { +#discord-btn:hover a { + box-shadow: 0 1px 4px rgba(0,0,0,0.16), 0 1px 6px rgba(0,0,0,0.23); + /*transform: scale(1.03) translate3d(0,0,0);*/ + background-size: 200%; + background-position: 1% 50%; +} + +#discord-btn:hover { background-color: transparent; } -#navbar-banner img { - max-height: 3rem; +#linode-logo { + padding-left: 15px; + background: url(https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg) no-repeat center; + filter: invert(1) grayscale(1); + background-size: 60px; + color: #00000000; } #django-logo { @@ -97,17 +124,16 @@ 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; } diff --git a/pydis_site/static/css/error_pages.css b/pydis_site/static/css/error_pages.css new file mode 100644 index 00000000..e59e2a54 --- /dev/null +++ b/pydis_site/static/css/error_pages.css @@ -0,0 +1,67 @@ +html { + height: 100%; +} + +body { + background-color: #7289DA; + background-image: url("https://raw.githubusercontent.com/python-discord/branding/main/logos/banner_pattern/banner_pattern.svg"); + background-size: 128px; + font-family: "Hind", "Helvetica", "Arial", sans-serif; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + margin: 0; +} + +h1, +p { + color: black; + padding: 0; + margin: 0; + margin-bottom: 10px; +} + +h1 { + margin-bottom: 15px; + font-size: 26px; +} + +p, +li { + line-height: 125%; +} + +a { + color: #7289DA; +} + +ul { + margin-bottom: 0; +} + +li { + margin-top: 10px; +} + +.error-box { + display: flex; + flex-direction: column; + max-width: 512px; + background-color: white; + border-radius: 20px; + overflow: hidden; + box-shadow: 5px 7px 40px rgba(0, 0, 0, 0.432); +} + +.logo-box { + display: flex; + justify-content: center; + height: 80px; + padding: 15px; + background-color: #758ad4; +} + +.content-box { + padding: 25px; +} diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index ba856a8e..ee6f6e4c 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -1,87 +1,226 @@ -.discord-banner { - border-radius: 0.5rem; +h1 { + padding-bottom: 0.5em; } -.hero-image { - width: 20rem; - margin: auto; +/* Mobile-only notice banner */ + +#mobile-notice { + margin: 5px; + margin-bottom: -10px!important; } -.hero-body { - padding-top: 1rem; - padding-bottom: 1rem; +/* Wave hero */ + +#wave-hero { + position: relative; + background-color: #7289DA; + color: #fff; + height: 32vw; + min-height: 270px; + max-height: 500px; + overflow-x: hidden; + width: 100%; + padding: 0; } -.section-sp img { - height: 5rem; - margin-right: 2rem; +#wave-hero .container { + z-index: 4; /* keep hero contents above wave animations */ } -.video-container iframe, -.video-container object, -.video-container embed { - width: 100%; - height: calc(92vw * 0.5625); - margin: 8px auto auto auto; +@media screen and (min-width: 769px) and (max-width: 1023px) { + #wave-hero .columns { + margin: 0 1em 0 1em; /* Stop cards touching canvas edges in table-view */ + } +} + +#wave-hero iframe { + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + transition: all 0.3s cubic-bezier(.25,.8,.25,1); + border-radius: 10px; + margin-top: 1em; + border: none; +} + +#wave-hero iframe:hover { + box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); +} + +#wave-hero-centered { + margin: auto auto; +} + +#wave-hero-right img{ + border-radius: 10px; + box-shadow: 0 1px 6px rgba(0,0,0,0.16), 0 1px 6px rgba(0,0,0,0.23); + margin-top: 1em; + text-align: right; +} + +#wave-hero .wave { + background: url(../../images/waves/wave_dark.svg) repeat-x; + position: absolute; + bottom: 0; + width: 6400px; + animation-name: wave; + animation-timing-function: cubic-bezier(.36,.45,.63,.53); + animation-iteration-count: infinite; + transform: translate3d(0,0,0); /* Trigger 3D acceleration for smoother animation */ +} + +#front-wave { + animation-duration: 60s; + animation-delay: -50s; + opacity: 0.5; + height: 178px; +} + +#back-wave { + animation-duration: 65s; + height: 198px; +} + +#bottom-wave { + animation-duration: 50s; + animation-delay: -10s; + background: url(../../images/waves/wave_white.svg) repeat-x !important; + height: 26px; + z-index: 3; +} + +@keyframes wave { + 0% { + margin-left: 0; + } + 100% { + margin-left: -1600px; + } +} + +/* Showcase */ + +#showcase { + margin: 0 1em; +} + +#showcase .mini-timeline { + height: 3px; + position: relative; + margin: 50px 0 50px 0; + background: linear-gradient(to right, #ffffff00, #666666ff, #ffffff00); + text-align: center; +} + +#showcase .mini-timeline i { + display: inline-block; + vertical-align: middle; + width: 30px; + height: 30px; + border-radius: 50%; + position: relative; + top: -14px; + margin: 0 4% 0 4%; + background-color: #3EB2EF; + color: white; + font-size: 15px; + line-height: 33px; + border:none; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + transition: all 0.3s cubic-bezier(.25,.8,.25,1); +} + +#showcase .mini-timeline i:hover { + box-shadow: 0 2px 5px rgba(0,0,0,0.16), 0 2px 5px rgba(0,0,0,0.23); + transform: scale(1.5); +} + +/* Projects */ + +#projects { + padding-top: 0; } -div.card.github-card { +#projects .card { box-shadow: none; border: #d1d5da 1px solid; border-radius: 3px; + transition: all 0.2s cubic-bezier(.25,.8,.25,1); + height: 100%; + display: flex; + flex-direction: column; } -div.repo-headline { +#projects .card:hover { + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); +} + +#projects .card-header { + box-shadow: none; font-size: 1.25rem; - margin-bottom: 8px; + padding: 1.5rem 1.5rem 0 1.5rem; } -span.repo-language-dot { - border-radius: 50%; - height: 12px; - width: 12px; - top: 1px; - display: inline-block; - position: relative; +#projects .card-header-icon { + font-size: 1.5rem; + padding: 0 1rem 0 0; } -span.repo-language-dot.python { - background-color: #3572A5; +#projects .card-header-title { + padding: 0; + color: #7289DA; } -span.repo-language-dot.html { - background-color: #e34c26; +#projects .card:hover .card-header-title { + color: #363636; } -span.repo-language-dot.css { - background-color: #563d7c; +#projects .card-content { + padding-top: 8px; + padding-bottom: 1rem; +} + +#projects .card-footer { + margin-top: auto; + border: none; } -span.repo-language-dot.javascript { - background-color: #f1e05a; +#projects .card-footer-item { + border: none; } -#repo-footer-item { - margin-left: 1.2rem; +#projects .card-footer-item i { + margin-right: 0.5rem; } -#sponsors-hero { +#projects .repo-language-dot { + border-radius: 50%; + height: 12px; + width: 12px; + top: -1px; + display: inline-block; + position: relative; +} + +#projects .repo-language-dot.python { background-color: #3572A5; } +#projects .repo-language-dot.html { background-color: #e34c26; } +#projects .repo-language-dot.css { background-color: #563d7c; } +#projects .repo-language-dot.javascript { background-color: #f1e05a; } + +/* Sponsors */ + +#sponsors .hero-body { padding-top: 2rem; padding-bottom: 3rem; + + text-align: center; } -@media screen and (min-width: 1088px) { - .video-container iframe { - height: calc(42vw * 0.5625); - max-height: 371px; - max-width: 660px; - } +#sponsors .columns { + justify-content: center; + margin: auto; + max-width: 80%; } -@media screen and (max-width: 1087px) { - .video-container iframe { - height: calc(92vw * 0.5625); - max-height: none; - max-width: none; - } +#sponsors img { + height: 5rem; + margin: auto 1rem; } diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css new file mode 100644 index 00000000..0a4dfbb6 --- /dev/null +++ b/pydis_site/static/css/home/timeline.css @@ -0,0 +1,3823 @@ +body { + background-color: hsl(0, 0%, 100%); + background-color: var(--color-bg, white) +} + +h2 { + font-size: 2em; +} + +@media (max-width: 500px) { + h2 { + font-size: 1em; + } +} + +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, main, form legend { + display: block +} + +ol, ul { + list-style: none +} + +blockquote, q { + quotes: none +} + +button, input, textarea, select { + margin: 0 +} + +.pastel-red { + background-color: #FF7878 !important; +} + +.pastel-orange { + background-color: #FFBF76 !important; +} + +.pastel-green { + background-color: #8bd6a7 !important; +} + +.pastel-blue { + background-color: #8edbec !important; +} + +.pastel-purple { + background-color: #CBB1FF !important; +} + +.pastel-pink { + background-color: #F6ACFF !important; +} + +.pastel-lime { + background-color: #b6df3a !important; +} + +.pastel-dark-blue { + background-color: #576297 !important; +} + +.pydis-logo-banner { + background-color: #7289DA !important; + border-radius: 10px; +} + +.pydis-logo-banner img { + padding-right: 20px; +} + +.btn, .form-control, .link, .reset { + background-color: transparent; + padding: 0; + border: 0; + border-radius: 0; + color: inherit; + line-height: inherit; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none +} + +select.form-control::-ms-expand { + display: none +} + +textarea { + resize: vertical; + overflow: auto; + vertical-align: top +} + +input::-ms-clear { + display: none +} + +table { + border-collapse: collapse; + border-spacing: 0 +} + +img, video, svg { + max-width: 100% +} + +[data-theme] { + background-color: hsl(0, 0%, 100%); + background-color: var(--color-bg, #fff); + color: hsl(240, 4%, 20%); + color: var(--color-contrast-high, #313135) +} + +:root { + --space-unit: 1em; + --space-xxxxs: calc(0.125*var(--space-unit)); + --space-xxxs: calc(0.25*var(--space-unit)); + --space-xxs: calc(0.375*var(--space-unit)); + --space-xs: calc(0.5*var(--space-unit)); + --space-sm: calc(0.75*var(--space-unit)); + --space-md: calc(1.25*var(--space-unit)); + --space-lg: calc(2*var(--space-unit)); + --space-xl: calc(3.25*var(--space-unit)); + --space-xxl: calc(5.25*var(--space-unit)); + --space-xxxl: calc(8.5*var(--space-unit)); + --space-xxxxl: calc(13.75*var(--space-unit)); + --component-padding: var(--space-md) +} + +:root { + --max-width-xxs: 32rem; + --max-width-xs: 38rem; + --max-width-sm: 48rem; + --max-width-md: 64rem; + --max-width-lg: 80rem; + --max-width-xl: 90rem; + --max-width-xxl: 120rem +} + +.container { + width: calc(100% - 1.25em); + width: calc(100% - 2*var(--component-padding)); + margin-left: auto; + margin-right: auto +} + +.max-width-xxs { + max-width: 32rem; + max-width: var(--max-width-xxs) +} + +.max-width-xs { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +.max-width-sm { + max-width: 48rem; + max-width: var(--max-width-sm) +} + +.max-width-md { + max-width: 64rem; + max-width: var(--max-width-md) +} + +.max-width-lg { + max-width: 80rem; + max-width: var(--max-width-lg) +} + +.max-width-xl { + max-width: 90rem; + max-width: var(--max-width-xl) +} + +.max-width-xxl { + max-width: 120rem; + max-width: var(--max-width-xxl) +} + +.max-width-adaptive-sm { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive-sm { + max-width: 48rem; + max-width: var(--max-width-sm) + } +} + +.max-width-adaptive-md { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive-md { + max-width: 64rem; + max-width: var(--max-width-md) + } +} + +.max-width-adaptive, .max-width-adaptive-lg { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive, .max-width-adaptive-lg { + max-width: 64rem; + max-width: var(--max-width-md) + } +} + +@media (min-width: 90rem) { + .max-width-adaptive, .max-width-adaptive-lg { + max-width: 80rem; + max-width: var(--max-width-lg) + } +} + +.max-width-adaptive-xl { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive-xl { + max-width: 64rem; + max-width: var(--max-width-md) + } +} + +@media (min-width: 90rem) { + .max-width-adaptive-xl { + max-width: 90rem; + max-width: var(--max-width-xl) + } +} + +.grid { + --grid-gap: 0px; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap +} + +.grid>* { + -ms-flex-preferred-size: 100%; + flex-basis: 100% +} + +[class*="grid-gap"] { + margin-bottom: 1em * -1; + margin-bottom: calc(var(--grid-gap, 1em)*-1); + margin-right: 1em * -1; + margin-right: calc(var(--grid-gap, 1em)*-1) +} + +[class*="grid-gap"]>* { + margin-bottom: 1em; + margin-bottom: var(--grid-gap, 1em); + margin-right: 1em; + margin-right: var(--grid-gap, 1em) +} + +.grid-gap-xxxxs { + --grid-gap: var(--space-xxxxs) +} + +.grid-gap-xxxs { + --grid-gap: var(--space-xxxs) +} + +.grid-gap-xxs { + --grid-gap: var(--space-xxs) +} + +.grid-gap-xs { + --grid-gap: var(--space-xs) +} + +.grid-gap-sm { + --grid-gap: var(--space-sm) +} + +.grid-gap-md { + --grid-gap: var(--space-md) +} + +.grid-gap-lg { + --grid-gap: var(--space-lg) +} + +.grid-gap-xl { + --grid-gap: var(--space-xl) +} + +.grid-gap-xxl { + --grid-gap: var(--space-xxl) +} + +.grid-gap-xxxl { + --grid-gap: var(--space-xxxl) +} + +.grid-gap-xxxxl { + --grid-gap: var(--space-xxxxl) +} + +.col { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% +} + +.col-1 { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-2 { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-3 { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) +} + +.col-4 { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-5 { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-6 { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) +} + +.col-7 { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-8 { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-9 { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) +} + +.col-10 { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-11 { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-12 { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) +} + +@media (min-width: 32rem) { + .col\@xs { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@xs { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@xs { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@xs { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@xs { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@xs { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@xs { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@xs { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@xs { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@xs { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@xs { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@xs { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@xs { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 48rem) { + .col\@sm { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@sm { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@sm { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@sm { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@sm { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@sm { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@sm { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@sm { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@sm { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@sm { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@sm { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@sm { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@sm { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 64rem) { + .col\@md { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@md { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@md { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@md { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@md { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@md { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@md { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@md { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@md { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@md { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@md { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@md { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@md { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 80rem) { + .col\@lg { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@lg { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@lg { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@lg { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@lg { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@lg { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@lg { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@lg { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@lg { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@lg { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@lg { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@lg { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@lg { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 90rem) { + .col\@xl { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@xl { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@xl { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@xl { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@xl { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@xl { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@xl { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@xl { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@xl { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@xl { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@xl { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@xl { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@xl { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +:root { + --radius-sm: calc(var(--radius, 0.25em)/2); + --radius-md: var(--radius, 0.25em); + --radius-lg: calc(var(--radius, 0.25em)*2); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, .085), 0 1px 8px rgba(0, 0, 0, .1); + --shadow-md: 0 1px 8px rgba(0, 0, 0, .1), 0 8px 24px rgba(0, 0, 0, .15); + --shadow-lg: 0 1px 8px rgba(0, 0, 0, .1), 0 16px 48px rgba(0, 0, 0, .1), 0 24px 60px rgba(0, 0, 0, .1); + --bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275); + --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); + --ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19); + --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1) +} + +:root { + --body-line-height: 1.4; + --heading-line-height: 1.2 +} + +body { + color: hsl(240, 4%, 20%); + color: var(--color-contrast-high, #313135) +} + +h1, h2, h3, h4 { + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21); + line-height: 1.2; + line-height: var(--heading-line-height, 1.2) +} + +.text-xxxl { + font-size: 2.48832em; + font-size: var(--text-xxxl, 2.488em) +} + +small, .text-sm { + font-size: 0.83333em; + font-size: var(--text-sm, 0.833em) +} + +.text-xs { + font-size: 0.69444em; + font-size: var(--text-xs, 0.694em) +} + +strong, .text-bold { + font-weight: bold +} + +s { + text-decoration: line-through +} + +u, .text-underline { + text-decoration: underline +} + + +.text-component h1, .text-component h2, .text-component h3, .text-component h4 { + line-height: 1.2; + line-height: var(--component-heading-line-height, 1.2); + margin-bottom: 0.25em; + margin-bottom: calc(var(--space-xxxs)*var(--text-vspace-multiplier, 1)) +} + +.text-component h2, .text-component h3, .text-component h4 { + margin-top: 0.75em; + margin-top: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) +} + +.text-component p, .text-component blockquote, .text-component ul li, .text-component ol li { + line-height: 1.4; + line-height: var(--component-body-line-height) +} + +.text-component ul, .text-component ol, .text-component p, .text-component blockquote, .text-component .text-component__block { + margin-bottom: 0.75em; + margin-bottom: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) +} + +.text-component ul, .text-component ol { + padding-left: 1em +} + +.text-component ul { + list-style-type: disc +} + +.text-component ol { + list-style-type: decimal +} + +.text-component img { + display: block; + margin: 0 auto +} + +.text-component figcaption { + text-align: center; + margin-top: 0.5em; + margin-top: var(--space-xs) +} + +.text-component em { + font-style: italic +} + +.text-component hr { + margin-top: 2em; + margin-top: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); + margin-bottom: 2em; + margin-bottom: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); + margin-left: auto; + margin-right: auto +} + +.text-component>*:first-child { + margin-top: 0 +} + +.text-component>*:last-child { + margin-bottom: 0 +} + +.text-component__block--full-width { + width: 100vw; + margin-left: calc(50% - 50vw) +} + +@media (min-width: 48rem) { + .text-component__block--left, .text-component__block--right { + width: 45% + } + .text-component__block--left img, .text-component__block--right img { + width: 100% + } + .text-component__block--left { + float: left; + margin-right: 0.75em; + margin-right: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) + } + .text-component__block--right { + float: right; + margin-left: 0.75em; + margin-left: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) + } +} + +@media (min-width: 90rem) { + .text-component__block--outset { + width: calc(100% + 10.5em); + width: calc(100% + 2*var(--space-xxl)) + } + .text-component__block--outset img { + width: 100% + } + .text-component__block--outset:not(.text-component__block--right) { + margin-left: -5.25em; + margin-left: calc(-1*var(--space-xxl)) + } + .text-component__block--left, .text-component__block--right { + width: 50% + } + .text-component__block--right.text-component__block--outset { + margin-right: -5.25em; + margin-right: calc(-1*var(--space-xxl)) + } +} + +:root { + --icon-xxs: 12px; + --icon-xs: 16px; + --icon-sm: 24px; + --icon-md: 32px; + --icon-lg: 48px; + --icon-xl: 64px; + --icon-xxl: 128px +} + +.icon--xxs { + font-size: 12px; + font-size: var(--icon-xxs) +} + +.icon--xs { + font-size: 16px; + font-size: var(--icon-xs) +} + +.icon--sm { + font-size: 24px; + font-size: var(--icon-sm) +} + +.icon--md { + font-size: 32px; + font-size: var(--icon-md) +} + +.icon--lg { + font-size: 48px; + font-size: var(--icon-lg) +} + +.icon--xl { + font-size: 64px; + font-size: var(--icon-xl) +} + +.icon--xxl { + font-size: 128px; + font-size: var(--icon-xxl) +} + +.icon--is-spinning { + -webkit-animation: icon-spin 1s infinite linear; + animation: icon-spin 1s infinite linear +} + +@-webkit-keyframes icon-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg) + } +} + +@keyframes icon-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg) + } +} + +.icon use { + color: inherit; + fill: currentColor +} + +.btn { + position: relative; + display: -ms-inline-flexbox; + display: inline-flex; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center; + white-space: nowrap; + text-decoration: none; + line-height: 1; + font-size: 1em; + font-size: var(--btn-font-size, 1em); + padding-top: 0.5em; + padding-top: var(--btn-padding-y, 0.5em); + padding-bottom: 0.5em; + padding-bottom: var(--btn-padding-y, 0.5em); + padding-left: 0.75em; + padding-left: var(--btn-padding-x, 0.75em); + padding-right: 0.75em; + padding-right: var(--btn-padding-x, 0.75em); + border-radius: 0.25em; + border-radius: var(--btn-radius, 0.25em) +} + +.btn--primary { + background-color: hsl(220, 90%, 56%); + background-color: var(--color-primary, #2a6df4); + color: hsl(0, 0%, 100%); + color: var(--color-white, #fff) +} + +.btn--subtle { + background-color: hsl(240, 1%, 83%); + background-color: var(--color-contrast-low, #d3d3d4); + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21) +} + +.btn--accent { + background-color: hsl(355, 90%, 61%); + background-color: var(--color-accent, #f54251); + color: hsl(0, 0%, 100%); + color: var(--color-white, #fff) +} + +.btn--disabled { + cursor: not-allowed +} + +.btn--sm { + font-size: 0.8em; + font-size: var(--btn-font-size-sm, 0.8em) +} + +.btn--md { + font-size: 1.2em; + font-size: var(--btn-font-size-md, 1.2em) +} + +.btn--lg { + font-size: 1.4em; + font-size: var(--btn-font-size-lg, 1.4em) +} + +.btn--icon { + padding: 0.5em; + padding: var(--btn-padding-y, 0.5em) +} + +.form-control { + background-color: hsl(0, 0%, 100%); + background-color: var(--color-bg, #f2f2f2); + padding-top: 0.5em; + padding-top: var(--form-control-padding-y, 0.5em); + padding-bottom: 0.5em; + padding-bottom: var(--form-control-padding-y, 0.5em); + padding-left: 0.75em; + padding-left: var(--form-control-padding-x, 0.75em); + padding-right: 0.75em; + padding-right: var(--form-control-padding-x, 0.75em); + border-radius: 0.25em; + border-radius: var(--form-control-radius, 0.25em) +} + +.form-control::-webkit-input-placeholder { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control::-moz-placeholder { + opacity: 1; + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control:-ms-input-placeholder { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control:-moz-placeholder { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control[disabled], .form-control[readonly] { + cursor: not-allowed +} + +.form-legend { + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21); + line-height: 1.2; + font-size: 1.2em; + font-size: var(--text-md, 1.2em); + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.form-label { + display: inline-block +} + +.form__msg-error { + background-color: hsl(355, 90%, 61%); + background-color: var(--color-error, #f54251); + color: hsl(0, 0%, 100%); + color: var(--color-white, #fff); + font-size: 0.83333em; + font-size: var(--text-sm, 0.833em); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + padding: 0.5em; + padding: var(--space-xs); + margin-top: 0.75em; + margin-top: var(--space-sm); + border-radius: 0.25em; + border-radius: var(--radius-md, 0.25em); + position: absolute; + clip: rect(1px, 1px, 1px, 1px) +} + +.form__msg-error::before { + content: ''; + position: absolute; + left: 0.75em; + left: var(--space-sm); + top: 0; + -webkit-transform: translateY(-100%); + -ms-transform: translateY(-100%); + transform: translateY(-100%); + width: 0; + height: 0; + border: 8px solid transparent; + border-bottom-color: hsl(355, 90%, 61%); + border-bottom-color: var(--color-error) +} + +.form__msg-error--is-visible { + position: relative; + clip: auto +} + +.radio-list>*, .checkbox-list>* { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-align: baseline; + align-items: baseline; + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.radio-list>*:last-of-type, .checkbox-list>*:last-of-type { + margin-bottom: 0 +} + +.radio-list label, .checkbox-list label { + line-height: 1.4; + line-height: var(--body-line-height); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none +} + +.radio-list input, .checkbox-list input { + vertical-align: top; + margin-right: 0.25em; + margin-right: var(--space-xxxs); + -ms-flex-negative: 0; + flex-shrink: 0 +} + +:root { + --zindex-header: 2; + --zindex-popover: 5; + --zindex-fixed-element: 10; + --zindex-overlay: 15 +} + +@media not all and (min-width: 32rem) { + .display\@xs { + display: none !important + } +} + +@media (min-width: 32rem) { + .hide\@xs { + display: none !important + } +} + +@media not all and (min-width: 48rem) { + .display\@sm { + display: none !important + } +} + +@media (min-width: 48rem) { + .hide\@sm { + display: none !important + } +} + +@media not all and (min-width: 64rem) { + .display\@md { + display: none !important + } +} + +@media (min-width: 64rem) { + .hide\@md { + display: none !important + } +} + +@media not all and (min-width: 80rem) { + .display\@lg { + display: none !important + } +} + +@media (min-width: 80rem) { + .hide\@lg { + display: none !important + } +} + +@media not all and (min-width: 90rem) { + .display\@xl { + display: none !important + } +} + +@media (min-width: 90rem) { + .hide\@xl { + display: none !important + } +} + +:root { + --display: block +} + +.is-visible { + display: block !important; + display: var(--display) !important +} + +.is-hidden { + display: none !important +} + +.sr-only { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + width: 1px; + height: 1px; + overflow: hidden; + padding: 0; + border: 0; + white-space: nowrap +} + +.flex { + display: -ms-flexbox; + display: flex +} + +.inline-flex { + display: -ms-inline-flexbox; + display: inline-flex +} + +.flex-wrap { + -ms-flex-wrap: wrap; + flex-wrap: wrap +} + +.flex-column { + -ms-flex-direction: column; + flex-direction: column +} + +.flex-row { + -ms-flex-direction: row; + flex-direction: row +} + +.flex-center { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center +} + +.justify-start { + -ms-flex-pack: start; + justify-content: flex-start +} + +.justify-end { + -ms-flex-pack: end; + justify-content: flex-end +} + +.justify-center { + -ms-flex-pack: center; + justify-content: center +} + +.justify-between { + -ms-flex-pack: justify; + justify-content: space-between +} + +.items-center { + -ms-flex-align: center; + align-items: center +} + +.items-start { + -ms-flex-align: start; + align-items: flex-start +} + +.items-end { + -ms-flex-align: end; + align-items: flex-end +} + +@media (min-width: 32rem) { + .flex-wrap\@xs { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@xs { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@xs { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@xs { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@xs { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@xs { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@xs { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@xs { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@xs { + -ms-flex-align: center; + align-items: center + } + .items-start\@xs { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@xs { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 48rem) { + .flex-wrap\@sm { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@sm { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@sm { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@sm { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@sm { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@sm { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@sm { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@sm { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@sm { + -ms-flex-align: center; + align-items: center + } + .items-start\@sm { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@sm { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 64rem) { + .flex-wrap\@md { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@md { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@md { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@md { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@md { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@md { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@md { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@md { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@md { + -ms-flex-align: center; + align-items: center + } + .items-start\@md { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@md { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 80rem) { + .flex-wrap\@lg { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@lg { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@lg { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@lg { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@lg { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@lg { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@lg { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@lg { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@lg { + -ms-flex-align: center; + align-items: center + } + .items-start\@lg { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@lg { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 90rem) { + .flex-wrap\@xl { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@xl { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@xl { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@xl { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@xl { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@xl { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@xl { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@xl { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@xl { + -ms-flex-align: center; + align-items: center + } + .items-start\@xl { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@xl { + -ms-flex-align: end; + align-items: flex-end + } +} + +.flex-grow { + -ms-flex-positive: 1; + flex-grow: 1 +} + +.flex-shrink-0 { + -ms-flex-negative: 0; + flex-shrink: 0 +} + +.flex-gap-xxxs { + margin-bottom: -0.25em; + margin-bottom: calc(-1*var(--space-xxxs)); + margin-right: -0.25em; + margin-right: calc(-1*var(--space-xxxs)) +} + +.flex-gap-xxxs>* { + margin-bottom: 0.25em; + margin-bottom: var(--space-xxxs); + margin-right: 0.25em; + margin-right: var(--space-xxxs) +} + +.flex-gap-xxs { + margin-bottom: -0.375em; + margin-bottom: calc(-1*var(--space-xxs)); + margin-right: -0.375em; + margin-right: calc(-1*var(--space-xxs)) +} + +.flex-gap-xxs>* { + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs); + margin-right: 0.375em; + margin-right: var(--space-xxs) +} + +.flex-gap-xs { + margin-bottom: -0.5em; + margin-bottom: calc(-1*var(--space-xs)); + margin-right: -0.5em; + margin-right: calc(-1*var(--space-xs)) +} + +.flex-gap-xs>* { + margin-bottom: 0.5em; + margin-bottom: var(--space-xs); + margin-right: 0.5em; + margin-right: var(--space-xs) +} + +.flex-gap-sm { + margin-bottom: -0.75em; + margin-bottom: calc(-1*var(--space-sm)); + margin-right: -0.75em; + margin-right: calc(-1*var(--space-sm)) +} + +.flex-gap-sm>* { + margin-bottom: 0.75em; + margin-bottom: var(--space-sm); + margin-right: 0.75em; + margin-right: var(--space-sm) +} + +.flex-gap-md { + margin-bottom: -1.25em; + margin-bottom: calc(-1*var(--space-md)); + margin-right: -1.25em; + margin-right: calc(-1*var(--space-md)) +} + +.flex-gap-md>* { + margin-bottom: 1.25em; + margin-bottom: var(--space-md); + margin-right: 1.25em; + margin-right: var(--space-md) +} + +.flex-gap-lg { + margin-bottom: -2em; + margin-bottom: calc(-1*var(--space-lg)); + margin-right: -2em; + margin-right: calc(-1*var(--space-lg)) +} + +.flex-gap-lg>* { + margin-bottom: 2em; + margin-bottom: var(--space-lg); + margin-right: 2em; + margin-right: var(--space-lg) +} + +.flex-gap-xl { + margin-bottom: -3.25em; + margin-bottom: calc(-1*var(--space-xl)); + margin-right: -3.25em; + margin-right: calc(-1*var(--space-xl)) +} + +.flex-gap-xl>* { + margin-bottom: 3.25em; + margin-bottom: var(--space-xl); + margin-right: 3.25em; + margin-right: var(--space-xl) +} + +.flex-gap-xxl { + margin-bottom: -5.25em; + margin-bottom: calc(-1*var(--space-xxl)); + margin-right: -5.25em; + margin-right: calc(-1*var(--space-xxl)) +} + +.flex-gap-xxl>* { + margin-bottom: 5.25em; + margin-bottom: var(--space-xxl); + margin-right: 5.25em; + margin-right: var(--space-xxl) +} + +.margin-xxxxs { + margin: 0.125em; + margin: var(--space-xxxxs) +} + +.margin-xxxs { + margin: 0.25em; + margin: var(--space-xxxs) +} + +.margin-xxs { + margin: 0.375em; + margin: var(--space-xxs) +} + +.margin-xs { + margin: 0.5em; + margin: var(--space-xs) +} + +.margin-sm { + margin: 0.75em; + margin: var(--space-sm) +} + +.margin-md { + margin: 1.25em; + margin: var(--space-md) +} + +.margin-lg { + margin: 2em; + margin: var(--space-lg) +} + +.margin-xl { + margin: 3.25em; + margin: var(--space-xl) +} + +.margin-xxl { + margin: 5.25em; + margin: var(--space-xxl) +} + +.margin-xxxl { + margin: 8.5em; + margin: var(--space-xxxl) +} + +.margin-xxxxl { + margin: 13.75em; + margin: var(--space-xxxxl) +} + +.margin-auto { + margin: auto +} + +.margin-top-xxxxs { + margin-top: 0.125em; + margin-top: var(--space-xxxxs) +} + +.margin-top-xxxs { + margin-top: 0.25em; + margin-top: var(--space-xxxs) +} + +.margin-top-xxs { + margin-top: 0.375em; + margin-top: var(--space-xxs) +} + +.margin-top-xs { + margin-top: 0.5em; + margin-top: var(--space-xs) +} + +.margin-top-sm { + margin-top: 0.75em; + margin-top: var(--space-sm) +} + +.margin-top-md { + margin-top: 1.25em; + margin-top: var(--space-md) +} + +.margin-top-lg { + margin-top: 2em; + margin-top: var(--space-lg) +} + +.margin-top-xl { + margin-top: 3.25em; + margin-top: var(--space-xl) +} + +.margin-top-xxl { + margin-top: 5.25em; + margin-top: var(--space-xxl) +} + +.margin-top-xxxl { + margin-top: 8.5em; + margin-top: var(--space-xxxl) +} + +.margin-top-xxxxl { + margin-top: 13.75em; + margin-top: var(--space-xxxxl) +} + +.margin-top-auto { + margin-top: auto +} + +.margin-bottom-xxxxs { + margin-bottom: 0.125em; + margin-bottom: var(--space-xxxxs) +} + +.margin-bottom-xxxs { + margin-bottom: 0.25em; + margin-bottom: var(--space-xxxs) +} + +.margin-bottom-xxs { + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.margin-bottom-xs { + margin-bottom: 0.5em; + margin-bottom: var(--space-xs) +} + +.margin-bottom-sm { + margin-bottom: 0.75em; + margin-bottom: var(--space-sm) +} + +.margin-bottom-md { + margin-bottom: 1.25em; + margin-bottom: var(--space-md) +} + +.margin-bottom-lg { + margin-bottom: 2em; + margin-bottom: var(--space-lg) +} + +.margin-bottom-xl { + margin-bottom: 3.25em; + margin-bottom: var(--space-xl) +} + +.margin-bottom-xxl { + margin-bottom: 5.25em; + margin-bottom: var(--space-xxl) +} + +.margin-bottom-xxxl { + margin-bottom: 8.5em; + margin-bottom: var(--space-xxxl) +} + +.margin-bottom-xxxxl { + margin-bottom: 13.75em; + margin-bottom: var(--space-xxxxl) +} + +.margin-bottom-auto { + margin-bottom: auto +} + +.margin-right-xxxxs { + margin-right: 0.125em; + margin-right: var(--space-xxxxs) +} + +.margin-right-xxxs { + margin-right: 0.25em; + margin-right: var(--space-xxxs) +} + +.margin-right-xxs { + margin-right: 0.375em; + margin-right: var(--space-xxs) +} + +.margin-right-xs { + margin-right: 0.5em; + margin-right: var(--space-xs) +} + +.margin-right-sm { + margin-right: 0.75em; + margin-right: var(--space-sm) +} + +.margin-right-md { + margin-right: 1.25em; + margin-right: var(--space-md) +} + +.margin-right-lg { + margin-right: 2em; + margin-right: var(--space-lg) +} + +.margin-right-xl { + margin-right: 3.25em; + margin-right: var(--space-xl) +} + +.margin-right-xxl { + margin-right: 5.25em; + margin-right: var(--space-xxl) +} + +.margin-right-xxxl { + margin-right: 8.5em; + margin-right: var(--space-xxxl) +} + +.margin-right-xxxxl { + margin-right: 13.75em; + margin-right: var(--space-xxxxl) +} + +.margin-right-auto { + margin-right: auto +} + +.margin-left-xxxxs { + margin-left: 0.125em; + margin-left: var(--space-xxxxs) +} + +.margin-left-xxxs { + margin-left: 0.25em; + margin-left: var(--space-xxxs) +} + +.margin-left-xxs { + margin-left: 0.375em; + margin-left: var(--space-xxs) +} + +.margin-left-xs { + margin-left: 0.5em; + margin-left: var(--space-xs) +} + +.margin-left-sm { + margin-left: 0.75em; + margin-left: var(--space-sm) +} + +.margin-left-md { + margin-left: 1.25em; + margin-left: var(--space-md) +} + +.margin-left-lg { + margin-left: 2em; + margin-left: var(--space-lg) +} + +.margin-left-xl { + margin-left: 3.25em; + margin-left: var(--space-xl) +} + +.margin-left-xxl { + margin-left: 5.25em; + margin-left: var(--space-xxl) +} + +.margin-left-xxxl { + margin-left: 8.5em; + margin-left: var(--space-xxxl) +} + +.margin-left-xxxxl { + margin-left: 13.75em; + margin-left: var(--space-xxxxl) +} + +.margin-left-auto { + margin-left: auto +} + +.margin-x-xxxxs { + margin-left: 0.125em; + margin-left: var(--space-xxxxs); + margin-right: 0.125em; + margin-right: var(--space-xxxxs) +} + +.margin-x-xxxs { + margin-left: 0.25em; + margin-left: var(--space-xxxs); + margin-right: 0.25em; + margin-right: var(--space-xxxs) +} + +.margin-x-xxs { + margin-left: 0.375em; + margin-left: var(--space-xxs); + margin-right: 0.375em; + margin-right: var(--space-xxs) +} + +.margin-x-xs { + margin-left: 0.5em; + margin-left: var(--space-xs); + margin-right: 0.5em; + margin-right: var(--space-xs) +} + +.margin-x-sm { + margin-left: 0.75em; + margin-left: var(--space-sm); + margin-right: 0.75em; + margin-right: var(--space-sm) +} + +.margin-x-md { + margin-left: 1.25em; + margin-left: var(--space-md); + margin-right: 1.25em; + margin-right: var(--space-md) +} + +.margin-x-lg { + margin-left: 2em; + margin-left: var(--space-lg); + margin-right: 2em; + margin-right: var(--space-lg) +} + +.margin-x-xl { + margin-left: 3.25em; + margin-left: var(--space-xl); + margin-right: 3.25em; + margin-right: var(--space-xl) +} + +.margin-x-xxl { + margin-left: 5.25em; + margin-left: var(--space-xxl); + margin-right: 5.25em; + margin-right: var(--space-xxl) +} + +.margin-x-xxxl { + margin-left: 8.5em; + margin-left: var(--space-xxxl); + margin-right: 8.5em; + margin-right: var(--space-xxxl) +} + +.margin-x-xxxxl { + margin-left: 13.75em; + margin-left: var(--space-xxxxl); + margin-right: 13.75em; + margin-right: var(--space-xxxxl) +} + +.margin-x-auto { + margin-left: auto; + margin-right: auto +} + +.margin-y-xxxxs { + margin-top: 0.125em; + margin-top: var(--space-xxxxs); + margin-bottom: 0.125em; + margin-bottom: var(--space-xxxxs) +} + +.margin-y-xxxs { + margin-top: 0.25em; + margin-top: var(--space-xxxs); + margin-bottom: 0.25em; + margin-bottom: var(--space-xxxs) +} + +.margin-y-xxs { + margin-top: 0.375em; + margin-top: var(--space-xxs); + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.margin-y-xs { + margin-top: 0.5em; + margin-top: var(--space-xs); + margin-bottom: 0.5em; + margin-bottom: var(--space-xs) +} + +.margin-y-sm { + margin-top: 0.75em; + margin-top: var(--space-sm); + margin-bottom: 0.75em; + margin-bottom: var(--space-sm) +} + +.margin-y-md { + margin-top: 1.25em; + margin-top: var(--space-md); + margin-bottom: 1.25em; + margin-bottom: var(--space-md) +} + +.margin-y-lg { + margin-top: 2em; + margin-top: var(--space-lg); + margin-bottom: 2em; + margin-bottom: var(--space-lg) +} + +.margin-y-xl { + margin-top: 3.25em; + margin-top: var(--space-xl); + margin-bottom: 3.25em; + margin-bottom: var(--space-xl) +} + +.margin-y-xxl { + margin-top: 5.25em; + margin-top: var(--space-xxl); + margin-bottom: 5.25em; + margin-bottom: var(--space-xxl) +} + +.margin-y-xxxl { + margin-top: 8.5em; + margin-top: var(--space-xxxl); + margin-bottom: 8.5em; + margin-bottom: var(--space-xxxl) +} + +.margin-y-xxxxl { + margin-top: 13.75em; + margin-top: var(--space-xxxxl); + margin-bottom: 13.75em; + margin-bottom: var(--space-xxxxl) +} + +.margin-y-auto { + margin-top: auto; + margin-bottom: auto +} + +@media not all and (min-width: 32rem) { + .has-margin\@xs { + margin: 0 !important + } +} + +@media not all and (min-width: 48rem) { + .has-margin\@sm { + margin: 0 !important + } +} + +@media not all and (min-width: 64rem) { + .has-margin\@md { + margin: 0 !important + } +} + +@media not all and (min-width: 80rem) { + .has-margin\@lg { + margin: 0 !important + } +} + +@media not all and (min-width: 90rem) { + .has-margin\@xl { + margin: 0 !important + } +} + +.padding-md { + padding: 1.25em; + padding: var(--space-md) +} + +.padding-xxxxs { + padding: 0.125em; + padding: var(--space-xxxxs) +} + +.padding-xxxs { + padding: 0.25em; + padding: var(--space-xxxs) +} + +.padding-xxs { + padding: 0.375em; + padding: var(--space-xxs) +} + +.padding-xs { + padding: 0.5em; + padding: var(--space-xs) +} + +.padding-sm { + padding: 0.75em; + padding: var(--space-sm) +} + +.padding-lg { + padding: 2em; + padding: var(--space-lg) +} + +.padding-xl { + padding: 3.25em; + padding: var(--space-xl) +} + +.padding-xxl { + padding: 5.25em; + padding: var(--space-xxl) +} + +.padding-xxxl { + padding: 8.5em; + padding: var(--space-xxxl) +} + +.padding-xxxxl { + padding: 13.75em; + padding: var(--space-xxxxl) +} + +.padding-component { + padding: 1.25em; + padding: var(--component-padding) +} + +.padding-top-md { + padding-top: 1.25em; + padding-top: var(--space-md) +} + +.padding-top-xxxxs { + padding-top: 0.125em; + padding-top: var(--space-xxxxs) +} + +.padding-top-xxxs { + padding-top: 0.25em; + padding-top: var(--space-xxxs) +} + +.padding-top-xxs { + padding-top: 0.375em; + padding-top: var(--space-xxs) +} + +.padding-top-xs { + padding-top: 0.5em; + padding-top: var(--space-xs) +} + +.padding-top-sm { + padding-top: 0.75em; + padding-top: var(--space-sm) +} + +.padding-top-lg { + padding-top: 2em; + padding-top: var(--space-lg) +} + +.padding-top-xl { + padding-top: 3.25em; + padding-top: var(--space-xl) +} + +.padding-top-xxl { + padding-top: 5.25em; + padding-top: var(--space-xxl) +} + +.padding-top-xxxl { + padding-top: 8.5em; + padding-top: var(--space-xxxl) +} + +.padding-top-xxxxl { + padding-top: 13.75em; + padding-top: var(--space-xxxxl) +} + +.padding-top-component { + padding-top: 1.25em; + padding-top: var(--component-padding) +} + +.padding-bottom-md { + padding-bottom: 1.25em; + padding-bottom: var(--space-md) +} + +.padding-bottom-xxxxs { + padding-bottom: 0.125em; + padding-bottom: var(--space-xxxxs) +} + +.padding-bottom-xxxs { + padding-bottom: 0.25em; + padding-bottom: var(--space-xxxs) +} + +.padding-bottom-xxs { + padding-bottom: 0.375em; + padding-bottom: var(--space-xxs) +} + +.padding-bottom-xs { + padding-bottom: 0.5em; + padding-bottom: var(--space-xs) +} + +.padding-bottom-sm { + padding-bottom: 0.75em; + padding-bottom: var(--space-sm) +} + +.padding-bottom-lg { + padding-bottom: 2em; + padding-bottom: var(--space-lg) +} + +.padding-bottom-xl { + padding-bottom: 3.25em; + padding-bottom: var(--space-xl) +} + +.padding-bottom-xxl { + padding-bottom: 5.25em; + padding-bottom: var(--space-xxl) +} + +.padding-bottom-xxxl { + padding-bottom: 8.5em; + padding-bottom: var(--space-xxxl) +} + +.padding-bottom-xxxxl { + padding-bottom: 13.75em; + padding-bottom: var(--space-xxxxl) +} + +.padding-bottom-component { + padding-bottom: 1.25em; + padding-bottom: var(--component-padding) +} + +.padding-right-md { + padding-right: 1.25em; + padding-right: var(--space-md) +} + +.padding-right-xxxxs { + padding-right: 0.125em; + padding-right: var(--space-xxxxs) +} + +.padding-right-xxxs { + padding-right: 0.25em; + padding-right: var(--space-xxxs) +} + +.padding-right-xxs { + padding-right: 0.375em; + padding-right: var(--space-xxs) +} + +.padding-right-xs { + padding-right: 0.5em; + padding-right: var(--space-xs) +} + +.padding-right-sm { + padding-right: 0.75em; + padding-right: var(--space-sm) +} + +.padding-right-lg { + padding-right: 2em; + padding-right: var(--space-lg) +} + +.padding-right-xl { + padding-right: 3.25em; + padding-right: var(--space-xl) +} + +.padding-right-xxl { + padding-right: 5.25em; + padding-right: var(--space-xxl) +} + +.padding-right-xxxl { + padding-right: 8.5em; + padding-right: var(--space-xxxl) +} + +.padding-right-xxxxl { + padding-right: 13.75em; + padding-right: var(--space-xxxxl) +} + +.padding-right-component { + padding-right: 1.25em; + padding-right: var(--component-padding) +} + +.padding-left-md { + padding-left: 1.25em; + padding-left: var(--space-md) +} + +.padding-left-xxxxs { + padding-left: 0.125em; + padding-left: var(--space-xxxxs) +} + +.padding-left-xxxs { + padding-left: 0.25em; + padding-left: var(--space-xxxs) +} + +.padding-left-xxs { + padding-left: 0.375em; + padding-left: var(--space-xxs) +} + +.padding-left-xs { + padding-left: 0.5em; + padding-left: var(--space-xs) +} + +.padding-left-sm { + padding-left: 0.75em; + padding-left: var(--space-sm) +} + +.padding-left-lg { + padding-left: 2em; + padding-left: var(--space-lg) +} + +.padding-left-xl { + padding-left: 3.25em; + padding-left: var(--space-xl) +} + +.padding-left-xxl { + padding-left: 5.25em; + padding-left: var(--space-xxl) +} + +.padding-left-xxxl { + padding-left: 8.5em; + padding-left: var(--space-xxxl) +} + +.padding-left-xxxxl { + padding-left: 13.75em; + padding-left: var(--space-xxxxl) +} + +.padding-left-component { + padding-left: 1.25em; + padding-left: var(--component-padding) +} + +.padding-x-md { + padding-left: 1.25em; + padding-left: var(--space-md); + padding-right: 1.25em; + padding-right: var(--space-md) +} + +.padding-x-xxxxs { + padding-left: 0.125em; + padding-left: var(--space-xxxxs); + padding-right: 0.125em; + padding-right: var(--space-xxxxs) +} + +.padding-x-xxxs { + padding-left: 0.25em; + padding-left: var(--space-xxxs); + padding-right: 0.25em; + padding-right: var(--space-xxxs) +} + +.padding-x-xxs { + padding-left: 0.375em; + padding-left: var(--space-xxs); + padding-right: 0.375em; + padding-right: var(--space-xxs) +} + +.padding-x-xs { + padding-left: 0.5em; + padding-left: var(--space-xs); + padding-right: 0.5em; + padding-right: var(--space-xs) +} + +.padding-x-sm { + padding-left: 0.75em; + padding-left: var(--space-sm); + padding-right: 0.75em; + padding-right: var(--space-sm) +} + +.padding-x-lg { + padding-left: 2em; + padding-left: var(--space-lg); + padding-right: 2em; + padding-right: var(--space-lg) +} + +.padding-x-xl { + padding-left: 3.25em; + padding-left: var(--space-xl); + padding-right: 3.25em; + padding-right: var(--space-xl) +} + +.padding-x-xxl { + padding-left: 5.25em; + padding-left: var(--space-xxl); + padding-right: 5.25em; + padding-right: var(--space-xxl) +} + +.padding-x-xxxl { + padding-left: 8.5em; + padding-left: var(--space-xxxl); + padding-right: 8.5em; + padding-right: var(--space-xxxl) +} + +.padding-x-xxxxl { + padding-left: 13.75em; + padding-left: var(--space-xxxxl); + padding-right: 13.75em; + padding-right: var(--space-xxxxl) +} + +.padding-x-component { + padding-left: 1.25em; + padding-left: var(--component-padding); + padding-right: 1.25em; + padding-right: var(--component-padding) +} + +.padding-y-md { + padding-top: 1.25em; + padding-top: var(--space-md); + padding-bottom: 1.25em; + padding-bottom: var(--space-md) +} + +.padding-y-xxxxs { + padding-top: 0.125em; + padding-top: var(--space-xxxxs); + padding-bottom: 0.125em; + padding-bottom: var(--space-xxxxs) +} + +.padding-y-xxxs { + padding-top: 0.25em; + padding-top: var(--space-xxxs); + padding-bottom: 0.25em; + padding-bottom: var(--space-xxxs) +} + +.padding-y-xxs { + padding-top: 0.375em; + padding-top: var(--space-xxs); + padding-bottom: 0.375em; + padding-bottom: var(--space-xxs) +} + +.padding-y-xs { + padding-top: 0.5em; + padding-top: var(--space-xs); + padding-bottom: 0.5em; + padding-bottom: var(--space-xs) +} + +.padding-y-sm { + padding-top: 0.75em; + padding-top: var(--space-sm); + padding-bottom: 0.75em; + padding-bottom: var(--space-sm) +} + +.padding-y-lg { + padding-top: 2em; + padding-top: var(--space-lg); + padding-bottom: 2em; + padding-bottom: var(--space-lg) +} + +.padding-y-xl { + padding-top: 3.25em; + padding-top: var(--space-xl); + padding-bottom: 3.25em; + padding-bottom: var(--space-xl) +} + +.padding-y-xxl { + padding-top: 5.25em; + padding-top: var(--space-xxl); + padding-bottom: 5.25em; + padding-bottom: var(--space-xxl) +} + +.padding-y-xxxl { + padding-top: 8.5em; + padding-top: var(--space-xxxl); + padding-bottom: 8.5em; + padding-bottom: var(--space-xxxl) +} + +.padding-y-xxxxl { + padding-top: 13.75em; + padding-top: var(--space-xxxxl); + padding-bottom: 13.75em; + padding-bottom: var(--space-xxxxl) +} + +.padding-y-component { + padding-top: 1.25em; + padding-top: var(--component-padding); + padding-bottom: 1.25em; + padding-bottom: var(--component-padding) +} + +@media not all and (min-width: 32rem) { + .has-padding\@xs { + padding: 0 !important + } +} + +@media not all and (min-width: 48rem) { + .has-padding\@sm { + padding: 0 !important + } +} + +@media not all and (min-width: 64rem) { + .has-padding\@md { + padding: 0 !important + } +} + +@media not all and (min-width: 80rem) { + .has-padding\@lg { + padding: 0 !important + } +} + +@media not all and (min-width: 90rem) { + .has-padding\@xl { + padding: 0 !important + } +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap +} + +.text-replace { + overflow: hidden; + color: transparent; + text-indent: 100%; + white-space: nowrap +} + +.text-center { + text-align: center +} + +.text-left { + text-align: left +} + +.text-right { + text-align: right +} + +@media (min-width: 32rem) { + .text-center\@xs { + text-align: center + } + .text-left\@xs { + text-align: left + } + .text-right\@xs { + text-align: right + } +} + +@media (min-width: 48rem) { + .text-center\@sm { + text-align: center + } + .text-left\@sm { + text-align: left + } + .text-right\@sm { + text-align: right + } +} + +@media (min-width: 64rem) { + .text-center\@md { + text-align: center + } + .text-left\@md { + text-align: left + } + .text-right\@md { + text-align: right + } +} + +@media (min-width: 80rem) { + .text-center\@lg { + text-align: center + } + .text-left\@lg { + text-align: left + } + .text-right\@lg { + text-align: right + } +} + +@media (min-width: 90rem) { + .text-center\@xl { + text-align: center + } + .text-left\@xl { + text-align: left + } + .text-right\@xl { + text-align: right + } +} + +.color-inherit { + color: inherit +} + +.color-contrast-medium { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.color-contrast-high { + color: hsl(240, 4%, 20%); + color: var(--color-contrast-high, #313135) +} + +.color-contrast-higher { + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21) +} + +.color-primary { + color: hsl(220, 90%, 56%); + color: var(--color-primary, #2a6df4) +} + +.color-accent { + color: hsl(355, 90%, 61%); + color: var(--color-accent, #f54251) +} + +.color-success { + color: hsl(94, 48%, 56%); + color: var(--color-success, #88c559) +} + +.color-warning { + color: hsl(46, 100%, 61%); + color: var(--color-warning, #ffd138) +} + +.color-error { + color: hsl(355, 90%, 61%); + color: var(--color-error, #f54251) +} + +.width-100\% { + width: 100% +} + +.height-100\% { + height: 100% +} + +.media-wrapper { + position: relative; + height: 0; + padding-bottom: 56.25% +} + +.media-wrapper iframe, .media-wrapper video, .media-wrapper img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100% +} + +.media-wrapper video, .media-wrapper img { + -o-object-fit: cover; + object-fit: cover +} + +.media-wrapper--4\:3 { + padding-bottom: 75% +} + +:root, [data-theme="default"] { + --color-primary-darker: hsl(220, 90%, 36%); + --color-primary-darker-h: 220; + --color-primary-darker-s: 90%; + --color-primary-darker-l: 36%; + --color-primary-dark: hsl(220, 90%, 46%); + --color-primary-dark-h: 220; + --color-primary-dark-s: 90%; + --color-primary-dark-l: 46%; + --color-primary: hsl(220, 90%, 56%); + --color-primary-h: 220; + --color-primary-s: 90%; + --color-primary-l: 56%; + --color-primary-light: hsl(220, 90%, 66%); + --color-primary-light-h: 220; + --color-primary-light-s: 90%; + --color-primary-light-l: 66%; + --color-primary-lighter: hsl(220, 90%, 76%); + --color-primary-lighter-h: 220; + --color-primary-lighter-s: 90%; + --color-primary-lighter-l: 76%; + --color-accent-darker: hsl(355, 90%, 41%); + --color-accent-darker-h: 355; + --color-accent-darker-s: 90%; + --color-accent-darker-l: 41%; + --color-accent-dark: hsl(355, 90%, 51%); + --color-accent-dark-h: 355; + --color-accent-dark-s: 90%; + --color-accent-dark-l: 51%; + --color-accent: hsl(355, 90%, 61%); + --color-accent-h: 355; + --color-accent-s: 90%; + --color-accent-l: 61%; + --color-accent-light: hsl(355, 90%, 71%); + --color-accent-light-h: 355; + --color-accent-light-s: 90%; + --color-accent-light-l: 71%; + --color-accent-lighter: hsl(355, 90%, 81%); + --color-accent-lighter-h: 355; + --color-accent-lighter-s: 90%; + --color-accent-lighter-l: 81%; + --color-black: hsl(240, 8%, 12%); + --color-black-h: 240; + --color-black-s: 8%; + --color-black-l: 12%; + --color-white: hsl(0, 0%, 100%); + --color-white-h: 0; + --color-white-s: 0%; + --color-white-l: 100%; + --color-success-darker: hsl(94, 48%, 36%); + --color-success-darker-h: 94; + --color-success-darker-s: 48%; + --color-success-darker-l: 36%; + --color-success-dark: hsl(94, 48%, 46%); + --color-success-dark-h: 94; + --color-success-dark-s: 48%; + --color-success-dark-l: 46%; + --color-success: hsl(94, 48%, 56%); + --color-success-h: 94; + --color-success-s: 48%; + --color-success-l: 56%; + --color-success-light: hsl(94, 48%, 66%); + --color-success-light-h: 94; + --color-success-light-s: 48%; + --color-success-light-l: 66%; + --color-success-lighter: hsl(94, 48%, 76%); + --color-success-lighter-h: 94; + --color-success-lighter-s: 48%; + --color-success-lighter-l: 76%; + --color-error-darker: hsl(355, 90%, 41%); + --color-error-darker-h: 355; + --color-error-darker-s: 90%; + --color-error-darker-l: 41%; + --color-error-dark: hsl(355, 90%, 51%); + --color-error-dark-h: 355; + --color-error-dark-s: 90%; + --color-error-dark-l: 51%; + --color-error: hsl(355, 90%, 61%); + --color-error-h: 355; + --color-error-s: 90%; + --color-error-l: 61%; + --color-error-light: hsl(355, 90%, 71%); + --color-error-light-h: 355; + --color-error-light-s: 90%; + --color-error-light-l: 71%; + --color-error-lighter: hsl(355, 90%, 81%); + --color-error-lighter-h: 355; + --color-error-lighter-s: 90%; + --color-error-lighter-l: 81%; + --color-warning-darker: hsl(46, 100%, 41%); + --color-warning-darker-h: 46; + --color-warning-darker-s: 100%; + --color-warning-darker-l: 41%; + --color-warning-dark: hsl(46, 100%, 51%); + --color-warning-dark-h: 46; + --color-warning-dark-s: 100%; + --color-warning-dark-l: 51%; + --color-warning: hsl(46, 100%, 61%); + --color-warning-h: 46; + --color-warning-s: 100%; + --color-warning-l: 61%; + --color-warning-light: hsl(46, 100%, 71%); + --color-warning-light-h: 46; + --color-warning-light-s: 100%; + --color-warning-light-l: 71%; + --color-warning-lighter: hsl(46, 100%, 81%); + --color-warning-lighter-h: 46; + --color-warning-lighter-s: 100%; + --color-warning-lighter-l: 81%; + --color-bg: hsl(0, 0%, 100%); + --color-bg-h: 0; + --color-bg-s: 0%; + --color-bg-l: 100%; + --color-contrast-lower: hsl(0, 0%, 95%); + --color-contrast-lower-h: 0; + --color-contrast-lower-s: 0%; + --color-contrast-lower-l: 95%; + --color-contrast-low: hsl(240, 1%, 83%); + --color-contrast-low-h: 240; + --color-contrast-low-s: 1%; + --color-contrast-low-l: 83%; + --color-contrast-medium: hsl(240, 1%, 48%); + --color-contrast-medium-h: 240; + --color-contrast-medium-s: 1%; + --color-contrast-medium-l: 48%; + --color-contrast-high: hsl(240, 4%, 20%); + --color-contrast-high-h: 240; + --color-contrast-high-s: 4%; + --color-contrast-high-l: 20%; + --color-contrast-higher: hsl(240, 8%, 12%); + --color-contrast-higher-h: 240; + --color-contrast-higher-s: 8%; + --color-contrast-higher-l: 12% +} + +@supports (--css: variables) { + @media (min-width: 64rem) { + :root { + --space-unit: 1.25em + } + } +} + +:root { + --radius: 0.25em +} + +:root { + --font-primary: sans-serif; + --text-base-size: 1em; + --text-scale-ratio: 1.2; + --text-xs: calc(1em/var(--text-scale-ratio)/var(--text-scale-ratio)); + --text-sm: calc(var(--text-xs)*var(--text-scale-ratio)); + --text-md: calc(var(--text-sm)*var(--text-scale-ratio)*var(--text-scale-ratio)); + --text-lg: calc(var(--text-md)*var(--text-scale-ratio)); + --text-xl: calc(var(--text-lg)*var(--text-scale-ratio)); + --text-xxl: calc(var(--text-xl)*var(--text-scale-ratio)); + --text-xxxl: calc(var(--text-xxl)*var(--text-scale-ratio)); + --body-line-height: 1.4; + --heading-line-height: 1.2; + --font-primary-capital-letter: 1 +} + +@supports (--css: variables) { + @media (min-width: 64rem) { + :root { + --text-base-size: 1.25em; + --text-scale-ratio: 1.25 + } + } +} + +mark { + background-color: hsla(355, 90%, 61%, 0.2); + background-color: hsla(var(--color-accent-h), var(--color-accent-s), var(--color-accent-l), 0.2); + color: inherit +} + +.text-component { + --line-height-multiplier: 1; + --text-vspace-multiplier: 1 +} + +.text-component blockquote { + padding-left: 1em; + border-left: 4px solid hsl(240, 1%, 83%); + border-left: 4px solid var(--color-contrast-low) +} + +.text-component hr { + background: hsl(240, 1%, 83%); + background: var(--color-contrast-low); + height: 1px +} + +.text-component figcaption { + font-size: 0.83333em; + font-size: var(--text-sm); + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium) +} + +.article.text-component { + --line-height-multiplier: 1.13; + --text-vspace-multiplier: 1.2 +} + +:root { + --btn-font-size: 1em; + --btn-font-size-sm: calc(var(--btn-font-size) - 0.2em); + --btn-font-size-md: calc(var(--btn-font-size) + 0.2em); + --btn-font-size-lg: calc(var(--btn-font-size) + 0.4em); + --btn-radius: 0.25em; + --btn-padding-x: var(--space-sm); + --btn-padding-y: var(--space-xs) +} + +.btn { + --color-shadow: hsla(240, 8%, 12%, 0.15); + --color-shadow: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); + box-shadow: 0 4px 16px hsla(240, 8%, 12%, 0.15); + box-shadow: 0 4px 16px hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); + cursor: pointer +} + +.btn--primary { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.btn--accent { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.btn--disabled { + opacity: 0.6 +} + +:root { + --form-control-padding-x: var(--space-sm); + --form-control-padding-y: var(--space-xs); + --form-control-radius: 0.25em +} + +.form-control { + border: 2px solid hsl(240, 1%, 83%); + border: 2px solid var(--color-contrast-low) +} + +.form-control:focus { + outline: none; + border-color: hsl(220, 90%, 56%); + border-color: var(--color-primary); + --color-shadow: hsla(220, 90%, 56%, 0.2); + --color-shadow: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.2); + box-shadow: undefined; + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-control:focus:focus { + box-shadow: 0 0 0 3px hsla(220, 90%, 56%, 0.2); + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-control[aria-invalid="true"] { + border-color: hsl(355, 90%, 61%); + border-color: var(--color-error) +} + +.form-control[aria-invalid="true"]:focus { + --color-shadow: hsla(355, 90%, 61%, 0.2); + --color-shadow: hsla(var(--color-error-h), var(--color-error-s), var(--color-error-l), 0.2); + box-shadow: undefined; + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-control[aria-invalid="true"]:focus:focus { + box-shadow: 0 0 0 3px hsla(355, 90%, 61%, 0.2); + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-label { + font-size: 0.83333em; + font-size: var(--text-sm) +} + +:root { + --cd-color-1: hsl(206, 21%, 24%); + --cd-color-1-h: 206; + --cd-color-1-s: 21%; + --cd-color-1-l: 24%; + --cd-color-2: hsl(205, 38%, 89%); + --cd-color-2-h: 205; + --cd-color-2-s: 38%; + --cd-color-2-l: 89%; + --cd-color-3: hsl(207, 10%, 55%); + --cd-color-3-h: 207; + --cd-color-3-s: 10%; + --cd-color-3-l: 55%; + --cd-color-4: hsl(111, 51%, 60%); + --cd-color-4-h: 111; + --cd-color-4-s: 51%; + --cd-color-4-l: 60%; + --cd-color-5: hsl(356, 53%, 49%); + --cd-color-5-h: 356; + --cd-color-5-s: 53%; + --cd-color-5-l: 49%; + --cd-color-6: hsl(47, 85%, 61%); + --cd-color-6-h: 47; + --cd-color-6-s: 85%; + --cd-color-6-l: 61%; + --cd-header-height: 200px; + --font-primary: 'Droid Serif', serif; + --font-secondary: 'Open Sans', sans-serif +} + +@supports (--css: variables) { + @media (min-width: 64rem) { + :root { + --cd-header-height: 300px + } + } +} + +.cd-main-header { + height: 200px; + height: var(--cd-header-height); + background: hsl(206, 21%, 24%); + background: var(--cd-color-1); + color: hsl(0, 0%, 100%); + color: var(--color-white); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.cd-main-header h1 { + color: inherit +} + +.cd-timeline { + overflow: hidden; + padding: 2em 0; + padding: var(--space-lg) 0; + color: hsl(207, 10%, 55%); + color: var(--cd-color-3); + background-color: hsl(205, 38%, 93.45%); + background-color: hsl(var(--cd-color-2-h), var(--cd-color-2-s), calc(var(--cd-color-2-l)*1.05)); +} + +.cd-timeline h2 { + font-weight: 700 +} + +.cd-timeline__container { + position: relative; + padding: 1.25em 0; + padding: var(--space-md) 0 +} + +.cd-timeline__container::before { + content: ''; + position: absolute; + top: 0; + left: 18px; + height: 100%; + width: 4px; + background: hsl(205, 38%, 89%); + background: var(--cd-color-2) +} + +@media (min-width: 64rem) { + .cd-timeline__container::before { + left: 50%; + -webkit-transform: translateX(-50%); + -ms-transform: translateX(-50%); + transform: translateX(-50%) + } +} + +.cd-timeline__block { + display: -ms-flexbox; + display: flex; + position: relative; + z-index: 1; + margin-bottom: 2em; + margin-bottom: var(--space-lg) +} + +.cd-timeline__block:last-child { + margin-bottom: 0 +} + +@media (min-width: 64rem) { + .cd-timeline__block:nth-child(even) { + -ms-flex-direction: row-reverse; + flex-direction: row-reverse + } +} + +.cd-timeline__img { + display: -ms-flexbox; + display: flex; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center; + -ms-flex-negative: 0; + flex-shrink: 0; + width: 30px; + height: 30px; + border-radius: 50%; + box-shadow: 0 0 0 4px hsl(0, 0%, 100%), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05); + box-shadow: 0 0 0 4px var(--color-white), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05) +} + +.cd-timeline__img i { + font-size: 1.5em; + color: white; +} + +@media (max-width: 64rem) { + .cd-timeline__img i { + font-size: 0.9em; + } +} + +.cd-timeline__img img { + width: 40px; + height: 40px; + margin-left: 2px; + margin-top: 2px; +} + +@media (max-width: 64rem) { + .cd-timeline__img img { + width: 20px; + height: 20px; + margin-left: 2px; + margin-top: 2px; + } +} + +@media (min-width: 64rem) { + .cd-timeline__img { + width: 60px; + height: 60px; + -ms-flex-order: 1; + order: 1; + margin-left: calc(5% - 30px); + will-change: transform; + } + + .cd-timeline__block:nth-child(even) .cd-timeline__img { + margin-right: calc(5% - 30px) + } +} + +.cd-timeline__img--picture { + background-color: #7289DA; +} + +.cd-timeline__img--movie { + background-color: hsl(356, 53%, 49%); + background-color: var(--cd-color-5) +} + +.cd-timeline__img--location { + background-color: hsl(47, 85%, 61%); + background-color: var(--cd-color-6) +} + +.cd-timeline__content { + -ms-flex-positive: 1; + flex-grow: 1; + position: relative; + margin-left: 1.25em; + margin-left: var(--space-md); + background: hsl(0, 0%, 100%); + background: var(--color-white); + border-radius: 0.25em; + border-radius: var(--radius-md); + padding: 1.25em; + padding: var(--space-md); + box-shadow: 0 3px 0 hsl(205, 38%, 89%); + box-shadow: 0 3px 0 var(--cd-color-2) +} + +.cd-timeline__content::before { + content: ''; + position: absolute; + top: 16px; + right: 100%; + width: 0; + height: 0; + border: 7px solid transparent; + border-right-color: hsl(0, 0%, 100%); + border-right-color: var(--color-white) +} + +.cd-timeline__content h2 { + color: hsl(206, 21%, 24%); + color: var(--cd-color-1) +} + +@media (min-width: 64rem) { + .cd-timeline__content { + width: 45%; + -ms-flex-positive: 0; + flex-grow: 0; + will-change: transform; + margin: 0; + font-size: 0.9em; + --line-height-multiplier: 1.2 + } + .cd-timeline__content::before { + top: 24px + } + .cd-timeline__block:nth-child(odd) .cd-timeline__content::before { + right: auto; + left: 100%; + width: 0; + height: 0; + border: 7px solid transparent; + border-left-color: hsl(0, 0%, 100%); + border-left-color: var(--color-white) + } +} + +.cd-timeline__date { + color: hsla(207, 10%, 55%, 0.7); + color: hsla(var(--cd-color-3-h), var(--cd-color-3-s), var(--cd-color-3-l), 0.7) +} + +@media (min-width: 64rem) { + .cd-timeline__date { + position: absolute; + width: 100%; + left: 120%; + top: 20px + } + .cd-timeline__block:nth-child(even) .cd-timeline__date { + left: auto; + right: 120%; + text-align: right + } +} + +@media (min-width: 64rem) { + .cd-timeline__img--hidden, .cd-timeline__content--hidden { + visibility: hidden + } + .cd-timeline__img--bounce-in { + -webkit-animation: cd-bounce-1 0.6s; + animation: cd-bounce-1 0.6s + } + .cd-timeline__content--bounce-in { + -webkit-animation: cd-bounce-2 0.6s; + animation: cd-bounce-2 0.6s + } + .cd-timeline__block:nth-child(even) .cd-timeline__content--bounce-in { + -webkit-animation-name: cd-bounce-2-inverse; + animation-name: cd-bounce-2-inverse + } + .cd-timeline__img--bounce-out { + -webkit-animation: cd-bounce-out-1 0.6s; + animation: cd-bounce-out-1 0.6s; + } + .cd-timeline__content--bounce-out { + -webkit-animation: cd-bounce-out-2 0.6s; + animation: cd-bounce-out-2 0.6s; + } +} + +@-webkit-keyframes cd-bounce-1 { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5) + } + 60% { + opacity: 1; + -webkit-transform: scale(1.2); + transform: scale(1.2) + } + 100% { + -webkit-transform: scale(1); + transform: scale(1) + } +} + +@keyframes cd-bounce-1 { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5) + } + 60% { + opacity: 1; + -webkit-transform: scale(1.2); + transform: scale(1.2) + } + 100% { + -webkit-transform: scale(1); + transform: scale(1) + } +} + +@-webkit-keyframes cd-bounce-2 { + 0% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@keyframes cd-bounce-2 { + 0% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@-webkit-keyframes cd-bounce-2-inverse { + 0% { + opacity: 0; + -webkit-transform: translateX(100px); + transform: translateX(100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(-20px); + transform: translateX(-20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@keyframes cd-bounce-2-inverse { + 0% { + opacity: 0; + -webkit-transform: translateX(100px); + transform: translateX(100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(-20px); + transform: translateX(-20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@-webkit-keyframes cd-bounce-out-1 { + 0% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1) + } + + 60% { + -webkit-transform: scale(1.2); + transform: scale(1.2) + } + + 100% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5) + } +} + +@keyframes cd-bounce-out-1 { + 0% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } + + 60% { + -webkit-transform: scale(1.2); + transform: scale(1.2); + } + + 100% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } +} + +@-webkit-keyframes cd-bounce-out-2 { + 0% { + opacity: 1; + -webkit-transform: translateX(0); + transform: translateX(0) + } + 60% { + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } +} + +@keyframes cd-bounce-out-2 { + 0% { + opacity: 1; + -webkit-transform: translateX(0); + transform: translateX(0) + } + 60% { + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } +} diff --git a/pydis_site/static/images/events/100k.png b/pydis_site/static/images/events/100k.png new file mode 100644 index 00000000..ae024d77 Binary files /dev/null and b/pydis_site/static/images/events/100k.png differ diff --git a/pydis_site/static/images/frontpage/welcome.jpg b/pydis_site/static/images/frontpage/welcome.jpg new file mode 100644 index 00000000..0eb8f672 Binary files /dev/null and b/pydis_site/static/images/frontpage/welcome.jpg differ diff --git a/pydis_site/static/images/navbar/discord.svg b/pydis_site/static/images/navbar/discord.svg new file mode 100644 index 00000000..406e3836 --- /dev/null +++ b/pydis_site/static/images/navbar/discord.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydis_site/static/images/navbar/navbar_discordjoin.svg b/pydis_site/static/images/navbar/navbar_discordjoin.svg deleted file mode 100644 index 75e6b102..00000000 --- a/pydis_site/static/images/navbar/navbar_discordjoin.svg +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/pydis_site/static/images/sponsors/adafruit.png b/pydis_site/static/images/sponsors/adafruit.png deleted file mode 100644 index eb14cf5d..00000000 Binary files a/pydis_site/static/images/sponsors/adafruit.png and /dev/null differ diff --git a/pydis_site/static/images/sponsors/notion.png b/pydis_site/static/images/sponsors/notion.png new file mode 100644 index 00000000..44ae9244 Binary files /dev/null and b/pydis_site/static/images/sponsors/notion.png differ diff --git a/pydis_site/static/images/sponsors/streamyard.png b/pydis_site/static/images/sponsors/streamyard.png new file mode 100644 index 00000000..a1527e8d Binary files /dev/null and b/pydis_site/static/images/sponsors/streamyard.png differ diff --git a/pydis_site/static/images/timeline/cd-icon-location.svg b/pydis_site/static/images/timeline/cd-icon-location.svg new file mode 100755 index 00000000..6128fecd --- /dev/null +++ b/pydis_site/static/images/timeline/cd-icon-location.svg @@ -0,0 +1,4 @@ + + + diff --git a/pydis_site/static/images/timeline/cd-icon-movie.svg b/pydis_site/static/images/timeline/cd-icon-movie.svg new file mode 100755 index 00000000..498a93fa --- /dev/null +++ b/pydis_site/static/images/timeline/cd-icon-movie.svg @@ -0,0 +1,4 @@ + + + diff --git a/pydis_site/static/images/timeline/cd-icon-picture.svg b/pydis_site/static/images/timeline/cd-icon-picture.svg new file mode 100755 index 00000000..015718a8 --- /dev/null +++ b/pydis_site/static/images/timeline/cd-icon-picture.svg @@ -0,0 +1,72 @@ + +image/svg+xml diff --git a/pydis_site/static/images/waves/wave_dark.svg b/pydis_site/static/images/waves/wave_dark.svg new file mode 100644 index 00000000..35174c47 --- /dev/null +++ b/pydis_site/static/images/waves/wave_dark.svg @@ -0,0 +1,73 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/pydis_site/static/images/waves/wave_white.svg b/pydis_site/static/images/waves/wave_white.svg new file mode 100644 index 00000000..441dacff --- /dev/null +++ b/pydis_site/static/images/waves/wave_white.svg @@ -0,0 +1,77 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/pydis_site/static/js/timeline/main.js b/pydis_site/static/js/timeline/main.js new file mode 100644 index 00000000..2ff7df57 --- /dev/null +++ b/pydis_site/static/js/timeline/main.js @@ -0,0 +1,104 @@ +(function(){ + // Vertical Timeline - by CodyHouse.co (modified) + function VerticalTimeline( element ) { + this.element = element; + this.blocks = this.element.getElementsByClassName("cd-timeline__block"); + this.images = this.element.getElementsByClassName("cd-timeline__img"); + this.contents = this.element.getElementsByClassName("cd-timeline__content"); + this.offset = 0.8; + this.hideBlocks(); + }; + + VerticalTimeline.prototype.hideBlocks = function() { + if ( !"classList" in document.documentElement ) { + return; // no animation on older browsers + } + //hide timeline blocks which are outside the viewport + var self = this; + for( var i = 0; i < this.blocks.length; i++) { + (function(i){ + if( self.blocks[i].getBoundingClientRect().top > window.innerHeight*self.offset ) { + self.images[i].classList.add("cd-timeline__img--hidden"); + self.contents[i].classList.add("cd-timeline__content--hidden"); + } + })(i); + } + }; + + VerticalTimeline.prototype.showBlocks = function() { + if ( ! "classList" in document.documentElement ) { + return; + } + var self = this; + for( var i = 0; i < this.blocks.length; i++) { + (function(i){ + if((self.contents[i].classList.contains("cd-timeline__content--hidden") || self.contents[i].classList.contains("cd-timeline__content--bounce-out")) && self.blocks[i].getBoundingClientRect().top <= window.innerHeight*self.offset ) { + // add bounce-in animation + self.images[i].classList.add("cd-timeline__img--bounce-in"); + self.contents[i].classList.add("cd-timeline__content--bounce-in"); + self.images[i].classList.remove("cd-timeline__img--hidden"); + self.contents[i].classList.remove("cd-timeline__content--hidden"); + self.images[i].classList.remove("cd-timeline__img--bounce-out"); + self.contents[i].classList.remove("cd-timeline__content--bounce-out"); + } + })(i); + } + }; + + VerticalTimeline.prototype.hideBlocksScroll = function () { + if ( ! "classList" in document.documentElement ) { + return; + } + var self = this; + for( var i = 0; i < this.blocks.length; i++) { + (function(i){ + if(self.contents[i].classList.contains("cd-timeline__content--bounce-in") && self.blocks[i].getBoundingClientRect().top > window.innerHeight*self.offset ) { + self.images[i].classList.remove("cd-timeline__img--bounce-in"); + self.contents[i].classList.remove("cd-timeline__content--bounce-in"); + self.images[i].classList.add("cd-timeline__img--bounce-out"); + self.contents[i].classList.add("cd-timeline__content--bounce-out"); + } + })(i); + } + } + + var verticalTimelines = document.getElementsByClassName("js-cd-timeline"), + verticalTimelinesArray = [], + scrolling = false; + if( verticalTimelines.length > 0 ) { + for( var i = 0; i < verticalTimelines.length; i++) { + (function(i){ + verticalTimelinesArray.push(new VerticalTimeline(verticalTimelines[i])); + })(i); + } + + //show timeline blocks on scrolling + window.addEventListener("scroll", function(event) { + if( !scrolling ) { + scrolling = true; + (!window.requestAnimationFrame) ? setTimeout(checkTimelineScroll, 250) : window.requestAnimationFrame(checkTimelineScroll); + } + }); + + function animationEnd(event) { + if (event.target.classList.contains("cd-timeline__img--bounce-out")) { + event.target.classList.add("cd-timeline__img--hidden"); + event.target.classList.remove("cd-timeline__img--bounce-out"); + } else if (event.target.classList.contains("cd-timeline__content--bounce-out")) { + event.target.classList.add("cd-timeline__content--hidden"); + event.target.classList.remove("cd-timeline__content--bounce-out"); + } + } + + window.addEventListener("animationend", animationEnd); + window.addEventListener("webkitAnimationEnd", animationEnd); + } + + function checkTimelineScroll() { + verticalTimelinesArray.forEach(function(timeline){ + timeline.showBlocks(); + timeline.hideBlocksScroll(); + }); + scrolling = false; + }; +})(); diff --git a/pydis_site/templates/404.html b/pydis_site/templates/404.html new file mode 100644 index 00000000..42e317d2 --- /dev/null +++ b/pydis_site/templates/404.html @@ -0,0 +1,34 @@ +{% load static %} + + + + + + Python Discord | 404 + + + + + + + + + +
      +
      + Python Discord banner +
      +
      +

      404 — Not Found

      +

      We couldn't find the page you're looking for. Here are a few things to try out:

      +
        +
      • Double check the URL. Are you sure you typed it out correctly? +
      • Come join our Discord Server. Maybe we can help you out over + there +
      +
      +
      + + + diff --git a/pydis_site/templates/500.html b/pydis_site/templates/500.html new file mode 100644 index 00000000..869892ec --- /dev/null +++ b/pydis_site/templates/500.html @@ -0,0 +1,29 @@ +{% load static %} + + + + + + Python Discord | 500 + + + + + + + + + +
      +
      + Python Discord banner +
      +
      +

      500 — Internal Server Error

      +

      Something went wrong at our end. Please try again shortly, or if the problem persists, please let us know on Discord.

      +
      +
      + + + diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html index 6fc0c6bb..906fc577 100644 --- a/pydis_site/templates/base/base.html +++ b/pydis_site/templates/base/base.html @@ -27,7 +27,6 @@ {# Font-awesome here is defined explicitly so that we can have Pro #} - {% block head %}{% endblock %} diff --git a/pydis_site/templates/base/footer.html b/pydis_site/templates/base/footer.html index 90f06f3c..bca43b5d 100644 --- a/pydis_site/templates/base/footer.html +++ b/pydis_site/templates/base/footer.html @@ -1,7 +1,7 @@

      - Built with and
      © {% now "Y" %} Python Discord + Powered by
      Built with and
      © {% now "Y" %} Python Discord

      diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index ebafa269..f19706cd 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -19,7 +19,7 @@ diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index f31363a4..18f6b77b 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -9,12 +9,63 @@ {% block content %} {% include "base/navbar.html" %} -
      + +
      +
      +

      100K Member Milestone!

      +
      +
      + Thanks to all our members for helping us create this friendly and helpful community! +

      + As a nice treat, we've created a Timeline page for people + to discover the events that made our community what it is today. Be sure to check it out! +
      +
      + + +
      + +
      +
      + + {# Embedded Welcome video #} +
      +
      + +
      +
      +
      +
      - {# Who are we? #} -
      + {# Animated wave elements #} + + + + +
      + + +
      + +

      Who are we?

      -
      +

      @@ -31,68 +82,125 @@

      You can find help with most Python-related problems in one of our help channels. - Our staff of over 50 dedicated expert Helpers are available around the clock + Our staff of over 100 dedicated expert Helpers are available around the clock in every timezone. Whether you're looking to learn the language or working on a complex project, we've got someone who can help you if you get stuck.

      - {# Right column container #} -
      - -
      -
      + {# Showcase box #} +
      +
      + +
      Interactive timeline
      - {# Projects #} -

      Projects

      -
      -
      - - {# Display projects from HomeView.repos #} - {% for repo in repo_data %} -
      -
      -
      - -
      - {{ repo.description }} -

      - {{ repo.language }} - {{ repo.stargazers }} - {{ repo.forks }} -
      -
      +
      + + + + + +
      -
      - {% endfor %} + +

      + Discover the history of our community, and learn about the events that made our community what it is today. +

      + + + +
      +
    - {# Sponsors #} -
    -
    + + {% if repo_data %} +
    +
    +

    Projects

    + + + +
    +
    + {% endif %} + + +
    + diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html new file mode 100644 index 00000000..d9069aca --- /dev/null +++ b/pydis_site/templates/home/timeline.html @@ -0,0 +1,693 @@ +{% extends 'base/base.html' %} +{% load static %} + +{% block title %}Timeline{% endblock %} +{% block head %} + + +{% endblock %} + +{% block content %} + {% include "base/navbar.html" %} + +
    +
    +
    +
    + Picture +
    + +
    +

    Python Discord is created

    +

    Joe Banks becomes one of the owners around 3 days after it + is created, and Leon Sandøy (lemon) joins the owner team later in the year, when the community + has around 300 members.

    + +
    + Jan 8th, 2017 +
    +
    +
    + +
    +
    + +
    + +
    +

    Python Discord hits 1,000 members

    +

    Our main source of new users at this point is a post on Reddit that + happens to get very good SEO. We are one of the top 10 search engine hits for the search term + "python discord".

    + +
    + Nov 10th, 2017 +
    +
    +
    + +
    +
    + Picture +
    + +
    +

    Our logo is born. Thanks @Aperture!

    +

    +

    + +
    + Feb 3rd, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    PyDis hits 2,000 members; pythondiscord.com and @Python are live

    +

    The public moderation bot we're using at the time, Rowboat, announces + it will be shutting down. We decide that we'll write our own bot to handle moderation, so that we + can have more control over its features. We also buy a domain and start making a website in Flask. +

    + +
    + Mar 4th, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    First code jam with the theme “snakes”

    +

    Our very first Code Jam attracts a handful of users who work in random + teams of 2. We ask our participants to write a snake-themed Discord bot. Most of the code written + for this jam still lives on in Sir Lancebot, and you can play with it by using the + .snakes command. For more information on this event, see the event page

    + +
    + Mar 23rd, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    The privacy policy is created

    +

    Since data privacy is quite important to us, we create a privacy page + pretty much as soon as our new bot and site starts collecting some data. To this day, we keep our privacy policy up to date with all + changes, and since April 2020 we've started doing monthly data reviews.

    + +
    + May 21st, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    Do You Even Python and PyDis merger

    +

    At this point in time, there are only two serious Python communities on + Discord - Ours, and one called Do You Even Python. We approach the owners of DYEP with a bold + proposal - let's shut down their community, replace it with links to ours, and in return we will let + their staff join our staff. This gives us a big boost in members, and eventually leads to @eivl and + @Mr. Hemlock joining our Admin team

    + +
    + Jun 9th, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    PyDis hits 5,000 members and partners with r/Python

    +

    As we continue to grow, we approach the r/Python subreddit and ask to + become their official Discord community. They agree, and we become listed in their sidebar, giving + us yet another source of new members.

    + +
    + Jun 20th, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    PyDis is now partnered with Discord; the vanity URL discord.gg/python is created

    +

    After being rejected for their Partner program several times, we + finally get approved. The recent partnership with the r/Python subreddit plays a significant role in + qualifying us for this partnership.

    + +
    + Jul 10th, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    First Hacktoberfest PyDis event; @Sir Lancebot is created

    +

    We create a second bot for our community and fill it up with simple, + fun and relatively easy issues. The idea is to create an approachable arena for our members to cut + their open-source teeth on, and to provide lots of help and hand-holding for those who get stuck. + We're training our members to be productive contributors in the open-source ecosystem.

    + +
    + Oct 1st, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    PyDis hits 10,000 members

    +

    We partner with RLBot, move from GitLab to GitHub, and start putting + together the first Advent of Code event.

    + +
    + Nov 24th, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    django-simple-bulma is released on PyPi

    +

    Our very first package on PyPI, django-simple-bulma is a package that + sets up the Bulma CSS framework for your Django application and lets you configure everything in + settings.py.

    + +
    + Dec 19th, 2018 +
    +
    +
    + +
    +
    + +
    + +
    +

    PyDis hits 15,000 members; the “hot ones special” video is released

    +
    + +
    + +
    + Apr 8th, 2019 +
    +
    +
    + +
    +
    + +
    + +
    +

    The Django rewrite of pythondiscord.com is now live!

    +

    The site is getting more and more complex, and it's time for a rewrite. + We decide to go for a different stack, and build a website based on Django, DRF, Bulma and + PostgreSQL.

    + +
    + Sep 15, 2019 +
    +
    +
    + +
    +
    + +
    + +
    +

    The code of conduct is created

    +

    Inspired by the Adafruit, Rust and Django communities, an essential + community pillar is created; Our Code of + Conduct.

    + +
    + Oct 26th, 2019 +
    +
    +
    + +
    +
    + Picture +
    + +
    +

    Sebastiaan Zeef becomes an owner

    +

    After being a long time active contributor to our projects and the driving + force behind many of our events, Sebastiaan Zeef joins the Owners Team alongside Joe & Leon.

    + +
    + Sept 22nd, 2019 +
    +
    +
    + +
    +
    + +
    + +
    +

    PyDis hits 30,000 members

    +

    More than tripling in size since the year before, the community hits + 30000 users. At this point, we're probably the largest Python chat community on the planet.

    + +
    + Dec 22nd, 2019 +
    +
    +
    + +
    +
    + +
    + +
    +

    PyDis sixth code jam with the theme “Ancient technology” and the technology Kivy

    +

    Our Code Jams are becoming an increasingly big deal, and the Kivy core + developers join us to judge the event and help out our members during the event. One of them, + @tshirtman, even joins our staff!

    + +
    + +
    + +
    + Jan 17, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    The new help channel system is live

    +

    We release our dynamic help-channel system, which allows you to claim + your very own help channel instead of fighting over the static help channels. We release a Help Channel Guide to + help our members fully understand how the system works.

    + +
    + Apr 5th, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    Python Discord hits 40,000 members, and is now bigger than Liechtenstein.

    +

    +

    + +
    + Apr 14, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    PyDis Game Jam 2020 with the “Three of a Kind” theme and Arcade as the technology

    +

    The creator of Arcade, Paul Vincent Craven, joins us as a judge. + Several of the Code Jam participants also end up getting involved contributing to the Arcade + repository.

    + +
    + +
    + +
    + Apr 17th, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    ModMail is now live

    +

    Having originally planned to write our own ModMail bot from scratch, we + come across an exceptionally good ModMail bot by + kyb3r and decide to just self-host that one instead.

    + +
    + May 25th, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    Python Discord is now listed on python.org/community

    +

    After working towards this goal for months, we finally work out an + arrangement with the PSF that allows us to be listed on that most holiest of websites: + https://python.org/. There was much rejoicing.

    + +
    + May 28th, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    Python Discord Public Statistics are now live

    +

    After getting numerous requests to publish beautiful data on member + count and channel use, we create stats.pythondiscord.com for + all to enjoy.

    + +
    + Jun 4th, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    PyDis summer code jam 2020 with the theme “Early Internet” and Django as the technology

    +

    Sponsored by the Django Software Foundation and JetBrains, the Summer + Code Jam for 2020 attracts hundreds of participants, and sees the creation of some fantastic + projects. Check them out in our judge stream below:

    + +
    + +
    + +
    + Jul 31st, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    Python Discord is now the new home of the PyWeek event!

    +

    PyWeek, a game jam that has been running since 2005, joins Python + Discord as one of our official events. Find more information about PyWeek on their official website.

    + +
    + Aug 16th, 2020 +
    +
    +
    + +
    +
    + Picture +
    + +
    +

    Python Discord hosts the 2020 CPython Core Developer Q&A

    +
    + +
    + +
    + Oct 21st, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    Python Discord hits 100,000 members!

    +

    Only six months after hitting 40,000 users, we hit 100,000 users. A + monumental milestone, + and one we're very proud of. To commemorate it, we create this timeline.

    + +
    + Oct 22nd, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    We migrate all our infrastructure to Kubernetes

    +

    As our tech stack grows, we decide to migrate all our services over to a + container orchestration paradigm via Kubernetes. This gives us better control and scalability. + Joe Banks takes on the role as DevOps Lead. +

    + +
    + Nov 29th, 2020 +
    +
    +
    + +
    +
    + +
    + +
    +

    Advent of Code attracts hundreds of participants

    +

    + A total of 443 Python Discord members sign up to be part of + Eric Wastl's excellent Advent of Code event. + As always, we provide dedicated announcements, scoreboards, bot commands and channels for our members + to enjoy the event in. + +

    + +
    + December 1st - 25th, 2020 +
    +
    +
    + + +
    +
    + +
    + +
    +

    We release The PEP 8 song

    +

    We release the PEP 8 song on our YouTube channel, which finds tens of + thousands of listeners!

    + +
    + +
    + +
    + February 8th, 2021 +
    +
    +
    + +
    +
    + +
    + +
    +

    We now have 150,000 members!

    +

    Our growth continues to accelerate.

    + +
    + Feb 18th, 2021 +
    +
    +
    + +
    +
    + +
    + +
    +

    Leon Sandøy appears on Talk Python To Me

    +

    Leon goes on the Talk Python to Me podcast with Michael Kennedy + to discuss the history of Python Discord, the critical importance of culture, and how to run a massive + community. You can find the episode at talkpython.fm. +

    + + + +
    + Mar 1st, 2021 +
    +
    +
    + +
    +
    + +
    + +
    +

    We're on the Teaching Python podcast!

    +

    Leon joins Sean and Kelly on the Teaching Python podcast to discuss how the pandemic has + changed the way we learn, and what role communities like Python Discord can play in this new world. + You can find the episode at teachingpython.fm. +

    + + + +
    + Mar 13th, 2021 +
    +
    +
    + +
    +
    + +
    + +
    +

    New feature: Weekly discussion channel

    +

    Every week (or two weeks), we'll be posting a new topic to discuss in a + channel called #weekly-topic-discussion. Our inaugural topic is a PyCon talk by Anthony Shaw called + Wily Python: Writing simpler and more maintainable Python.. +

    + +
    + +
    + +
    + Mar 13th, 2021 +
    +
    +
    + +
    +
    + +
    + +
    +

    Summer Code Jam 2020 Highlights

    +

    + We release a new video to our YouTube showing the best projects from the Summer Code Jam 2020. + Better late than never! +

    + +
    + +
    + +
    + Mar 21st, 2021 +
    +
    +
    + +
    +
    + + +{% endblock %} -- cgit v1.2.3 From 0c4e00105b950ea249a28263373a6377f91ed2a1 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Fri, 14 May 2021 21:41:00 +0800 Subject: Enable smooth scrolling on the website. This makes it less jarring when scrolling to a heading since many content pages include a Table of Contents. --- pydis_site/static/css/base/base.css | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'pydis_site/static/css') diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index d3602d35..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 { @@ -151,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; + } +} -- cgit v1.2.3