diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..4621dd1f0 --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react"], + "plugins": [ + "@babel/plugin-proposal-class-properties" + ] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..969fde0c9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +# this is what we will be mounting for development +#**/opentreemap +**/node_modules diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..ffb84462b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +FROM python:3.8 + +#RUN apt-get install -y software-properties-common && add-apt-repository -y ppa:ubuntu-toolchain-r/test + +#checkinstall \ +#libgdal1-dev \ + +RUN apt-get update \ + && apt-get install -y \ + gettext \ + libgeos-dev \ + libproj-dev \ + build-essential \ + python3-dev \ + python3-pip \ + libfreetype6-dev \ + binutils \ + libproj-dev \ + gdal-bin \ + curl + +RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && apt-get install -y nodejs + +RUN npm install -g yarn + +WORKDIR /usr/local/otm/app +# only copy what we need +COPY requirements.txt . +RUN pip install -r requirements.txt +# Bundle JS and CSS via webpack +RUN yarn --force + +# then copy everything else +COPY . . +COPY docker/local_settings.py /usr/local/otm/app/opentreemap/opentreemap/settings/local_settings.py + +RUN mkdir -p /usr/local/otm/static && mkdir -p /usr/local/otm/media && mkdir -p /usr/local/otm/emails diff --git a/MIGRATE-PYTHON3.md b/MIGRATE-PYTHON3.md new file mode 100644 index 000000000..58b7577b1 --- /dev/null +++ b/MIGRATE-PYTHON3.md @@ -0,0 +1,3 @@ +These are notes for migrating to python3 + +https://github.com/coderholic/django-cities/issues/184 diff --git a/__init__.py b/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/assets/css/sass/_base.scss b/assets/css/sass/_base.scss index 992a4b270..3c86c7603 100644 --- a/assets/css/sass/_base.scss +++ b/assets/css/sass/_base.scss @@ -1,5 +1,6 @@ // Where assets used in url(...) are -$staticUrl: '../../' !default; +//$staticUrl: '../../' !default; +$staticUrl: '../../'; // Fonts @import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700|Roboto:100,400,700); diff --git a/assets/css/sass/partials/_header.scss b/assets/css/sass/partials/_header.scss index 444013753..a5b27ac0a 100644 --- a/assets/css/sass/partials/_header.scss +++ b/assets/css/sass/partials/_header.scss @@ -39,15 +39,15 @@ position: relative; img { - max-width: 300px; + max-width: 700px; max-height: 78px; } } .instance-header .logo { - @media #{$screen-ltmd} { - display: none; - } + //@media #{$screen-ltmd} { + // display: none; + //} } .toolbar-wrapper { diff --git a/assets/css/sass/partials/_layout.scss b/assets/css/sass/partials/_layout.scss index fc4210740..08113deff 100644 --- a/assets/css/sass/partials/_layout.scss +++ b/assets/css/sass/partials/_layout.scss @@ -48,7 +48,7 @@ body { position: absolute; overflow: hidden; margin: 0; - top: 196px; + top: 210px; width: 100%; left: 0; @@ -219,6 +219,14 @@ body { background: $light-gray-color; border-radius: 6px; + // this is from the old bootstrap + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); + > :last-child { margin-bottom: 0; } @@ -355,4 +363,14 @@ body { height: calc(100% - #{$navbar-height}); } } + + // with react, we cannot easily add the above to the DOM, + // so we solve that by adding to these elements + .header.hide-search { + margin-top: -90px; + } + .content.explore-map.hide-search { + margin-top: 9px; + height: calc(100% - #{$navbar-height}); + } } diff --git a/assets/css/sass/partials/_navbar.scss b/assets/css/sass/partials/_navbar.scss index e7a6b2bad..792da2609 100644 --- a/assets/css/sass/partials/_navbar.scss +++ b/assets/css/sass/partials/_navbar.scss @@ -39,6 +39,13 @@ margin-right: 15px; } + &.mr-auto { + margin-left: 5px; + } + &.ml-auto { + margin-right: 30px; + } + > li { margin-left: 20px; height: #{$navbar-height - 1px}; diff --git a/assets/css/sass/partials/pages/_addtree.scss b/assets/css/sass/partials/pages/_addtree.scss index 344190e27..e51daeff8 100644 --- a/assets/css/sass/partials/pages/_addtree.scss +++ b/assets/css/sass/partials/pages/_addtree.scss @@ -105,7 +105,7 @@ } .add-step-header:before { - content: counter(steps) ". "; + //content: counter(steps) ". "; counter-increment: steps; } .add-step-header { @@ -256,6 +256,10 @@ } } + .photo-success { + background: $primary-color + } + @include checkboxes; } .add-step-footer { @@ -315,3 +319,7 @@ } } } + +.react-datepicker-popper { + z-index: 3 !important +} diff --git a/assets/css/sass/partials/pages/_map.scss b/assets/css/sass/partials/pages/_map.scss index 7f708e729..587145c85 100644 --- a/assets/css/sass/partials/pages/_map.scss +++ b/assets/css/sass/partials/pages/_map.scss @@ -302,3 +302,39 @@ $light-to-dark-green: #ffffff 10%, } } } + +.leaflet-control-layers:nth-child(2) .leaflet-control-layers-toggle { + background-image: url(#{$staticUrl}img/map_layers_icon.png); +} + +// for all our legend information +.info +{ + padding: 6px 8px; + font: 13px/16px Verdana, Geneva, sans-serif; + background: white; + background: rgba(255,255,255,0.8); + box-shadow: 0 0 15px rgba(0,0,0,0.2); + border-radius: 10px; +} + +.legend { + line-height: 19px; + padding:7px; + color: #555; +} + +.legend i { + width: 15px; + height: 15px; + float: left; + margin-right: 8px; + opacity: 0.7; +} + +.circle +{ + float: left; + border: 1px solid #222; + border-radius: 50%; +} diff --git a/assets/css/sass/partials/pages/_sidebar.scss b/assets/css/sass/partials/pages/_sidebar.scss index ecea64123..f1becc407 100644 --- a/assets/css/sass/partials/pages/_sidebar.scss +++ b/assets/css/sass/partials/pages/_sidebar.scss @@ -13,6 +13,8 @@ width: 100%; position: static; flex: 0 1 auto; + // tzinckgraf + overflow-y: auto; } > div { diff --git a/assets/css/sass/partials/pages/_treedetails.scss b/assets/css/sass/partials/pages/_treedetails.scss index e9692883c..8db04d41e 100644 --- a/assets/css/sass/partials/pages/_treedetails.scss +++ b/assets/css/sass/partials/pages/_treedetails.scss @@ -177,9 +177,10 @@ background: darken($light-gray-color, 5%); border-radius: 9px; - .item { + .carousel-item { .inspect-photo { display: block; + text-align: center; img { width: auto; height: auto; @@ -192,7 +193,7 @@ font-size: 3rem; opacity: 0; position: absolute; - right: 0px; + right: 25px; top: 0px; } @@ -200,6 +201,21 @@ display: block; opacity: .9; } + .photo-label { + position: absolute; + left: 6px; + top: 6px; + opacity: 0.85; + width: auto; + background: black; + color: white; + border: none; + transition: opacity 0.3s; + z-index: 999; + padding: 5px 12px; + font-size: 1.2rem !important; + border-radius: 6px; + } } } > a.carousel-control { diff --git a/assets/css/vendor/bootstrap.css b/assets/css/vendor/bootstrap.css index 04a5e400c..a3171bef4 100644 --- a/assets/css/vendor/bootstrap.css +++ b/assets/css/vendor/bootstrap.css @@ -1,75 +1,141 @@ /*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * Bootstrap v4.5.3 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +:root { + --blue: #007bff; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: #dc3545; + --orange: #fd7e14; + --yellow: #ffc107; + --green: #28a745; + --teal: #20c997; + --cyan: #17a2b8; + --white: #fff; + --gray: #6c757d; + --gray-dark: #343a40; + --primary: #007bff; + --secondary: #6c757d; + --success: #28a745; + --info: #17a2b8; + --warning: #ffc107; + --danger: #dc3545; + --light: #f8f9fa; + --dark: #343a40; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + html { font-family: sans-serif; + line-height: 1.15; -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; } + body { margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; } -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} -audio, -canvas, -progress, -video { - display: inline-block; - vertical-align: baseline; + +[tabindex="-1"]:focus:not(:focus-visible) { + outline: 0 !important; } -audio:not([controls]) { - display: none; + +hr { + box-sizing: content-box; height: 0; + overflow: visible; } -[hidden], -template { - display: none; + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; } -a { - background-color: transparent; + +p { + margin-top: 0; + margin-bottom: 1rem; } -a:active, -a:hover { - outline: 0; + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; } -abbr[title] { - border-bottom: 1px dotted; + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; } -b, -strong { - font-weight: bold; + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; } -dfn { - font-style: italic; + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; } -h1 { - margin: .67em 0; - font-size: 2em; + +dt { + font-weight: 700; } -mark { - color: #000; - background: #ff0; + +dd { + margin-bottom: .5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; } + small { font-size: 80%; } + sub, sup { position: relative; @@ -77,5775 +143,6998 @@ sup { line-height: 0; vertical-align: baseline; } -sup { - top: -.5em; -} + sub { bottom: -.25em; } -img { - border: 0; + +sup { + top: -.5em; } -svg:not(:root) { - overflow: hidden; + +a { + color: #007bff; + text-decoration: none; + background-color: transparent; } -figure { - margin: 1em 40px; + +a:hover { + color: #0056b3; + text-decoration: underline; } -hr { - height: 0; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; + +a:not([href]):not([class]) { + color: inherit; + text-decoration: none; } -pre { - overflow: auto; + +a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; } + +pre, code, kbd, -pre, samp { - font-family: monospace, monospace; + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 1em; } -button, + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + -ms-overflow-style: scrollbar; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + input, -optgroup, +button, select, +optgroup, textarea { margin: 0; - font: inherit; - color: inherit; + font-family: inherit; + font-size: inherit; + line-height: inherit; } -button { + +button, +input { overflow: visible; } + button, select { text-transform: none; } + +[role="button"] { + cursor: pointer; +} + +select { + word-wrap: normal; +} + button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { +[type="button"], +[type="reset"], +[type="submit"] { -webkit-appearance: button; - cursor: pointer; } -button[disabled], -html input[disabled] { - cursor: default; + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; } + button::-moz-focus-inner, -input::-moz-focus-inner { +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { padding: 0; - border: 0; + border-style: none; } -input { - line-height: normal; + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +textarea { + overflow: auto; + resize: vertical; } -input[type="checkbox"], -input[type="radio"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; } -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { height: auto; } -input[type="search"] { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - -webkit-appearance: textfield; + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: none; } -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { + +[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } -fieldset { - padding: .35em .625em .75em; - margin: 0 2px; - border: 1px solid #c0c0c0; + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; } -legend { - padding: 0; - border: 0; + +output { + display: inline-block; } -textarea { - overflow: auto; + +summary { + display: list-item; + cursor: pointer; } -optgroup { - font-weight: bold; + +template { + display: none; } -table { - border-spacing: 0; - border-collapse: collapse; + +[hidden] { + display: none !important; } -td, -th { - padding: 0; + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; } -/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ -@media print { - *, - *:before, - *:after { - color: #000 !important; - text-shadow: none !important; - background: transparent !important; - -webkit-box-shadow: none !important; - box-shadow: none !important; - } - a, - a:visited { - text-decoration: underline; - } - a[href]:after { - content: " (" attr(href) ")"; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - a[href^="#"]:after, - a[href^="javascript:"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - thead { - display: table-header-group; - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, - h3 { - page-break-after: avoid; - } - .navbar { - display: none; - } - .btn > .caret, - .dropup > .btn > .caret { - border-top-color: #000 !important; - } - .label { - border: 1px solid #000; - } - .table { - border-collapse: collapse !important; - } - .table td, - .table th { - background-color: #fff !important; - } - .table-bordered th, - .table-bordered td { - border: 1px solid #ddd !important; - } +h1, .h1 { + font-size: 2.5rem; } -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('#{$staticUrl}fonts/glyphicons-halflings-regular.eot'); - src: url('#{$staticUrl}fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), - url('#{$staticUrl}fonts/glyphicons-halflings-regular.woff') format('woff'), - url('#{$staticUrl}fonts/glyphicons-halflings-regular.ttf') format('truetype'), - url('#{$staticUrl}fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +h2, .h2 { + font-size: 2rem; } -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.glyphicon-asterisk:before { - content: "\2a"; +h3, .h3 { + font-size: 1.75rem; } -.glyphicon-plus:before { - content: "\2b"; -} -.glyphicon-euro:before, -.glyphicon-eur:before { - content: "\20ac"; -} -.glyphicon-minus:before { - content: "\2212"; -} -.glyphicon-cloud:before { - content: "\2601"; -} -.glyphicon-envelope:before { - content: "\2709"; -} -.glyphicon-pencil:before { - content: "\270f"; + +h4, .h4 { + font-size: 1.5rem; } -.glyphicon-glass:before { - content: "\e001"; + +h5, .h5 { + font-size: 1.25rem; } -.glyphicon-music:before { - content: "\e002"; + +h6, .h6 { + font-size: 1rem; } -.glyphicon-search:before { - content: "\e003"; + +.lead { + font-size: 1.25rem; + font-weight: 300; } -.glyphicon-heart:before { - content: "\e005"; + +.display-1 { + font-size: 6rem; + font-weight: 300; + line-height: 1.2; } -.glyphicon-star:before { - content: "\e006"; + +.display-2 { + font-size: 5.5rem; + font-weight: 300; + line-height: 1.2; } -.glyphicon-star-empty:before { - content: "\e007"; + +.display-3 { + font-size: 4.5rem; + font-weight: 300; + line-height: 1.2; } -.glyphicon-user:before { - content: "\e008"; + +.display-4 { + font-size: 3.5rem; + font-weight: 300; + line-height: 1.2; } -.glyphicon-film:before { - content: "\e009"; + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); } -.glyphicon-th-large:before { - content: "\e010"; + +small, +.small { + font-size: 80%; + font-weight: 400; } -.glyphicon-th:before { - content: "\e011"; + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; } -.glyphicon-th-list:before { - content: "\e012"; + +.list-unstyled { + padding-left: 0; + list-style: none; } -.glyphicon-ok:before { - content: "\e013"; + +.list-inline { + padding-left: 0; + list-style: none; } -.glyphicon-remove:before { - content: "\e014"; + +.list-inline-item { + display: inline-block; } -.glyphicon-zoom-in:before { - content: "\e015"; + +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; } -.glyphicon-zoom-out:before { - content: "\e016"; + +.initialism { + font-size: 90%; + text-transform: uppercase; } -.glyphicon-off:before { - content: "\e017"; + +.blockquote { + margin-bottom: 1rem; + font-size: 1.25rem; } -.glyphicon-signal:before { - content: "\e018"; + +.blockquote-footer { + display: block; + font-size: 80%; + color: #6c757d; } -.glyphicon-cog:before { - content: "\e019"; + +.blockquote-footer::before { + content: "\2014\00A0"; } -.glyphicon-trash:before { - content: "\e020"; + +.img-fluid { + max-width: 100%; + height: auto; } -.glyphicon-home:before { - content: "\e021"; + +.img-thumbnail { + padding: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + max-width: 100%; + height: auto; } -.glyphicon-file:before { - content: "\e022"; + +.figure { + display: inline-block; } -.glyphicon-time:before { - content: "\e023"; + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; } -.glyphicon-road:before { - content: "\e024"; + +.figure-caption { + font-size: 90%; + color: #6c757d; } -.glyphicon-download-alt:before { - content: "\e025"; + +code { + font-size: 87.5%; + color: #e83e8c; + word-wrap: break-word; } -.glyphicon-download:before { - content: "\e026"; + +a > code { + color: inherit; } -.glyphicon-upload:before { - content: "\e027"; + +kbd { + padding: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.2rem; } -.glyphicon-inbox:before { - content: "\e028"; + +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; } -.glyphicon-play-circle:before { - content: "\e029"; + +pre { + display: block; + font-size: 87.5%; + color: #212529; } -.glyphicon-repeat:before { - content: "\e030"; + +pre code { + font-size: inherit; + color: inherit; + word-break: normal; } -.glyphicon-refresh:before { - content: "\e031"; + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; } -.glyphicon-list-alt:before { - content: "\e032"; + +.container, +.container-fluid, +.container-sm, +.container-md, +.container-lg, +.container-xl { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; } -.glyphicon-lock:before { - content: "\e033"; + +@media (min-width: 576px) { + .container, .container-sm { + max-width: 540px; + } } -.glyphicon-flag:before { - content: "\e034"; + +@media (min-width: 768px) { + .container, .container-sm, .container-md { + max-width: 720px; + } } -.glyphicon-headphones:before { - content: "\e035"; + +@media (min-width: 992px) { + .container, .container-sm, .container-md, .container-lg { + max-width: 960px; + } } -.glyphicon-volume-off:before { - content: "\e036"; + +@media (min-width: 1200px) { + .container, .container-sm, .container-md, .container-lg, .container-xl { + max-width: 1140px; + } } -.glyphicon-volume-down:before { - content: "\e037"; + +.row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; } -.glyphicon-volume-up:before { - content: "\e038"; + +.no-gutters { + margin-right: 0; + margin-left: 0; } -.glyphicon-qrcode:before { - content: "\e039"; + +.no-gutters > .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; } -.glyphicon-barcode:before { - content: "\e040"; + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, +.col-xl-auto { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; } -.glyphicon-tag:before { - content: "\e041"; + +.col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; } -.glyphicon-tags:before { - content: "\e042"; + +.row-cols-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; } -.glyphicon-book:before { - content: "\e043"; + +.row-cols-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; } -.glyphicon-bookmark:before { - content: "\e044"; + +.row-cols-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; } -.glyphicon-print:before { - content: "\e045"; + +.row-cols-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; } -.glyphicon-camera:before { - content: "\e046"; + +.row-cols-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; } -.glyphicon-font:before { - content: "\e047"; + +.row-cols-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; } -.glyphicon-bold:before { - content: "\e048"; + +.col-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; } -.glyphicon-italic:before { - content: "\e049"; + +.col-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; } -.glyphicon-text-height:before { - content: "\e050"; + +.col-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; } -.glyphicon-text-width:before { - content: "\e051"; + +.col-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; } -.glyphicon-align-left:before { - content: "\e052"; + +.col-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; } -.glyphicon-align-center:before { - content: "\e053"; + +.col-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; } -.glyphicon-align-right:before { - content: "\e054"; + +.col-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; } -.glyphicon-align-justify:before { - content: "\e055"; + +.col-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; } -.glyphicon-list:before { - content: "\e056"; + +.col-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; } -.glyphicon-indent-left:before { - content: "\e057"; + +.col-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; } -.glyphicon-indent-right:before { - content: "\e058"; + +.col-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; } -.glyphicon-facetime-video:before { - content: "\e059"; + +.col-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; } -.glyphicon-picture:before { - content: "\e060"; + +.col-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; } -.glyphicon-map-marker:before { - content: "\e062"; + +.order-first { + -ms-flex-order: -1; + order: -1; } -.glyphicon-adjust:before { - content: "\e063"; + +.order-last { + -ms-flex-order: 13; + order: 13; } -.glyphicon-tint:before { - content: "\e064"; + +.order-0 { + -ms-flex-order: 0; + order: 0; } -.glyphicon-edit:before { - content: "\e065"; + +.order-1 { + -ms-flex-order: 1; + order: 1; } -.glyphicon-share:before { - content: "\e066"; + +.order-2 { + -ms-flex-order: 2; + order: 2; } -.glyphicon-check:before { - content: "\e067"; + +.order-3 { + -ms-flex-order: 3; + order: 3; } -.glyphicon-move:before { - content: "\e068"; + +.order-4 { + -ms-flex-order: 4; + order: 4; } -.glyphicon-step-backward:before { - content: "\e069"; + +.order-5 { + -ms-flex-order: 5; + order: 5; } -.glyphicon-fast-backward:before { - content: "\e070"; + +.order-6 { + -ms-flex-order: 6; + order: 6; } -.glyphicon-backward:before { - content: "\e071"; + +.order-7 { + -ms-flex-order: 7; + order: 7; } -.glyphicon-play:before { - content: "\e072"; + +.order-8 { + -ms-flex-order: 8; + order: 8; } -.glyphicon-pause:before { - content: "\e073"; + +.order-9 { + -ms-flex-order: 9; + order: 9; } -.glyphicon-stop:before { - content: "\e074"; + +.order-10 { + -ms-flex-order: 10; + order: 10; } -.glyphicon-forward:before { - content: "\e075"; + +.order-11 { + -ms-flex-order: 11; + order: 11; } -.glyphicon-fast-forward:before { - content: "\e076"; + +.order-12 { + -ms-flex-order: 12; + order: 12; } -.glyphicon-step-forward:before { - content: "\e077"; + +.offset-1 { + margin-left: 8.333333%; } -.glyphicon-eject:before { - content: "\e078"; + +.offset-2 { + margin-left: 16.666667%; } -.glyphicon-chevron-left:before { - content: "\e079"; + +.offset-3 { + margin-left: 25%; } -.glyphicon-chevron-right:before { - content: "\e080"; + +.offset-4 { + margin-left: 33.333333%; } -.glyphicon-plus-sign:before { - content: "\e081"; + +.offset-5 { + margin-left: 41.666667%; } -.glyphicon-minus-sign:before { - content: "\e082"; + +.offset-6 { + margin-left: 50%; } -.glyphicon-remove-sign:before { - content: "\e083"; + +.offset-7 { + margin-left: 58.333333%; } -.glyphicon-ok-sign:before { - content: "\e084"; + +.offset-8 { + margin-left: 66.666667%; } -.glyphicon-question-sign:before { - content: "\e085"; + +.offset-9 { + margin-left: 75%; } -.glyphicon-info-sign:before { - content: "\e086"; + +.offset-10 { + margin-left: 83.333333%; } -.glyphicon-screenshot:before { - content: "\e087"; + +.offset-11 { + margin-left: 91.666667%; } -.glyphicon-remove-circle:before { - content: "\e088"; + +@media (min-width: 576px) { + .col-sm { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-sm-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-sm-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-sm-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-sm-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-sm-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-sm-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-sm-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-sm-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-sm-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-sm-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-sm-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-sm-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-sm-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-sm-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-first { + -ms-flex-order: -1; + order: -1; + } + .order-sm-last { + -ms-flex-order: 13; + order: 13; + } + .order-sm-0 { + -ms-flex-order: 0; + order: 0; + } + .order-sm-1 { + -ms-flex-order: 1; + order: 1; + } + .order-sm-2 { + -ms-flex-order: 2; + order: 2; + } + .order-sm-3 { + -ms-flex-order: 3; + order: 3; + } + .order-sm-4 { + -ms-flex-order: 4; + order: 4; + } + .order-sm-5 { + -ms-flex-order: 5; + order: 5; + } + .order-sm-6 { + -ms-flex-order: 6; + order: 6; + } + .order-sm-7 { + -ms-flex-order: 7; + order: 7; + } + .order-sm-8 { + -ms-flex-order: 8; + order: 8; + } + .order-sm-9 { + -ms-flex-order: 9; + order: 9; + } + .order-sm-10 { + -ms-flex-order: 10; + order: 10; + } + .order-sm-11 { + -ms-flex-order: 11; + order: 11; + } + .order-sm-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.333333%; + } + .offset-sm-2 { + margin-left: 16.666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.333333%; + } + .offset-sm-5 { + margin-left: 41.666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.333333%; + } + .offset-sm-8 { + margin-left: 66.666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.333333%; + } + .offset-sm-11 { + margin-left: 91.666667%; + } } -.glyphicon-ok-circle:before { - content: "\e089"; -} -.glyphicon-ban-circle:before { - content: "\e090"; + +@media (min-width: 768px) { + .col-md { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-md-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-md-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-md-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-md-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-md-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-md-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-md-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-md-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-md-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-md-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-md-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-md-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-md-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-md-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-md-first { + -ms-flex-order: -1; + order: -1; + } + .order-md-last { + -ms-flex-order: 13; + order: 13; + } + .order-md-0 { + -ms-flex-order: 0; + order: 0; + } + .order-md-1 { + -ms-flex-order: 1; + order: 1; + } + .order-md-2 { + -ms-flex-order: 2; + order: 2; + } + .order-md-3 { + -ms-flex-order: 3; + order: 3; + } + .order-md-4 { + -ms-flex-order: 4; + order: 4; + } + .order-md-5 { + -ms-flex-order: 5; + order: 5; + } + .order-md-6 { + -ms-flex-order: 6; + order: 6; + } + .order-md-7 { + -ms-flex-order: 7; + order: 7; + } + .order-md-8 { + -ms-flex-order: 8; + order: 8; + } + .order-md-9 { + -ms-flex-order: 9; + order: 9; + } + .order-md-10 { + -ms-flex-order: 10; + order: 10; + } + .order-md-11 { + -ms-flex-order: 11; + order: 11; + } + .order-md-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.333333%; + } + .offset-md-2 { + margin-left: 16.666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.333333%; + } + .offset-md-5 { + margin-left: 41.666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.333333%; + } + .offset-md-8 { + margin-left: 66.666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.333333%; + } + .offset-md-11 { + margin-left: 91.666667%; + } } -.glyphicon-arrow-left:before { - content: "\e091"; + +@media (min-width: 992px) { + .col-lg { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-lg-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-lg-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-lg-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-lg-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-lg-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-lg-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-lg-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-lg-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-lg-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-lg-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-lg-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-lg-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-lg-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-lg-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-lg-first { + -ms-flex-order: -1; + order: -1; + } + .order-lg-last { + -ms-flex-order: 13; + order: 13; + } + .order-lg-0 { + -ms-flex-order: 0; + order: 0; + } + .order-lg-1 { + -ms-flex-order: 1; + order: 1; + } + .order-lg-2 { + -ms-flex-order: 2; + order: 2; + } + .order-lg-3 { + -ms-flex-order: 3; + order: 3; + } + .order-lg-4 { + -ms-flex-order: 4; + order: 4; + } + .order-lg-5 { + -ms-flex-order: 5; + order: 5; + } + .order-lg-6 { + -ms-flex-order: 6; + order: 6; + } + .order-lg-7 { + -ms-flex-order: 7; + order: 7; + } + .order-lg-8 { + -ms-flex-order: 8; + order: 8; + } + .order-lg-9 { + -ms-flex-order: 9; + order: 9; + } + .order-lg-10 { + -ms-flex-order: 10; + order: 10; + } + .order-lg-11 { + -ms-flex-order: 11; + order: 11; + } + .order-lg-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.333333%; + } + .offset-lg-2 { + margin-left: 16.666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.333333%; + } + .offset-lg-5 { + margin-left: 41.666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.333333%; + } + .offset-lg-8 { + margin-left: 66.666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.333333%; + } + .offset-lg-11 { + margin-left: 91.666667%; + } } -.glyphicon-arrow-right:before { - content: "\e092"; + +@media (min-width: 1200px) { + .col-xl { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-xl-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-xl-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-xl-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-xl-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-xl-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-xl-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-xl-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-xl-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-xl-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-xl-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-xl-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-xl-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-xl-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-xl-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-xl-first { + -ms-flex-order: -1; + order: -1; + } + .order-xl-last { + -ms-flex-order: 13; + order: 13; + } + .order-xl-0 { + -ms-flex-order: 0; + order: 0; + } + .order-xl-1 { + -ms-flex-order: 1; + order: 1; + } + .order-xl-2 { + -ms-flex-order: 2; + order: 2; + } + .order-xl-3 { + -ms-flex-order: 3; + order: 3; + } + .order-xl-4 { + -ms-flex-order: 4; + order: 4; + } + .order-xl-5 { + -ms-flex-order: 5; + order: 5; + } + .order-xl-6 { + -ms-flex-order: 6; + order: 6; + } + .order-xl-7 { + -ms-flex-order: 7; + order: 7; + } + .order-xl-8 { + -ms-flex-order: 8; + order: 8; + } + .order-xl-9 { + -ms-flex-order: 9; + order: 9; + } + .order-xl-10 { + -ms-flex-order: 10; + order: 10; + } + .order-xl-11 { + -ms-flex-order: 11; + order: 11; + } + .order-xl-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.333333%; + } + .offset-xl-2 { + margin-left: 16.666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.333333%; + } + .offset-xl-5 { + margin-left: 41.666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.333333%; + } + .offset-xl-8 { + margin-left: 66.666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.333333%; + } + .offset-xl-11 { + margin-left: 91.666667%; + } } -.glyphicon-arrow-up:before { - content: "\e093"; + +.table { + width: 100%; + margin-bottom: 1rem; + color: #212529; } -.glyphicon-arrow-down:before { - content: "\e094"; + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #dee2e6; } -.glyphicon-share-alt:before { - content: "\e095"; + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; } -.glyphicon-resize-full:before { - content: "\e096"; + +.table tbody + tbody { + border-top: 2px solid #dee2e6; } -.glyphicon-resize-small:before { - content: "\e097"; + +.table-sm th, +.table-sm td { + padding: 0.3rem; } -.glyphicon-exclamation-sign:before { - content: "\e101"; + +.table-bordered { + border: 1px solid #dee2e6; } -.glyphicon-gift:before { - content: "\e102"; + +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; } -.glyphicon-leaf:before { - content: "\e103"; + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; } -.glyphicon-fire:before { - content: "\e104"; + +.table-borderless th, +.table-borderless td, +.table-borderless thead th, +.table-borderless tbody + tbody { + border: 0; } -.glyphicon-eye-open:before { - content: "\e105"; + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); } -.glyphicon-eye-close:before { - content: "\e106"; + +.table-hover tbody tr:hover { + color: #212529; + background-color: rgba(0, 0, 0, 0.075); } -.glyphicon-warning-sign:before { - content: "\e107"; + +.table-primary, +.table-primary > th, +.table-primary > td { + background-color: #b8daff; } -.glyphicon-plane:before { - content: "\e108"; + +.table-primary th, +.table-primary td, +.table-primary thead th, +.table-primary tbody + tbody { + border-color: #7abaff; } -.glyphicon-calendar:before { - content: "\e109"; + +.table-hover .table-primary:hover { + background-color: #9fcdff; } -.glyphicon-random:before { - content: "\e110"; + +.table-hover .table-primary:hover > td, +.table-hover .table-primary:hover > th { + background-color: #9fcdff; } -.glyphicon-comment:before { - content: "\e111"; + +.table-secondary, +.table-secondary > th, +.table-secondary > td { + background-color: #d6d8db; } -.glyphicon-magnet:before { - content: "\e112"; + +.table-secondary th, +.table-secondary td, +.table-secondary thead th, +.table-secondary tbody + tbody { + border-color: #b3b7bb; } -.glyphicon-chevron-up:before { - content: "\e113"; + +.table-hover .table-secondary:hover { + background-color: #c8cbcf; } -.glyphicon-chevron-down:before { - content: "\e114"; + +.table-hover .table-secondary:hover > td, +.table-hover .table-secondary:hover > th { + background-color: #c8cbcf; } -.glyphicon-retweet:before { - content: "\e115"; + +.table-success, +.table-success > th, +.table-success > td { + background-color: #c3e6cb; } -.glyphicon-shopping-cart:before { - content: "\e116"; + +.table-success th, +.table-success td, +.table-success thead th, +.table-success tbody + tbody { + border-color: #8fd19e; } -.glyphicon-folder-close:before { - content: "\e117"; + +.table-hover .table-success:hover { + background-color: #b1dfbb; } -.glyphicon-folder-open:before { - content: "\e118"; + +.table-hover .table-success:hover > td, +.table-hover .table-success:hover > th { + background-color: #b1dfbb; } -.glyphicon-resize-vertical:before { - content: "\e119"; + +.table-info, +.table-info > th, +.table-info > td { + background-color: #bee5eb; } -.glyphicon-resize-horizontal:before { - content: "\e120"; + +.table-info th, +.table-info td, +.table-info thead th, +.table-info tbody + tbody { + border-color: #86cfda; } -.glyphicon-hdd:before { - content: "\e121"; + +.table-hover .table-info:hover { + background-color: #abdde5; } -.glyphicon-bullhorn:before { - content: "\e122"; + +.table-hover .table-info:hover > td, +.table-hover .table-info:hover > th { + background-color: #abdde5; } -.glyphicon-bell:before { - content: "\e123"; + +.table-warning, +.table-warning > th, +.table-warning > td { + background-color: #ffeeba; } -.glyphicon-certificate:before { - content: "\e124"; + +.table-warning th, +.table-warning td, +.table-warning thead th, +.table-warning tbody + tbody { + border-color: #ffdf7e; } -.glyphicon-thumbs-up:before { - content: "\e125"; + +.table-hover .table-warning:hover { + background-color: #ffe8a1; } -.glyphicon-thumbs-down:before { - content: "\e126"; + +.table-hover .table-warning:hover > td, +.table-hover .table-warning:hover > th { + background-color: #ffe8a1; } -.glyphicon-hand-right:before { - content: "\e127"; + +.table-danger, +.table-danger > th, +.table-danger > td { + background-color: #f5c6cb; } -.glyphicon-hand-left:before { - content: "\e128"; + +.table-danger th, +.table-danger td, +.table-danger thead th, +.table-danger tbody + tbody { + border-color: #ed969e; } -.glyphicon-hand-up:before { - content: "\e129"; + +.table-hover .table-danger:hover { + background-color: #f1b0b7; } -.glyphicon-hand-down:before { - content: "\e130"; + +.table-hover .table-danger:hover > td, +.table-hover .table-danger:hover > th { + background-color: #f1b0b7; } -.glyphicon-circle-arrow-right:before { - content: "\e131"; + +.table-light, +.table-light > th, +.table-light > td { + background-color: #fdfdfe; } -.glyphicon-circle-arrow-left:before { - content: "\e132"; + +.table-light th, +.table-light td, +.table-light thead th, +.table-light tbody + tbody { + border-color: #fbfcfc; } -.glyphicon-circle-arrow-up:before { - content: "\e133"; + +.table-hover .table-light:hover { + background-color: #ececf6; } -.glyphicon-circle-arrow-down:before { - content: "\e134"; + +.table-hover .table-light:hover > td, +.table-hover .table-light:hover > th { + background-color: #ececf6; } -.glyphicon-globe:before { - content: "\e135"; + +.table-dark, +.table-dark > th, +.table-dark > td { + background-color: #c6c8ca; } -.glyphicon-wrench:before { - content: "\e136"; + +.table-dark th, +.table-dark td, +.table-dark thead th, +.table-dark tbody + tbody { + border-color: #95999c; } -.glyphicon-tasks:before { - content: "\e137"; + +.table-hover .table-dark:hover { + background-color: #b9bbbe; } -.glyphicon-filter:before { - content: "\e138"; + +.table-hover .table-dark:hover > td, +.table-hover .table-dark:hover > th { + background-color: #b9bbbe; } -.glyphicon-briefcase:before { - content: "\e139"; + +.table-active, +.table-active > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); } -.glyphicon-fullscreen:before { - content: "\e140"; + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); } -.glyphicon-dashboard:before { - content: "\e141"; + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: rgba(0, 0, 0, 0.075); } -.glyphicon-paperclip:before { - content: "\e142"; + +.table .thead-dark th { + color: #fff; + background-color: #343a40; + border-color: #454d55; } -.glyphicon-heart-empty:before { - content: "\e143"; + +.table .thead-light th { + color: #495057; + background-color: #e9ecef; + border-color: #dee2e6; } -.glyphicon-link:before { - content: "\e144"; + +.table-dark { + color: #fff; + background-color: #343a40; } -.glyphicon-phone:before { - content: "\e145"; + +.table-dark th, +.table-dark td, +.table-dark thead th { + border-color: #454d55; } -.glyphicon-pushpin:before { - content: "\e146"; + +.table-dark.table-bordered { + border: 0; } -.glyphicon-usd:before { - content: "\e148"; + +.table-dark.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.05); } -.glyphicon-gbp:before { - content: "\e149"; + +.table-dark.table-hover tbody tr:hover { + color: #fff; + background-color: rgba(255, 255, 255, 0.075); } -.glyphicon-sort:before { - content: "\e150"; + +@media (max-width: 575.98px) { + .table-responsive-sm { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-sm > .table-bordered { + border: 0; + } } -.glyphicon-sort-by-alphabet:before { - content: "\e151"; + +@media (max-width: 767.98px) { + .table-responsive-md { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-md > .table-bordered { + border: 0; + } } -.glyphicon-sort-by-alphabet-alt:before { - content: "\e152"; + +@media (max-width: 991.98px) { + .table-responsive-lg { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-lg > .table-bordered { + border: 0; + } } -.glyphicon-sort-by-order:before { - content: "\e153"; + +@media (max-width: 1199.98px) { + .table-responsive-xl { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .table-responsive-xl > .table-bordered { + border: 0; + } } -.glyphicon-sort-by-order-alt:before { - content: "\e154"; + +.table-responsive { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; } -.glyphicon-sort-by-attributes:before { - content: "\e155"; + +.table-responsive > .table-bordered { + border: 0; } -.glyphicon-sort-by-attributes-alt:before { - content: "\e156"; + +.form-control { + display: block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } -.glyphicon-unchecked:before { - content: "\e157"; + +@media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } } -.glyphicon-expand:before { - content: "\e158"; + +.form-control::-ms-expand { + background-color: transparent; + border: 0; } -.glyphicon-collapse-down:before { - content: "\e159"; + +.form-control:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #495057; } -.glyphicon-collapse-up:before { - content: "\e160"; + +.form-control:focus { + color: #495057; + background-color: #fff; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } -.glyphicon-log-in:before { - content: "\e161"; + +.form-control::-webkit-input-placeholder { + color: #6c757d; + opacity: 1; } -.glyphicon-flash:before { - content: "\e162"; + +.form-control::-moz-placeholder { + color: #6c757d; + opacity: 1; } -.glyphicon-log-out:before { - content: "\e163"; + +.form-control:-ms-input-placeholder { + color: #6c757d; + opacity: 1; } -.glyphicon-new-window:before { - content: "\e164"; + +.form-control::-ms-input-placeholder { + color: #6c757d; + opacity: 1; } -.glyphicon-record:before { - content: "\e165"; + +.form-control::placeholder { + color: #6c757d; + opacity: 1; } -.glyphicon-save:before { - content: "\e166"; + +.form-control:disabled, .form-control[readonly] { + background-color: #e9ecef; + opacity: 1; } -.glyphicon-open:before { - content: "\e167"; + +input[type="date"].form-control, +input[type="time"].form-control, +input[type="datetime-local"].form-control, +input[type="month"].form-control { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } -.glyphicon-saved:before { - content: "\e168"; + +select.form-control:focus::-ms-value { + color: #495057; + background-color: #fff; } -.glyphicon-import:before { - content: "\e169"; + +.form-control-file, +.form-control-range { + display: block; + width: 100%; } -.glyphicon-export:before { - content: "\e170"; + +.col-form-label { + padding-top: calc(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; } -.glyphicon-send:before { - content: "\e171"; + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.25rem; + line-height: 1.5; } -.glyphicon-floppy-disk:before { - content: "\e172"; + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.875rem; + line-height: 1.5; } -.glyphicon-floppy-saved:before { - content: "\e173"; + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.375rem 0; + margin-bottom: 0; + font-size: 1rem; + line-height: 1.5; + color: #212529; + background-color: transparent; + border: solid transparent; + border-width: 1px 0; } -.glyphicon-floppy-remove:before { - content: "\e174"; + +.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; } -.glyphicon-floppy-save:before { - content: "\e175"; + +.form-control-sm { + height: calc(1.5em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; } -.glyphicon-floppy-open:before { - content: "\e176"; + +.form-control-lg { + height: calc(1.5em + 1rem + 2px); + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; } -.glyphicon-credit-card:before { - content: "\e177"; + +select.form-control[size], select.form-control[multiple] { + height: auto; } -.glyphicon-transfer:before { - content: "\e178"; + +textarea.form-control { + height: auto; } -.glyphicon-cutlery:before { - content: "\e179"; + +.form-group { + margin-bottom: 1rem; } -.glyphicon-header:before { - content: "\e180"; + +.form-text { + display: block; + margin-top: 0.25rem; } -.glyphicon-compressed:before { - content: "\e181"; + +.form-row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -5px; + margin-left: -5px; } -.glyphicon-earphone:before { - content: "\e182"; + +.form-row > .col, +.form-row > [class*="col-"] { + padding-right: 5px; + padding-left: 5px; } -.glyphicon-phone-alt:before { - content: "\e183"; + +.form-check { + position: relative; + display: block; + padding-left: 1.25rem; } -.glyphicon-tower:before { - content: "\e184"; + +.form-check-input { + position: absolute; + margin-top: 0.3rem; + margin-left: -1.25rem; } -.glyphicon-stats:before { - content: "\e185"; + +.form-check-input[disabled] ~ .form-check-label, +.form-check-input:disabled ~ .form-check-label { + color: #6c757d; } -.glyphicon-sd-video:before { - content: "\e186"; + +.form-check-label { + margin-bottom: 0; } -.glyphicon-hd-video:before { - content: "\e187"; + +.form-check-inline { + display: -ms-inline-flexbox; + display: inline-flex; + -ms-flex-align: center; + align-items: center; + padding-left: 0; + margin-right: 0.75rem; } -.glyphicon-subtitles:before { - content: "\e188"; + +.form-check-inline .form-check-input { + position: static; + margin-top: 0; + margin-right: 0.3125rem; + margin-left: 0; } -.glyphicon-sound-stereo:before { - content: "\e189"; + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #28a745; } -.glyphicon-sound-dolby:before { - content: "\e190"; + +.valid-tooltip { + position: absolute; + top: 100%; + left: 0; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(40, 167, 69, 0.9); + border-radius: 0.25rem; } -.glyphicon-sound-5-1:before { - content: "\e191"; + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; } -.glyphicon-sound-6-1:before { - content: "\e192"; + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: #28a745; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } -.glyphicon-sound-7-1:before { - content: "\e193"; + +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } -.glyphicon-copyright-mark:before { - content: "\e194"; + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); } -.glyphicon-registration-mark:before { - content: "\e195"; + +.was-validated .custom-select:valid, .custom-select.is-valid { + border-color: #28a745; + padding-right: calc(0.75em + 2.3125rem); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } -.glyphicon-cloud-download:before { - content: "\e197"; + +.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } -.glyphicon-cloud-upload:before { - content: "\e198"; + +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #28a745; } -.glyphicon-tree-conifer:before { - content: "\e199"; + +.was-validated .form-check-input:valid ~ .valid-feedback, +.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, +.form-check-input.is-valid ~ .valid-tooltip { + display: block; } -.glyphicon-tree-deciduous:before { - content: "\e200"; + +.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { + color: #28a745; } -.glyphicon-cd:before { - content: "\e201"; + +.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { + border-color: #28a745; } -.glyphicon-save-file:before { - content: "\e202"; + +.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { + border-color: #34ce57; + background-color: #34ce57; } -.glyphicon-open-file:before { - content: "\e203"; + +.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } -.glyphicon-level-up:before { - content: "\e204"; + +.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #28a745; } -.glyphicon-copy:before { - content: "\e205"; + +.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { + border-color: #28a745; } -.glyphicon-paste:before { - content: "\e206"; + +.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } -.glyphicon-alert:before { - content: "\e209"; + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #dc3545; } -.glyphicon-equalizer:before { - content: "\e210"; + +.invalid-tooltip { + position: absolute; + top: 100%; + left: 0; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(220, 53, 69, 0.9); + border-radius: 0.25rem; } -.glyphicon-king:before { - content: "\e211"; + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; } -.glyphicon-queen:before { - content: "\e212"; + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: #dc3545; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } -.glyphicon-pawn:before { - content: "\e213"; + +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } -.glyphicon-bishop:before { - content: "\e214"; + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); } -.glyphicon-knight:before { - content: "\e215"; + +.was-validated .custom-select:invalid, .custom-select.is-invalid { + border-color: #dc3545; + padding-right: calc(0.75em + 2.3125rem); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } -.glyphicon-baby-formula:before { - content: "\e216"; + +.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } -.glyphicon-tent:before { - content: "\26fa"; + +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #dc3545; } -.glyphicon-blackboard:before { - content: "\e218"; + +.was-validated .form-check-input:invalid ~ .invalid-feedback, +.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, +.form-check-input.is-invalid ~ .invalid-tooltip { + display: block; } -.glyphicon-bed:before { - content: "\e219"; + +.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { + color: #dc3545; } -.glyphicon-apple:before { - content: "\f8ff"; + +.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { + border-color: #dc3545; } -.glyphicon-erase:before { - content: "\e221"; + +.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { + border-color: #e4606d; + background-color: #e4606d; } -.glyphicon-hourglass:before { - content: "\231b"; + +.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } -.glyphicon-lamp:before { - content: "\e223"; + +.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #dc3545; } -.glyphicon-duplicate:before { - content: "\e224"; + +.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { + border-color: #dc3545; } -.glyphicon-piggy-bank:before { - content: "\e225"; + +.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } -.glyphicon-scissors:before { - content: "\e226"; + +.form-inline { + display: -ms-flexbox; + display: flex; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -ms-flex-align: center; + align-items: center; } -.glyphicon-bitcoin:before { - content: "\e227"; + +.form-inline .form-check { + width: 100%; } -.glyphicon-btc:before { - content: "\e227"; + +@media (min-width: 576px) { + .form-inline label { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + margin-bottom: 0; + } + .form-inline .form-group { + display: -ms-flexbox; + display: flex; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -ms-flex-align: center; + align-items: center; + margin-bottom: 0; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-plaintext { + display: inline-block; + } + .form-inline .input-group, + .form-inline .custom-select { + width: auto; + } + .form-inline .form-check { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + width: auto; + padding-left: 0; + } + .form-inline .form-check-input { + position: relative; + -ms-flex-negative: 0; + flex-shrink: 0; + margin-top: 0; + margin-right: 0.25rem; + margin-left: 0; + } + .form-inline .custom-control { + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + } + .form-inline .custom-control-label { + margin-bottom: 0; + } } -.glyphicon-xbt:before { - content: "\e227"; + +.btn { + display: inline-block; + font-weight: 400; + color: #212529; + text-align: center; + vertical-align: middle; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } -.glyphicon-yen:before { - content: "\00a5"; + +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } } -.glyphicon-jpy:before { - content: "\00a5"; + +.btn:hover { + color: #212529; + text-decoration: none; } -.glyphicon-ruble:before { - content: "\20bd"; + +.btn:focus, .btn.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } -.glyphicon-rub:before { - content: "\20bd"; + +.btn.disabled, .btn:disabled { + opacity: 0.65; } -.glyphicon-scale:before { - content: "\e230"; + +.btn:not(:disabled):not(.disabled) { + cursor: pointer; } -.glyphicon-ice-lolly:before { - content: "\e231"; + +a.btn.disabled, +fieldset:disabled a.btn { + pointer-events: none; } -.glyphicon-ice-lolly-tasted:before { - content: "\e232"; + +.btn-primary { + color: #fff; + background-color: #007bff; + border-color: #007bff; } -.glyphicon-education:before { - content: "\e233"; + +.btn-primary:hover { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; } -.glyphicon-option-horizontal:before { - content: "\e234"; + +.btn-primary:focus, .btn-primary.focus { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; + box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); } -.glyphicon-option-vertical:before { - content: "\e235"; + +.btn-primary.disabled, .btn-primary:disabled { + color: #fff; + background-color: #007bff; + border-color: #007bff; } -.glyphicon-menu-hamburger:before { - content: "\e236"; + +.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, +.show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #0062cc; + border-color: #005cbf; } -.glyphicon-modal-window:before { - content: "\e237"; + +.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); } -.glyphicon-oil:before { - content: "\e238"; + +.btn-secondary { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; } -.glyphicon-grain:before { - content: "\e239"; + +.btn-secondary:hover { + color: #fff; + background-color: #5a6268; + border-color: #545b62; } -.glyphicon-sunglasses:before { - content: "\e240"; + +.btn-secondary:focus, .btn-secondary.focus { + color: #fff; + background-color: #5a6268; + border-color: #545b62; + box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); } -.glyphicon-text-size:before { - content: "\e241"; + +.btn-secondary.disabled, .btn-secondary:disabled { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; } -.glyphicon-text-color:before { - content: "\e242"; + +.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, +.show > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #545b62; + border-color: #4e555b; } -.glyphicon-text-background:before { - content: "\e243"; + +.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5); } -.glyphicon-object-align-top:before { - content: "\e244"; + +.btn-success { + color: #fff; + background-color: #28a745; + border-color: #28a745; } -.glyphicon-object-align-bottom:before { - content: "\e245"; + +.btn-success:hover { + color: #fff; + background-color: #218838; + border-color: #1e7e34; } -.glyphicon-object-align-horizontal:before { - content: "\e246"; + +.btn-success:focus, .btn-success.focus { + color: #fff; + background-color: #218838; + border-color: #1e7e34; + box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5); } -.glyphicon-object-align-left:before { - content: "\e247"; + +.btn-success.disabled, .btn-success:disabled { + color: #fff; + background-color: #28a745; + border-color: #28a745; } -.glyphicon-object-align-vertical:before { - content: "\e248"; + +.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active, +.show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #1e7e34; + border-color: #1c7430; } -.glyphicon-object-align-right:before { - content: "\e249"; + +.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5); } -.glyphicon-triangle-right:before { - content: "\e250"; + +.btn-info { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; } -.glyphicon-triangle-left:before { - content: "\e251"; + +.btn-info:hover { + color: #fff; + background-color: #138496; + border-color: #117a8b; } -.glyphicon-triangle-bottom:before { - content: "\e252"; + +.btn-info:focus, .btn-info.focus { + color: #fff; + background-color: #138496; + border-color: #117a8b; + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); } -.glyphicon-triangle-top:before { - content: "\e253"; + +.btn-info.disabled, .btn-info:disabled { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; } -.glyphicon-console:before { - content: "\e254"; + +.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active, +.show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #117a8b; + border-color: #10707f; } -.glyphicon-superscript:before { - content: "\e255"; + +.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); } -.glyphicon-subscript:before { - content: "\e256"; + +.btn-warning { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; } -.glyphicon-menu-left:before { - content: "\e257"; + +.btn-warning:hover { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; } -.glyphicon-menu-right:before { - content: "\e258"; + +.btn-warning:focus, .btn-warning.focus { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); } -.glyphicon-menu-down:before { - content: "\e259"; + +.btn-warning.disabled, .btn-warning:disabled { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; } -.glyphicon-menu-up:before { - content: "\e260"; + +.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active, +.show > .btn-warning.dropdown-toggle { + color: #212529; + background-color: #d39e00; + border-color: #c69500; } -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + +.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); } -*:before, -*:after { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + +.btn-danger { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; } -html { - font-size: 10px; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +.btn-danger:hover { + color: #fff; + background-color: #c82333; + border-color: #bd2130; } -body { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 1.42857143; - color: #333; - background-color: #fff; + +.btn-danger:focus, .btn-danger.focus { + color: #fff; + background-color: #c82333; + border-color: #bd2130; + box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5); } -input, -button, -select, -textarea { - font-family: inherit; - font-size: inherit; - line-height: inherit; + +.btn-danger.disabled, .btn-danger:disabled { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; } -a { - color: #337ab7; - text-decoration: none; + +.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active, +.show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #bd2130; + border-color: #b21f2d; } -a:hover, -a:focus { - color: #23527c; - text-decoration: underline; + +.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5); } -a:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; + +.btn-light { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; } -figure { - margin: 0; + +.btn-light:hover { + color: #212529; + background-color: #e2e6ea; + border-color: #dae0e5; } -img { - vertical-align: middle; + +.btn-light:focus, .btn-light.focus { + color: #212529; + background-color: #e2e6ea; + border-color: #dae0e5; + box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5); } -.img-responsive, -.thumbnail > img, -.thumbnail a > img, -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - display: block; - max-width: 100%; - height: auto; + +.btn-light.disabled, .btn-light:disabled { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; } -.img-rounded { - border-radius: 6px; + +.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active, +.show > .btn-light.dropdown-toggle { + color: #212529; + background-color: #dae0e5; + border-color: #d3d9df; } -.img-thumbnail { - display: inline-block; - max-width: 100%; - height: auto; - padding: 4px; - line-height: 1.42857143; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 4px; - -webkit-transition: all .2s ease-in-out; - -o-transition: all .2s ease-in-out; - transition: all .2s ease-in-out; + +.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5); } -.img-circle { - border-radius: 50%; + +.btn-dark { + color: #fff; + background-color: #343a40; + border-color: #343a40; } -hr { - margin-top: 20px; - margin-bottom: 20px; - border: 0; - border-top: 1px solid #eee; + +.btn-dark:hover { + color: #fff; + background-color: #23272b; + border-color: #1d2124; } -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; + +.btn-dark:focus, .btn-dark.focus { + color: #fff; + background-color: #23272b; + border-color: #1d2124; + box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); } -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; + +.btn-dark.disabled, .btn-dark:disabled { + color: #fff; + background-color: #343a40; + border-color: #343a40; } -[role="button"] { - cursor: pointer; + +.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, +.show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #1d2124; + border-color: #171a1d; } -h1, -h2, -h3, -h4, -h5, -h6, -.h1, -.h2, -.h3, -.h4, -.h5, -.h6 { - font-family: inherit; - font-weight: 500; - line-height: 1.1; - color: inherit; + +.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5); } -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small, -.h1 small, -.h2 small, -.h3 small, -.h4 small, -.h5 small, -.h6 small, -h1 .small, -h2 .small, -h3 .small, -h4 .small, -h5 .small, -h6 .small, -.h1 .small, -.h2 .small, -.h3 .small, -.h4 .small, -.h5 .small, -.h6 .small { - font-weight: normal; - line-height: 1; - color: #777; -} -h1, -.h1, -h2, -.h2, -h3, -.h3 { - margin-top: 20px; - margin-bottom: 10px; -} -h1 small, -.h1 small, -h2 small, -.h2 small, -h3 small, -.h3 small, -h1 .small, -.h1 .small, -h2 .small, -.h2 .small, -h3 .small, -.h3 .small { - font-size: 65%; -} -h4, -.h4, -h5, -.h5, -h6, -.h6 { - margin-top: 10px; - margin-bottom: 10px; -} -h4 small, -.h4 small, -h5 small, -.h5 small, -h6 small, -.h6 small, -h4 .small, -.h4 .small, -h5 .small, -.h5 .small, -h6 .small, -.h6 .small { - font-size: 75%; + +.btn-outline-primary { + color: #007bff; + border-color: #007bff; } -h1, -.h1 { - font-size: 36px; + +.btn-outline-primary:hover { + color: #fff; + background-color: #007bff; + border-color: #007bff; } -h2, -.h2 { - font-size: 30px; + +.btn-outline-primary:focus, .btn-outline-primary.focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } -h3, -.h3 { - font-size: 24px; + +.btn-outline-primary.disabled, .btn-outline-primary:disabled { + color: #007bff; + background-color: transparent; } -h4, -.h4 { - font-size: 18px; + +.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, +.show > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #007bff; + border-color: #007bff; } -h5, -.h5 { - font-size: 14px; + +.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } -h6, -.h6 { - font-size: 12px; + +.btn-outline-secondary { + color: #6c757d; + border-color: #6c757d; } -p { - margin: 0 0 10px; + +.btn-outline-secondary:hover { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; } -.lead { - margin-bottom: 20px; - font-size: 16px; - font-weight: 300; - line-height: 1.4; + +.btn-outline-secondary:focus, .btn-outline-secondary.focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } -@media (min-width: 768px) { - .lead { - font-size: 21px; - } + +.btn-outline-secondary.disabled, .btn-outline-secondary:disabled { + color: #6c757d; + background-color: transparent; } -small, -.small { - font-size: 85%; + +.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, +.show > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; } -mark, -.mark { - padding: .2em; - background-color: #fcf8e3; + +.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } -.text-left { - text-align: left; + +.btn-outline-success { + color: #28a745; + border-color: #28a745; } -.text-right { - text-align: right; + +.btn-outline-success:hover { + color: #fff; + background-color: #28a745; + border-color: #28a745; } -.text-center { - text-align: center; + +.btn-outline-success:focus, .btn-outline-success.focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } -.text-justify { - text-align: justify; + +.btn-outline-success.disabled, .btn-outline-success:disabled { + color: #28a745; + background-color: transparent; } -.text-nowrap { - white-space: nowrap; + +.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active, +.show > .btn-outline-success.dropdown-toggle { + color: #fff; + background-color: #28a745; + border-color: #28a745; } -.text-lowercase { - text-transform: lowercase; + +.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } -.text-uppercase { - text-transform: uppercase; + +.btn-outline-info { + color: #17a2b8; + border-color: #17a2b8; } -.text-capitalize { - text-transform: capitalize; + +.btn-outline-info:hover { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; } -.text-muted { - color: #777; + +.btn-outline-info:focus, .btn-outline-info.focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } -.text-primary { - color: #337ab7; + +.btn-outline-info.disabled, .btn-outline-info:disabled { + color: #17a2b8; + background-color: transparent; } -a.text-primary:hover, -a.text-primary:focus { - color: #286090; + +.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active, +.show > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; } -.text-success { - color: #3c763d; + +.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } -a.text-success:hover, -a.text-success:focus { - color: #2b542c; + +.btn-outline-warning { + color: #ffc107; + border-color: #ffc107; } -.text-info { - color: #31708f; + +.btn-outline-warning:hover { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; } -a.text-info:hover, -a.text-info:focus { - color: #245269; + +.btn-outline-warning:focus, .btn-outline-warning.focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } -.text-warning { - color: #8a6d3b; + +.btn-outline-warning.disabled, .btn-outline-warning:disabled { + color: #ffc107; + background-color: transparent; } -a.text-warning:hover, -a.text-warning:focus { - color: #66512c; + +.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active, +.show > .btn-outline-warning.dropdown-toggle { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; } -.text-danger { - color: #a94442; + +.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } -a.text-danger:hover, -a.text-danger:focus { - color: #843534; + +.btn-outline-danger { + color: #dc3545; + border-color: #dc3545; } -.bg-primary { + +.btn-outline-danger:hover { color: #fff; - background-color: #337ab7; + background-color: #dc3545; + border-color: #dc3545; } -a.bg-primary:hover, -a.bg-primary:focus { - background-color: #286090; -} -.bg-success { - background-color: #dff0d8; + +.btn-outline-danger:focus, .btn-outline-danger.focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } -a.bg-success:hover, -a.bg-success:focus { - background-color: #c1e2b3; + +.btn-outline-danger.disabled, .btn-outline-danger:disabled { + color: #dc3545; + background-color: transparent; } -.bg-info { - background-color: #d9edf7; + +.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active, +.show > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; } -a.bg-info:hover, -a.bg-info:focus { - background-color: #afd9ee; + +.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } -.bg-warning { - background-color: #fcf8e3; + +.btn-outline-light { + color: #f8f9fa; + border-color: #f8f9fa; } -a.bg-warning:hover, -a.bg-warning:focus { - background-color: #f7ecb5; + +.btn-outline-light:hover { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; } -.bg-danger { - background-color: #f2dede; + +.btn-outline-light:focus, .btn-outline-light.focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } -a.bg-danger:hover, -a.bg-danger:focus { - background-color: #e4b9b9; + +.btn-outline-light.disabled, .btn-outline-light:disabled { + color: #f8f9fa; + background-color: transparent; } -.page-header { - padding-bottom: 9px; - margin: 40px 0 20px; - border-bottom: 1px solid #eee; + +.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active, +.show > .btn-outline-light.dropdown-toggle { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; } -ul, -ol { - margin-top: 0; - margin-bottom: 10px; + +.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } -ul ul, -ol ul, -ul ol, -ol ol { - margin-bottom: 0; + +.btn-outline-dark { + color: #343a40; + border-color: #343a40; } -.list-unstyled { - padding-left: 0; - list-style: none; + +.btn-outline-dark:hover { + color: #fff; + background-color: #343a40; + border-color: #343a40; } -.list-inline { - padding-left: 0; - margin-left: -5px; - list-style: none; + +.btn-outline-dark:focus, .btn-outline-dark.focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } -.list-inline > li { - display: inline-block; - padding-right: 5px; - padding-left: 5px; + +.btn-outline-dark.disabled, .btn-outline-dark:disabled { + color: #343a40; + background-color: transparent; } -dl { - margin-top: 0; - margin-bottom: 20px; + +.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active, +.show > .btn-outline-dark.dropdown-toggle { + color: #fff; + background-color: #343a40; + border-color: #343a40; } -dt, -dd { - line-height: 1.42857143; + +.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } -dt { - font-weight: bold; + +.btn-link { + font-weight: 400; + color: #007bff; + text-decoration: none; } -dd { - margin-left: 0; + +.btn-link:hover { + color: #0056b3; + text-decoration: underline; } -@media (min-width: 768px) { - .dl-horizontal dt { - float: left; - width: 160px; - overflow: hidden; - clear: left; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; - } - .dl-horizontal dd { - margin-left: 180px; - } + +.btn-link:focus, .btn-link.focus { + text-decoration: underline; } -abbr[title], -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted #777; + +.btn-link:disabled, .btn-link.disabled { + color: #6c757d; + pointer-events: none; } -.initialism { - font-size: 90%; - text-transform: uppercase; + +.btn-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; } -blockquote { - padding: 10px 20px; - margin: 0 0 20px; - font-size: 17.5px; - border-left: 5px solid #eee; -} -blockquote p:last-child, -blockquote ul:last-child, -blockquote ol:last-child { - margin-bottom: 0; + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; } -blockquote footer, -blockquote small, -blockquote .small { + +.btn-block { display: block; - font-size: 80%; - line-height: 1.42857143; - color: #777; -} -blockquote footer:before, -blockquote small:before, -blockquote .small:before { - content: '\2014 \00A0'; + width: 100%; } -.blockquote-reverse, -blockquote.pull-right { - padding-right: 15px; - padding-left: 0; - text-align: right; - border-right: 5px solid #eee; - border-left: 0; -} -.blockquote-reverse footer:before, -blockquote.pull-right footer:before, -.blockquote-reverse small:before, -blockquote.pull-right small:before, -.blockquote-reverse .small:before, -blockquote.pull-right .small:before { - content: ''; -} -.blockquote-reverse footer:after, -blockquote.pull-right footer:after, -.blockquote-reverse small:after, -blockquote.pull-right small:after, -.blockquote-reverse .small:after, -blockquote.pull-right .small:after { - content: '\00A0 \2014'; + +.btn-block + .btn-block { + margin-top: 0.5rem; } -address { - margin-bottom: 20px; - font-style: normal; - line-height: 1.42857143; + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; } -code, -kbd, -pre, -samp { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + +.fade { + transition: opacity 0.15s linear; } -code { - padding: 2px 4px; - font-size: 90%; - color: #c7254e; - background-color: #f9f2f4; - border-radius: 4px; + +@media (prefers-reduced-motion: reduce) { + .fade { + transition: none; + } } -kbd { - padding: 2px 4px; - font-size: 90%; - color: #fff; - background-color: #333; - border-radius: 3px; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); + +.fade:not(.show) { + opacity: 0; } -kbd kbd { - padding: 0; - font-size: 100%; - font-weight: bold; - -webkit-box-shadow: none; - box-shadow: none; + +.collapse:not(.show) { + display: none; } -pre { - display: block; - padding: 9.5px; - margin: 0 0 10px; - font-size: 13px; - line-height: 1.42857143; - color: #333; - word-break: break-all; - word-wrap: break-word; - background-color: #f5f5f5; - border: 1px solid #ccc; - border-radius: 4px; + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + transition: height 0.35s ease; } -pre code { - padding: 0; - font-size: inherit; - color: inherit; - white-space: pre-wrap; - background-color: transparent; - border-radius: 0; + +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } } -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; + +.dropup, +.dropright, +.dropdown, +.dropleft { + position: relative; } -.container { - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; + +.dropdown-toggle { + white-space: nowrap; +} + +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} + +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 1rem; + color: #212529; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.dropdown-menu-left { + right: auto; + left: 0; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + +@media (min-width: 576px) { + .dropdown-menu-sm-left { + right: auto; + left: 0; + } + .dropdown-menu-sm-right { + right: 0; + left: auto; + } } + @media (min-width: 768px) { - .container { - width: 750px; + .dropdown-menu-md-left { + right: auto; + left: 0; + } + .dropdown-menu-md-right { + right: 0; + left: auto; } } + @media (min-width: 992px) { - .container { - width: 970px; + .dropdown-menu-lg-left { + right: auto; + left: 0; + } + .dropdown-menu-lg-right { + right: 0; + left: auto; } } + @media (min-width: 1200px) { - .container { - width: 1170px; + .dropdown-menu-xl-left { + right: auto; + left: 0; + } + .dropdown-menu-xl-right { + right: 0; + left: auto; } } -.container-fluid { - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; + +.dropup .dropdown-menu { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.125rem; } -.row { - margin-right: -15px; - margin-left: -15px; + +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; } -.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { - position: relative; - min-height: 1px; - padding-right: 15px; - padding-left: 15px; + +.dropup .dropdown-toggle:empty::after { + margin-left: 0; } -.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { - float: left; + +.dropright .dropdown-menu { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: 0.125rem; } -.col-xs-12 { - width: 100%; + +.dropright .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; } -.col-xs-11 { - width: 91.66666667%; + +.dropright .dropdown-toggle:empty::after { + margin-left: 0; } -.col-xs-10 { - width: 83.33333333%; + +.dropright .dropdown-toggle::after { + vertical-align: 0; } -.col-xs-9 { - width: 75%; + +.dropleft .dropdown-menu { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: 0.125rem; } -.col-xs-8 { - width: 66.66666667%; + +.dropleft .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; } -.col-xs-7 { - width: 58.33333333%; + +.dropleft .dropdown-toggle::after { + display: none; } -.col-xs-6 { - width: 50%; + +.dropleft .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; } -.col-xs-5 { - width: 41.66666667%; + +.dropleft .dropdown-toggle:empty::after { + margin-left: 0; } -.col-xs-4 { - width: 33.33333333%; + +.dropleft .dropdown-toggle::before { + vertical-align: 0; } -.col-xs-3 { - width: 25%; + +.dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] { + right: auto; + bottom: auto; } -.col-xs-2 { - width: 16.66666667%; + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #e9ecef; } -.col-xs-1 { - width: 8.33333333%; + +.dropdown-item { + display: block; + width: 100%; + padding: 0.25rem 1.5rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; } -.col-xs-pull-12 { - right: 100%; + +.dropdown-item:hover, .dropdown-item:focus { + color: #16181b; + text-decoration: none; + background-color: #f8f9fa; } -.col-xs-pull-11 { - right: 91.66666667%; + +.dropdown-item.active, .dropdown-item:active { + color: #fff; + text-decoration: none; + background-color: #007bff; } -.col-xs-pull-10 { - right: 83.33333333%; + +.dropdown-item.disabled, .dropdown-item:disabled { + color: #6c757d; + pointer-events: none; + background-color: transparent; } -.col-xs-pull-9 { - right: 75%; + +.dropdown-menu.show { + display: block; } -.col-xs-pull-8 { - right: 66.66666667%; + +.dropdown-header { + display: block; + padding: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #6c757d; + white-space: nowrap; } -.col-xs-pull-7 { - right: 58.33333333%; + +.dropdown-item-text { + display: block; + padding: 0.25rem 1.5rem; + color: #212529; } -.col-xs-pull-6 { - right: 50%; + +.btn-group, +.btn-group-vertical { + position: relative; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; } -.col-xs-pull-5 { - right: 41.66666667%; + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; } -.col-xs-pull-4 { - right: 33.33333333%; + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 1; } -.col-xs-pull-3 { - right: 25%; + +.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; } -.col-xs-pull-2 { - right: 16.66666667%; + +.btn-toolbar { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-pack: start; + justify-content: flex-start; } -.col-xs-pull-1 { - right: 8.33333333%; + +.btn-toolbar .input-group { + width: auto; } -.col-xs-pull-0 { - right: auto; + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) { + margin-left: -1px; } -.col-xs-push-12 { - left: 100%; + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } -.col-xs-push-11 { - left: 91.66666667%; + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; } -.col-xs-push-10 { - left: 83.33333333%; + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; } -.col-xs-push-9 { - left: 75%; + +.dropdown-toggle-split::after, +.dropup .dropdown-toggle-split::after, +.dropright .dropdown-toggle-split::after { + margin-left: 0; } -.col-xs-push-8 { - left: 66.66666667%; + +.dropleft .dropdown-toggle-split::before { + margin-right: 0; } -.col-xs-push-7 { - left: 58.33333333%; + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; } -.col-xs-push-6 { - left: 50%; + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; } -.col-xs-push-5 { - left: 41.66666667%; + +.btn-group-vertical { + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-align: start; + align-items: flex-start; + -ms-flex-pack: center; + justify-content: center; } -.col-xs-push-4 { - left: 33.33333333%; + +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + width: 100%; } -.col-xs-push-3 { - left: 25%; + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: -1px; } -.col-xs-push-2 { - left: 16.66666667%; + +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } -.col-xs-push-1 { - left: 8.33333333%; + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; } -.col-xs-push-0 { - left: auto; + +.btn-group-toggle > .btn, +.btn-group-toggle > .btn-group > .btn { + margin-bottom: 0; } -.col-xs-offset-12 { - margin-left: 100%; + +.btn-group-toggle > .btn input[type="radio"], +.btn-group-toggle > .btn input[type="checkbox"], +.btn-group-toggle > .btn-group > .btn input[type="radio"], +.btn-group-toggle > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; } -.col-xs-offset-11 { - margin-left: 91.66666667%; + +.input-group { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: stretch; + align-items: stretch; + width: 100%; } -.col-xs-offset-10 { - margin-left: 83.33333333%; + +.input-group > .form-control, +.input-group > .form-control-plaintext, +.input-group > .custom-select, +.input-group > .custom-file { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 1%; + min-width: 0; + margin-bottom: 0; } -.col-xs-offset-9 { - margin-left: 75%; + +.input-group > .form-control + .form-control, +.input-group > .form-control + .custom-select, +.input-group > .form-control + .custom-file, +.input-group > .form-control-plaintext + .form-control, +.input-group > .form-control-plaintext + .custom-select, +.input-group > .form-control-plaintext + .custom-file, +.input-group > .custom-select + .form-control, +.input-group > .custom-select + .custom-select, +.input-group > .custom-select + .custom-file, +.input-group > .custom-file + .form-control, +.input-group > .custom-file + .custom-select, +.input-group > .custom-file + .custom-file { + margin-left: -1px; } -.col-xs-offset-8 { - margin-left: 66.66666667%; + +.input-group > .form-control:focus, +.input-group > .custom-select:focus, +.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label { + z-index: 3; } -.col-xs-offset-7 { - margin-left: 58.33333333%; + +.input-group > .custom-file .custom-file-input:focus { + z-index: 4; } -.col-xs-offset-6 { - margin-left: 50%; + +.input-group > .form-control:not(:last-child), +.input-group > .custom-select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } -.col-xs-offset-5 { - margin-left: 41.66666667%; + +.input-group > .form-control:not(:first-child), +.input-group > .custom-select:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; } -.col-xs-offset-4 { - margin-left: 33.33333333%; + +.input-group > .custom-file { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; } -.col-xs-offset-3 { - margin-left: 25%; + +.input-group > .custom-file:not(:last-child) .custom-file-label, +.input-group > .custom-file:not(:last-child) .custom-file-label::after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } -.col-xs-offset-2 { - margin-left: 16.66666667%; + +.input-group > .custom-file:not(:first-child) .custom-file-label { + border-top-left-radius: 0; + border-bottom-left-radius: 0; } -.col-xs-offset-1 { - margin-left: 8.33333333%; + +.input-group-prepend, +.input-group-append { + display: -ms-flexbox; + display: flex; } -.col-xs-offset-0 { - margin-left: 0; + +.input-group-prepend .btn, +.input-group-append .btn { + position: relative; + z-index: 2; } -@media (min-width: 768px) { - .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { - float: left; - } - .col-sm-12 { - width: 100%; - } - .col-sm-11 { - width: 91.66666667%; - } - .col-sm-10 { - width: 83.33333333%; - } - .col-sm-9 { - width: 75%; - } - .col-sm-8 { - width: 66.66666667%; - } - .col-sm-7 { - width: 58.33333333%; - } - .col-sm-6 { - width: 50%; - } - .col-sm-5 { - width: 41.66666667%; - } - .col-sm-4 { - width: 33.33333333%; - } - .col-sm-3 { - width: 25%; - } - .col-sm-2 { - width: 16.66666667%; - } - .col-sm-1 { - width: 8.33333333%; - } - .col-sm-pull-12 { - right: 100%; - } - .col-sm-pull-11 { - right: 91.66666667%; - } - .col-sm-pull-10 { - right: 83.33333333%; - } - .col-sm-pull-9 { - right: 75%; - } - .col-sm-pull-8 { - right: 66.66666667%; - } - .col-sm-pull-7 { - right: 58.33333333%; - } - .col-sm-pull-6 { - right: 50%; - } - .col-sm-pull-5 { - right: 41.66666667%; - } - .col-sm-pull-4 { - right: 33.33333333%; - } - .col-sm-pull-3 { - right: 25%; - } - .col-sm-pull-2 { - right: 16.66666667%; - } - .col-sm-pull-1 { - right: 8.33333333%; - } - .col-sm-pull-0 { - right: auto; - } - .col-sm-push-12 { - left: 100%; - } - .col-sm-push-11 { - left: 91.66666667%; - } - .col-sm-push-10 { - left: 83.33333333%; - } - .col-sm-push-9 { - left: 75%; - } - .col-sm-push-8 { - left: 66.66666667%; - } - .col-sm-push-7 { - left: 58.33333333%; - } - .col-sm-push-6 { - left: 50%; - } - .col-sm-push-5 { - left: 41.66666667%; - } - .col-sm-push-4 { - left: 33.33333333%; - } - .col-sm-push-3 { - left: 25%; - } - .col-sm-push-2 { - left: 16.66666667%; - } - .col-sm-push-1 { - left: 8.33333333%; - } - .col-sm-push-0 { - left: auto; - } - .col-sm-offset-12 { - margin-left: 100%; - } - .col-sm-offset-11 { - margin-left: 91.66666667%; - } - .col-sm-offset-10 { - margin-left: 83.33333333%; - } - .col-sm-offset-9 { - margin-left: 75%; - } - .col-sm-offset-8 { - margin-left: 66.66666667%; - } - .col-sm-offset-7 { - margin-left: 58.33333333%; - } - .col-sm-offset-6 { - margin-left: 50%; - } - .col-sm-offset-5 { - margin-left: 41.66666667%; - } - .col-sm-offset-4 { - margin-left: 33.33333333%; - } - .col-sm-offset-3 { - margin-left: 25%; - } - .col-sm-offset-2 { - margin-left: 16.66666667%; - } - .col-sm-offset-1 { - margin-left: 8.33333333%; - } - .col-sm-offset-0 { - margin-left: 0; - } + +.input-group-prepend .btn:focus, +.input-group-append .btn:focus { + z-index: 3; } -@media (min-width: 992px) { - .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { - float: left; - } - .col-md-12 { - width: 100%; - } - .col-md-11 { - width: 91.66666667%; - } - .col-md-10 { - width: 83.33333333%; - } - .col-md-9 { - width: 75%; - } - .col-md-8 { - width: 66.66666667%; - } - .col-md-7 { - width: 58.33333333%; - } - .col-md-6 { - width: 50%; - } - .col-md-5 { - width: 41.66666667%; - } - .col-md-4 { - width: 33.33333333%; - } - .col-md-3 { - width: 25%; - } - .col-md-2 { - width: 16.66666667%; - } - .col-md-1 { - width: 8.33333333%; - } - .col-md-pull-12 { - right: 100%; - } - .col-md-pull-11 { - right: 91.66666667%; - } - .col-md-pull-10 { - right: 83.33333333%; - } - .col-md-pull-9 { - right: 75%; - } - .col-md-pull-8 { - right: 66.66666667%; - } - .col-md-pull-7 { - right: 58.33333333%; - } - .col-md-pull-6 { - right: 50%; - } - .col-md-pull-5 { - right: 41.66666667%; - } - .col-md-pull-4 { - right: 33.33333333%; - } - .col-md-pull-3 { - right: 25%; - } - .col-md-pull-2 { - right: 16.66666667%; - } - .col-md-pull-1 { - right: 8.33333333%; - } - .col-md-pull-0 { - right: auto; - } - .col-md-push-12 { - left: 100%; - } - .col-md-push-11 { - left: 91.66666667%; - } - .col-md-push-10 { - left: 83.33333333%; - } - .col-md-push-9 { - left: 75%; - } - .col-md-push-8 { - left: 66.66666667%; - } - .col-md-push-7 { - left: 58.33333333%; - } - .col-md-push-6 { - left: 50%; - } - .col-md-push-5 { - left: 41.66666667%; - } - .col-md-push-4 { - left: 33.33333333%; - } - .col-md-push-3 { - left: 25%; - } - .col-md-push-2 { - left: 16.66666667%; - } - .col-md-push-1 { - left: 8.33333333%; - } - .col-md-push-0 { - left: auto; - } - .col-md-offset-12 { - margin-left: 100%; - } - .col-md-offset-11 { - margin-left: 91.66666667%; - } - .col-md-offset-10 { - margin-left: 83.33333333%; - } - .col-md-offset-9 { - margin-left: 75%; - } - .col-md-offset-8 { - margin-left: 66.66666667%; - } - .col-md-offset-7 { - margin-left: 58.33333333%; - } - .col-md-offset-6 { - margin-left: 50%; - } - .col-md-offset-5 { - margin-left: 41.66666667%; - } - .col-md-offset-4 { - margin-left: 33.33333333%; - } - .col-md-offset-3 { - margin-left: 25%; - } - .col-md-offset-2 { - margin-left: 16.66666667%; - } - .col-md-offset-1 { - margin-left: 8.33333333%; - } - .col-md-offset-0 { - margin-left: 0; - } + +.input-group-prepend .btn + .btn, +.input-group-prepend .btn + .input-group-text, +.input-group-prepend .input-group-text + .input-group-text, +.input-group-prepend .input-group-text + .btn, +.input-group-append .btn + .btn, +.input-group-append .btn + .input-group-text, +.input-group-append .input-group-text + .input-group-text, +.input-group-append .input-group-text + .btn { + margin-left: -1px; } -@media (min-width: 1200px) { - .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { - float: left; - } - .col-lg-12 { - width: 100%; - } - .col-lg-11 { - width: 91.66666667%; - } - .col-lg-10 { - width: 83.33333333%; - } - .col-lg-9 { - width: 75%; - } - .col-lg-8 { - width: 66.66666667%; - } - .col-lg-7 { - width: 58.33333333%; - } - .col-lg-6 { - width: 50%; - } - .col-lg-5 { - width: 41.66666667%; - } - .col-lg-4 { - width: 33.33333333%; - } - .col-lg-3 { - width: 25%; - } - .col-lg-2 { - width: 16.66666667%; - } - .col-lg-1 { - width: 8.33333333%; - } - .col-lg-pull-12 { - right: 100%; - } - .col-lg-pull-11 { - right: 91.66666667%; - } - .col-lg-pull-10 { - right: 83.33333333%; - } - .col-lg-pull-9 { - right: 75%; - } - .col-lg-pull-8 { - right: 66.66666667%; - } - .col-lg-pull-7 { - right: 58.33333333%; - } - .col-lg-pull-6 { - right: 50%; - } - .col-lg-pull-5 { - right: 41.66666667%; - } - .col-lg-pull-4 { - right: 33.33333333%; - } - .col-lg-pull-3 { - right: 25%; - } - .col-lg-pull-2 { - right: 16.66666667%; - } - .col-lg-pull-1 { - right: 8.33333333%; - } - .col-lg-pull-0 { - right: auto; - } - .col-lg-push-12 { - left: 100%; - } - .col-lg-push-11 { - left: 91.66666667%; - } - .col-lg-push-10 { - left: 83.33333333%; - } - .col-lg-push-9 { - left: 75%; - } - .col-lg-push-8 { - left: 66.66666667%; - } - .col-lg-push-7 { - left: 58.33333333%; - } - .col-lg-push-6 { - left: 50%; - } - .col-lg-push-5 { - left: 41.66666667%; - } - .col-lg-push-4 { - left: 33.33333333%; - } - .col-lg-push-3 { - left: 25%; - } - .col-lg-push-2 { - left: 16.66666667%; - } - .col-lg-push-1 { - left: 8.33333333%; - } - .col-lg-push-0 { - left: auto; - } - .col-lg-offset-12 { - margin-left: 100%; - } - .col-lg-offset-11 { - margin-left: 91.66666667%; - } - .col-lg-offset-10 { - margin-left: 83.33333333%; - } - .col-lg-offset-9 { - margin-left: 75%; - } - .col-lg-offset-8 { - margin-left: 66.66666667%; - } - .col-lg-offset-7 { - margin-left: 58.33333333%; - } - .col-lg-offset-6 { - margin-left: 50%; - } - .col-lg-offset-5 { - margin-left: 41.66666667%; - } - .col-lg-offset-4 { - margin-left: 33.33333333%; - } - .col-lg-offset-3 { - margin-left: 25%; - } - .col-lg-offset-2 { - margin-left: 16.66666667%; - } - .col-lg-offset-1 { - margin-left: 8.33333333%; - } - .col-lg-offset-0 { - margin-left: 0; - } + +.input-group-prepend { + margin-right: -1px; } -table { - background-color: transparent; + +.input-group-append { + margin-left: -1px; } -caption { - padding-top: 8px; - padding-bottom: 8px; - color: #777; - text-align: left; + +.input-group-text { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + padding: 0.375rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + text-align: center; + white-space: nowrap; + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: 0.25rem; } -th { - text-align: left; + +.input-group-text input[type="radio"], +.input-group-text input[type="checkbox"] { + margin-top: 0; } -.table { - width: 100%; - max-width: 100%; - margin-bottom: 20px; -} -.table > thead > tr > th, -.table > tbody > tr > th, -.table > tfoot > tr > th, -.table > thead > tr > td, -.table > tbody > tr > td, -.table > tfoot > tr > td { - padding: 8px; - line-height: 1.42857143; - vertical-align: top; - border-top: 1px solid #ddd; + +.input-group-lg > .form-control:not(textarea), +.input-group-lg > .custom-select { + height: calc(1.5em + 1rem + 2px); } -.table > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid #ddd; -} -.table > caption + thead > tr:first-child > th, -.table > colgroup + thead > tr:first-child > th, -.table > thead:first-child > tr:first-child > th, -.table > caption + thead > tr:first-child > td, -.table > colgroup + thead > tr:first-child > td, -.table > thead:first-child > tr:first-child > td { - border-top: 0; + +.input-group-lg > .form-control, +.input-group-lg > .custom-select, +.input-group-lg > .input-group-prepend > .input-group-text, +.input-group-lg > .input-group-append > .input-group-text, +.input-group-lg > .input-group-prepend > .btn, +.input-group-lg > .input-group-append > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; } -.table > tbody + tbody { - border-top: 2px solid #ddd; + +.input-group-sm > .form-control:not(textarea), +.input-group-sm > .custom-select { + height: calc(1.5em + 0.5rem + 2px); } -.table .table { - background-color: #fff; + +.input-group-sm > .form-control, +.input-group-sm > .custom-select, +.input-group-sm > .input-group-prepend > .input-group-text, +.input-group-sm > .input-group-append > .input-group-text, +.input-group-sm > .input-group-prepend > .btn, +.input-group-sm > .input-group-append > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; } -.table-condensed > thead > tr > th, -.table-condensed > tbody > tr > th, -.table-condensed > tfoot > tr > th, -.table-condensed > thead > tr > td, -.table-condensed > tbody > tr > td, -.table-condensed > tfoot > tr > td { - padding: 5px; + +.input-group-lg > .custom-select, +.input-group-sm > .custom-select { + padding-right: 1.75rem; } -.table-bordered { - border: 1px solid #ddd; -} -.table-bordered > thead > tr > th, -.table-bordered > tbody > tr > th, -.table-bordered > tfoot > tr > th, -.table-bordered > thead > tr > td, -.table-bordered > tbody > tr > td, -.table-bordered > tfoot > tr > td { - border: 1px solid #ddd; -} -.table-bordered > thead > tr > th, -.table-bordered > thead > tr > td { - border-bottom-width: 2px; + +.input-group > .input-group-prepend > .btn, +.input-group > .input-group-prepend > .input-group-text, +.input-group > .input-group-append:not(:last-child) > .btn, +.input-group > .input-group-append:not(:last-child) > .input-group-text, +.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } -.table-striped > tbody > tr:nth-of-type(odd) { - background-color: #f9f9f9; + +.input-group > .input-group-append > .btn, +.input-group > .input-group-append > .input-group-text, +.input-group > .input-group-prepend:not(:first-child) > .btn, +.input-group > .input-group-prepend:not(:first-child) > .input-group-text, +.input-group > .input-group-prepend:first-child > .btn:not(:first-child), +.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; } -.table-hover > tbody > tr:hover { - background-color: #f5f5f5; + +.custom-control { + position: relative; + z-index: 1; + display: block; + min-height: 1.5rem; + padding-left: 1.5rem; + -webkit-print-color-adjust: exact; + color-adjust: exact; } -table col[class*="col-"] { - position: static; - display: table-column; - float: none; + +.custom-control-inline { + display: -ms-inline-flexbox; + display: inline-flex; + margin-right: 1rem; } -table td[class*="col-"], -table th[class*="col-"] { - position: static; - display: table-cell; - float: none; -} -.table > thead > tr > td.active, -.table > tbody > tr > td.active, -.table > tfoot > tr > td.active, -.table > thead > tr > th.active, -.table > tbody > tr > th.active, -.table > tfoot > tr > th.active, -.table > thead > tr.active > td, -.table > tbody > tr.active > td, -.table > tfoot > tr.active > td, -.table > thead > tr.active > th, -.table > tbody > tr.active > th, -.table > tfoot > tr.active > th { - background-color: #f5f5f5; -} -.table-hover > tbody > tr > td.active:hover, -.table-hover > tbody > tr > th.active:hover, -.table-hover > tbody > tr.active:hover > td, -.table-hover > tbody > tr:hover > .active, -.table-hover > tbody > tr.active:hover > th { - background-color: #e8e8e8; -} -.table > thead > tr > td.success, -.table > tbody > tr > td.success, -.table > tfoot > tr > td.success, -.table > thead > tr > th.success, -.table > tbody > tr > th.success, -.table > tfoot > tr > th.success, -.table > thead > tr.success > td, -.table > tbody > tr.success > td, -.table > tfoot > tr.success > td, -.table > thead > tr.success > th, -.table > tbody > tr.success > th, -.table > tfoot > tr.success > th { - background-color: #dff0d8; -} -.table-hover > tbody > tr > td.success:hover, -.table-hover > tbody > tr > th.success:hover, -.table-hover > tbody > tr.success:hover > td, -.table-hover > tbody > tr:hover > .success, -.table-hover > tbody > tr.success:hover > th { - background-color: #d0e9c6; -} -.table > thead > tr > td.info, -.table > tbody > tr > td.info, -.table > tfoot > tr > td.info, -.table > thead > tr > th.info, -.table > tbody > tr > th.info, -.table > tfoot > tr > th.info, -.table > thead > tr.info > td, -.table > tbody > tr.info > td, -.table > tfoot > tr.info > td, -.table > thead > tr.info > th, -.table > tbody > tr.info > th, -.table > tfoot > tr.info > th { - background-color: #d9edf7; -} -.table-hover > tbody > tr > td.info:hover, -.table-hover > tbody > tr > th.info:hover, -.table-hover > tbody > tr.info:hover > td, -.table-hover > tbody > tr:hover > .info, -.table-hover > tbody > tr.info:hover > th { - background-color: #c4e3f3; -} -.table > thead > tr > td.warning, -.table > tbody > tr > td.warning, -.table > tfoot > tr > td.warning, -.table > thead > tr > th.warning, -.table > tbody > tr > th.warning, -.table > tfoot > tr > th.warning, -.table > thead > tr.warning > td, -.table > tbody > tr.warning > td, -.table > tfoot > tr.warning > td, -.table > thead > tr.warning > th, -.table > tbody > tr.warning > th, -.table > tfoot > tr.warning > th { - background-color: #fcf8e3; -} -.table-hover > tbody > tr > td.warning:hover, -.table-hover > tbody > tr > th.warning:hover, -.table-hover > tbody > tr.warning:hover > td, -.table-hover > tbody > tr:hover > .warning, -.table-hover > tbody > tr.warning:hover > th { - background-color: #faf2cc; -} -.table > thead > tr > td.danger, -.table > tbody > tr > td.danger, -.table > tfoot > tr > td.danger, -.table > thead > tr > th.danger, -.table > tbody > tr > th.danger, -.table > tfoot > tr > th.danger, -.table > thead > tr.danger > td, -.table > tbody > tr.danger > td, -.table > tfoot > tr.danger > td, -.table > thead > tr.danger > th, -.table > tbody > tr.danger > th, -.table > tfoot > tr.danger > th { - background-color: #f2dede; -} -.table-hover > tbody > tr > td.danger:hover, -.table-hover > tbody > tr > th.danger:hover, -.table-hover > tbody > tr.danger:hover > td, -.table-hover > tbody > tr:hover > .danger, -.table-hover > tbody > tr.danger:hover > th { - background-color: #ebcccc; + +.custom-control-input { + position: absolute; + left: 0; + z-index: -1; + width: 1rem; + height: 1.25rem; + opacity: 0; } -.table-responsive { - min-height: .01%; - overflow-x: auto; + +.custom-control-input:checked ~ .custom-control-label::before { + color: #fff; + border-color: #007bff; + background-color: #007bff; } -@media screen and (max-width: 767px) { - .table-responsive { - width: 100%; - margin-bottom: 15px; - overflow-y: hidden; - -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid #ddd; - } - .table-responsive > .table { - margin-bottom: 0; - } - .table-responsive > .table > thead > tr > th, - .table-responsive > .table > tbody > tr > th, - .table-responsive > .table > tfoot > tr > th, - .table-responsive > .table > thead > tr > td, - .table-responsive > .table > tbody > tr > td, - .table-responsive > .table > tfoot > tr > td { - white-space: nowrap; - } - .table-responsive > .table-bordered { - border: 0; - } - .table-responsive > .table-bordered > thead > tr > th:first-child, - .table-responsive > .table-bordered > tbody > tr > th:first-child, - .table-responsive > .table-bordered > tfoot > tr > th:first-child, - .table-responsive > .table-bordered > thead > tr > td:first-child, - .table-responsive > .table-bordered > tbody > tr > td:first-child, - .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; - } - .table-responsive > .table-bordered > thead > tr > th:last-child, - .table-responsive > .table-bordered > tbody > tr > th:last-child, - .table-responsive > .table-bordered > tfoot > tr > th:last-child, - .table-responsive > .table-bordered > thead > tr > td:last-child, - .table-responsive > .table-bordered > tbody > tr > td:last-child, - .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; - } - .table-responsive > .table-bordered > tbody > tr:last-child > th, - .table-responsive > .table-bordered > tfoot > tr:last-child > th, - .table-responsive > .table-bordered > tbody > tr:last-child > td, - .table-responsive > .table-bordered > tfoot > tr:last-child > td { - border-bottom: 0; - } + +.custom-control-input:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } -fieldset { - min-width: 0; - padding: 0; - margin: 0; - border: 0; + +.custom-control-input:focus:not(:checked) ~ .custom-control-label::before { + border-color: #80bdff; } -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 20px; - font-size: 21px; - line-height: inherit; - color: #333; - border: 0; - border-bottom: 1px solid #e5e5e5; + +.custom-control-input:not(:disabled):active ~ .custom-control-label::before { + color: #fff; + background-color: #b3d7ff; + border-color: #b3d7ff; } -label { - display: inline-block; - max-width: 100%; - margin-bottom: 5px; - font-weight: bold; + +.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label { + color: #6c757d; } -input[type="search"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + +.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before { + background-color: #e9ecef; } -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; - line-height: normal; + +.custom-control-label { + position: relative; + margin-bottom: 0; + vertical-align: top; } -input[type="file"] { + +.custom-control-label::before { + position: absolute; + top: 0.25rem; + left: -1.5rem; display: block; + width: 1rem; + height: 1rem; + pointer-events: none; + content: ""; + background-color: #fff; + border: #adb5bd solid 1px; } -input[type="range"] { + +.custom-control-label::after { + position: absolute; + top: 0.25rem; + left: -1.5rem; display: block; - width: 100%; + width: 1rem; + height: 1rem; + content: ""; + background: no-repeat 50% / 50% 50%; } -select[multiple], -select[size] { - height: auto; + +.custom-checkbox .custom-control-label::before { + border-radius: 0.25rem; } -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; + +.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e"); } -output { - display: block; - padding-top: 7px; - font-size: 14px; - line-height: 1.42857143; - color: #555; + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before { + border-color: #007bff; + background-color: #007bff; } -.form-control { - display: block; - width: 100%; - height: 34px; - padding: 6px 12px; - font-size: 14px; - line-height: 1.42857143; - color: #555; - background-color: #fff; - background-image: none; - border: 1px solid #ccc; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; - -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e"); } -.form-control:focus { - border-color: #66afe9; - outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); + +.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); } -.form-control::-moz-placeholder { - color: #999; - opacity: 1; + +.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); } -.form-control:-ms-input-placeholder { - color: #999; + +.custom-radio .custom-control-label::before { + border-radius: 50%; } -.form-control::-webkit-input-placeholder { - color: #999; + +.custom-radio .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); } -.form-control[disabled], -.form-control[readonly], -fieldset[disabled] .form-control { - background-color: #eee; - opacity: 1; + +.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); } -.form-control[disabled], -fieldset[disabled] .form-control { - cursor: not-allowed; + +.custom-switch { + padding-left: 2.25rem; } -textarea.form-control { - height: auto; + +.custom-switch .custom-control-label::before { + left: -2.25rem; + width: 1.75rem; + pointer-events: all; + border-radius: 0.5rem; } -input[type="search"] { - -webkit-appearance: none; + +.custom-switch .custom-control-label::after { + top: calc(0.25rem + 2px); + left: calc(-2.25rem + 2px); + width: calc(1rem - 4px); + height: calc(1rem - 4px); + background-color: #adb5bd; + border-radius: 0.5rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; } -@media screen and (-webkit-min-device-pixel-ratio: 0) { - input[type="date"].form-control, - input[type="time"].form-control, - input[type="datetime-local"].form-control, - input[type="month"].form-control { - line-height: 34px; - } - input[type="date"].input-sm, - input[type="time"].input-sm, - input[type="datetime-local"].input-sm, - input[type="month"].input-sm, - .input-group-sm input[type="date"], - .input-group-sm input[type="time"], - .input-group-sm input[type="datetime-local"], - .input-group-sm input[type="month"] { - line-height: 30px; - } - input[type="date"].input-lg, - input[type="time"].input-lg, - input[type="datetime-local"].input-lg, - input[type="month"].input-lg, - .input-group-lg input[type="date"], - .input-group-lg input[type="time"], - .input-group-lg input[type="datetime-local"], - .input-group-lg input[type="month"] { - line-height: 46px; + +@media (prefers-reduced-motion: reduce) { + .custom-switch .custom-control-label::after { + transition: none; } } -.form-group { - margin-bottom: 15px; -} -.radio, -.checkbox { - position: relative; - display: block; - margin-top: 10px; - margin-bottom: 10px; -} -.radio label, -.checkbox label { - min-height: 20px; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; -} -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: absolute; - margin-top: 4px \9; - margin-left: -20px; + +.custom-switch .custom-control-input:checked ~ .custom-control-label::after { + background-color: #fff; + -webkit-transform: translateX(0.75rem); + transform: translateX(0.75rem); } -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; + +.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); } -.radio-inline, -.checkbox-inline { - position: relative; + +.custom-select { display: inline-block; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; vertical-align: middle; - cursor: pointer; -} -.radio-inline + .radio-inline, -.checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; -} -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"].disabled, -input[type="checkbox"].disabled, -fieldset[disabled] input[type="radio"], -fieldset[disabled] input[type="checkbox"] { - cursor: not-allowed; -} -.radio-inline.disabled, -.checkbox-inline.disabled, -fieldset[disabled] .radio-inline, -fieldset[disabled] .checkbox-inline { - cursor: not-allowed; -} -.radio.disabled label, -.checkbox.disabled label, -fieldset[disabled] .radio label, -fieldset[disabled] .checkbox label { - cursor: not-allowed; -} -.form-control-static { - min-height: 34px; - padding-top: 7px; - padding-bottom: 7px; - margin-bottom: 0; -} -.form-control-static.input-lg, -.form-control-static.input-sm { - padding-right: 0; - padding-left: 0; + background: #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px; + border: 1px solid #ced4da; + border-radius: 0.25rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } -.input-sm { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; + +.custom-select:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } -select.input-sm { - height: 30px; - line-height: 30px; + +.custom-select:focus::-ms-value { + color: #495057; + background-color: #fff; } -textarea.input-sm, -select[multiple].input-sm { + +.custom-select[multiple], .custom-select[size]:not([size="1"]) { height: auto; + padding-right: 0.75rem; + background-image: none; } -.form-group-sm .form-control { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.form-group-sm select.form-control { - height: 30px; - line-height: 30px; -} -.form-group-sm textarea.form-control, -.form-group-sm select[multiple].form-control { - height: auto; + +.custom-select:disabled { + color: #6c757d; + background-color: #e9ecef; } -.form-group-sm .form-control-static { - height: 30px; - min-height: 32px; - padding: 6px 10px; - font-size: 12px; - line-height: 1.5; + +.custom-select::-ms-expand { + display: none; } -.input-lg { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; - border-radius: 6px; + +.custom-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #495057; } -select.input-lg { - height: 46px; - line-height: 46px; + +.custom-select-sm { + height: calc(1.5em + 0.5rem + 2px); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; } -textarea.input-lg, -select[multiple].input-lg { - height: auto; + +.custom-select-lg { + height: calc(1.5em + 1rem + 2px); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; } -.form-group-lg .form-control { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; - border-radius: 6px; + +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin-bottom: 0; } -.form-group-lg select.form-control { - height: 46px; - line-height: 46px; + +.custom-file-input { + position: relative; + z-index: 2; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin: 0; + opacity: 0; } -.form-group-lg textarea.form-control, -.form-group-lg select[multiple].form-control { - height: auto; + +.custom-file-input:focus ~ .custom-file-label { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } -.form-group-lg .form-control-static { - height: 46px; - min-height: 38px; - padding: 11px 16px; - font-size: 18px; - line-height: 1.3333333; + +.custom-file-input[disabled] ~ .custom-file-label, +.custom-file-input:disabled ~ .custom-file-label { + background-color: #e9ecef; } -.has-feedback { - position: relative; + +.custom-file-input:lang(en) ~ .custom-file-label::after { + content: "Browse"; } -.has-feedback .form-control { - padding-right: 42.5px; + +.custom-file-input ~ .custom-file-label[data-browse]::after { + content: attr(data-browse); } -.form-control-feedback { + +.custom-file-label { position: absolute; top: 0; right: 0; - z-index: 2; - display: block; - width: 34px; - height: 34px; - line-height: 34px; - text-align: center; - pointer-events: none; -} -.input-lg + .form-control-feedback, -.input-group-lg + .form-control-feedback, -.form-group-lg .form-control + .form-control-feedback { - width: 46px; - height: 46px; - line-height: 46px; + left: 0; + z-index: 1; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; } -.input-sm + .form-control-feedback, -.input-group-sm + .form-control-feedback, -.form-group-sm .form-control + .form-control-feedback { - width: 30px; - height: 30px; - line-height: 30px; -} -.has-success .help-block, -.has-success .control-label, -.has-success .radio, -.has-success .checkbox, -.has-success .radio-inline, -.has-success .checkbox-inline, -.has-success.radio label, -.has-success.checkbox label, -.has-success.radio-inline label, -.has-success.checkbox-inline label { - color: #3c763d; -} -.has-success .form-control { - border-color: #3c763d; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -} -.has-success .form-control:focus { - border-color: #2b542c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; -} -.has-success .input-group-addon { - color: #3c763d; - background-color: #dff0d8; - border-color: #3c763d; -} -.has-success .form-control-feedback { - color: #3c763d; -} -.has-warning .help-block, -.has-warning .control-label, -.has-warning .radio, -.has-warning .checkbox, -.has-warning .radio-inline, -.has-warning .checkbox-inline, -.has-warning.radio label, -.has-warning.checkbox label, -.has-warning.radio-inline label, -.has-warning.checkbox-inline label { - color: #8a6d3b; -} -.has-warning .form-control { - border-color: #8a6d3b; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -} -.has-warning .form-control:focus { - border-color: #66512c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; -} -.has-warning .input-group-addon { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #8a6d3b; -} -.has-warning .form-control-feedback { - color: #8a6d3b; -} -.has-error .help-block, -.has-error .control-label, -.has-error .radio, -.has-error .checkbox, -.has-error .radio-inline, -.has-error .checkbox-inline, -.has-error.radio label, -.has-error.checkbox label, -.has-error.radio-inline label, -.has-error.checkbox-inline label { - color: #a94442; -} -.has-error .form-control { - border-color: #a94442; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -} -.has-error .form-control:focus { - border-color: #843534; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; -} -.has-error .input-group-addon { - color: #a94442; - background-color: #f2dede; - border-color: #a94442; -} -.has-error .form-control-feedback { - color: #a94442; -} -.has-feedback label ~ .form-control-feedback { - top: 25px; -} -.has-feedback label.sr-only ~ .form-control-feedback { + +.custom-file-label::after { + position: absolute; top: 0; -} -.help-block { + right: 0; + bottom: 0; + z-index: 3; display: block; - margin-top: 5px; - margin-bottom: 10px; - color: #737373; + height: calc(1.5em + 0.75rem); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + content: "Browse"; + background-color: #e9ecef; + border-left: inherit; + border-radius: 0 0.25rem 0.25rem 0; } -@media (min-width: 768px) { - .form-inline .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .form-control { - display: inline-block; - width: auto; - vertical-align: middle; - } - .form-inline .form-control-static { - display: inline-block; - } - .form-inline .input-group { - display: inline-table; - vertical-align: middle; - } - .form-inline .input-group .input-group-addon, - .form-inline .input-group .input-group-btn, - .form-inline .input-group .form-control { - width: auto; - } - .form-inline .input-group > .form-control { - width: 100%; - } - .form-inline .control-label { - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio, - .form-inline .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio label, - .form-inline .checkbox label { - padding-left: 0; - } - .form-inline .radio input[type="radio"], - .form-inline .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - .form-inline .has-feedback .form-control-feedback { - top: 0; - } + +.custom-range { + width: 100%; + height: 1.4rem; + padding: 0; + background-color: transparent; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } -.form-horizontal .radio, -.form-horizontal .checkbox, -.form-horizontal .radio-inline, -.form-horizontal .checkbox-inline { - padding-top: 7px; - margin-top: 0; - margin-bottom: 0; + +.custom-range:focus { + outline: none; +} + +.custom-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } -.form-horizontal .radio, -.form-horizontal .checkbox { - min-height: 27px; + +.custom-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } -.form-horizontal .form-group { - margin-right: -15px; - margin-left: -15px; + +.custom-range:focus::-ms-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } -@media (min-width: 768px) { - .form-horizontal .control-label { - padding-top: 7px; - margin-bottom: 0; - text-align: right; - } + +.custom-range::-moz-focus-outer { + border: 0; } -.form-horizontal .has-feedback .form-control-feedback { - right: 15px; + +.custom-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -webkit-appearance: none; + appearance: none; } -@media (min-width: 768px) { - .form-horizontal .form-group-lg .control-label { - padding-top: 14.333333px; - font-size: 18px; + +@media (prefers-reduced-motion: reduce) { + .custom-range::-webkit-slider-thumb { + -webkit-transition: none; + transition: none; } } -@media (min-width: 768px) { - .form-horizontal .form-group-sm .control-label { - padding-top: 6px; - font-size: 12px; - } + +.custom-range::-webkit-slider-thumb:active { + background-color: #b3d7ff; } -.btn { - display: inline-block; - padding: 6px 12px; - margin-bottom: 0; - font-size: 14px; - font-weight: normal; - line-height: 1.42857143; - text-align: center; - white-space: nowrap; - vertical-align: middle; - -ms-touch-action: manipulation; - touch-action: manipulation; + +.custom-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-image: none; - border: 1px solid transparent; - border-radius: 4px; -} -.btn:focus, -.btn:active:focus, -.btn.active:focus, -.btn.focus, -.btn:active.focus, -.btn.active.focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; } -.btn:hover, -.btn:focus, -.btn.focus { - color: #333; - text-decoration: none; + +.custom-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -moz-appearance: none; + appearance: none; } -.btn:active, -.btn.active { - background-image: none; - outline: 0; - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); -} -.btn.disabled, -.btn[disabled], -fieldset[disabled] .btn { - cursor: not-allowed; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - box-shadow: none; - opacity: .65; + +@media (prefers-reduced-motion: reduce) { + .custom-range::-moz-range-thumb { + -moz-transition: none; + transition: none; + } } -a.btn.disabled, -fieldset[disabled] a.btn { - pointer-events: none; + +.custom-range::-moz-range-thumb:active { + background-color: #b3d7ff; } -.btn-default { - color: #333; - background-color: #fff; - border-color: #ccc; -} -.btn-default:focus, -.btn-default.focus { - color: #333; - background-color: #e6e6e6; - border-color: #8c8c8c; -} -.btn-default:hover { - color: #333; - background-color: #e6e6e6; - border-color: #adadad; -} -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - color: #333; - background-color: #e6e6e6; - border-color: #adadad; -} -.btn-default:active:hover, -.btn-default.active:hover, -.open > .dropdown-toggle.btn-default:hover, -.btn-default:active:focus, -.btn-default.active:focus, -.open > .dropdown-toggle.btn-default:focus, -.btn-default:active.focus, -.btn-default.active.focus, -.open > .dropdown-toggle.btn-default.focus { - color: #333; - background-color: #d4d4d4; - border-color: #8c8c8c; -} -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - background-image: none; + +.custom-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; } -.btn-default.disabled, -.btn-default[disabled], -fieldset[disabled] .btn-default, -.btn-default.disabled:hover, -.btn-default[disabled]:hover, -fieldset[disabled] .btn-default:hover, -.btn-default.disabled:focus, -.btn-default[disabled]:focus, -fieldset[disabled] .btn-default:focus, -.btn-default.disabled.focus, -.btn-default[disabled].focus, -fieldset[disabled] .btn-default.focus, -.btn-default.disabled:active, -.btn-default[disabled]:active, -fieldset[disabled] .btn-default:active, -.btn-default.disabled.active, -.btn-default[disabled].active, -fieldset[disabled] .btn-default.active { - background-color: #fff; - border-color: #ccc; + +.custom-range::-ms-thumb { + width: 1rem; + height: 1rem; + margin-top: 0; + margin-right: 0.2rem; + margin-left: 0.2rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -ms-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + appearance: none; } -.btn-default .badge { - color: #fff; - background-color: #333; + +@media (prefers-reduced-motion: reduce) { + .custom-range::-ms-thumb { + -ms-transition: none; + transition: none; + } } -.btn-primary { - color: #fff; - background-color: #337ab7; - border-color: #2e6da4; + +.custom-range::-ms-thumb:active { + background-color: #b3d7ff; } -.btn-primary:focus, -.btn-primary.focus { - color: #fff; - background-color: #286090; - border-color: #122b40; + +.custom-range::-ms-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: transparent; + border-color: transparent; + border-width: 0.5rem; } -.btn-primary:hover { - color: #fff; - background-color: #286090; - border-color: #204d74; + +.custom-range::-ms-fill-lower { + background-color: #dee2e6; + border-radius: 1rem; } -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - color: #fff; - background-color: #286090; - border-color: #204d74; -} -.btn-primary:active:hover, -.btn-primary.active:hover, -.open > .dropdown-toggle.btn-primary:hover, -.btn-primary:active:focus, -.btn-primary.active:focus, -.open > .dropdown-toggle.btn-primary:focus, -.btn-primary:active.focus, -.btn-primary.active.focus, -.open > .dropdown-toggle.btn-primary.focus { - color: #fff; - background-color: #204d74; - border-color: #122b40; + +.custom-range::-ms-fill-upper { + margin-right: 15px; + background-color: #dee2e6; + border-radius: 1rem; } -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - background-image: none; + +.custom-range:disabled::-webkit-slider-thumb { + background-color: #adb5bd; } -.btn-primary.disabled, -.btn-primary[disabled], -fieldset[disabled] .btn-primary, -.btn-primary.disabled:hover, -.btn-primary[disabled]:hover, -fieldset[disabled] .btn-primary:hover, -.btn-primary.disabled:focus, -.btn-primary[disabled]:focus, -fieldset[disabled] .btn-primary:focus, -.btn-primary.disabled.focus, -.btn-primary[disabled].focus, -fieldset[disabled] .btn-primary.focus, -.btn-primary.disabled:active, -.btn-primary[disabled]:active, -fieldset[disabled] .btn-primary:active, -.btn-primary.disabled.active, -.btn-primary[disabled].active, -fieldset[disabled] .btn-primary.active { - background-color: #337ab7; - border-color: #2e6da4; -} -.btn-primary .badge { - color: #337ab7; - background-color: #fff; + +.custom-range:disabled::-webkit-slider-runnable-track { + cursor: default; } -.btn-success { - color: #fff; - background-color: #5cb85c; - border-color: #4cae4c; + +.custom-range:disabled::-moz-range-thumb { + background-color: #adb5bd; } -.btn-success:focus, -.btn-success.focus { - color: #fff; - background-color: #449d44; - border-color: #255625; + +.custom-range:disabled::-moz-range-track { + cursor: default; } -.btn-success:hover { - color: #fff; - background-color: #449d44; - border-color: #398439; + +.custom-range:disabled::-ms-thumb { + background-color: #adb5bd; } -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - color: #fff; - background-color: #449d44; - border-color: #398439; -} -.btn-success:active:hover, -.btn-success.active:hover, -.open > .dropdown-toggle.btn-success:hover, -.btn-success:active:focus, -.btn-success.active:focus, -.open > .dropdown-toggle.btn-success:focus, -.btn-success:active.focus, -.btn-success.active.focus, -.open > .dropdown-toggle.btn-success.focus { - color: #fff; - background-color: #398439; - border-color: #255625; + +.custom-control-label::before, +.custom-file-label, +.custom-select { + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - background-image: none; + +@media (prefers-reduced-motion: reduce) { + .custom-control-label::before, + .custom-file-label, + .custom-select { + transition: none; + } } -.btn-success.disabled, -.btn-success[disabled], -fieldset[disabled] .btn-success, -.btn-success.disabled:hover, -.btn-success[disabled]:hover, -fieldset[disabled] .btn-success:hover, -.btn-success.disabled:focus, -.btn-success[disabled]:focus, -fieldset[disabled] .btn-success:focus, -.btn-success.disabled.focus, -.btn-success[disabled].focus, -fieldset[disabled] .btn-success.focus, -.btn-success.disabled:active, -.btn-success[disabled]:active, -fieldset[disabled] .btn-success:active, -.btn-success.disabled.active, -.btn-success[disabled].active, -fieldset[disabled] .btn-success.active { - background-color: #5cb85c; - border-color: #4cae4c; -} -.btn-success .badge { - color: #5cb85c; - background-color: #fff; + +.nav { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; } -.btn-info { - color: #fff; - background-color: #5bc0de; - border-color: #46b8da; + +.nav-link { + display: block; + padding: 0.5rem 1rem; } -.btn-info:focus, -.btn-info.focus { - color: #fff; - background-color: #31b0d5; - border-color: #1b6d85; + +.nav-link:hover, .nav-link:focus { + text-decoration: none; } -.btn-info:hover { - color: #fff; - background-color: #31b0d5; - border-color: #269abc; + +.nav-link.disabled { + color: #6c757d; + pointer-events: none; + cursor: default; } -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - color: #fff; - background-color: #31b0d5; - border-color: #269abc; -} -.btn-info:active:hover, -.btn-info.active:hover, -.open > .dropdown-toggle.btn-info:hover, -.btn-info:active:focus, -.btn-info.active:focus, -.open > .dropdown-toggle.btn-info:focus, -.btn-info:active.focus, -.btn-info.active.focus, -.open > .dropdown-toggle.btn-info.focus { - color: #fff; - background-color: #269abc; - border-color: #1b6d85; + +.nav-tabs { + border-bottom: 1px solid #dee2e6; } -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - background-image: none; + +.nav-tabs .nav-item { + margin-bottom: -1px; +} + +.nav-tabs .nav-link { + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #e9ecef #e9ecef #dee2e6; +} + +.nav-tabs .nav-link.disabled { + color: #6c757d; + background-color: transparent; + border-color: transparent; } -.btn-info.disabled, -.btn-info[disabled], -fieldset[disabled] .btn-info, -.btn-info.disabled:hover, -.btn-info[disabled]:hover, -fieldset[disabled] .btn-info:hover, -.btn-info.disabled:focus, -.btn-info[disabled]:focus, -fieldset[disabled] .btn-info:focus, -.btn-info.disabled.focus, -.btn-info[disabled].focus, -fieldset[disabled] .btn-info.focus, -.btn-info.disabled:active, -.btn-info[disabled]:active, -fieldset[disabled] .btn-info:active, -.btn-info.disabled.active, -.btn-info[disabled].active, -fieldset[disabled] .btn-info.active { - background-color: #5bc0de; - border-color: #46b8da; -} -.btn-info .badge { - color: #5bc0de; + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: #495057; background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; } -.btn-warning { - color: #fff; - background-color: #f0ad4e; - border-color: #eea236; + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; } -.btn-warning:focus, -.btn-warning.focus { - color: #fff; - background-color: #ec971f; - border-color: #985f0d; + +.nav-pills .nav-link { + border-radius: 0.25rem; } -.btn-warning:hover { + +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { color: #fff; - background-color: #ec971f; - border-color: #d58512; + background-color: #007bff; } -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - color: #fff; - background-color: #ec971f; - border-color: #d58512; -} -.btn-warning:active:hover, -.btn-warning.active:hover, -.open > .dropdown-toggle.btn-warning:hover, -.btn-warning:active:focus, -.btn-warning.active:focus, -.open > .dropdown-toggle.btn-warning:focus, -.btn-warning:active.focus, -.btn-warning.active.focus, -.open > .dropdown-toggle.btn-warning.focus { - color: #fff; - background-color: #d58512; - border-color: #985f0d; + +.nav-fill > .nav-link, +.nav-fill .nav-item { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + text-align: center; } -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - background-image: none; + +.nav-justified > .nav-link, +.nav-justified .nav-item { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + text-align: center; } -.btn-warning.disabled, -.btn-warning[disabled], -fieldset[disabled] .btn-warning, -.btn-warning.disabled:hover, -.btn-warning[disabled]:hover, -fieldset[disabled] .btn-warning:hover, -.btn-warning.disabled:focus, -.btn-warning[disabled]:focus, -fieldset[disabled] .btn-warning:focus, -.btn-warning.disabled.focus, -.btn-warning[disabled].focus, -fieldset[disabled] .btn-warning.focus, -.btn-warning.disabled:active, -.btn-warning[disabled]:active, -fieldset[disabled] .btn-warning:active, -.btn-warning.disabled.active, -.btn-warning[disabled].active, -fieldset[disabled] .btn-warning.active { - background-color: #f0ad4e; - border-color: #eea236; -} -.btn-warning .badge { - color: #f0ad4e; - background-color: #fff; + +.tab-content > .tab-pane { + display: none; } -.btn-danger { - color: #fff; - background-color: #d9534f; - border-color: #d43f3a; + +.tab-content > .active { + display: block; } -.btn-danger:focus, -.btn-danger.focus { - color: #fff; - background-color: #c9302c; - border-color: #761c19; + +.navbar { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 0.5rem 1rem; } -.btn-danger:hover { - color: #fff; - background-color: #c9302c; - border-color: #ac2925; + +.navbar .container, +.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: justify; + justify-content: space-between; } -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - color: #fff; - background-color: #c9302c; - border-color: #ac2925; -} -.btn-danger:active:hover, -.btn-danger.active:hover, -.open > .dropdown-toggle.btn-danger:hover, -.btn-danger:active:focus, -.btn-danger.active:focus, -.open > .dropdown-toggle.btn-danger:focus, -.btn-danger:active.focus, -.btn-danger.active.focus, -.open > .dropdown-toggle.btn-danger.focus { - color: #fff; - background-color: #ac2925; - border-color: #761c19; + +.navbar-brand { + display: inline-block; + padding-top: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + line-height: inherit; + white-space: nowrap; } -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - background-image: none; + +.navbar-brand:hover, .navbar-brand:focus { + text-decoration: none; } -.btn-danger.disabled, -.btn-danger[disabled], -fieldset[disabled] .btn-danger, -.btn-danger.disabled:hover, -.btn-danger[disabled]:hover, -fieldset[disabled] .btn-danger:hover, -.btn-danger.disabled:focus, -.btn-danger[disabled]:focus, -fieldset[disabled] .btn-danger:focus, -.btn-danger.disabled.focus, -.btn-danger[disabled].focus, -fieldset[disabled] .btn-danger.focus, -.btn-danger.disabled:active, -.btn-danger[disabled]:active, -fieldset[disabled] .btn-danger:active, -.btn-danger.disabled.active, -.btn-danger[disabled].active, -fieldset[disabled] .btn-danger.active { - background-color: #d9534f; - border-color: #d43f3a; -} -.btn-danger .badge { - color: #d9534f; - background-color: #fff; + +.navbar-nav { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; } -.btn-link { - font-weight: normal; - color: #337ab7; - border-radius: 0; + +.navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; } -.btn-link, -.btn-link:active, -.btn-link.active, -.btn-link[disabled], -fieldset[disabled] .btn-link { - background-color: transparent; - -webkit-box-shadow: none; - box-shadow: none; + +.navbar-nav .dropdown-menu { + position: static; + float: none; } -.btn-link, -.btn-link:hover, -.btn-link:focus, -.btn-link:active { - border-color: transparent; + +.navbar-text { + display: inline-block; + padding-top: 0.5rem; + padding-bottom: 0.5rem; } -.btn-link:hover, -.btn-link:focus { - color: #23527c; - text-decoration: underline; + +.navbar-collapse { + -ms-flex-preferred-size: 100%; + flex-basis: 100%; + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-align: center; + align-items: center; +} + +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; } -.btn-link[disabled]:hover, -fieldset[disabled] .btn-link:hover, -.btn-link[disabled]:focus, -fieldset[disabled] .btn-link:focus { - color: #777; + +.navbar-toggler:hover, .navbar-toggler:focus { text-decoration: none; } -.btn-lg, -.btn-group-lg > .btn { - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; - border-radius: 6px; -} -.btn-sm, -.btn-group-sm > .btn { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + content: ""; + background: no-repeat center center; + background-size: 100% 100%; } -.btn-xs, -.btn-group-xs > .btn { - padding: 1px 5px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; + +@media (max-width: 575.98px) { + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl { + padding-right: 0; + padding-left: 0; + } } -.btn-block { - display: block; - width: 100%; + +@media (min-width: 576px) { + .navbar-expand-sm { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-sm .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } } -.btn-block + .btn-block { - margin-top: 5px; + +@media (max-width: 767.98px) { + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl { + padding-right: 0; + padding-left: 0; + } } -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; + +@media (min-width: 768px) { + .navbar-expand-md { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-md .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } } -.fade { - opacity: 0; - -webkit-transition: opacity .15s linear; - -o-transition: opacity .15s linear; - transition: opacity .15s linear; + +@media (max-width: 991.98px) { + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl { + padding-right: 0; + padding-left: 0; + } } -.fade.in { - opacity: 1; + +@media (min-width: 992px) { + .navbar-expand-lg { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-lg .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } } -.collapse { - display: none; + +@media (max-width: 1199.98px) { + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl { + padding-right: 0; + padding-left: 0; + } } -.collapse.in { - display: block; + +@media (min-width: 1200px) { + .navbar-expand-xl { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-xl .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } } -tr.collapse.in { - display: table-row; + +.navbar-expand { + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -ms-flex-pack: start; + justify-content: flex-start; } -tbody.collapse.in { - display: table-row-group; + +.navbar-expand > .container, +.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl { + padding-right: 0; + padding-left: 0; } -.collapsing { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition-timing-function: ease; - -o-transition-timing-function: ease; - transition-timing-function: ease; - -webkit-transition-duration: .35s; - -o-transition-duration: .35s; - transition-duration: .35s; - -webkit-transition-property: height, visibility; - -o-transition-property: height, visibility; - transition-property: height, visibility; -} -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: 4px dashed; - border-top: 4px solid \9; - border-right: 4px solid transparent; - border-left: 4px solid transparent; + +.navbar-expand .navbar-nav { + -ms-flex-direction: row; + flex-direction: row; } -.dropup, -.dropdown { - position: relative; + +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; } -.dropdown-toggle:focus { - outline: 0; + +.navbar-expand .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; } -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; - font-size: 14px; - text-align: left; - list-style: none; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, .15); - border-radius: 4px; - -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); - box-shadow: 0 6px 12px rgba(0, 0, 0, .175); -} -.dropdown-menu.pull-right { - right: 0; - left: auto; + +.navbar-expand > .container, +.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; } -.dropdown-menu .divider { - height: 1px; - margin: 9px 0; - overflow: hidden; - background-color: #e5e5e5; + +.navbar-expand .navbar-collapse { + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; } -.dropdown-menu > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 1.42857143; - color: #333; - white-space: nowrap; + +.navbar-expand .navbar-toggler { + display: none; } -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - color: #262626; - text-decoration: none; - background-color: #f5f5f5; + +.navbar-light .navbar-brand { + color: rgba(0, 0, 0, 0.9); } -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - color: #fff; - text-decoration: none; - background-color: #337ab7; - outline: 0; + +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); } -.dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - color: #777; + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); } -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - text-decoration: none; - cursor: not-allowed; - background-color: transparent; - background-image: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); } -.open > .dropdown-menu { - display: block; + +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); } -.open > a { - outline: 0; + +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.show, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); } -.dropdown-menu-right { - right: 0; - left: auto; + +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 0, 0, 0.1); } -.dropdown-menu-left { - right: auto; - left: 0; + +.navbar-light .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } -.dropdown-header { - display: block; - padding: 3px 20px; - font-size: 12px; - line-height: 1.42857143; - color: #777; - white-space: nowrap; + +.navbar-light .navbar-text { + color: rgba(0, 0, 0, 0.5); } -.dropdown-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 990; + +.navbar-light .navbar-text a { + color: rgba(0, 0, 0, 0.9); } -.pull-right > .dropdown-menu { - right: 0; - left: auto; + +.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); } -.dropup .caret, -.navbar-fixed-bottom .dropdown .caret { - content: ""; - border-top: 0; - border-bottom: 4px dashed; - border-bottom: 4px solid \9; + +.navbar-dark .navbar-brand { + color: #fff; } -.dropup .dropdown-menu, -.navbar-fixed-bottom .dropdown .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 2px; + +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; } -@media (min-width: 768px) { - .navbar-right .dropdown-menu { - right: 0; - left: auto; - } - .navbar-right .dropdown-menu-left { - right: auto; - left: 0; - } + +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); } -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-block; - vertical-align: middle; + +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); } -.btn-group > .btn, -.btn-group-vertical > .btn { - position: relative; - float: left; + +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); } -.btn-group > .btn:hover, -.btn-group-vertical > .btn:hover, -.btn-group > .btn:focus, -.btn-group-vertical > .btn:focus, -.btn-group > .btn:active, -.btn-group-vertical > .btn:active, -.btn-group > .btn.active, -.btn-group-vertical > .btn.active { - z-index: 2; + +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .nav-link.show, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; } -.btn-group .btn + .btn, -.btn-group .btn + .btn-group, -.btn-group .btn-group + .btn, -.btn-group .btn-group + .btn-group { - margin-left: -1px; + +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.5); + border-color: rgba(255, 255, 255, 0.1); } -.btn-toolbar { - margin-left: -5px; + +.navbar-dark .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } -.btn-toolbar .btn, -.btn-toolbar .btn-group, -.btn-toolbar .input-group { - float: left; + +.navbar-dark .navbar-text { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-text a { + color: #fff; } -.btn-toolbar > .btn, -.btn-toolbar > .btn-group, -.btn-toolbar > .input-group { - margin-left: 5px; + +.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { + color: #fff; } -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; + +.card { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; } -.btn-group > .btn:first-child { + +.card > hr { + margin-right: 0; margin-left: 0; } -.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; + +.card > .list-group { + border-top: inherit; + border-bottom: inherit; } -.btn-group > .btn:last-child:not(:first-child), -.btn-group > .dropdown-toggle:not(:first-child) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + +.card > .list-group:first-child { + border-top-width: 0; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); } -.btn-group > .btn-group { - float: left; + +.card > .list-group:last-child { + border-bottom-width: 0; + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); } -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; + +.card > .card-header + .list-group, +.card > .list-group + .card-footer { + border-top: 0; } -.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { - border-top-right-radius: 0; - border-bottom-right-radius: 0; + +.card-body { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + min-height: 1px; + padding: 1.25rem; } -.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + +.card-title { + margin-bottom: 0.75rem; } -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; + +.card-subtitle { + margin-top: -0.375rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link:hover { + text-decoration: none; } -.btn-group > .btn + .dropdown-toggle { - padding-right: 8px; - padding-left: 8px; + +.card-link + .card-link { + margin-left: 1.25rem; } -.btn-group > .btn-lg + .dropdown-toggle { - padding-right: 12px; - padding-left: 12px; + +.card-header { + padding: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); } -.btn-group.open .dropdown-toggle { - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; } -.btn-group.open .dropdown-toggle.btn-link { - -webkit-box-shadow: none; - box-shadow: none; + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); } -.btn .caret { - margin-left: 0; + +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); } -.btn-lg .caret { - border-width: 5px 5px 0; - border-bottom-width: 0; + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; } -.dropup .btn-lg .caret { - border-width: 0 5px 5px; + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; } -.btn-group-vertical > .btn, -.btn-group-vertical > .btn-group, -.btn-group-vertical > .btn-group > .btn { - display: block; - float: none; + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; + border-radius: calc(0.25rem - 1px); +} + +.card-img, +.card-img-top, +.card-img-bottom { + -ms-flex-negative: 0; + flex-shrink: 0; width: 100%; - max-width: 100%; } -.btn-group-vertical > .btn-group > .btn { - float: none; + +.card-img, +.card-img-top { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); } -.btn-group-vertical > .btn + .btn, -.btn-group-vertical > .btn + .btn-group, -.btn-group-vertical > .btn-group + .btn, -.btn-group-vertical > .btn-group + .btn-group { - margin-top: -1px; - margin-left: 0; + +.card-img, +.card-img-bottom { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); } -.btn-group-vertical > .btn:not(:first-child):not(:last-child) { - border-radius: 0; + +.card-deck .card { + margin-bottom: 15px; } -.btn-group-vertical > .btn:first-child:not(:last-child) { - border-top-right-radius: 4px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; + +@media (min-width: 576px) { + .card-deck { + display: -ms-flexbox; + display: flex; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + margin-right: -15px; + margin-left: -15px; + } + .card-deck .card { + -ms-flex: 1 0 0%; + flex: 1 0 0%; + margin-right: 15px; + margin-bottom: 0; + margin-left: 15px; + } } -.btn-group-vertical > .btn:last-child:not(:first-child) { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-left-radius: 4px; + +.card-group > .card { + margin-bottom: 15px; } -.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; + +@media (min-width: 576px) { + .card-group { + display: -ms-flexbox; + display: flex; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + } + .card-group > .card { + -ms-flex: 1 0 0%; + flex: 1 0 0%; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-top, + .card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, + .card-group > .card:not(:last-child) .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-top, + .card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, + .card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.card-columns .card { + margin-bottom: 0.75rem; +} + +@media (min-width: 576px) { + .card-columns { + -webkit-column-count: 3; + -moz-column-count: 3; + column-count: 3; + -webkit-column-gap: 1.25rem; + -moz-column-gap: 1.25rem; + column-gap: 1.25rem; + orphans: 1; + widows: 1; + } + .card-columns .card { + display: inline-block; + width: 100%; + } +} + +.accordion { + overflow-anchor: none; } -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + +.accordion > .card { + overflow: hidden; +} + +.accordion > .card:not(:last-of-type) { + border-bottom: 0; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } -.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + +.accordion > .card:not(:first-of-type) { border-top-left-radius: 0; border-top-right-radius: 0; } -.btn-group-justified { - display: table; - width: 100%; - table-layout: fixed; - border-collapse: separate; + +.accordion > .card > .card-header { + border-radius: 0; + margin-bottom: -1px; } -.btn-group-justified > .btn, -.btn-group-justified > .btn-group { - display: table-cell; - float: none; - width: 1%; + +.breadcrumb { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #e9ecef; + border-radius: 0.25rem; } -.btn-group-justified > .btn-group .btn { - width: 100%; + +.breadcrumb-item { + display: -ms-flexbox; + display: flex; } -.btn-group-justified > .btn-group .dropdown-menu { - left: auto; + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; } -[data-toggle="buttons"] > .btn input[type="radio"], -[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], -[data-toggle="buttons"] > .btn input[type="checkbox"], -[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.5rem; + color: #6c757d; + content: "/"; } -.input-group { - position: relative; - display: table; - border-collapse: separate; + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: underline; } -.input-group[class*="col-"] { - float: none; - padding-right: 0; + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: none; +} + +.breadcrumb-item.active { + color: #6c757d; +} + +.pagination { + display: -ms-flexbox; + display: flex; padding-left: 0; + list-style: none; + border-radius: 0.25rem; } -.input-group .form-control { + +.page-link { position: relative; + display: block; + padding: 0.5rem 0.75rem; + margin-left: -1px; + line-height: 1.25; + color: #007bff; + background-color: #fff; + border: 1px solid #dee2e6; +} + +.page-link:hover { z-index: 2; - float: left; - width: 100%; - margin-bottom: 0; + color: #0056b3; + text-decoration: none; + background-color: #e9ecef; + border-color: #dee2e6; } -.input-group-lg > .form-control, -.input-group-lg > .input-group-addon, -.input-group-lg > .input-group-btn > .btn { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; - border-radius: 6px; -} -select.input-group-lg > .form-control, -select.input-group-lg > .input-group-addon, -select.input-group-lg > .input-group-btn > .btn { - height: 46px; - line-height: 46px; -} -textarea.input-group-lg > .form-control, -textarea.input-group-lg > .input-group-addon, -textarea.input-group-lg > .input-group-btn > .btn, -select[multiple].input-group-lg > .form-control, -select[multiple].input-group-lg > .input-group-addon, -select[multiple].input-group-lg > .input-group-btn > .btn { - height: auto; + +.page-link:focus { + z-index: 3; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } -.input-group-sm > .form-control, -.input-group-sm > .input-group-addon, -.input-group-sm > .input-group-btn > .btn { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -select.input-group-sm > .form-control, -select.input-group-sm > .input-group-addon, -select.input-group-sm > .input-group-btn > .btn { - height: 30px; - line-height: 30px; -} -textarea.input-group-sm > .form-control, -textarea.input-group-sm > .input-group-addon, -textarea.input-group-sm > .input-group-btn > .btn, -select[multiple].input-group-sm > .form-control, -select[multiple].input-group-sm > .input-group-addon, -select[multiple].input-group-sm > .input-group-btn > .btn { - height: auto; + +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; } -.input-group-addon, -.input-group-btn, -.input-group .form-control { - display: table-cell; + +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; } -.input-group-addon:not(:first-child):not(:last-child), -.input-group-btn:not(:first-child):not(:last-child), -.input-group .form-control:not(:first-child):not(:last-child) { - border-radius: 0; + +.page-item.active .page-link { + z-index: 3; + color: #fff; + background-color: #007bff; + border-color: #007bff; } -.input-group-addon, -.input-group-btn { - width: 1%; - white-space: nowrap; - vertical-align: middle; + +.page-item.disabled .page-link { + color: #6c757d; + pointer-events: none; + cursor: auto; + background-color: #fff; + border-color: #dee2e6; } -.input-group-addon { - padding: 6px 12px; - font-size: 14px; - font-weight: normal; - line-height: 1; - color: #555; - text-align: center; - background-color: #eee; - border: 1px solid #ccc; - border-radius: 4px; -} -.input-group-addon.input-sm { - padding: 5px 10px; - font-size: 12px; - border-radius: 3px; -} -.input-group-addon.input-lg { - padding: 10px 16px; - font-size: 18px; - border-radius: 6px; -} -.input-group-addon input[type="radio"], -.input-group-addon input[type="checkbox"] { - margin-top: 0; + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; + line-height: 1.5; } -.input-group .form-control:first-child, -.input-group-addon:first-child, -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group > .btn, -.input-group-btn:first-child > .dropdown-toggle, -.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), -.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.input-group-addon:first-child { - border-right: 0; -} -.input-group .form-control:last-child, -.input-group-addon:last-child, -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group > .btn, -.input-group-btn:last-child > .dropdown-toggle, -.input-group-btn:first-child > .btn:not(:first-child), -.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; } -.input-group-addon:last-child { - border-left: 0; + +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; } -.input-group-btn { - position: relative; - font-size: 0; - white-space: nowrap; + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; } -.input-group-btn > .btn { - position: relative; + +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; } -.input-group-btn > .btn + .btn { - margin-left: -1px; + +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; } -.input-group-btn > .btn:hover, -.input-group-btn > .btn:focus, -.input-group-btn > .btn:active { - z-index: 2; + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group { - margin-right: -1px; + +@media (prefers-reduced-motion: reduce) { + .badge { + transition: none; + } } -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group { - z-index: 2; - margin-left: -1px; + +a.badge:hover, a.badge:focus { + text-decoration: none; } -.nav { - padding-left: 0; - margin-bottom: 0; - list-style: none; + +.badge:empty { + display: none; } -.nav > li { + +.btn .badge { position: relative; - display: block; + top: -1px; } -.nav > li > a { - position: relative; - display: block; - padding: 10px 15px; + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; } -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; - background-color: #eee; + +.badge-primary { + color: #fff; + background-color: #007bff; } -.nav > li.disabled > a { - color: #777; + +a.badge-primary:hover, a.badge-primary:focus { + color: #fff; + background-color: #0062cc; } -.nav > li.disabled > a:hover, -.nav > li.disabled > a:focus { - color: #777; - text-decoration: none; - cursor: not-allowed; - background-color: transparent; + +a.badge-primary:focus, a.badge-primary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } -.nav .open > a, -.nav .open > a:hover, -.nav .open > a:focus { - background-color: #eee; - border-color: #337ab7; + +.badge-secondary { + color: #fff; + background-color: #6c757d; } -.nav .nav-divider { - height: 1px; - margin: 9px 0; - overflow: hidden; - background-color: #e5e5e5; + +a.badge-secondary:hover, a.badge-secondary:focus { + color: #fff; + background-color: #545b62; } -.nav > li > a > img { - max-width: none; + +a.badge-secondary:focus, a.badge-secondary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } -.nav-tabs { - border-bottom: 1px solid #ddd; + +.badge-success { + color: #fff; + background-color: #28a745; } -.nav-tabs > li { - float: left; - margin-bottom: -1px; + +a.badge-success:hover, a.badge-success:focus { + color: #fff; + background-color: #1e7e34; } -.nav-tabs > li > a { - margin-right: 2px; - line-height: 1.42857143; - border: 1px solid transparent; - border-radius: 4px 4px 0 0; + +a.badge-success:focus, a.badge-success.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } -.nav-tabs > li > a:hover { - border-color: #eee #eee #ddd; + +.badge-info { + color: #fff; + background-color: #17a2b8; } -.nav-tabs > li.active > a, -.nav-tabs > li.active > a:hover, -.nav-tabs > li.active > a:focus { - color: #555; - cursor: default; - background-color: #fff; - border: 1px solid #ddd; - border-bottom-color: transparent; + +a.badge-info:hover, a.badge-info:focus { + color: #fff; + background-color: #117a8b; } -.nav-tabs.nav-justified { - width: 100%; - border-bottom: 0; + +a.badge-info:focus, a.badge-info.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } -.nav-tabs.nav-justified > li { - float: none; + +.badge-warning { + color: #212529; + background-color: #ffc107; } -.nav-tabs.nav-justified > li > a { - margin-bottom: 5px; - text-align: center; + +a.badge-warning:hover, a.badge-warning:focus { + color: #212529; + background-color: #d39e00; } -.nav-tabs.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; + +a.badge-warning:focus, a.badge-warning.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } -@media (min-width: 768px) { - .nav-tabs.nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-tabs.nav-justified > li > a { - margin-bottom: 0; - } + +.badge-danger { + color: #fff; + background-color: #dc3545; } -.nav-tabs.nav-justified > li > a { - margin-right: 0; - border-radius: 4px; + +a.badge-danger:hover, a.badge-danger:focus { + color: #fff; + background-color: #bd2130; } -.nav-tabs.nav-justified > .active > a, -.nav-tabs.nav-justified > .active > a:hover, -.nav-tabs.nav-justified > .active > a:focus { - border: 1px solid #ddd; + +a.badge-danger:focus, a.badge-danger.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } -@media (min-width: 768px) { - .nav-tabs.nav-justified > li > a { - border-bottom: 1px solid #ddd; - border-radius: 4px 4px 0 0; - } - .nav-tabs.nav-justified > .active > a, - .nav-tabs.nav-justified > .active > a:hover, - .nav-tabs.nav-justified > .active > a:focus { - border-bottom-color: #fff; - } + +.badge-light { + color: #212529; + background-color: #f8f9fa; } -.nav-pills > li { - float: left; + +a.badge-light:hover, a.badge-light:focus { + color: #212529; + background-color: #dae0e5; } -.nav-pills > li > a { - border-radius: 4px; + +a.badge-light:focus, a.badge-light.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } -.nav-pills > li + li { - margin-left: 2px; + +.badge-dark { + color: #fff; + background-color: #343a40; } -.nav-pills > li.active > a, -.nav-pills > li.active > a:hover, -.nav-pills > li.active > a:focus { + +a.badge-dark:hover, a.badge-dark:focus { color: #fff; - background-color: #337ab7; + background-color: #1d2124; } -.nav-stacked > li { - float: none; + +a.badge-dark:focus, a.badge-dark.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } -.nav-stacked > li + li { - margin-top: 2px; - margin-left: 0; + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #e9ecef; + border-radius: 0.3rem; } -.nav-justified { - width: 100%; + +@media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; + } } -.nav-justified > li { - float: none; + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; } -.nav-justified > li > a { - margin-bottom: 5px; - text-align: center; + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; } -.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; + +.alert-heading { + color: inherit; } -@media (min-width: 768px) { - .nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-justified > li > a { - margin-bottom: 0; - } + +.alert-link { + font-weight: 700; } -.nav-tabs-justified { - border-bottom: 0; + +.alert-dismissible { + padding-right: 4rem; } -.nav-tabs-justified > li > a { - margin-right: 0; - border-radius: 4px; + +.alert-dismissible .close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 0.75rem 1.25rem; + color: inherit; } -.nav-tabs-justified > .active > a, -.nav-tabs-justified > .active > a:hover, -.nav-tabs-justified > .active > a:focus { - border: 1px solid #ddd; + +.alert-primary { + color: #004085; + background-color: #cce5ff; + border-color: #b8daff; } -@media (min-width: 768px) { - .nav-tabs-justified > li > a { - border-bottom: 1px solid #ddd; - border-radius: 4px 4px 0 0; - } - .nav-tabs-justified > .active > a, - .nav-tabs-justified > .active > a:hover, - .nav-tabs-justified > .active > a:focus { - border-bottom-color: #fff; - } + +.alert-primary hr { + border-top-color: #9fcdff; } -.tab-content > .tab-pane { - display: none; + +.alert-primary .alert-link { + color: #002752; } -.tab-content > .active { - display: block; + +.alert-secondary { + color: #383d41; + background-color: #e2e3e5; + border-color: #d6d8db; } -.nav-tabs .dropdown-menu { - margin-top: -1px; - border-top-left-radius: 0; - border-top-right-radius: 0; + +.alert-secondary hr { + border-top-color: #c8cbcf; } -.navbar { - position: relative; - min-height: 50px; - margin-bottom: 20px; - border: 1px solid transparent; + +.alert-secondary .alert-link { + color: #202326; } -@media (min-width: 768px) { - .navbar { - border-radius: 4px; - } + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; } -@media (min-width: 768px) { - .navbar-header { - float: left; - } + +.alert-success hr { + border-top-color: #b1dfbb; } -.navbar-collapse { - padding-right: 15px; - padding-left: 15px; - overflow-x: visible; - -webkit-overflow-scrolling: touch; - border-top: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); + +.alert-success .alert-link { + color: #0b2e13; } -.navbar-collapse.in { - overflow-y: auto; + +.alert-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; } -@media (min-width: 768px) { - .navbar-collapse { - width: auto; - border-top: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-collapse.collapse { - display: block !important; - height: auto !important; - padding-bottom: 0; - overflow: visible !important; - } - .navbar-collapse.in { - overflow-y: visible; - } - .navbar-fixed-top .navbar-collapse, - .navbar-static-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - padding-right: 0; - padding-left: 0; - } + +.alert-info hr { + border-top-color: #abdde5; } -.navbar-fixed-top .navbar-collapse, -.navbar-fixed-bottom .navbar-collapse { - max-height: 340px; + +.alert-info .alert-link { + color: #062c33; } -@media (max-device-width: 480px) and (orientation: landscape) { - .navbar-fixed-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - max-height: 200px; - } + +.alert-warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; } -.container > .navbar-header, -.container-fluid > .navbar-header, -.container > .navbar-collapse, -.container-fluid > .navbar-collapse { - margin-right: -15px; - margin-left: -15px; + +.alert-warning hr { + border-top-color: #ffe8a1; } -@media (min-width: 768px) { - .container > .navbar-header, - .container-fluid > .navbar-header, - .container > .navbar-collapse, - .container-fluid > .navbar-collapse { - margin-right: 0; - margin-left: 0; - } + +.alert-warning .alert-link { + color: #533f03; } -.navbar-static-top { - z-index: 1000; - border-width: 0 0 1px; + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; } -@media (min-width: 768px) { - .navbar-static-top { - border-radius: 0; - } + +.alert-danger hr { + border-top-color: #f1b0b7; } -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; + +.alert-danger .alert-link { + color: #491217; } -@media (min-width: 768px) { - .navbar-fixed-top, - .navbar-fixed-bottom { - border-radius: 0; - } + +.alert-light { + color: #818182; + background-color: #fefefe; + border-color: #fdfdfe; } -.navbar-fixed-top { - top: 0; - border-width: 0 0 1px; + +.alert-light hr { + border-top-color: #ececf6; } -.navbar-fixed-bottom { - bottom: 0; - margin-bottom: 0; - border-width: 1px 0 0; + +.alert-light .alert-link { + color: #686868; } -.navbar-brand { - float: left; - height: 50px; - padding: 15px 15px; - font-size: 18px; - line-height: 20px; + +.alert-dark { + color: #1b1e21; + background-color: #d6d8d9; + border-color: #c6c8ca; } -.navbar-brand:hover, -.navbar-brand:focus { - text-decoration: none; + +.alert-dark hr { + border-top-color: #b9bbbe; } -.navbar-brand > img { - display: block; + +.alert-dark .alert-link { + color: #040505; } -@media (min-width: 768px) { - .navbar > .container .navbar-brand, - .navbar > .container-fluid .navbar-brand { - margin-left: -15px; + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; } } -.navbar-toggle { - position: relative; - float: right; - padding: 9px 10px; - margin-top: 8px; - margin-right: 15px; - margin-bottom: 8px; - background-color: transparent; - background-image: none; - border: 1px solid transparent; - border-radius: 4px; -} -.navbar-toggle:focus { - outline: 0; + +@keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } } -.navbar-toggle .icon-bar { - display: block; - width: 22px; - height: 2px; - border-radius: 1px; + +.progress { + display: -ms-flexbox; + display: flex; + height: 1rem; + overflow: hidden; + line-height: 0; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.25rem; } -.navbar-toggle .icon-bar + .icon-bar { - margin-top: 4px; + +.progress-bar { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-pack: center; + justify-content: center; + overflow: hidden; + color: #fff; + text-align: center; + white-space: nowrap; + background-color: #007bff; + transition: width 0.6s ease; } -@media (min-width: 768px) { - .navbar-toggle { - display: none; + +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; } } -.navbar-nav { - margin: 7.5px -15px; + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; } -.navbar-nav > li > a { - padding-top: 10px; - padding-bottom: 10px; - line-height: 20px; + +.progress-bar-animated { + -webkit-animation: progress-bar-stripes 1s linear infinite; + animation: progress-bar-stripes 1s linear infinite; } -@media (max-width: 767px) { - .navbar-nav .open .dropdown-menu { - position: static; - float: none; - width: auto; - margin-top: 0; - background-color: transparent; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; + +@media (prefers-reduced-motion: reduce) { + .progress-bar-animated { + -webkit-animation: none; + animation: none; } - .navbar-nav .open .dropdown-menu > li > a, - .navbar-nav .open .dropdown-menu .dropdown-header { - padding: 5px 15px 5px 25px; +} + +.media { + display: -ms-flexbox; + display: flex; + -ms-flex-align: start; + align-items: flex-start; +} + +.media-body { + -ms-flex: 1; + flex: 1; +} + +.list-group { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + border-radius: 0.25rem; +} + +.list-group-item-action { + width: 100%; + color: #495057; + text-align: inherit; +} + +.list-group-item-action:hover, .list-group-item-action:focus { + z-index: 1; + color: #495057; + text-decoration: none; + background-color: #f8f9fa; +} + +.list-group-item-action:active { + color: #212529; + background-color: #e9ecef; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.75rem 1.25rem; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} + +.list-group-item:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} + +.list-group-item:last-child { + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; +} + +.list-group-item.disabled, .list-group-item:disabled { + color: #6c757d; + pointer-events: none; + background-color: #fff; +} + +.list-group-item.active { + z-index: 2; + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.list-group-item + .list-group-item { + border-top-width: 0; +} + +.list-group-item + .list-group-item.active { + margin-top: -1px; + border-top-width: 1px; +} + +.list-group-horizontal { + -ms-flex-direction: row; + flex-direction: row; +} + +.list-group-horizontal > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; +} + +.list-group-horizontal > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; +} + +.list-group-horizontal > .list-group-item.active { + margin-top: 0; +} + +.list-group-horizontal > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; +} + +.list-group-horizontal > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; +} + +@media (min-width: 576px) { + .list-group-horizontal-sm { + -ms-flex-direction: row; + flex-direction: row; } - .navbar-nav .open .dropdown-menu > li > a { - line-height: 20px; + .list-group-horizontal-sm > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } - .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-nav .open .dropdown-menu > li > a:focus { - background-image: none; + .list-group-horizontal-sm > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } -} -@media (min-width: 768px) { - .navbar-nav { - float: left; - margin: 0; + .list-group-horizontal-sm > .list-group-item.active { + margin-top: 0; } - .navbar-nav > li { - float: left; + .list-group-horizontal-sm > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; } - .navbar-nav > li > a { - padding-top: 15px; - padding-bottom: 15px; + .list-group-horizontal-sm > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } } -.navbar-form { - padding: 10px 15px; - margin-top: 8px; - margin-right: -15px; - margin-bottom: 8px; - margin-left: -15px; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); -} + @media (min-width: 768px) { - .navbar-form .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .form-control { - display: inline-block; - width: auto; - vertical-align: middle; + .list-group-horizontal-md { + -ms-flex-direction: row; + flex-direction: row; } - .navbar-form .form-control-static { - display: inline-block; + .list-group-horizontal-md > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } - .navbar-form .input-group { - display: inline-table; - vertical-align: middle; + .list-group-horizontal-md > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } - .navbar-form .input-group .input-group-addon, - .navbar-form .input-group .input-group-btn, - .navbar-form .input-group .form-control { - width: auto; + .list-group-horizontal-md > .list-group-item.active { + margin-top: 0; } - .navbar-form .input-group > .form-control { - width: 100%; + .list-group-horizontal-md > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; } - .navbar-form .control-label { - margin-bottom: 0; - vertical-align: middle; + .list-group-horizontal-md > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } - .navbar-form .radio, - .navbar-form .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; +} + +@media (min-width: 992px) { + .list-group-horizontal-lg { + -ms-flex-direction: row; + flex-direction: row; } - .navbar-form .radio label, - .navbar-form .checkbox label { - padding-left: 0; + .list-group-horizontal-lg > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } - .navbar-form .radio input[type="radio"], - .navbar-form .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; + .list-group-horizontal-lg > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } - .navbar-form .has-feedback .form-control-feedback { - top: 0; + .list-group-horizontal-lg > .list-group-item.active { + margin-top: 0; } -} -@media (max-width: 767px) { - .navbar-form .form-group { - margin-bottom: 5px; + .list-group-horizontal-lg > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; } - .navbar-form .form-group:last-child { - margin-bottom: 0; + .list-group-horizontal-lg > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } } -@media (min-width: 768px) { - .navbar-form { - width: auto; - padding-top: 0; - padding-bottom: 0; - margin-right: 0; - margin-left: 0; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; + +@media (min-width: 1200px) { + .list-group-horizontal-xl { + -ms-flex-direction: row; + flex-direction: row; } -} -.navbar-nav > li > .dropdown-menu { - margin-top: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { - margin-bottom: 0; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.navbar-btn { - margin-top: 8px; - margin-bottom: 8px; -} -.navbar-btn.btn-sm { - margin-top: 10px; - margin-bottom: 10px; -} -.navbar-btn.btn-xs { - margin-top: 14px; - margin-bottom: 14px; -} -.navbar-text { - margin-top: 15px; - margin-bottom: 15px; -} -@media (min-width: 768px) { - .navbar-text { - float: left; - margin-right: 15px; - margin-left: 15px; + .list-group-horizontal-xl > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } -} -@media (min-width: 768px) { - .navbar-left { - float: left !important; + .list-group-horizontal-xl > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } - .navbar-right { - float: right !important; - margin-right: -15px; + .list-group-horizontal-xl > .list-group-item.active { + margin-top: 0; } - .navbar-right ~ .navbar-right { - margin-right: 0; + .list-group-horizontal-xl > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } } -.navbar-default { - background-color: #f8f8f8; - border-color: #e7e7e7; -} -.navbar-default .navbar-brand { - color: #777; + +.list-group-flush { + border-radius: 0; } -.navbar-default .navbar-brand:hover, -.navbar-default .navbar-brand:focus { - color: #5e5e5e; - background-color: transparent; + +.list-group-flush > .list-group-item { + border-width: 0 0 1px; } -.navbar-default .navbar-text { - color: #777; + +.list-group-flush > .list-group-item:last-child { + border-bottom-width: 0; } -.navbar-default .navbar-nav > li > a { - color: #777; + +.list-group-item-primary { + color: #004085; + background-color: #b8daff; } -.navbar-default .navbar-nav > li > a:hover, -.navbar-default .navbar-nav > li > a:focus { - color: #333; - background-color: transparent; + +.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { + color: #004085; + background-color: #9fcdff; } -.navbar-default .navbar-nav > .active > a, -.navbar-default .navbar-nav > .active > a:hover, -.navbar-default .navbar-nav > .active > a:focus { - color: #555; - background-color: #e7e7e7; + +.list-group-item-primary.list-group-item-action.active { + color: #fff; + background-color: #004085; + border-color: #004085; } -.navbar-default .navbar-nav > .disabled > a, -.navbar-default .navbar-nav > .disabled > a:hover, -.navbar-default .navbar-nav > .disabled > a:focus { - color: #ccc; - background-color: transparent; + +.list-group-item-secondary { + color: #383d41; + background-color: #d6d8db; } -.navbar-default .navbar-toggle { - border-color: #ddd; + +.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { + color: #383d41; + background-color: #c8cbcf; } -.navbar-default .navbar-toggle:hover, -.navbar-default .navbar-toggle:focus { - background-color: #ddd; + +.list-group-item-secondary.list-group-item-action.active { + color: #fff; + background-color: #383d41; + border-color: #383d41; } -.navbar-default .navbar-toggle .icon-bar { - background-color: #888; + +.list-group-item-success { + color: #155724; + background-color: #c3e6cb; } -.navbar-default .navbar-collapse, -.navbar-default .navbar-form { - border-color: #e7e7e7; + +.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { + color: #155724; + background-color: #b1dfbb; } -.navbar-default .navbar-nav > .open > a, -.navbar-default .navbar-nav > .open > a:hover, -.navbar-default .navbar-nav > .open > a:focus { - color: #555; - background-color: #e7e7e7; + +.list-group-item-success.list-group-item-action.active { + color: #fff; + background-color: #155724; + border-color: #155724; } -@media (max-width: 767px) { - .navbar-default .navbar-nav .open .dropdown-menu > li > a { - color: #777; - } - .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { - color: #333; - background-color: transparent; - } - .navbar-default .navbar-nav .open .dropdown-menu > .active > a, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #555; - background-color: #e7e7e7; - } - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #ccc; - background-color: transparent; - } + +.list-group-item-info { + color: #0c5460; + background-color: #bee5eb; } -.navbar-default .navbar-link { - color: #777; + +.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { + color: #0c5460; + background-color: #abdde5; } -.navbar-default .navbar-link:hover { - color: #333; + +.list-group-item-info.list-group-item-action.active { + color: #fff; + background-color: #0c5460; + border-color: #0c5460; } -.navbar-default .btn-link { - color: #777; + +.list-group-item-warning { + color: #856404; + background-color: #ffeeba; } -.navbar-default .btn-link:hover, -.navbar-default .btn-link:focus { - color: #333; + +.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { + color: #856404; + background-color: #ffe8a1; } -.navbar-default .btn-link[disabled]:hover, -fieldset[disabled] .navbar-default .btn-link:hover, -.navbar-default .btn-link[disabled]:focus, -fieldset[disabled] .navbar-default .btn-link:focus { - color: #ccc; + +.list-group-item-warning.list-group-item-action.active { + color: #fff; + background-color: #856404; + border-color: #856404; } -.navbar-inverse { - background-color: #222; - border-color: #080808; + +.list-group-item-danger { + color: #721c24; + background-color: #f5c6cb; } -.navbar-inverse .navbar-brand { - color: #9d9d9d; + +.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { + color: #721c24; + background-color: #f1b0b7; } -.navbar-inverse .navbar-brand:hover, -.navbar-inverse .navbar-brand:focus { + +.list-group-item-danger.list-group-item-action.active { color: #fff; - background-color: transparent; + background-color: #721c24; + border-color: #721c24; } -.navbar-inverse .navbar-text { - color: #9d9d9d; + +.list-group-item-light { + color: #818182; + background-color: #fdfdfe; } -.navbar-inverse .navbar-nav > li > a { - color: #9d9d9d; + +.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { + color: #818182; + background-color: #ececf6; } -.navbar-inverse .navbar-nav > li > a:hover, -.navbar-inverse .navbar-nav > li > a:focus { + +.list-group-item-light.list-group-item-action.active { color: #fff; - background-color: transparent; + background-color: #818182; + border-color: #818182; } -.navbar-inverse .navbar-nav > .active > a, -.navbar-inverse .navbar-nav > .active > a:hover, -.navbar-inverse .navbar-nav > .active > a:focus { - color: #fff; - background-color: #080808; + +.list-group-item-dark { + color: #1b1e21; + background-color: #c6c8ca; } -.navbar-inverse .navbar-nav > .disabled > a, -.navbar-inverse .navbar-nav > .disabled > a:hover, -.navbar-inverse .navbar-nav > .disabled > a:focus { - color: #444; - background-color: transparent; + +.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { + color: #1b1e21; + background-color: #b9bbbe; } -.navbar-inverse .navbar-toggle { - border-color: #333; + +.list-group-item-dark.list-group-item-action.active { + color: #fff; + background-color: #1b1e21; + border-color: #1b1e21; } -.navbar-inverse .navbar-toggle:hover, -.navbar-inverse .navbar-toggle:focus { - background-color: #333; + +.close { + float: right; + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .5; } -.navbar-inverse .navbar-toggle .icon-bar { - background-color: #fff; + +.close:hover { + color: #000; + text-decoration: none; } -.navbar-inverse .navbar-collapse, -.navbar-inverse .navbar-form { - border-color: #101010; + +.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus { + opacity: .75; } -.navbar-inverse .navbar-nav > .open > a, -.navbar-inverse .navbar-nav > .open > a:hover, -.navbar-inverse .navbar-nav > .open > a:focus { - color: #fff; - background-color: #080808; + +button.close { + padding: 0; + background-color: transparent; + border: 0; } -@media (max-width: 767px) { - .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { - border-color: #080808; - } - .navbar-inverse .navbar-nav .open .dropdown-menu .divider { - background-color: #080808; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { - color: #9d9d9d; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { - color: #fff; - background-color: transparent; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #fff; - background-color: #080808; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #444; - background-color: transparent; - } + +a.close.disabled { + pointer-events: none; } -.navbar-inverse .navbar-link { - color: #9d9d9d; + +.toast { + -ms-flex-preferred-size: 350px; + flex-basis: 350px; + max-width: 350px; + font-size: 0.875rem; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); + opacity: 0; + border-radius: 0.25rem; } -.navbar-inverse .navbar-link:hover { - color: #fff; + +.toast:not(:last-child) { + margin-bottom: 0.75rem; } -.navbar-inverse .btn-link { - color: #9d9d9d; + +.toast.showing { + opacity: 1; } -.navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link:focus { - color: #fff; + +.toast.show { + display: block; + opacity: 1; } -.navbar-inverse .btn-link[disabled]:hover, -fieldset[disabled] .navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link[disabled]:focus, -fieldset[disabled] .navbar-inverse .btn-link:focus { - color: #444; + +.toast.hide { + display: none; } -.breadcrumb { - padding: 8px 15px; - margin-bottom: 20px; - list-style: none; - background-color: #f5f5f5; - border-radius: 4px; -} -.breadcrumb > li { - display: inline-block; -} -.breadcrumb > li + li:before { - padding: 0 5px; - color: #ccc; - content: "/\00a0"; -} -.breadcrumb > .active { - color: #777; -} -.pagination { - display: inline-block; - padding-left: 0; - margin: 20px 0; - border-radius: 4px; -} -.pagination > li { - display: inline; -} -.pagination > li > a, -.pagination > li > span { - position: relative; - float: left; - padding: 6px 12px; - margin-left: -1px; - line-height: 1.42857143; - color: #337ab7; - text-decoration: none; - background-color: #fff; - border: 1px solid #ddd; -} -.pagination > li:first-child > a, -.pagination > li:first-child > span { - margin-left: 0; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; -} -.pagination > li:last-child > a, -.pagination > li:last-child > span { - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; -} -.pagination > li > a:hover, -.pagination > li > span:hover, -.pagination > li > a:focus, -.pagination > li > span:focus { - z-index: 3; - color: #23527c; - background-color: #eee; - border-color: #ddd; -} -.pagination > .active > a, -.pagination > .active > span, -.pagination > .active > a:hover, -.pagination > .active > span:hover, -.pagination > .active > a:focus, -.pagination > .active > span:focus { - z-index: 2; - color: #fff; - cursor: default; - background-color: #337ab7; - border-color: #337ab7; -} -.pagination > .disabled > span, -.pagination > .disabled > span:hover, -.pagination > .disabled > span:focus, -.pagination > .disabled > a, -.pagination > .disabled > a:hover, -.pagination > .disabled > a:focus { - color: #777; - cursor: not-allowed; - background-color: #fff; - border-color: #ddd; -} -.pagination-lg > li > a, -.pagination-lg > li > span { - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; -} -.pagination-lg > li:first-child > a, -.pagination-lg > li:first-child > span { - border-top-left-radius: 6px; - border-bottom-left-radius: 6px; -} -.pagination-lg > li:last-child > a, -.pagination-lg > li:last-child > span { - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; -} -.pagination-sm > li > a, -.pagination-sm > li > span { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; -} -.pagination-sm > li:first-child > a, -.pagination-sm > li:first-child > span { - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; -} -.pagination-sm > li:last-child > a, -.pagination-sm > li:last-child > span { - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; -} -.pager { - padding-left: 0; - margin: 20px 0; - text-align: center; - list-style: none; -} -.pager li { - display: inline; -} -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 15px; -} -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #eee; -} -.pager .next > a, -.pager .next > span { - float: right; -} -.pager .previous > a, -.pager .previous > span { - float: left; + +.toast-header { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + padding: 0.25rem 0.75rem; + color: #6c757d; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); } -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: #777; - cursor: not-allowed; - background-color: #fff; + +.toast-body { + padding: 0.75rem; } -.label { - display: inline; - padding: .2em .6em .3em; - font-size: 75%; - font-weight: bold; - line-height: 1; - color: #fff; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: .25em; + +.modal-open { + overflow: hidden; } -a.label:hover, -a.label:focus { - color: #fff; - text-decoration: none; - cursor: pointer; + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; } -.label:empty { + +.modal { + position: fixed; + top: 0; + left: 0; + z-index: 1050; display: none; + width: 100%; + height: 100%; + overflow: hidden; + outline: 0; } -.btn .label { + +.modal-dialog { position: relative; - top: -1px; -} -.label-default { - background-color: #777; + width: auto; + margin: 0.5rem; + pointer-events: none; } -.label-default[href]:hover, -.label-default[href]:focus { - background-color: #5e5e5e; + +.modal.fade .modal-dialog { + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out; + -webkit-transform: translate(0, -50px); + transform: translate(0, -50px); } -.label-primary { - background-color: #337ab7; + +@media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + transition: none; + } } -.label-primary[href]:hover, -.label-primary[href]:focus { - background-color: #286090; + +.modal.show .modal-dialog { + -webkit-transform: none; + transform: none; } -.label-success { - background-color: #5cb85c; + +.modal.modal-static .modal-dialog { + -webkit-transform: scale(1.02); + transform: scale(1.02); } -.label-success[href]:hover, -.label-success[href]:focus { - background-color: #449d44; + +.modal-dialog-scrollable { + display: -ms-flexbox; + display: flex; + max-height: calc(100% - 1rem); } -.label-info { - background-color: #5bc0de; + +.modal-dialog-scrollable .modal-content { + max-height: calc(100vh - 1rem); + overflow: hidden; } -.label-info[href]:hover, -.label-info[href]:focus { - background-color: #31b0d5; + +.modal-dialog-scrollable .modal-header, +.modal-dialog-scrollable .modal-footer { + -ms-flex-negative: 0; + flex-shrink: 0; } -.label-warning { - background-color: #f0ad4e; + +.modal-dialog-scrollable .modal-body { + overflow-y: auto; } -.label-warning[href]:hover, -.label-warning[href]:focus { - background-color: #ec971f; + +.modal-dialog-centered { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + min-height: calc(100% - 1rem); } -.label-danger { - background-color: #d9534f; + +.modal-dialog-centered::before { + display: block; + height: calc(100vh - 1rem); + height: -webkit-min-content; + height: -moz-min-content; + height: min-content; + content: ""; } -.label-danger[href]:hover, -.label-danger[href]:focus { - background-color: #c9302c; + +.modal-dialog-centered.modal-dialog-scrollable { + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-pack: center; + justify-content: center; + height: 100%; } -.badge { - display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: 12px; - font-weight: bold; - line-height: 1; - color: #fff; - text-align: center; - white-space: nowrap; - vertical-align: middle; - background-color: #777; - border-radius: 10px; + +.modal-dialog-centered.modal-dialog-scrollable .modal-content { + max-height: none; } -.badge:empty { - display: none; + +.modal-dialog-centered.modal-dialog-scrollable::before { + content: none; } -.btn .badge { + +.modal-content { position: relative; - top: -1px; + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + width: 100%; + pointer-events: auto; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; } -.btn-xs .badge, -.btn-group-xs > .btn .badge { + +.modal-backdrop { + position: fixed; top: 0; - padding: 1px 5px; -} -a.badge:hover, -a.badge:focus { - color: #fff; - text-decoration: none; - cursor: pointer; -} -.list-group-item.active > .badge, -.nav-pills > .active > a > .badge { - color: #337ab7; - background-color: #fff; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; } -.list-group-item > .badge { - float: right; + +.modal-backdrop.fade { + opacity: 0; } -.list-group-item > .badge + .badge { - margin-right: 5px; + +.modal-backdrop.show { + opacity: 0.5; } -.nav-pills > li > a > .badge { - margin-left: 3px; + +.modal-header { + display: -ms-flexbox; + display: flex; + -ms-flex-align: start; + align-items: flex-start; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 1rem 1rem; + border-bottom: 1px solid #dee2e6; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); } -.jumbotron { - padding-top: 30px; - padding-bottom: 30px; - margin-bottom: 30px; - color: inherit; - background-color: #eee; + +.modal-header .close { + padding: 1rem 1rem; + margin: -1rem -1rem -1rem auto; } -.jumbotron h1, -.jumbotron .h1 { - color: inherit; + +.modal-title { + margin-bottom: 0; + line-height: 1.5; } -.jumbotron p { - margin-bottom: 15px; - font-size: 21px; - font-weight: 200; + +.modal-body { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1rem; } -.jumbotron > hr { - border-top-color: #d5d5d5; + +.modal-footer { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: end; + justify-content: flex-end; + padding: 0.75rem; + border-top: 1px solid #dee2e6; + border-bottom-right-radius: calc(0.3rem - 1px); + border-bottom-left-radius: calc(0.3rem - 1px); } -.container .jumbotron, -.container-fluid .jumbotron { - border-radius: 6px; + +.modal-footer > * { + margin: 0.25rem; } -.jumbotron .container { - max-width: 100%; + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; } -@media screen and (min-width: 768px) { - .jumbotron { - padding-top: 48px; - padding-bottom: 48px; + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; } - .container .jumbotron, - .container-fluid .jumbotron { - padding-right: 60px; - padding-left: 60px; + .modal-dialog-scrollable { + max-height: calc(100% - 3.5rem); } - .jumbotron h1, - .jumbotron .h1 { - font-size: 63px; + .modal-dialog-scrollable .modal-content { + max-height: calc(100vh - 3.5rem); + } + .modal-dialog-centered { + min-height: calc(100% - 3.5rem); + } + .modal-dialog-centered::before { + height: calc(100vh - 3.5rem); + height: -webkit-min-content; + height: -moz-min-content; + height: min-content; + } + .modal-sm { + max-width: 300px; } } -.thumbnail { - display: block; - padding: 4px; - margin-bottom: 20px; - line-height: 1.42857143; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 4px; - -webkit-transition: border .2s ease-in-out; - -o-transition: border .2s ease-in-out; - transition: border .2s ease-in-out; -} -.thumbnail > img, -.thumbnail a > img { - margin-right: auto; - margin-left: auto; -} -a.thumbnail:hover, -a.thumbnail:focus, -a.thumbnail.active { - border-color: #337ab7; -} -.thumbnail .caption { - padding: 9px; - color: #333; + +@media (min-width: 992px) { + .modal-lg, + .modal-xl { + max-width: 800px; + } } -.alert { - padding: 15px; - margin-bottom: 20px; - border: 1px solid transparent; - border-radius: 4px; + +@media (min-width: 1200px) { + .modal-xl { + max-width: 1140px; + } } -.alert h4 { - margin-top: 0; - color: inherit; + +.tooltip { + position: absolute; + z-index: 1070; + display: block; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + opacity: 0; } -.alert .alert-link { - font-weight: bold; + +.tooltip.show { + opacity: 0.9; } -.alert > p, -.alert > ul { - margin-bottom: 0; + +.tooltip .arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; } -.alert > p + p { - margin-top: 5px; + +.tooltip .arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; } -.alert-dismissable, -.alert-dismissible { - padding-right: 35px; + +.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { + padding: 0.4rem 0; } -.alert-dismissable .close, -.alert-dismissible .close { - position: relative; - top: -2px; - right: -21px; - color: inherit; + +.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { + bottom: 0; } -.alert-success { - color: #3c763d; - background-color: #dff0d8; - border-color: #d6e9c6; + +.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { + top: 0; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; } -.alert-success hr { - border-top-color: #c9e2b3; + +.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { + padding: 0 0.4rem; } -.alert-success .alert-link { - color: #2b542c; + +.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; } -.alert-info { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; + +.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { + right: 0; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; } -.alert-info hr { - border-top-color: #a6e1ec; + +.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { + padding: 0.4rem 0; } -.alert-info .alert-link { - color: #245269; + +.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { + top: 0; } -.alert-warning { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #faebcc; + +.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { + bottom: 0; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; } -.alert-warning hr { - border-top-color: #f7e1b5; + +.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { + padding: 0 0.4rem; } -.alert-warning .alert-link { - color: #66512c; + +.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; } -.alert-danger { - color: #a94442; - background-color: #f2dede; - border-color: #ebccd1; + +.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { + left: 0; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; } -.alert-danger hr { - border-top-color: #e4b9c0; + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; } -.alert-danger .alert-link { - color: #843534; -} -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -@-o-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: block; + max-width: 276px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; } -@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } + +.popover .arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; + margin: 0 0.3rem; } -.progress { - height: 20px; - margin-bottom: 20px; - overflow: hidden; - background-color: #f5f5f5; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); + +.popover .arrow::before, .popover .arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; } -.progress-bar { - float: left; - width: 0; - height: 100%; - font-size: 12px; - line-height: 20px; - color: #fff; - text-align: center; - background-color: #337ab7; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); - -webkit-transition: width .6s ease; - -o-transition: width .6s ease; - transition: width .6s ease; -} -.progress-striped .progress-bar, -.progress-bar-striped { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - background-size: 40px 40px; -} -.progress.active .progress-bar, -.progress-bar.active { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} -.progress-bar-success { - background-color: #5cb85c; -} -.progress-striped .progress-bar-success { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.progress-bar-info { - background-color: #5bc0de; -} -.progress-striped .progress-bar-info { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.progress-bar-warning { - background-color: #f0ad4e; -} -.progress-striped .progress-bar-warning { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.progress-bar-danger { - background-color: #d9534f; -} -.progress-striped .progress-bar-danger { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + +.bs-popover-top, .bs-popover-auto[x-placement^="top"] { + margin-bottom: 0.5rem; } -.media { - margin-top: 15px; + +.bs-popover-top > .arrow, .bs-popover-auto[x-placement^="top"] > .arrow { + bottom: calc(-0.5rem - 1px); } -.media:first-child { - margin-top: 0; + +.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^="top"] > .arrow::before { + bottom: 0; + border-width: 0.5rem 0.5rem 0; + border-top-color: rgba(0, 0, 0, 0.25); } -.media, -.media-body { - overflow: hidden; - zoom: 1; + +.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^="top"] > .arrow::after { + bottom: 1px; + border-width: 0.5rem 0.5rem 0; + border-top-color: #fff; } -.media-body { - width: 10000px; + +.bs-popover-right, .bs-popover-auto[x-placement^="right"] { + margin-left: 0.5rem; } -.media-object { - display: block; + +.bs-popover-right > .arrow, .bs-popover-auto[x-placement^="right"] > .arrow { + left: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; } -.media-object.img-thumbnail { - max-width: none; + +.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^="right"] > .arrow::before { + left: 0; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: rgba(0, 0, 0, 0.25); } -.media-right, -.media > .pull-right { - padding-left: 10px; + +.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^="right"] > .arrow::after { + left: 1px; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #fff; } -.media-left, -.media > .pull-left { - padding-right: 10px; + +.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { + margin-top: 0.5rem; } -.media-left, -.media-right, -.media-body { - display: table-cell; - vertical-align: top; + +.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^="bottom"] > .arrow { + top: calc(-0.5rem - 1px); } -.media-middle { - vertical-align: middle; + +.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^="bottom"] > .arrow::before { + top: 0; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: rgba(0, 0, 0, 0.25); } -.media-bottom { - vertical-align: bottom; + +.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after { + top: 1px; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #fff; } -.media-heading { - margin-top: 0; - margin-bottom: 5px; + +.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f7f7f7; } -.media-list { - padding-left: 0; - list-style: none; + +.bs-popover-left, .bs-popover-auto[x-placement^="left"] { + margin-right: 0.5rem; } -.list-group { - padding-left: 0; - margin-bottom: 20px; + +.bs-popover-left > .arrow, .bs-popover-auto[x-placement^="left"] > .arrow { + right: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; } -.list-group-item { - position: relative; - display: block; - padding: 10px 15px; - margin-bottom: -1px; - background-color: #fff; - border: 1px solid #ddd; + +.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^="left"] > .arrow::before { + right: 0; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: rgba(0, 0, 0, 0.25); } -.list-group-item:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; + +.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^="left"] > .arrow::after { + right: 1px; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #fff; } -.list-group-item:last-child { + +.popover-header { + padding: 0.5rem 0.75rem; margin-bottom: 0; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; -} -a.list-group-item, -button.list-group-item { - color: #555; -} -a.list-group-item .list-group-item-heading, -button.list-group-item .list-group-item-heading { - color: #333; -} -a.list-group-item:hover, -button.list-group-item:hover, -a.list-group-item:focus, -button.list-group-item:focus { - color: #555; - text-decoration: none; - background-color: #f5f5f5; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); } -button.list-group-item { - width: 100%; - text-align: left; + +.popover-header:empty { + display: none; } -.list-group-item.disabled, -.list-group-item.disabled:hover, -.list-group-item.disabled:focus { - color: #777; - cursor: not-allowed; - background-color: #eee; + +.popover-body { + padding: 0.5rem 0.75rem; + color: #212529; } -.list-group-item.disabled .list-group-item-heading, -.list-group-item.disabled:hover .list-group-item-heading, -.list-group-item.disabled:focus .list-group-item-heading { - color: inherit; + +.carousel { + position: relative; } -.list-group-item.disabled .list-group-item-text, -.list-group-item.disabled:hover .list-group-item-text, -.list-group-item.disabled:focus .list-group-item-text { - color: #777; + +.carousel.pointer-event { + -ms-touch-action: pan-y; + touch-action: pan-y; } -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - z-index: 2; - color: #fff; - background-color: #337ab7; - border-color: #337ab7; -} -.list-group-item.active .list-group-item-heading, -.list-group-item.active:hover .list-group-item-heading, -.list-group-item.active:focus .list-group-item-heading, -.list-group-item.active .list-group-item-heading > small, -.list-group-item.active:hover .list-group-item-heading > small, -.list-group-item.active:focus .list-group-item-heading > small, -.list-group-item.active .list-group-item-heading > .small, -.list-group-item.active:hover .list-group-item-heading > .small, -.list-group-item.active:focus .list-group-item-heading > .small { - color: inherit; + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; } -.list-group-item.active .list-group-item-text, -.list-group-item.active:hover .list-group-item-text, -.list-group-item.active:focus .list-group-item-text { - color: #c7ddef; + +.carousel-inner::after { + display: block; + clear: both; + content: ""; } -.list-group-item-success { - color: #3c763d; - background-color: #dff0d8; + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + transition: -webkit-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out; } -a.list-group-item-success, -button.list-group-item-success { - color: #3c763d; + +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } } -a.list-group-item-success .list-group-item-heading, -button.list-group-item-success .list-group-item-heading { - color: inherit; + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; } -a.list-group-item-success:hover, -button.list-group-item-success:hover, -a.list-group-item-success:focus, -button.list-group-item-success:focus { - color: #3c763d; - background-color: #d0e9c6; -} -a.list-group-item-success.active, -button.list-group-item-success.active, -a.list-group-item-success.active:hover, -button.list-group-item-success.active:hover, -a.list-group-item-success.active:focus, -button.list-group-item-success.active:focus { - color: #fff; - background-color: #3c763d; - border-color: #3c763d; + +.carousel-item-next:not(.carousel-item-left), +.active.carousel-item-right { + -webkit-transform: translateX(100%); + transform: translateX(100%); } -.list-group-item-info { - color: #31708f; - background-color: #d9edf7; + +.carousel-item-prev:not(.carousel-item-right), +.active.carousel-item-left { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); } -a.list-group-item-info, -button.list-group-item-info { - color: #31708f; + +.carousel-fade .carousel-item { + opacity: 0; + transition-property: opacity; + -webkit-transform: none; + transform: none; } -a.list-group-item-info .list-group-item-heading, -button.list-group-item-info .list-group-item-heading { - color: inherit; + +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-left, +.carousel-fade .carousel-item-prev.carousel-item-right { + z-index: 1; + opacity: 1; } -a.list-group-item-info:hover, -button.list-group-item-info:hover, -a.list-group-item-info:focus, -button.list-group-item-info:focus { - color: #31708f; - background-color: #c4e3f3; -} -a.list-group-item-info.active, -button.list-group-item-info.active, -a.list-group-item-info.active:hover, -button.list-group-item-info.active:hover, -a.list-group-item-info.active:focus, -button.list-group-item-info.active:focus { - color: #fff; - background-color: #31708f; - border-color: #31708f; + +.carousel-fade .active.carousel-item-left, +.carousel-fade .active.carousel-item-right { + z-index: 0; + opacity: 0; + transition: opacity 0s 0.6s; } -.list-group-item-warning { - color: #8a6d3b; - background-color: #fcf8e3; + +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-left, + .carousel-fade .active.carousel-item-right { + transition: none; + } } -a.list-group-item-warning, -button.list-group-item-warning { - color: #8a6d3b; + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + width: 15%; + color: #fff; + text-align: center; + opacity: 0.5; + transition: opacity 0.15s ease; } -a.list-group-item-warning .list-group-item-heading, -button.list-group-item-warning .list-group-item-heading { - color: inherit; + +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } } -a.list-group-item-warning:hover, -button.list-group-item-warning:hover, -a.list-group-item-warning:focus, -button.list-group-item-warning:focus { - color: #8a6d3b; - background-color: #faf2cc; -} -a.list-group-item-warning.active, -button.list-group-item-warning.active, -a.list-group-item-warning.active:hover, -button.list-group-item-warning.active:hover, -a.list-group-item-warning.active:focus, -button.list-group-item-warning.active:focus { + +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { color: #fff; - background-color: #8a6d3b; - border-color: #8a6d3b; + text-decoration: none; + outline: 0; + opacity: 0.9; } -.list-group-item-danger { - color: #a94442; - background-color: #f2dede; + +.carousel-control-prev { + left: 0; } -a.list-group-item-danger, -button.list-group-item-danger { - color: #a94442; + +.carousel-control-next { + right: 0; } -a.list-group-item-danger .list-group-item-heading, -button.list-group-item-danger .list-group-item-heading { - color: inherit; + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 20px; + height: 20px; + background: no-repeat 50% / 100% 100%; } -a.list-group-item-danger:hover, -button.list-group-item-danger:hover, -a.list-group-item-danger:focus, -button.list-group-item-danger:focus { - color: #a94442; - background-color: #ebcccc; -} -a.list-group-item-danger.active, -button.list-group-item-danger.active, -a.list-group-item-danger.active:hover, -button.list-group-item-danger.active:hover, -a.list-group-item-danger.active:focus, -button.list-group-item-danger.active:focus { - color: #fff; - background-color: #a94442; - border-color: #a94442; + +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e"); } -.list-group-item-heading { - margin-top: 0; - margin-bottom: 5px; + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e"); } -.list-group-item-text { - margin-bottom: 0; - line-height: 1.3; + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 15; + display: -ms-flexbox; + display: flex; + -ms-flex-pack: center; + justify-content: center; + padding-left: 0; + margin-right: 15%; + margin-left: 15%; + list-style: none; } -.panel { - margin-bottom: 20px; + +.carousel-indicators li { + box-sizing: content-box; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + width: 30px; + height: 3px; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; background-color: #fff; - border: 1px solid transparent; - border-radius: 4px; - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); - box-shadow: 0 1px 1px rgba(0, 0, 0, .05); -} -.panel-body { - padding: 15px; + background-clip: padding-box; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: .5; + transition: opacity 0.6s ease; } -.panel-heading { - padding: 10px 15px; - border-bottom: 1px solid transparent; - border-top-left-radius: 3px; - border-top-right-radius: 3px; + +@media (prefers-reduced-motion: reduce) { + .carousel-indicators li { + transition: none; + } } -.panel-heading > .dropdown .dropdown-toggle { - color: inherit; + +.carousel-indicators .active { + opacity: 1; } -.panel-title { - margin-top: 0; - margin-bottom: 0; - font-size: 16px; - color: inherit; + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; } -.panel-title > a, -.panel-title > small, -.panel-title > .small, -.panel-title > small > a, -.panel-title > .small > a { - color: inherit; + +@-webkit-keyframes spinner-border { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } } -.panel-footer { - padding: 10px 15px; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; + +@keyframes spinner-border { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } } -.panel > .list-group, -.panel > .panel-collapse > .list-group { - margin-bottom: 0; + +.spinner-border { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + -webkit-animation: spinner-border .75s linear infinite; + animation: spinner-border .75s linear infinite; } -.panel > .list-group .list-group-item, -.panel > .panel-collapse > .list-group .list-group-item { - border-width: 1px 0; - border-radius: 0; + +.spinner-border-sm { + width: 1rem; + height: 1rem; + border-width: 0.2em; } -.panel > .list-group:first-child .list-group-item:first-child, -.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { - border-top: 0; - border-top-left-radius: 3px; - border-top-right-radius: 3px; + +@-webkit-keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 50% { + opacity: 1; + -webkit-transform: none; + transform: none; + } } -.panel > .list-group:last-child .list-group-item:last-child, -.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { - border-bottom: 0; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; + +@keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 50% { + opacity: 1; + -webkit-transform: none; + transform: none; + } } -.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { - border-top-left-radius: 0; - border-top-right-radius: 0; + +.spinner-grow { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + background-color: currentColor; + border-radius: 50%; + opacity: 0; + -webkit-animation: spinner-grow .75s linear infinite; + animation: spinner-grow .75s linear infinite; } -.panel-heading + .list-group .list-group-item:first-child { - border-top-width: 0; + +.spinner-grow-sm { + width: 1rem; + height: 1rem; } -.list-group + .panel-footer { - border-top-width: 0; + +.align-baseline { + vertical-align: baseline !important; } -.panel > .table, -.panel > .table-responsive > .table, -.panel > .panel-collapse > .table { - margin-bottom: 0; + +.align-top { + vertical-align: top !important; } -.panel > .table caption, -.panel > .table-responsive > .table caption, -.panel > .panel-collapse > .table caption { - padding-right: 15px; - padding-left: 15px; + +.align-middle { + vertical-align: middle !important; } -.panel > .table:first-child, -.panel > .table-responsive:first-child > .table:first-child { - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel > .table:first-child > thead:first-child > tr:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { - border-top-left-radius: 3px; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { - border-top-right-radius: 3px; -} -.panel > .table:last-child, -.panel > .table-responsive:last-child > .table:last-child { - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { - border-bottom-left-radius: 3px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { - border-bottom-right-radius: 3px; -} -.panel > .panel-body + .table, -.panel > .panel-body + .table-responsive, -.panel > .table + .panel-body, -.panel > .table-responsive + .panel-body { - border-top: 1px solid #ddd; -} -.panel > .table > tbody:first-child > tr:first-child th, -.panel > .table > tbody:first-child > tr:first-child td { - border-top: 0; + +.align-bottom { + vertical-align: bottom !important; } -.panel > .table-bordered, -.panel > .table-responsive > .table-bordered { - border: 0; + +.align-text-bottom { + vertical-align: text-bottom !important; } -.panel > .table-bordered > thead > tr > th:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, -.panel > .table-bordered > tbody > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, -.panel > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-bordered > thead > tr > td:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, -.panel > .table-bordered > tbody > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, -.panel > .table-bordered > tfoot > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; -} -.panel > .table-bordered > thead > tr > th:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, -.panel > .table-bordered > tbody > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, -.panel > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-bordered > thead > tr > td:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, -.panel > .table-bordered > tbody > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, -.panel > .table-bordered > tfoot > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; + +.align-text-top { + vertical-align: text-top !important; } -.panel > .table-bordered > thead > tr:first-child > td, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, -.panel > .table-bordered > tbody > tr:first-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, -.panel > .table-bordered > thead > tr:first-child > th, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, -.panel > .table-bordered > tbody > tr:first-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { - border-bottom: 0; + +.bg-primary { + background-color: #007bff !important; } -.panel > .table-bordered > tbody > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, -.panel > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-bordered > tbody > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, -.panel > .table-bordered > tfoot > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { - border-bottom: 0; + +a.bg-primary:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #0062cc !important; } -.panel > .table-responsive { - margin-bottom: 0; - border: 0; + +.bg-secondary { + background-color: #6c757d !important; } -.panel-group { - margin-bottom: 20px; + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #545b62 !important; } -.panel-group .panel { - margin-bottom: 0; - border-radius: 4px; + +.bg-success { + background-color: #28a745 !important; } -.panel-group .panel + .panel { - margin-top: 5px; + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #1e7e34 !important; } -.panel-group .panel-heading { - border-bottom: 0; + +.bg-info { + background-color: #17a2b8 !important; } -.panel-group .panel-heading + .panel-collapse > .panel-body, -.panel-group .panel-heading + .panel-collapse > .list-group { - border-top: 1px solid #ddd; + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #117a8b !important; } -.panel-group .panel-footer { - border-top: 0; + +.bg-warning { + background-color: #ffc107 !important; +} + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #d39e00 !important; +} + +.bg-danger { + background-color: #dc3545 !important; +} + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #bd2130 !important; } -.panel-group .panel-footer + .panel-collapse .panel-body { - border-bottom: 1px solid #ddd; + +.bg-light { + background-color: #f8f9fa !important; } -.panel-default { - border-color: #ddd; + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: #dae0e5 !important; } -.panel-default > .panel-heading { - color: #333; - background-color: #f5f5f5; - border-color: #ddd; + +.bg-dark { + background-color: #343a40 !important; } -.panel-default > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #ddd; + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + background-color: #1d2124 !important; } -.panel-default > .panel-heading .badge { - color: #f5f5f5; - background-color: #333; + +.bg-white { + background-color: #fff !important; } -.panel-default > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #ddd; + +.bg-transparent { + background-color: transparent !important; } -.panel-primary { - border-color: #337ab7; + +.border { + border: 1px solid #dee2e6 !important; } -.panel-primary > .panel-heading { - color: #fff; - background-color: #337ab7; - border-color: #337ab7; + +.border-top { + border-top: 1px solid #dee2e6 !important; } -.panel-primary > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #337ab7; + +.border-right { + border-right: 1px solid #dee2e6 !important; } -.panel-primary > .panel-heading .badge { - color: #337ab7; - background-color: #fff; + +.border-bottom { + border-bottom: 1px solid #dee2e6 !important; +} + +.border-left { + border-left: 1px solid #dee2e6 !important; +} + +.border-0 { + border: 0 !important; } -.panel-primary > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #337ab7; + +.border-top-0 { + border-top: 0 !important; +} + +.border-right-0 { + border-right: 0 !important; } -.panel-success { - border-color: #d6e9c6; + +.border-bottom-0 { + border-bottom: 0 !important; } -.panel-success > .panel-heading { - color: #3c763d; - background-color: #dff0d8; - border-color: #d6e9c6; + +.border-left-0 { + border-left: 0 !important; } -.panel-success > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #d6e9c6; + +.border-primary { + border-color: #007bff !important; } -.panel-success > .panel-heading .badge { - color: #dff0d8; - background-color: #3c763d; + +.border-secondary { + border-color: #6c757d !important; } -.panel-success > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #d6e9c6; + +.border-success { + border-color: #28a745 !important; } -.panel-info { - border-color: #bce8f1; + +.border-info { + border-color: #17a2b8 !important; } -.panel-info > .panel-heading { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; + +.border-warning { + border-color: #ffc107 !important; } -.panel-info > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #bce8f1; + +.border-danger { + border-color: #dc3545 !important; } -.panel-info > .panel-heading .badge { - color: #d9edf7; - background-color: #31708f; + +.border-light { + border-color: #f8f9fa !important; } -.panel-info > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #bce8f1; + +.border-dark { + border-color: #343a40 !important; } -.panel-warning { - border-color: #faebcc; + +.border-white { + border-color: #fff !important; } -.panel-warning > .panel-heading { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #faebcc; + +.rounded-sm { + border-radius: 0.2rem !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-right { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-left { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-lg { + border-radius: 0.3rem !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-pill { + border-radius: 50rem !important; } -.panel-warning > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #faebcc; + +.rounded-0 { + border-radius: 0 !important; +} + +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; } -.panel-warning > .panel-heading .badge { - color: #fcf8e3; - background-color: #8a6d3b; + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: -ms-flexbox !important; + display: flex !important; } -.panel-warning > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #faebcc; + +.d-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; } -.panel-danger { - border-color: #ebccd1; + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-sm-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } } -.panel-danger > .panel-heading { - color: #a94442; - background-color: #f2dede; - border-color: #ebccd1; + +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-md-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } } -.panel-danger > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #ebccd1; + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-lg-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } } -.panel-danger > .panel-heading .badge { - color: #f2dede; - background-color: #a94442; + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-xl-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } } -.panel-danger > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #ebccd1; + +@media print { + .d-print-none { + display: none !important; + } + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-print-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } } + .embed-responsive { position: relative; display: block; - height: 0; + width: 100%; padding: 0; overflow: hidden; } + +.embed-responsive::before { + display: block; + content: ""; +} + .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, @@ -5859,724 +7148,3011 @@ button.list-group-item-danger.active:focus { height: 100%; border: 0; } -.embed-responsive-16by9 { - padding-bottom: 56.25%; + +.embed-responsive-21by9::before { + padding-top: 42.857143%; } -.embed-responsive-4by3 { - padding-bottom: 75%; + +.embed-responsive-16by9::before { + padding-top: 56.25%; } -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); + +.embed-responsive-4by3::before { + padding-top: 75%; } -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, .15); + +.embed-responsive-1by1::before { + padding-top: 100%; } -.well-lg { - padding: 24px; - border-radius: 6px; + +.flex-row { + -ms-flex-direction: row !important; + flex-direction: row !important; } -.well-sm { - padding: 9px; - border-radius: 3px; + +.flex-column { + -ms-flex-direction: column !important; + flex-direction: column !important; } -.close { - float: right; - font-size: 21px; - font-weight: bold; - line-height: 1; - color: #000; - text-shadow: 0 1px 0 #fff; - filter: alpha(opacity=20); - opacity: .2; + +.flex-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; } -.close:hover, -.close:focus { - color: #000; - text-decoration: none; - cursor: pointer; - filter: alpha(opacity=50); - opacity: .5; + +.flex-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; } -button.close { - -webkit-appearance: none; - padding: 0; - cursor: pointer; - background: transparent; - border: 0; + +.flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; } -.modal-open { - overflow: hidden; + +.flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; } -.modal { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1050; - display: none; - overflow: hidden; - -webkit-overflow-scrolling: touch; - outline: 0; + +.flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; } -.modal.fade .modal-dialog { - -webkit-transition: -webkit-transform .3s ease-out; - -o-transition: -o-transform .3s ease-out; - transition: transform .3s ease-out; - -webkit-transform: translate(0, -25%); - -ms-transform: translate(0, -25%); - -o-transform: translate(0, -25%); - transform: translate(0, -25%); -} -.modal.in .modal-dialog { - -webkit-transform: translate(0, 0); - -ms-transform: translate(0, 0); - -o-transform: translate(0, 0); - transform: translate(0, 0); + +.flex-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; } -.modal-open .modal { - overflow-x: hidden; - overflow-y: auto; + +.flex-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; } -.modal-dialog { - position: relative; - width: auto; - margin: 10px; + +.flex-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; } -.modal-content { - position: relative; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #999; - border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; - outline: 0; - -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); - box-shadow: 0 3px 9px rgba(0, 0, 0, .5); + +.flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; } -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000; + +.flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; } -.modal-backdrop.fade { - filter: alpha(opacity=0); - opacity: 0; + +.justify-content-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; } -.modal-backdrop.in { - filter: alpha(opacity=50); - opacity: .5; + +.justify-content-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; } -.modal-header { - min-height: 16.42857143px; - padding: 15px; - border-bottom: 1px solid #e5e5e5; + +.justify-content-center { + -ms-flex-pack: center !important; + justify-content: center !important; } -.modal-header .close { - margin-top: -2px; + +.justify-content-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; } -.modal-title { - margin: 0; - line-height: 1.42857143; + +.justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; } -.modal-body { - position: relative; - padding: 15px; + +.align-items-start { + -ms-flex-align: start !important; + align-items: flex-start !important; } -.modal-footer { - padding: 15px; - text-align: right; - border-top: 1px solid #e5e5e5; + +.align-items-end { + -ms-flex-align: end !important; + align-items: flex-end !important; } -.modal-footer .btn + .btn { - margin-bottom: 0; - margin-left: 5px; + +.align-items-center { + -ms-flex-align: center !important; + align-items: center !important; } -.modal-footer .btn-group .btn + .btn { - margin-left: -1px; + +.align-items-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; } -.modal-footer .btn-block + .btn-block { - margin-left: 0; + +.align-items-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; } -.modal-scrollbar-measure { - position: absolute; - top: -9999px; - width: 50px; - height: 50px; - overflow: scroll; + +.align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; +} + +.align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; +} + +.align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; +} + +.align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; +} + +.align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; +} + +.align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; +} + +.align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; +} + +.align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; +} + +.align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; +} + +.align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; +} + +.align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; +} + +.align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-sm-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-sm-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-sm-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-sm-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-sm-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-sm-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-sm-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-sm-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-sm-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-sm-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-sm-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-sm-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-sm-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-sm-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-sm-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-sm-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-sm-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-sm-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-sm-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-sm-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-sm-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-sm-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-sm-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-sm-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-sm-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-sm-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-sm-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } } + @media (min-width: 768px) { - .modal-dialog { - width: 600px; - margin: 30px auto; + .flex-md-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-md-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-md-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-md-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-md-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-md-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-md-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-md-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-md-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-md-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-md-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-md-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-md-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-md-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-md-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 992px) { + .flex-lg-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-lg-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-lg-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-lg-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-lg-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-lg-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-lg-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-lg-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-lg-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-lg-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-lg-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-lg-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-lg-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-lg-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 1200px) { + .flex-xl-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-xl-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-xl-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-xl-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-xl-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-xl-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-xl-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-xl-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-xl-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-xl-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-xl-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-xl-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-xl-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-xl-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +.float-left { + float: left !important; +} + +.float-right { + float: right !important; +} + +.float-none { + float: none !important; +} + +@media (min-width: 576px) { + .float-sm-left { + float: left !important; + } + .float-sm-right { + float: right !important; + } + .float-sm-none { + float: none !important; + } +} + +@media (min-width: 768px) { + .float-md-left { + float: left !important; + } + .float-md-right { + float: right !important; + } + .float-md-none { + float: none !important; + } +} + +@media (min-width: 992px) { + .float-lg-left { + float: left !important; + } + .float-lg-right { + float: right !important; + } + .float-lg-none { + float: none !important; + } +} + +@media (min-width: 1200px) { + .float-xl-left { + float: left !important; + } + .float-xl-right { + float: right !important; + } + .float-xl-none { + float: none !important; + } +} + +.user-select-all { + -webkit-user-select: all !important; + -moz-user-select: all !important; + -ms-user-select: all !important; + user-select: all !important; +} + +.user-select-auto { + -webkit-user-select: auto !important; + -moz-user-select: auto !important; + -ms-user-select: auto !important; + user-select: auto !important; +} + +.user-select-none { + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; +} + +.overflow-auto { + overflow: auto !important; +} + +.overflow-hidden { + overflow: hidden !important; +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: -webkit-sticky !important; + position: sticky !important; +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +@supports ((position: -webkit-sticky) or (position: sticky)) { + .sticky-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + overflow: visible; + clip: auto; + white-space: normal; +} + +.shadow-sm { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-lg { + box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; +} + +.shadow-none { + box-shadow: none !important; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.w-auto { + width: auto !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.h-auto { + height: auto !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.min-vw-100 { + min-width: 100vw !important; +} + +.min-vh-100 { + min-height: 100vh !important; +} + +.vw-100 { + width: 100vw !important; +} + +.vh-100 { + height: 100vh !important; +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-n1 { + margin: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.5rem !important; +} + +.m-n3 { + margin: -1rem !important; +} + +.mt-n3, +.my-n3 { + margin-top: -1rem !important; +} + +.mr-n3, +.mx-n3 { + margin-right: -1rem !important; +} + +.mb-n3, +.my-n3 { + margin-bottom: -1rem !important; +} + +.ml-n3, +.mx-n3 { + margin-left: -1rem !important; +} + +.m-n4 { + margin: -1.5rem !important; +} + +.mt-n4, +.my-n4 { + margin-top: -1.5rem !important; +} + +.mr-n4, +.mx-n4 { + margin-right: -1.5rem !important; +} + +.mb-n4, +.my-n4 { + margin-bottom: -1.5rem !important; +} + +.ml-n4, +.mx-n4 { + margin-left: -1.5rem !important; +} + +.m-n5 { + margin: -3rem !important; +} + +.mt-n5, +.my-n5 { + margin-top: -3rem !important; +} + +.mr-n5, +.mx-n5 { + margin-right: -3rem !important; +} + +.mb-n5, +.my-n5 { + margin-bottom: -3rem !important; +} + +.ml-n5, +.mx-n5 { + margin-left: -3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + .mt-sm-0, + .my-sm-0 { + margin-top: 0 !important; + } + .mr-sm-0, + .mx-sm-0 { + margin-right: 0 !important; + } + .mb-sm-0, + .my-sm-0 { + margin-bottom: 0 !important; + } + .ml-sm-0, + .mx-sm-0 { + margin-left: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .mt-sm-3, + .my-sm-3 { + margin-top: 1rem !important; + } + .mr-sm-3, + .mx-sm-3 { + margin-right: 1rem !important; + } + .mb-sm-3, + .my-sm-3 { + margin-bottom: 1rem !important; + } + .ml-sm-3, + .mx-sm-3 { + margin-left: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .mt-sm-4, + .my-sm-4 { + margin-top: 1.5rem !important; + } + .mr-sm-4, + .mx-sm-4 { + margin-right: 1.5rem !important; + } + .mb-sm-4, + .my-sm-4 { + margin-bottom: 1.5rem !important; + } + .ml-sm-4, + .mx-sm-4 { + margin-left: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .mt-sm-5, + .my-sm-5 { + margin-top: 3rem !important; + } + .mr-sm-5, + .mx-sm-5 { + margin-right: 3rem !important; + } + .mb-sm-5, + .my-sm-5 { + margin-bottom: 3rem !important; + } + .ml-sm-5, + .mx-sm-5 { + margin-left: 3rem !important; + } + .p-sm-0 { + padding: 0 !important; + } + .pt-sm-0, + .py-sm-0 { + padding-top: 0 !important; + } + .pr-sm-0, + .px-sm-0 { + padding-right: 0 !important; + } + .pb-sm-0, + .py-sm-0 { + padding-bottom: 0 !important; + } + .pl-sm-0, + .px-sm-0 { + padding-left: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .pt-sm-3, + .py-sm-3 { + padding-top: 1rem !important; + } + .pr-sm-3, + .px-sm-3 { + padding-right: 1rem !important; + } + .pb-sm-3, + .py-sm-3 { + padding-bottom: 1rem !important; + } + .pl-sm-3, + .px-sm-3 { + padding-left: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .pt-sm-4, + .py-sm-4 { + padding-top: 1.5rem !important; + } + .pr-sm-4, + .px-sm-4 { + padding-right: 1.5rem !important; + } + .pb-sm-4, + .py-sm-4 { + padding-bottom: 1.5rem !important; + } + .pl-sm-4, + .px-sm-4 { + padding-left: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .pt-sm-5, + .py-sm-5 { + padding-top: 3rem !important; + } + .pr-sm-5, + .px-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-5, + .py-sm-5 { + padding-bottom: 3rem !important; + } + .pl-sm-5, + .px-sm-5 { + padding-left: 3rem !important; + } + .m-sm-n1 { + margin: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.5rem !important; + } + .m-sm-n3 { + margin: -1rem !important; + } + .mt-sm-n3, + .my-sm-n3 { + margin-top: -1rem !important; + } + .mr-sm-n3, + .mx-sm-n3 { + margin-right: -1rem !important; + } + .mb-sm-n3, + .my-sm-n3 { + margin-bottom: -1rem !important; + } + .ml-sm-n3, + .mx-sm-n3 { + margin-left: -1rem !important; + } + .m-sm-n4 { + margin: -1.5rem !important; + } + .mt-sm-n4, + .my-sm-n4 { + margin-top: -1.5rem !important; + } + .mr-sm-n4, + .mx-sm-n4 { + margin-right: -1.5rem !important; + } + .mb-sm-n4, + .my-sm-n4 { + margin-bottom: -1.5rem !important; + } + .ml-sm-n4, + .mx-sm-n4 { + margin-left: -1.5rem !important; + } + .m-sm-n5 { + margin: -3rem !important; + } + .mt-sm-n5, + .my-sm-n5 { + margin-top: -3rem !important; + } + .mr-sm-n5, + .mx-sm-n5 { + margin-right: -3rem !important; + } + .mb-sm-n5, + .my-sm-n5 { + margin-bottom: -3rem !important; + } + .ml-sm-n5, + .mx-sm-n5 { + margin-left: -3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mt-sm-auto, + .my-sm-auto { + margin-top: auto !important; + } + .mr-sm-auto, + .mx-sm-auto { + margin-right: auto !important; + } + .mb-sm-auto, + .my-sm-auto { + margin-bottom: auto !important; + } + .ml-sm-auto, + .mx-sm-auto { + margin-left: auto !important; + } +} + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + .mt-md-0, + .my-md-0 { + margin-top: 0 !important; + } + .mr-md-0, + .mx-md-0 { + margin-right: 0 !important; + } + .mb-md-0, + .my-md-0 { + margin-bottom: 0 !important; + } + .ml-md-0, + .mx-md-0 { + margin-left: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .mt-md-3, + .my-md-3 { + margin-top: 1rem !important; + } + .mr-md-3, + .mx-md-3 { + margin-right: 1rem !important; + } + .mb-md-3, + .my-md-3 { + margin-bottom: 1rem !important; + } + .ml-md-3, + .mx-md-3 { + margin-left: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .mt-md-4, + .my-md-4 { + margin-top: 1.5rem !important; + } + .mr-md-4, + .mx-md-4 { + margin-right: 1.5rem !important; + } + .mb-md-4, + .my-md-4 { + margin-bottom: 1.5rem !important; + } + .ml-md-4, + .mx-md-4 { + margin-left: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .mt-md-5, + .my-md-5 { + margin-top: 3rem !important; + } + .mr-md-5, + .mx-md-5 { + margin-right: 3rem !important; + } + .mb-md-5, + .my-md-5 { + margin-bottom: 3rem !important; + } + .ml-md-5, + .mx-md-5 { + margin-left: 3rem !important; + } + .p-md-0 { + padding: 0 !important; + } + .pt-md-0, + .py-md-0 { + padding-top: 0 !important; + } + .pr-md-0, + .px-md-0 { + padding-right: 0 !important; + } + .pb-md-0, + .py-md-0 { + padding-bottom: 0 !important; + } + .pl-md-0, + .px-md-0 { + padding-left: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .pt-md-3, + .py-md-3 { + padding-top: 1rem !important; + } + .pr-md-3, + .px-md-3 { + padding-right: 1rem !important; + } + .pb-md-3, + .py-md-3 { + padding-bottom: 1rem !important; + } + .pl-md-3, + .px-md-3 { + padding-left: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .pt-md-4, + .py-md-4 { + padding-top: 1.5rem !important; + } + .pr-md-4, + .px-md-4 { + padding-right: 1.5rem !important; + } + .pb-md-4, + .py-md-4 { + padding-bottom: 1.5rem !important; + } + .pl-md-4, + .px-md-4 { + padding-left: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .pt-md-5, + .py-md-5 { + padding-top: 3rem !important; + } + .pr-md-5, + .px-md-5 { + padding-right: 3rem !important; + } + .pb-md-5, + .py-md-5 { + padding-bottom: 3rem !important; + } + .pl-md-5, + .px-md-5 { + padding-left: 3rem !important; + } + .m-md-n1 { + margin: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.5rem !important; + } + .m-md-n3 { + margin: -1rem !important; + } + .mt-md-n3, + .my-md-n3 { + margin-top: -1rem !important; + } + .mr-md-n3, + .mx-md-n3 { + margin-right: -1rem !important; + } + .mb-md-n3, + .my-md-n3 { + margin-bottom: -1rem !important; + } + .ml-md-n3, + .mx-md-n3 { + margin-left: -1rem !important; + } + .m-md-n4 { + margin: -1.5rem !important; + } + .mt-md-n4, + .my-md-n4 { + margin-top: -1.5rem !important; + } + .mr-md-n4, + .mx-md-n4 { + margin-right: -1.5rem !important; + } + .mb-md-n4, + .my-md-n4 { + margin-bottom: -1.5rem !important; + } + .ml-md-n4, + .mx-md-n4 { + margin-left: -1.5rem !important; + } + .m-md-n5 { + margin: -3rem !important; + } + .mt-md-n5, + .my-md-n5 { + margin-top: -3rem !important; + } + .mr-md-n5, + .mx-md-n5 { + margin-right: -3rem !important; + } + .mb-md-n5, + .my-md-n5 { + margin-bottom: -3rem !important; + } + .ml-md-n5, + .mx-md-n5 { + margin-left: -3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mt-md-auto, + .my-md-auto { + margin-top: auto !important; + } + .mr-md-auto, + .mx-md-auto { + margin-right: auto !important; + } + .mb-md-auto, + .my-md-auto { + margin-bottom: auto !important; + } + .ml-md-auto, + .mx-md-auto { + margin-left: auto !important; + } +} + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + .mt-lg-0, + .my-lg-0 { + margin-top: 0 !important; + } + .mr-lg-0, + .mx-lg-0 { + margin-right: 0 !important; + } + .mb-lg-0, + .my-lg-0 { + margin-bottom: 0 !important; + } + .ml-lg-0, + .mx-lg-0 { + margin-left: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .mt-lg-3, + .my-lg-3 { + margin-top: 1rem !important; + } + .mr-lg-3, + .mx-lg-3 { + margin-right: 1rem !important; + } + .mb-lg-3, + .my-lg-3 { + margin-bottom: 1rem !important; + } + .ml-lg-3, + .mx-lg-3 { + margin-left: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .mt-lg-4, + .my-lg-4 { + margin-top: 1.5rem !important; + } + .mr-lg-4, + .mx-lg-4 { + margin-right: 1.5rem !important; + } + .mb-lg-4, + .my-lg-4 { + margin-bottom: 1.5rem !important; + } + .ml-lg-4, + .mx-lg-4 { + margin-left: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .mt-lg-5, + .my-lg-5 { + margin-top: 3rem !important; + } + .mr-lg-5, + .mx-lg-5 { + margin-right: 3rem !important; + } + .mb-lg-5, + .my-lg-5 { + margin-bottom: 3rem !important; + } + .ml-lg-5, + .mx-lg-5 { + margin-left: 3rem !important; + } + .p-lg-0 { + padding: 0 !important; + } + .pt-lg-0, + .py-lg-0 { + padding-top: 0 !important; + } + .pr-lg-0, + .px-lg-0 { + padding-right: 0 !important; + } + .pb-lg-0, + .py-lg-0 { + padding-bottom: 0 !important; + } + .pl-lg-0, + .px-lg-0 { + padding-left: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .pt-lg-3, + .py-lg-3 { + padding-top: 1rem !important; + } + .pr-lg-3, + .px-lg-3 { + padding-right: 1rem !important; + } + .pb-lg-3, + .py-lg-3 { + padding-bottom: 1rem !important; + } + .pl-lg-3, + .px-lg-3 { + padding-left: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .pt-lg-4, + .py-lg-4 { + padding-top: 1.5rem !important; + } + .pr-lg-4, + .px-lg-4 { + padding-right: 1.5rem !important; + } + .pb-lg-4, + .py-lg-4 { + padding-bottom: 1.5rem !important; + } + .pl-lg-4, + .px-lg-4 { + padding-left: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .pt-lg-5, + .py-lg-5 { + padding-top: 3rem !important; + } + .pr-lg-5, + .px-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-5, + .py-lg-5 { + padding-bottom: 3rem !important; + } + .pl-lg-5, + .px-lg-5 { + padding-left: 3rem !important; + } + .m-lg-n1 { + margin: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.5rem !important; + } + .m-lg-n3 { + margin: -1rem !important; + } + .mt-lg-n3, + .my-lg-n3 { + margin-top: -1rem !important; + } + .mr-lg-n3, + .mx-lg-n3 { + margin-right: -1rem !important; + } + .mb-lg-n3, + .my-lg-n3 { + margin-bottom: -1rem !important; + } + .ml-lg-n3, + .mx-lg-n3 { + margin-left: -1rem !important; + } + .m-lg-n4 { + margin: -1.5rem !important; + } + .mt-lg-n4, + .my-lg-n4 { + margin-top: -1.5rem !important; + } + .mr-lg-n4, + .mx-lg-n4 { + margin-right: -1.5rem !important; + } + .mb-lg-n4, + .my-lg-n4 { + margin-bottom: -1.5rem !important; + } + .ml-lg-n4, + .mx-lg-n4 { + margin-left: -1.5rem !important; + } + .m-lg-n5 { + margin: -3rem !important; + } + .mt-lg-n5, + .my-lg-n5 { + margin-top: -3rem !important; + } + .mr-lg-n5, + .mx-lg-n5 { + margin-right: -3rem !important; } - .modal-content { - -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); - box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + .mb-lg-n5, + .my-lg-n5 { + margin-bottom: -3rem !important; } - .modal-sm { - width: 300px; + .ml-lg-n5, + .mx-lg-n5 { + margin-left: -3rem !important; } -} -@media (min-width: 992px) { - .modal-lg { - width: 900px; + .m-lg-auto { + margin: auto !important; + } + .mt-lg-auto, + .my-lg-auto { + margin-top: auto !important; + } + .mr-lg-auto, + .mx-lg-auto { + margin-right: auto !important; + } + .mb-lg-auto, + .my-lg-auto { + margin-bottom: auto !important; + } + .ml-lg-auto, + .mx-lg-auto { + margin-left: auto !important; } } -.tooltip { - position: absolute; - z-index: 1070; - display: block; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 12px; - font-style: normal; - font-weight: normal; - line-height: 1.42857143; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - letter-spacing: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; - white-space: normal; - filter: alpha(opacity=0); - opacity: 0; - line-break: auto; -} -.tooltip.in { - filter: alpha(opacity=90); - opacity: .9; -} -.tooltip.top { - padding: 5px 0; - margin-top: -3px; -} -.tooltip.right { - padding: 0 5px; - margin-left: 3px; -} -.tooltip.bottom { - padding: 5px 0; - margin-top: 3px; -} -.tooltip.left { - padding: 0 5px; - margin-left: -3px; -} -.tooltip-inner { - max-width: 200px; - padding: 3px 8px; - color: #fff; - text-align: center; - background-color: #000; - border-radius: 4px; -} -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-width: 5px 5px 0; - border-top-color: #000; -} -.tooltip.top-left .tooltip-arrow { - right: 5px; - bottom: 0; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #000; -} -.tooltip.top-right .tooltip-arrow { - bottom: 0; - left: 5px; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #000; -} -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-width: 5px 5px 5px 0; - border-right-color: #000; -} -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-width: 5px 0 5px 5px; - border-left-color: #000; -} -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000; -} -.tooltip.bottom-left .tooltip-arrow { - top: 0; - right: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000; -} -.tooltip.bottom-right .tooltip-arrow { - top: 0; - left: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000; +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + .mt-xl-0, + .my-xl-0 { + margin-top: 0 !important; + } + .mr-xl-0, + .mx-xl-0 { + margin-right: 0 !important; + } + .mb-xl-0, + .my-xl-0 { + margin-bottom: 0 !important; + } + .ml-xl-0, + .mx-xl-0 { + margin-left: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .mt-xl-3, + .my-xl-3 { + margin-top: 1rem !important; + } + .mr-xl-3, + .mx-xl-3 { + margin-right: 1rem !important; + } + .mb-xl-3, + .my-xl-3 { + margin-bottom: 1rem !important; + } + .ml-xl-3, + .mx-xl-3 { + margin-left: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .mt-xl-4, + .my-xl-4 { + margin-top: 1.5rem !important; + } + .mr-xl-4, + .mx-xl-4 { + margin-right: 1.5rem !important; + } + .mb-xl-4, + .my-xl-4 { + margin-bottom: 1.5rem !important; + } + .ml-xl-4, + .mx-xl-4 { + margin-left: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .mt-xl-5, + .my-xl-5 { + margin-top: 3rem !important; + } + .mr-xl-5, + .mx-xl-5 { + margin-right: 3rem !important; + } + .mb-xl-5, + .my-xl-5 { + margin-bottom: 3rem !important; + } + .ml-xl-5, + .mx-xl-5 { + margin-left: 3rem !important; + } + .p-xl-0 { + padding: 0 !important; + } + .pt-xl-0, + .py-xl-0 { + padding-top: 0 !important; + } + .pr-xl-0, + .px-xl-0 { + padding-right: 0 !important; + } + .pb-xl-0, + .py-xl-0 { + padding-bottom: 0 !important; + } + .pl-xl-0, + .px-xl-0 { + padding-left: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .pt-xl-3, + .py-xl-3 { + padding-top: 1rem !important; + } + .pr-xl-3, + .px-xl-3 { + padding-right: 1rem !important; + } + .pb-xl-3, + .py-xl-3 { + padding-bottom: 1rem !important; + } + .pl-xl-3, + .px-xl-3 { + padding-left: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .pt-xl-4, + .py-xl-4 { + padding-top: 1.5rem !important; + } + .pr-xl-4, + .px-xl-4 { + padding-right: 1.5rem !important; + } + .pb-xl-4, + .py-xl-4 { + padding-bottom: 1.5rem !important; + } + .pl-xl-4, + .px-xl-4 { + padding-left: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .pt-xl-5, + .py-xl-5 { + padding-top: 3rem !important; + } + .pr-xl-5, + .px-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-5, + .py-xl-5 { + padding-bottom: 3rem !important; + } + .pl-xl-5, + .px-xl-5 { + padding-left: 3rem !important; + } + .m-xl-n1 { + margin: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.5rem !important; + } + .m-xl-n3 { + margin: -1rem !important; + } + .mt-xl-n3, + .my-xl-n3 { + margin-top: -1rem !important; + } + .mr-xl-n3, + .mx-xl-n3 { + margin-right: -1rem !important; + } + .mb-xl-n3, + .my-xl-n3 { + margin-bottom: -1rem !important; + } + .ml-xl-n3, + .mx-xl-n3 { + margin-left: -1rem !important; + } + .m-xl-n4 { + margin: -1.5rem !important; + } + .mt-xl-n4, + .my-xl-n4 { + margin-top: -1.5rem !important; + } + .mr-xl-n4, + .mx-xl-n4 { + margin-right: -1.5rem !important; + } + .mb-xl-n4, + .my-xl-n4 { + margin-bottom: -1.5rem !important; + } + .ml-xl-n4, + .mx-xl-n4 { + margin-left: -1.5rem !important; + } + .m-xl-n5 { + margin: -3rem !important; + } + .mt-xl-n5, + .my-xl-n5 { + margin-top: -3rem !important; + } + .mr-xl-n5, + .mx-xl-n5 { + margin-right: -3rem !important; + } + .mb-xl-n5, + .my-xl-n5 { + margin-bottom: -3rem !important; + } + .ml-xl-n5, + .mx-xl-n5 { + margin-left: -3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mt-xl-auto, + .my-xl-auto { + margin-top: auto !important; + } + .mr-xl-auto, + .mx-xl-auto { + margin-right: auto !important; + } + .mb-xl-auto, + .my-xl-auto { + margin-bottom: auto !important; + } + .ml-xl-auto, + .mx-xl-auto { + margin-left: auto !important; + } } -.popover { + +.stretched-link::after { position: absolute; top: 0; + right: 0; + bottom: 0; left: 0; - z-index: 1060; - display: none; - max-width: 276px; - padding: 1px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - font-style: normal; - font-weight: normal; - line-height: 1.42857143; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - letter-spacing: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; - white-space: normal; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); - box-shadow: 0 5px 10px rgba(0, 0, 0, .2); - - line-break: auto; -} -.popover.top { - margin-top: -10px; -} -.popover.right { - margin-left: 10px; -} -.popover.bottom { - margin-top: 10px; -} -.popover.left { - margin-left: -10px; -} -.popover-title { - padding: 8px 14px; - margin: 0; - font-size: 14px; - background-color: #f7f7f7; - border-bottom: 1px solid #ebebeb; - border-radius: 5px 5px 0 0; -} -.popover-content { - padding: 9px 14px; -} -.popover > .arrow, -.popover > .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.popover > .arrow { - border-width: 11px; -} -.popover > .arrow:after { + z-index: 1; + pointer-events: auto; content: ""; - border-width: 10px; -} -.popover.top > .arrow { - bottom: -11px; - left: 50%; - margin-left: -11px; - border-top-color: #999; - border-top-color: rgba(0, 0, 0, .25); - border-bottom-width: 0; -} -.popover.top > .arrow:after { - bottom: 1px; - margin-left: -10px; - content: " "; - border-top-color: #fff; - border-bottom-width: 0; + background-color: rgba(0, 0, 0, 0); } -.popover.right > .arrow { - top: 50%; - left: -11px; - margin-top: -11px; - border-right-color: #999; - border-right-color: rgba(0, 0, 0, .25); - border-left-width: 0; + +.text-monospace { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; } -.popover.right > .arrow:after { - bottom: -10px; - left: 1px; - content: " "; - border-right-color: #fff; - border-left-width: 0; + +.text-justify { + text-align: justify !important; } -.popover.bottom > .arrow { - top: -11px; - left: 50%; - margin-left: -11px; - border-top-width: 0; - border-bottom-color: #999; - border-bottom-color: rgba(0, 0, 0, .25); + +.text-wrap { + white-space: normal !important; } -.popover.bottom > .arrow:after { - top: 1px; - margin-left: -10px; - content: " "; - border-top-width: 0; - border-bottom-color: #fff; + +.text-nowrap { + white-space: nowrap !important; } -.popover.left > .arrow { - top: 50%; - right: -11px; - margin-top: -11px; - border-right-width: 0; - border-left-color: #999; - border-left-color: rgba(0, 0, 0, .25); + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.popover.left > .arrow:after { - right: 1px; - bottom: -10px; - content: " "; - border-right-width: 0; - border-left-color: #fff; + +.text-left { + text-align: left !important; } -.carousel { - position: relative; + +.text-right { + text-align: right !important; } -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; + +.text-center { + text-align: center !important; } -.carousel-inner > .item { - position: relative; - display: none; - -webkit-transition: .6s ease-in-out left; - -o-transition: .6s ease-in-out left; - transition: .6s ease-in-out left; + +@media (min-width: 576px) { + .text-sm-left { + text-align: left !important; + } + .text-sm-right { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } } -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - line-height: 1; + +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; + } + .text-md-right { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } } -@media all and (transform-3d), (-webkit-transform-3d) { - .carousel-inner > .item { - -webkit-transition: -webkit-transform .6s ease-in-out; - -o-transition: -o-transform .6s ease-in-out; - transition: transform .6s ease-in-out; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-perspective: 1000px; - perspective: 1000px; +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; } - .carousel-inner > .item.next, - .carousel-inner > .item.active.right { - left: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); + .text-lg-right { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} + +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; } - .carousel-inner > .item.prev, - .carousel-inner > .item.active.left { - left: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); + .text-xl-right { + text-align: right !important; } - .carousel-inner > .item.next.left, - .carousel-inner > .item.prev.right, - .carousel-inner > .item.active { - left: 0; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); + .text-xl-center { + text-align: center !important; } } -.carousel-inner > .active, -.carousel-inner > .next, -.carousel-inner > .prev { - display: block; + +.text-lowercase { + text-transform: lowercase !important; } -.carousel-inner > .active { - left: 0; + +.text-uppercase { + text-transform: uppercase !important; } -.carousel-inner > .next, -.carousel-inner > .prev { - position: absolute; - top: 0; - width: 100%; + +.text-capitalize { + text-transform: capitalize !important; } -.carousel-inner > .next { - left: 100%; + +.font-weight-light { + font-weight: 300 !important; } -.carousel-inner > .prev { - left: -100%; + +.font-weight-lighter { + font-weight: lighter !important; } -.carousel-inner > .next.left, -.carousel-inner > .prev.right { - left: 0; + +.font-weight-normal { + font-weight: 400 !important; } -.carousel-inner > .active.left { - left: -100%; + +.font-weight-bold { + font-weight: 700 !important; } -.carousel-inner > .active.right { - left: 100%; + +.font-weight-bolder { + font-weight: bolder !important; } -.carousel-control { - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 15%; - font-size: 20px; - color: #fff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, .6); - filter: alpha(opacity=50); - opacity: .5; + +.font-italic { + font-style: italic !important; } -.carousel-control.left { - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); - background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); - background-repeat: repeat-x; + +.text-white { + color: #fff !important; } -.carousel-control.right { - right: 0; - left: auto; - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); - background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); - background-repeat: repeat-x; -} -.carousel-control:hover, -.carousel-control:focus { - color: #fff; - text-decoration: none; - filter: alpha(opacity=90); - outline: 0; - opacity: .9; + +.text-primary { + color: #007bff !important; } -.carousel-control .icon-prev, -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-left, -.carousel-control .glyphicon-chevron-right { - position: absolute; - top: 50%; - z-index: 5; - display: inline-block; - margin-top: -10px; + +a.text-primary:hover, a.text-primary:focus { + color: #0056b3 !important; } -.carousel-control .icon-prev, -.carousel-control .glyphicon-chevron-left { - left: 50%; - margin-left: -10px; + +.text-secondary { + color: #6c757d !important; } -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-right { - right: 50%; - margin-right: -10px; + +a.text-secondary:hover, a.text-secondary:focus { + color: #494f54 !important; } -.carousel-control .icon-prev, -.carousel-control .icon-next { - width: 20px; - height: 20px; - font-family: serif; - line-height: 1; + +.text-success { + color: #28a745 !important; } -.carousel-control .icon-prev:before { - content: '\2039'; + +a.text-success:hover, a.text-success:focus { + color: #19692c !important; } -.carousel-control .icon-next:before { - content: '\203a'; + +.text-info { + color: #17a2b8 !important; } -.carousel-indicators { - position: absolute; - bottom: 10px; - left: 50%; - z-index: 15; - width: 60%; - padding-left: 0; - margin-left: -30%; - text-align: center; - list-style: none; + +a.text-info:hover, a.text-info:focus { + color: #0f6674 !important; } -.carousel-indicators li { - display: inline-block; - width: 10px; - height: 10px; - margin: 1px; - text-indent: -999px; - cursor: pointer; - background-color: #000 \9; - background-color: rgba(0, 0, 0, 0); - border: 1px solid #fff; - border-radius: 10px; + +.text-warning { + color: #ffc107 !important; } -.carousel-indicators .active { - width: 12px; - height: 12px; - margin: 0; - background-color: #fff; + +a.text-warning:hover, a.text-warning:focus { + color: #ba8b00 !important; } -.carousel-caption { - position: absolute; - right: 15%; - bottom: 20px; - left: 15%; - z-index: 10; - padding-top: 20px; - padding-bottom: 20px; - color: #fff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, .6); + +.text-danger { + color: #dc3545 !important; } -.carousel-caption .btn { - text-shadow: none; + +a.text-danger:hover, a.text-danger:focus { + color: #a71d2a !important; } -@media screen and (min-width: 768px) { - .carousel-control .glyphicon-chevron-left, - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-prev, - .carousel-control .icon-next { - width: 30px; - height: 30px; - margin-top: -15px; - font-size: 30px; - } - .carousel-control .glyphicon-chevron-left, - .carousel-control .icon-prev { - margin-left: -15px; - } - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-next { - margin-right: -15px; - } - .carousel-caption { - right: 20%; - left: 20%; - padding-bottom: 30px; - } - .carousel-indicators { - bottom: 20px; - } -} -.clearfix:before, -.clearfix:after, -.dl-horizontal dd:before, -.dl-horizontal dd:after, -.container:before, -.container:after, -.container-fluid:before, -.container-fluid:after, -.row:before, -.row:after, -.form-horizontal .form-group:before, -.form-horizontal .form-group:after, -.btn-toolbar:before, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:before, -.btn-group-vertical > .btn-group:after, -.nav:before, -.nav:after, -.navbar:before, -.navbar:after, -.navbar-header:before, -.navbar-header:after, -.navbar-collapse:before, -.navbar-collapse:after, -.pager:before, -.pager:after, -.panel-body:before, -.panel-body:after, -.modal-footer:before, -.modal-footer:after { - display: table; - content: " "; -} -.clearfix:after, -.dl-horizontal dd:after, -.container:after, -.container-fluid:after, -.row:after, -.form-horizontal .form-group:after, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:after, -.nav:after, -.navbar:after, -.navbar-header:after, -.navbar-collapse:after, -.pager:after, -.panel-body:after, -.modal-footer:after { - clear: both; + +.text-light { + color: #f8f9fa !important; } -.center-block { - display: block; - margin-right: auto; - margin-left: auto; + +a.text-light:hover, a.text-light:focus { + color: #cbd3da !important; } -.pull-right { - float: right !important; + +.text-dark { + color: #343a40 !important; } -.pull-left { - float: left !important; + +a.text-dark:hover, a.text-dark:focus { + color: #121416 !important; } -.hide { - display: none !important; + +.text-body { + color: #212529 !important; } -.show { - display: block !important; + +.text-muted { + color: #6c757d !important; } -.invisible { - visibility: hidden; + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; } + +.text-white-50 { + color: rgba(255, 255, 255, 0.5) !important; +} + .text-hide { font: 0/0 a; color: transparent; @@ -6584,220 +10160,104 @@ button.close { background-color: transparent; border: 0; } -.hidden { - display: none !important; -} -.affix { - position: fixed; -} -@-ms-viewport { - width: device-width; -} -.visible-xs, -.visible-sm, -.visible-md, -.visible-lg { - display: none !important; -} -.visible-xs-block, -.visible-xs-inline, -.visible-xs-inline-block, -.visible-sm-block, -.visible-sm-inline, -.visible-sm-inline-block, -.visible-md-block, -.visible-md-inline, -.visible-md-inline-block, -.visible-lg-block, -.visible-lg-inline, -.visible-lg-inline-block { - display: none !important; -} -@media (max-width: 767px) { - .visible-xs { - display: block !important; - } - table.visible-xs { - display: table !important; - } - tr.visible-xs { - display: table-row !important; - } - th.visible-xs, - td.visible-xs { - display: table-cell !important; - } -} -@media (max-width: 767px) { - .visible-xs-block { - display: block !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline { - display: inline !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline-block { - display: inline-block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm { - display: block !important; - } - table.visible-sm { - display: table !important; - } - tr.visible-sm { - display: table-row !important; - } - th.visible-sm, - td.visible-sm { - display: table-cell !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-block { - display: block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline { - display: inline !important; - } + +.text-decoration-none { + text-decoration: none !important; } -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline-block { - display: inline-block !important; - } + +.text-break { + word-break: break-word !important; + word-wrap: break-word !important; } -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md { - display: block !important; - } - table.visible-md { - display: table !important; - } - tr.visible-md { - display: table-row !important; - } - th.visible-md, - td.visible-md { - display: table-cell !important; - } + +.text-reset { + color: inherit !important; } -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-block { - display: block !important; - } + +.visible { + visibility: visible !important; } -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline { - display: inline !important; - } + +.invisible { + visibility: hidden !important; } -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline-block { - display: inline-block !important; + +@media print { + *, + *::before, + *::after { + text-shadow: none !important; + box-shadow: none !important; } -} -@media (min-width: 1200px) { - .visible-lg { - display: block !important; + a:not(.btn) { + text-decoration: underline; } - table.visible-lg { - display: table !important; + abbr[title]::after { + content: " (" attr(title) ")"; } - tr.visible-lg { - display: table-row !important; + pre { + white-space: pre-wrap !important; } - th.visible-lg, - td.visible-lg { - display: table-cell !important; + pre, + blockquote { + border: 1px solid #adb5bd; + page-break-inside: avoid; } -} -@media (min-width: 1200px) { - .visible-lg-block { - display: block !important; + thead { + display: table-header-group; } -} -@media (min-width: 1200px) { - .visible-lg-inline { - display: inline !important; + tr, + img { + page-break-inside: avoid; } -} -@media (min-width: 1200px) { - .visible-lg-inline-block { - display: inline-block !important; + p, + h2, + h3 { + orphans: 3; + widows: 3; } -} -@media (max-width: 767px) { - .hidden-xs { - display: none !important; + h2, + h3 { + page-break-after: avoid; } -} -@media (min-width: 768px) and (max-width: 991px) { - .hidden-sm { - display: none !important; + @page { + size: a3; } -} -@media (min-width: 992px) and (max-width: 1199px) { - .hidden-md { - display: none !important; + body { + min-width: 992px !important; } -} -@media (min-width: 1200px) { - .hidden-lg { - display: none !important; + .container { + min-width: 992px !important; } -} -.visible-print { - display: none !important; -} -@media print { - .visible-print { - display: block !important; + .navbar { + display: none; } - table.visible-print { - display: table !important; + .badge { + border: 1px solid #000; } - tr.visible-print { - display: table-row !important; + .table { + border-collapse: collapse !important; } - th.visible-print, - td.visible-print { - display: table-cell !important; + .table td, + .table th { + background-color: #fff !important; } -} -.visible-print-block { - display: none !important; -} -@media print { - .visible-print-block { - display: block !important; + .table-bordered th, + .table-bordered td { + border: 1px solid #dee2e6 !important; } -} -.visible-print-inline { - display: none !important; -} -@media print { - .visible-print-inline { - display: inline !important; + .table-dark { + color: inherit; } -} -.visible-print-inline-block { - display: none !important; -} -@media print { - .visible-print-inline-block { - display: inline-block !important; + .table-dark th, + .table-dark td, + .table-dark thead th, + .table-dark tbody + tbody { + border-color: #dee2e6; } -} -@media print { - .hidden-print { - display: none !important; + .table .thead-dark th { + color: inherit; + border-color: #dee2e6; } } -/*# sourceMappingURL=bootstrap.css.map */ +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/assets/css/vendor/bootstrap.css.map b/assets/css/vendor/bootstrap.css.map new file mode 100644 index 000000000..549dbb45e --- /dev/null +++ b/assets/css/vendor/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","bootstrap.css","../../scss/_root.scss","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/vendor/_rfs.scss","../../scss/mixins/_hover.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/mixins/_border-radius.scss","../../scss/_code.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/_tables.scss","../../scss/mixins/_table-row.scss","../../scss/_functions.scss","../../scss/_forms.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_forms.scss","../../scss/mixins/_gradients.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/mixins/_nav-divider.scss","../../scss/_button-group.scss","../../scss/_input-group.scss","../../scss/_custom-forms.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/mixins/_badge.scss","../../scss/_jumbotron.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_media.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/utilities/_align.scss","../../scss/mixins/_background-variant.scss","../../scss/utilities/_background.scss","../../scss/utilities/_borders.scss","../../scss/utilities/_display.scss","../../scss/utilities/_embed.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_float.scss","../../scss/utilities/_interactions.scss","../../scss/utilities/_overflow.scss","../../scss/utilities/_position.scss","../../scss/utilities/_screenreaders.scss","../../scss/mixins/_screen-reader.scss","../../scss/utilities/_shadows.scss","../../scss/utilities/_sizing.scss","../../scss/utilities/_spacing.scss","../../scss/utilities/_stretched-link.scss","../../scss/utilities/_text.scss","../../scss/mixins/_text-truncate.scss","../../scss/mixins/_text-emphasis.scss","../../scss/mixins/_text-hide.scss","../../scss/utilities/_visibility.scss","../../scss/_print.scss"],"names":[],"mappings":"AAAA;;;;;ECKE;ACJF;EAGI,eAAc;EAAd,iBAAc;EAAd,iBAAc;EAAd,eAAc;EAAd,cAAc;EAAd,iBAAc;EAAd,iBAAc;EAAd,gBAAc;EAAd,eAAc;EAAd,eAAc;EAAd,aAAc;EAAd,eAAc;EAAd,oBAAc;EAId,kBAAc;EAAd,oBAAc;EAAd,kBAAc;EAAd,eAAc;EAAd,kBAAc;EAAd,iBAAc;EAAd,gBAAc;EAAd,eAAc;EAId,kBAAiC;EAAjC,sBAAiC;EAAjC,sBAAiC;EAAjC,sBAAiC;EAAjC,uBAAiC;EAKnC,+MAAyB;EACzB,6GAAwB;ADiB1B;;AEjBA;;;EAGE,sBAAsB;AFoBxB;;AEjBA;EACE,uBAAuB;EACvB,iBAAiB;EACjB,8BAA8B;EAC9B,6CCXa;AH+Bf;;AEdA;EACE,cAAc;AFiBhB;;AEPA;EACE,SAAS;EACT,kMCqOiN;ECrJ7M,eAtCY;EFxChB,gBC8O+B;ED7O/B,gBCkP+B;EDjP/B,cCnCgB;EDoChB,gBAAgB;EAChB,sBC9Ca;AHwDf;;AAEA;EECE,qBAAqB;AFCvB;;AEQA;EACE,uBAAuB;EACvB,SAAS;EACT,iBAAiB;AFLnB;;AEkBA;EACE,aAAa;EACb,qBCgNuC;AH/NzC;;AEsBA;EACE,aAAa;EACb,mBCoF8B;AHvGhC;;AE8BA;;EAEE,0BAA0B;EAC1B,yCAAiC;EAAjC,iCAAiC;EACjC,YAAY;EACZ,gBAAgB;EAChB,sCAA8B;EAA9B,8BAA8B;AF3BhC;;AE8BA;EACE,mBAAmB;EACnB,kBAAkB;EAClB,oBAAoB;AF3BtB;;AE8BA;;;EAGE,aAAa;EACb,mBAAmB;AF3BrB;;AE8BA;;;;EAIE,gBAAgB;AF3BlB;;AE8BA;EACE,gBCiJ+B;AH5KjC;;AE8BA;EACE,oBAAoB;EACpB,cAAc;AF3BhB;;AE8BA;EACE,gBAAgB;AF3BlB;;AE8BA;;EAEE,mBCoIkC;AH/JpC;;AE8BA;EExFI,cAAW;AJ8Df;;AEmCA;;EAEE,kBAAkB;EEnGhB,cAAW;EFqGb,cAAc;EACd,wBAAwB;AFhC1B;;AEmCA;EAAM,cAAc;AF/BpB;;AEgCA;EAAM,UAAU;AF5BhB;;AEmCA;EACE,cCvJe;EDwJf,qBCX4C;EDY5C,6BAA6B;AFhC/B;;AKhJE;EHmLE,cCd8D;EDe9D,0BCd+C;AHjBnD;;AEwCA;EACE,cAAc;EACd,qBAAqB;AFrCvB;;AK1JE;EHkME,cAAc;EACd,qBAAqB;AFpCzB;;AE6CA;;;;EAIE,iGCyDgH;EC7M9G,cAAW;AJ2Gf;;AE6CA;EAEE,aAAa;EAEb,mBAAmB;EAEnB,cAAc;EAGd,6BAA6B;AF/C/B;;AEuDA;EAEE,gBAAgB;AFrDlB;;AE6DA;EACE,sBAAsB;EACtB,kBAAkB;AF1DpB;;AE6DA;EAGE,gBAAgB;EAChB,sBAAsB;AF5DxB;;AEoEA;EACE,yBAAyB;AFjE3B;;AEoEA;EACE,oBC6EkC;ED5ElC,uBC4EkC;ED3ElC,cCtQgB;EDuQhB,gBAAgB;EAChB,oBAAoB;AFjEtB;;AEwEA;EAEE,mBAAmB;EACnB,gCAAgC;AFtElC;;AE8EA;EAEE,qBAAqB;EACrB,qBC2J2C;AHvO7C;;AEkFA;EAEE,gBAAgB;AFhFlB;;AEuFA;EACE,mBAAmB;EACnB,0CAA0C;AFpF5C;;AEuFA;;;;;EAKE,SAAS;EACT,oBAAoB;EE5PlB,kBAAW;EF8Pb,oBAAoB;AFpFtB;;AEuFA;;EAEE,iBAAiB;AFpFnB;;AEuFA;;EAEE,oBAAoB;AFpFtB;;AAEA;EEyFE,eAAe;AFvFjB;;AE6FA;EACE,iBAAiB;AF1FnB;;AEiGA;;;;EAIE,0BAA0B;AF9F5B;;AEmGE;;;;EAKI,eAAe;AFjGrB;;AEuGA;;;;EAIE,UAAU;EACV,kBAAkB;AFpGpB;;AEuGA;;EAEE,sBAAsB;EACtB,UAAU;AFpGZ;;AEwGA;EACE,cAAc;EAEd,gBAAgB;AFtGlB;;AEyGA;EAME,YAAY;EAEZ,UAAU;EACV,SAAS;EACT,SAAS;AF5GX;;AEiHA;EACE,cAAc;EACd,WAAW;EACX,eAAe;EACf,UAAU;EACV,oBAAoB;EEnShB,iBAtCY;EF2UhB,oBAAoB;EACpB,cAAc;EACd,mBAAmB;AF9GrB;;AEiHA;EACE,wBAAwB;AF9G1B;;AAEA;;EEkHE,YAAY;AF/Gd;;AAEA;EEqHE,oBAAoB;EACpB,wBAAwB;AFnH1B;;AAEA;EEyHE,wBAAwB;AFvH1B;;AE+HA;EACE,aAAa;EACb,0BAA0B;AF5H5B;;AEmIA;EACE,qBAAqB;AFhIvB;;AEmIA;EACE,kBAAkB;EAClB,eAAe;AFhIjB;;AEmIA;EACE,aAAa;AFhIf;;AAEA;EEoIE,wBAAwB;AFlI1B;;AM1VA;;EAEE,qBHqSuC;EGnSvC,gBHqS+B;EGpS/B,gBHqS+B;AHuDjC;;AMxVA;EFgHM,iBAtCY;AJkRlB;;AM3VA;EF+GM,eAtCY;AJsRlB;;AM9VA;EF8GM,kBAtCY;AJ0RlB;;AMjWA;EF6GM,iBAtCY;AJ8RlB;;AMpWA;EF4GM,kBAtCY;AJkSlB;;AMvWA;EF2GM,eAtCY;AJsSlB;;AMzWA;EFyGM,kBAtCY;EEjEhB,gBHuS+B;AHqEjC;;AMxWA;EFmGM,eAtCY;EE3DhB,gBH0R+B;EGzR/B,gBHiR+B;AH0FjC;;AMzWA;EF8FM,iBAtCY;EEtDhB,gBHsR+B;EGrR/B,gBH4Q+B;AHgGjC;;AM1WA;EFyFM,iBAtCY;EEjDhB,gBHkR+B;EGjR/B,gBHuQ+B;AHsGjC;;AM3WA;EFoFM,iBAtCY;EE5ChB,gBH8Q+B;EG7Q/B,gBHkQ+B;AH4GjC;;AEjVA;EIpBE,gBHgFW;EG/EX,mBH+EW;EG9EX,SAAS;EACT,wCHzCa;AHkZf;;AMjWA;;EFMI,cAAW;EEHb,gBH0N+B;AH0IjC;;AMjWA;;EAEE,cHkQgC;EGjQhC,yBH0QmC;AH0FrC;;AM5VA;EC/EE,eAAe;EACf,gBAAgB;AP+alB;;AM5VA;ECpFE,eAAe;EACf,gBAAgB;APoblB;;AM9VA;EACE,qBAAqB;ANiWvB;;AMlWA;EAII,oBHoP+B;AH8GnC;;AMxVA;EFjCI,cAAW;EEmCb,yBAAyB;AN2V3B;;AMvVA;EACE,mBHuBW;ECRP,kBAtCY;AJkXlB;;AMvVA;EACE,cAAc;EF7CZ,cAAW;EE+Cb,cH1GgB;AHoclB;;AM7VA;EAMI,qBAAqB;AN2VzB;;AQ9cA;ECIE,eAAe;EAGf,YAAY;AT4cd;;AQ7cA;EACE,gBL+/BwC;EK9/BxC,sBLRa;EKSb,yBLNgB;EOQd,sBP6NgC;EMpOlC,eAAe;EAGf,YAAY;ATqdd;;AQvcA;EAEE,qBAAqB;ARycvB;;AQtcA;EACE,qBAA0B;EAC1B,cAAc;ARychB;;AQtcA;EJkCI,cAAW;EIhCb,cL3BgB;AHoelB;;AWhfA;EPuEI,gBAAW;EOrEb,cRmCe;EQlCf,qBAAqB;AXmfvB;;AWhfE;EACE,cAAc;AXmflB;;AW9eA;EACE,sBRmlCuC;ECzhCrC,gBAAW;EOxDb,WRTa;EQUb,yBRDgB;EOEd,qBP+N+B;AHkRnC;;AWtfA;EASI,UAAU;EPkDV,eAAW;EOhDX,gBRwQ6B;AHyOjC;;AEzSA;ESjME,cAAc;EPyCZ,gBAAW;EOvCb,cRjBgB;AH+flB;;AWjfA;EP0CI,kBAAW;EOlCX,cAAc;EACd,kBAAkB;AX8etB;;AWzeA;EACE,iBR0jCuC;EQzjCvC,kBAAkB;AX4epB;;AYphBE;;;;;;ECDA,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;EACzB,kBAAkB;EAClB,iBAAiB;Ab8hBnB;;Ac3eI;EFzCE;IACE,gBT+LG;EHyVT;AACF;;AcjfI;EFzCE;IACE,gBTgMG;EH8VT;AACF;;AcvfI;EFzCE;IACE,gBTiMG;EHmWT;AACF;;Ac7fI;EFzCE;IACE,iBTkMI;EHwWV;AACF;;AY/gBE;ECnCA,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,mBAA0B;EAC1B,kBAAyB;AbsjB3B;;AYhhBE;EACE,eAAe;EACf,cAAc;AZmhBlB;;AYrhBE;;EAMI,gBAAgB;EAChB,eAAe;AZohBrB;;Ae1kBE;;;;;;EACE,kBAAkB;EAClB,WAAW;EACX,mBAA0B;EAC1B,kBAAyB;AfklB7B;;Ae5jBM;EACE,0BAAa;EAAb,aAAa;EACb,oBAAY;EAAZ,YAAY;EACZ,eAAe;Af+jBvB;;Ae1jBU;EFwBN,kBAAuB;EAAvB,cAAuB;EACvB,eAAwB;AbsiB5B;;Ae/jBU;EFwBN,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;Ab2iB5B;;AepkBU;EFwBN,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;AbgjB5B;;AezkBU;EFwBN,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;AbqjB5B;;Ae9kBU;EFwBN,iBAAuB;EAAvB,aAAuB;EACvB,cAAwB;Ab0jB5B;;AenlBU;EFwBN,wBAAuB;EAAvB,oBAAuB;EACvB,qBAAwB;Ab+jB5B;;AellBM;EFCJ,kBAAc;EAAd,cAAc;EACd,WAAW;EACX,eAAe;AbqlBjB;;AellBU;EFbR,uBAAsC;EAAtC,mBAAsC;EAItC,oBAAuC;AbgmBzC;;AevlBU;EFbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbqmBzC;;Ae5lBU;EFbR,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;Ab0mBzC;;AejmBU;EFbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab+mBzC;;AetmBU;EFbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbonBzC;;Ae3mBU;EFbR,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AbynBzC;;AehnBU;EFbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab8nBzC;;AernBU;EFbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbmoBzC;;Ae1nBU;EFbR,iBAAsC;EAAtC,aAAsC;EAItC,cAAuC;AbwoBzC;;Ae/nBU;EFbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;Ab6oBzC;;AepoBU;EFbR,wBAAsC;EAAtC,oBAAsC;EAItC,qBAAuC;AbkpBzC;;AezoBU;EFbR,kBAAsC;EAAtC,cAAsC;EAItC,eAAuC;AbupBzC;;AexoBM;EAAwB,kBAAS;EAAT,SAAS;Af4oBvC;;Ae1oBM;EAAuB,kBZmKG;EYnKH,SZmKG;AH2ehC;;Ae3oBQ;EAAwB,iBADZ;EACY,QADZ;AfgpBpB;;Ae/oBQ;EAAwB,iBADZ;EACY,QADZ;AfopBpB;;AenpBQ;EAAwB,iBADZ;EACY,QADZ;AfwpBpB;;AevpBQ;EAAwB,iBADZ;EACY,QADZ;Af4pBpB;;Ae3pBQ;EAAwB,iBADZ;EACY,QADZ;AfgqBpB;;Ae/pBQ;EAAwB,iBADZ;EACY,QADZ;AfoqBpB;;AenqBQ;EAAwB,iBADZ;EACY,QADZ;AfwqBpB;;AevqBQ;EAAwB,iBADZ;EACY,QADZ;Af4qBpB;;Ae3qBQ;EAAwB,iBADZ;EACY,QADZ;AfgrBpB;;Ae/qBQ;EAAwB,iBADZ;EACY,QADZ;AforBpB;;AenrBQ;EAAwB,kBADZ;EACY,SADZ;AfwrBpB;;AevrBQ;EAAwB,kBADZ;EACY,SADZ;Af4rBpB;;Ae3rBQ;EAAwB,kBADZ;EACY,SADZ;AfgsBpB;;AexrBY;EFhBV,sBAA8C;Ab4sBhD;;Ae5rBY;EFhBV,uBAA8C;AbgtBhD;;AehsBY;EFhBV,gBAA8C;AbotBhD;;AepsBY;EFhBV,uBAA8C;AbwtBhD;;AexsBY;EFhBV,uBAA8C;Ab4tBhD;;Ae5sBY;EFhBV,gBAA8C;AbguBhD;;AehtBY;EFhBV,uBAA8C;AbouBhD;;AeptBY;EFhBV,uBAA8C;AbwuBhD;;AextBY;EFhBV,gBAA8C;Ab4uBhD;;Ae5tBY;EFhBV,uBAA8C;AbgvBhD;;AehuBY;EFhBV,uBAA8C;AbovBhD;;Ac/uBI;EC3BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;Ef8wBrB;EezwBQ;IFwBN,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EbovB1B;Ee7wBQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbwvB1B;EejxBQ;IFwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb4vB1B;EerxBQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbgwB1B;EezxBQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbowB1B;Ee7xBQ;IFwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbwwB1B;Ee3xBI;IFCJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;Eb6xBf;Ee1xBQ;IFbR,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EbuyBvC;Ee9xBQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb2yBvC;EelyBQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb+yBvC;EetyBQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbmzBvC;Ee1yBQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbuzBvC;Ee9yBQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb2zBvC;EelzBQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+zBvC;EetzBQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebm0BvC;Ee1zBQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebu0BvC;Ee9zBQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb20BvC;Eel0BQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+0BvC;Eet0BQ;IFbR,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Ebm1BvC;Eep0BI;IAAwB,kBAAS;IAAT,SAAS;Efu0BrC;Eer0BI;IAAuB,kBZmKG;IYnKH,SZmKG;EHqqB9B;Eer0BM;IAAwB,iBADZ;IACY,QADZ;Efy0BlB;Eex0BM;IAAwB,iBADZ;IACY,QADZ;Ef40BlB;Ee30BM;IAAwB,iBADZ;IACY,QADZ;Ef+0BlB;Ee90BM;IAAwB,iBADZ;IACY,QADZ;Efk1BlB;Eej1BM;IAAwB,iBADZ;IACY,QADZ;Efq1BlB;Eep1BM;IAAwB,iBADZ;IACY,QADZ;Efw1BlB;Eev1BM;IAAwB,iBADZ;IACY,QADZ;Ef21BlB;Ee11BM;IAAwB,iBADZ;IACY,QADZ;Ef81BlB;Ee71BM;IAAwB,iBADZ;IACY,QADZ;Efi2BlB;Eeh2BM;IAAwB,iBADZ;IACY,QADZ;Efo2BlB;Een2BM;IAAwB,kBADZ;IACY,SADZ;Efu2BlB;Eet2BM;IAAwB,kBADZ;IACY,SADZ;Ef02BlB;Eez2BM;IAAwB,kBADZ;IACY,SADZ;Ef62BlB;Eer2BU;IFhBV,cAA4B;Ebw3B5B;Eex2BU;IFhBV,sBAA8C;Eb23B9C;Ee32BU;IFhBV,uBAA8C;Eb83B9C;Ee92BU;IFhBV,gBAA8C;Ebi4B9C;Eej3BU;IFhBV,uBAA8C;Ebo4B9C;Eep3BU;IFhBV,uBAA8C;Ebu4B9C;Eev3BU;IFhBV,gBAA8C;Eb04B9C;Ee13BU;IFhBV,uBAA8C;Eb64B9C;Ee73BU;IFhBV,uBAA8C;Ebg5B9C;Eeh4BU;IFhBV,gBAA8C;Ebm5B9C;Een4BU;IFhBV,uBAA8C;Ebs5B9C;Eet4BU;IFhBV,uBAA8C;Eby5B9C;AACF;;Acr5BI;EC3BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;Efo7BrB;Ee/6BQ;IFwBN,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;Eb05B1B;Een7BQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb85B1B;Eev7BQ;IFwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Ebk6B1B;Ee37BQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Ebs6B1B;Ee/7BQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb06B1B;Een8BQ;IFwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb86B1B;Eej8BI;IFCJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;Ebm8Bf;Eeh8BQ;IFbR,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;Eb68BvC;Eep8BQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebi9BvC;Eex8BQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebq9BvC;Ee58BQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eby9BvC;Eeh9BQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb69BvC;Eep9BQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Ebi+BvC;Eex9BQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebq+BvC;Ee59BQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eby+BvC;Eeh+BQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb6+BvC;Eep+BQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebi/BvC;Eex+BQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebq/BvC;Ee5+BQ;IFbR,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Eby/BvC;Ee1+BI;IAAwB,kBAAS;IAAT,SAAS;Ef6+BrC;Ee3+BI;IAAuB,kBZmKG;IYnKH,SZmKG;EH20B9B;Ee3+BM;IAAwB,iBADZ;IACY,QADZ;Ef++BlB;Ee9+BM;IAAwB,iBADZ;IACY,QADZ;Efk/BlB;Eej/BM;IAAwB,iBADZ;IACY,QADZ;Efq/BlB;Eep/BM;IAAwB,iBADZ;IACY,QADZ;Efw/BlB;Eev/BM;IAAwB,iBADZ;IACY,QADZ;Ef2/BlB;Ee1/BM;IAAwB,iBADZ;IACY,QADZ;Ef8/BlB;Ee7/BM;IAAwB,iBADZ;IACY,QADZ;EfigClB;EehgCM;IAAwB,iBADZ;IACY,QADZ;EfogClB;EengCM;IAAwB,iBADZ;IACY,QADZ;EfugClB;EetgCM;IAAwB,iBADZ;IACY,QADZ;Ef0gClB;EezgCM;IAAwB,kBADZ;IACY,SADZ;Ef6gClB;Ee5gCM;IAAwB,kBADZ;IACY,SADZ;EfghClB;Ee/gCM;IAAwB,kBADZ;IACY,SADZ;EfmhClB;Ee3gCU;IFhBV,cAA4B;Eb8hC5B;Ee9gCU;IFhBV,sBAA8C;EbiiC9C;EejhCU;IFhBV,uBAA8C;EboiC9C;EephCU;IFhBV,gBAA8C;EbuiC9C;EevhCU;IFhBV,uBAA8C;Eb0iC9C;Ee1hCU;IFhBV,uBAA8C;Eb6iC9C;Ee7hCU;IFhBV,gBAA8C;EbgjC9C;EehiCU;IFhBV,uBAA8C;EbmjC9C;EeniCU;IFhBV,uBAA8C;EbsjC9C;EetiCU;IFhBV,gBAA8C;EbyjC9C;EeziCU;IFhBV,uBAA8C;Eb4jC9C;Ee5iCU;IFhBV,uBAA8C;Eb+jC9C;AACF;;Ac3jCI;EC3BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;Ef0lCrB;EerlCQ;IFwBN,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EbgkC1B;EezlCQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbokC1B;Ee7lCQ;IFwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbwkC1B;EejmCQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb4kC1B;EermCQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbglC1B;EezmCQ;IFwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;EbolC1B;EevmCI;IFCJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;EbymCf;EetmCQ;IFbR,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EbmnCvC;Ee1mCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbunCvC;Ee9mCQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb2nCvC;EelnCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+nCvC;EetnCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbmoCvC;Ee1nCQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbuoCvC;Ee9nCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb2oCvC;EeloCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb+oCvC;EetoCQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbmpCvC;Ee1oCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbupCvC;Ee9oCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb2pCvC;EelpCQ;IFbR,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Eb+pCvC;EehpCI;IAAwB,kBAAS;IAAT,SAAS;EfmpCrC;EejpCI;IAAuB,kBZmKG;IYnKH,SZmKG;EHi/B9B;EejpCM;IAAwB,iBADZ;IACY,QADZ;EfqpClB;EeppCM;IAAwB,iBADZ;IACY,QADZ;EfwpClB;EevpCM;IAAwB,iBADZ;IACY,QADZ;Ef2pClB;Ee1pCM;IAAwB,iBADZ;IACY,QADZ;Ef8pClB;Ee7pCM;IAAwB,iBADZ;IACY,QADZ;EfiqClB;EehqCM;IAAwB,iBADZ;IACY,QADZ;EfoqClB;EenqCM;IAAwB,iBADZ;IACY,QADZ;EfuqClB;EetqCM;IAAwB,iBADZ;IACY,QADZ;Ef0qClB;EezqCM;IAAwB,iBADZ;IACY,QADZ;Ef6qClB;Ee5qCM;IAAwB,iBADZ;IACY,QADZ;EfgrClB;Ee/qCM;IAAwB,kBADZ;IACY,SADZ;EfmrClB;EelrCM;IAAwB,kBADZ;IACY,SADZ;EfsrClB;EerrCM;IAAwB,kBADZ;IACY,SADZ;EfyrClB;EejrCU;IFhBV,cAA4B;EbosC5B;EeprCU;IFhBV,sBAA8C;EbusC9C;EevrCU;IFhBV,uBAA8C;Eb0sC9C;Ee1rCU;IFhBV,gBAA8C;Eb6sC9C;Ee7rCU;IFhBV,uBAA8C;EbgtC9C;EehsCU;IFhBV,uBAA8C;EbmtC9C;EensCU;IFhBV,gBAA8C;EbstC9C;EetsCU;IFhBV,uBAA8C;EbytC9C;EezsCU;IFhBV,uBAA8C;Eb4tC9C;Ee5sCU;IFhBV,gBAA8C;Eb+tC9C;Ee/sCU;IFhBV,uBAA8C;EbkuC9C;EeltCU;IFhBV,uBAA8C;EbquC9C;AACF;;AcjuCI;EC3BE;IACE,0BAAa;IAAb,aAAa;IACb,oBAAY;IAAZ,YAAY;IACZ,eAAe;EfgwCrB;Ee3vCQ;IFwBN,kBAAuB;IAAvB,cAAuB;IACvB,eAAwB;EbsuC1B;Ee/vCQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;Eb0uC1B;EenwCQ;IFwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb8uC1B;EevwCQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbkvC1B;Ee3wCQ;IFwBN,iBAAuB;IAAvB,aAAuB;IACvB,cAAwB;EbsvC1B;Ee/wCQ;IFwBN,wBAAuB;IAAvB,oBAAuB;IACvB,qBAAwB;Eb0vC1B;Ee7wCI;IFCJ,kBAAc;IAAd,cAAc;IACd,WAAW;IACX,eAAe;Eb+wCf;Ee5wCQ;IFbR,uBAAsC;IAAtC,mBAAsC;IAItC,oBAAuC;EbyxCvC;EehxCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb6xCvC;EepxCQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbiyCvC;EexxCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbqyCvC;Ee5xCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbyyCvC;EehyCQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;Eb6yCvC;EepyCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbizCvC;EexyCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;EbqzCvC;Ee5yCQ;IFbR,iBAAsC;IAAtC,aAAsC;IAItC,cAAuC;EbyzCvC;EehzCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Eb6zCvC;EepzCQ;IFbR,wBAAsC;IAAtC,oBAAsC;IAItC,qBAAuC;Ebi0CvC;EexzCQ;IFbR,kBAAsC;IAAtC,cAAsC;IAItC,eAAuC;Ebq0CvC;EetzCI;IAAwB,kBAAS;IAAT,SAAS;EfyzCrC;EevzCI;IAAuB,kBZmKG;IYnKH,SZmKG;EHupC9B;EevzCM;IAAwB,iBADZ;IACY,QADZ;Ef2zClB;Ee1zCM;IAAwB,iBADZ;IACY,QADZ;Ef8zClB;Ee7zCM;IAAwB,iBADZ;IACY,QADZ;Efi0ClB;Eeh0CM;IAAwB,iBADZ;IACY,QADZ;Efo0ClB;Een0CM;IAAwB,iBADZ;IACY,QADZ;Efu0ClB;Eet0CM;IAAwB,iBADZ;IACY,QADZ;Ef00ClB;Eez0CM;IAAwB,iBADZ;IACY,QADZ;Ef60ClB;Ee50CM;IAAwB,iBADZ;IACY,QADZ;Efg1ClB;Ee/0CM;IAAwB,iBADZ;IACY,QADZ;Efm1ClB;Eel1CM;IAAwB,iBADZ;IACY,QADZ;Efs1ClB;Eer1CM;IAAwB,kBADZ;IACY,SADZ;Efy1ClB;Eex1CM;IAAwB,kBADZ;IACY,SADZ;Ef41ClB;Ee31CM;IAAwB,kBADZ;IACY,SADZ;Ef+1ClB;Eev1CU;IFhBV,cAA4B;Eb02C5B;Ee11CU;IFhBV,sBAA8C;Eb62C9C;Ee71CU;IFhBV,uBAA8C;Ebg3C9C;Eeh2CU;IFhBV,gBAA8C;Ebm3C9C;Een2CU;IFhBV,uBAA8C;Ebs3C9C;Eet2CU;IFhBV,uBAA8C;Eby3C9C;Eez2CU;IFhBV,gBAA8C;Eb43C9C;Ee52CU;IFhBV,uBAA8C;Eb+3C9C;Ee/2CU;IFhBV,uBAA8C;Ebk4C9C;Eel3CU;IFhBV,gBAA8C;Ebq4C9C;Eer3CU;IFhBV,uBAA8C;Ebw4C9C;Eex3CU;IFhBV,uBAA8C;Eb24C9C;AACF;;AgB/7CA;EACE,WAAW;EACX,mBbiIW;EahIX,cbSgB;AHy7ClB;;AgBr8CA;;EAQI,gBbkVgC;EajVhC,mBAAmB;EACnB,6BbJc;AHs8ClB;;AgB58CA;EAcI,sBAAsB;EACtB,gCbTc;AH28ClB;;AgBj9CA;EAmBI,6Bbbc;AH+8ClB;;AgBz7CA;;EAGI,eb4T+B;AH+nCnC;;AgBl7CA;EACE,yBbnCgB;AHw9ClB;;AgBt7CA;;EAKI,yBbvCc;AH69ClB;;AgB37CA;;EAWM,wBAA4C;AhBq7ClD;;AgBh7CA;;;;EAKI,SAAS;AhBk7Cb;;AgB16CA;EAEI,qCb1DW;AHs+Cf;;AK3+CE;EW2EI,cbvEY;EawEZ,sCbvES;AH2+Cf;;AiBv/CE;;;EAII,yBCgG4D;AlBy5ClE;;AiB7/CE;;;;EAYM,qBCwF0D;AlBg6ClE;;AK7/CE;EYiBM,yBAJsC;AjBo/C9C;;AiBr/CE;;EASQ,yBARoC;AjBy/C9C;;AiB7gDE;;;EAII,yBCgG4D;AlB+6ClE;;AiBnhDE;;;;EAYM,qBCwF0D;AlBs7ClE;;AKnhDE;EYiBM,yBAJsC;AjB0gD9C;;AiB3gDE;;EASQ,yBARoC;AjB+gD9C;;AiBniDE;;;EAII,yBCgG4D;AlBq8ClE;;AiBziDE;;;;EAYM,qBCwF0D;AlB48ClE;;AKziDE;EYiBM,yBAJsC;AjBgiD9C;;AiBjiDE;;EASQ,yBARoC;AjBqiD9C;;AiBzjDE;;;EAII,yBCgG4D;AlB29ClE;;AiB/jDE;;;;EAYM,qBCwF0D;AlBk+ClE;;AK/jDE;EYiBM,yBAJsC;AjBsjD9C;;AiBvjDE;;EASQ,yBARoC;AjB2jD9C;;AiB/kDE;;;EAII,yBCgG4D;AlBi/ClE;;AiBrlDE;;;;EAYM,qBCwF0D;AlBw/ClE;;AKrlDE;EYiBM,yBAJsC;AjB4kD9C;;AiB7kDE;;EASQ,yBARoC;AjBilD9C;;AiBrmDE;;;EAII,yBCgG4D;AlBugDlE;;AiB3mDE;;;;EAYM,qBCwF0D;AlB8gDlE;;AK3mDE;EYiBM,yBAJsC;AjBkmD9C;;AiBnmDE;;EASQ,yBARoC;AjBumD9C;;AiB3nDE;;;EAII,yBCgG4D;AlB6hDlE;;AiBjoDE;;;;EAYM,qBCwF0D;AlBoiDlE;;AKjoDE;EYiBM,yBAJsC;AjBwnD9C;;AiBznDE;;EASQ,yBARoC;AjB6nD9C;;AiBjpDE;;;EAII,yBCgG4D;AlBmjDlE;;AiBvpDE;;;;EAYM,qBCwF0D;AlB0jDlE;;AKvpDE;EYiBM,yBAJsC;AjB8oD9C;;AiB/oDE;;EASQ,yBARoC;AjBmpD9C;;AiBvqDE;;;EAII,sCdQS;AHiqDf;;AKtqDE;EYiBM,sCAJsC;AjB6pD9C;;AiB9pDE;;EASQ,sCARoC;AjBkqD9C;;AgB5kDA;EAGM,Wb3GS;Ea4GT,yBbpGY;EaqGZ,qBbgQqD;AH60C3D;;AgBllDA;EAWM,cb5GY;Ea6GZ,yBblHY;EamHZ,qBblHY;AH6rDlB;;AgBtkDA;EACE,Wb3Ha;Ea4Hb,yBbpHgB;AH6rDlB;;AgB3kDA;;;EAOI,qBb4OuD;AH81C3D;;AgBjlDA;EAWI,SAAS;AhB0kDb;;AgBrlDA;EAgBM,2Cb1IS;AHmtDf;;AK9sDE;EW4IM,WbjJO;EakJP,4CblJO;AHwtDf;;ActpDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhBwjDvC;EgB7jDG;IASK,SAAS;EhBujDjB;AACF;;AclqDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhBokDvC;EgBzkDG;IASK,SAAS;EhBmkDjB;AACF;;Ac9qDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhBglDvC;EgBrlDG;IASK,SAAS;EhB+kDjB;AACF;;Ac1rDI;EEiGA;IAEI,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,iCAAiC;EhB4lDvC;EgBjmDG;IASK,SAAS;EhB2lDjB;AACF;;AgB1mDA;EAOQ,cAAc;EACd,WAAW;EACX,gBAAgB;EAChB,iCAAiC;AhBumDzC;;AgBjnDA;EAcU,SAAS;AhBumDnB;;AmBpxDA;EACE,cAAc;EACd,WAAW;EACX,mCDiH8D;EChH9D,yBhByXkC;ECpQ9B,eAtCY;Ee5EhB,gBhBkR+B;EgBjR/B,gBhBsR+B;EgBrR/B,chBDgB;EgBEhB,sBhBTa;EgBUb,4BAA4B;EAC5B,yBhBPgB;EOOd,sBP6NgC;EiB/N9B,wEjBue4F;AHmzClG;;AoBtxDM;EDdN;ICeQ,gBAAgB;EpB0xDtB;AACF;;AmB1yDA;EAsBI,6BAA6B;EAC7B,SAAS;AnBwxDb;;AmB/yDA;EA4BI,kBAAkB;EAClB,0BhBrBc;AH4yDlB;;AqB7yDE;EACE,clBAc;EkBCd,sBlBRW;EkBSX,qBlBqdsE;EkBpdtE,UAAU;EAKR,gDlBaW;AH+xDjB;;AmB5zDA;EAqCI,chB9Bc;EgBgCd,UAAU;AnB0xDd;;AmBj0DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnB0xDd;;AmBj0DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnB0xDd;;AmBj0DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnB0xDd;;AmBj0DA;EAqCI,chB9Bc;EgBgCd,UAAU;AnB0xDd;;AmBj0DA;EAiDI,yBhB9Cc;EgBgDd,UAAU;AnBmxDd;;AmB/wDA;;;;EAKI,wBAAgB;EAAhB,qBAAgB;EAAhB,gBAAgB;AnBixDpB;;AmB7wDA;EAOI,chB/Dc;EgBgEd,sBhBvEW;AHi1Df;;AmBrwDA;;EAEE,cAAc;EACd,WAAW;AnBwwDb;;AmB9vDA;EACE,iCDyB8D;ECxB9D,oCDwB8D;ECvB9D,gBAAgB;Ef3Bd,kBAAW;Ee6Bb,gBhB+L+B;AHkkDjC;;AmB9vDA;EACE,+BDiB8D;EChB9D,kCDgB8D;EdK1D,kBAtCY;EemBhB,gBhB6H+B;AHooDjC;;AmB9vDA;EACE,gCDU8D;ECT9D,mCDS8D;EdK1D,mBAtCY;Ee0BhB,gBhBuH+B;AH0oDjC;;AmBxvDA;EACE,cAAc;EACd,WAAW;EACX,mBAA2B;EAC3B,gBAAgB;EfDZ,eAtCY;EeyChB,gBhBkK+B;EgBjK/B,chBnHgB;EgBoHhB,6BAA6B;EAC7B,yBAAyB;EACzB,mBAAmC;AnB2vDrC;;AmBrwDA;EAcI,gBAAgB;EAChB,eAAe;AnB2vDnB;;AmB/uDA;EACE,kCD9B8D;EC+B9D,uBhBoPiC;EC9Q7B,mBAtCY;EekEhB,gBhB+E+B;EOxN7B,qBP+N+B;AH6pDnC;;AmB/uDA;EACE,gCDtC8D;ECuC9D,oBhBiPgC;ECnR5B,kBAtCY;Ee0EhB,gBhBsE+B;EOvN7B,qBP8N+B;AHsqDnC;;AmB9uDA;EAGI,YAAY;AnB+uDhB;;AmB3uDA;EACE,YAAY;AnB8uDd;;AmBtuDA;EACE,mBhB0U0C;AH+5C5C;;AmBtuDA;EACE,cAAc;EACd,mBhB2T4C;AH86C9C;;AmBjuDA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,kBAA0C;EAC1C,iBAAyC;AnBouD3C;;AmBxuDA;;EAQI,kBAA0C;EAC1C,iBAAyC;AnBquD7C;;AmB5tDA;EACE,kBAAkB;EAClB,cAAc;EACd,qBhBgS6C;AH+7C/C;;AmB5tDA;EACE,kBAAkB;EAClB,kBhB4R2C;EgB3R3C,qBhB0R6C;AHq8C/C;;AmBluDA;;EAQI,chBzNc;AHw7DlB;;AmB3tDA;EACE,gBAAgB;AnB8tDlB;;AmB3tDA;EACE,2BAAoB;EAApB,oBAAoB;EACpB,sBAAmB;EAAnB,mBAAmB;EACnB,eAAe;EACf,qBhB6Q4C;AHi9C9C;;AmBluDA;EAQI,gBAAgB;EAChB,aAAa;EACb,uBhBwQ4C;EgBvQ5C,cAAc;AnB8tDlB;;AqB36DE;EACE,aAAa;EACb,WAAW;EACX,mBlB0c0C;ECjb1C,cAAW;EiBvBX,clBPa;AHq7DjB;;AqB36DE;EACE,kBAAkB;EAClB,SAAS;EACT,OAAO;EACP,UAAU;EACV,aAAa;EACb,eAAe;EACf,uBlBgyBqC;EkB/xBrC,iBAAiB;EjBmEf,mBAtCY;EiB3Bd,gBlBsO6B;EkBrO7B,WlBxDW;EkByDX,wClBtBa;EOxBb,sBP6NgC;AHgwDpC;;AqB/8DI;;;;EAuCE,cAAc;ArB+6DpB;;AqBt9DI;EA6CE,qBlBnCW;EkBsCT,oCH0CwD;EGzCxD,iRHpB0E;EGqB1E,4BAA4B;EAC5B,2DAA6D;EAC7D,gEHsCwD;AlBq4DhE;;AqB/9DI;EAwDI,qBlB9CS;EkB+CT,gDlB/CS;AH09DjB;;AqBp+DI;EAkEI,oCHwBwD;EGvBxD,kFHuBwD;AlB+4DhE;;AqBz+DI;EA0EE,qBlBhEW;EkBmET,uCHawD;EGZxD,ujBAA8J;ArBi6DtK;;AqB/+DI;EAkFI,qBlBxES;EkByET,gDlBzES;AH0+DjB;;AqBp/DI;EA2FI,clBjFS;AH8+DjB;;AqBx/DI;;;EAgGI,cAAc;ArB85DtB;;AqB9/DI;EAwGI,clB9FS;AHw/DjB;;AqBlgEI;EA2GM,qBlBjGO;AH4/DjB;;AqBtgEI;EAiHM,qBAAkC;EC3IxC,yBD4I+C;ArBy5DnD;;AqB3gEI;EAwHM,gDlB9GO;AHqgEjB;;AqB/gEI;EA4HM,qBlBlHO;AHygEjB;;AqBnhEI;EAsII,qBlB5HS;AH6gEjB;;AqBvhEI;EA2IM,qBlBjIO;EkBkIP,gDlBlIO;AHkhEjB;;AqBhhEE;EACE,aAAa;EACb,WAAW;EACX,mBlB0c0C;ECjb1C,cAAW;EiBvBX,clBVa;AH6hEjB;;AqBhhEE;EACE,kBAAkB;EAClB,SAAS;EACT,OAAO;EACP,UAAU;EACV,aAAa;EACb,eAAe;EACf,uBlBgyBqC;EkB/xBrC,iBAAiB;EjBmEf,mBAtCY;EiB3Bd,gBlBsO6B;EkBrO7B,WlBxDW;EkByDX,wClBzBa;EOrBb,sBP6NgC;AHq2DpC;;AqBpjEI;;;;EAuCE,cAAc;ArBohEpB;;AqB3jEI;EA6CE,qBlBtCW;EkByCT,oCH0CwD;EGzCxD,4UHpB0E;EGqB1E,4BAA4B;EAC5B,2DAA6D;EAC7D,gEHsCwD;AlB0+DhE;;AqBpkEI;EAwDI,qBlBjDS;EkBkDT,gDlBlDS;AHkkEjB;;AqBzkEI;EAkEI,oCHwBwD;EGvBxD,kFHuBwD;AlBo/DhE;;AqB9kEI;EA0EE,qBlBnEW;EkBsET,uCHawD;EGZxD,knBAA8J;ArBsgEtK;;AqBplEI;EAkFI,qBlB3ES;EkB4ET,gDlB5ES;AHklEjB;;AqBzlEI;EA2FI,clBpFS;AHslEjB;;AqB7lEI;;;EAgGI,cAAc;ArBmgEtB;;AqBnmEI;EAwGI,clBjGS;AHgmEjB;;AqBvmEI;EA2GM,qBlBpGO;AHomEjB;;AqB3mEI;EAiHM,qBAAkC;EC3IxC,yBD4I+C;ArB8/DnD;;AqBhnEI;EAwHM,gDlBjHO;AH6mEjB;;AqBpnEI;EA4HM,qBlBrHO;AHinEjB;;AqBxnEI;EAsII,qBlB/HS;AHqnEjB;;AqB5nEI;EA2IM,qBlBpIO;EkBqIP,gDlBrIO;AH0nEjB;;AmB/4DA;EACE,oBAAa;EAAb,aAAa;EACb,uBAAmB;EAAnB,mBAAmB;EACnB,sBAAmB;EAAnB,mBAAmB;AnBk5DrB;;AmBr5DA;EASI,WAAW;AnBg5Df;;Ac/mEI;EKsNJ;IAeM,oBAAa;IAAb,aAAa;IACb,sBAAmB;IAAnB,mBAAmB;IACnB,qBAAuB;IAAvB,uBAAuB;IACvB,gBAAgB;EnB+4DpB;EmBj6DF;IAuBM,oBAAa;IAAb,aAAa;IACb,kBAAc;IAAd,cAAc;IACd,uBAAmB;IAAnB,mBAAmB;IACnB,sBAAmB;IAAnB,mBAAmB;IACnB,gBAAgB;EnB64DpB;EmBx6DF;IAgCM,qBAAqB;IACrB,WAAW;IACX,sBAAsB;EnB24D1B;EmB76DF;IAuCM,qBAAqB;EnBy4DzB;EmBh7DF;;IA4CM,WAAW;EnBw4Df;EmBp7DF;IAkDM,oBAAa;IAAb,aAAa;IACb,sBAAmB;IAAnB,mBAAmB;IACnB,qBAAuB;IAAvB,uBAAuB;IACvB,WAAW;IACX,eAAe;EnBq4DnB;EmB37DF;IAyDM,kBAAkB;IAClB,oBAAc;IAAd,cAAc;IACd,aAAa;IACb,qBhB+KwC;IgB9KxC,cAAc;EnBq4DlB;EmBl8DF;IAiEM,sBAAmB;IAAnB,mBAAmB;IACnB,qBAAuB;IAAvB,uBAAuB;EnBo4D3B;EmBt8DF;IAqEM,gBAAgB;EnBo4DpB;AACF;;AuBttEA;EACE,qBAAqB;EAErB,gBpBsR+B;EoBrR/B,cpBMgB;EoBLhB,kBAAkB;EAGlB,sBAAsB;EACtB,yBAAiB;EAAjB,sBAAiB;EAAjB,qBAAiB;EAAjB,iBAAiB;EACjB,6BAA6B;EAC7B,6BAA2C;ECuF3C,yBrB2RkC;ECpQ9B,eAtCY;EoBiBhB,gBrB0L+B;EOlR7B,sBP6NgC;EiB/N9B,qIjBgb6I;AH4yDnJ;;AoBxtEM;EGdN;IHeQ,gBAAgB;EpB4tEtB;AACF;;AKtuEE;EkBUE,cpBNc;EoBOd,qBAAqB;AvBguEzB;;AuBjvEA;EAsBI,UAAU;EACV,gDpBMa;AHytEjB;;AuBtvEA;EA6BI,apBiZ6B;AH40DjC;;AuB1vEA;EAkCI,eAAsD;AvB4tE1D;;AuB9sEA;;EAEE,oBAAoB;AvBitEtB;;AuBxsEE;EC3DA,WrBCa;EmBDX,yBnB6Ba;EqB3Bf,qBrB2Be;AH4uEjB;;AKnwEE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBgxE7H;;AwBpwEE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,gDAAiF;AxBkwEvF;;AwB7vEE;EAEE,WrB1BW;EqB2BX,yBrBCa;EqBAb,qBrBAa;AH+vEjB;;AwBxvEE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBoyEnN;;AwBrvEI;;EAKI,gDAAiF;AxBqvEzF;;AuB7uEE;EC3DA,WrBCa;EmBDX,yBnBOc;EqBLhB,qBrBKgB;AHuyElB;;AKxyEE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBqzE7H;;AwBzyEE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,iDAAiF;AxBuyEvF;;AwBlyEE;EAEE,WrB1BW;EqB2BX,yBrBrBc;EqBsBd,qBrBtBc;AH0zElB;;AwB7xEE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBy0EnN;;AwB1xEI;;EAKI,iDAAiF;AxB0xEzF;;AuBlxEE;EC3DA,WrBCa;EmBDX,yBnBoCa;EqBlCf,qBrBkCe;AH+yEjB;;AK70EE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxB01E7H;;AwB90EE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,+CAAiF;AxB40EvF;;AwBv0EE;EAEE,WrB1BW;EqB2BX,yBrBQa;EqBPb,qBrBOa;AHk0EjB;;AwBl0EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxB82EnN;;AwB/zEI;;EAKI,+CAAiF;AxB+zEzF;;AuBvzEE;EC3DA,WrBCa;EmBDX,yBnBsCa;EqBpCf,qBrBoCe;AHk1EjB;;AKl3EE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxB+3E7H;;AwBn3EE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,gDAAiF;AxBi3EvF;;AwB52EE;EAEE,WrB1BW;EqB2BX,yBrBUa;EqBTb,qBrBSa;AHq2EjB;;AwBv2EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBm5EnN;;AwBp2EI;;EAKI,gDAAiF;AxBo2EzF;;AuB51EE;EC3DA,crBUgB;EmBVd,yBnBmCa;EqBjCf,qBrBiCe;AH03EjB;;AKv5EE;EmBAE,crBIc;EmBVd,yBEDoF;EASpF,qBATyH;AxBo6E7H;;AwBx5EE;EAEE,crBHc;EmBVd,yBEDoF;EAgBpF,qBAhByH;EAqBvH,gDAAiF;AxBs5EvF;;AwBj5EE;EAEE,crBjBc;EqBkBd,yBrBOa;EqBNb,qBrBMa;AH64EjB;;AwB54EE;;EAGE,crB7Bc;EqB8Bd,yBAzCuK;EA6CvK,qBA7C+M;AxBw7EnN;;AwBz4EI;;EAKI,gDAAiF;AxBy4EzF;;AuBj4EE;EC3DA,WrBCa;EmBDX,yBnBiCa;EqB/Bf,qBrB+Be;AHi6EjB;;AK57EE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBy8E7H;;AwB77EE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,+CAAiF;AxB27EvF;;AwBt7EE;EAEE,WrB1BW;EqB2BX,yBrBKa;EqBJb,qBrBIa;AHo7EjB;;AwBj7EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxB69EnN;;AwB96EI;;EAKI,+CAAiF;AxB86EzF;;AuBt6EE;EC3DA,crBUgB;EmBVd,yBnBEc;EqBAhB,qBrBAgB;AHq+ElB;;AKj+EE;EmBAE,crBIc;EmBVd,yBEDoF;EASpF,qBATyH;AxB8+E7H;;AwBl+EE;EAEE,crBHc;EmBVd,yBEDoF;EAgBpF,qBAhByH;EAqBvH,iDAAiF;AxBg+EvF;;AwB39EE;EAEE,crBjBc;EqBkBd,yBrB1Bc;EqB2Bd,qBrB3Bc;AHw/ElB;;AwBt9EE;;EAGE,crB7Bc;EqB8Bd,yBAzCuK;EA6CvK,qBA7C+M;AxBkgFnN;;AwBn9EI;;EAKI,iDAAiF;AxBm9EzF;;AuB38EE;EC3DA,WrBCa;EmBDX,yBnBSc;EqBPhB,qBrBOgB;AHmgFlB;;AKtgFE;EmBAE,WrBLW;EmBDX,yBEDoF;EASpF,qBATyH;AxBmhF7H;;AwBvgFE;EAEE,WrBZW;EmBDX,yBEDoF;EAgBpF,qBAhByH;EAqBvH,8CAAiF;AxBqgFvF;;AwBhgFE;EAEE,WrB1BW;EqB2BX,yBrBnBc;EqBoBd,qBrBpBc;AHshFlB;;AwB3/EE;;EAGE,WrBtCW;EqBuCX,yBAzCuK;EA6CvK,qBA7C+M;AxBuiFnN;;AwBx/EI;;EAKI,8CAAiF;AxBw/EzF;;AuB1+EE;ECPA,crB7Be;EqB8Bf,qBrB9Be;AHmhFjB;;AK1iFE;EmBwDE,WrB7DW;EqB8DX,yBrBlCa;EqBmCb,qBrBnCa;AHyhFjB;;AwBn/EE;EAEE,+CrBxCa;AH6hFjB;;AwBl/EE;EAEE,crB7Ca;EqB8Cb,6BAA6B;AxBo/EjC;;AwBj/EE;;EAGE,WrBhFW;EqBiFX,yBrBrDa;EqBsDb,qBrBtDa;AHyiFjB;;AwBj/EI;;EAKI,+CrB7DS;AH8iFjB;;AuB1gFE;ECPA,crBnDgB;EqBoDhB,qBrBpDgB;AHykFlB;;AK1kFE;EmBwDE,WrB7DW;EqB8DX,yBrBxDc;EqByDd,qBrBzDc;AH+kFlB;;AwBnhFE;EAEE,iDrB9Dc;AHmlFlB;;AwBlhFE;EAEE,crBnEc;EqBoEd,6BAA6B;AxBohFjC;;AwBjhFE;;EAGE,WrBhFW;EqBiFX,yBrB3Ec;EqB4Ed,qBrB5Ec;AH+lFlB;;AwBjhFI;;EAKI,iDrBnFU;AHomFlB;;AuB1iFE;ECPA,crBtBe;EqBuBf,qBrBvBe;AH4kFjB;;AK1mFE;EmBwDE,WrB7DW;EqB8DX,yBrB3Ba;EqB4Bb,qBrB5Ba;AHklFjB;;AwBnjFE;EAEE,+CrBjCa;AHslFjB;;AwBljFE;EAEE,crBtCa;EqBuCb,6BAA6B;AxBojFjC;;AwBjjFE;;EAGE,WrBhFW;EqBiFX,yBrB9Ca;EqB+Cb,qBrB/Ca;AHkmFjB;;AwBjjFI;;EAKI,+CrBtDS;AHumFjB;;AuB1kFE;ECPA,crBpBe;EqBqBf,qBrBrBe;AH0mFjB;;AK1oFE;EmBwDE,WrB7DW;EqB8DX,yBrBzBa;EqB0Bb,qBrB1Ba;AHgnFjB;;AwBnlFE;EAEE,gDrB/Ba;AHonFjB;;AwBllFE;EAEE,crBpCa;EqBqCb,6BAA6B;AxBolFjC;;AwBjlFE;;EAGE,WrBhFW;EqBiFX,yBrB5Ca;EqB6Cb,qBrB7Ca;AHgoFjB;;AwBjlFI;;EAKI,gDrBpDS;AHqoFjB;;AuB1mFE;ECPA,crBvBe;EqBwBf,qBrBxBe;AH6oFjB;;AK1qFE;EmBwDE,crBpDc;EqBqDd,yBrB5Ba;EqB6Bb,qBrB7Ba;AHmpFjB;;AwBnnFE;EAEE,+CrBlCa;AHupFjB;;AwBlnFE;EAEE,crBvCa;EqBwCb,6BAA6B;AxBonFjC;;AwBjnFE;;EAGE,crBvEc;EqBwEd,yBrB/Ca;EqBgDb,qBrBhDa;AHmqFjB;;AwBjnFI;;EAKI,+CrBvDS;AHwqFjB;;AuB1oFE;ECPA,crBzBe;EqB0Bf,qBrB1Be;AH+qFjB;;AK1sFE;EmBwDE,WrB7DW;EqB8DX,yBrB9Ba;EqB+Bb,qBrB/Ba;AHqrFjB;;AwBnpFE;EAEE,+CrBpCa;AHyrFjB;;AwBlpFE;EAEE,crBzCa;EqB0Cb,6BAA6B;AxBopFjC;;AwBjpFE;;EAGE,WrBhFW;EqBiFX,yBrBjDa;EqBkDb,qBrBlDa;AHqsFjB;;AwBjpFI;;EAKI,+CrBzDS;AH0sFjB;;AuB1qFE;ECPA,crBxDgB;EqByDhB,qBrBzDgB;AH8uFlB;;AK1uFE;EmBwDE,crBpDc;EqBqDd,yBrB7Dc;EqB8Dd,qBrB9Dc;AHovFlB;;AwBnrFE;EAEE,iDrBnEc;AHwvFlB;;AwBlrFE;EAEE,crBxEc;EqByEd,6BAA6B;AxBorFjC;;AwBjrFE;;EAGE,crBvEc;EqBwEd,yBrBhFc;EqBiFd,qBrBjFc;AHowFlB;;AwBjrFI;;EAKI,iDrBxFU;AHywFlB;;AuB1sFE;ECPA,crBjDgB;EqBkDhB,qBrBlDgB;AHuwFlB;;AK1wFE;EmBwDE,WrB7DW;EqB8DX,yBrBtDc;EqBuDd,qBrBvDc;AH6wFlB;;AwBntFE;EAEE,8CrB5Dc;AHixFlB;;AwBltFE;EAEE,crBjEc;EqBkEd,6BAA6B;AxBotFjC;;AwBjtFE;;EAGE,WrBhFW;EqBiFX,yBrBzEc;EqB0Ed,qBrB1Ec;AH6xFlB;;AwBjtFI;;EAKI,8CrBjFU;AHkyFlB;;AuB/tFA;EACE,gBpB4M+B;EoB3M/B,cpBjDe;EoBkDf,qBpB2F4C;AHuoF9C;;AK3yFE;EkB4EE,cpByF8D;EoBxF9D,0BpByF+C;AH0oFnD;;AuB1uFA;EAYI,0BpBoF+C;AH8oFnD;;AuB9uFA;EAiBI,cpBtFc;EoBuFd,oBAAoB;AvBiuFxB;;AuBttFA;ECPE,oBrB0SgC;ECnR5B,kBAtCY;EoBiBhB,gBrB+H+B;EOvN7B,qBP8N+B;AH4lFnC;;AuBztFA;ECXE,uBrBqSiC;EC9Q7B,mBAtCY;EoBiBhB,gBrBgI+B;EOxN7B,qBP+N+B;AHkmFnC;;AuBvtFA;EACE,cAAc;EACd,WAAW;AvB0tFb;;AuB5tFA;EAMI,kBpBuT+B;AHm6EnC;;AuBrtFA;;;EAII,WAAW;AvButFf;;AyBl2FA;ELgBM,gCjBiP2C;AHqmFjD;;AoBl1FM;EKpBN;ILqBQ,gBAAgB;EpBs1FtB;AACF;;AyB52FA;EAII,UAAU;AzB42Fd;;AyBx2FA;EAEI,aAAa;AzB02FjB;;AyBt2FA;EACE,kBAAkB;EAClB,SAAS;EACT,gBAAgB;ELDZ,6BjBkPwC;AHynF9C;;AoBv2FM;EKNN;ILOQ,gBAAgB;EpB22FtB;AACF;;A0Bh4FA;;;;EAIE,kBAAkB;A1Bm4FpB;;A0Bh4FA;EACE,mBAAmB;A1Bm4FrB;;A2B/2FI;EACE,qBAAqB;EACrB,oBxB+N0C;EwB9N1C,uBxB6N0C;EwB5N1C,WAAW;EAhCf,uBAA8B;EAC9B,qCAA4C;EAC5C,gBAAgB;EAChB,oCAA2C;A3Bm5F7C;;A2B91FI;EACE,cAAc;A3Bi2FpB;;A0B34FA;EACE,kBAAkB;EAClB,SAAS;EACT,OAAO;EACP,avBwpBsC;EuBvpBtC,aAAa;EACb,WAAW;EACX,gBvB8tBuC;EuB7tBvC,iBvB8tBmC;EuB7tBnC,oBAA4B;EtBsGxB,eAtCY;EsB9DhB,cvBXgB;EuBYhB,gBAAgB;EAChB,gBAAgB;EAChB,sBvBvBa;EuBwBb,4BAA4B;EAC5B,qCvBfa;EOCX,sBP6NgC;AHgsFpC;;A0Bt4FI;EACE,WAAW;EACX,OAAO;A1By4Fb;;A0Bt4FI;EACE,QAAQ;EACR,UAAU;A1By4FhB;;Ac73FI;EYnBA;IACE,WAAW;IACX,OAAO;E1Bo5FX;E0Bj5FE;IACE,QAAQ;IACR,UAAU;E1Bm5Fd;AACF;;Acx4FI;EYnBA;IACE,WAAW;IACX,OAAO;E1B+5FX;E0B55FE;IACE,QAAQ;IACR,UAAU;E1B85Fd;AACF;;Acn5FI;EYnBA;IACE,WAAW;IACX,OAAO;E1B06FX;E0Bv6FE;IACE,QAAQ;IACR,UAAU;E1By6Fd;AACF;;Ac95FI;EYnBA;IACE,WAAW;IACX,OAAO;E1Bq7FX;E0Bl7FE;IACE,QAAQ;IACR,UAAU;E1Bo7Fd;AACF;;A0B96FA;EAEI,SAAS;EACT,YAAY;EACZ,aAAa;EACb,uBvB4rBuC;AHovE3C;;A2B/8FI;EACE,qBAAqB;EACrB,oBxB+N0C;EwB9N1C,uBxB6N0C;EwB5N1C,WAAW;EAzBf,aAAa;EACb,qCAA4C;EAC5C,0BAAiC;EACjC,oCAA2C;A3B4+F7C;;A2B97FI;EACE,cAAc;A3Bi8FpB;;A0Bv7FA;EAEI,MAAM;EACN,WAAW;EACX,UAAU;EACV,aAAa;EACb,qBvB8qBuC;AH2wE3C;;A2Bt+FI;EACE,qBAAqB;EACrB,oBxB+N0C;EwB9N1C,uBxB6N0C;EwB5N1C,WAAW;EAlBf,mCAA0C;EAC1C,eAAe;EACf,sCAA6C;EAC7C,wBAA+B;A3B4/FjC;;A2Br9FI;EACE,cAAc;A3Bw9FpB;;A2Br/FI;EDmDE,iBAAiB;A1Bs8FvB;;A0Bj8FA;EAEI,MAAM;EACN,WAAW;EACX,UAAU;EACV,aAAa;EACb,sBvB6pBuC;AHsyE3C;;A2BjgGI;EACE,qBAAqB;EACrB,oBxB+N0C;EwB9N1C,uBxB6N0C;EwB5N1C,WAAW;A3BogGjB;;A2BxgGI;EAgBI,aAAa;A3B4/FrB;;A2Bz/FM;EACE,qBAAqB;EACrB,qBxB4MwC;EwB3MxC,uBxB0MwC;EwBzMxC,WAAW;EA9BjB,mCAA0C;EAC1C,yBAAgC;EAChC,sCAA6C;A3B2hG/C;;A2B1/FI;EACE,cAAc;A3B6/FpB;;A2BvgGM;EDiDA,iBAAiB;A1B09FvB;;A0Bn9FA;EAKI,WAAW;EACX,YAAY;A1Bk9FhB;;A0B78FA;EE9GE,SAAS;EACT,gBAAmB;EACnB,gBAAgB;EAChB,6BzBCgB;AH8jGlB;;A0B78FA;EACE,cAAc;EACd,WAAW;EACX,uBvBipBwC;EuBhpBxC,WAAW;EACX,gBvBgK+B;EuB/J/B,cvBhHgB;EuBiHhB,mBAAmB;EAEnB,mBAAmB;EACnB,6BAA6B;EAC7B,SAAS;A1B+8FX;;AKpkGE;EqBoIE,cvBinBqD;EuBhnBrD,qBAAqB;EJ/IrB,yBnBEc;AHklGlB;;A0Bh+FA;EAiCI,WvBpJW;EuBqJX,qBAAqB;EJtJrB,yBnB6Ba;AH6jGjB;;A0Bt+FA;EAwCI,cvBrJc;EuBsJd,oBAAoB;EACpB,6BAA6B;A1Bk8FjC;;A0B17FA;EACE,cAAc;A1B67FhB;;A0Bz7FA;EACE,cAAc;EACd,sBvB2lBwC;EuB1lBxC,gBAAgB;EtBrDZ,mBAtCY;EsB6FhB,cvBzKgB;EuB0KhB,mBAAmB;A1B47FrB;;A0Bx7FA;EACE,cAAc;EACd,uBvBilBwC;EuBhlBxC,cvB9KgB;AHymGlB;;A6BtnGA;;EAEE,kBAAkB;EAClB,2BAAoB;EAApB,oBAAoB;EACpB,sBAAsB;A7BynGxB;;A6B7nGA;;EAOI,kBAAkB;EAClB,kBAAc;EAAd,cAAc;A7B2nGlB;;AK1nGE;;EwBII,UAAU;A7B2nGhB;;A6BxoGA;;;;EAkBM,UAAU;A7B6nGhB;;A6BvnGA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,oBAA2B;EAA3B,2BAA2B;A7B0nG7B;;A6B7nGA;EAMI,WAAW;A7B2nGf;;A6BvnGA;;EAII,iB1BmM6B;AHq7FjC;;A6B5nGA;;EnBHI,0BmBa8B;EnBZ9B,6BmBY8B;A7BwnGlC;;A6BloGA;;EnBWI,yBmBI6B;EnBH7B,4BmBG6B;A7BynGjC;;A6BzmGA;EACE,wBAAmC;EACnC,uBAAkC;A7B4mGpC;;A6B9mGA;;;EAOI,cAAc;A7B6mGlB;;A6B1mGE;EACE,eAAe;A7B6mGnB;;A6BzmGA;EACE,uBAAsC;EACtC,sBAAqC;A7B4mGvC;;A6BzmGA;EACE,sBAAsC;EACtC,qBAAqC;A7B4mGvC;;A6BxlGA;EACE,0BAAsB;EAAtB,sBAAsB;EACtB,qBAAuB;EAAvB,uBAAuB;EACvB,qBAAuB;EAAvB,uBAAuB;A7B2lGzB;;A6B9lGA;;EAOI,WAAW;A7B4lGf;;A6BnmGA;;EAYI,gB1BkH6B;AH0+FjC;;A6BxmGA;;EnBrEI,6BmBuF+B;EnBtF/B,4BmBsF+B;A7B4lGnC;;A6B9mGA;;EnBnFI,yBmB0G4B;EnBzG5B,0BmByG4B;A7B6lGhC;;A6B5kGA;;EAGI,gBAAgB;A7B8kGpB;;A6BjlGA;;;;EAOM,kBAAkB;EAClB,sBAAsB;EACtB,oBAAoB;A7BilG1B;;A8B1uGA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,uBAAoB;EAApB,oBAAoB;EACpB,WAAW;A9B6uGb;;A8BlvGA;;;;EAWI,kBAAkB;EAClB,kBAAc;EAAd,cAAc;EACd,SAAS;EACT,YAAY;EACZ,gBAAgB;A9B8uGpB;;A8B7vGA;;;;;;;;;;;;EAoBM,iB3BkN2B;AHsiGjC;;A8B5wGA;;;EA4BI,UAAU;A9BsvGd;;A8BlxGA;EAiCI,UAAU;A9BqvGd;;A8BtxGA;;EpB4BI,0BoBUmD;EpBTnD,6BoBSmD;A9BsvGvD;;A8B5xGA;;EpB0CI,yBoBHmD;EpBInD,4BoBJmD;A9B2vGvD;;A8BlyGA;EA6CI,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;A9ByvGvB;;A8BvyGA;;EpB4BI,0BoBqB6E;EpBpB7E,6BoBoB6E;A9B4vGjF;;A8B7yGA;EpB0CI,yBoBQsE;EpBPtE,4BoBOsE;A9BgwG1E;;A8BrvGA;;EAEE,oBAAa;EAAb,aAAa;A9BwvGf;;A8B1vGA;;EAQI,kBAAkB;EAClB,UAAU;A9BuvGd;;A8BhwGA;;EAYM,UAAU;A9ByvGhB;;A8BrwGA;;;;;;;;EAoBI,iB3BqJ6B;AHumGjC;;A8BxvGA;EAAuB,kB3BiJU;AH2mGjC;;A8B3vGA;EAAsB,iB3BgJW;AH+mGjC;;A8BvvGA;EACE,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,yB3B4RkC;E2B3RlC,gBAAgB;E1BuBZ,eAtCY;E0BiBhB,gB3BqL+B;E2BpL/B,gB3ByL+B;E2BxL/B,c3B9FgB;E2B+FhB,kBAAkB;EAClB,mBAAmB;EACnB,yB3BtGgB;E2BuGhB,yB3BrGgB;EOOd,sBP6NgC;AH4nGpC;;A8BvwGA;;EAkBI,aAAa;A9B0vGjB;;A8BhvGA;;EAEE,gCZR8D;AlB2vGhE;;A8BhvGA;;;;;;EAME,oB3BuQgC;ECnR5B,kBAtCY;E0BoDhB,gB3B4F+B;EOvN7B,qBP8N+B;AHipGnC;;A8BhvGA;;EAEE,kCZzB8D;AlB4wGhE;;A8BhvGA;;;;;;EAME,uB3BiPiC;EC9Q7B,mBAtCY;E0BqEhB,gB3B4E+B;EOxN7B,qBP+N+B;AHiqGnC;;A8BhvGA;;EAEE,sBAA0E;A9BmvG5E;;A8BxuGA;;;;;;EpB7II,0BoBmJ4B;EpBlJ5B,6BoBkJ4B;A9B4uGhC;;A8BzuGA;;;;;;EpBxII,yBoB8I2B;EpB7I3B,4BoB6I2B;A9B6uG/B;;A+Bl6GA;EACE,kBAAkB;EAClB,UAAU;EACV,cAAc;EACd,kBAA+C;EAC/C,oBAAqE;EACrE,iCAAmB;EAAnB,mBAAmB;A/Bq6GrB;;A+Bl6GA;EACE,2BAAoB;EAApB,oBAAoB;EACpB,kB5Bwf0C;AH66F5C;;A+Bl6GA;EACE,kBAAkB;EAClB,OAAO;EACP,WAAW;EACX,W5Bof0C;E4Bnf1C,eAAkF;EAClF,UAAU;A/Bq6GZ;;A+B36GA;EASI,W5BzBW;E4B0BX,qB5BEa;EmB7Bb,yBnB6Ba;AHq6GjB;;A+Bj7GA;EAoBM,gD5BRW;AHy6GjB;;A+Br7GA;EAyBI,qB5BqbsE;AH2+F1E;;A+Bz7GA;EA6BI,W5B7CW;E4B8CX,yB5Bif8E;E4Bhf9E,qB5Bgf8E;AHg7FlF;;A+B/7GA;EAuCM,c5BjDY;AH68GlB;;A+Bn8GA;EA0CQ,yB5BxDU;AHq9GlB;;A+Bn5GA;EACE,kBAAkB;EAClB,gBAAgB;EAEhB,mBAAmB;A/Bq5GrB;;A+Bz5GA;EASI,kBAAkB;EAClB,YAA+E;EAC/E,aAA+D;EAC/D,cAAc;EACd,W5BubwC;E4BtbxC,Y5BsbwC;E4BrbxC,oBAAoB;EACpB,WAAW;EACX,sB5BrFW;E4BsFX,yB5B+I6B;AHqwGjC;;A+Bt6GA;EAwBI,kBAAkB;EAClB,YAA+E;EAC/E,aAA+D;EAC/D,cAAc;EACd,W5BwawC;E4BvaxC,Y5BuawC;E4BtaxC,WAAW;EACX,mCAAgE;A/Bk5GpE;;A+Bz4GA;ErBjGI,sBP6NgC;AHixGpC;;A+B74GA;EAOM,kOb7D4E;AlBu8GlF;;A+Bj5GA;EAaM,qB5B7FW;EmB7Bb,yBnB6Ba;AHs+GjB;;A+Bt5GA;EAkBM,+KbxE4E;AlBg9GlF;;A+B15GA;ET7GI,wCnB6Ba;AH8+GjB;;A+B95GA;ET7GI,wCnB6Ba;AHk/GjB;;A+B93GA;EAGI,kB5ByZ+C;AHs+FnD;;A+Bl4GA;EAQM,8KblG4E;AlBg+GlF;;A+Bt4GA;ETjJI,wCnB6Ba;AH8/GjB;;A+Bl3GA;EACE,qBAA2D;A/Bq3G7D;;A+Bt3GA;EAKM,cAAqD;EACrD,c5BiY+E;E4BhY/E,mBAAmB;EAEnB,qB5B+X4E;AHq/FlF;;A+B73GA;EAaM,wBblE0D;EamE1D,0BbnE0D;EaoE1D,uBbhD0D;EaiD1D,wBbjD0D;EakD1D,yB5BpLY;E4BsLZ,qB5BqX4E;EiBviB5E,iJjByf+H;EiBzf/H,yIjByf+H;EiBzf/H,8KjByf+H;AH6iGrI;;AoBliHM;EW2JN;IX1JQ,gBAAgB;EpBsiHtB;AACF;;A+B74GA;EA0BM,sB5BlMS;E4BmMT,sCAA4E;EAA5E,8BAA4E;A/Bu3GlF;;A+Bl5GA;ETzKI,wCnB6Ba;AHkiHjB;;A+Bz2GA;EACE,qBAAqB;EACrB,WAAW;EACX,mCbrG8D;EasG9D,0C5BmKkC;ECpQ9B,eAtCY;E2B0IhB,gB5B4D+B;E4B3D/B,gB5BgE+B;E4B/D/B,c5BvNgB;E4BwNhB,sBAAsB;EACtB,uO5BkW+I;E4BjW/I,yB5B7NgB;EOOd,sBP6NgC;E4BJlC,wBAAgB;EAAhB,qBAAgB;EAAhB,gBAAgB;A/B02GlB;;A+Bz3GA;EAkBI,qB5BuPsE;E4BtPtE,UAAU;EAKR,gD5BjNW;AHwjHjB;;A+B/3GA;EAiCM,c5B/OY;E4BgPZ,sB5BvPS;AHylHf;;A+Bp4GA;EAwCI,YAAY;EACZ,sB5B8HgC;E4B7HhC,sBAAsB;A/Bg2G1B;;A+B14GA;EA8CI,c5B7Pc;E4B8Pd,yB5BlQc;AHkmHlB;;A+B/4GA;EAoDI,aAAa;A/B+1GjB;;A+Bn5GA;EAyDI,kBAAkB;EAClB,0B5BxQc;AHsmHlB;;A+B11GA;EACE,kCbjK8D;EakK9D,oB5BgHkC;E4B/GlC,uB5B+GkC;E4B9GlC,oB5B+GiC;EC9Q7B,mBAtCY;AJmiHlB;;A+B11GA;EACE,gCbzK8D;Ea0K9D,mB5B6GiC;E4B5GjC,sB5B4GiC;E4B3GjC,kB5B4GgC;ECnR5B,kBAtCY;AJ2iHlB;;A+Br1GA;EACE,kBAAkB;EAClB,qBAAqB;EACrB,WAAW;EACX,mCbzL8D;Ea0L9D,gBAAgB;A/Bw1GlB;;A+Br1GA;EACE,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,mCbjM8D;EakM9D,SAAS;EACT,UAAU;A/Bw1GZ;;A+B91GA;EASI,qB5BqKsE;E4BpKtE,gD5B9Ra;AHunHjB;;A+Bn2GA;;EAgBI,yB5B9Tc;AHspHlB;;A+Bx2GA;EAqBM,iB5B4TQ;AH2hGd;;A+B52GA;EA0BI,0BAA0B;A/Bs1G9B;;A+Bl1GA;EACE,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,OAAO;EACP,UAAU;EACV,mCbjO8D;EakO9D,yB5BuCkC;E4BrClC,gB5B/D+B;E4BgE/B,gB5B3D+B;E4B4D/B,c5BlVgB;E4BmVhB,sB5B1Va;E4B2Vb,yB5BvVgB;EOOd,sBP6NgC;AHw8GpC;;A+Bl2GA;EAkBI,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,SAAS;EACT,UAAU;EACV,cAAc;EACd,6BbnP4D;EaoP5D,yB5BqBgC;E4BpBhC,gB5B3E6B;E4B4E7B,c5BlWc;E4BmWd,iBAAiB;ET3WjB,yBnBGc;E4B0Wd,oBAAoB;ErBjWpB,kCqBkWgF;A/Bo1GpF;;A+B10GA;EACE,WAAW;EACX,cbzQ2B;Ea0Q3B,UAAU;EACV,6BAA6B;EAC7B,wBAAgB;EAAhB,qBAAgB;EAAhB,gBAAgB;A/B60GlB;;A+Bl1GA;EAQI,aAAa;A/B80GjB;;A+Bt1GA;EAY8B,gE5BvWb;AHqrHjB;;A+B11GA;EAa8B,gE5BxWb;AHyrHjB;;A+B91GA;EAc8B,gE5BzWb;AH6rHjB;;A+Bl2GA;EAkBI,SAAS;A/Bo1Gb;;A+Bt2GA;EAsBI,W5BqN6C;E4BpN7C,Y5BoN6C;E4BnN7C,oBAAyE;EThZzE,yBnB6Ba;E4BqXb,S5BoN0C;EO1lB1C,mBP2lB6C;EiB7lB3C,oHjByf+H;EiBzf/H,4GjByf+H;E4B7GjI,wBAAgB;EAAhB,gBAAgB;A/Bm1GpB;;AoB3tHM;EW0WN;IXzWQ,wBAAgB;IAAhB,gBAAgB;EpB+tHtB;AACF;;A+Bv3GA;ETxXI,yBnB2mB2E;AHwoG/E;;A+B33GA;EAsCI,W5B8LoC;E4B7LpC,c5B8LqC;E4B7LrC,kBAAkB;EAClB,e5B6LuC;E4B5LvC,yB5B9Zc;E4B+Zd,yBAAyB;ErBvZzB,mBPolBoC;AH6pGxC;;A+Br4GA;EAiDI,W5B0L6C;E4BzL7C,Y5ByL6C;EmBnmB7C,yBnB6Ba;E4B+Yb,S5B0L0C;EO1lB1C,mBP2lB6C;EiB7lB3C,iHjByf+H;EiBzf/H,4GjByf+H;E4BnFjI,qBAAgB;EAAhB,gBAAgB;A/Bu1GpB;;AoBzvHM;EW0WN;IXzWQ,qBAAgB;IAAhB,gBAAgB;EpB6vHtB;AACF;;A+Br5GA;ETxXI,yBnB2mB2E;AHsqG/E;;A+Bz5GA;EAgEI,W5BoKoC;E4BnKpC,c5BoKqC;E4BnKrC,kBAAkB;EAClB,e5BmKuC;E4BlKvC,yB5Bxbc;E4Bybd,yBAAyB;ErBjbzB,mBPolBoC;AH2rGxC;;A+Bn6GA;EA2EI,W5BgK6C;E4B/J7C,Y5B+J6C;E4B9J7C,aAAa;EACb,oB5BpE+B;E4BqE/B,mB5BrE+B;EmBlY/B,yBnB6Ba;E4B4ab,S5B6J0C;EO1lB1C,mBP2lB6C;EiB7lB3C,gHjByf+H;EiBzf/H,4GjByf+H;E4BtDjI,gBAAgB;A/B21GpB;;AoB1xHM;EW0WN;IXzWQ,oBAAgB;IAAhB,gBAAgB;EpB8xHtB;AACF;;A+Bt7GA;ETxXI,yBnB2mB2E;AHusG/E;;A+B17GA;EA6FI,W5BuIoC;E4BtIpC,c5BuIqC;E4BtIrC,kBAAkB;EAClB,e5BsIuC;E4BrIvC,6BAA6B;EAC7B,yBAAyB;EACzB,oBAA4C;A/Bi2GhD;;A+Bp8GA;EAwGI,yB5B5dc;EOQd,mBPolBoC;AHiuGxC;;A+Bz8GA;EA6GI,kBAAkB;EAClB,yB5Blec;EOQd,mBPolBoC;AHuuGxC;;A+B/8GA;EAoHM,yB5BteY;AHq0HlB;;A+Bn9GA;EAwHM,eAAe;A/B+1GrB;;A+Bv9GA;EA4HM,yB5B9eY;AH60HlB;;A+B39GA;EAgIM,eAAe;A/B+1GrB;;A+B/9GA;EAoIM,yB5BtfY;AHq1HlB;;A+B11GA;;;EXvfM,4GjByf+H;AH81GrI;;AoBn1HM;EWmfN;;;IXlfQ,gBAAgB;EpBy1HtB;AACF;;AgC12HA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,eAAe;EACf,gBAAgB;EAChB,gBAAgB;AhC62HlB;;AgC12HA;EACE,cAAc;EACd,oB7ByqBsC;AHosGxC;;AK52HE;E2BGE,qBAAqB;AhC62HzB;;AgCn3HA;EAWI,c7BXc;E6BYd,oBAAoB;EACpB,eAAe;AhC42HnB;;AgCp2HA;EACE,gC7BzBgB;AHg4HlB;;AgCx2HA;EAII,mB7BsM6B;AHkqHjC;;AgC52HA;EAQI,6BAAgD;EtBfhD,+BPoNgC;EOnNhC,gCPmNgC;AHqqHpC;;AKp4HE;E2B8BI,qC7BpCY;AH84HlB;;AgCt3HA;EAgBM,c7BrCY;E6BsCZ,6BAA6B;EAC7B,yBAAyB;AhC02H/B;;AgC53HA;;EAwBI,c7B5Cc;E6B6Cd,sB7BpDW;E6BqDX,kC7BrDW;AH85Hf;;AgCn4HA;EA+BI,gB7B2K6B;EOjN7B,yBsBwC4B;EtBvC5B,0BsBuC4B;AhCw2HhC;;AgC/1HA;EtB1DI,sBP6NgC;AHgsHpC;;AgCn2HA;;EAOI,W7B5EW;E6B6EX,yB7BjDa;AHk5HjB;;AgCx1HA;;EAGI,kBAAc;EAAd,cAAc;EACd,kBAAkB;AhC01HtB;;AgCt1HA;;EAGI,0BAAa;EAAb,aAAa;EACb,oBAAY;EAAZ,YAAY;EACZ,kBAAkB;AhCw1HtB;;AgC/0HA;EAEI,aAAa;AhCi1HjB;;AgCn1HA;EAKI,cAAc;AhCk1HlB;;AiCz7HA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,sBAAmB;EAAnB,mBAAmB;EACnB,sBAA8B;EAA9B,8BAA8B;EAC9B,oB9BgHW;AH40Hb;;AiCl8HA;;EAWI,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,sBAAmB;EAAnB,mBAAmB;EACnB,sBAA8B;EAA9B,8BAA8B;AjC47HlC;;AiCx6HA;EACE,qBAAqB;EACrB,sB9BiqB+E;E8BhqB/E,yB9BgqB+E;E8B/pB/E,kB9BgFW;ECRP,kBAtCY;E6BhChB,oBAAoB;EACpB,mBAAmB;AjC26HrB;;AKr9HE;E4B6CE,qBAAqB;AjC46HzB;;AiCn6HA;EACE,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,eAAe;EACf,gBAAgB;EAChB,gBAAgB;AjCs6HlB;;AiC36HA;EAQI,gBAAgB;EAChB,eAAe;AjCu6HnB;;AiCh7HA;EAaI,gBAAgB;EAChB,WAAW;AjCu6Hf;;AiC95HA;EACE,qBAAqB;EACrB,mB9BwlBuC;E8BvlBvC,sB9BulBuC;AH00GzC;;AiCr5HA;EACE,6BAAgB;EAAhB,gBAAgB;EAChB,oBAAY;EAAZ,YAAY;EAGZ,sBAAmB;EAAnB,mBAAmB;AjCs5HrB;;AiCl5HA;EACE,wB9BmmBwC;EC1lBpC,kBAtCY;E6B+BhB,cAAc;EACd,6BAA6B;EAC7B,6BAAuC;EvBxGrC,sBP6NgC;AHiyHpC;;AKhgIE;E4B8GE,qBAAqB;AjCs5HzB;;AiCh5HA;EACE,qBAAqB;EACrB,YAAY;EACZ,aAAa;EACb,sBAAsB;EACtB,WAAW;EACX,mCAAmC;EACnC,0BAA0B;AjCm5H5B;;Acr9HI;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjC44HvB;AACF;;Ac1+HI;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjCk4HjC;EiCv5HG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjCk4H3B;EiC15HG;IA2BO,kBAAkB;EjCk4H5B;EiC75HG;IA+BO,qB9B4hB6B;I8B3hB7B,oB9B2hB6B;EHs2GvC;EiCj6HG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjC+3HzB;EiCr6HG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjCi3HxB;EiCz6HG;IA4DK,aAAa;EjCg3HrB;AACF;;Acz/HI;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjCg7HvB;AACF;;Ac9gII;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjCs6HjC;EiC37HG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjCs6H3B;EiC97HG;IA2BO,kBAAkB;EjCs6H5B;EiCj8HG;IA+BO,qB9B4hB6B;I8B3hB7B,oB9B2hB6B;EH04GvC;EiCr8HG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjCm6HzB;EiCz8HG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjCq5HxB;EiC78HG;IA4DK,aAAa;EjCo5HrB;AACF;;Ac7hII;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjCo9HvB;AACF;;AcljII;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjC08HjC;EiC/9HG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjC08H3B;EiCl+HG;IA2BO,kBAAkB;EjC08H5B;EiCr+HG;IA+BO,qB9B4hB6B;I8B3hB7B,oB9B2hB6B;EH86GvC;EiCz+HG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjCu8HzB;EiC7+HG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjCy7HxB;EiCj/HG;IA4DK,aAAa;EjCw7HrB;AACF;;AcjkII;EmB4EC;;IAGK,gBAAgB;IAChB,eAAe;EjCw/HvB;AACF;;ActlII;EmByFA;IAoBI,yBAAqB;IAArB,qBAAqB;IACrB,oBAA2B;IAA3B,2BAA2B;EjC8+HjC;EiCngIG;IAwBK,uBAAmB;IAAnB,mBAAmB;EjC8+H3B;EiCtgIG;IA2BO,kBAAkB;EjC8+H5B;EiCzgIG;IA+BO,qB9B4hB6B;I8B3hB7B,oB9B2hB6B;EHk9GvC;EiC7gIG;;IAsCK,qBAAiB;IAAjB,iBAAiB;EjC2+HzB;EiCjhIG;IAqDK,+BAAwB;IAAxB,wBAAwB;IAGxB,6BAAgB;IAAhB,gBAAgB;EjC69HxB;EiCrhIG;IA4DK,aAAa;EjC49HrB;AACF;;AiC9hIA;EAyBQ,yBAAqB;EAArB,qBAAqB;EACrB,oBAA2B;EAA3B,2BAA2B;AjCygInC;;AiCniIA;;EAQU,gBAAgB;EAChB,eAAe;AjCgiIzB;;AiCziIA;EA6BU,uBAAmB;EAAnB,mBAAmB;AjCghI7B;;AiC7iIA;EAgCY,kBAAkB;AjCihI9B;;AiCjjIA;EAoCY,qB9B4hB6B;E8B3hB7B,oB9B2hB6B;AHs/GzC;;AiCtjIA;;EA2CU,qBAAiB;EAAjB,iBAAiB;AjCghI3B;;AiC3jIA;EA0DU,+BAAwB;EAAxB,wBAAwB;EAGxB,6BAAgB;EAAhB,gBAAgB;AjCmgI1B;;AiChkIA;EAiEU,aAAa;AjCmgIvB;;AiCt/HA;EAEI,yB9B/MW;AHusIf;;AKxsIE;E4BmNI,yB9BlNS;AH2sIf;;AiC9/HA;EAWM,yB9BxNS;AH+sIf;;AKhtIE;E4B4NM,yB9B3NO;AHmtIf;;AiCtgIA;EAkBQ,yB9B/NO;AHutIf;;AiC1gIA;;;;EA0BM,yB9BvOS;AH8tIf;;AiCjhIA;EA+BI,yB9B5OW;E8B6OX,gC9B7OW;AHmuIf;;AiCthIA;EAoCI,mRfrM8E;AlB2rIlF;;AiC1hIA;EAwCI,yB9BrPW;AH2uIf;;AiC9hIA;EA0CM,yB9BvPS;AH+uIf;;AKhvIE;E4B2PM,yB9B1PO;AHmvIf;;AiCl/HA;EAEI,W9B7QW;AHiwIf;;AKxvIE;E4BuQI,W9BhRS;AHqwIf;;AiC1/HA;EAWM,+B9BtRS;AHywIf;;AKhwIE;E4BgRM,gC9BzRO;AH6wIf;;AiClgIA;EAkBQ,gC9B7RO;AHixIf;;AiCtgIA;;;;EA0BM,W9BrSS;AHwxIf;;AiC7gIA;EA+BI,+B9B1SW;E8B2SX,sC9B3SW;AH6xIf;;AiClhIA;EAoCI,yRfzP8E;AlB2uIlF;;AiCthIA;EAwCI,+B9BnTW;AHqyIf;;AiC1hIA;EA0CM,W9BrTS;AHyyIf;;AKhyIE;E4B+SM,W9BxTO;AH6yIf;;AkChzIA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,YAAY;EAEZ,qBAAqB;EACrB,sB/BJa;E+BKb,2BAA2B;EAC3B,sC/BIa;EOCX,sBP6NgC;AHilIpC;;AkC5zIA;EAaI,eAAe;EACf,cAAc;AlCmzIlB;;AkCj0IA;EAkBI,mBAAmB;EACnB,sBAAsB;AlCmzI1B;;AkCt0IA;EAsBM,mBAAmB;ExBCrB,2CQmH4D;ERlH5D,4CQkH4D;AlBksIhE;;AkC50IA;EA2BM,sBAAsB;ExBUxB,+CQqG4D;ERpG5D,8CQoG4D;AlBwsIhE;;AkCl1IA;;EAoCI,aAAa;AlCmzIjB;;AkC/yIA;EAGE,kBAAc;EAAd,cAAc;EAGd,eAAe;EACf,gB/B0wByC;AHoiH3C;;AkC1yIA;EACE,sB/BowBwC;AHyiH1C;;AkC1yIA;EACE,qBAA+B;EAC/B,gBAAgB;AlC6yIlB;;AkC1yIA;EACE,gBAAgB;AlC6yIlB;;AKl2IE;E6B0DE,qBAAqB;AlC4yIzB;;AkC9yIA;EAMI,oB/BmvBuC;AHyjH3C;;AkCpyIA;EACE,wB/B0uByC;E+BzuBzC,gBAAgB;EAEhB,qC/BrEa;E+BsEb,6C/BtEa;AH42If;;AkC3yIA;ExBhEI,0DwBwE8E;AlCuyIlF;;AkCnyIA;EACE,wB/B8tByC;E+B5tBzC,qC/BhFa;E+BiFb,0C/BjFa;AHs3If;;AkCzyIA;ExB5EI,0DQ4H4D;AlB6vIhE;;AkC7xIA;EACE,uBAAiC;EACjC,uB/B4sBwC;E+B3sBxC,sBAAgC;EAChC,gBAAgB;AlCgyIlB;;AkC7xIA;EACE,uBAAiC;EACjC,sBAAgC;AlCgyIlC;;AkC5xIA;EACE,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,SAAS;EACT,OAAO;EACP,gB/BusByC;EOtzBvC,kCQ4H4D;AlBmxIhE;;AkC5xIA;;;EAGE,oBAAc;EAAd,cAAc;EACd,WAAW;AlC+xIb;;AkC5xIA;;ExBjHI,2CQmH4D;ERlH5D,4CQkH4D;AlBgyIhE;;AkC7xIA;;ExBxGI,+CQqG4D;ERpG5D,8CQoG4D;AlBsyIhE;;AkC3xIA;EAEI,mB/B+qBsD;AH8mH1D;;Ac53II;EoB6FJ;IAMI,oBAAa;IAAb,aAAa;IACb,uBAAmB;IAAnB,mBAAmB;IACnB,mB/ByqBsD;I+BxqBtD,kB/BwqBsD;EHsnHxD;EkCvyIF;IAaM,gBAAY;IAAZ,YAAY;IACZ,kB/BmqBoD;I+BlqBpD,gBAAgB;IAChB,iB/BiqBoD;EH4nHxD;AACF;;AkCpxIA;EAII,mB/BmpBsD;AHioH1D;;Ac/4II;EoBuHJ;IAQI,oBAAa;IAAb,aAAa;IACb,uBAAmB;IAAnB,mBAAmB;ElCqxIrB;EkC9xIF;IAcM,gBAAY;IAAZ,YAAY;IACZ,gBAAgB;ElCmxIpB;EkClyIF;IAkBQ,cAAc;IACd,cAAc;ElCmxIpB;EkCtyIF;IxBjJI,0BwB0KoC;IxBzKpC,6BwByKoC;ElCixItC;EkC1yIF;;IA8BY,0BAA0B;ElCgxIpC;EkC9yIF;;IAmCY,6BAA6B;ElC+wIvC;EkClzIF;IxBnII,yBwB2KmC;IxB1KnC,4BwB0KmC;ElC8wIrC;EkCtzIF;;IA6CY,yBAAyB;ElC6wInC;EkC1zIF;;IAkDY,4BAA4B;ElC4wItC;AACF;;AkChwIA;EAEI,sB/BwkBsC;AH0rH1C;;Ac17II;EoBsLJ;IAMI,uB/BqlBiC;I+BrlBjC,oB/BqlBiC;I+BrlBjC,e/BqlBiC;I+BplBjC,2B/BqlBuC;I+BrlBvC,wB/BqlBuC;I+BrlBvC,mB/BqlBuC;I+BplBvC,UAAU;IACV,SAAS;ElCmwIX;EkC5wIF;IAYM,qBAAqB;IACrB,WAAW;ElCmwIf;AACF;;AkC1vIA;EACE,qBAAqB;AlC6vIvB;;AkC9vIA;EAII,gBAAgB;AlC8vIpB;;AkClwIA;EAOM,gBAAgB;ExBvOlB,6BwBwOiC;ExBvOjC,4BwBuOiC;AlCgwIrC;;AkCxwIA;ExB9OI,yBwB0P8B;ExBzP9B,0BwByP8B;AlCiwIlC;;AkC7wIA;ExBvPI,gBwBuQ0B;EACxB,mB/B9C2B;AH+yIjC;;AmC3hJA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,qBhC6hCsC;EgC5hCtC,mBhC+hCsC;EgC7hCtC,gBAAgB;EAChB,yBhCEgB;EOSd,sBP6NgC;AHszIpC;;AmC1hJA;EACE,oBAAa;EAAb,aAAa;AnC6hJf;;AmC9hJA;EAKI,oBhCihCqC;AH4gHzC;;AmCliJA;EAQM,qBAAqB;EACrB,qBhC6gCmC;EgC5gCnC,chCRY;EgCSZ,YhCkhCuC;AH4gH7C;;AmCziJA;EAsBI,0BAA0B;AnCuhJ9B;;AmC7iJA;EA0BI,qBAAqB;AnCuhJzB;;AmCjjJA;EA8BI,chC5Bc;AHmjJlB;;AoChkJA;EACE,oBAAa;EAAb,aAAa;E7BGb,eAAe;EACf,gBAAgB;EGad,sBP6NgC;AHw1IpC;;AoCjkJA;EACE,kBAAkB;EAClB,cAAc;EACd,uBjC8wBwC;EiC7wBxC,iBjCkO+B;EiCjO/B,iBjCixBsC;EiChxBtC,cjCuBe;EiCrBf,sBjCPa;EiCQb,yBjCLgB;AHwkJlB;;AoC5kJA;EAYI,UAAU;EACV,cjC8J8D;EiC7J9D,qBAAqB;EACrB,yBjCZc;EiCad,qBjCZc;AHglJlB;;AoCplJA;EAoBI,UAAU;EACV,UjCywBiC;EiCxwBjC,gDjCOa;AH6jJjB;;AoChkJA;EAGM,cAAc;E1BahB,+BP+LgC;EO9LhC,kCP8LgC;AHu3IpC;;AoCtkJA;E1BEI,gCP6MgC;EO5MhC,mCP4MgC;AH43IpC;;AoC3kJA;EAcI,UAAU;EACV,WjCxCW;EiCyCX,yBjCba;EiCcb,qBjCda;AH+kJjB;;AoCllJA;EAqBI,cjCxCc;EiCyCd,oBAAoB;EAEpB,YAAY;EACZ,sBjClDW;EiCmDX,qBjChDc;AHgnJlB;;AqCvnJE;EACE,uBlCuxBsC;EC5pBpC,kBAtCY;EiCnFd,gBlCmO6B;AHu5IjC;;AqCrnJM;E3BqCF,8BPgM+B;EO/L/B,iCP+L+B;AHq5InC;;AqCrnJM;E3BkBF,+BP8M+B;EO7M/B,kCP6M+B;AH05InC;;AqCvoJE;EACE,uBlCqxBqC;EC1pBnC,mBAtCY;EiCnFd,gBlCoO6B;AHs6IjC;;AqCroJM;E3BqCF,8BPiM+B;EOhM/B,iCPgM+B;AHo6InC;;AqCroJM;E3BkBF,+BP+M+B;EO9M/B,kCP8M+B;AHy6InC;;AsCrpJA;EACE,qBAAqB;EACrB,qBnCs5BsC;ECr1BpC,cAAW;EkC/Db,gBnCuR+B;EmCtR/B,cAAc;EACd,kBAAkB;EAClB,mBAAmB;EACnB,wBAAwB;E5BKtB,sBP6NgC;EiB/N9B,qIjBgb6I;AHuuInJ;;AoBnpJM;EkBfN;IlBgBQ,gBAAgB;EpBupJtB;AACF;;AK7pJE;EiCGI,qBAAqB;AtC8pJ3B;;AsC5qJA;EAoBI,aAAa;AtC4pJjB;;AsCvpJA;EACE,kBAAkB;EAClB,SAAS;AtC0pJX;;AsCnpJA;EACE,oBnC23BsC;EmC13BtC,mBnC03BsC;EOj5BpC,oBPo5BqC;AH0xHzC;;AsC9oJE;ECjDA,WpCMa;EoCLb,yBpCiCe;AHkqJjB;;AKrrJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvCmsJxC;;AuCtsJU;EAQJ,UAAU;EACV,+CpCsBW;AH4qJjB;;AsC7pJE;ECjDA,WpCMa;EoCLb,yBpCWgB;AHusJlB;;AKpsJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvCktJxC;;AuCrtJU;EAQJ,UAAU;EACV,iDpCAY;AHitJlB;;AsC5qJE;ECjDA,WpCMa;EoCLb,yBpCwCe;AHyrJjB;;AKntJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvCiuJxC;;AuCpuJU;EAQJ,UAAU;EACV,+CpC6BW;AHmsJjB;;AsC3rJE;ECjDA,WpCMa;EoCLb,yBpC0Ce;AHssJjB;;AKluJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvCgvJxC;;AuCnvJU;EAQJ,UAAU;EACV,gDpC+BW;AHgtJjB;;AsC1sJE;ECjDA,cpCegB;EoCdhB,yBpCuCe;AHwtJjB;;AKjvJE;EkCVI,cpCUY;EoCTZ,yBAAkC;AvC+vJxC;;AuClwJU;EAQJ,UAAU;EACV,+CpC4BW;AHkuJjB;;AsCztJE;ECjDA,WpCMa;EoCLb,yBpCqCe;AHyuJjB;;AKhwJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC8wJxC;;AuCjxJU;EAQJ,UAAU;EACV,+CpC0BW;AHmvJjB;;AsCxuJE;ECjDA,cpCegB;EoCdhB,yBpCMgB;AHuxJlB;;AK/wJE;EkCVI,cpCUY;EoCTZ,yBAAkC;AvC6xJxC;;AuChyJU;EAQJ,UAAU;EACV,iDpCLY;AHiyJlB;;AsCvvJE;ECjDA,WpCMa;EoCLb,yBpCagB;AH+xJlB;;AK9xJE;EkCVI,WpCCS;EoCAT,yBAAkC;AvC4yJxC;;AuC/yJU;EAQJ,UAAU;EACV,8CpCEY;AHyyJlB;;AwCxzJA;EACE,kBAAoD;EACpD,mBrCmzBsC;EqCjzBtC,yBrCKgB;EOSd,qBP8N+B;AH+kJnC;;AcnwJI;E0B5DJ;IAQI,kBrC6yBoC;EH+gItC;AACF;;AwCzzJA;EACE,gBAAgB;EAChB,eAAe;E9BIb,gB8BHsB;AxC4zJ1B;;AyCv0JA;EACE,kBAAkB;EAClB,wBtCm9ByC;EsCl9BzC,mBtCm9BsC;EsCl9BtC,6BAA6C;E/BU3C,sBP6NgC;AHomJpC;;AyCt0JA;EAEE,cAAc;AzCw0JhB;;AyCp0JA;EACE,gBtC4Q+B;AH2jJjC;;AyC/zJA;EACE,mBAAsD;AzCk0JxD;;AyCn0JA;EAKI,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,UAAU;EACV,wBtCo7BuC;EsCn7BvC,cAAc;AzCk0JlB;;AyCxzJE;EC/CA,cxBwGgE;EInG9D,yBJmG8D;EwBtGhE,qBxBsGgE;AlBqwJlE;;A0Cz2JE;EACE,yBAAqC;A1C42JzC;;A0Cz2JE;EACE,cAA0B;A1C42J9B;;AyCt0JE;EC/CA,cxBwGgE;EInG9D,yBJmG8D;EwBtGhE,qBxBsGgE;AlBmxJlE;;A0Cv3JE;EACE,yBAAqC;A1C03JzC;;A0Cv3JE;EACE,cAA0B;A1C03J9B;;AyCp1JE;EC/CA,cxBwGgE;EInG9D,yBJmG8D;EwBtGhE,qBxBsGgE;AlBiyJlE;;A0Cr4JE;EACE,yBAAqC;A1Cw4JzC;;A0Cr4JE;EACE,cAA0B;A1Cw4J9B;;AyCl2JE;EC/CA,cxBwGgE;EInG9D,yBJmG8D;EwBtGhE,qBxBsGgE;AlB+yJlE;;A0Cn5JE;EACE,yBAAqC;A1Cs5JzC;;A0Cn5JE;EACE,cAA0B;A1Cs5J9B;;AyCh3JE;EC/CA,cxBwGgE;EInG9D,yBJmG8D;EwBtGhE,qBxBsGgE;AlB6zJlE;;A0Cj6JE;EACE,yBAAqC;A1Co6JzC;;A0Cj6JE;EACE,cAA0B;A1Co6J9B;;AyC93JE;EC/CA,cxBwGgE;EInG9D,yBJmG8D;EwBtGhE,qBxBsGgE;AlB20JlE;;A0C/6JE;EACE,yBAAqC;A1Ck7JzC;;A0C/6JE;EACE,cAA0B;A1Ck7J9B;;AyC54JE;EC/CA,cxBwGgE;EInG9D,yBJmG8D;EwBtGhE,qBxBsGgE;AlBy1JlE;;A0C77JE;EACE,yBAAqC;A1Cg8JzC;;A0C77JE;EACE,cAA0B;A1Cg8J9B;;AyC15JE;EC/CA,cxBwGgE;EInG9D,yBJmG8D;EwBtGhE,qBxBsGgE;AlBu2JlE;;A0C38JE;EACE,yBAAqC;A1C88JzC;;A0C38JE;EACE,cAA0B;A1C88J9B;;A2Ct9JE;EACE;IAAO,2BAAuC;E3C09JhD;E2Cz9JE;IAAK,wBAAwB;E3C49J/B;AACF;;A2C/9JE;EACE;IAAO,2BAAuC;E3C09JhD;E2Cz9JE;IAAK,wBAAwB;E3C49J/B;AACF;;A2Cz9JA;EACE,oBAAa;EAAb,aAAa;EACb,YxC49BsC;EwC39BtC,gBAAgB;EAChB,cAAc;EvCmHV,kBAtCY;EuC3EhB,yBxCLgB;EOSd,sBP6NgC;AH4vJpC;;A2Cx9JA;EACE,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,qBAAuB;EAAvB,uBAAuB;EACvB,gBAAgB;EAChB,WxCjBa;EwCkBb,kBAAkB;EAClB,mBAAmB;EACnB,yBxCQe;EiBnBX,2BjB89B4C;AHygIlD;;AoBn+JM;EuBDN;IvBEQ,gBAAgB;EpBu+JtB;AACF;;A2C99JA;ErBYE,qMAA6I;EqBV7I,0BxCq8BsC;AH4hIxC;;A2C79JE;EACE,0DxCu8BkD;EwCv8BlD,kDxCu8BkD;AHyhItD;;A2C79JM;EAJJ;IAKM,uBAAe;IAAf,eAAe;E3Ci+JrB;AACF;;A4C5gKA;EACE,oBAAa;EAAb,aAAa;EACb,qBAAuB;EAAvB,uBAAuB;A5C+gKzB;;A4C5gKA;EACE,WAAO;EAAP,OAAO;A5C+gKT;;A6CjhKA;EACE,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EAGtB,eAAe;EACf,gBAAgB;EnCQd,sBP6NgC;AH8yJpC;;A6CzgKA;EACE,WAAW;EACX,c1CRgB;E0CShB,mBAAmB;A7C4gKrB;;AKnhKE;EwCWE,UAAU;EACV,c1Cdc;E0Ced,qBAAqB;EACrB,yB1CtBc;AHkiKlB;;A6CthKA;EAcI,c1ClBc;E0CmBd,yB1C1Bc;AHsiKlB;;A6CngKA;EACE,kBAAkB;EAClB,cAAc;EACd,wB1C28ByC;E0Cx8BzC,sB1C3Ca;E0C4Cb,sC1ClCa;AHsiKf;;A6C3gKA;EnCjBI,+BmC2BkC;EnC1BlC,gCmC0BkC;A7CsgKtC;;A6ChhKA;EnCHI,mCmCiBqC;EnChBrC,kCmCgBqC;A7CugKzC;;A6CrhKA;EAmBI,c1ClDc;E0CmDd,oBAAoB;EACpB,sB1C1DW;AHgkKf;;A6C3hKA;EA0BI,UAAU;EACV,W1ChEW;E0CiEX,yB1CrCa;E0CsCb,qB1CtCa;AH2iKjB;;A6CliKA;EAiCI,mBAAmB;A7CqgKvB;;A6CtiKA;EAoCM,gB1C4J2B;E0C3J3B,qB1C2J2B;AH22JjC;;A6Cx/JI;EACE,uBAAmB;EAAnB,mBAAmB;A7C2/JzB;;A6C5/JI;EnCtBA,kCPsKgC;EOlLhC,0BmCwCwC;A7C2/J5C;;A6CjgKI;EnClCA,gCPkLgC;EOtKhC,4BmCiC0C;A7C2/J9C;;A6CtgKI;EAeM,aAAa;A7C2/JvB;;A6C1gKI;EAmBM,qB1C0HuB;E0CzHvB,oBAAoB;A7C2/J9B;;A6C/gKI;EAuBQ,iB1CsHqB;E0CrHrB,sB1CqHqB;AHu4JjC;;AcvjKI;E+BmCA;IACE,uBAAmB;IAAnB,mBAAmB;E7CwhKvB;E6CzhKE;InCtBA,kCPsKgC;IOlLhC,0BmCwCwC;E7CuhK1C;E6C7hKE;InClCA,gCPkLgC;IOtKhC,4BmCiC0C;E7CshK5C;E6CjiKE;IAeM,aAAa;E7CqhKrB;E6CpiKE;IAmBM,qB1C0HuB;I0CzHvB,oBAAoB;E7CohK5B;E6CxiKE;IAuBQ,iB1CsHqB;I0CrHrB,sB1CqHqB;EH+5J/B;AACF;;AchlKI;E+BmCA;IACE,uBAAmB;IAAnB,mBAAmB;E7CijKvB;E6CljKE;InCtBA,kCPsKgC;IOlLhC,0BmCwCwC;E7CgjK1C;E6CtjKE;InClCA,gCPkLgC;IOtKhC,4BmCiC0C;E7C+iK5C;E6C1jKE;IAeM,aAAa;E7C8iKrB;E6C7jKE;IAmBM,qB1C0HuB;I0CzHvB,oBAAoB;E7C6iK5B;E6CjkKE;IAuBQ,iB1CsHqB;I0CrHrB,sB1CqHqB;EHw7J/B;AACF;;AczmKI;E+BmCA;IACE,uBAAmB;IAAnB,mBAAmB;E7C0kKvB;E6C3kKE;InCtBA,kCPsKgC;IOlLhC,0BmCwCwC;E7CykK1C;E6C/kKE;InClCA,gCPkLgC;IOtKhC,4BmCiC0C;E7CwkK5C;E6CnlKE;IAeM,aAAa;E7CukKrB;E6CtlKE;IAmBM,qB1C0HuB;I0CzHvB,oBAAoB;E7CskK5B;E6C1lKE;IAuBQ,iB1CsHqB;I0CrHrB,sB1CqHqB;EHi9J/B;AACF;;AcloKI;E+BmCA;IACE,uBAAmB;IAAnB,mBAAmB;E7CmmKvB;E6CpmKE;InCtBA,kCPsKgC;IOlLhC,0BmCwCwC;E7CkmK1C;E6CxmKE;InClCA,gCPkLgC;IOtKhC,4BmCiC0C;E7CimK5C;E6C5mKE;IAeM,aAAa;E7CgmKrB;E6C/mKE;IAmBM,qB1C0HuB;I0CzHvB,oBAAoB;E7C+lK5B;E6CnnKE;IAuBQ,iB1CsHqB;I0CrHrB,sB1CqHqB;EH0+J/B;AACF;;A6CllKA;EnCnHI,gBmCoHsB;A7CqlK1B;;A6CtlKA;EAII,qB1CmG6B;AHm/JjC;;A6C1lKA;EAOM,sBAAsB;A7CulK5B;;A8ChuKE;EACE,c5BqG8D;E4BpG9D,yB5BoG8D;AlB+nKlE;;AKxtKE;EyCPM,c5BgG0D;E4B/F1D,yBAAyC;A9CmuKjD;;A8C1uKE;EAWM,W3CPO;E2CQP,yB5B0F0D;E4BzF1D,qB5ByF0D;AlB0oKlE;;A8ChvKE;EACE,c5BqG8D;E4BpG9D,yB5BoG8D;AlB+oKlE;;AKxuKE;EyCPM,c5BgG0D;E4B/F1D,yBAAyC;A9CmvKjD;;A8C1vKE;EAWM,W3CPO;E2CQP,yB5B0F0D;E4BzF1D,qB5ByF0D;AlB0pKlE;;A8ChwKE;EACE,c5BqG8D;E4BpG9D,yB5BoG8D;AlB+pKlE;;AKxvKE;EyCPM,c5BgG0D;E4B/F1D,yBAAyC;A9CmwKjD;;A8C1wKE;EAWM,W3CPO;E2CQP,yB5B0F0D;E4BzF1D,qB5ByF0D;AlB0qKlE;;A8ChxKE;EACE,c5BqG8D;E4BpG9D,yB5BoG8D;AlB+qKlE;;AKxwKE;EyCPM,c5BgG0D;E4B/F1D,yBAAyC;A9CmxKjD;;A8C1xKE;EAWM,W3CPO;E2CQP,yB5B0F0D;E4BzF1D,qB5ByF0D;AlB0rKlE;;A8ChyKE;EACE,c5BqG8D;E4BpG9D,yB5BoG8D;AlB+rKlE;;AKxxKE;EyCPM,c5BgG0D;E4B/F1D,yBAAyC;A9CmyKjD;;A8C1yKE;EAWM,W3CPO;E2CQP,yB5B0F0D;E4BzF1D,qB5ByF0D;AlB0sKlE;;A8ChzKE;EACE,c5BqG8D;E4BpG9D,yB5BoG8D;AlB+sKlE;;AKxyKE;EyCPM,c5BgG0D;E4B/F1D,yBAAyC;A9CmzKjD;;A8C1zKE;EAWM,W3CPO;E2CQP,yB5B0F0D;E4BzF1D,qB5ByF0D;AlB0tKlE;;A8Ch0KE;EACE,c5BqG8D;E4BpG9D,yB5BoG8D;AlB+tKlE;;AKxzKE;EyCPM,c5BgG0D;E4B/F1D,yBAAyC;A9Cm0KjD;;A8C10KE;EAWM,W3CPO;E2CQP,yB5B0F0D;E4BzF1D,qB5ByF0D;AlB0uKlE;;A8Ch1KE;EACE,c5BqG8D;E4BpG9D,yB5BoG8D;AlB+uKlE;;AKx0KE;EyCPM,c5BgG0D;E4B/F1D,yBAAyC;A9Cm1KjD;;A8C11KE;EAWM,W3CPO;E2CQP,yB5B0F0D;E4BzF1D,qB5ByF0D;AlB0vKlE;;A+Cn2KA;EACE,YAAY;E3C8HR,iBAtCY;E2CtFhB,gB5C6R+B;E4C5R/B,cAAc;EACd,W5CYa;E4CXb,yB5CCa;E4CAb,WAAW;A/Cs2Kb;;AKj2KE;E0CDE,W5CMW;E4CLX,qBAAqB;A/Cs2KzB;;AKl2KE;E0CCI,YAAY;A/Cq2KlB;;A+C11KA;EACE,UAAU;EACV,6BAA6B;EAC7B,SAAS;A/C61KX;;A+Cv1KA;EACE,oBAAoB;A/C01KtB;;AgDh4KA;EAGE,8B7Cq4BuC;E6Cr4BvC,iB7Cq4BuC;E6Cp4BvC,gB7Co4BuC;ECzwBnC,mBAtCY;E4ClFhB,2C7CAa;E6CCb,4BAA4B;EAC5B,oC7Cs4BmD;E6Cr4BnD,gD7COa;E6CNb,UAAU;EtCOR,sBP83BsC;AH4/I1C;;AgD54KA;EAeI,sB7C03BsC;AHugJ1C;;AgDh5KA;EAmBI,UAAU;AhDi4Kd;;AgDp5KA;EAuBI,cAAc;EACd,UAAU;AhDi4Kd;;AgDz5KA;EA4BI,aAAa;AhDi4KjB;;AgD73KA;EACE,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,wB7Cs2BwC;E6Cr2BxC,c7CvBgB;E6CwBhB,2C7C9Ba;E6C+Bb,4BAA4B;EAC5B,4C7C82BoD;EO13BlD,2CQmH4D;ERlH5D,4CQkH4D;AlB2xKhE;;AgD93KA;EACE,gB7C61BwC;AHoiJ1C;;AiDv6KA;EAEE,gBAAgB;AjDy6KlB;;AiD36KA;EAKI,kBAAkB;EAClB,gBAAgB;AjD06KpB;;AiDr6KA;EACE,eAAe;EACf,MAAM;EACN,OAAO;EACP,a9C2pBsC;E8C1pBtC,aAAa;EACb,WAAW;EACX,YAAY;EACZ,gBAAgB;EAGhB,UAAU;AjDs6KZ;;AiD/5KA;EACE,kBAAkB;EAClB,WAAW;EACX,c9C24BuC;E8Cz4BvC,oBAAoB;AjDi6KtB;;AiD95KE;E7B3BI,2CjBg8BoD;EiBh8BpD,mCjBg8BoD;EiBh8BpD,oEjBg8BoD;E8Cn6BtD,sC9Ci6BmD;E8Cj6BnD,8B9Ci6BmD;AHggJvD;;AoB17KM;E6BuBJ;I7BtBM,gBAAgB;EpB87KtB;AACF;;AiDr6KE;EACE,uB9C+5BoC;E8C/5BpC,e9C+5BoC;AHygJxC;;AiDp6KE;EACE,8B9C45B2C;E8C55B3C,sB9C45B2C;AH2gJ/C;;AiDn6KA;EACE,oBAAa;EAAb,aAAa;EACb,6B/BmF8D;AlBm1KhE;;AiDx6KA;EAKI,8B/BgF4D;E+B/E5D,gBAAgB;AjDu6KpB;;AiD76KA;;EAWI,oBAAc;EAAd,cAAc;AjDu6KlB;;AiDl7KA;EAeI,gBAAgB;AjDu6KpB;;AiDn6KA;EACE,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,6B/B+D8D;AlBu2KhE;;AiDz6KA;EAOI,cAAc;EACd,0B/B0D4D;E+BzD5D,2BAAmB;EAAnB,wBAAmB;EAAnB,mBAAmB;EACnB,WAAW;AjDs6Kf;;AiDh7KA;EAeI,0BAAsB;EAAtB,sBAAsB;EACtB,qBAAuB;EAAvB,uBAAuB;EACvB,YAAY;AjDq6KhB;;AiDt7KA;EAoBM,gBAAgB;AjDs6KtB;;AiD17KA;EAwBM,aAAa;AjDs6KnB;;AiDh6KA;EACE,kBAAkB;EAClB,oBAAa;EAAb,aAAa;EACb,0BAAsB;EAAtB,sBAAsB;EACtB,WAAW;EAGX,oBAAoB;EACpB,sB9C3Ga;E8C4Gb,4BAA4B;EAC5B,oC9CnGa;EOCX,qBP8N+B;E8CxHjC,UAAU;AjD+5KZ;;AiD35KA;EACE,eAAe;EACf,MAAM;EACN,OAAO;EACP,a9C+iBsC;E8C9iBtC,YAAY;EACZ,aAAa;EACb,sB9ClHa;AHghLf;;AiDr6KA;EAUW,UAAU;AjD+5KrB;;AiDz6KA;EAWW,Y9CyzB2B;AHymJtC;;AiD75KA;EACE,oBAAa;EAAb,aAAa;EACb,qBAAuB;EAAvB,uBAAuB;EACvB,sBAA8B;EAA9B,8BAA8B;EAC9B,kB9CszBsC;E8CrzBtC,gC9CvIgB;EOiBd,0CQmH4D;ERlH5D,2CQkH4D;AlBq6KhE;;AiDv6KA;EASI,kB9CizBoC;E8C/yBpC,8BAA6F;AjDi6KjG;;AiD55KA;EACE,gBAAgB;EAChB,gB9CsI+B;AHyxKjC;;AiD15KA;EACE,kBAAkB;EAGlB,kBAAc;EAAd,cAAc;EACd,a9CowBsC;AHupJxC;;AiDv5KA;EACE,oBAAa;EAAb,aAAa;EACb,mBAAe;EAAf,eAAe;EACf,sBAAmB;EAAnB,mBAAmB;EACnB,kBAAyB;EAAzB,yBAAyB;EACzB,gBAAgE;EAChE,6B9CxKgB;EO+Bd,8CQqG4D;ERpG5D,6CQoG4D;AlBg8KhE;;AiDl6KA;EAaI,eAAwC;AjDy5K5C;;AiDp5KA;EACE,kBAAkB;EAClB,YAAY;EACZ,WAAW;EACX,YAAY;EACZ,gBAAgB;AjDu5KlB;;Ac9hLI;EmCzBJ;IAuKI,gB9CiwBqC;I8ChwBrC,oBAAyC;EjDq5K3C;EiDviLF;IAsJI,+B/BjE4D;ElBq9K9D;EiD1iLF;IAyJM,gC/BpE0D;ElBw9K9D;EiD1hLF;IA2II,+B/BzE4D;ElB29K9D;EiD7hLF;IA8IM,4B/B5E0D;I+B6E1D,2BAAmB;IAAnB,wBAAmB;IAAnB,mBAAmB;EjDk5KvB;EiD14KA;IAAY,gB9CyuB2B;EHoqJvC;AACF;;AcrjLI;EmC2KF;;IAEE,gB9CiuBqC;EH6qJvC;AACF;;Ac5jLI;EmCkLF;IAAY,iB9C2tB4B;EHorJxC;AACF;;AkD7nLA;EACE,kBAAkB;EAClB,a/C+qBsC;E+C9qBtC,cAAc;EACd,S/Cu1BmC;EgD31BnC,kMhDmRiN;EgDjRjN,kBAAkB;EAClB,gBhD2R+B;EgD1R/B,gBhD+R+B;EgD9R/B,gBAAgB;EAChB,iBAAiB;EACjB,qBAAqB;EACrB,iBAAiB;EACjB,oBAAoB;EACpB,sBAAsB;EACtB,kBAAkB;EAClB,oBAAoB;EACpB,mBAAmB;EACnB,gBAAgB;E/CgHZ,mBAtCY;E8C9EhB,qBAAqB;EACrB,UAAU;AlD0oLZ;;AkDrpLA;EAaW,Y/C20B2B;AHi0JtC;;AkDzpLA;EAgBI,kBAAkB;EAClB,cAAc;EACd,a/C20BqC;E+C10BrC,c/C20BqC;AHk0JzC;;AkDhqLA;EAsBM,kBAAkB;EAClB,WAAW;EACX,yBAAyB;EACzB,mBAAmB;AlD8oLzB;;AkDzoLA;EACE,iBAAgC;AlD4oLlC;;AkD7oLA;EAII,SAAS;AlD6oLb;;AkDjpLA;EAOM,MAAM;EACN,6BAAgE;EAChE,sB/CvBS;AHqqLf;;AkDzoLA;EACE,iB/CizBuC;AH21JzC;;AkD7oLA;EAII,OAAO;EACP,a/C6yBqC;E+C5yBrC,c/C2yBqC;AHk2JzC;;AkDnpLA;EASM,QAAQ;EACR,oCAA2F;EAC3F,wB/CvCS;AHqrLf;;AkDzoLA;EACE,iBAAgC;AlD4oLlC;;AkD7oLA;EAII,MAAM;AlD6oLV;;AkDjpLA;EAOM,SAAS;EACT,6B/C0xBmC;E+CzxBnC,yB/CrDS;AHmsLf;;AkDzoLA;EACE,iB/CmxBuC;AHy3JzC;;AkD7oLA;EAII,QAAQ;EACR,a/C+wBqC;E+C9wBrC,c/C6wBqC;AHg4JzC;;AkDnpLA;EASM,OAAO;EACP,oC/C0wBmC;E+CzwBnC,uB/CrES;AHmtLf;;AkDznLA;EACE,gB/CyuBuC;E+CxuBvC,uB/C8uBuC;E+C7uBvC,W/CvGa;E+CwGb,kBAAkB;EAClB,sB/C/Fa;EOCX,sBP6NgC;AH8/KpC;;AoD7uLA;EACE,kBAAkB;EAClB,MAAM;EACN,OAAO;EACP,ajD6qBsC;EiD5qBtC,cAAc;EACd,gBjDy2BuC;EgD92BvC,kMhDmRiN;EgDjRjN,kBAAkB;EAClB,gBhD2R+B;EgD1R/B,gBhD+R+B;EgD9R/B,gBAAgB;EAChB,iBAAiB;EACjB,qBAAqB;EACrB,iBAAiB;EACjB,oBAAoB;EACpB,sBAAsB;EACtB,kBAAkB;EAClB,oBAAoB;EACpB,mBAAmB;EACnB,gBAAgB;E/CgHZ,mBAtCY;EgD7EhB,qBAAqB;EACrB,sBjDNa;EiDOb,4BAA4B;EAC5B,oCjDEa;EOCX,qBP8N+B;AH0hLnC;;AoD1wLA;EAoBI,kBAAkB;EAClB,cAAc;EACd,WjDy2BoC;EiDx2BpC,cjDy2BqC;EiDx2BrC,gBjDwN+B;AHkiLnC;;AoDlxLA;EA4BM,kBAAkB;EAClB,cAAc;EACd,WAAW;EACX,yBAAyB;EACzB,mBAAmB;ApD0vLzB;;AoDrvLA;EACE,qBjD01BuC;AH85JzC;;AoDzvLA;EAII,2BlCqG4D;AlBopLhE;;AoD7vLA;EAOM,SAAS;EACT,6BAAgE;EAChE,qCjDq1BiE;AHq6JvE;;AoDnwLA;EAaM,WjD0L2B;EiDzL3B,6BAAgE;EAChE,sBjD7CS;AHuyLf;;AoDrvLA;EACE,mBjDs0BuC;AHk7JzC;;AoDzvLA;EAII,yBlCiF4D;EkChF5D,ajDk0BqC;EiDj0BrC,YjDg0BoC;EiD/zBpC,gBAAgC;ApDyvLpC;;AoDhwLA;EAUM,OAAO;EACP,oCAA2F;EAC3F,uCjD8zBiE;AH47JvE;;AoDtwLA;EAgBM,SjDmK2B;EiDlK3B,oCAA2F;EAC3F,wBjDpES;AH8zLf;;AoDrvLA;EACE,kBjD+yBuC;AHy8JzC;;AoDzvLA;EAII,wBlC0D4D;AlB+rLhE;;AoD7vLA;EAOM,MAAM;EACN,oCAA2F;EAC3F,wCjD0yBiE;AHg9JvE;;AoDnwLA;EAaM,QjD+I2B;EiD9I3B,oCAA2F;EAC3F,yBjDxFS;AHk1Lf;;AoDzwLA;EAqBI,kBAAkB;EAClB,MAAM;EACN,SAAS;EACT,cAAc;EACd,WjDsxBoC;EiDrxBpC,oBAAsC;EACtC,WAAW;EACX,gCjD0wBuD;AH8+J3D;;AoDpvLA;EACE,oBjD+wBuC;AHw+JzC;;AoDxvLA;EAII,0BlC0B4D;EkCzB5D,ajD2wBqC;EiD1wBrC,YjDywBoC;EiDxwBpC,gBAAgC;ApDwvLpC;;AoD/vLA;EAUM,QAAQ;EACR,oCjDqwBmC;EiDpwBnC,sCjDuwBiE;AHk/JvE;;AoDrwLA;EAgBM,UjD4G2B;EiD3G3B,oCjD+vBmC;EiD9vBnC,uBjD3HS;AHo3Lf;;AoDnuLA;EACE,uBjDguBwC;EiD/tBxC,gBAAgB;EhD3BZ,eAtCY;EgDoEhB,yBjDytByD;EiDxtBzD,gCAAyE;E1CnIvE,0CQmH4D;ERlH5D,2CQkH4D;AlBuvLhE;;AoD7uLA;EAUI,aAAa;ApDuuLjB;;AoDnuLA;EACE,uBjDktBwC;EiDjtBxC,cjDxJgB;AH83LlB;;AqDj4LA;EACE,kBAAkB;ArDo4LpB;;AqDj4LA;EACE,uBAAmB;EAAnB,mBAAmB;ArDo4LrB;;AqDj4LA;EACE,kBAAkB;EAClB,WAAW;EACX,gBAAgB;ArDo4LlB;;AsD35LE;EACE,cAAc;EACd,WAAW;EACX,WAAW;AtD85Lf;;AqDt4LA;EACE,kBAAkB;EAClB,aAAa;EACb,WAAW;EACX,WAAW;EACX,mBAAmB;EACnB,mCAA2B;EAA3B,2BAA2B;EjClBvB,8CjBqjCkF;EiBrjClF,sCjBqjCkF;EiBrjClF,0EjBqjCkF;AHu2JxF;;AoBx5LM;EiCQN;IjCPQ,gBAAgB;EpB45LtB;AACF;;AqD54LA;;;EAGE,cAAc;ArD+4LhB;;AqD54LA;;EAEE,mCAA2B;EAA3B,2BAA2B;ArD+4L7B;;AqD54LA;;EAEE,oCAA4B;EAA5B,4BAA4B;ArD+4L9B;;AqDv4LA;EAEI,UAAU;EACV,4BAA4B;EAC5B,uBAAe;EAAf,eAAe;ArDy4LnB;;AqD74LA;;;EAUI,UAAU;EACV,UAAU;ArDy4Ld;;AqDp5LA;;EAgBI,UAAU;EACV,UAAU;EjC5DR,2BjBojCkC;AHk5JxC;;AoBl8LM;EiCuCN;;IjCtCQ,gBAAgB;EpBu8LtB;AACF;;AqDv4LA;;EAEE,kBAAkB;EAClB,MAAM;EACN,SAAS;EACT,UAAU;EAEV,oBAAa;EAAb,aAAa;EACb,sBAAmB;EAAnB,mBAAmB;EACnB,qBAAuB;EAAvB,uBAAuB;EACvB,UlDg9BsC;EkD/8BtC,WlD1Fa;EkD2Fb,kBAAkB;EAClB,YlD88BqC;EiBjiCjC,8BjBmiCgD;AH07JtD;;AoBz9LM;EiCkEN;;IjCjEQ,gBAAgB;EpB89LtB;AACF;;AKp+LE;;;EgDwFE,WlDjGW;EkDkGX,qBAAqB;EACrB,UAAU;EACV,YlDu8BmC;AH28JvC;;AqD/4LA;EACE,OAAO;ArDk5LT;;AqD74LA;EACE,QAAQ;ArDg5LV;;AqDz4LA;;EAEE,qBAAqB;EACrB,WlDg8BuC;EkD/7BvC,YlD+7BuC;EkD97BvC,qCAAqC;ArD44LvC;;AqD14LA;EACE,sNnCvEgF;AlBo9LlF;;AqD34LA;EACE,uNnC1EgF;AlBw9LlF;;AqDr4LA;EACE,kBAAkB;EAClB,QAAQ;EACR,SAAS;EACT,OAAO;EACP,WAAW;EACX,oBAAa;EAAb,aAAa;EACb,qBAAuB;EAAvB,uBAAuB;EACvB,eAAe;EAEf,iBlDs5BsC;EkDr5BtC,gBlDq5BsC;EkDp5BtC,gBAAgB;ArDu4LlB;;AqDn5LA;EAeI,uBAAuB;EACvB,kBAAc;EAAd,cAAc;EACd,WlDo5BqC;EkDn5BrC,WlDo5BoC;EkDn5BpC,iBlDq5BoC;EkDp5BpC,gBlDo5BoC;EkDn5BpC,mBAAmB;EACnB,eAAe;EACf,sBlDhKW;EkDiKX,4BAA4B;EAE5B,kCAAiE;EACjE,qCAAoE;EACpE,WAAW;EjC5JT,6BjB0iC+C;AH0/JrD;;AoBhiMM;EiC4HN;IjC3HQ,gBAAgB;EpBoiMtB;AACF;;AqD16LA;EAiCI,UAAU;ArD64Ld;;AqDp4LA;EACE,kBAAkB;EAClB,UAA2C;EAC3C,YAAY;EACZ,SAA0C;EAC1C,WAAW;EACX,iBAAiB;EACjB,oBAAoB;EACpB,WlD3La;EkD4Lb,kBAAkB;ArDu4LpB;;AuDtkMA;EACE;IAAK,iCAAyB;IAAzB,yBAAyB;EvD0kM9B;AACF;;AuD5kMA;EACE;IAAK,iCAAyB;IAAzB,yBAAyB;EvD0kM9B;AACF;;AuDxkMA;EACE,qBAAqB;EACrB,WpDgkC0B;EoD/jC1B,YpD+jC0B;EoD9jC1B,2BAA2B;EAC3B,iCAAgD;EAChD,+BAA+B;EAE/B,kBAAkB;EAClB,sDAA8C;EAA9C,8CAA8C;AvD0kMhD;;AuDvkMA;EACE,WpDyjC4B;EoDxjC5B,YpDwjC4B;EoDvjC5B,mBpDyjC4B;AHihK9B;;AuDnkMA;EACE;IACE,2BAAmB;IAAnB,mBAAmB;EvDskMrB;EuDpkMA;IACE,UAAU;IACV,uBAAe;IAAf,eAAe;EvDskMjB;AACF;;AuD7kMA;EACE;IACE,2BAAmB;IAAnB,mBAAmB;EvDskMrB;EuDpkMA;IACE,UAAU;IACV,uBAAe;IAAf,eAAe;EvDskMjB;AACF;;AuDnkMA;EACE,qBAAqB;EACrB,WpDgiC0B;EoD/hC1B,YpD+hC0B;EoD9hC1B,2BAA2B;EAC3B,8BAA8B;EAE9B,kBAAkB;EAClB,UAAU;EACV,oDAA4C;EAA5C,4CAA4C;AvDqkM9C;;AuDlkMA;EACE,WpDyhC4B;EoDxhC5B,YpDwhC4B;AH6iK9B;;AwDznMA;EAAqB,mCAAmC;AxD6nMxD;;AwD5nMA;EAAqB,8BAA8B;AxDgoMnD;;AwD/nMA;EAAqB,iCAAiC;AxDmoMtD;;AwDloMA;EAAqB,iCAAiC;AxDsoMtD;;AwDroMA;EAAqB,sCAAsC;AxDyoM3D;;AwDxoMA;EAAqB,mCAAmC;AxD4oMxD;;AyD9oME;EACE,oCAAmC;AzDipMvC;;AKvoME;;;EoDLI,oCAAgD;AzDkpMtD;;AyDxpME;EACE,oCAAmC;AzD2pMvC;;AKjpME;;;EoDLI,oCAAgD;AzD4pMtD;;AyDlqME;EACE,oCAAmC;AzDqqMvC;;AK3pME;;;EoDLI,oCAAgD;AzDsqMtD;;AyD5qME;EACE,oCAAmC;AzD+qMvC;;AKrqME;;;EoDLI,oCAAgD;AzDgrMtD;;AyDtrME;EACE,oCAAmC;AzDyrMvC;;AK/qME;;;EoDLI,oCAAgD;AzD0rMtD;;AyDhsME;EACE,oCAAmC;AzDmsMvC;;AKzrME;;;EoDLI,oCAAgD;AzDosMtD;;AyD1sME;EACE,oCAAmC;AzD6sMvC;;AKnsME;;;EoDLI,oCAAgD;AzD8sMtD;;AyDptME;EACE,oCAAmC;AzDutMvC;;AK7sME;;;EoDLI,oCAAgD;AzDwtMtD;;A0DvtMA;EACE,iCAAmC;A1D0tMrC;;A0DvtMA;EACE,wCAAwC;A1D0tM1C;;A2DruMA;EAAkB,oCAAoD;A3DyuMtE;;A2DxuMA;EAAkB,wCAAwD;A3D4uM1E;;A2D3uMA;EAAkB,0CAA0D;A3D+uM5E;;A2D9uMA;EAAkB,2CAA2D;A3DkvM7E;;A2DjvMA;EAAkB,yCAAyD;A3DqvM3E;;A2DnvMA;EAAmB,oBAAoB;A3DuvMvC;;A2DtvMA;EAAmB,wBAAwB;A3D0vM3C;;A2DzvMA;EAAmB,0BAA0B;A3D6vM7C;;A2D5vMA;EAAmB,2BAA2B;A3DgwM9C;;A2D/vMA;EAAmB,yBAAyB;A3DmwM5C;;A2DhwME;EACE,gCAA+B;A3DmwMnC;;A2DpwME;EACE,gCAA+B;A3DuwMnC;;A2DxwME;EACE,gCAA+B;A3D2wMnC;;A2D5wME;EACE,gCAA+B;A3D+wMnC;;A2DhxME;EACE,gCAA+B;A3DmxMnC;;A2DpxME;EACE,gCAA+B;A3DuxMnC;;A2DxxME;EACE,gCAA+B;A3D2xMnC;;A2D5xME;EACE,gCAA+B;A3D+xMnC;;A2D3xMA;EACE,6BAA+B;A3D8xMjC;;A2DvxMA;EACE,gCAA2C;A3D0xM7C;;A2DvxMA;EACE,iCAAwC;A3D0xM1C;;A2DvxMA;EACE,0CAAiD;EACjD,2CAAkD;A3D0xMpD;;A2DvxMA;EACE,2CAAkD;EAClD,8CAAqD;A3D0xMvD;;A2DvxMA;EACE,8CAAqD;EACrD,6CAAoD;A3D0xMtD;;A2DvxMA;EACE,0CAAiD;EACjD,6CAAoD;A3D0xMtD;;A2DvxMA;EACE,gCAA2C;A3D0xM7C;;A2DvxMA;EACE,6BAA6B;A3D0xM/B;;A2DvxMA;EACE,+BAAuC;A3D0xMzC;;A2DvxMA;EACE,2BAA2B;A3D0xM7B;;AsDl2ME;EACE,cAAc;EACd,WAAW;EACX,WAAW;AtDq2Mf;;A4D91MM;EAAwB,wBAA0B;A5Dk2MxD;;A4Dl2MM;EAAwB,0BAA0B;A5Ds2MxD;;A4Dt2MM;EAAwB,gCAA0B;A5D02MxD;;A4D12MM;EAAwB,yBAA0B;A5D82MxD;;A4D92MM;EAAwB,yBAA0B;A5Dk3MxD;;A4Dl3MM;EAAwB,6BAA0B;A5Ds3MxD;;A4Dt3MM;EAAwB,8BAA0B;A5D03MxD;;A4D13MM;EAAwB,+BAA0B;EAA1B,wBAA0B;A5D83MxD;;A4D93MM;EAAwB,sCAA0B;EAA1B,+BAA0B;A5Dk4MxD;;Acj1MI;E8CjDE;IAAwB,wBAA0B;E5Du4MtD;E4Dv4MI;IAAwB,0BAA0B;E5D04MtD;E4D14MI;IAAwB,gCAA0B;E5D64MtD;E4D74MI;IAAwB,yBAA0B;E5Dg5MtD;E4Dh5MI;IAAwB,yBAA0B;E5Dm5MtD;E4Dn5MI;IAAwB,6BAA0B;E5Ds5MtD;E4Dt5MI;IAAwB,8BAA0B;E5Dy5MtD;E4Dz5MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5D45MtD;E4D55MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5D+5MtD;AACF;;Ac/2MI;E8CjDE;IAAwB,wBAA0B;E5Dq6MtD;E4Dr6MI;IAAwB,0BAA0B;E5Dw6MtD;E4Dx6MI;IAAwB,gCAA0B;E5D26MtD;E4D36MI;IAAwB,yBAA0B;E5D86MtD;E4D96MI;IAAwB,yBAA0B;E5Di7MtD;E4Dj7MI;IAAwB,6BAA0B;E5Do7MtD;E4Dp7MI;IAAwB,8BAA0B;E5Du7MtD;E4Dv7MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5D07MtD;E4D17MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5D67MtD;AACF;;Ac74MI;E8CjDE;IAAwB,wBAA0B;E5Dm8MtD;E4Dn8MI;IAAwB,0BAA0B;E5Ds8MtD;E4Dt8MI;IAAwB,gCAA0B;E5Dy8MtD;E4Dz8MI;IAAwB,yBAA0B;E5D48MtD;E4D58MI;IAAwB,yBAA0B;E5D+8MtD;E4D/8MI;IAAwB,6BAA0B;E5Dk9MtD;E4Dl9MI;IAAwB,8BAA0B;E5Dq9MtD;E4Dr9MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5Dw9MtD;E4Dx9MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5D29MtD;AACF;;Ac36MI;E8CjDE;IAAwB,wBAA0B;E5Di+MtD;E4Dj+MI;IAAwB,0BAA0B;E5Do+MtD;E4Dp+MI;IAAwB,gCAA0B;E5Du+MtD;E4Dv+MI;IAAwB,yBAA0B;E5D0+MtD;E4D1+MI;IAAwB,yBAA0B;E5D6+MtD;E4D7+MI;IAAwB,6BAA0B;E5Dg/MtD;E4Dh/MI;IAAwB,8BAA0B;E5Dm/MtD;E4Dn/MI;IAAwB,+BAA0B;IAA1B,wBAA0B;E5Ds/MtD;E4Dt/MI;IAAwB,sCAA0B;IAA1B,+BAA0B;E5Dy/MtD;AACF;;A4Dh/MA;EAEI;IAAqB,wBAA0B;E5Dm/MjD;E4Dn/ME;IAAqB,0BAA0B;E5Ds/MjD;E4Dt/ME;IAAqB,gCAA0B;E5Dy/MjD;E4Dz/ME;IAAqB,yBAA0B;E5D4/MjD;E4D5/ME;IAAqB,yBAA0B;E5D+/MjD;E4D//ME;IAAqB,6BAA0B;E5DkgNjD;E4DlgNE;IAAqB,8BAA0B;E5DqgNjD;E4DrgNE;IAAqB,+BAA0B;IAA1B,wBAA0B;E5DwgNjD;E4DxgNE;IAAqB,sCAA0B;IAA1B,+BAA0B;E5D2gNjD;AACF;;A6DjiNA;EACE,kBAAkB;EAClB,cAAc;EACd,WAAW;EACX,UAAU;EACV,gBAAgB;A7DoiNlB;;A6DziNA;EAQI,cAAc;EACd,WAAW;A7DqiNf;;A6D9iNA;;;;;EAiBI,kBAAkB;EAClB,MAAM;EACN,SAAS;EACT,OAAO;EACP,WAAW;EACX,YAAY;EACZ,SAAS;A7DqiNb;;A6D7hNE;EAEI,uBAA4F;A7D+hNlG;;A6DjiNE;EAEI,mBAA4F;A7DmiNlG;;A6DriNE;EAEI,gBAA4F;A7DuiNlG;;A6DziNE;EAEI,iBAA4F;A7D2iNlG;;A8DpkNI;EAAgC,kCAA8B;EAA9B,8BAA8B;A9DwkNlE;;A8DvkNI;EAAgC,qCAAiC;EAAjC,iCAAiC;A9D2kNrE;;A8D1kNI;EAAgC,0CAAsC;EAAtC,sCAAsC;A9D8kN1E;;A8D7kNI;EAAgC,6CAAyC;EAAzC,yCAAyC;A9DilN7E;;A8D/kNI;EAA8B,8BAA0B;EAA1B,0BAA0B;A9DmlN5D;;A8DllNI;EAA8B,gCAA4B;EAA5B,4BAA4B;A9DslN9D;;A8DrlNI;EAA8B,sCAAkC;EAAlC,kCAAkC;A9DylNpE;;A8DxlNI;EAA8B,6BAAyB;EAAzB,yBAAyB;A9D4lN3D;;A8D3lNI;EAA8B,+BAAuB;EAAvB,uBAAuB;A9D+lNzD;;A8D9lNI;EAA8B,+BAAuB;EAAvB,uBAAuB;A9DkmNzD;;A8DjmNI;EAA8B,+BAAyB;EAAzB,yBAAyB;A9DqmN3D;;A8DpmNI;EAA8B,+BAAyB;EAAzB,yBAAyB;A9DwmN3D;;A8DtmNI;EAAoC,+BAAsC;EAAtC,sCAAsC;A9D0mN9E;;A8DzmNI;EAAoC,6BAAoC;EAApC,oCAAoC;A9D6mN5E;;A8D5mNI;EAAoC,gCAAkC;EAAlC,kCAAkC;A9DgnN1E;;A8D/mNI;EAAoC,iCAAyC;EAAzC,yCAAyC;A9DmnNjF;;A8DlnNI;EAAoC,oCAAwC;EAAxC,wCAAwC;A9DsnNhF;;A8DpnNI;EAAiC,gCAAkC;EAAlC,kCAAkC;A9DwnNvE;;A8DvnNI;EAAiC,8BAAgC;EAAhC,gCAAgC;A9D2nNrE;;A8D1nNI;EAAiC,iCAA8B;EAA9B,8BAA8B;A9D8nNnE;;A8D7nNI;EAAiC,mCAAgC;EAAhC,gCAAgC;A9DioNrE;;A8DhoNI;EAAiC,kCAA+B;EAA/B,+BAA+B;A9DooNpE;;A8DloNI;EAAkC,oCAAoC;EAApC,oCAAoC;A9DsoN1E;;A8DroNI;EAAkC,kCAAkC;EAAlC,kCAAkC;A9DyoNxE;;A8DxoNI;EAAkC,qCAAgC;EAAhC,gCAAgC;A9D4oNtE;;A8D3oNI;EAAkC,sCAAuC;EAAvC,uCAAuC;A9D+oN7E;;A8D9oNI;EAAkC,yCAAsC;EAAtC,sCAAsC;A9DkpN5E;;A8DjpNI;EAAkC,sCAAiC;EAAjC,iCAAiC;A9DqpNvE;;A8DnpNI;EAAgC,oCAA2B;EAA3B,2BAA2B;A9DupN/D;;A8DtpNI;EAAgC,qCAAiC;EAAjC,iCAAiC;A9D0pNrE;;A8DzpNI;EAAgC,mCAA+B;EAA/B,+BAA+B;A9D6pNnE;;A8D5pNI;EAAgC,sCAA6B;EAA7B,6BAA6B;A9DgqNjE;;A8D/pNI;EAAgC,wCAA+B;EAA/B,+BAA+B;A9DmqNnE;;A8DlqNI;EAAgC,uCAA8B;EAA9B,8BAA8B;A9DsqNlE;;Ac1pNI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9DitNhE;E8DhtNE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9DmtNnE;E8DltNE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9DqtNxE;E8DptNE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9DutN3E;E8DrtNE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9DwtN1D;E8DvtNE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9D0tN5D;E8DztNE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9D4tNlE;E8D3tNE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9D8tNzD;E8D7tNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DguNvD;E8D/tNE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9DkuNvD;E8DjuNE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9DouNzD;E8DnuNE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9DsuNzD;E8DpuNE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9DuuN5E;E8DtuNE;IAAoC,6BAAoC;IAApC,oCAAoC;E9DyuN1E;E8DxuNE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9D2uNxE;E8D1uNE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9D6uN/E;E8D5uNE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9D+uN9E;E8D7uNE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9DgvNrE;E8D/uNE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9DkvNnE;E8DjvNE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9DovNjE;E8DnvNE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9DsvNnE;E8DrvNE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9DwvNlE;E8DtvNE;IAAkC,oCAAoC;IAApC,oCAAoC;E9DyvNxE;E8DxvNE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9D2vNtE;E8D1vNE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9D6vNpE;E8D5vNE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9D+vN3E;E8D9vNE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9DiwN1E;E8DhwNE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9DmwNrE;E8DjwNE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9DowN7D;E8DnwNE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9DswNnE;E8DrwNE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9DwwNjE;E8DvwNE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9D0wN/D;E8DzwNE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9D4wNjE;E8D3wNE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9D8wNhE;AACF;;AcnwNI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9D0zNhE;E8DzzNE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D4zNnE;E8D3zNE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9D8zNxE;E8D7zNE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9Dg0N3E;E8D9zNE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9Di0N1D;E8Dh0NE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9Dm0N5D;E8Dl0NE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9Dq0NlE;E8Dp0NE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9Du0NzD;E8Dt0NE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9Dy0NvD;E8Dx0NE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9D20NvD;E8D10NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D60NzD;E8D50NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D+0NzD;E8D70NE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9Dg1N5E;E8D/0NE;IAAoC,6BAAoC;IAApC,oCAAoC;E9Dk1N1E;E8Dj1NE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9Do1NxE;E8Dn1NE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9Ds1N/E;E8Dr1NE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9Dw1N9E;E8Dt1NE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9Dy1NrE;E8Dx1NE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9D21NnE;E8D11NE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9D61NjE;E8D51NE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9D+1NnE;E8D91NE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9Di2NlE;E8D/1NE;IAAkC,oCAAoC;IAApC,oCAAoC;E9Dk2NxE;E8Dj2NE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9Do2NtE;E8Dn2NE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9Ds2NpE;E8Dr2NE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9Dw2N3E;E8Dv2NE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9D02N1E;E8Dz2NE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9D42NrE;E8D12NE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9D62N7D;E8D52NE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D+2NnE;E8D92NE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9Di3NjE;E8Dh3NE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9Dm3N/D;E8Dl3NE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9Dq3NjE;E8Dp3NE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9Du3NhE;AACF;;Ac52NI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9Dm6NhE;E8Dl6NE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9Dq6NnE;E8Dp6NE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9Du6NxE;E8Dt6NE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9Dy6N3E;E8Dv6NE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9D06N1D;E8Dz6NE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9D46N5D;E8D36NE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9D86NlE;E8D76NE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9Dg7NzD;E8D/6NE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9Dk7NvD;E8Dj7NE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9Do7NvD;E8Dn7NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9Ds7NzD;E8Dr7NE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9Dw7NzD;E8Dt7NE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9Dy7N5E;E8Dx7NE;IAAoC,6BAAoC;IAApC,oCAAoC;E9D27N1E;E8D17NE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9D67NxE;E8D57NE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9D+7N/E;E8D97NE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9Di8N9E;E8D/7NE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9Dk8NrE;E8Dj8NE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9Do8NnE;E8Dn8NE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9Ds8NjE;E8Dr8NE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9Dw8NnE;E8Dv8NE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9D08NlE;E8Dx8NE;IAAkC,oCAAoC;IAApC,oCAAoC;E9D28NxE;E8D18NE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9D68NtE;E8D58NE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9D+8NpE;E8D98NE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9Di9N3E;E8Dh9NE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9Dm9N1E;E8Dl9NE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9Dq9NrE;E8Dn9NE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9Ds9N7D;E8Dr9NE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9Dw9NnE;E8Dv9NE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9D09NjE;E8Dz9NE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9D49N/D;E8D39NE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9D89NjE;E8D79NE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9Dg+NhE;AACF;;Acr9NI;EgDlDA;IAAgC,kCAA8B;IAA9B,8BAA8B;E9D4gOhE;E8D3gOE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9D8gOnE;E8D7gOE;IAAgC,0CAAsC;IAAtC,sCAAsC;E9DghOxE;E8D/gOE;IAAgC,6CAAyC;IAAzC,yCAAyC;E9DkhO3E;E8DhhOE;IAA8B,8BAA0B;IAA1B,0BAA0B;E9DmhO1D;E8DlhOE;IAA8B,gCAA4B;IAA5B,4BAA4B;E9DqhO5D;E8DphOE;IAA8B,sCAAkC;IAAlC,kCAAkC;E9DuhOlE;E8DthOE;IAA8B,6BAAyB;IAAzB,yBAAyB;E9DyhOzD;E8DxhOE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9D2hOvD;E8D1hOE;IAA8B,+BAAuB;IAAvB,uBAAuB;E9D6hOvD;E8D5hOE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9D+hOzD;E8D9hOE;IAA8B,+BAAyB;IAAzB,yBAAyB;E9DiiOzD;E8D/hOE;IAAoC,+BAAsC;IAAtC,sCAAsC;E9DkiO5E;E8DjiOE;IAAoC,6BAAoC;IAApC,oCAAoC;E9DoiO1E;E8DniOE;IAAoC,gCAAkC;IAAlC,kCAAkC;E9DsiOxE;E8DriOE;IAAoC,iCAAyC;IAAzC,yCAAyC;E9DwiO/E;E8DviOE;IAAoC,oCAAwC;IAAxC,wCAAwC;E9D0iO9E;E8DxiOE;IAAiC,gCAAkC;IAAlC,kCAAkC;E9D2iOrE;E8D1iOE;IAAiC,8BAAgC;IAAhC,gCAAgC;E9D6iOnE;E8D5iOE;IAAiC,iCAA8B;IAA9B,8BAA8B;E9D+iOjE;E8D9iOE;IAAiC,mCAAgC;IAAhC,gCAAgC;E9DijOnE;E8DhjOE;IAAiC,kCAA+B;IAA/B,+BAA+B;E9DmjOlE;E8DjjOE;IAAkC,oCAAoC;IAApC,oCAAoC;E9DojOxE;E8DnjOE;IAAkC,kCAAkC;IAAlC,kCAAkC;E9DsjOtE;E8DrjOE;IAAkC,qCAAgC;IAAhC,gCAAgC;E9DwjOpE;E8DvjOE;IAAkC,sCAAuC;IAAvC,uCAAuC;E9D0jO3E;E8DzjOE;IAAkC,yCAAsC;IAAtC,sCAAsC;E9D4jO1E;E8D3jOE;IAAkC,sCAAiC;IAAjC,iCAAiC;E9D8jOrE;E8D5jOE;IAAgC,oCAA2B;IAA3B,2BAA2B;E9D+jO7D;E8D9jOE;IAAgC,qCAAiC;IAAjC,iCAAiC;E9DikOnE;E8DhkOE;IAAgC,mCAA+B;IAA/B,+BAA+B;E9DmkOjE;E8DlkOE;IAAgC,sCAA6B;IAA7B,6BAA6B;E9DqkO/D;E8DpkOE;IAAgC,wCAA+B;IAA/B,+BAA+B;E9DukOjE;E8DtkOE;IAAgC,uCAA8B;IAA9B,8BAA8B;E9DykOhE;AACF;;A+DpnOI;EAAwB,sBAAsB;A/DwnOlD;;A+DvnOI;EAAwB,uBAAuB;A/D2nOnD;;A+D1nOI;EAAwB,sBAAsB;A/D8nOlD;;Ac1kOI;EiDtDA;IAAwB,sBAAsB;E/DqoOhD;E+DpoOE;IAAwB,uBAAuB;E/DuoOjD;E+DtoOE;IAAwB,sBAAsB;E/DyoOhD;AACF;;ActlOI;EiDtDA;IAAwB,sBAAsB;E/DipOhD;E+DhpOE;IAAwB,uBAAuB;E/DmpOjD;E+DlpOE;IAAwB,sBAAsB;E/DqpOhD;AACF;;AclmOI;EiDtDA;IAAwB,sBAAsB;E/D6pOhD;E+D5pOE;IAAwB,uBAAuB;E/D+pOjD;E+D9pOE;IAAwB,sBAAsB;E/DiqOhD;AACF;;Ac9mOI;EiDtDA;IAAwB,sBAAsB;E/DyqOhD;E+DxqOE;IAAwB,uBAAuB;E/D2qOjD;E+D1qOE;IAAwB,sBAAsB;E/D6qOhD;AACF;;AgEnrOE;EAAyB,mCAA8B;EAA9B,gCAA8B;EAA9B,+BAA8B;EAA9B,2BAA8B;AhEurOzD;;AgEvrOE;EAAyB,oCAA8B;EAA9B,iCAA8B;EAA9B,gCAA8B;EAA9B,4BAA8B;AhE2rOzD;;AgE3rOE;EAAyB,oCAA8B;EAA9B,iCAA8B;EAA9B,gCAA8B;EAA9B,4BAA8B;AhE+rOzD;;AiE/rOE;EAAsB,yBAA2B;AjEmsOnD;;AiEnsOE;EAAsB,2BAA2B;AjEusOnD;;AkEtsOE;EAAyB,2BAA8B;AlE0sOzD;;AkE1sOE;EAAyB,6BAA8B;AlE8sOzD;;AkE9sOE;EAAyB,6BAA8B;AlEktOzD;;AkEltOE;EAAyB,0BAA8B;AlEstOzD;;AkEttOE;EAAyB,mCAA8B;EAA9B,2BAA8B;AlE0tOzD;;AkErtOA;EACE,eAAe;EACf,MAAM;EACN,QAAQ;EACR,OAAO;EACP,a/DgqBsC;AHwjNxC;;AkErtOA;EACE,eAAe;EACf,QAAQ;EACR,SAAS;EACT,OAAO;EACP,a/DwpBsC;AHgkNxC;;AkEptO8B;EAD9B;IAEI,wBAAgB;IAAhB,gBAAgB;IAChB,MAAM;IACN,a/DgpBoC;EHwkNtC;AACF;;AmElvOA;ECEE,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,UAAU;EACV,YAAY;EACZ,gBAAgB;EAChB,sBAAsB;EACtB,mBAAmB;EACnB,SAAS;ApEovOX;;AoE1uOE;EAEE,gBAAgB;EAChB,WAAW;EACX,YAAY;EACZ,iBAAiB;EACjB,UAAU;EACV,mBAAmB;ApE4uOvB;;AqEzwOA;EAAa,8DAAqC;ArE6wOlD;;AqE5wOA;EAAU,wDAAkC;ArEgxO5C;;AqE/wOA;EAAa,uDAAqC;ArEmxOlD;;AqElxOA;EAAe,2BAA2B;ArEsxO1C;;AsErxOI;EAAuB,qBAA4B;AtEyxOvD;;AsEzxOI;EAAuB,qBAA4B;AtE6xOvD;;AsE7xOI;EAAuB,qBAA4B;AtEiyOvD;;AsEjyOI;EAAuB,sBAA4B;AtEqyOvD;;AsEryOI;EAAuB,sBAA4B;AtEyyOvD;;AsEzyOI;EAAuB,sBAA4B;AtE6yOvD;;AsE7yOI;EAAuB,sBAA4B;AtEizOvD;;AsEjzOI;EAAuB,sBAA4B;AtEqzOvD;;AsErzOI;EAAuB,uBAA4B;AtEyzOvD;;AsEzzOI;EAAuB,uBAA4B;AtE6zOvD;;AsEzzOA;EAAU,0BAA0B;AtE6zOpC;;AsE5zOA;EAAU,2BAA2B;AtEg0OrC;;AsE5zOA;EAAc,2BAA2B;AtEg0OzC;;AsE/zOA;EAAc,4BAA4B;AtEm0O1C;;AsEj0OA;EAAU,uBAAuB;AtEq0OjC;;AsEp0OA;EAAU,wBAAwB;AtEw0OlC;;AuEj1OQ;EAAgC,oBAA4B;AvEq1OpE;;AuEp1OQ;;EAEE,wBAAoC;AvEu1O9C;;AuEr1OQ;;EAEE,0BAAwC;AvEw1OlD;;AuEt1OQ;;EAEE,2BAA0C;AvEy1OpD;;AuEv1OQ;;EAEE,yBAAsC;AvE01OhD;;AuEz2OQ;EAAgC,0BAA4B;AvE62OpE;;AuE52OQ;;EAEE,8BAAoC;AvE+2O9C;;AuE72OQ;;EAEE,gCAAwC;AvEg3OlD;;AuE92OQ;;EAEE,iCAA0C;AvEi3OpD;;AuE/2OQ;;EAEE,+BAAsC;AvEk3OhD;;AuEj4OQ;EAAgC,yBAA4B;AvEq4OpE;;AuEp4OQ;;EAEE,6BAAoC;AvEu4O9C;;AuEr4OQ;;EAEE,+BAAwC;AvEw4OlD;;AuEt4OQ;;EAEE,gCAA0C;AvEy4OpD;;AuEv4OQ;;EAEE,8BAAsC;AvE04OhD;;AuEz5OQ;EAAgC,uBAA4B;AvE65OpE;;AuE55OQ;;EAEE,2BAAoC;AvE+5O9C;;AuE75OQ;;EAEE,6BAAwC;AvEg6OlD;;AuE95OQ;;EAEE,8BAA0C;AvEi6OpD;;AuE/5OQ;;EAEE,4BAAsC;AvEk6OhD;;AuEj7OQ;EAAgC,yBAA4B;AvEq7OpE;;AuEp7OQ;;EAEE,6BAAoC;AvEu7O9C;;AuEr7OQ;;EAEE,+BAAwC;AvEw7OlD;;AuEt7OQ;;EAEE,gCAA0C;AvEy7OpD;;AuEv7OQ;;EAEE,8BAAsC;AvE07OhD;;AuEz8OQ;EAAgC,uBAA4B;AvE68OpE;;AuE58OQ;;EAEE,2BAAoC;AvE+8O9C;;AuE78OQ;;EAEE,6BAAwC;AvEg9OlD;;AuE98OQ;;EAEE,8BAA0C;AvEi9OpD;;AuE/8OQ;;EAEE,4BAAsC;AvEk9OhD;;AuEj+OQ;EAAgC,qBAA4B;AvEq+OpE;;AuEp+OQ;;EAEE,yBAAoC;AvEu+O9C;;AuEr+OQ;;EAEE,2BAAwC;AvEw+OlD;;AuEt+OQ;;EAEE,4BAA0C;AvEy+OpD;;AuEv+OQ;;EAEE,0BAAsC;AvE0+OhD;;AuEz/OQ;EAAgC,2BAA4B;AvE6/OpE;;AuE5/OQ;;EAEE,+BAAoC;AvE+/O9C;;AuE7/OQ;;EAEE,iCAAwC;AvEggPlD;;AuE9/OQ;;EAEE,kCAA0C;AvEigPpD;;AuE//OQ;;EAEE,gCAAsC;AvEkgPhD;;AuEjhPQ;EAAgC,0BAA4B;AvEqhPpE;;AuEphPQ;;EAEE,8BAAoC;AvEuhP9C;;AuErhPQ;;EAEE,gCAAwC;AvEwhPlD;;AuEthPQ;;EAEE,iCAA0C;AvEyhPpD;;AuEvhPQ;;EAEE,+BAAsC;AvE0hPhD;;AuEziPQ;EAAgC,wBAA4B;AvE6iPpE;;AuE5iPQ;;EAEE,4BAAoC;AvE+iP9C;;AuE7iPQ;;EAEE,8BAAwC;AvEgjPlD;;AuE9iPQ;;EAEE,+BAA0C;AvEijPpD;;AuE/iPQ;;EAEE,6BAAsC;AvEkjPhD;;AuEjkPQ;EAAgC,0BAA4B;AvEqkPpE;;AuEpkPQ;;EAEE,8BAAoC;AvEukP9C;;AuErkPQ;;EAEE,gCAAwC;AvEwkPlD;;AuEtkPQ;;EAEE,iCAA0C;AvEykPpD;;AuEvkPQ;;EAEE,+BAAsC;AvE0kPhD;;AuEzlPQ;EAAgC,wBAA4B;AvE6lPpE;;AuE5lPQ;;EAEE,4BAAoC;AvE+lP9C;;AuE7lPQ;;EAEE,8BAAwC;AvEgmPlD;;AuE9lPQ;;EAEE,+BAA0C;AvEimPpD;;AuE/lPQ;;EAEE,6BAAsC;AvEkmPhD;;AuE1lPQ;EAAwB,2BAA2B;AvE8lP3D;;AuE7lPQ;;EAEE,+BAA+B;AvEgmPzC;;AuE9lPQ;;EAEE,iCAAiC;AvEimP3C;;AuE/lPQ;;EAEE,kCAAkC;AvEkmP5C;;AuEhmPQ;;EAEE,gCAAgC;AvEmmP1C;;AuElnPQ;EAAwB,0BAA2B;AvEsnP3D;;AuErnPQ;;EAEE,8BAA+B;AvEwnPzC;;AuEtnPQ;;EAEE,gCAAiC;AvEynP3C;;AuEvnPQ;;EAEE,iCAAkC;AvE0nP5C;;AuExnPQ;;EAEE,+BAAgC;AvE2nP1C;;AuE1oPQ;EAAwB,wBAA2B;AvE8oP3D;;AuE7oPQ;;EAEE,4BAA+B;AvEgpPzC;;AuE9oPQ;;EAEE,8BAAiC;AvEipP3C;;AuE/oPQ;;EAEE,+BAAkC;AvEkpP5C;;AuEhpPQ;;EAEE,6BAAgC;AvEmpP1C;;AuElqPQ;EAAwB,0BAA2B;AvEsqP3D;;AuErqPQ;;EAEE,8BAA+B;AvEwqPzC;;AuEtqPQ;;EAEE,gCAAiC;AvEyqP3C;;AuEvqPQ;;EAEE,iCAAkC;AvE0qP5C;;AuExqPQ;;EAEE,+BAAgC;AvE2qP1C;;AuE1rPQ;EAAwB,wBAA2B;AvE8rP3D;;AuE7rPQ;;EAEE,4BAA+B;AvEgsPzC;;AuE9rPQ;;EAEE,8BAAiC;AvEisP3C;;AuE/rPQ;;EAEE,+BAAkC;AvEksP5C;;AuEhsPQ;;EAEE,6BAAgC;AvEmsP1C;;AuE7rPI;EAAmB,uBAAuB;AvEisP9C;;AuEhsPI;;EAEE,2BAA2B;AvEmsPjC;;AuEjsPI;;EAEE,6BAA6B;AvEosPnC;;AuElsPI;;EAEE,8BAA8B;AvEqsPpC;;AuEnsPI;;EAEE,4BAA4B;AvEssPlC;;Ac/sPI;EyDlDI;IAAgC,oBAA4B;EvEswPlE;EuErwPM;;IAEE,wBAAoC;EvEuwP5C;EuErwPM;;IAEE,0BAAwC;EvEuwPhD;EuErwPM;;IAEE,2BAA0C;EvEuwPlD;EuErwPM;;IAEE,yBAAsC;EvEuwP9C;EuEtxPM;IAAgC,0BAA4B;EvEyxPlE;EuExxPM;;IAEE,8BAAoC;EvE0xP5C;EuExxPM;;IAEE,gCAAwC;EvE0xPhD;EuExxPM;;IAEE,iCAA0C;EvE0xPlD;EuExxPM;;IAEE,+BAAsC;EvE0xP9C;EuEzyPM;IAAgC,yBAA4B;EvE4yPlE;EuE3yPM;;IAEE,6BAAoC;EvE6yP5C;EuE3yPM;;IAEE,+BAAwC;EvE6yPhD;EuE3yPM;;IAEE,gCAA0C;EvE6yPlD;EuE3yPM;;IAEE,8BAAsC;EvE6yP9C;EuE5zPM;IAAgC,uBAA4B;EvE+zPlE;EuE9zPM;;IAEE,2BAAoC;EvEg0P5C;EuE9zPM;;IAEE,6BAAwC;EvEg0PhD;EuE9zPM;;IAEE,8BAA0C;EvEg0PlD;EuE9zPM;;IAEE,4BAAsC;EvEg0P9C;EuE/0PM;IAAgC,yBAA4B;EvEk1PlE;EuEj1PM;;IAEE,6BAAoC;EvEm1P5C;EuEj1PM;;IAEE,+BAAwC;EvEm1PhD;EuEj1PM;;IAEE,gCAA0C;EvEm1PlD;EuEj1PM;;IAEE,8BAAsC;EvEm1P9C;EuEl2PM;IAAgC,uBAA4B;EvEq2PlE;EuEp2PM;;IAEE,2BAAoC;EvEs2P5C;EuEp2PM;;IAEE,6BAAwC;EvEs2PhD;EuEp2PM;;IAEE,8BAA0C;EvEs2PlD;EuEp2PM;;IAEE,4BAAsC;EvEs2P9C;EuEr3PM;IAAgC,qBAA4B;EvEw3PlE;EuEv3PM;;IAEE,yBAAoC;EvEy3P5C;EuEv3PM;;IAEE,2BAAwC;EvEy3PhD;EuEv3PM;;IAEE,4BAA0C;EvEy3PlD;EuEv3PM;;IAEE,0BAAsC;EvEy3P9C;EuEx4PM;IAAgC,2BAA4B;EvE24PlE;EuE14PM;;IAEE,+BAAoC;EvE44P5C;EuE14PM;;IAEE,iCAAwC;EvE44PhD;EuE14PM;;IAEE,kCAA0C;EvE44PlD;EuE14PM;;IAEE,gCAAsC;EvE44P9C;EuE35PM;IAAgC,0BAA4B;EvE85PlE;EuE75PM;;IAEE,8BAAoC;EvE+5P5C;EuE75PM;;IAEE,gCAAwC;EvE+5PhD;EuE75PM;;IAEE,iCAA0C;EvE+5PlD;EuE75PM;;IAEE,+BAAsC;EvE+5P9C;EuE96PM;IAAgC,wBAA4B;EvEi7PlE;EuEh7PM;;IAEE,4BAAoC;EvEk7P5C;EuEh7PM;;IAEE,8BAAwC;EvEk7PhD;EuEh7PM;;IAEE,+BAA0C;EvEk7PlD;EuEh7PM;;IAEE,6BAAsC;EvEk7P9C;EuEj8PM;IAAgC,0BAA4B;EvEo8PlE;EuEn8PM;;IAEE,8BAAoC;EvEq8P5C;EuEn8PM;;IAEE,gCAAwC;EvEq8PhD;EuEn8PM;;IAEE,iCAA0C;EvEq8PlD;EuEn8PM;;IAEE,+BAAsC;EvEq8P9C;EuEp9PM;IAAgC,wBAA4B;EvEu9PlE;EuEt9PM;;IAEE,4BAAoC;EvEw9P5C;EuEt9PM;;IAEE,8BAAwC;EvEw9PhD;EuEt9PM;;IAEE,+BAA0C;EvEw9PlD;EuEt9PM;;IAEE,6BAAsC;EvEw9P9C;EuEh9PM;IAAwB,2BAA2B;EvEm9PzD;EuEl9PM;;IAEE,+BAA+B;EvEo9PvC;EuEl9PM;;IAEE,iCAAiC;EvEo9PzC;EuEl9PM;;IAEE,kCAAkC;EvEo9P1C;EuEl9PM;;IAEE,gCAAgC;EvEo9PxC;EuEn+PM;IAAwB,0BAA2B;EvEs+PzD;EuEr+PM;;IAEE,8BAA+B;EvEu+PvC;EuEr+PM;;IAEE,gCAAiC;EvEu+PzC;EuEr+PM;;IAEE,iCAAkC;EvEu+P1C;EuEr+PM;;IAEE,+BAAgC;EvEu+PxC;EuEt/PM;IAAwB,wBAA2B;EvEy/PzD;EuEx/PM;;IAEE,4BAA+B;EvE0/PvC;EuEx/PM;;IAEE,8BAAiC;EvE0/PzC;EuEx/PM;;IAEE,+BAAkC;EvE0/P1C;EuEx/PM;;IAEE,6BAAgC;EvE0/PxC;EuEzgQM;IAAwB,0BAA2B;EvE4gQzD;EuE3gQM;;IAEE,8BAA+B;EvE6gQvC;EuE3gQM;;IAEE,gCAAiC;EvE6gQzC;EuE3gQM;;IAEE,iCAAkC;EvE6gQ1C;EuE3gQM;;IAEE,+BAAgC;EvE6gQxC;EuE5hQM;IAAwB,wBAA2B;EvE+hQzD;EuE9hQM;;IAEE,4BAA+B;EvEgiQvC;EuE9hQM;;IAEE,8BAAiC;EvEgiQzC;EuE9hQM;;IAEE,+BAAkC;EvEgiQ1C;EuE9hQM;;IAEE,6BAAgC;EvEgiQxC;EuE1hQE;IAAmB,uBAAuB;EvE6hQ5C;EuE5hQE;;IAEE,2BAA2B;EvE8hQ/B;EuE5hQE;;IAEE,6BAA6B;EvE8hQjC;EuE5hQE;;IAEE,8BAA8B;EvE8hQlC;EuE5hQE;;IAEE,4BAA4B;EvE8hQhC;AACF;;AcxiQI;EyDlDI;IAAgC,oBAA4B;EvE+lQlE;EuE9lQM;;IAEE,wBAAoC;EvEgmQ5C;EuE9lQM;;IAEE,0BAAwC;EvEgmQhD;EuE9lQM;;IAEE,2BAA0C;EvEgmQlD;EuE9lQM;;IAEE,yBAAsC;EvEgmQ9C;EuE/mQM;IAAgC,0BAA4B;EvEknQlE;EuEjnQM;;IAEE,8BAAoC;EvEmnQ5C;EuEjnQM;;IAEE,gCAAwC;EvEmnQhD;EuEjnQM;;IAEE,iCAA0C;EvEmnQlD;EuEjnQM;;IAEE,+BAAsC;EvEmnQ9C;EuEloQM;IAAgC,yBAA4B;EvEqoQlE;EuEpoQM;;IAEE,6BAAoC;EvEsoQ5C;EuEpoQM;;IAEE,+BAAwC;EvEsoQhD;EuEpoQM;;IAEE,gCAA0C;EvEsoQlD;EuEpoQM;;IAEE,8BAAsC;EvEsoQ9C;EuErpQM;IAAgC,uBAA4B;EvEwpQlE;EuEvpQM;;IAEE,2BAAoC;EvEypQ5C;EuEvpQM;;IAEE,6BAAwC;EvEypQhD;EuEvpQM;;IAEE,8BAA0C;EvEypQlD;EuEvpQM;;IAEE,4BAAsC;EvEypQ9C;EuExqQM;IAAgC,yBAA4B;EvE2qQlE;EuE1qQM;;IAEE,6BAAoC;EvE4qQ5C;EuE1qQM;;IAEE,+BAAwC;EvE4qQhD;EuE1qQM;;IAEE,gCAA0C;EvE4qQlD;EuE1qQM;;IAEE,8BAAsC;EvE4qQ9C;EuE3rQM;IAAgC,uBAA4B;EvE8rQlE;EuE7rQM;;IAEE,2BAAoC;EvE+rQ5C;EuE7rQM;;IAEE,6BAAwC;EvE+rQhD;EuE7rQM;;IAEE,8BAA0C;EvE+rQlD;EuE7rQM;;IAEE,4BAAsC;EvE+rQ9C;EuE9sQM;IAAgC,qBAA4B;EvEitQlE;EuEhtQM;;IAEE,yBAAoC;EvEktQ5C;EuEhtQM;;IAEE,2BAAwC;EvEktQhD;EuEhtQM;;IAEE,4BAA0C;EvEktQlD;EuEhtQM;;IAEE,0BAAsC;EvEktQ9C;EuEjuQM;IAAgC,2BAA4B;EvEouQlE;EuEnuQM;;IAEE,+BAAoC;EvEquQ5C;EuEnuQM;;IAEE,iCAAwC;EvEquQhD;EuEnuQM;;IAEE,kCAA0C;EvEquQlD;EuEnuQM;;IAEE,gCAAsC;EvEquQ9C;EuEpvQM;IAAgC,0BAA4B;EvEuvQlE;EuEtvQM;;IAEE,8BAAoC;EvEwvQ5C;EuEtvQM;;IAEE,gCAAwC;EvEwvQhD;EuEtvQM;;IAEE,iCAA0C;EvEwvQlD;EuEtvQM;;IAEE,+BAAsC;EvEwvQ9C;EuEvwQM;IAAgC,wBAA4B;EvE0wQlE;EuEzwQM;;IAEE,4BAAoC;EvE2wQ5C;EuEzwQM;;IAEE,8BAAwC;EvE2wQhD;EuEzwQM;;IAEE,+BAA0C;EvE2wQlD;EuEzwQM;;IAEE,6BAAsC;EvE2wQ9C;EuE1xQM;IAAgC,0BAA4B;EvE6xQlE;EuE5xQM;;IAEE,8BAAoC;EvE8xQ5C;EuE5xQM;;IAEE,gCAAwC;EvE8xQhD;EuE5xQM;;IAEE,iCAA0C;EvE8xQlD;EuE5xQM;;IAEE,+BAAsC;EvE8xQ9C;EuE7yQM;IAAgC,wBAA4B;EvEgzQlE;EuE/yQM;;IAEE,4BAAoC;EvEizQ5C;EuE/yQM;;IAEE,8BAAwC;EvEizQhD;EuE/yQM;;IAEE,+BAA0C;EvEizQlD;EuE/yQM;;IAEE,6BAAsC;EvEizQ9C;EuEzyQM;IAAwB,2BAA2B;EvE4yQzD;EuE3yQM;;IAEE,+BAA+B;EvE6yQvC;EuE3yQM;;IAEE,iCAAiC;EvE6yQzC;EuE3yQM;;IAEE,kCAAkC;EvE6yQ1C;EuE3yQM;;IAEE,gCAAgC;EvE6yQxC;EuE5zQM;IAAwB,0BAA2B;EvE+zQzD;EuE9zQM;;IAEE,8BAA+B;EvEg0QvC;EuE9zQM;;IAEE,gCAAiC;EvEg0QzC;EuE9zQM;;IAEE,iCAAkC;EvEg0Q1C;EuE9zQM;;IAEE,+BAAgC;EvEg0QxC;EuE/0QM;IAAwB,wBAA2B;EvEk1QzD;EuEj1QM;;IAEE,4BAA+B;EvEm1QvC;EuEj1QM;;IAEE,8BAAiC;EvEm1QzC;EuEj1QM;;IAEE,+BAAkC;EvEm1Q1C;EuEj1QM;;IAEE,6BAAgC;EvEm1QxC;EuEl2QM;IAAwB,0BAA2B;EvEq2QzD;EuEp2QM;;IAEE,8BAA+B;EvEs2QvC;EuEp2QM;;IAEE,gCAAiC;EvEs2QzC;EuEp2QM;;IAEE,iCAAkC;EvEs2Q1C;EuEp2QM;;IAEE,+BAAgC;EvEs2QxC;EuEr3QM;IAAwB,wBAA2B;EvEw3QzD;EuEv3QM;;IAEE,4BAA+B;EvEy3QvC;EuEv3QM;;IAEE,8BAAiC;EvEy3QzC;EuEv3QM;;IAEE,+BAAkC;EvEy3Q1C;EuEv3QM;;IAEE,6BAAgC;EvEy3QxC;EuEn3QE;IAAmB,uBAAuB;EvEs3Q5C;EuEr3QE;;IAEE,2BAA2B;EvEu3Q/B;EuEr3QE;;IAEE,6BAA6B;EvEu3QjC;EuEr3QE;;IAEE,8BAA8B;EvEu3QlC;EuEr3QE;;IAEE,4BAA4B;EvEu3QhC;AACF;;Acj4QI;EyDlDI;IAAgC,oBAA4B;EvEw7QlE;EuEv7QM;;IAEE,wBAAoC;EvEy7Q5C;EuEv7QM;;IAEE,0BAAwC;EvEy7QhD;EuEv7QM;;IAEE,2BAA0C;EvEy7QlD;EuEv7QM;;IAEE,yBAAsC;EvEy7Q9C;EuEx8QM;IAAgC,0BAA4B;EvE28QlE;EuE18QM;;IAEE,8BAAoC;EvE48Q5C;EuE18QM;;IAEE,gCAAwC;EvE48QhD;EuE18QM;;IAEE,iCAA0C;EvE48QlD;EuE18QM;;IAEE,+BAAsC;EvE48Q9C;EuE39QM;IAAgC,yBAA4B;EvE89QlE;EuE79QM;;IAEE,6BAAoC;EvE+9Q5C;EuE79QM;;IAEE,+BAAwC;EvE+9QhD;EuE79QM;;IAEE,gCAA0C;EvE+9QlD;EuE79QM;;IAEE,8BAAsC;EvE+9Q9C;EuE9+QM;IAAgC,uBAA4B;EvEi/QlE;EuEh/QM;;IAEE,2BAAoC;EvEk/Q5C;EuEh/QM;;IAEE,6BAAwC;EvEk/QhD;EuEh/QM;;IAEE,8BAA0C;EvEk/QlD;EuEh/QM;;IAEE,4BAAsC;EvEk/Q9C;EuEjgRM;IAAgC,yBAA4B;EvEogRlE;EuEngRM;;IAEE,6BAAoC;EvEqgR5C;EuEngRM;;IAEE,+BAAwC;EvEqgRhD;EuEngRM;;IAEE,gCAA0C;EvEqgRlD;EuEngRM;;IAEE,8BAAsC;EvEqgR9C;EuEphRM;IAAgC,uBAA4B;EvEuhRlE;EuEthRM;;IAEE,2BAAoC;EvEwhR5C;EuEthRM;;IAEE,6BAAwC;EvEwhRhD;EuEthRM;;IAEE,8BAA0C;EvEwhRlD;EuEthRM;;IAEE,4BAAsC;EvEwhR9C;EuEviRM;IAAgC,qBAA4B;EvE0iRlE;EuEziRM;;IAEE,yBAAoC;EvE2iR5C;EuEziRM;;IAEE,2BAAwC;EvE2iRhD;EuEziRM;;IAEE,4BAA0C;EvE2iRlD;EuEziRM;;IAEE,0BAAsC;EvE2iR9C;EuE1jRM;IAAgC,2BAA4B;EvE6jRlE;EuE5jRM;;IAEE,+BAAoC;EvE8jR5C;EuE5jRM;;IAEE,iCAAwC;EvE8jRhD;EuE5jRM;;IAEE,kCAA0C;EvE8jRlD;EuE5jRM;;IAEE,gCAAsC;EvE8jR9C;EuE7kRM;IAAgC,0BAA4B;EvEglRlE;EuE/kRM;;IAEE,8BAAoC;EvEilR5C;EuE/kRM;;IAEE,gCAAwC;EvEilRhD;EuE/kRM;;IAEE,iCAA0C;EvEilRlD;EuE/kRM;;IAEE,+BAAsC;EvEilR9C;EuEhmRM;IAAgC,wBAA4B;EvEmmRlE;EuElmRM;;IAEE,4BAAoC;EvEomR5C;EuElmRM;;IAEE,8BAAwC;EvEomRhD;EuElmRM;;IAEE,+BAA0C;EvEomRlD;EuElmRM;;IAEE,6BAAsC;EvEomR9C;EuEnnRM;IAAgC,0BAA4B;EvEsnRlE;EuErnRM;;IAEE,8BAAoC;EvEunR5C;EuErnRM;;IAEE,gCAAwC;EvEunRhD;EuErnRM;;IAEE,iCAA0C;EvEunRlD;EuErnRM;;IAEE,+BAAsC;EvEunR9C;EuEtoRM;IAAgC,wBAA4B;EvEyoRlE;EuExoRM;;IAEE,4BAAoC;EvE0oR5C;EuExoRM;;IAEE,8BAAwC;EvE0oRhD;EuExoRM;;IAEE,+BAA0C;EvE0oRlD;EuExoRM;;IAEE,6BAAsC;EvE0oR9C;EuEloRM;IAAwB,2BAA2B;EvEqoRzD;EuEpoRM;;IAEE,+BAA+B;EvEsoRvC;EuEpoRM;;IAEE,iCAAiC;EvEsoRzC;EuEpoRM;;IAEE,kCAAkC;EvEsoR1C;EuEpoRM;;IAEE,gCAAgC;EvEsoRxC;EuErpRM;IAAwB,0BAA2B;EvEwpRzD;EuEvpRM;;IAEE,8BAA+B;EvEypRvC;EuEvpRM;;IAEE,gCAAiC;EvEypRzC;EuEvpRM;;IAEE,iCAAkC;EvEypR1C;EuEvpRM;;IAEE,+BAAgC;EvEypRxC;EuExqRM;IAAwB,wBAA2B;EvE2qRzD;EuE1qRM;;IAEE,4BAA+B;EvE4qRvC;EuE1qRM;;IAEE,8BAAiC;EvE4qRzC;EuE1qRM;;IAEE,+BAAkC;EvE4qR1C;EuE1qRM;;IAEE,6BAAgC;EvE4qRxC;EuE3rRM;IAAwB,0BAA2B;EvE8rRzD;EuE7rRM;;IAEE,8BAA+B;EvE+rRvC;EuE7rRM;;IAEE,gCAAiC;EvE+rRzC;EuE7rRM;;IAEE,iCAAkC;EvE+rR1C;EuE7rRM;;IAEE,+BAAgC;EvE+rRxC;EuE9sRM;IAAwB,wBAA2B;EvEitRzD;EuEhtRM;;IAEE,4BAA+B;EvEktRvC;EuEhtRM;;IAEE,8BAAiC;EvEktRzC;EuEhtRM;;IAEE,+BAAkC;EvEktR1C;EuEhtRM;;IAEE,6BAAgC;EvEktRxC;EuE5sRE;IAAmB,uBAAuB;EvE+sR5C;EuE9sRE;;IAEE,2BAA2B;EvEgtR/B;EuE9sRE;;IAEE,6BAA6B;EvEgtRjC;EuE9sRE;;IAEE,8BAA8B;EvEgtRlC;EuE9sRE;;IAEE,4BAA4B;EvEgtRhC;AACF;;Ac1tRI;EyDlDI;IAAgC,oBAA4B;EvEixRlE;EuEhxRM;;IAEE,wBAAoC;EvEkxR5C;EuEhxRM;;IAEE,0BAAwC;EvEkxRhD;EuEhxRM;;IAEE,2BAA0C;EvEkxRlD;EuEhxRM;;IAEE,yBAAsC;EvEkxR9C;EuEjyRM;IAAgC,0BAA4B;EvEoyRlE;EuEnyRM;;IAEE,8BAAoC;EvEqyR5C;EuEnyRM;;IAEE,gCAAwC;EvEqyRhD;EuEnyRM;;IAEE,iCAA0C;EvEqyRlD;EuEnyRM;;IAEE,+BAAsC;EvEqyR9C;EuEpzRM;IAAgC,yBAA4B;EvEuzRlE;EuEtzRM;;IAEE,6BAAoC;EvEwzR5C;EuEtzRM;;IAEE,+BAAwC;EvEwzRhD;EuEtzRM;;IAEE,gCAA0C;EvEwzRlD;EuEtzRM;;IAEE,8BAAsC;EvEwzR9C;EuEv0RM;IAAgC,uBAA4B;EvE00RlE;EuEz0RM;;IAEE,2BAAoC;EvE20R5C;EuEz0RM;;IAEE,6BAAwC;EvE20RhD;EuEz0RM;;IAEE,8BAA0C;EvE20RlD;EuEz0RM;;IAEE,4BAAsC;EvE20R9C;EuE11RM;IAAgC,yBAA4B;EvE61RlE;EuE51RM;;IAEE,6BAAoC;EvE81R5C;EuE51RM;;IAEE,+BAAwC;EvE81RhD;EuE51RM;;IAEE,gCAA0C;EvE81RlD;EuE51RM;;IAEE,8BAAsC;EvE81R9C;EuE72RM;IAAgC,uBAA4B;EvEg3RlE;EuE/2RM;;IAEE,2BAAoC;EvEi3R5C;EuE/2RM;;IAEE,6BAAwC;EvEi3RhD;EuE/2RM;;IAEE,8BAA0C;EvEi3RlD;EuE/2RM;;IAEE,4BAAsC;EvEi3R9C;EuEh4RM;IAAgC,qBAA4B;EvEm4RlE;EuEl4RM;;IAEE,yBAAoC;EvEo4R5C;EuEl4RM;;IAEE,2BAAwC;EvEo4RhD;EuEl4RM;;IAEE,4BAA0C;EvEo4RlD;EuEl4RM;;IAEE,0BAAsC;EvEo4R9C;EuEn5RM;IAAgC,2BAA4B;EvEs5RlE;EuEr5RM;;IAEE,+BAAoC;EvEu5R5C;EuEr5RM;;IAEE,iCAAwC;EvEu5RhD;EuEr5RM;;IAEE,kCAA0C;EvEu5RlD;EuEr5RM;;IAEE,gCAAsC;EvEu5R9C;EuEt6RM;IAAgC,0BAA4B;EvEy6RlE;EuEx6RM;;IAEE,8BAAoC;EvE06R5C;EuEx6RM;;IAEE,gCAAwC;EvE06RhD;EuEx6RM;;IAEE,iCAA0C;EvE06RlD;EuEx6RM;;IAEE,+BAAsC;EvE06R9C;EuEz7RM;IAAgC,wBAA4B;EvE47RlE;EuE37RM;;IAEE,4BAAoC;EvE67R5C;EuE37RM;;IAEE,8BAAwC;EvE67RhD;EuE37RM;;IAEE,+BAA0C;EvE67RlD;EuE37RM;;IAEE,6BAAsC;EvE67R9C;EuE58RM;IAAgC,0BAA4B;EvE+8RlE;EuE98RM;;IAEE,8BAAoC;EvEg9R5C;EuE98RM;;IAEE,gCAAwC;EvEg9RhD;EuE98RM;;IAEE,iCAA0C;EvEg9RlD;EuE98RM;;IAEE,+BAAsC;EvEg9R9C;EuE/9RM;IAAgC,wBAA4B;EvEk+RlE;EuEj+RM;;IAEE,4BAAoC;EvEm+R5C;EuEj+RM;;IAEE,8BAAwC;EvEm+RhD;EuEj+RM;;IAEE,+BAA0C;EvEm+RlD;EuEj+RM;;IAEE,6BAAsC;EvEm+R9C;EuE39RM;IAAwB,2BAA2B;EvE89RzD;EuE79RM;;IAEE,+BAA+B;EvE+9RvC;EuE79RM;;IAEE,iCAAiC;EvE+9RzC;EuE79RM;;IAEE,kCAAkC;EvE+9R1C;EuE79RM;;IAEE,gCAAgC;EvE+9RxC;EuE9+RM;IAAwB,0BAA2B;EvEi/RzD;EuEh/RM;;IAEE,8BAA+B;EvEk/RvC;EuEh/RM;;IAEE,gCAAiC;EvEk/RzC;EuEh/RM;;IAEE,iCAAkC;EvEk/R1C;EuEh/RM;;IAEE,+BAAgC;EvEk/RxC;EuEjgSM;IAAwB,wBAA2B;EvEogSzD;EuEngSM;;IAEE,4BAA+B;EvEqgSvC;EuEngSM;;IAEE,8BAAiC;EvEqgSzC;EuEngSM;;IAEE,+BAAkC;EvEqgS1C;EuEngSM;;IAEE,6BAAgC;EvEqgSxC;EuEphSM;IAAwB,0BAA2B;EvEuhSzD;EuEthSM;;IAEE,8BAA+B;EvEwhSvC;EuEthSM;;IAEE,gCAAiC;EvEwhSzC;EuEthSM;;IAEE,iCAAkC;EvEwhS1C;EuEthSM;;IAEE,+BAAgC;EvEwhSxC;EuEviSM;IAAwB,wBAA2B;EvE0iSzD;EuEziSM;;IAEE,4BAA+B;EvE2iSvC;EuEziSM;;IAEE,8BAAiC;EvE2iSzC;EuEziSM;;IAEE,+BAAkC;EvE2iS1C;EuEziSM;;IAEE,6BAAgC;EvE2iSxC;EuEriSE;IAAmB,uBAAuB;EvEwiS5C;EuEviSE;;IAEE,2BAA2B;EvEyiS/B;EuEviSE;;IAEE,6BAA6B;EvEyiSjC;EuEviSE;;IAEE,8BAA8B;EvEyiSlC;EuEviSE;;IAEE,4BAA4B;EvEyiShC;AACF;;AwE3mSA;EAEI,kBAAkB;EAClB,MAAM;EACN,QAAQ;EACR,SAAS;EACT,OAAO;EACP,UAAU;EAEV,oBAAoB;EACpB,WAAW;EAEX,kCAAkC;AxE2mStC;;AyErnSA;EAAkB,4GAA8C;AzEynShE;;AyErnSA;EAAiB,8BAA8B;AzEynS/C;;AyExnSA;EAAiB,8BAA8B;AzE4nS/C;;AyE3nSA;EAAiB,8BAA8B;AzE+nS/C;;AyE9nSA;ECTE,gBAAgB;EAChB,uBAAuB;EACvB,mBAAmB;A1E2oSrB;;AyE5nSI;EAAwB,2BAA2B;AzEgoSvD;;AyE/nSI;EAAwB,4BAA4B;AzEmoSxD;;AyEloSI;EAAwB,6BAA6B;AzEsoSzD;;AcjmSI;E2DvCA;IAAwB,2BAA2B;EzE6oSrD;EyE5oSE;IAAwB,4BAA4B;EzE+oStD;EyE9oSE;IAAwB,6BAA6B;EzEipSvD;AACF;;Ac7mSI;E2DvCA;IAAwB,2BAA2B;EzEypSrD;EyExpSE;IAAwB,4BAA4B;EzE2pStD;EyE1pSE;IAAwB,6BAA6B;EzE6pSvD;AACF;;AcznSI;E2DvCA;IAAwB,2BAA2B;EzEqqSrD;EyEpqSE;IAAwB,4BAA4B;EzEuqStD;EyEtqSE;IAAwB,6BAA6B;EzEyqSvD;AACF;;AcroSI;E2DvCA;IAAwB,2BAA2B;EzEirSrD;EyEhrSE;IAAwB,4BAA4B;EzEmrStD;EyElrSE;IAAwB,6BAA6B;EzEqrSvD;AACF;;AyEhrSA;EAAmB,oCAAoC;AzEorSvD;;AyEnrSA;EAAmB,oCAAoC;AzEurSvD;;AyEtrSA;EAAmB,qCAAqC;AzE0rSxD;;AyEtrSA;EAAuB,2BAA0C;AzE0rSjE;;AyEzrSA;EAAuB,+BAA4C;AzE6rSnE;;AyE5rSA;EAAuB,2BAA2C;AzEgsSlE;;AyE/rSA;EAAuB,2BAAyC;AzEmsShE;;AyElsSA;EAAuB,8BAA2C;AzEssSlE;;AyErsSA;EAAuB,6BAA6B;AzEysSpD;;AyErsSA;EAAc,sBAAwB;AzEysStC;;A2EhvSE;EACE,yBAAwB;A3EmvS5B;;AKzuSE;EsELM,yBAA0E;A3EkvSlF;;A2ExvSE;EACE,yBAAwB;A3E2vS5B;;AKjvSE;EsELM,yBAA0E;A3E0vSlF;;A2EhwSE;EACE,yBAAwB;A3EmwS5B;;AKzvSE;EsELM,yBAA0E;A3EkwSlF;;A2ExwSE;EACE,yBAAwB;A3E2wS5B;;AKjwSE;EsELM,yBAA0E;A3E0wSlF;;A2EhxSE;EACE,yBAAwB;A3EmxS5B;;AKzwSE;EsELM,yBAA0E;A3EkxSlF;;A2ExxSE;EACE,yBAAwB;A3E2xS5B;;AKjxSE;EsELM,yBAA0E;A3E0xSlF;;A2EhySE;EACE,yBAAwB;A3EmyS5B;;AKzxSE;EsELM,yBAA0E;A3EkySlF;;A2ExySE;EACE,yBAAwB;A3E2yS5B;;AKjySE;EsELM,yBAA0E;A3E0ySlF;;AyEnwSA;EAAa,yBAA6B;AzEuwS1C;;AyEtwSA;EAAc,yBAA6B;AzE0wS3C;;AyExwSA;EAAiB,oCAAkC;AzE4wSnD;;AyE3wSA;EAAiB,0CAAkC;AzE+wSnD;;AyE3wSA;EGvDE,WAAW;EACX,kBAAkB;EAClB,iBAAiB;EACjB,6BAA6B;EAC7B,SAAS;A5Es0SX;;AyE/wSA;EAAwB,gCAAgC;AzEmxSxD;;AyEjxSA;EACE,iCAAiC;EACjC,gCAAgC;AzEoxSlC;;AyE/wSA;EAAc,yBAAyB;AzEmxSvC;;A6Ep1SA;EACE,8BAA8B;A7Eu1ShC;;A6Ep1SA;EACE,6BAA6B;A7Eu1S/B;;A8Ev1SE;E5EOF;;;I4EDM,4BAA4B;IAE5B,2BAA2B;E9Eu1S/B;E8Ep1SE;IAEI,0BAA0B;E9Eq1ShC;E8E50SE;IACE,6BAA6B;E9E80SjC;EEhpSF;I4E/KM,gCAAgC;E9Ek0SpC;E8Eh0SE;;IAEE,yB3EzCY;I2E0CZ,wBAAwB;E9Ek0S5B;E8E1zSE;IACE,2BAA2B;E9E4zS/B;E8EzzSE;;IAEE,wBAAwB;E9E2zS5B;E8ExzSE;;;IAGE,UAAU;IACV,SAAS;E9E0zSb;E8EvzSE;;IAEE,uBAAuB;E9EyzS3B;E8EjzSE;IACE,Q3E2hCgC;EHwxQpC;EE/1SF;I4E+CM,2BAA2C;E9EmzS/C;E8EjzSE;IACE,2BAA2C;E9EmzS/C;EiCj4SF;I6CmFM,aAAa;E9EizSjB;EsCh5SF;IwCkGM,sB3EtFS;EHu4Sb;EgBp5SF;I8DuGM,oCAAoC;E9EgzSxC;E8EjzSE;;IAKI,iCAAmC;E9EgzSzC;EgBn3SF;;I8D0EQ,oCAAsC;E9E6yS5C;EgBlySF;I8DNM,cAAc;E9E2ySlB;EiBj6SA;;;;I6D4HM,qB3EvHU;EHk6ShB;EgB7zSF;I8DuBM,cAAc;IACd,qB3E7HY;EHs6ShB;AACF","file":"bootstrap.css","sourcesContent":["/*!\n * Bootstrap v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"code\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"input-group\";\n@import \"custom-forms\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"jumbotron\";\n@import \"alert\";\n@import \"progress\";\n@import \"media\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"utilities\";\n@import \"print\";\n","/*!\n * Bootstrap v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=\"button\"] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014\\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-wrap: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container,\n.container-fluid,\n.container-sm,\n.container-md,\n.container-lg,\n.container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n color: #212529;\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-primary th,\n.table-primary td,\n.table-primary thead th,\n.table-primary tbody + tbody {\n border-color: #7abaff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-secondary th,\n.table-secondary td,\n.table-secondary thead th,\n.table-secondary tbody + tbody {\n border-color: #b3b7bb;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-success th,\n.table-success td,\n.table-success thead th,\n.table-success tbody + tbody {\n border-color: #8fd19e;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-info th,\n.table-info td,\n.table-info thead th,\n.table-info tbody + tbody {\n border-color: #86cfda;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-warning th,\n.table-warning td,\n.table-warning thead th,\n.table-warning tbody + tbody {\n border-color: #ffdf7e;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-danger th,\n.table-danger td,\n.table-danger thead th,\n.table-danger tbody + tbody {\n border-color: #ed969e;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-light th,\n.table-light td,\n.table-light thead th,\n.table-light tbody + tbody {\n border-color: #fbfcfc;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th,\n.table-dark tbody + tbody {\n border-color: #95999c;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #343a40;\n border-color: #454d55;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #343a40;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #454d55;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\ninput[type=\"date\"].form-control,\ninput[type=\"time\"].form-control,\ninput[type=\"datetime-local\"].form-control,\ninput[type=\"month\"].form-control {\n appearance: none;\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n font-size: 1rem;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.form-control-lg {\n height: calc(1.5em + 1rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control[size], select.form-control[multiple] {\n height: auto;\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input[disabled] ~ .form-check-label,\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: inline-flex;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #28a745;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:valid, .custom-select.is-valid {\n border-color: #28a745;\n padding-right: calc(0.75em + 2.3125rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n border-color: #34ce57;\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:invalid, .custom-select.is-invalid {\n border-color: #dc3545;\n padding-right: calc(0.75em + 2.3125rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n border-color: #e4606d;\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: flex;\n flex-flow: row wrap;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: flex;\n flex: 0 0 auto;\n flex-flow: row wrap;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n flex-shrink: 0;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n align-items: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #212529;\n text-align: center;\n vertical-align: middle;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover {\n color: #212529;\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\n.btn:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n text-decoration: none;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-right {\n right: 0;\n left: auto;\n }\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .form-control-plaintext,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n min-width: 0;\n margin-bottom: 0;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .form-control-plaintext + .form-control,\n.input-group > .form-control-plaintext + .custom-select,\n.input-group > .form-control-plaintext + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n}\n\n.input-group > .custom-file .custom-file-input:focus {\n z-index: 4;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: flex;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn:focus,\n.input-group-append .btn:focus {\n z-index: 3;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: calc(1.5em + 1rem + 2px);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: calc(1.5em + 0.5rem + 2px);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: 1.75rem;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n z-index: 1;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n color-adjust: exact;\n}\n\n.custom-control-inline {\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n left: 0;\n z-index: -1;\n width: 1rem;\n height: 1.25rem;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #80bdff;\n}\n\n.custom-control-input:not(:disabled):active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n border-color: #b3d7ff;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n vertical-align: top;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n background-color: #fff;\n border: #adb5bd solid 1px;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background: no-repeat 50% / 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-switch {\n padding-left: 2.25rem;\n}\n\n.custom-switch .custom-control-label::before {\n left: -2.25rem;\n width: 1.75rem;\n pointer-events: all;\n border-radius: 0.5rem;\n}\n\n.custom-switch .custom-control-label::after {\n top: calc(0.25rem + 2px);\n left: calc(-2.25rem + 2px);\n width: calc(1rem - 4px);\n height: calc(1rem - 4px);\n background-color: #adb5bd;\n border-radius: 0.5rem;\n transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-switch .custom-control-label::after {\n transition: none;\n }\n}\n\n.custom-switch .custom-control-input:checked ~ .custom-control-label::after {\n background-color: #fff;\n transform: translateX(0.75rem);\n}\n\n.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: #fff url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n display: none;\n}\n\n.custom-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.custom-select-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.custom-select-lg {\n height: calc(1.5em + 1rem + 2px);\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input[disabled] ~ .custom-file-label,\n.custom-file-input:disabled ~ .custom-file-label {\n background-color: #e9ecef;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-input ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: calc(1.5em + 0.75rem);\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: inherit;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n height: 1.4rem;\n padding: 0;\n background-color: transparent;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-ms-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-moz-range-thumb {\n transition: none;\n }\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: 0;\n margin-right: 0.2rem;\n margin-left: 0.2rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-ms-thumb {\n transition: none;\n }\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-webkit-slider-runnable-track {\n cursor: default;\n}\n\n.custom-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-moz-range-track {\n cursor: default;\n}\n\n.custom-range:disabled::-ms-thumb {\n background-color: #adb5bd;\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-control-label::before,\n .custom-file-label,\n .custom-select {\n transition: none;\n }\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill > .nav-link,\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified > .nav-link,\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar .container,\n.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl {\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n flex-flow: row nowrap;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n}\n\n.card > .list-group:first-child {\n border-top-width: 0;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card > .list-group:last-child {\n border-bottom-width: 0;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card > .card-header + .list-group,\n.card > .list-group + .card-footer {\n border-top: 0;\n}\n\n.card-body {\n flex: 1 1 auto;\n min-height: 1px;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n flex-shrink: 0;\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n display: flex;\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n flex: 1 0 0%;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n .card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n .card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n .card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n .card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n column-count: 3;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion {\n overflow-anchor: none;\n}\n\n.accordion > .card {\n overflow: hidden;\n}\n\n.accordion > .card:not(:last-of-type) {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion > .card:not(:first-of-type) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.accordion > .card > .card-header {\n border-radius: 0;\n margin-bottom: -1px;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item {\n display: flex;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 3;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 3;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .badge {\n transition: none;\n }\n}\n\na.badge:hover, a.badge:focus {\n text-decoration: none;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\na.badge-primary:hover, a.badge-primary:focus {\n color: #fff;\n background-color: #0062cc;\n}\n\na.badge-primary:focus, a.badge-primary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\na.badge-secondary:hover, a.badge-secondary:focus {\n color: #fff;\n background-color: #545b62;\n}\n\na.badge-secondary:focus, a.badge-secondary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\na.badge-success:hover, a.badge-success:focus {\n color: #fff;\n background-color: #1e7e34;\n}\n\na.badge-success:focus, a.badge-success.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\na.badge-info:hover, a.badge-info:focus {\n color: #fff;\n background-color: #117a8b;\n}\n\na.badge-info:focus, a.badge-info.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\na.badge-warning:hover, a.badge-warning:focus {\n color: #212529;\n background-color: #d39e00;\n}\n\na.badge-warning:focus, a.badge-warning.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\na.badge-danger:hover, a.badge-danger:focus {\n color: #fff;\n background-color: #bd2130;\n}\n\na.badge-danger:focus, a.badge-danger.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\na.badge-light:hover, a.badge-light:focus {\n color: #212529;\n background-color: #dae0e5;\n}\n\na.badge-light:focus, a.badge-light.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\na.badge-dark:hover, a.badge-dark:focus {\n color: #fff;\n background-color: #1d2124;\n}\n\na.badge-dark:focus, a.badge-dark.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n line-height: 0;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.media {\n display: flex;\n align-items: flex-start;\n}\n\n.media-body {\n flex: 1;\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n border-radius: 0.25rem;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n}\n\n.list-group-item:last-child {\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n\n.list-group-item + .list-group-item.active {\n margin-top: -1px;\n border-top-width: 1px;\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n\n.list-group-horizontal > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n\n.list-group-horizontal > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n\n.list-group-horizontal > .list-group-item.active {\n margin-top: 0;\n}\n\n.list-group-horizontal > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n}\n\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n.list-group-flush {\n border-radius: 0;\n}\n\n.list-group-flush > .list-group-item {\n border-width: 0 0 1px;\n}\n\n.list-group-flush > .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover {\n color: #000;\n text-decoration: none;\n}\n\n.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {\n opacity: .75;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n}\n\na.close.disabled {\n pointer-events: none;\n}\n\n.toast {\n flex-basis: 350px;\n max-width: 350px;\n font-size: 0.875rem;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);\n opacity: 0;\n border-radius: 0.25rem;\n}\n\n.toast:not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast.showing {\n opacity: 1;\n}\n\n.toast.show {\n display: block;\n opacity: 1;\n}\n\n.toast.hide {\n display: none;\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: 0.25rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.toast-body {\n padding: 0.75rem;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n transform: none;\n}\n\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n display: flex;\n max-height: calc(100% - 1rem);\n}\n\n.modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 1rem);\n overflow: hidden;\n}\n\n.modal-dialog-scrollable .modal-header,\n.modal-dialog-scrollable .modal-footer {\n flex-shrink: 0;\n}\n\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-dialog-centered::before {\n display: block;\n height: calc(100vh - 1rem);\n height: min-content;\n content: \"\";\n}\n\n.modal-dialog-centered.modal-dialog-scrollable {\n flex-direction: column;\n justify-content: center;\n height: 100%;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable .modal-content {\n max-height: none;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable::before {\n content: none;\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.modal-header .close {\n padding: 1rem 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n padding: 0.75rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: calc(0.3rem - 1px);\n border-bottom-left-radius: calc(0.3rem - 1px);\n}\n\n.modal-footer > * {\n margin: 0.25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-scrollable {\n max-height: calc(100% - 3.5rem);\n }\n .modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 3.5rem);\n }\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n .modal-dialog-centered::before {\n height: calc(100vh - 3.5rem);\n height: min-content;\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n max-width: 800px;\n }\n}\n\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top > .arrow, .bs-popover-auto[x-placement^=\"top\"] > .arrow {\n bottom: calc(-0.5rem - 1px);\n}\n\n.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^=\"top\"] > .arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^=\"top\"] > .arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right > .arrow, .bs-popover-auto[x-placement^=\"right\"] > .arrow {\n left: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^=\"right\"] > .arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^=\"right\"] > .arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow {\n top: calc(-0.5rem - 1px);\n}\n\n.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left > .arrow, .bs-popover-auto[x-placement^=\"left\"] > .arrow {\n right: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^=\"left\"] > .arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^=\"left\"] > .arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-right {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: no-repeat 50% / 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: flex;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: .5;\n transition: opacity 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators li {\n transition: none;\n }\n}\n\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg);\n }\n}\n\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded-sm {\n border-radius: 0.2rem !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-lg {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.user-select-all {\n user-select: all !important;\n}\n\n.user-select-auto {\n user-select: auto !important;\n}\n\n.user-select-none {\n user-select: none !important;\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports (position: sticky) {\n .sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n pointer-events: auto;\n content: \"\";\n background-color: rgba(0, 0, 0, 0);\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !important;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-lighter {\n font-weight: lighter !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-weight-bolder {\n font-weight: bolder !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0056b3 !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #494f54 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #19692c !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #0f6674 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #ba8b00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #a71d2a !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #cbd3da !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #121416 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-break {\n word-break: break-word !important;\n word-wrap: break-word !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// Do not forget to update getting-started/theming.md!\n:root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$color}: #{$value};\n }\n\n @each $bp, $value in $grid-breakpoints {\n --breakpoint-#{$bp}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --font-family-sans-serif: #{inspect($font-family-sans-serif)};\n --font-family-monospace: #{inspect($font-family-monospace)};\n}\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `<th>` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `<h1>`-`<h6>` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `<p>`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `<td>` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Set the cursor for non-`<button>` buttons\n//\n// Details at https://github.com/twbs/bootstrap/pull/30562\n[role=\"button\"] {\n cursor: pointer;\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `<div>`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\", \"%3c\"),\n (\">\", \"%3e\"),\n (\"#\", \"%23\"),\n (\"(\", \"%28\"),\n (\")\", \"%29\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `<body>` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n$table-th-font-weight: null !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: null !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: null !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'><path fill='#{$custom-control-indicator-checked-color}' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/></svg>\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'><path stroke='#{$custom-checkbox-indicator-indeterminate-color}' d='M0 2h4'/></svg>\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: null !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'><circle r='3' fill='#{$custom-control-indicator-checked-color}'/></svg>\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'><path fill='#{$custom-select-indicator-color}' d='M2 0L0 2h4zm0 5L0 3h4z'/></svg>\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'><path fill='#{$form-feedback-icon-valid-color}' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/></svg>\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='#{$form-feedback-icon-invalid-color}' viewBox='0 0 12 12'><circle cx='6' cy='6' r='4.5'/><path stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/><circle cx='6' cy='8.2' r='.6' fill='#{$form-feedback-icon-invalid-color}' stroke='none'/></svg>\") !default;\n\n$form-validation-states: () !default;\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'><path stroke='#{$navbar-dark-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'><path stroke='#{$navbar-light-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-x: 0 !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n$dropdown-header-padding: $dropdown-padding-y $dropdown-item-padding-x !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' width='8' height='8' viewBox='0 0 8 8'><path d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/></svg>\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' width='8' height='8' viewBox='0 0 8 8'><path d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/></svg>\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n$user-selects: all, auto, none !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n","// stylelint-disable declaration-no-important, selector-list-comma-newline-after\n\n//\n// Headings\n//\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1, .h1 { @include font-size($h1-font-size); }\nh2, .h2 { @include font-size($h2-font-size); }\nh3, .h3 { @include font-size($h3-font-size); }\nh4, .h4 { @include font-size($h4-font-size); }\nh5, .h5 { @include font-size($h5-font-size); }\nh6, .h6 { @include font-size($h6-font-size); }\n\n.lead {\n @include font-size($lead-font-size);\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n.display-1 {\n @include font-size($display1-size);\n font-weight: $display1-weight;\n line-height: $display-line-height;\n}\n.display-2 {\n @include font-size($display2-size);\n font-weight: $display2-weight;\n line-height: $display-line-height;\n}\n.display-3 {\n @include font-size($display3-size);\n font-weight: $display3-weight;\n line-height: $display-line-height;\n}\n.display-4 {\n @include font-size($display4-size);\n font-weight: $display4-weight;\n line-height: $display-line-height;\n}\n\n\n//\n// Horizontal rules\n//\n\nhr {\n margin-top: $hr-margin-y;\n margin-bottom: $hr-margin-y;\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n}\n\n\n//\n// Emphasis\n//\n\nsmall,\n.small {\n @include font-size($small-font-size);\n font-weight: $font-weight-normal;\n}\n\nmark,\n.mark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled();\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled();\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n @include font-size(90%);\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $spacer;\n @include font-size($blockquote-font-size);\n}\n\n.blockquote-footer {\n display: block;\n @include font-size($blockquote-small-font-size);\n color: $blockquote-small-color;\n\n &::before {\n content: \"\\2014\\00A0\"; // em dash, nbsp\n }\n}\n","// Lists\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n@mixin list-unstyled() {\n padding-left: 0;\n list-style: none;\n}\n","// Responsive images (ensure images don't scale beyond their parents)\n//\n// This is purposefully opt-in via an explicit class rather than being the default for all `<img>`s.\n// We previously tried the \"images are responsive by default\" approach in Bootstrap v2,\n// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)\n// which weren't expecting the images within themselves to be involuntarily resized.\n// See also https://github.com/twbs/bootstrap/issues/18178\n.img-fluid {\n @include img-fluid();\n}\n\n\n// Image thumbnails\n.img-thumbnail {\n padding: $thumbnail-padding;\n background-color: $thumbnail-bg;\n border: $thumbnail-border-width solid $thumbnail-border-color;\n @include border-radius($thumbnail-border-radius);\n @include box-shadow($thumbnail-box-shadow);\n\n // Keep them at most 100% wide\n @include img-fluid();\n}\n\n//\n// Figures\n//\n\n.figure {\n // Ensures the caption's text aligns with the image.\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: $spacer / 2;\n line-height: 1;\n}\n\n.figure-caption {\n @include font-size($figure-caption-font-size);\n color: $figure-caption-color;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n@mixin img-fluid() {\n // Part 1: Set a maximum relative to the parent\n max-width: 100%;\n // Part 2: Override the height to auto, otherwise images will be stretched\n // when setting a width and height attribute on the img element.\n height: auto;\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size.\n\n@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {\n background-image: url($file-1x);\n\n // Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,\n // but doesn't convert dppx=>dpi.\n // There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.\n // Compatibility info: https://caniuse.com/#feat=css-media-resolution\n @media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx\n only screen and (min-resolution: 2dppx) { // Standardized\n background-image: url($file-2x);\n background-size: $width-1x $height-1x;\n }\n @include deprecate(\"`img-retina()`\", \"v4.3.0\", \"v5\");\n}\n","// stylelint-disable property-disallowed-list\n// Single side border-radius\n\n// Helper function to replace negative values with 0\n@function valid-radius($radius) {\n $return: ();\n @each $value in $radius {\n @if type-of($value) == number {\n $return: append($return, max($value, 0));\n } @else {\n $return: append($return, $value);\n }\n }\n @return $return;\n}\n\n@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) {\n @if $enable-rounded {\n border-radius: valid-radius($radius);\n }\n @else if $fallback-border-radius != false {\n border-radius: $fallback-border-radius;\n }\n}\n\n@mixin border-top-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: valid-radius($radius);\n border-top-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: valid-radius($radius);\n border-bottom-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-bottom-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: valid-radius($radius);\n border-bottom-left-radius: valid-radius($radius);\n }\n}\n\n@mixin border-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: valid-radius($radius);\n border-bottom-left-radius: valid-radius($radius);\n }\n}\n\n@mixin border-top-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: valid-radius($radius);\n }\n}\n\n@mixin border-top-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-bottom-right-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-bottom-left-radius($radius) {\n @if $enable-rounded {\n border-bottom-left-radius: valid-radius($radius);\n }\n}\n","// Inline code\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n @include box-shadow($kbd-box-shadow);\n\n kbd {\n padding: 0;\n @include font-size(100%);\n font-weight: $nested-kbd-font-weight;\n @include box-shadow(none);\n }\n}\n\n// Blocks of code\npre {\n display: block;\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: $pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n @include deprecate(\"The `make-container-max-widths` mixin\", \"v4.5.2\", \"v5\");\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @if $columns > 0 {\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n }\n\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n @if $columns > 0 {\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n }\n}\n","//\n// Basic Bootstrap table\n//\n\n.table {\n width: 100%;\n margin-bottom: $spacer;\n color: $table-color;\n background-color: $table-bg; // Reset for nesting within parents with `background-color`.\n\n th,\n td {\n padding: $table-cell-padding;\n vertical-align: top;\n border-top: $table-border-width solid $table-border-color;\n }\n\n thead th {\n vertical-align: bottom;\n border-bottom: (2 * $table-border-width) solid $table-border-color;\n }\n\n tbody + tbody {\n border-top: (2 * $table-border-width) solid $table-border-color;\n }\n}\n\n\n//\n// Condensed table w/ half padding\n//\n\n.table-sm {\n th,\n td {\n padding: $table-cell-padding-sm;\n }\n}\n\n\n// Border versions\n//\n// Add or remove borders all around the table and between all the columns.\n\n.table-bordered {\n border: $table-border-width solid $table-border-color;\n\n th,\n td {\n border: $table-border-width solid $table-border-color;\n }\n\n thead {\n th,\n td {\n border-bottom-width: 2 * $table-border-width;\n }\n }\n}\n\n.table-borderless {\n th,\n td,\n thead th,\n tbody + tbody {\n border: 0;\n }\n}\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-accent-bg;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n tbody tr {\n @include hover() {\n color: $table-hover-color;\n background-color: $table-hover-bg;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n@each $color, $value in $theme-colors {\n @include table-row-variant($color, theme-color-level($color, $table-bg-level), theme-color-level($color, $table-border-level));\n}\n\n@include table-row-variant(active, $table-active-bg);\n\n\n// Dark styles\n//\n// Same table markup, but inverted color scheme: dark background and light text.\n\n// stylelint-disable-next-line no-duplicate-selectors\n.table {\n .thead-dark {\n th {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n border-color: $table-dark-border-color;\n }\n }\n\n .thead-light {\n th {\n color: $table-head-color;\n background-color: $table-head-bg;\n border-color: $table-border-color;\n }\n }\n}\n\n.table-dark {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n\n th,\n td,\n thead th {\n border-color: $table-dark-border-color;\n }\n\n &.table-bordered {\n border: 0;\n }\n\n &.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-dark-accent-bg;\n }\n }\n\n &.table-hover {\n tbody tr {\n @include hover() {\n color: $table-dark-hover-color;\n background-color: $table-dark-hover-bg;\n }\n }\n }\n}\n\n\n// Responsive tables\n//\n// Generate series of `.table-responsive-*` classes for configuring the screen\n// size of where your table will overflow.\n\n.table-responsive {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n &#{$infix} {\n @include media-breakpoint-down($breakpoint) {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n\n // Prevent double border on horizontal scroll due to use of `display: block;`\n > .table-bordered {\n border: 0;\n }\n }\n }\n }\n}\n","// Tables\n\n@mixin table-row-variant($state, $background, $border: null) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table-#{$state} {\n &,\n > th,\n > td {\n background-color: $background;\n }\n\n @if $border != null {\n th,\n td,\n thead th,\n tbody + tbody {\n border-color: $border;\n }\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover {\n $hover-background: darken($background, 5%);\n\n .table-#{$state} {\n @include hover() {\n background-color: $hover-background;\n\n > td,\n > th {\n background-color: $hover-background;\n }\n }\n }\n }\n}\n","// Bootstrap functions\n//\n// Utility mixins and functions for evaluating source code across our variables, maps, and mixins.\n\n// Ascending\n// Used to evaluate Sass maps like our grid breakpoints.\n@mixin _assert-ascending($map, $map-name) {\n $prev-key: null;\n $prev-num: null;\n @each $key, $num in $map {\n @if $prev-num == null or unit($num) == \"%\" or unit($prev-num) == \"%\" {\n // Do nothing\n } @else if not comparable($prev-num, $num) {\n @warn \"Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !\";\n } @else if $prev-num >= $num {\n @warn \"Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !\";\n }\n $prev-key: $key;\n $prev-num: $num;\n }\n}\n\n// Starts at zero\n// Used to ensure the min-width of the lowest breakpoint starts at 0.\n@mixin _assert-starts-at-zero($map, $map-name: \"$grid-breakpoints\") {\n @if length($map) > 0 {\n $values: map-values($map);\n $first-value: nth($values, 1);\n @if $first-value != 0 {\n @warn \"First breakpoint in #{$map-name} must start at 0, but starts at #{$first-value}.\";\n }\n }\n}\n\n// Replace `$search` with `$replace` in `$string`\n// Used on our SVG icon backgrounds for custom forms.\n//\n// @author Hugo Giraudel\n// @param {String} $string - Initial string\n// @param {String} $search - Substring to replace\n// @param {String} $replace ('') - New value\n// @return {String} - Updated string\n@function str-replace($string, $search, $replace: \"\") {\n $index: str-index($string, $search);\n\n @if $index {\n @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);\n }\n\n @return $string;\n}\n\n// See https://codepen.io/kevinweber/pen/dXWoRw\n//\n// Requires the use of quotes around data URIs.\n\n@function escape-svg($string) {\n @if str-index($string, \"data:image/svg+xml\") {\n @each $char, $encoded in $escaped-characters {\n // Do not escape the url brackets\n @if str-index($string, \"url(\") == 1 {\n $string: url(\"#{str-replace(str-slice($string, 6, -3), $char, $encoded)}\");\n } @else {\n $string: str-replace($string, $char, $encoded);\n }\n }\n }\n\n @return $string;\n}\n\n// Color contrast\n@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {\n $r: red($color);\n $g: green($color);\n $b: blue($color);\n\n $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;\n\n @if ($yiq >= $yiq-contrasted-threshold) {\n @return $dark;\n } @else {\n @return $light;\n }\n}\n\n// Retrieve color Sass maps\n@function color($key: \"blue\") {\n @return map-get($colors, $key);\n}\n\n@function theme-color($key: \"primary\") {\n @return map-get($theme-colors, $key);\n}\n\n@function gray($key: \"100\") {\n @return map-get($grays, $key);\n}\n\n// Request a theme color level\n@function theme-color-level($color-name: \"primary\", $level: 0) {\n $color: theme-color($color-name);\n $color-base: if($level > 0, $black, $white);\n $level: abs($level);\n\n @return mix($color-base, $color, $level * $theme-color-interval);\n}\n\n// Return valid calc\n@function add($value1, $value2, $return-calc: true) {\n @if $value1 == null {\n @return $value2;\n }\n\n @if $value2 == null {\n @return $value1;\n }\n\n @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {\n @return $value1 + $value2;\n }\n\n @return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + unquote(\" + \") + $value2);\n}\n\n@function subtract($value1, $value2, $return-calc: true) {\n @if $value1 == null and $value2 == null {\n @return null;\n }\n\n @if $value1 == null {\n @return -$value2;\n }\n\n @if $value2 == null {\n @return $value1;\n }\n\n @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {\n @return $value1 - $value2;\n }\n\n @return if($return-calc == true, calc(#{$value1} - #{$value2}), $value1 + unquote(\" - \") + $value2);\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Textual form controls\n//\n\n.form-control {\n display: block;\n width: 100%;\n height: $input-height;\n padding: $input-padding-y $input-padding-x;\n font-family: $input-font-family;\n @include font-size($input-font-size);\n font-weight: $input-font-weight;\n line-height: $input-line-height;\n color: $input-color;\n background-color: $input-bg;\n background-clip: padding-box;\n border: $input-border-width solid $input-border-color;\n\n // Note: This has no effect on <select>s in some browsers, due to the limited stylability of `<select>`s in CSS.\n @include border-radius($input-border-radius, 0);\n\n @include box-shadow($input-box-shadow);\n @include transition($input-transition);\n\n // Unstyle the caret on `<select>`s in IE10+.\n &::-ms-expand {\n background-color: transparent;\n border: 0;\n }\n\n // Remove select outline from select box in FF\n &:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 $input-color;\n }\n\n // Customize the `:focus` state to imitate native WebKit styles.\n @include form-control-focus($ignore-warning: true);\n\n // Placeholder\n &::placeholder {\n color: $input-placeholder-color;\n // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526.\n opacity: 1;\n }\n\n // Disabled and read-only inputs\n //\n // HTML5 says that controls under a fieldset > legend:first-child won't be\n // disabled if the fieldset is disabled. Due to implementation difficulty, we\n // don't honor that edge case; we style them as disabled anyway.\n &:disabled,\n &[readonly] {\n background-color: $input-disabled-bg;\n // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655.\n opacity: 1;\n }\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n &.form-control {\n appearance: none; // Fix appearance for date inputs in Safari\n }\n}\n\nselect.form-control {\n &:focus::-ms-value {\n // Suppress the nested default white text on blue background highlight given to\n // the selected option text when the (still closed) <select> receives focus\n // in IE and (under certain conditions) Edge, as it looks bad and cannot be made to\n // match the appearance of the native widget.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n}\n\n// Make file inputs better match text inputs by forcing them to new lines.\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n\n//\n// Labels\n//\n\n// For use with horizontal and inline forms, when you need the label (or legend)\n// text to align with the form controls.\n.col-form-label {\n padding-top: add($input-padding-y, $input-border-width);\n padding-bottom: add($input-padding-y, $input-border-width);\n margin-bottom: 0; // Override the `<label>/<legend>` default\n @include font-size(inherit); // Override the `<legend>` default\n line-height: $input-line-height;\n}\n\n.col-form-label-lg {\n padding-top: add($input-padding-y-lg, $input-border-width);\n padding-bottom: add($input-padding-y-lg, $input-border-width);\n @include font-size($input-font-size-lg);\n line-height: $input-line-height-lg;\n}\n\n.col-form-label-sm {\n padding-top: add($input-padding-y-sm, $input-border-width);\n padding-bottom: add($input-padding-y-sm, $input-border-width);\n @include font-size($input-font-size-sm);\n line-height: $input-line-height-sm;\n}\n\n\n// Readonly controls as plain text\n//\n// Apply class to a readonly input to make it appear like regular plain\n// text (without any border, background color, focus indicator)\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: $input-padding-y 0;\n margin-bottom: 0; // match inputs if this class comes on inputs with default margins\n @include font-size($input-font-size);\n line-height: $input-line-height;\n color: $input-plaintext-color;\n background-color: transparent;\n border: solid transparent;\n border-width: $input-border-width 0;\n\n &.form-control-sm,\n &.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n\n// Form control sizing\n//\n// Build on `.form-control` with modifier classes to decrease or increase the\n// height and font-size of form controls.\n//\n// Repeated in `_input_group.scss` to avoid Sass extend issues.\n\n.form-control-sm {\n height: $input-height-sm;\n padding: $input-padding-y-sm $input-padding-x-sm;\n @include font-size($input-font-size-sm);\n line-height: $input-line-height-sm;\n @include border-radius($input-border-radius-sm);\n}\n\n.form-control-lg {\n height: $input-height-lg;\n padding: $input-padding-y-lg $input-padding-x-lg;\n @include font-size($input-font-size-lg);\n line-height: $input-line-height-lg;\n @include border-radius($input-border-radius-lg);\n}\n\n// stylelint-disable-next-line no-duplicate-selectors\nselect.form-control {\n &[size],\n &[multiple] {\n height: auto;\n }\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n margin-bottom: $form-group-margin-bottom;\n}\n\n.form-text {\n display: block;\n margin-top: $form-text-margin-top;\n}\n\n\n// Form grid\n//\n// Special replacement for our grid system's `.row` for tighter form layouts.\n\n.form-row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$form-grid-gutter-width / 2;\n margin-left: -$form-grid-gutter-width / 2;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: $form-grid-gutter-width / 2;\n padding-left: $form-grid-gutter-width / 2;\n }\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.form-check {\n position: relative;\n display: block;\n padding-left: $form-check-input-gutter;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: $form-check-input-margin-y;\n margin-left: -$form-check-input-gutter;\n\n // Use [disabled] and :disabled for workaround https://github.com/twbs/bootstrap/issues/28247\n &[disabled] ~ .form-check-label,\n &:disabled ~ .form-check-label {\n color: $text-muted;\n }\n}\n\n.form-check-label {\n margin-bottom: 0; // Override default `<label>` bottom margin\n}\n\n.form-check-inline {\n display: inline-flex;\n align-items: center;\n padding-left: 0; // Override base .form-check\n margin-right: $form-check-inline-margin-x;\n\n // Undo .form-check-input defaults and add some `margin-right`.\n .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: $form-check-inline-input-margin-x;\n margin-left: 0;\n }\n}\n\n\n// Form validation\n//\n// Provide feedback to users when form field values are valid or invalid. Works\n// primarily for client-side validation via scoped `:invalid` and `:valid`\n// pseudo-classes but also includes `.is-invalid` and `.is-valid` classes for\n// server side validation.\n\n@each $state, $data in $form-validation-states {\n @include form-validation-state($state, map-get($data, color), map-get($data, icon));\n}\n\n// Inline forms\n//\n// Make forms appear inline(-block) by adding the `.form-inline` class. Inline\n// forms begin stacked on extra small (mobile) devices and then go inline when\n// viewports reach <768px.\n//\n// Requires wrapping inputs and labels with `.form-group` for proper display of\n// default HTML form controls and our custom form controls (e.g., input groups).\n\n.form-inline {\n display: flex;\n flex-flow: row wrap;\n align-items: center; // Prevent shorter elements from growing to same height as others (e.g., small buttons growing to normal sized button height)\n\n // Because we use flex, the initial sizing of checkboxes is collapsed and\n // doesn't occupy the full-width (which is what we want for xs grid tier),\n // so we force that here.\n .form-check {\n width: 100%;\n }\n\n // Kick in the inline\n @include media-breakpoint-up(sm) {\n label {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 0;\n }\n\n // Inline-block all the things for \"inline\"\n .form-group {\n display: flex;\n flex: 0 0 auto;\n flex-flow: row wrap;\n align-items: center;\n margin-bottom: 0;\n }\n\n // Allow folks to *not* use `.form-group`\n .form-control {\n display: inline-block;\n width: auto; // Prevent labels from stacking above inputs in `.form-group`\n vertical-align: middle;\n }\n\n // Make static controls behave like regular ones\n .form-control-plaintext {\n display: inline-block;\n }\n\n .input-group,\n .custom-select {\n width: auto;\n }\n\n // Remove default margin on radios/checkboxes that were used for stacking, and\n // then undo the floating of radios and checkboxes to match.\n .form-check {\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-check-input {\n position: relative;\n flex-shrink: 0;\n margin-top: 0;\n margin-right: $form-check-input-margin-x;\n margin-left: 0;\n }\n\n .custom-control {\n align-items: center;\n justify-content: center;\n }\n .custom-control-label {\n margin-bottom: 0;\n }\n }\n}\n","// stylelint-disable property-disallowed-list\n@mixin transition($transition...) {\n @if length($transition) == 0 {\n $transition: $transition-base;\n }\n\n @if length($transition) > 1 {\n @each $value in $transition {\n @if $value == null or $value == none {\n @warn \"The keyword 'none' or 'null' must be used as a single argument.\";\n }\n }\n }\n\n @if $enable-transitions {\n @if nth($transition, 1) != null {\n transition: $transition;\n }\n\n @if $enable-prefers-reduced-motion-media-query and nth($transition, 1) != null and nth($transition, 1) != none {\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n }\n }\n}\n","// Form control focus state\n//\n// Generate a customized focus state and for any input with the specified color,\n// which defaults to the `$input-focus-border-color` variable.\n//\n// We highly encourage you to not customize the default value, but instead use\n// this to tweak colors on an as-needed basis. This aesthetic change is based on\n// WebKit's default styles, but applicable to a wider range of browsers. Its\n// usability and accessibility should be taken into account with any change.\n//\n// Example usage: change the default blue border and shadow to white for better\n// contrast against a dark gray background.\n@mixin form-control-focus($ignore-warning: false) {\n &:focus {\n color: $input-focus-color;\n background-color: $input-focus-bg;\n border-color: $input-focus-border-color;\n outline: 0;\n @if $enable-shadows {\n @include box-shadow($input-box-shadow, $input-focus-box-shadow);\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: $input-focus-box-shadow;\n }\n }\n @include deprecate(\"The `form-control-focus()` mixin\", \"v4.4.0\", \"v5\", $ignore-warning);\n}\n\n// This mixin uses an `if()` technique to be compatible with Dart Sass\n// See https://github.com/sass/sass/issues/1873#issuecomment-152293725 for more details\n@mixin form-validation-state-selector($state) {\n @if ($state == \"valid\" or $state == \"invalid\") {\n .was-validated #{if(&, \"&\", \"\")}:#{$state},\n #{if(&, \"&\", \"\")}.is-#{$state} {\n @content;\n }\n } @else {\n #{if(&, \"&\", \"\")}.is-#{$state} {\n @content;\n }\n }\n}\n\n@mixin form-validation-state($state, $color, $icon) {\n .#{$state}-feedback {\n display: none;\n width: 100%;\n margin-top: $form-feedback-margin-top;\n @include font-size($form-feedback-font-size);\n color: $color;\n }\n\n .#{$state}-tooltip {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 5;\n display: none;\n max-width: 100%; // Contain to parent when possible\n padding: $form-feedback-tooltip-padding-y $form-feedback-tooltip-padding-x;\n margin-top: .1rem;\n @include font-size($form-feedback-tooltip-font-size);\n line-height: $form-feedback-tooltip-line-height;\n color: color-yiq($color);\n background-color: rgba($color, $form-feedback-tooltip-opacity);\n @include border-radius($form-feedback-tooltip-border-radius);\n }\n\n @include form-validation-state-selector($state) {\n ~ .#{$state}-feedback,\n ~ .#{$state}-tooltip {\n display: block;\n }\n }\n\n .form-control {\n @include form-validation-state-selector($state) {\n border-color: $color;\n\n @if $enable-validation-icons {\n padding-right: $input-height-inner;\n background-image: escape-svg($icon);\n background-repeat: no-repeat;\n background-position: right $input-height-inner-quarter center;\n background-size: $input-height-inner-half $input-height-inner-half;\n }\n\n &:focus {\n border-color: $color;\n box-shadow: 0 0 0 $input-focus-width rgba($color, .25);\n }\n }\n }\n\n // stylelint-disable-next-line selector-no-qualifying-type\n textarea.form-control {\n @include form-validation-state-selector($state) {\n @if $enable-validation-icons {\n padding-right: $input-height-inner;\n background-position: top $input-height-inner-quarter right $input-height-inner-quarter;\n }\n }\n }\n\n .custom-select {\n @include form-validation-state-selector($state) {\n border-color: $color;\n\n @if $enable-validation-icons {\n padding-right: $custom-select-feedback-icon-padding-right;\n background: $custom-select-background, escape-svg($icon) $custom-select-bg no-repeat $custom-select-feedback-icon-position / $custom-select-feedback-icon-size;\n }\n\n &:focus {\n border-color: $color;\n box-shadow: 0 0 0 $input-focus-width rgba($color, .25);\n }\n }\n }\n\n .form-check-input {\n @include form-validation-state-selector($state) {\n ~ .form-check-label {\n color: $color;\n }\n\n ~ .#{$state}-feedback,\n ~ .#{$state}-tooltip {\n display: block;\n }\n }\n }\n\n .custom-control-input {\n @include form-validation-state-selector($state) {\n ~ .custom-control-label {\n color: $color;\n\n &::before {\n border-color: $color;\n }\n }\n\n &:checked {\n ~ .custom-control-label::before {\n border-color: lighten($color, 10%);\n @include gradient-bg(lighten($color, 10%));\n }\n }\n\n &:focus {\n ~ .custom-control-label::before {\n box-shadow: 0 0 0 $input-focus-width rgba($color, .25);\n }\n\n &:not(:checked) ~ .custom-control-label::before {\n border-color: $color;\n }\n }\n }\n }\n\n // custom file\n .custom-file-input {\n @include form-validation-state-selector($state) {\n ~ .custom-file-label {\n border-color: $color;\n }\n\n &:focus {\n ~ .custom-file-label {\n border-color: $color;\n box-shadow: 0 0 0 $input-focus-width rgba($color, .25);\n }\n }\n }\n }\n}\n","// Gradients\n\n@mixin gradient-bg($color) {\n @if $enable-gradients {\n background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x;\n } @else {\n background-color: $color;\n }\n}\n\n// Horizontal gradient, from left to right\n//\n// Creates two color stops, start and end, by specifying a color and position for each color stop.\n@mixin gradient-x($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {\n background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent);\n background-repeat: repeat-x;\n}\n\n// Vertical gradient, from top to bottom\n//\n// Creates two color stops, start and end, by specifying a color and position for each color stop.\n@mixin gradient-y($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {\n background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent);\n background-repeat: repeat-x;\n}\n\n@mixin gradient-directional($start-color: $gray-700, $end-color: $gray-800, $deg: 45deg) {\n background-image: linear-gradient($deg, $start-color, $end-color);\n background-repeat: repeat-x;\n}\n@mixin gradient-x-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {\n background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color);\n background-repeat: no-repeat;\n}\n@mixin gradient-y-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {\n background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color);\n background-repeat: no-repeat;\n}\n@mixin gradient-radial($inner-color: $gray-700, $outer-color: $gray-800) {\n background-image: radial-gradient(circle, $inner-color, $outer-color);\n background-repeat: no-repeat;\n}\n@mixin gradient-striped($color: rgba($white, .15), $angle: 45deg) {\n background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Base styles\n//\n\n.btn {\n display: inline-block;\n font-family: $btn-font-family;\n font-weight: $btn-font-weight;\n color: $body-color;\n text-align: center;\n text-decoration: if($link-decoration == none, null, none);\n white-space: $btn-white-space;\n vertical-align: middle;\n user-select: none;\n background-color: transparent;\n border: $btn-border-width solid transparent;\n @include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $btn-line-height, $btn-border-radius);\n @include transition($btn-transition);\n\n @include hover() {\n color: $body-color;\n text-decoration: none;\n }\n\n &:focus,\n &.focus {\n outline: 0;\n box-shadow: $btn-focus-box-shadow;\n }\n\n // Disabled comes first so active can properly restyle\n &.disabled,\n &:disabled {\n opacity: $btn-disabled-opacity;\n @include box-shadow(none);\n }\n\n &:not(:disabled):not(.disabled) {\n cursor: if($enable-pointer-cursor-for-buttons, pointer, null);\n\n &:active,\n &.active {\n @include box-shadow($btn-active-box-shadow);\n\n &:focus {\n @include box-shadow($btn-focus-box-shadow, $btn-active-box-shadow);\n }\n }\n }\n}\n\n// Future-proof disabling of clicks on `<a>` elements\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n\n//\n// Alternate buttons\n//\n\n@each $color, $value in $theme-colors {\n .btn-#{$color} {\n @include button-variant($value, $value);\n }\n}\n\n@each $color, $value in $theme-colors {\n .btn-outline-#{$color} {\n @include button-outline-variant($value);\n }\n}\n\n\n//\n// Link buttons\n//\n\n// Make a button look and behave like a link\n.btn-link {\n font-weight: $font-weight-normal;\n color: $link-color;\n text-decoration: $link-decoration;\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n\n &:focus,\n &.focus {\n text-decoration: $link-hover-decoration;\n }\n\n &:disabled,\n &.disabled {\n color: $btn-link-disabled-color;\n pointer-events: none;\n }\n\n // No need for an active state here\n}\n\n\n//\n// Button Sizes\n//\n\n.btn-lg {\n @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-line-height-lg, $btn-border-radius-lg);\n}\n\n.btn-sm {\n @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-line-height-sm, $btn-border-radius-sm);\n}\n\n\n//\n// Block button\n//\n\n.btn-block {\n display: block;\n width: 100%;\n\n // Vertically space out multiple block buttons\n + .btn-block {\n margin-top: $btn-block-spacing-y;\n }\n}\n\n// Specificity overrides\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n &.btn-block {\n width: 100%;\n }\n}\n","// Button variants\n//\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n\n@mixin button-variant($background, $border, $hover-background: darken($background, 7.5%), $hover-border: darken($border, 10%), $active-background: darken($background, 10%), $active-border: darken($border, 12.5%)) {\n color: color-yiq($background);\n @include gradient-bg($background);\n border-color: $border;\n @include box-shadow($btn-box-shadow);\n\n @include hover() {\n color: color-yiq($hover-background);\n @include gradient-bg($hover-background);\n border-color: $hover-border;\n }\n\n &:focus,\n &.focus {\n color: color-yiq($hover-background);\n @include gradient-bg($hover-background);\n border-color: $hover-border;\n @if $enable-shadows {\n @include box-shadow($btn-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5));\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);\n }\n }\n\n // Disabled comes first so active can properly restyle\n &.disabled,\n &:disabled {\n color: color-yiq($background);\n background-color: $background;\n border-color: $border;\n // Remove CSS gradients if they're enabled\n @if $enable-gradients {\n background-image: none;\n }\n }\n\n &:not(:disabled):not(.disabled):active,\n &:not(:disabled):not(.disabled).active,\n .show > &.dropdown-toggle {\n color: color-yiq($active-background);\n background-color: $active-background;\n @if $enable-gradients {\n background-image: none; // Remove the gradient for the pressed/active state\n }\n border-color: $active-border;\n\n &:focus {\n @if $enable-shadows and $btn-active-box-shadow != none {\n @include box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5));\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);\n }\n }\n }\n}\n\n@mixin button-outline-variant($color, $color-hover: color-yiq($color), $active-background: $color, $active-border: $color) {\n color: $color;\n border-color: $color;\n\n @include hover() {\n color: $color-hover;\n background-color: $active-background;\n border-color: $active-border;\n }\n\n &:focus,\n &.focus {\n box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);\n }\n\n &.disabled,\n &:disabled {\n color: $color;\n background-color: transparent;\n }\n\n &:not(:disabled):not(.disabled):active,\n &:not(:disabled):not(.disabled).active,\n .show > &.dropdown-toggle {\n color: color-yiq($active-background);\n background-color: $active-background;\n border-color: $active-border;\n\n &:focus {\n @if $enable-shadows and $btn-active-box-shadow != none {\n @include box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width rgba($color, .5));\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);\n }\n }\n }\n}\n\n// Button sizes\n@mixin button-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {\n padding: $padding-y $padding-x;\n @include font-size($font-size);\n line-height: $line-height;\n // Manually declare to provide an override to the browser default\n @include border-radius($border-radius, 0);\n}\n",".fade {\n @include transition($transition-fade);\n\n &:not(.show) {\n opacity: 0;\n }\n}\n\n.collapse {\n &:not(.show) {\n display: none;\n }\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n @include transition($transition-collapse);\n}\n","// The dropdown wrapper (`<div>`)\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n\n // Generate the caret automatically\n @include caret();\n}\n\n// The dropdown menu\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: $zindex-dropdown;\n display: none; // none by default, but block on \"open\" of the menu\n float: left;\n min-width: $dropdown-min-width;\n padding: $dropdown-padding-y $dropdown-padding-x;\n margin: $dropdown-spacer 0 0; // override default ul\n @include font-size($dropdown-font-size);\n color: $dropdown-color;\n text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)\n list-style: none;\n background-color: $dropdown-bg;\n background-clip: padding-box;\n border: $dropdown-border-width solid $dropdown-border-color;\n @include border-radius($dropdown-border-radius);\n @include box-shadow($dropdown-box-shadow);\n}\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .dropdown-menu#{$infix}-left {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu#{$infix}-right {\n right: 0;\n left: auto;\n }\n }\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n// Just add .dropup after the standard .dropdown class and you're set.\n.dropup {\n .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: $dropdown-spacer;\n }\n\n .dropdown-toggle {\n @include caret(up);\n }\n}\n\n.dropright {\n .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: $dropdown-spacer;\n }\n\n .dropdown-toggle {\n @include caret(right);\n &::after {\n vertical-align: 0;\n }\n }\n}\n\n.dropleft {\n .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: $dropdown-spacer;\n }\n\n .dropdown-toggle {\n @include caret(left);\n &::before {\n vertical-align: 0;\n }\n }\n}\n\n// When enabled Popper.js, reset basic dropdown position\n// stylelint-disable-next-line no-duplicate-selectors\n.dropdown-menu {\n &[x-placement^=\"top\"],\n &[x-placement^=\"right\"],\n &[x-placement^=\"bottom\"],\n &[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n }\n}\n\n// Dividers (basically an `<hr>`) within the dropdown\n.dropdown-divider {\n @include nav-divider($dropdown-divider-bg, $dropdown-divider-margin-y, true);\n}\n\n// Links, buttons, and more within the dropdown menu\n//\n// `<button>`-specific styles are denoted with `// For <button>s`\n.dropdown-item {\n display: block;\n width: 100%; // For `<button>`s\n padding: $dropdown-item-padding-y $dropdown-item-padding-x;\n clear: both;\n font-weight: $font-weight-normal;\n color: $dropdown-link-color;\n text-align: inherit; // For `<button>`s\n text-decoration: if($link-decoration == none, null, none);\n white-space: nowrap; // prevent links from randomly breaking onto new lines\n background-color: transparent; // For `<button>`s\n border: 0; // For `<button>`s\n\n // Prevent dropdown overflow if there's no padding\n // See https://github.com/twbs/bootstrap/pull/27703\n @if $dropdown-padding-y == 0 {\n &:first-child {\n @include border-top-radius($dropdown-inner-border-radius);\n }\n\n &:last-child {\n @include border-bottom-radius($dropdown-inner-border-radius);\n }\n }\n\n @include hover-focus() {\n color: $dropdown-link-hover-color;\n text-decoration: none;\n @include gradient-bg($dropdown-link-hover-bg);\n }\n\n &.active,\n &:active {\n color: $dropdown-link-active-color;\n text-decoration: none;\n @include gradient-bg($dropdown-link-active-bg);\n }\n\n &.disabled,\n &:disabled {\n color: $dropdown-link-disabled-color;\n pointer-events: none;\n background-color: transparent;\n // Remove CSS gradients if they're enabled\n @if $enable-gradients {\n background-image: none;\n }\n }\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n// Dropdown section headers\n.dropdown-header {\n display: block;\n padding: $dropdown-header-padding;\n margin-bottom: 0; // for use with heading elements\n @include font-size($font-size-sm);\n color: $dropdown-header-color;\n white-space: nowrap; // as with > li > a\n}\n\n// Dropdown text\n.dropdown-item-text {\n display: block;\n padding: $dropdown-item-padding-y $dropdown-item-padding-x;\n color: $dropdown-link-color;\n}\n","@mixin caret-down() {\n border-top: $caret-width solid;\n border-right: $caret-width solid transparent;\n border-bottom: 0;\n border-left: $caret-width solid transparent;\n}\n\n@mixin caret-up() {\n border-top: 0;\n border-right: $caret-width solid transparent;\n border-bottom: $caret-width solid;\n border-left: $caret-width solid transparent;\n}\n\n@mixin caret-right() {\n border-top: $caret-width solid transparent;\n border-right: 0;\n border-bottom: $caret-width solid transparent;\n border-left: $caret-width solid;\n}\n\n@mixin caret-left() {\n border-top: $caret-width solid transparent;\n border-right: $caret-width solid;\n border-bottom: $caret-width solid transparent;\n}\n\n@mixin caret($direction: down) {\n @if $enable-caret {\n &::after {\n display: inline-block;\n margin-left: $caret-spacing;\n vertical-align: $caret-vertical-align;\n content: \"\";\n @if $direction == down {\n @include caret-down();\n } @else if $direction == up {\n @include caret-up();\n } @else if $direction == right {\n @include caret-right();\n }\n }\n\n @if $direction == left {\n &::after {\n display: none;\n }\n\n &::before {\n display: inline-block;\n margin-right: $caret-spacing;\n vertical-align: $caret-vertical-align;\n content: \"\";\n @include caret-left();\n }\n }\n\n &:empty::after {\n margin-left: 0;\n }\n }\n}\n","// Horizontal dividers\n//\n// Dividers (basically an hr) within dropdowns and nav lists\n\n@mixin nav-divider($color: $nav-divider-color, $margin-y: $nav-divider-margin-y, $ignore-warning: false) {\n height: 0;\n margin: $margin-y 0;\n overflow: hidden;\n border-top: 1px solid $color;\n @include deprecate(\"The `nav-divider()` mixin\", \"v4.4.0\", \"v5\", $ignore-warning);\n}\n","// stylelint-disable selector-no-qualifying-type\n\n// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle; // match .btn alignment given font-size hack above\n\n > .btn {\n position: relative;\n flex: 1 1 auto;\n\n // Bring the hover, focused, and \"active\" buttons to the front to overlay\n // the borders properly\n @include hover() {\n z-index: 1;\n }\n &:focus,\n &:active,\n &.active {\n z-index: 1;\n }\n }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n\n .input-group {\n width: auto;\n }\n}\n\n.btn-group {\n // Prevent double borders when buttons are next to each other\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) {\n margin-left: -$btn-border-width;\n }\n\n // Reset rounded corners\n > .btn:not(:last-child):not(.dropdown-toggle),\n > .btn-group:not(:last-child) > .btn {\n @include border-right-radius(0);\n }\n\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) > .btn {\n @include border-left-radius(0);\n }\n}\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-sm > .btn { @extend .btn-sm; }\n.btn-group-lg > .btn { @extend .btn-lg; }\n\n\n//\n// Split button dropdowns\n//\n\n.dropdown-toggle-split {\n padding-right: $btn-padding-x * .75;\n padding-left: $btn-padding-x * .75;\n\n &::after,\n .dropup &::after,\n .dropright &::after {\n margin-left: 0;\n }\n\n .dropleft &::before {\n margin-right: 0;\n }\n}\n\n.btn-sm + .dropdown-toggle-split {\n padding-right: $btn-padding-x-sm * .75;\n padding-left: $btn-padding-x-sm * .75;\n}\n\n.btn-lg + .dropdown-toggle-split {\n padding-right: $btn-padding-x-lg * .75;\n padding-left: $btn-padding-x-lg * .75;\n}\n\n\n// The clickable button for toggling the menu\n// Set the same inset shadow as the :active state\n.btn-group.show .dropdown-toggle {\n @include box-shadow($btn-active-box-shadow);\n\n // Show no shadow for `.btn-link` since it has no other button styles.\n &.btn-link {\n @include box-shadow(none);\n }\n}\n\n\n//\n// Vertical button groups\n//\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n\n > .btn,\n > .btn-group {\n width: 100%;\n }\n\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) {\n margin-top: -$btn-border-width;\n }\n\n // Reset rounded corners\n > .btn:not(:last-child):not(.dropdown-toggle),\n > .btn-group:not(:last-child) > .btn {\n @include border-bottom-radius(0);\n }\n\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) > .btn {\n @include border-top-radius(0);\n }\n}\n\n\n// Checkbox and radio options\n//\n// In order to support the browser's form validation feedback, powered by the\n// `required` attribute, we have to \"hide\" the inputs via `clip`. We cannot use\n// `display: none;` or `visibility: hidden;` as that also hides the popover.\n// Simply visually hiding the inputs via `opacity` would leave them clickable in\n// certain cases which is prevented by using `clip` and `pointer-events`.\n// This way, we ensure a DOM element is visible to position the popover from.\n//\n// See https://github.com/twbs/bootstrap/pull/12794 and\n// https://github.com/twbs/bootstrap/pull/14559 for more information.\n\n.btn-group-toggle {\n > .btn,\n > .btn-group > .btn {\n margin-bottom: 0; // Override default `<label>` value\n\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Base styles\n//\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap; // For form validation feedback\n align-items: stretch;\n width: 100%;\n\n > .form-control,\n > .form-control-plaintext,\n > .custom-select,\n > .custom-file {\n position: relative; // For focus state's z-index\n flex: 1 1 auto;\n width: 1%;\n min-width: 0; // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size\n margin-bottom: 0;\n\n + .form-control,\n + .custom-select,\n + .custom-file {\n margin-left: -$input-border-width;\n }\n }\n\n // Bring the \"active\" form control to the top of surrounding elements\n > .form-control:focus,\n > .custom-select:focus,\n > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n }\n\n // Bring the custom file input above the label\n > .custom-file .custom-file-input:focus {\n z-index: 4;\n }\n\n > .form-control,\n > .custom-select {\n &:not(:last-child) { @include border-right-radius(0); }\n &:not(:first-child) { @include border-left-radius(0); }\n }\n\n // Custom file inputs have more complex markup, thus requiring different\n // border-radius overrides.\n > .custom-file {\n display: flex;\n align-items: center;\n\n &:not(:last-child) .custom-file-label,\n &:not(:last-child) .custom-file-label::after { @include border-right-radius(0); }\n &:not(:first-child) .custom-file-label { @include border-left-radius(0); }\n }\n}\n\n\n// Prepend and append\n//\n// While it requires one extra layer of HTML for each, dedicated prepend and\n// append elements allow us to 1) be less clever, 2) simplify our selectors, and\n// 3) support HTML5 form validation.\n\n.input-group-prepend,\n.input-group-append {\n display: flex;\n\n // Ensure buttons are always above inputs for more visually pleasing borders.\n // This isn't needed for `.input-group-text` since it shares the same border-color\n // as our inputs.\n .btn {\n position: relative;\n z-index: 2;\n\n &:focus {\n z-index: 3;\n }\n }\n\n .btn + .btn,\n .btn + .input-group-text,\n .input-group-text + .input-group-text,\n .input-group-text + .btn {\n margin-left: -$input-border-width;\n }\n}\n\n.input-group-prepend { margin-right: -$input-border-width; }\n.input-group-append { margin-left: -$input-border-width; }\n\n\n// Textual addons\n//\n// Serves as a catch-all element for any text or radio/checkbox input you wish\n// to prepend or append to an input.\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: $input-padding-y $input-padding-x;\n margin-bottom: 0; // Allow use of <label> elements by overriding our default margin-bottom\n @include font-size($input-font-size); // Match inputs\n font-weight: $font-weight-normal;\n line-height: $input-line-height;\n color: $input-group-addon-color;\n text-align: center;\n white-space: nowrap;\n background-color: $input-group-addon-bg;\n border: $input-border-width solid $input-group-addon-border-color;\n @include border-radius($input-border-radius);\n\n // Nuke default margins from checkboxes and radios to vertically center within.\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n margin-top: 0;\n }\n}\n\n\n// Sizing\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: $input-height-lg;\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: $input-padding-y-lg $input-padding-x-lg;\n @include font-size($input-font-size-lg);\n line-height: $input-line-height-lg;\n @include border-radius($input-border-radius-lg);\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: $input-height-sm;\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: $input-padding-y-sm $input-padding-x-sm;\n @include font-size($input-font-size-sm);\n line-height: $input-line-height-sm;\n @include border-radius($input-border-radius-sm);\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: $custom-select-padding-x + $custom-select-indicator-padding;\n}\n\n\n// Prepend and append rounded corners\n//\n// These rulesets must come after the sizing ones to properly override sm and lg\n// border-radius values when extending. They're more specific than we'd like\n// with the `.input-group >` part, but without it, we cannot override the sizing.\n\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n @include border-right-radius(0);\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n @include border-left-radius(0);\n}\n","// Embedded icons from Open Iconic.\n// Released under MIT and copyright 2014 Waybury.\n// https://useiconic.com/open\n\n\n// Checkboxes and radios\n//\n// Base class takes care of all the key behavioral aspects.\n\n.custom-control {\n position: relative;\n z-index: 1;\n display: block;\n min-height: $font-size-base * $line-height-base;\n padding-left: $custom-control-gutter + $custom-control-indicator-size;\n color-adjust: exact; // Keep themed appearance for print\n}\n\n.custom-control-inline {\n display: inline-flex;\n margin-right: $custom-control-spacer-x;\n}\n\n.custom-control-input {\n position: absolute;\n left: 0;\n z-index: -1; // Put the input behind the label so it doesn't overlay text\n width: $custom-control-indicator-size;\n height: ($font-size-base * $line-height-base + $custom-control-indicator-size) / 2;\n opacity: 0;\n\n &:checked ~ .custom-control-label::before {\n color: $custom-control-indicator-checked-color;\n border-color: $custom-control-indicator-checked-border-color;\n @include gradient-bg($custom-control-indicator-checked-bg);\n @include box-shadow($custom-control-indicator-checked-box-shadow);\n }\n\n &:focus ~ .custom-control-label::before {\n // the mixin is not used here to make sure there is feedback\n @if $enable-shadows {\n box-shadow: $input-box-shadow, $custom-control-indicator-focus-box-shadow;\n } @else {\n box-shadow: $custom-control-indicator-focus-box-shadow;\n }\n }\n\n &:focus:not(:checked) ~ .custom-control-label::before {\n border-color: $custom-control-indicator-focus-border-color;\n }\n\n &:not(:disabled):active ~ .custom-control-label::before {\n color: $custom-control-indicator-active-color;\n background-color: $custom-control-indicator-active-bg;\n border-color: $custom-control-indicator-active-border-color;\n @include box-shadow($custom-control-indicator-active-box-shadow);\n }\n\n // Use [disabled] and :disabled to work around https://github.com/twbs/bootstrap/issues/28247\n &[disabled],\n &:disabled {\n ~ .custom-control-label {\n color: $custom-control-label-disabled-color;\n\n &::before {\n background-color: $custom-control-indicator-disabled-bg;\n }\n }\n }\n}\n\n// Custom control indicators\n//\n// Build the custom controls out of pseudo-elements.\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n color: $custom-control-label-color;\n vertical-align: top;\n cursor: $custom-control-cursor;\n\n // Background-color and (when enabled) gradient\n &::before {\n position: absolute;\n top: ($font-size-base * $line-height-base - $custom-control-indicator-size) / 2;\n left: -($custom-control-gutter + $custom-control-indicator-size);\n display: block;\n width: $custom-control-indicator-size;\n height: $custom-control-indicator-size;\n pointer-events: none;\n content: \"\";\n background-color: $custom-control-indicator-bg;\n border: $custom-control-indicator-border-color solid $custom-control-indicator-border-width;\n @include box-shadow($custom-control-indicator-box-shadow);\n }\n\n // Foreground (icon)\n &::after {\n position: absolute;\n top: ($font-size-base * $line-height-base - $custom-control-indicator-size) / 2;\n left: -($custom-control-gutter + $custom-control-indicator-size);\n display: block;\n width: $custom-control-indicator-size;\n height: $custom-control-indicator-size;\n content: \"\";\n background: no-repeat 50% / #{$custom-control-indicator-bg-size};\n }\n}\n\n\n// Checkboxes\n//\n// Tweak just a few things for checkboxes.\n\n.custom-checkbox {\n .custom-control-label::before {\n @include border-radius($custom-checkbox-indicator-border-radius);\n }\n\n .custom-control-input:checked ~ .custom-control-label {\n &::after {\n background-image: escape-svg($custom-checkbox-indicator-icon-checked);\n }\n }\n\n .custom-control-input:indeterminate ~ .custom-control-label {\n &::before {\n border-color: $custom-checkbox-indicator-indeterminate-border-color;\n @include gradient-bg($custom-checkbox-indicator-indeterminate-bg);\n @include box-shadow($custom-checkbox-indicator-indeterminate-box-shadow);\n }\n &::after {\n background-image: escape-svg($custom-checkbox-indicator-icon-indeterminate);\n }\n }\n\n .custom-control-input:disabled {\n &:checked ~ .custom-control-label::before {\n @include gradient-bg($custom-control-indicator-checked-disabled-bg);\n }\n &:indeterminate ~ .custom-control-label::before {\n @include gradient-bg($custom-control-indicator-checked-disabled-bg);\n }\n }\n}\n\n// Radios\n//\n// Tweak just a few things for radios.\n\n.custom-radio {\n .custom-control-label::before {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: $custom-radio-indicator-border-radius;\n }\n\n .custom-control-input:checked ~ .custom-control-label {\n &::after {\n background-image: escape-svg($custom-radio-indicator-icon-checked);\n }\n }\n\n .custom-control-input:disabled {\n &:checked ~ .custom-control-label::before {\n @include gradient-bg($custom-control-indicator-checked-disabled-bg);\n }\n }\n}\n\n\n// switches\n//\n// Tweak a few things for switches\n\n.custom-switch {\n padding-left: $custom-switch-width + $custom-control-gutter;\n\n .custom-control-label {\n &::before {\n left: -($custom-switch-width + $custom-control-gutter);\n width: $custom-switch-width;\n pointer-events: all;\n // stylelint-disable-next-line property-disallowed-list\n border-radius: $custom-switch-indicator-border-radius;\n }\n\n &::after {\n top: add(($font-size-base * $line-height-base - $custom-control-indicator-size) / 2, $custom-control-indicator-border-width * 2);\n left: add(-($custom-switch-width + $custom-control-gutter), $custom-control-indicator-border-width * 2);\n width: $custom-switch-indicator-size;\n height: $custom-switch-indicator-size;\n background-color: $custom-control-indicator-border-color;\n // stylelint-disable-next-line property-disallowed-list\n border-radius: $custom-switch-indicator-border-radius;\n @include transition(transform .15s ease-in-out, $custom-forms-transition);\n }\n }\n\n .custom-control-input:checked ~ .custom-control-label {\n &::after {\n background-color: $custom-control-indicator-bg;\n transform: translateX($custom-switch-width - $custom-control-indicator-size);\n }\n }\n\n .custom-control-input:disabled {\n &:checked ~ .custom-control-label::before {\n @include gradient-bg($custom-control-indicator-checked-disabled-bg);\n }\n }\n}\n\n\n// Select\n//\n// Replaces the browser default select with a custom one, mostly pulled from\n// https://primer.github.io/.\n//\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: $custom-select-height;\n padding: $custom-select-padding-y ($custom-select-padding-x + $custom-select-indicator-padding) $custom-select-padding-y $custom-select-padding-x;\n font-family: $custom-select-font-family;\n @include font-size($custom-select-font-size);\n font-weight: $custom-select-font-weight;\n line-height: $custom-select-line-height;\n color: $custom-select-color;\n vertical-align: middle;\n background: $custom-select-bg $custom-select-background;\n border: $custom-select-border-width solid $custom-select-border-color;\n @include border-radius($custom-select-border-radius, 0);\n @include box-shadow($custom-select-box-shadow);\n appearance: none;\n\n &:focus {\n border-color: $custom-select-focus-border-color;\n outline: 0;\n @if $enable-shadows {\n @include box-shadow($custom-select-box-shadow, $custom-select-focus-box-shadow);\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: $custom-select-focus-box-shadow;\n }\n\n &::-ms-value {\n // For visual consistency with other platforms/browsers,\n // suppress the default white text on blue background highlight given to\n // the selected option text when the (still closed) <select> receives focus\n // in IE and (under certain conditions) Edge.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n }\n\n &[multiple],\n &[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: $custom-select-padding-x;\n background-image: none;\n }\n\n &:disabled {\n color: $custom-select-disabled-color;\n background-color: $custom-select-disabled-bg;\n }\n\n // Hides the default caret in IE11\n &::-ms-expand {\n display: none;\n }\n\n // Remove outline from select box in FF\n &:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 $custom-select-color;\n }\n}\n\n.custom-select-sm {\n height: $custom-select-height-sm;\n padding-top: $custom-select-padding-y-sm;\n padding-bottom: $custom-select-padding-y-sm;\n padding-left: $custom-select-padding-x-sm;\n @include font-size($custom-select-font-size-sm);\n}\n\n.custom-select-lg {\n height: $custom-select-height-lg;\n padding-top: $custom-select-padding-y-lg;\n padding-bottom: $custom-select-padding-y-lg;\n padding-left: $custom-select-padding-x-lg;\n @include font-size($custom-select-font-size-lg);\n}\n\n\n// File\n//\n// Custom file input.\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: $custom-file-height;\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: $custom-file-height;\n margin: 0;\n opacity: 0;\n\n &:focus ~ .custom-file-label {\n border-color: $custom-file-focus-border-color;\n box-shadow: $custom-file-focus-box-shadow;\n }\n\n // Use [disabled] and :disabled to work around https://github.com/twbs/bootstrap/issues/28247\n &[disabled] ~ .custom-file-label,\n &:disabled ~ .custom-file-label {\n background-color: $custom-file-disabled-bg;\n }\n\n @each $lang, $value in $custom-file-text {\n &:lang(#{$lang}) ~ .custom-file-label::after {\n content: $value;\n }\n }\n\n ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n }\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: $custom-file-height;\n padding: $custom-file-padding-y $custom-file-padding-x;\n font-family: $custom-file-font-family;\n font-weight: $custom-file-font-weight;\n line-height: $custom-file-line-height;\n color: $custom-file-color;\n background-color: $custom-file-bg;\n border: $custom-file-border-width solid $custom-file-border-color;\n @include border-radius($custom-file-border-radius);\n @include box-shadow($custom-file-box-shadow);\n\n &::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: $custom-file-height-inner;\n padding: $custom-file-padding-y $custom-file-padding-x;\n line-height: $custom-file-line-height;\n color: $custom-file-button-color;\n content: \"Browse\";\n @include gradient-bg($custom-file-button-bg);\n border-left: inherit;\n @include border-radius(0 $custom-file-border-radius $custom-file-border-radius 0);\n }\n}\n\n// Range\n//\n// Style range inputs the same across browsers. Vendor-specific rules for pseudo\n// elements cannot be mixed. As such, there are no shared styles for focus or\n// active states on prefixed selectors.\n\n.custom-range {\n width: 100%;\n height: add($custom-range-thumb-height, $custom-range-thumb-focus-box-shadow-width * 2);\n padding: 0; // Need to reset padding\n background-color: transparent;\n appearance: none;\n\n &:focus {\n outline: none;\n\n // Pseudo-elements must be split across multiple rulesets to have an effect.\n // No box-shadow() mixin for focus accessibility.\n &::-webkit-slider-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }\n &::-moz-range-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }\n &::-ms-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }\n }\n\n &::-moz-focus-outer {\n border: 0;\n }\n\n &::-webkit-slider-thumb {\n width: $custom-range-thumb-width;\n height: $custom-range-thumb-height;\n margin-top: ($custom-range-track-height - $custom-range-thumb-height) / 2; // Webkit specific\n @include gradient-bg($custom-range-thumb-bg);\n border: $custom-range-thumb-border;\n @include border-radius($custom-range-thumb-border-radius);\n @include box-shadow($custom-range-thumb-box-shadow);\n @include transition($custom-forms-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($custom-range-thumb-active-bg);\n }\n }\n\n &::-webkit-slider-runnable-track {\n width: $custom-range-track-width;\n height: $custom-range-track-height;\n color: transparent; // Why?\n cursor: $custom-range-track-cursor;\n background-color: $custom-range-track-bg;\n border-color: transparent;\n @include border-radius($custom-range-track-border-radius);\n @include box-shadow($custom-range-track-box-shadow);\n }\n\n &::-moz-range-thumb {\n width: $custom-range-thumb-width;\n height: $custom-range-thumb-height;\n @include gradient-bg($custom-range-thumb-bg);\n border: $custom-range-thumb-border;\n @include border-radius($custom-range-thumb-border-radius);\n @include box-shadow($custom-range-thumb-box-shadow);\n @include transition($custom-forms-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($custom-range-thumb-active-bg);\n }\n }\n\n &::-moz-range-track {\n width: $custom-range-track-width;\n height: $custom-range-track-height;\n color: transparent;\n cursor: $custom-range-track-cursor;\n background-color: $custom-range-track-bg;\n border-color: transparent; // Firefox specific?\n @include border-radius($custom-range-track-border-radius);\n @include box-shadow($custom-range-track-box-shadow);\n }\n\n &::-ms-thumb {\n width: $custom-range-thumb-width;\n height: $custom-range-thumb-height;\n margin-top: 0; // Edge specific\n margin-right: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.\n margin-left: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.\n @include gradient-bg($custom-range-thumb-bg);\n border: $custom-range-thumb-border;\n @include border-radius($custom-range-thumb-border-radius);\n @include box-shadow($custom-range-thumb-box-shadow);\n @include transition($custom-forms-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($custom-range-thumb-active-bg);\n }\n }\n\n &::-ms-track {\n width: $custom-range-track-width;\n height: $custom-range-track-height;\n color: transparent;\n cursor: $custom-range-track-cursor;\n background-color: transparent;\n border-color: transparent;\n border-width: $custom-range-thumb-height / 2;\n @include box-shadow($custom-range-track-box-shadow);\n }\n\n &::-ms-fill-lower {\n background-color: $custom-range-track-bg;\n @include border-radius($custom-range-track-border-radius);\n }\n\n &::-ms-fill-upper {\n margin-right: 15px; // arbitrary?\n background-color: $custom-range-track-bg;\n @include border-radius($custom-range-track-border-radius);\n }\n\n &:disabled {\n &::-webkit-slider-thumb {\n background-color: $custom-range-thumb-disabled-bg;\n }\n\n &::-webkit-slider-runnable-track {\n cursor: default;\n }\n\n &::-moz-range-thumb {\n background-color: $custom-range-thumb-disabled-bg;\n }\n\n &::-moz-range-track {\n cursor: default;\n }\n\n &::-ms-thumb {\n background-color: $custom-range-thumb-disabled-bg;\n }\n }\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n @include transition($custom-forms-transition);\n}\n","// Base class\n//\n// Kickstart any navigation component with a set of style resets. Works with\n// `<nav>`s, `<ul>`s or `<ol>`s.\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: $nav-link-padding-y $nav-link-padding-x;\n text-decoration: if($link-decoration == none, null, none);\n\n @include hover-focus() {\n text-decoration: none;\n }\n\n // Disabled state lightens text\n &.disabled {\n color: $nav-link-disabled-color;\n pointer-events: none;\n cursor: default;\n }\n}\n\n//\n// Tabs\n//\n\n.nav-tabs {\n border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color;\n\n .nav-item {\n margin-bottom: -$nav-tabs-border-width;\n }\n\n .nav-link {\n border: $nav-tabs-border-width solid transparent;\n @include border-top-radius($nav-tabs-border-radius);\n\n @include hover-focus() {\n border-color: $nav-tabs-link-hover-border-color;\n }\n\n &.disabled {\n color: $nav-link-disabled-color;\n background-color: transparent;\n border-color: transparent;\n }\n }\n\n .nav-link.active,\n .nav-item.show .nav-link {\n color: $nav-tabs-link-active-color;\n background-color: $nav-tabs-link-active-bg;\n border-color: $nav-tabs-link-active-border-color;\n }\n\n .dropdown-menu {\n // Make dropdown border overlap tab border\n margin-top: -$nav-tabs-border-width;\n // Remove the top rounded corners here since there is a hard edge above the menu\n @include border-top-radius(0);\n }\n}\n\n\n//\n// Pills\n//\n\n.nav-pills {\n .nav-link {\n @include border-radius($nav-pills-border-radius);\n }\n\n .nav-link.active,\n .show > .nav-link {\n color: $nav-pills-link-active-color;\n background-color: $nav-pills-link-active-bg;\n }\n}\n\n\n//\n// Justified variants\n//\n\n.nav-fill {\n > .nav-link,\n .nav-item {\n flex: 1 1 auto;\n text-align: center;\n }\n}\n\n.nav-justified {\n > .nav-link,\n .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n }\n}\n\n\n// Tabbable tabs\n//\n// Hide tabbable panes to start, show them when `.active`\n\n.tab-content {\n > .tab-pane {\n display: none;\n }\n > .active {\n display: block;\n }\n}\n","// Contents\n//\n// Navbar\n// Navbar brand\n// Navbar nav\n// Navbar text\n// Navbar divider\n// Responsive navbar\n// Navbar position\n// Navbar themes\n\n\n// Navbar\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap; // allow us to do the line break for collapsing content\n align-items: center;\n justify-content: space-between; // space out brand from logo\n padding: $navbar-padding-y $navbar-padding-x;\n\n // Because flex properties aren't inherited, we need to redeclare these first\n // few properties so that content nested within behave properly.\n %container-flex-properties {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n }\n\n .container,\n .container-fluid {\n @extend %container-flex-properties;\n }\n\n @each $breakpoint, $container-max-width in $container-max-widths {\n > .container#{breakpoint-infix($breakpoint, $container-max-widths)} {\n @extend %container-flex-properties;\n }\n }\n}\n\n\n// Navbar brand\n//\n// Used for brand, project, or site names.\n\n.navbar-brand {\n display: inline-block;\n padding-top: $navbar-brand-padding-y;\n padding-bottom: $navbar-brand-padding-y;\n margin-right: $navbar-padding-x;\n @include font-size($navbar-brand-font-size);\n line-height: inherit;\n white-space: nowrap;\n\n @include hover-focus() {\n text-decoration: none;\n }\n}\n\n\n// Navbar nav\n//\n// Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`).\n\n.navbar-nav {\n display: flex;\n flex-direction: column; // cannot use `inherit` to get the `.navbar`s value\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n\n .nav-link {\n padding-right: 0;\n padding-left: 0;\n }\n\n .dropdown-menu {\n position: static;\n float: none;\n }\n}\n\n\n// Navbar text\n//\n//\n\n.navbar-text {\n display: inline-block;\n padding-top: $nav-link-padding-y;\n padding-bottom: $nav-link-padding-y;\n}\n\n\n// Responsive navbar\n//\n// Custom styles for responsive collapsing and toggling of navbar contents.\n// Powered by the collapse Bootstrap JavaScript plugin.\n\n// When collapsed, prevent the toggleable navbar contents from appearing in\n// the default flexbox row orientation. Requires the use of `flex-wrap: wrap`\n// on the `.navbar` parent.\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n // For always expanded or extra full navbars, ensure content aligns itself\n // properly vertically. Can be easily overridden with flex utilities.\n align-items: center;\n}\n\n// Button for toggling the navbar when in its collapsed state\n.navbar-toggler {\n padding: $navbar-toggler-padding-y $navbar-toggler-padding-x;\n @include font-size($navbar-toggler-font-size);\n line-height: 1;\n background-color: transparent; // remove default button style\n border: $border-width solid transparent; // remove default button style\n @include border-radius($navbar-toggler-border-radius);\n\n @include hover-focus() {\n text-decoration: none;\n }\n}\n\n// Keep as a separate element so folks can easily override it with another icon\n// or image file as needed.\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n// Generate series of `.navbar-expand-*` responsive classes for configuring\n// where your navbar collapses.\n.navbar-expand {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n &#{$infix} {\n @include media-breakpoint-down($breakpoint) {\n %container-navbar-expand-#{$breakpoint} {\n padding-right: 0;\n padding-left: 0;\n }\n\n > .container,\n > .container-fluid {\n @extend %container-navbar-expand-#{$breakpoint};\n }\n\n @each $size, $container-max-width in $container-max-widths {\n > .container#{breakpoint-infix($size, $container-max-widths)} {\n @extend %container-navbar-expand-#{$breakpoint};\n }\n }\n }\n\n @include media-breakpoint-up($next) {\n flex-flow: row nowrap;\n justify-content: flex-start;\n\n .navbar-nav {\n flex-direction: row;\n\n .dropdown-menu {\n position: absolute;\n }\n\n .nav-link {\n padding-right: $navbar-nav-link-padding-x;\n padding-left: $navbar-nav-link-padding-x;\n }\n }\n\n // For nesting containers, have to redeclare for alignment purposes\n %container-nesting-#{$breakpoint} {\n flex-wrap: nowrap;\n }\n\n > .container,\n > .container-fluid {\n @extend %container-nesting-#{$breakpoint};\n }\n\n @each $size, $container-max-width in $container-max-widths {\n > .container#{breakpoint-infix($size, $container-max-widths)} {\n @extend %container-nesting-#{$breakpoint};\n }\n }\n\n .navbar-collapse {\n display: flex !important; // stylelint-disable-line declaration-no-important\n\n // Changes flex-bases to auto because of an IE10 bug\n flex-basis: auto;\n }\n\n .navbar-toggler {\n display: none;\n }\n }\n }\n }\n}\n\n\n// Navbar themes\n//\n// Styles for switching between navbars with light or dark background.\n\n// Dark links against a light background\n.navbar-light {\n .navbar-brand {\n color: $navbar-light-brand-color;\n\n @include hover-focus() {\n color: $navbar-light-brand-hover-color;\n }\n }\n\n .navbar-nav {\n .nav-link {\n color: $navbar-light-color;\n\n @include hover-focus() {\n color: $navbar-light-hover-color;\n }\n\n &.disabled {\n color: $navbar-light-disabled-color;\n }\n }\n\n .show > .nav-link,\n .active > .nav-link,\n .nav-link.show,\n .nav-link.active {\n color: $navbar-light-active-color;\n }\n }\n\n .navbar-toggler {\n color: $navbar-light-color;\n border-color: $navbar-light-toggler-border-color;\n }\n\n .navbar-toggler-icon {\n background-image: escape-svg($navbar-light-toggler-icon-bg);\n }\n\n .navbar-text {\n color: $navbar-light-color;\n a {\n color: $navbar-light-active-color;\n\n @include hover-focus() {\n color: $navbar-light-active-color;\n }\n }\n }\n}\n\n// White links against a dark background\n.navbar-dark {\n .navbar-brand {\n color: $navbar-dark-brand-color;\n\n @include hover-focus() {\n color: $navbar-dark-brand-hover-color;\n }\n }\n\n .navbar-nav {\n .nav-link {\n color: $navbar-dark-color;\n\n @include hover-focus() {\n color: $navbar-dark-hover-color;\n }\n\n &.disabled {\n color: $navbar-dark-disabled-color;\n }\n }\n\n .show > .nav-link,\n .active > .nav-link,\n .nav-link.show,\n .nav-link.active {\n color: $navbar-dark-active-color;\n }\n }\n\n .navbar-toggler {\n color: $navbar-dark-color;\n border-color: $navbar-dark-toggler-border-color;\n }\n\n .navbar-toggler-icon {\n background-image: escape-svg($navbar-dark-toggler-icon-bg);\n }\n\n .navbar-text {\n color: $navbar-dark-color;\n a {\n color: $navbar-dark-active-color;\n\n @include hover-focus() {\n color: $navbar-dark-active-color;\n }\n }\n }\n}\n","//\n// Base styles\n//\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106\n height: $card-height;\n word-wrap: break-word;\n background-color: $card-bg;\n background-clip: border-box;\n border: $card-border-width solid $card-border-color;\n @include border-radius($card-border-radius);\n\n > hr {\n margin-right: 0;\n margin-left: 0;\n }\n\n > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n\n &:first-child {\n border-top-width: 0;\n @include border-top-radius($card-inner-border-radius);\n }\n\n &:last-child {\n border-bottom-width: 0;\n @include border-bottom-radius($card-inner-border-radius);\n }\n }\n\n // Due to specificity of the above selector (`.card > .list-group`), we must\n // use a child selector here to prevent double borders.\n > .card-header + .list-group,\n > .list-group + .card-footer {\n border-top: 0;\n }\n}\n\n.card-body {\n // Enable `flex-grow: 1` for decks and groups so that card blocks take up\n // as much space as possible, ensuring footers are aligned to the bottom.\n flex: 1 1 auto;\n // Workaround for the image size bug in IE\n // See: https://github.com/twbs/bootstrap/pull/28855\n min-height: 1px;\n padding: $card-spacer-x;\n color: $card-color;\n}\n\n.card-title {\n margin-bottom: $card-spacer-y;\n}\n\n.card-subtitle {\n margin-top: -$card-spacer-y / 2;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link {\n @include hover() {\n text-decoration: none;\n }\n\n + .card-link {\n margin-left: $card-spacer-x;\n }\n}\n\n//\n// Optional textual caps\n//\n\n.card-header {\n padding: $card-spacer-y $card-spacer-x;\n margin-bottom: 0; // Removes the default margin-bottom of <hN>\n color: $card-cap-color;\n background-color: $card-cap-bg;\n border-bottom: $card-border-width solid $card-border-color;\n\n &:first-child {\n @include border-radius($card-inner-border-radius $card-inner-border-radius 0 0);\n }\n}\n\n.card-footer {\n padding: $card-spacer-y $card-spacer-x;\n color: $card-cap-color;\n background-color: $card-cap-bg;\n border-top: $card-border-width solid $card-border-color;\n\n &:last-child {\n @include border-radius(0 0 $card-inner-border-radius $card-inner-border-radius);\n }\n}\n\n\n//\n// Header navs\n//\n\n.card-header-tabs {\n margin-right: -$card-spacer-x / 2;\n margin-bottom: -$card-spacer-y;\n margin-left: -$card-spacer-x / 2;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -$card-spacer-x / 2;\n margin-left: -$card-spacer-x / 2;\n}\n\n// Card image\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: $card-img-overlay-padding;\n @include border-radius($card-inner-border-radius);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n flex-shrink: 0; // For IE: https://github.com/twbs/bootstrap/issues/29396\n width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch\n}\n\n.card-img,\n.card-img-top {\n @include border-top-radius($card-inner-border-radius);\n}\n\n.card-img,\n.card-img-bottom {\n @include border-bottom-radius($card-inner-border-radius);\n}\n\n\n// Card deck\n\n.card-deck {\n .card {\n margin-bottom: $card-deck-margin;\n }\n\n @include media-breakpoint-up(sm) {\n display: flex;\n flex-flow: row wrap;\n margin-right: -$card-deck-margin;\n margin-left: -$card-deck-margin;\n\n .card {\n // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n flex: 1 0 0%;\n margin-right: $card-deck-margin;\n margin-bottom: 0; // Override the default\n margin-left: $card-deck-margin;\n }\n }\n}\n\n\n//\n// Card groups\n//\n\n.card-group {\n // The child selector allows nested `.card` within `.card-group`\n // to display properly.\n > .card {\n margin-bottom: $card-group-margin;\n }\n\n @include media-breakpoint-up(sm) {\n display: flex;\n flex-flow: row wrap;\n // The child selector allows nested `.card` within `.card-group`\n // to display properly.\n > .card {\n // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n flex: 1 0 0%;\n margin-bottom: 0;\n\n + .card {\n margin-left: 0;\n border-left: 0;\n }\n\n // Handle rounded corners\n @if $enable-rounded {\n &:not(:last-child) {\n @include border-right-radius(0);\n\n .card-img-top,\n .card-header {\n // stylelint-disable-next-line property-disallowed-list\n border-top-right-radius: 0;\n }\n .card-img-bottom,\n .card-footer {\n // stylelint-disable-next-line property-disallowed-list\n border-bottom-right-radius: 0;\n }\n }\n\n &:not(:first-child) {\n @include border-left-radius(0);\n\n .card-img-top,\n .card-header {\n // stylelint-disable-next-line property-disallowed-list\n border-top-left-radius: 0;\n }\n .card-img-bottom,\n .card-footer {\n // stylelint-disable-next-line property-disallowed-list\n border-bottom-left-radius: 0;\n }\n }\n }\n }\n }\n}\n\n\n//\n// Columns\n//\n\n.card-columns {\n .card {\n margin-bottom: $card-columns-margin;\n }\n\n @include media-breakpoint-up(sm) {\n column-count: $card-columns-count;\n column-gap: $card-columns-gap;\n orphans: 1;\n widows: 1;\n\n .card {\n display: inline-block; // Don't let them vertically span multiple columns\n width: 100%; // Don't let their width change\n }\n }\n}\n\n\n//\n// Accordion\n//\n\n.accordion {\n overflow-anchor: none;\n\n > .card {\n overflow: hidden;\n\n &:not(:last-of-type) {\n border-bottom: 0;\n @include border-bottom-radius(0);\n }\n\n &:not(:first-of-type) {\n @include border-top-radius(0);\n }\n\n > .card-header {\n @include border-radius(0);\n margin-bottom: -$card-border-width;\n }\n }\n}\n",".breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: $breadcrumb-padding-y $breadcrumb-padding-x;\n margin-bottom: $breadcrumb-margin-bottom;\n @include font-size($breadcrumb-font-size);\n list-style: none;\n background-color: $breadcrumb-bg;\n @include border-radius($breadcrumb-border-radius);\n}\n\n.breadcrumb-item {\n display: flex;\n\n // The separator between breadcrumbs (by default, a forward-slash: \"/\")\n + .breadcrumb-item {\n padding-left: $breadcrumb-item-padding;\n\n &::before {\n display: inline-block; // Suppress underlining of the separator in modern browsers\n padding-right: $breadcrumb-item-padding;\n color: $breadcrumb-divider-color;\n content: escape-svg($breadcrumb-divider);\n }\n }\n\n // IE9-11 hack to properly handle hyperlink underlines for breadcrumbs built\n // without `<ul>`s. The `::before` pseudo-element generates an element\n // *within* the .breadcrumb-item and thereby inherits the `text-decoration`.\n //\n // To trick IE into suppressing the underline, we give the pseudo-element an\n // underline and then immediately remove it.\n + .breadcrumb-item:hover::before {\n text-decoration: underline;\n }\n // stylelint-disable-next-line no-duplicate-selectors\n + .breadcrumb-item:hover::before {\n text-decoration: none;\n }\n\n &.active {\n color: $breadcrumb-active-color;\n }\n}\n",".pagination {\n display: flex;\n @include list-unstyled();\n @include border-radius();\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: $pagination-padding-y $pagination-padding-x;\n margin-left: -$pagination-border-width;\n line-height: $pagination-line-height;\n color: $pagination-color;\n text-decoration: if($link-decoration == none, null, none);\n background-color: $pagination-bg;\n border: $pagination-border-width solid $pagination-border-color;\n\n &:hover {\n z-index: 2;\n color: $pagination-hover-color;\n text-decoration: none;\n background-color: $pagination-hover-bg;\n border-color: $pagination-hover-border-color;\n }\n\n &:focus {\n z-index: 3;\n outline: $pagination-focus-outline;\n box-shadow: $pagination-focus-box-shadow;\n }\n}\n\n.page-item {\n &:first-child {\n .page-link {\n margin-left: 0;\n @include border-left-radius($border-radius);\n }\n }\n &:last-child {\n .page-link {\n @include border-right-radius($border-radius);\n }\n }\n\n &.active .page-link {\n z-index: 3;\n color: $pagination-active-color;\n background-color: $pagination-active-bg;\n border-color: $pagination-active-border-color;\n }\n\n &.disabled .page-link {\n color: $pagination-disabled-color;\n pointer-events: none;\n // Opinionated: remove the \"hand\" cursor set previously for .page-link\n cursor: auto;\n background-color: $pagination-disabled-bg;\n border-color: $pagination-disabled-border-color;\n }\n}\n\n\n//\n// Sizing\n//\n\n.pagination-lg {\n @include pagination-size($pagination-padding-y-lg, $pagination-padding-x-lg, $font-size-lg, $line-height-lg, $border-radius-lg);\n}\n\n.pagination-sm {\n @include pagination-size($pagination-padding-y-sm, $pagination-padding-x-sm, $font-size-sm, $line-height-sm, $border-radius-sm);\n}\n","// Pagination\n\n@mixin pagination-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {\n .page-link {\n padding: $padding-y $padding-x;\n @include font-size($font-size);\n line-height: $line-height;\n }\n\n .page-item {\n &:first-child {\n .page-link {\n @include border-left-radius($border-radius);\n }\n }\n &:last-child {\n .page-link {\n @include border-right-radius($border-radius);\n }\n }\n }\n}\n","// Base class\n//\n// Requires one of the contextual, color modifier classes for `color` and\n// `background-color`.\n\n.badge {\n display: inline-block;\n padding: $badge-padding-y $badge-padding-x;\n @include font-size($badge-font-size);\n font-weight: $badge-font-weight;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n @include border-radius($badge-border-radius);\n @include transition($badge-transition);\n\n @at-root a#{&} {\n @include hover-focus() {\n text-decoration: none;\n }\n }\n\n // Empty badges collapse automatically\n &:empty {\n display: none;\n }\n}\n\n// Quick fix for badges in buttons\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n// Pill badges\n//\n// Make them extra rounded with a modifier to replace v3's badges.\n\n.badge-pill {\n padding-right: $badge-pill-padding-x;\n padding-left: $badge-pill-padding-x;\n @include border-radius($badge-pill-border-radius);\n}\n\n// Colors\n//\n// Contextual variations (linked badges get darker on :hover).\n\n@each $color, $value in $theme-colors {\n .badge-#{$color} {\n @include badge-variant($value);\n }\n}\n","@mixin badge-variant($bg) {\n color: color-yiq($bg);\n background-color: $bg;\n\n @at-root a#{&} {\n @include hover-focus() {\n color: color-yiq($bg);\n background-color: darken($bg, 10%);\n }\n\n &:focus,\n &.focus {\n outline: 0;\n box-shadow: 0 0 0 $badge-focus-width rgba($bg, .5);\n }\n }\n}\n",".jumbotron {\n padding: $jumbotron-padding ($jumbotron-padding / 2);\n margin-bottom: $jumbotron-padding;\n color: $jumbotron-color;\n background-color: $jumbotron-bg;\n @include border-radius($border-radius-lg);\n\n @include media-breakpoint-up(sm) {\n padding: ($jumbotron-padding * 2) $jumbotron-padding;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n @include border-radius(0);\n}\n","//\n// Base styles\n//\n\n.alert {\n position: relative;\n padding: $alert-padding-y $alert-padding-x;\n margin-bottom: $alert-margin-bottom;\n border: $alert-border-width solid transparent;\n @include border-radius($alert-border-radius);\n}\n\n// Headings for larger alerts\n.alert-heading {\n // Specified to prevent conflicts of changing $headings-color\n color: inherit;\n}\n\n// Provide class for links that match alerts\n.alert-link {\n font-weight: $alert-link-font-weight;\n}\n\n\n// Dismissible alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissible {\n padding-right: $close-font-size + $alert-padding-x * 2;\n\n // Adjust close link position\n .close {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n padding: $alert-padding-y $alert-padding-x;\n color: inherit;\n }\n}\n\n\n// Alternate styles\n//\n// Generate contextual modifier classes for colorizing the alert.\n\n@each $color, $value in $theme-colors {\n .alert-#{$color} {\n @include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level));\n }\n}\n","@mixin alert-variant($background, $border, $color) {\n color: $color;\n @include gradient-bg($background);\n border-color: $border;\n\n hr {\n border-top-color: darken($border, 5%);\n }\n\n .alert-link {\n color: darken($color, 10%);\n }\n}\n","// Disable animation if transitions are disabled\n@if $enable-transitions {\n @keyframes progress-bar-stripes {\n from { background-position: $progress-height 0; }\n to { background-position: 0 0; }\n }\n}\n\n.progress {\n display: flex;\n height: $progress-height;\n overflow: hidden; // force rounded corners by cropping it\n line-height: 0;\n @include font-size($progress-font-size);\n background-color: $progress-bg;\n @include border-radius($progress-border-radius);\n @include box-shadow($progress-box-shadow);\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: $progress-bar-color;\n text-align: center;\n white-space: nowrap;\n background-color: $progress-bar-bg;\n @include transition($progress-bar-transition);\n}\n\n.progress-bar-striped {\n @include gradient-striped();\n background-size: $progress-height $progress-height;\n}\n\n@if $enable-transitions {\n .progress-bar-animated {\n animation: progress-bar-stripes $progress-bar-animation-timing;\n\n @if $enable-prefers-reduced-motion-media-query {\n @media (prefers-reduced-motion: reduce) {\n animation: none;\n }\n }\n }\n}\n",".media {\n display: flex;\n align-items: flex-start;\n}\n\n.media-body {\n flex: 1;\n}\n","// Base class\n//\n// Easily usable on <ul>, <ol>, or <div>.\n\n.list-group {\n display: flex;\n flex-direction: column;\n\n // No need to set list-style: none; since .list-group-item is block level\n padding-left: 0; // reset padding because ul and ol\n margin-bottom: 0;\n @include border-radius($list-group-border-radius);\n}\n\n\n// Interactive list items\n//\n// Use anchor or button elements instead of `li`s or `div`s to create interactive\n// list items. Includes an extra `.active` modifier class for selected items.\n\n.list-group-item-action {\n width: 100%; // For `<button>`s (anchors become 100% by default though)\n color: $list-group-action-color;\n text-align: inherit; // For `<button>`s (anchors inherit)\n\n // Hover state\n @include hover-focus() {\n z-index: 1; // Place hover/focus items above their siblings for proper border styling\n color: $list-group-action-hover-color;\n text-decoration: none;\n background-color: $list-group-hover-bg;\n }\n\n &:active {\n color: $list-group-action-active-color;\n background-color: $list-group-action-active-bg;\n }\n}\n\n\n// Individual list items\n//\n// Use on `li`s or `div`s within the `.list-group` parent.\n\n.list-group-item {\n position: relative;\n display: block;\n padding: $list-group-item-padding-y $list-group-item-padding-x;\n color: $list-group-color;\n text-decoration: if($link-decoration == none, null, none);\n background-color: $list-group-bg;\n border: $list-group-border-width solid $list-group-border-color;\n\n &:first-child {\n @include border-top-radius(inherit);\n }\n\n &:last-child {\n @include border-bottom-radius(inherit);\n }\n\n &.disabled,\n &:disabled {\n color: $list-group-disabled-color;\n pointer-events: none;\n background-color: $list-group-disabled-bg;\n }\n\n // Include both here for `<a>`s and `<button>`s\n &.active {\n z-index: 2; // Place active items above their siblings for proper border styling\n color: $list-group-active-color;\n background-color: $list-group-active-bg;\n border-color: $list-group-active-border-color;\n }\n\n & + & {\n border-top-width: 0;\n\n &.active {\n margin-top: -$list-group-border-width;\n border-top-width: $list-group-border-width;\n }\n }\n}\n\n\n// Horizontal\n//\n// Change the layout of list group items from vertical (default) to horizontal.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .list-group-horizontal#{$infix} {\n flex-direction: row;\n\n > .list-group-item {\n &:first-child {\n @include border-bottom-left-radius($list-group-border-radius);\n @include border-top-right-radius(0);\n }\n\n &:last-child {\n @include border-top-right-radius($list-group-border-radius);\n @include border-bottom-left-radius(0);\n }\n\n &.active {\n margin-top: 0;\n }\n\n + .list-group-item {\n border-top-width: $list-group-border-width;\n border-left-width: 0;\n\n &.active {\n margin-left: -$list-group-border-width;\n border-left-width: $list-group-border-width;\n }\n }\n }\n }\n }\n}\n\n\n// Flush list items\n//\n// Remove borders and border-radius to keep list group items edge-to-edge. Most\n// useful within other components (e.g., cards).\n\n.list-group-flush {\n @include border-radius(0);\n\n > .list-group-item {\n border-width: 0 0 $list-group-border-width;\n\n &:last-child {\n border-bottom-width: 0;\n }\n }\n}\n\n\n// Contextual variants\n//\n// Add modifier classes to change text and background color on individual items.\n// Organizationally, this must come after the `:hover` states.\n\n@each $color, $value in $theme-colors {\n @include list-group-item-variant($color, theme-color-level($color, -9), theme-color-level($color, 6));\n}\n","// List Groups\n\n@mixin list-group-item-variant($state, $background, $color) {\n .list-group-item-#{$state} {\n color: $color;\n background-color: $background;\n\n &.list-group-item-action {\n @include hover-focus() {\n color: $color;\n background-color: darken($background, 5%);\n }\n\n &.active {\n color: $white;\n background-color: $color;\n border-color: $color;\n }\n }\n }\n}\n",".close {\n float: right;\n @include font-size($close-font-size);\n font-weight: $close-font-weight;\n line-height: 1;\n color: $close-color;\n text-shadow: $close-text-shadow;\n opacity: .5;\n\n // Override <a>'s hover style\n @include hover() {\n color: $close-color;\n text-decoration: none;\n }\n\n &:not(:disabled):not(.disabled) {\n @include hover-focus() {\n opacity: .75;\n }\n }\n}\n\n// Additional properties for button version\n// iOS requires the button element instead of an anchor tag.\n// If you want the anchor version, it requires `href=\"#\"`.\n// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n// stylelint-disable-next-line selector-no-qualifying-type\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n}\n\n// Future-proof disabling of clicks on `<a>` elements\n\n// stylelint-disable-next-line selector-no-qualifying-type\na.close.disabled {\n pointer-events: none;\n}\n",".toast {\n // Prevents from shrinking in IE11, when in a flex container\n // See https://github.com/twbs/bootstrap/issues/28341\n flex-basis: $toast-max-width;\n max-width: $toast-max-width;\n @include font-size($toast-font-size);\n color: $toast-color;\n background-color: $toast-background-color;\n background-clip: padding-box;\n border: $toast-border-width solid $toast-border-color;\n box-shadow: $toast-box-shadow;\n opacity: 0;\n @include border-radius($toast-border-radius);\n\n &:not(:last-child) {\n margin-bottom: $toast-padding-x;\n }\n\n &.showing {\n opacity: 1;\n }\n\n &.show {\n display: block;\n opacity: 1;\n }\n\n &.hide {\n display: none;\n }\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: $toast-padding-y $toast-padding-x;\n color: $toast-header-color;\n background-color: $toast-header-background-color;\n background-clip: padding-box;\n border-bottom: $toast-border-width solid $toast-header-border-color;\n @include border-top-radius(subtract($toast-border-radius, $toast-border-width));\n}\n\n.toast-body {\n padding: $toast-padding-x; // apply to both vertical and horizontal\n}\n","// .modal-open - body class for killing the scroll\n// .modal - container to scroll within\n// .modal-dialog - positioning shell for the actual modal\n// .modal-content - actual modal w/ bg and corners and stuff\n\n\n.modal-open {\n // Kill the scroll on the body\n overflow: hidden;\n\n .modal {\n overflow-x: hidden;\n overflow-y: auto;\n }\n}\n\n// Container that the modal scrolls within\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: $zindex-modal;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n // Prevent Chrome on Windows from adding a focus outline. For details, see\n // https://github.com/twbs/bootstrap/pull/10951.\n outline: 0;\n // We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a\n // gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342\n // See also https://github.com/twbs/bootstrap/issues/17695\n}\n\n// Shell div to position the modal with bottom padding\n.modal-dialog {\n position: relative;\n width: auto;\n margin: $modal-dialog-margin;\n // allow clicks to pass through for custom click handling to close modal\n pointer-events: none;\n\n // When fading in the modal, animate it to slide down\n .modal.fade & {\n @include transition($modal-transition);\n transform: $modal-fade-transform;\n }\n .modal.show & {\n transform: $modal-show-transform;\n }\n\n // When trying to close, animate focus to scale\n .modal.modal-static & {\n transform: $modal-scale-transform;\n }\n}\n\n.modal-dialog-scrollable {\n display: flex; // IE10/11\n max-height: subtract(100%, $modal-dialog-margin * 2);\n\n .modal-content {\n max-height: subtract(100vh, $modal-dialog-margin * 2); // IE10/11\n overflow: hidden;\n }\n\n .modal-header,\n .modal-footer {\n flex-shrink: 0;\n }\n\n .modal-body {\n overflow-y: auto;\n }\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: subtract(100%, $modal-dialog-margin * 2);\n\n // Ensure `modal-dialog-centered` extends the full height of the view (IE10/11)\n &::before {\n display: block; // IE10\n height: subtract(100vh, $modal-dialog-margin * 2);\n height: min-content; // Reset height to 0 except on IE\n content: \"\";\n }\n\n // Ensure `.modal-body` shows scrollbar (IE10/11)\n &.modal-dialog-scrollable {\n flex-direction: column;\n justify-content: center;\n height: 100%;\n\n .modal-content {\n max-height: none;\n }\n\n &::before {\n content: none;\n }\n }\n}\n\n// Actual modal\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog`\n // counteract the pointer-events: none; in the .modal-dialog\n color: $modal-content-color;\n pointer-events: auto;\n background-color: $modal-content-bg;\n background-clip: padding-box;\n border: $modal-content-border-width solid $modal-content-border-color;\n @include border-radius($modal-content-border-radius);\n @include box-shadow($modal-content-box-shadow-xs);\n // Remove focus outline from opened modal\n outline: 0;\n}\n\n// Modal background\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: $zindex-modal-backdrop;\n width: 100vw;\n height: 100vh;\n background-color: $modal-backdrop-bg;\n\n // Fade for backdrop\n &.fade { opacity: 0; }\n &.show { opacity: $modal-backdrop-opacity; }\n}\n\n// Modal header\n// Top section of the modal w/ title and dismiss\n.modal-header {\n display: flex;\n align-items: flex-start; // so the close btn always stays on the upper right corner\n justify-content: space-between; // Put modal header elements (title and dismiss) on opposite ends\n padding: $modal-header-padding;\n border-bottom: $modal-header-border-width solid $modal-header-border-color;\n @include border-top-radius($modal-content-inner-border-radius);\n\n .close {\n padding: $modal-header-padding;\n // auto on the left force icon to the right even when there is no .modal-title\n margin: (-$modal-header-padding-y) (-$modal-header-padding-x) (-$modal-header-padding-y) auto;\n }\n}\n\n// Title text within header\n.modal-title {\n margin-bottom: 0;\n line-height: $modal-title-line-height;\n}\n\n// Modal body\n// Where all modal content resides (sibling of .modal-header and .modal-footer)\n.modal-body {\n position: relative;\n // Enable `flex-grow: 1` so that the body take up as much space as possible\n // when there should be a fixed height on `.modal-dialog`.\n flex: 1 1 auto;\n padding: $modal-inner-padding;\n}\n\n// Footer (for actions)\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n align-items: center; // vertically center\n justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items\n padding: $modal-inner-padding - $modal-footer-margin-between / 2;\n border-top: $modal-footer-border-width solid $modal-footer-border-color;\n @include border-bottom-radius($modal-content-inner-border-radius);\n\n // Place margin between footer elements\n // This solution is far from ideal because of the universal selector usage,\n // but is needed to fix https://github.com/twbs/bootstrap/issues/24800\n > * {\n margin: $modal-footer-margin-between / 2;\n }\n}\n\n// Measure scrollbar width for padding body during modal show/hide\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n// Scale up the modal\n@include media-breakpoint-up(sm) {\n // Automatically set modal's width for larger viewports\n .modal-dialog {\n max-width: $modal-md;\n margin: $modal-dialog-margin-y-sm-up auto;\n }\n\n .modal-dialog-scrollable {\n max-height: subtract(100%, $modal-dialog-margin-y-sm-up * 2);\n\n .modal-content {\n max-height: subtract(100vh, $modal-dialog-margin-y-sm-up * 2);\n }\n }\n\n .modal-dialog-centered {\n min-height: subtract(100%, $modal-dialog-margin-y-sm-up * 2);\n\n &::before {\n height: subtract(100vh, $modal-dialog-margin-y-sm-up * 2);\n height: min-content;\n }\n }\n\n .modal-content {\n @include box-shadow($modal-content-box-shadow-sm-up);\n }\n\n .modal-sm { max-width: $modal-sm; }\n}\n\n@include media-breakpoint-up(lg) {\n .modal-lg,\n .modal-xl {\n max-width: $modal-lg;\n }\n}\n\n@include media-breakpoint-up(xl) {\n .modal-xl { max-width: $modal-xl; }\n}\n","// Base class\n.tooltip {\n position: absolute;\n z-index: $zindex-tooltip;\n display: block;\n margin: $tooltip-margin;\n // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.\n // So reset our font and text properties to avoid inheriting weird values.\n @include reset-text();\n @include font-size($tooltip-font-size);\n // Allow breaking very long words so they don't overflow the tooltip's bounds\n word-wrap: break-word;\n opacity: 0;\n\n &.show { opacity: $tooltip-opacity; }\n\n .arrow {\n position: absolute;\n display: block;\n width: $tooltip-arrow-width;\n height: $tooltip-arrow-height;\n\n &::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n }\n }\n}\n\n.bs-tooltip-top {\n padding: $tooltip-arrow-height 0;\n\n .arrow {\n bottom: 0;\n\n &::before {\n top: 0;\n border-width: $tooltip-arrow-height ($tooltip-arrow-width / 2) 0;\n border-top-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-right {\n padding: 0 $tooltip-arrow-height;\n\n .arrow {\n left: 0;\n width: $tooltip-arrow-height;\n height: $tooltip-arrow-width;\n\n &::before {\n right: 0;\n border-width: ($tooltip-arrow-width / 2) $tooltip-arrow-height ($tooltip-arrow-width / 2) 0;\n border-right-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-bottom {\n padding: $tooltip-arrow-height 0;\n\n .arrow {\n top: 0;\n\n &::before {\n bottom: 0;\n border-width: 0 ($tooltip-arrow-width / 2) $tooltip-arrow-height;\n border-bottom-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-left {\n padding: 0 $tooltip-arrow-height;\n\n .arrow {\n right: 0;\n width: $tooltip-arrow-height;\n height: $tooltip-arrow-width;\n\n &::before {\n left: 0;\n border-width: ($tooltip-arrow-width / 2) 0 ($tooltip-arrow-width / 2) $tooltip-arrow-height;\n border-left-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-auto {\n &[x-placement^=\"top\"] {\n @extend .bs-tooltip-top;\n }\n &[x-placement^=\"right\"] {\n @extend .bs-tooltip-right;\n }\n &[x-placement^=\"bottom\"] {\n @extend .bs-tooltip-bottom;\n }\n &[x-placement^=\"left\"] {\n @extend .bs-tooltip-left;\n }\n}\n\n// Wrapper for the tooltip content\n.tooltip-inner {\n max-width: $tooltip-max-width;\n padding: $tooltip-padding-y $tooltip-padding-x;\n color: $tooltip-color;\n text-align: center;\n background-color: $tooltip-bg;\n @include border-radius($tooltip-border-radius);\n}\n","@mixin reset-text() {\n font-family: $font-family-base;\n // We deliberately do NOT reset font-size or word-wrap.\n font-style: normal;\n font-weight: $font-weight-normal;\n line-height: $line-height-base;\n text-align: left; // Fallback for where `start` is not supported\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n}\n",".popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: $zindex-popover;\n display: block;\n max-width: $popover-max-width;\n // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.\n // So reset our font and text properties to avoid inheriting weird values.\n @include reset-text();\n @include font-size($popover-font-size);\n // Allow breaking very long words so they don't overflow the popover's bounds\n word-wrap: break-word;\n background-color: $popover-bg;\n background-clip: padding-box;\n border: $popover-border-width solid $popover-border-color;\n @include border-radius($popover-border-radius);\n @include box-shadow($popover-box-shadow);\n\n .arrow {\n position: absolute;\n display: block;\n width: $popover-arrow-width;\n height: $popover-arrow-height;\n margin: 0 $popover-border-radius;\n\n &::before,\n &::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n }\n }\n}\n\n.bs-popover-top {\n margin-bottom: $popover-arrow-height;\n\n > .arrow {\n bottom: subtract(-$popover-arrow-height, $popover-border-width);\n\n &::before {\n bottom: 0;\n border-width: $popover-arrow-height ($popover-arrow-width / 2) 0;\n border-top-color: $popover-arrow-outer-color;\n }\n\n &::after {\n bottom: $popover-border-width;\n border-width: $popover-arrow-height ($popover-arrow-width / 2) 0;\n border-top-color: $popover-arrow-color;\n }\n }\n}\n\n.bs-popover-right {\n margin-left: $popover-arrow-height;\n\n > .arrow {\n left: subtract(-$popover-arrow-height, $popover-border-width);\n width: $popover-arrow-height;\n height: $popover-arrow-width;\n margin: $popover-border-radius 0; // make sure the arrow does not touch the popover's rounded corners\n\n &::before {\n left: 0;\n border-width: ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2) 0;\n border-right-color: $popover-arrow-outer-color;\n }\n\n &::after {\n left: $popover-border-width;\n border-width: ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2) 0;\n border-right-color: $popover-arrow-color;\n }\n }\n}\n\n.bs-popover-bottom {\n margin-top: $popover-arrow-height;\n\n > .arrow {\n top: subtract(-$popover-arrow-height, $popover-border-width);\n\n &::before {\n top: 0;\n border-width: 0 ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2);\n border-bottom-color: $popover-arrow-outer-color;\n }\n\n &::after {\n top: $popover-border-width;\n border-width: 0 ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2);\n border-bottom-color: $popover-arrow-color;\n }\n }\n\n // This will remove the popover-header's border just below the arrow\n .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: $popover-arrow-width;\n margin-left: -$popover-arrow-width / 2;\n content: \"\";\n border-bottom: $popover-border-width solid $popover-header-bg;\n }\n}\n\n.bs-popover-left {\n margin-right: $popover-arrow-height;\n\n > .arrow {\n right: subtract(-$popover-arrow-height, $popover-border-width);\n width: $popover-arrow-height;\n height: $popover-arrow-width;\n margin: $popover-border-radius 0; // make sure the arrow does not touch the popover's rounded corners\n\n &::before {\n right: 0;\n border-width: ($popover-arrow-width / 2) 0 ($popover-arrow-width / 2) $popover-arrow-height;\n border-left-color: $popover-arrow-outer-color;\n }\n\n &::after {\n right: $popover-border-width;\n border-width: ($popover-arrow-width / 2) 0 ($popover-arrow-width / 2) $popover-arrow-height;\n border-left-color: $popover-arrow-color;\n }\n }\n}\n\n.bs-popover-auto {\n &[x-placement^=\"top\"] {\n @extend .bs-popover-top;\n }\n &[x-placement^=\"right\"] {\n @extend .bs-popover-right;\n }\n &[x-placement^=\"bottom\"] {\n @extend .bs-popover-bottom;\n }\n &[x-placement^=\"left\"] {\n @extend .bs-popover-left;\n }\n}\n\n\n// Offset the popover to account for the popover arrow\n.popover-header {\n padding: $popover-header-padding-y $popover-header-padding-x;\n margin-bottom: 0; // Reset the default from Reboot\n @include font-size($font-size-base);\n color: $popover-header-color;\n background-color: $popover-header-bg;\n border-bottom: $popover-border-width solid darken($popover-header-bg, 5%);\n @include border-top-radius($popover-inner-border-radius);\n\n &:empty {\n display: none;\n }\n}\n\n.popover-body {\n padding: $popover-body-padding-y $popover-body-padding-x;\n color: $popover-body-color;\n}\n","// Notes on the classes:\n//\n// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically)\n// even when their scroll action started on a carousel, but for compatibility (with Firefox)\n// we're preventing all actions instead\n// 2. The .carousel-item-left and .carousel-item-right is used to indicate where\n// the active slide is heading.\n// 3. .active.carousel-item is the current slide.\n// 4. .active.carousel-item-left and .active.carousel-item-right is the current\n// slide in its in-transition state. Only one of these occurs at a time.\n// 5. .carousel-item-next.carousel-item-left and .carousel-item-prev.carousel-item-right\n// is the upcoming slide in transition.\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n @include clearfix();\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n @include transition($carousel-transition);\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n transform: translateX(-100%);\n}\n\n\n//\n// Alternate transitions\n//\n\n.carousel-fade {\n .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n }\n\n .carousel-item.active,\n .carousel-item-next.carousel-item-left,\n .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n }\n\n .active.carousel-item-left,\n .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n @include transition(opacity 0s $carousel-transition-duration);\n }\n}\n\n\n//\n// Left/right controls for nav\n//\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n // Use flex for alignment (1-3)\n display: flex; // 1. allow flex styles\n align-items: center; // 2. vertically center contents\n justify-content: center; // 3. horizontally center contents\n width: $carousel-control-width;\n color: $carousel-control-color;\n text-align: center;\n opacity: $carousel-control-opacity;\n @include transition($carousel-control-transition);\n\n // Hover/focus state\n @include hover-focus() {\n color: $carousel-control-color;\n text-decoration: none;\n outline: 0;\n opacity: $carousel-control-hover-opacity;\n }\n}\n.carousel-control-prev {\n left: 0;\n @if $enable-gradients {\n background-image: linear-gradient(90deg, rgba($black, .25), rgba($black, .001));\n }\n}\n.carousel-control-next {\n right: 0;\n @if $enable-gradients {\n background-image: linear-gradient(270deg, rgba($black, .25), rgba($black, .001));\n }\n}\n\n// Icons for within\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: $carousel-control-icon-width;\n height: $carousel-control-icon-width;\n background: no-repeat 50% / 100% 100%;\n}\n.carousel-control-prev-icon {\n background-image: escape-svg($carousel-control-prev-icon-bg);\n}\n.carousel-control-next-icon {\n background-image: escape-svg($carousel-control-next-icon-bg);\n}\n\n\n// Optional indicator pips\n//\n// Add an ordered list with the following class and add a list item for each\n// slide your carousel holds.\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: flex;\n justify-content: center;\n padding-left: 0; // override <ol> default\n // Use the .carousel-control's width as margin so we don't overlay those\n margin-right: $carousel-control-width;\n margin-left: $carousel-control-width;\n list-style: none;\n\n li {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: $carousel-indicator-width;\n height: $carousel-indicator-height;\n margin-right: $carousel-indicator-spacer;\n margin-left: $carousel-indicator-spacer;\n text-indent: -999px;\n cursor: pointer;\n background-color: $carousel-indicator-active-bg;\n background-clip: padding-box;\n // Use transparent borders to increase the hit area by 10px on top and bottom.\n border-top: $carousel-indicator-hit-area-height solid transparent;\n border-bottom: $carousel-indicator-hit-area-height solid transparent;\n opacity: .5;\n @include transition($carousel-indicator-transition);\n }\n\n .active {\n opacity: 1;\n }\n}\n\n\n// Optional captions\n//\n//\n\n.carousel-caption {\n position: absolute;\n right: (100% - $carousel-caption-width) / 2;\n bottom: 20px;\n left: (100% - $carousel-caption-width) / 2;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: $carousel-caption-color;\n text-align: center;\n}\n","@mixin clearfix() {\n &::after {\n display: block;\n clear: both;\n content: \"\";\n }\n}\n","//\n// Rotating border\n//\n\n@keyframes spinner-border {\n to { transform: rotate(360deg); }\n}\n\n.spinner-border {\n display: inline-block;\n width: $spinner-width;\n height: $spinner-height;\n vertical-align: text-bottom;\n border: $spinner-border-width solid currentColor;\n border-right-color: transparent;\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 50%;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: $spinner-width-sm;\n height: $spinner-height-sm;\n border-width: $spinner-border-width-sm;\n}\n\n//\n// Growing circle\n//\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: $spinner-width;\n height: $spinner-height;\n vertical-align: text-bottom;\n background-color: currentColor;\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 50%;\n opacity: 0;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: $spinner-width-sm;\n height: $spinner-height-sm;\n}\n","// stylelint-disable declaration-no-important\n\n.align-baseline { vertical-align: baseline !important; } // Browser default\n.align-top { vertical-align: top !important; }\n.align-middle { vertical-align: middle !important; }\n.align-bottom { vertical-align: bottom !important; }\n.align-text-bottom { vertical-align: text-bottom !important; }\n.align-text-top { vertical-align: text-top !important; }\n","// stylelint-disable declaration-no-important\n\n// Contextual backgrounds\n\n@mixin bg-variant($parent, $color, $ignore-warning: false) {\n #{$parent} {\n background-color: $color !important;\n }\n a#{$parent},\n button#{$parent} {\n @include hover-focus() {\n background-color: darken($color, 10%) !important;\n }\n }\n @include deprecate(\"The `bg-variant` mixin\", \"v4.4.0\", \"v5\", $ignore-warning);\n}\n\n@mixin bg-gradient-variant($parent, $color, $ignore-warning: false) {\n #{$parent} {\n background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x !important;\n }\n @include deprecate(\"The `bg-gradient-variant` mixin\", \"v4.5.0\", \"v5\", $ignore-warning);\n}\n","// stylelint-disable declaration-no-important\n\n@each $color, $value in $theme-colors {\n @include bg-variant(\".bg-#{$color}\", $value, true);\n}\n\n@if $enable-gradients {\n @each $color, $value in $theme-colors {\n @include bg-gradient-variant(\".bg-gradient-#{$color}\", $value, true);\n }\n}\n\n.bg-white {\n background-color: $white !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n","// stylelint-disable property-disallowed-list, declaration-no-important\n\n//\n// Border\n//\n\n.border { border: $border-width solid $border-color !important; }\n.border-top { border-top: $border-width solid $border-color !important; }\n.border-right { border-right: $border-width solid $border-color !important; }\n.border-bottom { border-bottom: $border-width solid $border-color !important; }\n.border-left { border-left: $border-width solid $border-color !important; }\n\n.border-0 { border: 0 !important; }\n.border-top-0 { border-top: 0 !important; }\n.border-right-0 { border-right: 0 !important; }\n.border-bottom-0 { border-bottom: 0 !important; }\n.border-left-0 { border-left: 0 !important; }\n\n@each $color, $value in $theme-colors {\n .border-#{$color} {\n border-color: $value !important;\n }\n}\n\n.border-white {\n border-color: $white !important;\n}\n\n//\n// Border-radius\n//\n\n.rounded-sm {\n border-radius: $border-radius-sm !important;\n}\n\n.rounded {\n border-radius: $border-radius !important;\n}\n\n.rounded-top {\n border-top-left-radius: $border-radius !important;\n border-top-right-radius: $border-radius !important;\n}\n\n.rounded-right {\n border-top-right-radius: $border-radius !important;\n border-bottom-right-radius: $border-radius !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: $border-radius !important;\n border-bottom-left-radius: $border-radius !important;\n}\n\n.rounded-left {\n border-top-left-radius: $border-radius !important;\n border-bottom-left-radius: $border-radius !important;\n}\n\n.rounded-lg {\n border-radius: $border-radius-lg !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: $rounded-pill !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// Credit: Nicolas Gallagher and SUIT CSS.\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n\n &::before {\n display: block;\n content: \"\";\n }\n\n .embed-responsive-item,\n iframe,\n embed,\n object,\n video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n }\n}\n\n@each $embed-responsive-aspect-ratio in $embed-responsive-aspect-ratios {\n $embed-responsive-aspect-ratio-x: nth($embed-responsive-aspect-ratio, 1);\n $embed-responsive-aspect-ratio-y: nth($embed-responsive-aspect-ratio, 2);\n\n .embed-responsive-#{$embed-responsive-aspect-ratio-x}by#{$embed-responsive-aspect-ratio-y} {\n &::before {\n padding-top: percentage($embed-responsive-aspect-ratio-y / $embed-responsive-aspect-ratio-x);\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .float#{$infix}-left { float: left !important; }\n .float#{$infix}-right { float: right !important; }\n .float#{$infix}-none { float: none !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n@each $value in $user-selects {\n .user-select-#{$value} { user-select: $value !important; }\n}\n","// stylelint-disable declaration-no-important\n\n@each $value in $overflows {\n .overflow-#{$value} { overflow: $value !important; }\n}\n","// stylelint-disable declaration-no-important\n\n// Common values\n@each $position in $positions {\n .position-#{$position} { position: $position !important; }\n}\n\n// Shorthand\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: $zindex-fixed;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: $zindex-fixed;\n}\n\n.sticky-top {\n @supports (position: sticky) {\n position: sticky;\n top: 0;\n z-index: $zindex-sticky;\n }\n}\n","//\n// Screenreaders\n//\n\n.sr-only {\n @include sr-only();\n}\n\n.sr-only-focusable {\n @include sr-only-focusable();\n}\n","// Only display content to screen readers\n//\n// See: https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/\n// See: https://hugogiraudel.com/2016/10/13/css-hide-and-seek/\n\n@mixin sr-only() {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px; // Fix for https://github.com/twbs/bootstrap/issues/25686\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n//\n// Useful for \"Skip to main content\" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n//\n// Credit: HTML5 Boilerplate\n\n@mixin sr-only-focusable() {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n }\n}\n","// stylelint-disable declaration-no-important\n\n.shadow-sm { box-shadow: $box-shadow-sm !important; }\n.shadow { box-shadow: $box-shadow !important; }\n.shadow-lg { box-shadow: $box-shadow-lg !important; }\n.shadow-none { box-shadow: none !important; }\n","// stylelint-disable declaration-no-important\n\n// Width and height\n\n@each $prop, $abbrev in (width: w, height: h) {\n @each $size, $length in $sizes {\n .#{$abbrev}-#{$size} { #{$prop}: $length !important; }\n }\n}\n\n.mw-100 { max-width: 100% !important; }\n.mh-100 { max-height: 100% !important; }\n\n// Viewport additional helpers\n\n.min-vw-100 { min-width: 100vw !important; }\n.min-vh-100 { min-height: 100vh !important; }\n\n.vw-100 { width: 100vw !important; }\n.vh-100 { height: 100vh !important; }\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n","//\n// Stretched link\n//\n\n.stretched-link {\n &::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n // Just in case `pointer-events: none` is set on a parent\n pointer-events: auto;\n content: \"\";\n // IE10 bugfix, see https://stackoverflow.com/questions/16947967/ie10-hover-pseudo-class-doesnt-work-without-background-color\n background-color: rgba(0, 0, 0, 0);\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Text\n//\n\n.text-monospace { font-family: $font-family-monospace !important; }\n\n// Alignment\n\n.text-justify { text-align: justify !important; }\n.text-wrap { white-space: normal !important; }\n.text-nowrap { white-space: nowrap !important; }\n.text-truncate { @include text-truncate(); }\n\n// Responsive alignment\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .text#{$infix}-left { text-align: left !important; }\n .text#{$infix}-right { text-align: right !important; }\n .text#{$infix}-center { text-align: center !important; }\n }\n}\n\n// Transformation\n\n.text-lowercase { text-transform: lowercase !important; }\n.text-uppercase { text-transform: uppercase !important; }\n.text-capitalize { text-transform: capitalize !important; }\n\n// Weight and italics\n\n.font-weight-light { font-weight: $font-weight-light !important; }\n.font-weight-lighter { font-weight: $font-weight-lighter !important; }\n.font-weight-normal { font-weight: $font-weight-normal !important; }\n.font-weight-bold { font-weight: $font-weight-bold !important; }\n.font-weight-bolder { font-weight: $font-weight-bolder !important; }\n.font-italic { font-style: italic !important; }\n\n// Contextual colors\n\n.text-white { color: $white !important; }\n\n@each $color, $value in $theme-colors {\n @include text-emphasis-variant(\".text-#{$color}\", $value, true);\n}\n\n.text-body { color: $body-color !important; }\n.text-muted { color: $text-muted !important; }\n\n.text-black-50 { color: rgba($black, .5) !important; }\n.text-white-50 { color: rgba($white, .5) !important; }\n\n// Misc\n\n.text-hide {\n @include text-hide($ignore-warning: true);\n}\n\n.text-decoration-none { text-decoration: none !important; }\n\n.text-break {\n word-break: break-word !important; // Deprecated, but avoids issues with flex containers\n word-wrap: break-word !important; // Used instead of `overflow-wrap` for IE & Edge Legacy\n}\n\n// Reset\n\n.text-reset { color: inherit !important; }\n","// Text truncate\n// Requires inline-block or block for proper styling\n\n@mixin text-truncate() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","// stylelint-disable declaration-no-important\n\n// Typography\n\n@mixin text-emphasis-variant($parent, $color, $ignore-warning: false) {\n #{$parent} {\n color: $color !important;\n }\n @if $emphasized-link-hover-darken-percentage != 0 {\n a#{$parent} {\n @include hover-focus() {\n color: darken($color, $emphasized-link-hover-darken-percentage) !important;\n }\n }\n }\n @include deprecate(\"`text-emphasis-variant()`\", \"v4.4.0\", \"v5\", $ignore-warning);\n}\n","// CSS image replacement\n@mixin text-hide($ignore-warning: false) {\n // stylelint-disable-next-line font-family-no-missing-generic-family-keyword\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n\n @include deprecate(\"`text-hide()`\", \"v4.1.0\", \"v5\", $ignore-warning);\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Visibility utilities\n//\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type\n\n// Source: https://github.com/h5bp/main.css/blob/master/src/_print.css\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request:\n// https://www.phpied.com/delay-loading-your-print-css/\n// ==========================================================================\n\n@if $enable-print-styles {\n @media print {\n *,\n *::before,\n *::after {\n // Bootstrap specific; comment out `color` and `background`\n //color: $black !important; // Black prints faster\n text-shadow: none !important;\n //background: transparent !important;\n box-shadow: none !important;\n }\n\n a {\n &:not(.btn) {\n text-decoration: underline;\n }\n }\n\n // Bootstrap specific; comment the following selector out\n //a[href]::after {\n // content: \" (\" attr(href) \")\";\n //}\n\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n\n // Bootstrap specific; comment the following selector out\n //\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n //\n\n //a[href^=\"#\"]::after,\n //a[href^=\"javascript:\"]::after {\n // content: \"\";\n //}\n\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: $border-width solid $gray-500; // Bootstrap custom code; using `$border-width` instead of 1px\n page-break-inside: avoid;\n }\n\n //\n // Printing Tables:\n // https://web.archive.org/web/20180815150934/http://css-discuss.incutio.com/wiki/Printing_Tables\n //\n\n thead {\n display: table-header-group;\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Specify a size and min-width to make printing closer across browsers.\n // We don't set margin here because it breaks `size` in Chrome. We also\n // don't use `!important` on `size` as it breaks in Chrome.\n @page {\n size: $print-page-size;\n }\n body {\n min-width: $print-body-min-width !important;\n }\n .container {\n min-width: $print-body-min-width !important;\n }\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .badge {\n border: $border-width solid $black;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: $white !important;\n }\n }\n\n .table-bordered {\n th,\n td {\n border: 1px solid $gray-300 !important;\n }\n }\n\n .table-dark {\n color: inherit;\n\n th,\n td,\n thead th,\n tbody + tbody {\n border-color: $table-border-color;\n }\n }\n\n .table .thead-dark th {\n color: inherit;\n border-color: $table-border-color;\n }\n\n // Bootstrap specific changes end\n }\n}\n"]} \ No newline at end of file diff --git a/assets/css/vendor/fontello.css b/assets/css/vendor/fontello.scss similarity index 100% rename from assets/css/vendor/fontello.css rename to assets/css/vendor/fontello.scss diff --git a/assets/css/vendor/leaflet.draw.css b/assets/css/vendor/leaflet.draw.scss similarity index 100% rename from assets/css/vendor/leaflet.draw.css rename to assets/css/vendor/leaflet.draw.scss diff --git a/assets/css/vendor/leaflet.css b/assets/css/vendor/leaflet.scss similarity index 100% rename from assets/css/vendor/leaflet.css rename to assets/css/vendor/leaflet.scss diff --git a/assets/img/map_layers_icon.png b/assets/img/map_layers_icon.png new file mode 100644 index 000000000..92ae4297c Binary files /dev/null and b/assets/img/map_layers_icon.png differ diff --git a/build_all.sh b/build_all.sh new file mode 100755 index 000000000..8c21a418e --- /dev/null +++ b/build_all.sh @@ -0,0 +1,4 @@ +#!/bin/bash +python opentreemap/manage.py collectstatic_js_reverse +npm run build-profile +python opentreemap/manage.py collectstatic --noinput diff --git a/dev-requirements.txt b/dev-requirements.txt index e35dc9c3f..ca0bd44dc 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,6 @@ -django-debug-toolbar==1.5 # https://django-debug-toolbar.readthedocs.io/en/stable/changes.html -django-extensions==1.7.4 -ipython==5.1.0 -ipdb==0.10.1 -six==1.10.0 +#setuptools==44.0.0 +#django-debug-toolbar==1.5 # https://django-debug-toolbar.readthedocs.io/en/stable/changes.html +#django-extensions==1.7.4 +#ipython==5.1.0 +#ipdb==0.10.1 +#six==1.10.0 diff --git a/docker/local_settings.py b/docker/local_settings.py new file mode 100644 index 000000000..dda047c88 --- /dev/null +++ b/docker/local_settings.py @@ -0,0 +1,23 @@ +EXTRA_UNMANAGED_APPS = ('django_extensions',) + +STATIC_ROOT = '/usr/local/otm/static' +MEDIA_ROOT = '/usr/local/otm/media' + +DATABASES = { + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'NAME': 'otm', + 'USER': 'otm', + 'PASSWORD': 'otm', + 'HOST': 'database', + 'PORT': '5432' + } +} + +CELERY_BROKER_URL = 'redis://redis:6379/' +CELERY_RESULT_BACKEND = 'redis://redis:6379/' + +EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' +EMAIL_FILE_PATH = '/usr/local/otm/emails' + +ECO_SERVICE_URL = 'http://otm-ecoservice:13000' diff --git a/opentreemap/__init__.py b/opentreemap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opentreemap/api/auth.py b/opentreemap/api/auth.py index a4a0422a2..76bea5fbb 100644 --- a/opentreemap/api/auth.py +++ b/opentreemap/api/auth.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import base64 import hashlib import hmac import re -import urllib +import urllib.request +import urllib.parse +import urllib.error -from django.http import HttpResponse +from django.http import HttpResponse, RawPostDataException from django.contrib.auth import authenticate @@ -29,27 +29,37 @@ def get_signature_for_request(request, secret_key): # This used to use request.REQUEST, but after some testing and analysis it # seems that both iOS & Android always pass named parameters in the query # string, even for non-GET requests - params = sorted(request.GET.iteritems(), key=lambda a: a[0]) + params = sorted(iter(request.GET.items()), key=lambda a: a[0]) - paramstr = '&'.join(['%s=%s' % (k, urllib.quote_plus(str(v))) + paramstr = '&'.join(['%s=%s' % (k, urllib.parse.quote_plus(str(v))) for (k, v) in params if k.lower() != "signature"]) sign_string = '\n'.join([httpverb, hostheader, request_uri, paramstr]) - # Sometimes reeading from body fails, so try reading as a file-like + # Sometimes reading from body fails, so try reading as a file-like object try: - body_encoded = base64.b64encode(request.body) - except: - body_encoded = base64.b64encode(request.read()) + body_decoded = base64.b64encode(request.body).decode() + except RawPostDataException: + body_decoded = base64.b64encode(request.read()).decode() + + if body_decoded: + sign_string += body_decoded - if body_encoded: - sign_string += body_encoded + try: + binary_secret_key = secret_key.encode() + except (AttributeError, UnicodeEncodeError): + binary_secret_key = secret_key + #hmac.new(str(secret_key), str(sign_string), hashlib.sha256).digest()) sig = base64.b64encode( - hmac.new(secret_key, sign_string, hashlib.sha256).digest()) + hmac.new(binary_secret_key, sign_string.encode(), hashlib.sha256).digest() + ) - return sig + if sig is None: + return sig + + return sig.decode() def create_401unauthorized(body="Unauthorized"): @@ -67,11 +77,15 @@ def firstmatch(regx, strg): return m.group(1) -def decodebasicauth(strg): - if strg is None: +def split_basicauth(strg): + """ + Returns username, password from decoded, + stringified, basic auth credentials + """ + if strg is None or len(strg) == 0: return None else: - m = re.match(r'([^:]*)\:(.*)', base64.decodestring(strg)) + m = re.match(r'([^:]*)\:(.*)', strg) if m is not None: return (m.group(1), m.group(2)) else: @@ -79,7 +93,22 @@ def decodebasicauth(strg): def parse_basicauth(authstr): - auth = decodebasicauth(firstmatch('Basic (.*)', authstr)) + string_wrapped_binary_credentials = firstmatch("Basic (.*)", authstr) + if string_wrapped_binary_credentials is None: + return None + + # tease bytes-like object out of string, i.e. "b'credentials'" + reg_exp = r"'(.*?)\'" + parsed_credentials = re.search(r"'(.*?)\'", string_wrapped_binary_credentials) + str_credentials = parsed_credentials.groups()[0] + + # decode the binary encoded credentials + decoded_str_credentials = base64.decodebytes( + bytes(str_credentials, 'utf-8') + ).decode() + + auth = split_basicauth(decoded_str_credentials) + if auth is None: return None else: diff --git a/opentreemap/api/decorators.py b/opentreemap/api/decorators.py index 948fbeb62..354b9a30d 100644 --- a/opentreemap/api/decorators.py +++ b/opentreemap/api/decorators.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import datetime @@ -73,6 +71,7 @@ def wrapperf(request, *args, **kwargs): return _bad_request try: + #cred = APIAccessCredential.objects.get(access_key=str.encode(key)) cred = APIAccessCredential.objects.get(access_key=key) except APIAccessCredential.DoesNotExist: return _bad_request @@ -97,7 +96,7 @@ def wrapperf(request, *args, **kwargs): user = parse_user_from_request(request) if require_login: - if user is None or user.is_anonymous(): + if user is None or user.is_anonymous: return create_401unauthorized() if user is None: @@ -117,7 +116,7 @@ def login_required(view_f): def wrapperf(request, *args, **kwargs): user = parse_user_from_request(request) or request.user - if user is not None and not user.is_anonymous(): + if user is not None and not user.is_anonymous: request.user = user return view_f(request, *args, **kwargs) diff --git a/opentreemap/api/instance.py b/opentreemap/api/instance.py index 7cb06352f..7d7e81b33 100644 --- a/opentreemap/api/instance.py +++ b/opentreemap/api/instance.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json import copy @@ -53,13 +51,13 @@ def wrapper(request, *args, **kwargs): if request.api_version < 4: multichoice_fields = { field for field, info - in instance_info_dict['fields'].iteritems() + in instance_info_dict['fields'].items() if info['data_type'] == 'multichoice'} # Remove multichoice fields from perms instance_info_dict['fields'] = { field: info for field, info - in instance_info_dict['fields'].iteritems() + in instance_info_dict['fields'].items() if field not in multichoice_fields} # Remove multichoice fields from field groups @@ -95,7 +93,7 @@ def instances_closest_to_point(request, lat, lng): """ user = request.user user_instance_ids = [] - if user and not user.is_anonymous(): + if user and not user.is_anonymous: user_instance_ids = InstanceUser.objects.filter(user=user)\ .values_list('instance_id', flat=True)\ .distinct() @@ -300,7 +298,7 @@ def public_instances(request): def _contextify_instances(instances): """ Converts instances to context dictionary""" - return map(_instance_info_dict, instances) + return list(map(_instance_info_dict, instances)) def _instance_info_dict(instance): diff --git a/opentreemap/api/migrations/0001_initial.py b/opentreemap/api/migrations/0001_initial.py index 37fc81245..17467f6df 100644 --- a/opentreemap/api/migrations/0001_initial.py +++ b/opentreemap/api/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/api/migrations/0002_apiaccesscredential_user.py b/opentreemap/api/migrations/0002_apiaccesscredential_user.py index 64ee64fba..5ee674e4f 100644 --- a/opentreemap/api/migrations/0002_apiaccesscredential_user.py +++ b/opentreemap/api/migrations/0002_apiaccesscredential_user.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations from django.conf import settings @@ -16,6 +16,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='apiaccesscredential', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True), + field=models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL, null=True), ), ] diff --git a/opentreemap/api/models.py b/opentreemap/api/models.py index 37d42a0c8..751cd6709 100755 --- a/opentreemap/api/models.py +++ b/opentreemap/api/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import uuid import base64 @@ -22,18 +20,18 @@ class APIAccessCredential(models.Model): # If user is None this credential can access # any user's data if that user's username # and password are also provided - user = models.ForeignKey(User, null=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) enabled = models.BooleanField(default=True) - def __unicode__(self): + def __str__(self): return self.access_key @classmethod def create(clz, user=None): - secret_key = base64.urlsafe_b64encode(os.urandom(64)) + secret_key = base64.urlsafe_b64encode(os.urandom(64)).decode() access_key = base64.urlsafe_b64encode(uuid.uuid4().bytes)\ - .replace('=', '') + .replace(b'=', b'').decode() return APIAccessCredential.objects.create( user=user, access_key=access_key, secret_key=secret_key) diff --git a/opentreemap/api/plots.py b/opentreemap/api/plots.py index c02dca24c..9365e4528 100644 --- a/opentreemap/api/plots.py +++ b/opentreemap/api/plots.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json from functools import wraps @@ -10,6 +8,8 @@ from django.shortcuts import get_object_or_404 from django.contrib.gis.geos import Point from django.contrib.gis.measure import D +from django.contrib.gis.db.models.functions import Distance +from django.http import RawPostDataException from django_tinsel.exceptions import HttpBadRequestException @@ -42,7 +42,7 @@ def plots_closest_to_point(request, instance, lat, lng): try: max_plots = int(request.GET.get('max_plots', '1')) - if max_plots not in xrange(1, 501): + if max_plots not in range(1, 501): raise ValueError() except ValueError: raise HttpBadRequestException( @@ -52,12 +52,11 @@ def plots_closest_to_point(request, instance, lat, lng): distance = float(request.GET.get( 'distance', settings.MAP_CLICK_RADIUS)) except ValueError: - raise HttpBadRequestException( - 'The distance parameter must be a number') + raise HttpBadRequestException('The distance parameter must be a number') - plots = Plot.objects.distance(point)\ - .filter(instance=instance)\ - .filter(geom__distance_lte=(point, D(m=distance)))\ + #plots = Plot.objects.distance(point)\ + plots = Plot.objects.filter(geom__distance_lte=(point, D(m=distance)))\ + .annotate(distance=Distance('geom', point))\ .order_by('distance')[0:max_plots] def ctxt_for_plot(plot): @@ -74,13 +73,22 @@ def update_or_create_plot(request, instance, plot_id=None): # The API communicates via nested dictionaries but # our internal functions prefer dotted pairs (which # is what inline edit form users) - request_dict = json.loads(request.body) + # Sometimes reading from body fails, so try reading as a file-like object + try: + body_decoded = request.body.decode() + except RawPostDataException: + body_decoded = request.read().decode() + + if body_decoded: + request_dict = json.loads(body_decoded) + else: + request_dict = {} data = {} for model in ["plot", "tree"]: if model in request_dict: - for key, val in request_dict[model].iteritems(): + for key, val in request_dict[model].items(): data["%s.%s" % (model, key)] = val # We explicitly disallow setting a plot's tree id. diff --git a/opentreemap/api/test_utils.py b/opentreemap/api/test_utils.py index fdff6f07a..dc75c6948 100644 --- a/opentreemap/api/test_utils.py +++ b/opentreemap/api/test_utils.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.contrib.gis.geos.collections import MultiPolygon from django.contrib.gis.geos.polygon import Polygon diff --git a/opentreemap/api/tests.py b/opentreemap/api/tests.py index cff420004..41e929100 100644 --- a/opentreemap/api/tests.py +++ b/opentreemap/api/tests.py @@ -1,19 +1,20 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division -from StringIO import StringIO + +from io import BytesIO from json import loads, dumps -from urlparse import urlparse +from urllib.parse import urlparse -import urllib +import urllib.request +import urllib.parse +import urllib.error import os import json import base64 import datetime import psycopg2 from unittest.case import skip +from django_tinsel.exceptions import HttpBadRequestException from django.db import connection from django.contrib.auth.models import AnonymousUser @@ -23,7 +24,7 @@ from django.http import HttpRequest from django.conf import settings from django.core.exceptions import ValidationError -from django.core.urlresolvers import reverse +from django.urls import reverse from django.core.files import File from treemap.lib.udf import udf_create @@ -84,9 +85,9 @@ def _get_path(parsed_url): """ # If there are parameters, add them if parsed_url[3]: - return urllib.unquote(parsed_url[2] + ";" + parsed_url[3]) + return urllib.parse.unquote(parsed_url[2] + ";" + parsed_url[3]) else: - return urllib.unquote(parsed_url[2]) + return urllib.parse.unquote(parsed_url[2]) def send_json_body(url, body_object, client, method, user=None): @@ -96,11 +97,11 @@ def send_json_body(url, body_object, client, method, user=None): are posting form data, so you need to manually setup the parameters to override that default functionality. """ - body_string = dumps(body_object) - body_stream = StringIO(body_string) + body_binary_string = dumps(body_object).encode() + body_stream = BytesIO(body_binary_string) parsed_url = urlparse(url) client_params = { - 'CONTENT_LENGTH': len(body_string), + 'CONTENT_LENGTH': len(body_binary_string), 'CONTENT_TYPE': 'application/json', 'PATH_INFO': _get_path(parsed_url), 'QUERY_STRING': parsed_url[4], @@ -375,8 +376,8 @@ def test_locations_plots_endpoint_max_plots_param_must_be_a_number(self): API_PFX, self.instance.url_name)) self.assertEqual(response.status_code, 400) self.assertEqual(response.content, - 'The max_plots parameter must be ' - 'a number between 1 and 500') + b'The max_plots parameter must be ' + b'a number between 1 and 500') def test_locations_plots_max_plots_param_cannot_be_greater_than_500(self): response = get_signed( @@ -385,8 +386,8 @@ def test_locations_plots_max_plots_param_cannot_be_greater_than_500(self): API_PFX, self.instance.url_name)) self.assertEqual(response.status_code, 400) self.assertEqual(response.content, - 'The max_plots parameter must be ' - 'a number between 1 and 500') + b'The max_plots parameter must be ' + b'a number between 1 and 500') response = get_signed( self.client, "%s/instance/%s/locations/0,0/plots?max_plots=500" % @@ -402,8 +403,8 @@ def test_locations_plots_endpoint_max_plots_param_cannot_be_less_than_1( self.assertEqual(response.status_code, 400) self.assertEqual(response.content, - 'The max_plots parameter must be a ' - 'number between 1 and 500') + b'The max_plots parameter must be a ' + b'number between 1 and 500') response = get_signed( self.client, "%s/instance/%s/locations/0,0/plots?max_plots=1" % @@ -419,7 +420,7 @@ def test_locations_plots_endpoint_distance_param_must_be_a_number(self): self.assertEqual(response.status_code, 400) self.assertEqual(response.content, - 'The distance parameter must be a number') + b'The distance parameter must be a number') response = get_signed( self.client, @@ -472,7 +473,7 @@ def test_create_plot_with_tree(self): data, self.client, self.user) self.assertEqual(200, response.status_code, - "Create failed:" + response.content) + "Create failed:" + response.content.decode()) # Assert that a plot was added self.assertEqual(plot_count + 1, Plot.objects.count()) @@ -509,7 +510,7 @@ def test_create_plot_with_invalid_tree_returns_400(self): self.assertEqual(400, response.status_code, "Expected creating a million foot " - "tall tree to return 400:" + response.content) + "tall tree to return 400:" + response.content.decode()) body_dict = loads(response.content) self.assertTrue('fieldErrors' in body_dict, @@ -548,7 +549,7 @@ def test_create_plot_with_geometry(self): data, self.client, self.user) self.assertEqual(200, response.status_code, - "Create failed:" + response.content) + "Create failed:" + response.content.decode()) # Assert that a plot was added self.assertEqual(plot_count + 1, Plot.objects.count()) @@ -794,7 +795,7 @@ def test_update_plot_with_pending(self): self.assertEqual(3, len(Audit.pending_audits()), "Expected 3 pends, one for each edited field") - self.assertEqual(3, len(response_json['pending_edits'].keys()), + self.assertEqual(3, len(list(response_json['pending_edits'].keys())), "Expected the json response to have a " "pending_edits dict with 3 keys, one for each field") @@ -808,10 +809,10 @@ def test_invalid_field_returns_200_field_is_not_in_response(self): self.assertEqual(200, response.status_code) response_json = loads(response.content) - self.assertFalse("error" in response_json.keys(), + self.assertFalse("error" in list(response_json.keys()), "Did not expect an error") - self.assertFalse("foo" in response_json.keys(), + self.assertFalse("foo" in list(response_json.keys()), "Did not expect foo to be added to the plot") def test_update_creates_tree(self): @@ -924,7 +925,7 @@ def test_update_tree_with_pending(self): "Expected 1 pend record for the edited field.") response_json = loads(response.content) - self.assertEqual(1, len(response_json['pending_edits'].keys()), + self.assertEqual(1, len(list(response_json['pending_edits'].keys())), "Expected the json response to have a" " pending_edits dict with 1 keys") @@ -1334,8 +1335,8 @@ def setUp(self): 'sort_key': 'Date'} ] self.instance.save() - self.instance.logo.save(Instance.test_png_name, - File(open(Instance.test_png_path, 'r'))) + with open(Instance.test_png_path, 'rb') as f: + self.instance.logo.save(Instance.test_png_name, f) def test_returns_config_colors(self): request = sign_request_as_user(make_request(), self.user) @@ -1418,7 +1419,7 @@ def test_multichoice_fields_v4(self): response = instance_info_endpoint(request, 4, self.instance.url_name) info_dict = json.loads(response.content) - self.assertIn('plot.udf:multi', info_dict['fields'].keys()) + self.assertIn('plot.udf:multi', list(info_dict['fields'].keys())) self.assertTrue(any('plot.udf:multi' in group.get('field_keys', []) for group in info_dict['field_key_groups'])) @@ -1428,7 +1429,7 @@ def test_multichoice_removed_in_v3(self): response = instance_info_endpoint(request, 3, self.instance.url_name) info_dict = json.loads(response.content) - self.assertNotIn('plot.udf:multi', info_dict['fields'].keys()) + self.assertNotIn('plot.udf:multi', list(info_dict['fields'].keys())) self.assertFalse(any('plot.udf:multi' in group.get('field_keys', []) for group in info_dict['field_key_groups'])) @@ -1568,7 +1569,7 @@ def _test_post_photo(self, path): self.instance.url_name, plot_id) - with open(path) as img: + with open(path, 'rb') as img: req = self.factory.post( url, {'name': 'afile', 'file': img}) @@ -1621,7 +1622,7 @@ def testUploadPhoto(self): url = reverse('update_user_photo', kwargs={'version': 3, 'user_id': peon.pk}) - with open(TreePhotoTest.test_jpeg_path) as img: + with open(TreePhotoTest.test_jpeg_path, 'rb') as img: req = self.factory.post( url, {'name': 'afile', 'file': img}) @@ -1630,7 +1631,7 @@ def testUploadPhoto(self): response = update_profile_photo_endpoint(req, LATEST_API, str(peon.pk)) - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) peon = User.objects.get(pk=peon.pk) self.assertIsNotNone(peon.photo) @@ -1648,7 +1649,7 @@ def testCanOnlyUploadAsSelf(self): grunt = make_user(username='grunt', password='pw') grunt.save() - with open(TreePhotoTest.test_jpeg_path) as img: + with open(TreePhotoTest.test_jpeg_path, 'rb') as img: req = self.factory.post( url, {'name': 'afile', 'file': img}) @@ -1657,7 +1658,7 @@ def testCanOnlyUploadAsSelf(self): response = update_profile_photo_endpoint(req, LATEST_API, str(grunt.pk)) - self.assertEquals(response.status_code, 403) + self.assertEqual(response.status_code, 403) def testCreateUser(self): rslt = create_user(self.make_post_request(self.defaultUserDict)) @@ -1665,7 +1666,7 @@ def testCreateUser(self): user = User.objects.get(pk=pk) - for field, target_value in self.defaultUserDict.iteritems(): + for field, target_value in self.defaultUserDict.items(): if field != 'password': self.assertEqual(getattr(user, field), target_value) @@ -1758,12 +1759,12 @@ def updatePeonRequest(d): updatePeonRequest({'last_name': 'l1'}) peon = User.objects.get(pk=peon.pk) - self.assertEquals(peon.last_name, 'l1') + self.assertEqual(peon.last_name, 'l1') updatePeonRequest({'last_name': 'l2'}) peon = User.objects.get(pk=peon.pk) - self.assertEquals(peon.last_name, 'l2') + self.assertEqual(peon.last_name, 'l2') updatePeonRequest({'password': 'whateva'}) @@ -1785,12 +1786,12 @@ def updatePeonRequest(d): updatePeonRequest({'lastname': 'l1'}) peon = User.objects.get(pk=peon.pk) - self.assertEquals(peon.last_name, 'l1') + self.assertEqual(peon.last_name, 'l1') updatePeonRequest({'lastname': 'l2'}) peon = User.objects.get(pk=peon.pk) - self.assertEquals(peon.last_name, 'l2') + self.assertEqual(peon.last_name, 'l2') def testCantRemoveRequiredFields(self): peon = make_user(username='peon', password='pw') @@ -1802,7 +1803,7 @@ def testCantRemoveRequiredFields(self): resp = put_json(url, {'username': ''}, self.client, user=peon) - self.assertEquals(resp.status_code, 400) + self.assertEqual(resp.status_code, 400) def testCanOnlyUpdateLoggedInUser(self): peon = make_user(username='peon', password='pw') @@ -1817,7 +1818,7 @@ def testCanOnlyUpdateLoggedInUser(self): resp = put_json(url, {'password': 'whateva'}, self.client, user=grunt) - self.assertEquals(resp.status_code, 403) + self.assertEqual(resp.status_code, 403) class SigningTest(OTMTestCase): @@ -1876,7 +1877,7 @@ def testAwsExample(self): sig = get_signature_for_request( req, b'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY') - self.assertEquals( + self.assertEqual( sig, 'i91nKc4PWAt0JJIdXwz9HxZCJDdiy6cf/Mj6vPxyYIs=') def testTimestampVoidsSignature(self): @@ -1884,6 +1885,7 @@ def testTimestampVoidsSignature(self): url = ('http://testserver.com/test/blah?' 'timestamp=%%s&' 'k1=4&k2=a&access_key=%s' % acred.access_key) + #'k1=4&k2=a&access_key=%s' % acred.access_key.decode()) curtime = datetime.datetime.now() invalid = curtime - datetime.timedelta(minutes=100) @@ -1902,7 +1904,7 @@ def testPOSTBodyChangesSig(self): url = "%s/i/plots/1/tree/photo" % API_PFX def get_sig(path): - with open(path) as img: + with open(path, 'rb') as img: req = self.factory.post( url, {'name': 'afile', 'file': img}) @@ -1950,6 +1952,7 @@ def testMalformedTimestamp(self): url = ('http://testserver.com/test/blah?' 'timestamp=%%s&' 'k1=4&k2=a&access_key=%s' % acred.access_key) + #'k1=4&k2=a&access_key=%s' % acred.access_key.decode()) req = self.sign_and_send(url % ('%sFAIL' % timestamp), acred.secret_key) @@ -1957,6 +1960,7 @@ def testMalformedTimestamp(self): self.assertEqual(req.status_code, 400) req = self.sign_and_send(url % timestamp, acred.secret_key) + #req = self.sign_and_send(url % timestamp, acred.secret_key.decode()) self.assertRequestWasSuccess(req) @@ -1972,6 +1976,7 @@ def testMissingAccessKey(self): self.assertEqual(req.status_code, 400) + #req = self.sign_and_send('%s&access_key=%s' % (url, acred.access_key.decode()), req = self.sign_and_send('%s&access_key=%s' % (url, acred.access_key), acred.secret_key) @@ -1988,8 +1993,9 @@ def testAuthenticatesAsUser(self): 'timestamp=%s&' 'k1=4&k2=a&access_key=%s' % (timestamp, acred.access_key), + #(timestamp, acred.access_key.decode()), acred.secret_key) - + #acred.secret_key.decode()) self.assertEqual(req.user.pk, peon.pk) @@ -2003,7 +2009,7 @@ def test_401(self): self.assertEqual(ret.status_code, 401) def test_ok(self): - auth = base64.b64encode("jim:password") + auth = base64.b64encode(b"jim:password") withauth = {"HTTP_AUTHORIZATION": "Basic %s" % auth} ret = get_signed(self.client, "%s/user" % API_PFX, **withauth) @@ -2015,14 +2021,14 @@ def test_malformed_auth(self): ret = get_signed(self.client, "%s/user" % API_PFX, **withauth) self.assertEqual(ret.status_code, 401) - auth = base64.b64encode("foobar") + auth = base64.b64encode(b"foobar") withauth = {"HTTP_AUTHORIZATION": "Basic %s" % auth} ret = get_signed(self.client, "%s/user" % API_PFX, **withauth) self.assertEqual(ret.status_code, 401) def test_bad_cred(self): - auth = base64.b64encode("jim:passwordz") + auth = base64.b64encode(b"jim:passwordz") withauth = {"HTTP_AUTHORIZATION": "Basic %s" % auth} ret = get_signed(self.client, "%s/user" % API_PFX, **withauth) @@ -2034,8 +2040,8 @@ def test_user_has_rep(self): ijim.reputation = 1001 ijim.save() - auth = base64.b64encode("jim:password") - withauth = dict(self.sign.items() + + auth = base64.b64encode(b"jim:password") + withauth = dict(list(self.sign.items()) + [("HTTP_AUTHORIZATION", "Basic %s" % auth)]) ret = self.client.get("%s/user" % API_PFX, **withauth) @@ -2060,18 +2066,18 @@ def _test_requires_admin_access(self, endpoint_name): iuser.save_with_user(iuser) resp = get_signed(self.client, url, user=self.user1) - self.assertEquals(resp.status_code, 403) + self.assertEqual(resp.status_code, 403) iuser.admin = True iuser.save_with_user(self.user1) resp = get_signed(self.client, url, user=self.user1) - self.assertEquals(resp.status_code, 200) + self.assertEqual(resp.status_code, 200) iuser.delete_with_user(self.user1) resp = get_signed(self.client, url, user=self.user1) - self.assertEquals(resp.status_code, 401) + self.assertEqual(resp.status_code, 401) def test_csv_requires_admin(self): self._test_requires_admin_access('users_csv') @@ -2089,7 +2095,7 @@ def test_send_password_reset_email_url(self): url = "%s/send-password-reset-email?email=%s" response = post_json(url % (API_PFX, self.jim.email), {}, self.client, None) - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) class SpeciesListTest(OTMTestCase): diff --git a/opentreemap/api/urls.py b/opentreemap/api/urls.py index 83cc62d7b..5ce6edbc4 100644 --- a/opentreemap/api/urls.py +++ b/opentreemap/api/urls.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import url diff --git a/opentreemap/api/user.py b/opentreemap/api/user.py index 052d76333..e9c74d93a 100644 --- a/opentreemap/api/user.py +++ b/opentreemap/api/user.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json from functools import wraps @@ -140,7 +138,7 @@ def wrapper(request, *args, **kwargs): # You can't directly set a new request body # (http://stackoverflow.com/a/22745559) request._body = body - request._stream = BytesIO(body) + request._stream = BytesIO(body.encode()) return user_view_fn(request, *args, **kwargs) diff --git a/opentreemap/api/views.py b/opentreemap/api/views.py index 69bed8cc6..383092fd4 100644 --- a/opentreemap/api/views.py +++ b/opentreemap/api/views.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from functools import partial diff --git a/opentreemap/appevents/handlers.py b/opentreemap/appevents/handlers.py index e3adffaca..c004605e5 100644 --- a/opentreemap/appevents/handlers.py +++ b/opentreemap/appevents/handlers.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.utils.timezone import now diff --git a/opentreemap/appevents/migrations/0001_initial.py b/opentreemap/appevents/migrations/0001_initial.py index 0669fabdc..60b7f6402 100644 --- a/opentreemap/appevents/migrations/0001_initial.py +++ b/opentreemap/appevents/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations import treemap.json_field diff --git a/opentreemap/appevents/migrations/0002_auto_20170907_0937.py b/opentreemap/appevents/migrations/0002_auto_20170907_0937.py index 10566c72f..2e4b3e3ab 100644 --- a/opentreemap/appevents/migrations/0002_auto_20170907_0937.py +++ b/opentreemap/appevents/migrations/0002_auto_20170907_0937.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.4 on 2017-09-07 14:37 -from __future__ import unicode_literals + from django.db import migrations import treemap.DotDict diff --git a/opentreemap/appevents/models.py b/opentreemap/appevents/models.py index 457ac8a65..bd7557861 100644 --- a/opentreemap/appevents/models.py +++ b/opentreemap/appevents/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.db import models @@ -23,10 +21,10 @@ class AppEvent(models.Model): def create(cls, event_type, **kwargs): # TODO: If a callable is not associated with the event_type, throw app_event = AppEvent(event_type=event_type) - for key, value in kwargs.iteritems(): + for key, value in kwargs.items(): app_event.data[key] = value app_event.save() return app_event # The signals need to be imported after the models are defined -import signals # NOQA +from . import signals # NOQA diff --git a/opentreemap/appevents/signals.py b/opentreemap/appevents/signals.py index 01f1c735a..2fc7014f6 100644 --- a/opentreemap/appevents/signals.py +++ b/opentreemap/appevents/signals.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.db.models.signals import post_save from django.dispatch import receiver -from models import AppEvent +from .models import AppEvent -from tasks import queue_events_to_be_handled +from .tasks import queue_events_to_be_handled @receiver(post_save, sender=AppEvent) diff --git a/opentreemap/appevents/tasks.py b/opentreemap/appevents/tasks.py index af6ab0823..5f1583299 100644 --- a/opentreemap/appevents/tasks.py +++ b/opentreemap/appevents/tasks.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from celery import shared_task from django.db import transaction diff --git a/opentreemap/appevents/tests.py b/opentreemap/appevents/tests.py index 9a68731b5..8f88bdede 100644 --- a/opentreemap/appevents/tests.py +++ b/opentreemap/appevents/tests.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from datetime import timedelta diff --git a/opentreemap/exporter/decorators.py b/opentreemap/exporter/decorators.py index a446377aa..3b7aafd8b 100644 --- a/opentreemap/exporter/decorators.py +++ b/opentreemap/exporter/decorators.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from functools import wraps from celery import chain diff --git a/opentreemap/exporter/group.py b/opentreemap/exporter/group.py new file mode 100644 index 000000000..d5b63ebf0 --- /dev/null +++ b/opentreemap/exporter/group.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +import csv +import json + +from datetime import datetime + +from contextlib import contextmanager + +from django.core.exceptions import ValidationError +from django.db.models import Count, F, Q + +from treemap.lib.dates import DATETIME_FORMAT +from treemap.models import NeighborhoodGroup, Audit + +from exporter.util import sanitize_unicode_record + + +def write_groups(csv_obj, instance, aggregation_level, min_join_ts=None, min_edit_ts=None): + field_names = None + values = None + + if aggregation_level == 'neighborhood': + field_names = ['ward', 'neighborhood', 'total'] + values = get_neighborhood_count(instance) + elif aggregation_level == 'user': + # FIXME remove the data being saved in that public S3 + #field_names = ['ward', 'neighborhood', 'user_email', 'total'] + #values = _get_user_neighborhood_count(instance) + return + + writer = csv.DictWriter(csv_obj, field_names) + writer.writeheader() + for stats in values: + writer.writerow(stats) + + +def _get_user_neighborhood_trees(instance): + return (NeighborhoodGroup.objects + .filter(user__mapfeature__plot__tree__isnull=False) + .prefetch_related('user', 'mapfeature', 'plot', 'tree', 'species') + .annotate( + user_email=F('user__email'), + tree_common_name=F('user__mapfeature__plot__tree__species__common_name') + ).values( + 'ward', + 'neighborhood', + 'user_email', + 'tree_common_name' + ).all()) + + +def _get_user_neighborhood_count(instance): + return (NeighborhoodGroup.objects + .prefetch_related('user', 'mapfeature', 'plot', 'tree') + .filter(user__mapfeature__plot__tree__isnull=False) + .values( + 'ward', + 'neighborhood', + ) + .annotate( + user_email=F('user__email'), + total=Count('user__mapfeature__plot__tree')) + .all()) + + +def get_neighborhood_count(instance): + return (NeighborhoodGroup.objects + .prefetch_related('user', 'mapfeature', 'plot', 'tree') + .filter(user__mapfeature__plot__tree__isnull=False) + .values( + 'ward', + 'neighborhood', + ) + .annotate( + total=Count('user__mapfeature__plot__tree')) + .all()) diff --git a/opentreemap/exporter/lib.py b/opentreemap/exporter/lib.py index 793f2cdf2..08104de45 100644 --- a/opentreemap/exporter/lib.py +++ b/opentreemap/exporter/lib.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals -from __future__ import print_function -from __future__ import division def export_enabled_for(instance, user): @@ -8,7 +5,7 @@ def export_enabled_for(instance, user): if instance.non_admins_can_export: return True else: - if user.is_authenticated(): + if user.is_authenticated: iuser = user.get_instance_user(instance) return iuser is not None and iuser.admin else: diff --git a/opentreemap/exporter/management/commands/create_photo_csv.py b/opentreemap/exporter/management/commands/create_photo_csv.py index 96724d0b1..1a500037d 100644 --- a/opentreemap/exporter/management/commands/create_photo_csv.py +++ b/opentreemap/exporter/management/commands/create_photo_csv.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.core.management.base import BaseCommand diff --git a/opentreemap/exporter/migrations/0001_initial.py b/opentreemap/exporter/migrations/0001_initial.py index 1c7a042db..4e883a5d4 100644 --- a/opentreemap/exporter/migrations/0001_initial.py +++ b/opentreemap/exporter/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations from django.conf import settings @@ -22,8 +22,8 @@ class Migration(migrations.Migration): ('created', models.DateTimeField(null=True, blank=True)), ('modified', models.DateTimeField(null=True, blank=True)), ('description', models.CharField(max_length=255)), - ('instance', models.ForeignKey(to='treemap.Instance')), - ('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)), + ('instance', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance')), + ('user', models.ForeignKey(on_delete=models.CASCADE, blank=True, to=settings.AUTH_USER_MODEL, null=True)), ], ), ] diff --git a/opentreemap/exporter/migrations/0002_exportjob_status_choices_to_list.py b/opentreemap/exporter/migrations/0002_exportjob_status_choices_to_list.py new file mode 100644 index 000000000..9adc20805 --- /dev/null +++ b/opentreemap/exporter/migrations/0002_exportjob_status_choices_to_list.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2020-01-08 19:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('exporter', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='exportjob', + name='status', + field=models.IntegerField(choices=[(-1, 'Something went wrong with your export.'), (0, 'Pending'), (1, 'Query returned no trees or planting sites.'), (2, 'User has no permissions on this model'), (3, 'Ready')], default=0), + ), + ] diff --git a/opentreemap/exporter/models.py b/opentreemap/exporter/models.py index 35df2a1fe..6a9ec8750 100644 --- a/opentreemap/exporter/models.py +++ b/opentreemap/exporter/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import datetime @@ -34,11 +32,11 @@ class ExportJob(models.Model): COMPLETE: 'Ready', } - instance = models.ForeignKey(Instance) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) - status = models.IntegerField(choices=STATUS_CHOICES.items(), + status = models.IntegerField(choices=list(STATUS_CHOICES.items()), default=PENDING) - user = models.ForeignKey(User, null=True, blank=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) outfile = models.FileField(upload_to="exports/%Y/%m/%d") created = models.DateTimeField(null=True, blank=True) modified = models.DateTimeField(null=True, blank=True) diff --git a/opentreemap/exporter/tasks.py b/opentreemap/exporter/tasks.py index 4f11d7c49..a0f91f41f 100644 --- a/opentreemap/exporter/tasks.py +++ b/opentreemap/exporter/tasks.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import csv +import datetime import logging from contextlib import contextmanager @@ -30,6 +29,7 @@ from exporter.models import ExportJob from exporter.user import write_users +from exporter.group import write_groups from exporter.util import sanitize_unicode_record from importer import fields @@ -78,7 +78,7 @@ def _values_for_model( if field_name.startswith('udf:'): name = field_name[4:] - if name in model_class.collection_udf_settings.keys(): + if name in list(model_class.collection_udf_settings.keys()): field_definition_id = None for udfd in udf_defs(instance, model): if udfd.iscollection and udfd.name == name: @@ -132,6 +132,21 @@ def async_users_export(job, data_format): job.save() +@shared_task +@_job_transaction +def async_groups_export(job, aggregation_level): + instance = job.instance + + filename = 'groups.{}.{}.csv'.format( + aggregation_level, + datetime.datetime.now().strftime('%Y%m%d') + ) + file_obj = TemporaryFile() + write_groups(file_obj, instance, aggregation_level) + job.complete_with(filename, File(file_obj)) + job.save() + + @shared_task @_job_transaction def async_csv_export(job, model, query, display_filters): @@ -146,7 +161,7 @@ def async_csv_export(job, model, query, display_filters): filter(instance=instance)) values = _values_for_model(instance, job, 'treemap_species', 'Species', select, select_params) - field_names = values + select.keys() + field_names = values + list(select.keys()) limited_qs = (initial_qs .extra(select=select, select_params=select_params) @@ -198,7 +213,7 @@ def async_csv_export(job, model, query, display_filters): limited_qs = (initial_qs .extra(select=select, select_params=select_params) - .values(*field_header_map.keys())) + .values(*list(field_header_map.keys()))) else: limited_qs = initial_qs.none() @@ -213,7 +228,7 @@ def async_csv_export(job, model, query, display_filters): else: csv_file = TemporaryFile() write_csv(limited_qs, csv_file, - field_order=field_header_map.keys(), + field_order=list(field_header_map.keys()), field_header_map=field_header_map, field_serializer_map=field_serializer_map) filename = generate_filename(limited_qs).replace('plot', 'tree') @@ -289,7 +304,7 @@ def _csv_field_serializer_map(instance, field_names): def make_serializer(factor, digits): return lambda x: str(round(factor * x, digits)) - for name, details in convertable_fields.iteritems(): + for name, details in convertable_fields.items(): model_name, field = details factor = storage_to_instance_units_factor(instance, model_name, diff --git a/opentreemap/exporter/tests.py b/opentreemap/exporter/tests.py index c4c134876..ee72da3d7 100644 --- a/opentreemap/exporter/tests.py +++ b/opentreemap/exporter/tests.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import csv import json @@ -40,16 +38,19 @@ def setUp(self): password='bar') def assertCSVRowValue(self, csv_file, row_index, headers_and_values): - csvreader = csv.reader(csv_file, delimiter=b",") - rows = list(csvreader) + # decode bytes object into string list required by the csv reader + str_rows = [line.decode('utf-8') for line in csv_file] # strip the BOM out - rows[0][0] = rows[0][0][3:] + str_rows[0] = str_rows[0][1:] + + csvreader = csv.reader(str_rows, delimiter=",") + reader_rows = list(csvreader) - self.assertTrue(len(rows) > 1) - for (header, value) in headers_and_values.iteritems(): - target_column = rows[0].index(header) - self.assertEqual(value, rows[row_index][target_column]) + self.assertTrue(len(reader_rows) > 1) + for (header, value) in headers_and_values.items(): + target_column = reader_rows[0].index(header) + self.assertEqual(value, reader_rows[row_index][target_column]) def assertTaskProducesCSV(self, user, model, assert_fields_and_values): self._assertTaskProducesCSVBase(user, model, assert_fields_and_values) @@ -79,11 +80,12 @@ def assertPsuedoAsyncTaskWorks(self, model, request = make_request(user=user) ctx = begin_export(request, self.instance, model) - self.assertIn('job_id', ctx.keys()) + self.assertIn('job_id', list(ctx.keys())) self.assertEqual(ctx['start_status'], 'OK') job_id = ctx['job_id'] job = ExportJob.objects.get(pk=job_id) + self.assertCSVRowValue(job.outfile, 1, {assertion_field: assertion_value}) @@ -91,7 +93,7 @@ def assertPsuedoAsyncTaskWorks(self, model, self.assertIn('.csv', ctx['url']) self.assertEqual(ctx['status'], 'COMPLETE') - self.assertRegexpMatches(job.outfile.name, assertion_filename) + self.assertRegex(job.outfile.name, assertion_filename) class ExportTreeTaskTest(AsyncCSVTestCase): @@ -167,7 +169,7 @@ def test_psuedo_async_species_export(self): class UserExportsTestCase(OTMTestCase): def assertUserJSON(self, data, expectations): - for key, expectation in expectations.items(): + for key, expectation in list(expectations.items()): value = data[key] self.assertEqual(expectation, value, "failure for key '%s': expected '%s', found '%s'" @@ -215,19 +217,21 @@ class UserExportsTest(UserExportsTestCase): def get_csv_data_with_base_assertions(self): resp = users_csv(make_request(), self.instance) - reader = csv.reader(resp) - # Skip BOM and entry line - reader.next() - reader.next() + # decode bytes object into string list required by the csv reader + str_rows = [line.decode('utf-8') for line in resp] + + # strip the BOM and entry line out + reader = csv.reader(str_rows[2:]) - header = reader.next() + # grab and strip the header + header = next(reader) - data = [dict(zip(header, [x.decode('utf8') for x in row])) - for row in reader] + data = (lambda h=header, reader=reader: + [dict(list(zip(h, [x for x in row]))) for row in reader])() commander, user1data, user2data = data - self.assertEquals(commander['username'], self.commander.username) + self.assertEqual(commander['username'], self.commander.username) self.assertUserJSON(user1data, {'username': self.user1.username, 'email': '', @@ -252,7 +256,7 @@ def get_csv_data_with_base_assertions(self): def test_export_users_csv_keep_info_private(self): data = self.get_csv_data_with_base_assertions() commander, user1data, user2data = data - self.assertEquals(commander['username'], self.commander.username) + self.assertEqual(commander['username'], self.commander.username) self.assertUserJSON(user1data, {'first_name': '', 'last_name': '', @@ -263,7 +267,7 @@ def test_export_users_csv_make_info_public(self): self.user1.save() data = self.get_csv_data_with_base_assertions() commander, user1data, user2data = data - self.assertEquals(commander['username'], self.commander.username) + self.assertEqual(commander['username'], self.commander.username) self.assertUserJSON(user1data, {'first_name': self.user1.first_name, 'last_name': self.user1.last_name, @@ -287,8 +291,8 @@ def test_export_users_json_make_info_public(self): commander, user1data, user2data = data - self.assertEquals(commander['username'], self.commander.username) - self.assertEquals(user1data.get('email'), None) + self.assertEqual(commander['username'], self.commander.username) + self.assertEqual(user1data.get('email'), None) self.assertUserJSON(user1data, {'username': self.user1.username, 'email_hash': self.user1.email_hash, @@ -326,9 +330,9 @@ def test_min_edit_date(self): data = json.loads(resp.content) - self.assertEquals(len(data), 1) + self.assertEqual(len(data), 1) - self.assertEquals(data[0]['username'], self.user2.username) + self.assertEqual(data[0]['username'], self.user2.username) def test_min_join_date(self): last_week = now() - datetime.timedelta(days=7) @@ -353,9 +357,9 @@ def test_min_join_date(self): data = json.loads(resp.content) - self.assertEquals(len(data), 1) + self.assertEqual(len(data), 1) - self.assertEquals(data[0]['username'], self.user2.username) + self.assertEqual(data[0]['username'], self.user2.username) def test_min_join_date_validation(self): with self.assertRaises(ValidationError): diff --git a/opentreemap/exporter/urls.py b/opentreemap/exporter/urls.py index ddeaa08f2..12321dda9 100644 --- a/opentreemap/exporter/urls.py +++ b/opentreemap/exporter/urls.py @@ -1,6 +1,4 @@ -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import url diff --git a/opentreemap/exporter/user.py b/opentreemap/exporter/user.py index 41b54efaf..85902a99d 100644 --- a/opentreemap/exporter/user.py +++ b/opentreemap/exporter/user.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import csv import json @@ -101,7 +99,7 @@ def _user_as_dict(user, instance): last_edit = last_edits[0] modeldata.update({'last_edit_%s' % k: v - for (k, v) in last_edit.dict().iteritems()}) + for (k, v) in last_edit.dict().items()}) return sanitize_unicode_record(modeldata) diff --git a/opentreemap/exporter/util.py b/opentreemap/exporter/util.py index 19291e2b3..5b83de91f 100644 --- a/opentreemap/exporter/util.py +++ b/opentreemap/exporter/util.py @@ -1,17 +1,14 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division def sanitize_unicode_value(value): # make sure every text value is of type 'str', coercing unicode - if isinstance(value, unicode): - return value.encode("utf-8") - elif isinstance(value, str): + if isinstance(value, str): return value + elif isinstance(value, int): + return str(value) else: - return str(value).encode("utf-8") + return value.decode("utf-8") # originally copied from, but now divergent from: @@ -19,7 +16,7 @@ def sanitize_unicode_value(value): # master/djqscsv/djqscsv.py#L123 def sanitize_unicode_record(record): obj = type(record)() - for key, val in record.iteritems(): + for key, val in record.items(): if val: obj[sanitize_unicode_value(key)] = sanitize_unicode_value(val) diff --git a/opentreemap/exporter/views.py b/opentreemap/exporter/views.py index f3e0760fa..0d577c7d9 100644 --- a/opentreemap/exporter/views.py +++ b/opentreemap/exporter/views.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.http import Http404 from django.shortcuts import get_object_or_404 -from tasks import async_csv_export, async_users_export +from .tasks import async_csv_export, async_users_export, async_groups_export from django_tinsel.utils import decorate as do from django_tinsel.decorators import json_api_call @@ -59,7 +57,7 @@ def users_json(request, instance): ############################################ def begin_export_users(request, instance, data_format): - if not request.user.is_authenticated(): + if not request.user.is_authenticated: raise Http404() if not instance.feature_enabled('exports'): @@ -77,6 +75,25 @@ def begin_export_users(request, instance, data_format): return {'start_status': 'OK', 'job_id': job.pk} +def begin_export_groups(request, instance, aggregation_level): + if not request.user.is_authenticated(): + raise Http404() + + if not instance.feature_enabled('exports'): + return EXPORTS_FEATURE_DISABLED_CONTEXT + elif not export_enabled_for(instance, request.user): + return EXPORTS_NOT_ENABLED_CONTEXT + + job = ExportJob.objects.create( + instance=instance, + user=request.user, + description='group export with %s aggregation level' % aggregation_level) + + async_groups_export.delay(job.pk, aggregation_level) + + return {'start_status': 'OK', 'job_id': job.pk} + + def begin_export(request, instance, model): if not instance.feature_enabled('exports'): return EXPORTS_FEATURE_DISABLED_CONTEXT @@ -89,7 +106,7 @@ def begin_export(request, instance, model): job = ExportJob(instance=instance, description='csv export of %s' % model) - if request.user.is_authenticated(): + if request.user.is_authenticated: job.user = request.user job.save() diff --git a/opentreemap/frontend/__init__.py b/opentreemap/frontend/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opentreemap/frontend/admin.py b/opentreemap/frontend/admin.py new file mode 100644 index 000000000..13be29d96 --- /dev/null +++ b/opentreemap/frontend/admin.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin + +# Register your models here. diff --git a/opentreemap/frontend/apps.py b/opentreemap/frontend/apps.py new file mode 100644 index 000000000..7e4fc39a2 --- /dev/null +++ b/opentreemap/frontend/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class FrontendConfig(AppConfig): + name = 'frontend' diff --git a/opentreemap/frontend/js/src/L.UTFGrid-min.js b/opentreemap/frontend/js/src/L.UTFGrid-min.js new file mode 100644 index 000000000..2519d6c13 --- /dev/null +++ b/opentreemap/frontend/js/src/L.UTFGrid-min.js @@ -0,0 +1 @@ +function corslite(t,e,i){function o(t){return t>=200&&t<300||304===t}function n(){void 0===a.status||o(a.status)?e.call(a,null,a):e.call(a,a,null)}var l=!1;if(void 0===window.XMLHttpRequest)return e(Error("Browser not supported"));if(void 0===i){var s=t.match(/^\s*https?:\/\/[^\/]*/);i=s&&s[0]!==location.protocol+"//"+location.hostname+(location.port?":"+location.port:"")}var a=new window.XMLHttpRequest;if(i&&!("withCredentials"in a)){a=new window.XDomainRequest;var r=e;e=function(){if(l)r.apply(this,arguments);else{var t=this,e=arguments;setTimeout(function(){r.apply(t,e)},0)}}}return"onload"in a?a.onload=n:a.onreadystatechange=function(){4===a.readyState&&n()},a.onerror=function(t){e.call(this,t||!0,null),e=function(){}},a.onprogress=function(){},a.ontimeout=function(t){e.call(this,t,null),e=function(){}},a.onabort=function(t){e.call(this,t,null),e=function(){}},a.open("GET",t,!0),a.send(null),l=!0,a}"undefined"!=typeof module&&(module.exports=corslite),L.UTFGrid=L.TileLayer.extend({options:{resolution:4,pointerCursor:!0,mouseInterval:66},_mouseOn:null,_mouseOnTile:null,_tileCharCode:null,_cache:null,_idIndex:null,_throttleMove:null,_updateCursor:function(){},onAdd:function(t){this._cache={},this._idIndex={},L.TileLayer.prototype.onAdd.call(this,t),this._throttleMove=L.Util.throttle(this._move,this.options.mouseInterval,this),this.options.pointerCursor&&(this._updateCursor=function(t){this._container.style.cursor=t}),t.on("boxzoomstart",this._disconnectMapEventHandlers,this),t.on("boxzoomend",this._throttleConnectEventHandlers,this),this._connectMapEventHandlers()},onRemove:function(){var t=this._map;t.off("boxzoomstart",this._disconnectMapEventHandlers,this),t.off("boxzoomend",this._throttleConnectEventHandlers,this),this._disconnectMapEventHandlers(),this._updateCursor(""),L.TileLayer.prototype.onRemove.call(this,t)},createTile:function(t){return this._loadTile(t),document.createElement("div")},setUrl:function(t,e){return this._cache={},L.TileLayer.prototype.setUrl.call(this,t,e)},_connectMapEventHandlers:function(){this._map.on("click",this._onClick,this),this._map.on("mousemove",this._throttleMove,this)},_disconnectMapEventHandlers:function(){this._map.off("click",this._onClick,this),this._map.off("mousemove",this._throttleMove,this)},_throttleConnectEventHandlers:function(){setTimeout(this._connectMapEventHandlers.bind(this),100)},_update:function(t,e){L.TileLayer.prototype._update.call(this,t,e)},_loadTile:function(t){var e=this.getTileUrl(t),i=this._tileCoordsToKey(t),o=this;this._cache[i]||corslite(e,function(t,e){if(t)return void o.fire("error",{error:t});var n=JSON.parse(e.responseText);o._cache[i]=n,L.Util.bind(o._handleTileLoad,o)(i,n)},!0)},_handleTileLoad:function(t,e){},_onClick:function(t){this.fire("click",this._objectForEvent(t))},_move:function(t){if(null!=t.latlng){var e=this._objectForEvent(t);e._tileCharCode!==this._tileCharCode?(this._mouseOn&&(this.fire("mouseout",{latlng:t.latlng,data:this._mouseOn,_tile:this._mouseOnTile,_tileCharCode:this._tileCharCode}),this._updateCursor("")),e.data&&(this.fire("mouseover",e),this._updateCursor("pointer")),this._mouseOn=e.data,this._mouseOnTile=e._tile,this._tileCharCode=e._tileCharCode):e.data&&this.fire("mousemove",e)}},_objectForEvent:function(t){if(t.latlng){var e=this._map,i=e.project(t.latlng),o=this.options.tileSize,n=this.options.resolution,l=Math.floor(i.x/o),s=Math.floor(i.y/o),a=Math.floor((i.x-l*o)/n),r=Math.floor((i.y-s*o)/n),h=e.options.crs.scale(e.getZoom())/o;l=(l+h)%h,s=(s+h)%h;var d=this._tileCoordsToKey({z:e.getZoom(),x:l,y:s}),u=this._cache[d];if(!u)return{latlng:t.latlng,data:null,_tile:null,_tileCharCode:null};var c=u.grid[r].charCodeAt(a),_=this._utfDecode(c),f=u.keys[_],p=u.data[f];return u.data.hasOwnProperty(f)||(p=null),{latlng:t.latlng,data:p,id:p?p.id:null,_tile:d,_tileCharCode:d+":"+c}}},_dataForCharCode:function(t,e){var i=this._cache[t],o=this._utfDecode(e),n=i.keys[o],l=i.data[n];return i.data.hasOwnProperty(n)||(l=null),l},_utfDecode:function(t){return t>=93&&t--,t>=35&&t--,t-32},_utfEncode:function(t){var e=t+32;return e>=34&&e++,e>=92&&e++,e}}),L.utfGrid=function(t,e){return new L.UTFGrid(t,e)},L.UTFGridCanvas=L.UTFGrid.extend({options:{idField:"ID",buildIndex:!0,fillColor:"black",shadowBlur:0,shadowColor:null,debug:!1},_adjacentTiles:null,onAdd:function(t){this._adjacentTiles=[],L.UTFGrid.prototype.onAdd.call(this,t)},createTile:function(t){this._loadTile(t);var e=document.createElement("canvas");return e.width=e.height=this.options.tileSize,this.options.debug&&this._drawDefaultTile(e.getContext("2d"),this._tileCoordsToKey(t)),e},_connectMapEventHandlers:function(){L.UTFGrid.prototype._connectMapEventHandlers.call(this),this.on("mouseover",this._handleMouseOver,this),this.on("mouseout",this._handleMouseOut,this)},_disconnectMapEventHandlers:function(){L.UTFGrid.prototype._disconnectMapEventHandlers.call(this),this.off("mouseover",this._handleMouseOver,this),this.off("mouseout",this._handleMouseOut,this)},_handleMouseOver:function(t){if(null!=t._tile&&null!=t._tileCharCode){this._clearAdjacentTiles();var e=t._tile;if(this._drawTile(e,parseInt(t._tileCharCode.split(":")[3])),t.data&&this._idIndex){var i=t.data[this.options.idField],o=e.split(":")[2];if(!(i&&this._idIndex[i]&&this._idIndex[i][o]))return;var n=this._idIndex[i][o];for(var l in n)l!==e&&(this._drawTile(l,n[l]),this._adjacentTiles.push(l))}}},_handleMouseOut:function(t){this._resetTile(t._tile),this._clearAdjacentTiles()},_clearAdjacentTiles:function(){if(this._adjacentTiles){for(var t=0;t<this._adjacentTiles.length;t++)this._resetTile(this._adjacentTiles[t]);this._adjacentTiles=[]}},_handleTileLoad:function(t,e){if(this.options.buildIndex)for(var i,o,n,l=this.options.idField,s=t.split(":")[2],a=0;a<e.keys.length;a++)(o=e.data[e.keys[a]])&&(i=o[l])&&(null==this._idIndex[i]&&(this._idIndex[i]={}),n=this._idIndex[i],null==n[s]&&(n[s]={}),n[s][t]=this._utfEncode(a))},_drawTile:function(t,e){if(null!=this._tiles[t]){var i=this._tiles[t].el,o=i.getContext("2d");this._resetTile(t);var n=this._cache[t].grid;o.fillStyle=this.options.fillColor;for(var l=this.options.tileSize/this.options.resolution,s=0;s<l;s++)for(var a=0;a<l;a++)if(n[a].charCodeAt(s)===e){for(var r=1;a<63&&n[a+1].charCodeAt(s)===e;)a++,r++;o.fillRect(4*s,4*a-4*(r-1),4,4*r)}this.options.shadowBlur&&this._addShadow(i,o)}},_resetTile:function(t){if(null!=this._tiles[t]){var e=this._tiles[t].el;e.width=this.options.tileSize,this.options.debug&&this._drawDefaultTile(e.getContext("2d"),t)}},_drawDefaultTile:function(t,e){t.fillStyle="black",t.fillText(e,20,20),t.strokeStyle="red",t.beginPath(),t.moveTo(0,0),t.lineTo(255,0),t.lineTo(255,255),t.lineTo(0,255),t.closePath(),t.stroke()},_addShadow:function(t,e){e.shadowBlur=this.options.shadowBlur,e.shadowColor=this.options.shadowColor||this.options.fillColor,e.globalAlpha=.7,e.globalCompositeOperation="lighter";e.drawImage(t,-1,-1),e.drawImage(t,1,1),e.drawImage(t,0,-1),e.drawImage(t,-1,0),e.globalAlpha=1}}),L.utfGridCanvas=function(t,e){return new L.UTFGridCanvas(t,e)}; \ No newline at end of file diff --git a/opentreemap/frontend/js/src/account/MyTreesOverTime.js b/opentreemap/frontend/js/src/account/MyTreesOverTime.js new file mode 100644 index 000000000..e69de29bb diff --git a/opentreemap/frontend/js/src/account/Profile.css b/opentreemap/frontend/js/src/account/Profile.css new file mode 100644 index 000000000..611855826 --- /dev/null +++ b/opentreemap/frontend/js/src/account/Profile.css @@ -0,0 +1,54 @@ +.circle-tile { + text-align: center; + margin-right: auto; + margin-left: auto; + #padding-bottom: 50px; +} + +.circle-tile-heading { + border: 3px solid Prgba(255, 255, 255, 0.3); + border-radius: 100%; + color: #FFFFFF; + height: 80px; + margin: 0 auto -40px; + position: relative; + transition: all 0.3s ease-in-out 0s; + width: 80px; +} + +.circle-tile-heading .fa { + line-height: 80px; +} + +.circle-tile-content { + padding-top: 50px; +} + +.circle-tile-number { + font-size: 52px; + font-weight: 700; + line-height: 1; + padding: 5px 0 15px; +} + +.circle-tile-description { + text-transform: uppercase; +} + +.circle-tile-footer { + background-color: rgba(0, 0, 0, 0.1); + color: rgba(255, 255, 255, 0.5); +} + +.center-table { + margin-left: auto; + margin-right: auto; +} + +.line { + height: 60vh; +} + +.profile-container { + padding-top: 50px; +} diff --git a/opentreemap/frontend/js/src/account/Profile.js b/opentreemap/frontend/js/src/account/Profile.js new file mode 100644 index 000000000..2ddc059de --- /dev/null +++ b/opentreemap/frontend/js/src/account/Profile.js @@ -0,0 +1,197 @@ +import React, { useEffect, useRef, useState } from 'react'; +import axios from 'axios'; +import config from 'treemap/lib/config'; +import reverse from 'reverse'; + +import './Profile.css'; + +import { Line, Bar } from 'react-chartjs-2'; + + +export default function Profile(props) { + return ( + <div className="container contained profile"> + <h3>My Dashboard</h3> + <div className="row"> + <div className="col-md-9"> + <MyTotalTrees /> + </div> + + <div className="col-md-6 profile-container"> + <h3>My Trees Over Time (By Week)</h3> + <div className="line"> + <MyTreesOverTime /> + </div> + </div> + + <div className="col-md-6 profile-container"> + <h3>My Trees By Neighborhood</h3> + <div className="line"> + <MyTreesByNeighborhood /> + </div> + </div> + + <div className="col-md-9 profile-container"> + <h3>My Realized EcoBenefits</h3> + <MyEcobenefits /> + </div> + </div> + </div> + ); +} + + +function MyTreesOverTime(props) { + const [data, setData] = useState({ + labels: [], + datasets: [], + }); + + var options = { + maintainAspectRatio: false, + scales: { + xAxes: [{ + title: "time", + type: 'time', + gridLines: { + lineWidth: 2 + }, + time: { + unit: "day", + unitStepSize: 1000, + displayFormats: { + millisecond: 'MMM DD', + second: 'MMM DD', + minute: 'MMM DD', + hour: 'MMM DD', + day: 'MMM DD', + week: 'MMM DD', + month: 'MMM DD', + quarter: 'MMM DD', + year: 'MMM DD', + } + } + }] + } + } + + useEffect(() => { + const url = reverse.Urls.get_reports_user_data(config.instance.url_name) + axios.get(url, {withCredential: true, params: {data_set: 'count_over_time'}}) + .then(res => { + setData({ + labels: res.data.data.map(x => (new Date(x.name).toLocaleDateString('en-US'))), + datasets: [{ + data: res.data.data.map(x => x.count), + label: "Trees By Week", + borderColor: '#8baa3d', + backgroundColor: '#8baa3d', + }] + }); + }); + }, []); + + return (<Line data={data} options={options}/>); +} + + +function MyTreesByNeighborhood(props) { + const [data, setData] = useState({ + labels: [], + datasets: [] + }); + + var options = { + maintainAspectRatio: false, + } + + useEffect(() => { + const url = reverse.Urls.get_reports_user_data(config.instance.url_name) + axios.get(url, {withCredential: true, params: {data_set: 'count', aggregation_level: 'neighborhood'}}) + .then(res => { + setData({ + labels: res.data.data.map(x => x.name), + datasets: [{ + data: res.data.data.map(x => x.count), + label: "Trees By Neighborhood", + borderColor: '#8baa3d', + backgroundColor: '#8baa3d', + }] + }); + }); + }, []); + + return ( + <Bar data={data} options={options} /> + + ); +} + + +function MyTotalTrees(props) { + const [count, setCount] = useState(null); + + useEffect(() => { + const url = reverse.Urls.get_reports_user_data(config.instance.url_name) + axios.get(url, {withCredential: true, params: {data_set: 'count'}}) + .then(res => { + setCount(res.data.data[0].count); + }); + }, []); + return ( + <div className="circle-tile"> + <div className="circle-tile-content"> + <div className="circle-tile-description text-faded">My Total Trees / Tree Pits</div> + <div className="circle-tile-number text-faded ">{count}</div> + </div> + </div> + ); +} + + +function MyEcobenefits(props) { + const [benefits, setBenefits] = useState({}); + + var formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + + // These options are needed to round to whole numbers if that's what you want. + //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1) + //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) + }); + + useEffect(() => { + const url = reverse.Urls.get_reports_user_data(config.instance.url_name) + axios.get(url, {withCredential: true, params: {data_set: 'ecobenefits_by_user'}}) + .then(res => { + const data = res.data.data.data; + const columns = res.data.data.columns; + const benefits = columns + .map((col, i) => [col, data[i]]) + .filter(x => x[0] != 'Name') // remove the name field as it won't be a table + .reduce((_map, x) => {_map[x[0]] = x[1]; return _map;}, {}) + setBenefits(benefits); + }); + }, []); + + return ( + <div> + <table className="table center-table"> + {Object.keys(benefits).map(key => { + const value = key.indexOf('$') == -1 + ? benefits[key].toLocaleString('en-US', {maximumFractionDigits: 2}) + : formatter.format(benefits[key]); + + return ( + <tr> + <td>{key}</td> + <td>{value}</td> + </tr> + ); + })} + </table> + </div> + ); + +} diff --git a/opentreemap/frontend/js/src/account_profile.js b/opentreemap/frontend/js/src/account_profile.js new file mode 100644 index 000000000..1e82387a2 --- /dev/null +++ b/opentreemap/frontend/js/src/account_profile.js @@ -0,0 +1,12 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Profile from './account/Profile'; + +console.log('TEST'); + +ReactDOM.render( + <React.StrictMode> + <Profile /> + </React.StrictMode>, + document.getElementById('app') +); diff --git a/opentreemap/frontend/js/src/common/Geocode.js b/opentreemap/frontend/js/src/common/Geocode.js new file mode 100644 index 000000000..71778b7d8 --- /dev/null +++ b/opentreemap/frontend/js/src/common/Geocode.js @@ -0,0 +1,27 @@ +import React, { Component, useEffect } from 'react'; +import axios from 'axios'; + +import reverse from 'reverse'; +import config from 'treemap/lib/config'; + +export function geocode(address) { + try { + const data = { + address: address.text, + key: address.magicKey, + forStorage: true, + ...config.instance.extend + }; + + const url = reverse.Urls.geocode(); + return axios.get(url, + { + params: data, + withCredential: true + }); + } catch (error) { + return new Promise((resolve, reject) => { + throw new Error("Could not find the address"); + }) + } +} diff --git a/opentreemap/frontend/js/src/common/GeolocateTypeahead.js b/opentreemap/frontend/js/src/common/GeolocateTypeahead.js new file mode 100644 index 000000000..dcdb6b566 --- /dev/null +++ b/opentreemap/frontend/js/src/common/GeolocateTypeahead.js @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import { AsyncTypeahead } from 'react-bootstrap-typeahead'; +import axios from 'axios'; + +import config from 'treemap/lib/config'; +import { geocode } from './Geocode'; + + +export function GeolocateTypeahead(props) { + const { onLocationFound } = props; + + const [ options, setOptions] = useState([]); + const [ errors, setErrors ] = useState(null); + const [ address, setAddress ] = useState(null); + const [ isLoading, setIsLoading ] = useState(false); + const url = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest?f=json'; + + const handleSearch = (query) => { + setIsLoading(true); + const searchExtent = { + spatialReference: {wkid: 102100}, + ...config.instance.extent + }; + const urlWithQuery = `${url}&searchExtent=${encodeURI(JSON.stringify(searchExtent))}&text=${query}`; + axios.get(urlWithQuery) + .then(res => { + setOptions(res.data?.suggestions || []); + setErrors(null); + setIsLoading(false); + }).catch(err => { + console.log(err); + setOptions([]); + setErrors(null); + setIsLoading(false); + }); + } + + const searchAddress = () => { + if (address == null || address.length == 0) { + setErrors(["Could not find address"]); + return; + } + + setErrors(null); + + geocode( + address[0] + ).then(res => { + onLocationFound({latlng: res.data}); + setErrors(null); + }).catch(err => { + setErrors(err); + }); + } + + return ( + <form className="form-inline"> + <AsyncTypeahead + id="geocode-typeahead" + placeholder="Address" + options={options} + minLength={3} + onSearch={handleSearch} + isLoading={isLoading} + labelKey="text" + onChange={(e) => { + setAddress(e); + }} + /> + <a className="btn geocode" onClick={() => searchAddress()}>Search</a> + {errors != null + ? <div>Could not find address</div> + : '' + } + </form> + ); +} diff --git a/opentreemap/frontend/js/src/common/Navbar.js b/opentreemap/frontend/js/src/common/Navbar.js new file mode 100644 index 000000000..e7a97ae8c --- /dev/null +++ b/opentreemap/frontend/js/src/common/Navbar.js @@ -0,0 +1,54 @@ +import React, { Component } from 'react'; +import {Navbar, Nav} from 'react-bootstrap'; +import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'; + +import Users from './admin/Users'; +import Admin from './admin/Admin'; +import MapMain from './map/MapMain'; +import Map from './map/Map'; + +import 'bootstrap/dist/css/bootstrap.min.css'; + + +class App extends Component { + constructor(props) { + super(props); + this.state = { }; + } + + render() { + return ( + <div> + <Navbar> + <Navbar.Collapse className="pull-left navbar-nav navbar-left"> + <Nav> + <Nav.Link href="/">Add A Tree</Nav.Link> + <Nav.Link href="/map">Explore Map</Nav.Link> + <Nav.Link href="/users">View Edits</Nav.Link> + <Nav.Link href="/admin">Manage</Nav.Link> + <Nav.Link href="/users">Dashboard</Nav.Link> + <Nav.Link href="/users">Users</Nav.Link> + </Nav> + </Navbar.Collapse> + + <Navbar.Collapse className="pull-right navbar-nav navbar-right"> + <Nav> + <Nav.Link href="/users">Login</Nav.Link> + </Nav> + </Navbar.Collapse> + </Navbar> + <Router> + <Switch> + <Route path='/admin' component={Admin} /> + <Route path='/map' component={MapMain} /> + <Route exact path='/' component={Users} /> + </Switch> + </Router> + </div> + ); + } +} + + + +export default App; diff --git a/opentreemap/frontend/js/src/common/util/ApiRequest.js b/opentreemap/frontend/js/src/common/util/ApiRequest.js new file mode 100644 index 000000000..64a0f74ec --- /dev/null +++ b/opentreemap/frontend/js/src/common/util/ApiRequest.js @@ -0,0 +1,33 @@ +import axios from 'axios'; + +import hmacSHA256 from 'crypto-js/hmac-sha256'; +import Base64 from 'crypto-js/enc-base64'; + +const ACCESS_KEY = 'test_access'; +const SECRET_KEY = 'secret key'; + +export function createSignature(url) { + const verb = 'GET' + var urlObject = new URL(url); + + var host = urlObject.host; + var pathname = urlObject.pathname; + var searchParams = urlObject.searchParams; + + // now we can add a timestamp and access key + searchParams.append('access_key', ACCESS_KEY); + searchParams.append('timestamp', (new Date()).toISOString().split('.')[0]); + //searchParams.append('timestamp', '2020-12-28T03:02:52'); + + var paramString = Array.from(searchParams.entries()) + .map(x => `${x[0]}=${encodeURIComponent(x[1])}`) + .join('&'); + + var stringToSign = [verb, host, pathname, paramString].join('\n'); + var hmacRaw = hmacSHA256(stringToSign, SECRET_KEY); + var hmac = Base64.stringify(hmacSHA256(stringToSign, SECRET_KEY)) + var hmac2 = Base64.stringify(hmacSHA256(SECRET_KEY, stringToSign)); + searchParams.append('signature', hmac); + + return axios.get(urlObject.toString()); +} diff --git a/opentreemap/frontend/js/src/common/util/ReactTable.js b/opentreemap/frontend/js/src/common/util/ReactTable.js new file mode 100644 index 000000000..0d08360e6 --- /dev/null +++ b/opentreemap/frontend/js/src/common/util/ReactTable.js @@ -0,0 +1,153 @@ +import { Button, Form, Table } from 'react-bootstrap'; +import { useTable, useSortBy, useGlobalFilter, usePagination } from 'react-table'; + + +export const ReactTable = ({columns, data}) => { + + const filterTypes = { + text: (rows, id, filterValue) => { + return rows.filter(row => { + const rowValue = row.values[id]; + return rowValue !== undefined + ? String(rowValue) + .toLowerCase() + .startsWith(String(filterValue).toLowerCase()) + : true; + }); + } + }; + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + page, + canPreviousPage, + canNextPage, + pageOptions, + pageCount, + gotoPage, + nextPage, + previousPage, + setPageSize, + setGlobalFilter, + visibleColumns, + state: { pageIndex, pageSize, globalFilter }, + } = useTable({ + columns, + data, + initialState: { pageIndex: 0 } + }, + useGlobalFilter, + useSortBy, + usePagination + ); + + return ( + <div> + <div + className="p-1 border-0 d-flex justify-content-end" + colSpan={visibleColumns.length} + > + <GlobalFilter + globalFilter={globalFilter} + setGlobalFilter={setGlobalFilter} + /> + </div> + + <Table striped bordered hover {...getTableProps()}> + <thead> + {headerGroups.map(headerGroup => ( + <tr {...headerGroup.getHeaderGroupProps()}> + {headerGroup.headers.map(column => { + const {render, getHeaderProps} = column; + return ( + <th {...getHeaderProps()}>{render("Header")}</th> + ) + })} + </tr> + ))} + </thead> + <tbody {...getTableBodyProps()}> + {page.map((row, i) => { + prepareRow(row); + return ( + <tr {...row.getRowProps()}> + {row.cells.map(cell => { + return ( + <td {...cell.getCellProps()}>{cell.render("Cell")}</td> + ); + })} + </tr> + ); + })} + </tbody> + </Table> + <div> + <Button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>{"<<"}</Button>{" "} + <Button onClick={() => previousPage()} disabled={!canPreviousPage}> {"<"} </Button>{" "} + <Button onClick={() => nextPage()} disabled={!canNextPage}> {">"} </Button>{" "} + <Button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}> {">>"} </Button>{" "} + <span> Page{" "} <strong> {pageIndex + 1} of {pageOptions.length} </strong>{" "} </span> + <span> + | Go to page:{" "} + <input + type="number" + defaultValue={pageIndex + 1} + onChange={e => { + const page = e.target.value ? Number(e.target.value) - 1 : 0; + gotoPage(page); + }} + style={{ width: "100px" }} + /> + </span>{" "} + <select + value={pageSize} + onChange={e => { + setPageSize(Number(e.target.value)); + }} + > {[10, 20, 30, 40, 50].map(pageSize => ( + <option key={pageSize} value={pageSize}> + Show {pageSize} + </option> + ))} + </select> + </div> + </div> + ); +} + + +const GlobalFilter = ({ globalFilter, setGlobalFilter }) => { + return ( + <CustomImport + value={globalFilter || ""} + onChange={e => { + setGlobalFilter(e.target.value || undefined) + }} + placeholder="Search all..." + /> + ); +} + + +const CustomImport = props => { + + let { placeholder, name, value, onChange = () => null } = props; + + return ( + <Form.Group> + <Form.Control + placeholder={placeholder} + name={name} + value={value ? value : ""} + onChange={onChange} + /> + </Form.Group> + ); +}; + + +export default ReactTable; diff --git a/opentreemap/frontend/js/src/common/util/VectorTileLayer.js b/opentreemap/frontend/js/src/common/util/VectorTileLayer.js new file mode 100644 index 000000000..8b35ae795 --- /dev/null +++ b/opentreemap/frontend/js/src/common/util/VectorTileLayer.js @@ -0,0 +1,413 @@ +import { useEffect } from 'react'; +import { + MapLayer, withLeaflet +} from 'react-leaflet'; +import * as PropTypes from 'prop-types'; +import vectorTileLayer from 'leaflet-vector-tile-layer'; +import DebugFactory from 'debug'; + +import 'leaflet.vectorgrid'; + +import { useLeafletContext, LayerProps, + createTileLayerComponent, + updateGridLayer, + withPane } from '@react-leaflet/core'; + +const debug = DebugFactory('VectorTileLayer'); + +import L from 'leaflet'; + + +export const VectorTileLayer = createTileLayerComponent( + function createTileLayer({ url, ...props }, context) { + const container = context.layerContainer || context.map; + const { + activeStyle, + hoverStyle, + onClick, + onDblclick, + onMouseout, + onMouseover, + style, + ...options + } = props; // extract the url, rest are the options to maintain compatibility with vector tile layer. + debug('createLeafletElement', { + url, + options + }); + useEffect(() => { + const { + layerContainer + } = context.layerContainer || context.map; + const { + tooltipClassName = '', tooltip = null, popup = null + } = props; + //this.leafletElement.addTo(layerContainer); + // bind tooltip + if (tooltip) { + /*this.leafletElement.bindTooltip((layer) => { + if (isFunction(tooltip)) { + return tooltip(layer); + } else if (isString(tooltip) && layer.properties.hasOwnProperty(tooltip)) { + return layer.properties[tooltip]; + } else if (isString(tooltip)) { + return tooltip; + } + return ''; + }, { + sticky: true, + direction: 'auto', + className: tooltipClassName + }); + */ + } + // bind popup + if (popup) { + /* + this.leafletElement.bindPopup((layer) => { + if (isFunction(popup)) { + return popup(layer); + } else if (isString(popup)) { + return popup; + } + return ''; + }); + */ + } + }, []); + + //const layer = vectorTileLayer(url, options); + const layer = L.vectorGrid.protobuf(url, { + interactive: true, + rendererFactory: L.svg.tile, + attribution: props.attribution, + vectorTileLayerStyles: { + points: { + weight: 0.5, + opacity: 1, + color: '#ccc', + fillColor: '#390870', + fillOpacity: 0.6, + fill: true, + stroke: true + }, + debug: { + stroke: false + }, + groups: { + stroke: false + }, + lines: { + stroke: false + } + } + }).on('mouseover', (e) => { + const { + properties + } = e.layer; + //this._propagateEvent(onMouseover, e); + + // on mouseover styling + let st; + const featureId = this._getFeatureId(e.layer); + if (isFunction(hoverStyle)) { + st = hoverStyle(properties); + } else if (isObject(hoverStyle)) { + st = Object.assign({}, hoverStyle); + } + if (!isEmpty(st) && featureId) { + //this.clearHighlight(); + //this.highlight = featureId; + const base = Object.assign({}, baseStyle(properties)); + const hover = Object.assign(base, st); + //this.setFeatureStyle(featureId, hover); + } + }) + .on('mouseout', (e) => { + console.log('mouseout'); + //this._propagateEvent(onMouseout, e); + //this.clearHighlight(); + }) + .on('click', (e) => { + const { + properties + } = e.layer; + debugger; + const featureId = this._getFeatureId(e.layer); + + this._propagateEvent(onClick, e); + + // set active style + let st; + if (isFunction(activeStyle)) { + st = activeStyle(properties); + } else if (isObject(activeStyle)) { + st = Object.assign({}, activeStyle); + } + if (!isEmpty(st) && featureId) { + //this.clearActive(); + //this.active = featureId; + const base = Object.assign({}, baseStyle(properties)); + const active = Object.assign(base, st); + //this.setFeatureStyle(featureId, active); + } + }) + .on('dblclick', (e) => { + console.log('double click'); + //this._propagateEvent(onDblclick, e); + //this.clearActive(); + }); + + return { + instance: layer, + context, + } + }, + updateGridLayer); + + +/* +class VectorTileLayer extends MapLayer { + static propTypes = { + leaflet: PropTypes.shape({ + map: PropTypes.object.isRequired, + pane: PropTypes.object, + layerContainer: PropTypes.object.isRequired + }), + url: PropTypes.string.isRequired + }; + + createLeafletElement(props) { + const { + map, + pane, + layerContainer + } = props.leaflet; + const { + activeStyle, + hoverStyle, + onClick, + onDblclick, + onMouseout, + onMouseover, + style, + url, + ...options + } = props; // extract the url, rest are the options to maintain compatibility with vector tile layer. + debug('createLeafletElement', { + url, + options + }); + // zIndex, + // style, + // hoverStyle, + // activeStyle, + // onClick, + // onMouseover, + // onMouseout, + // onDblclick, + // interactive = true, + // vectorTileLayerStyles, + // url, + // maxNativeZoom, + // maxZoom, + // minZoom, + // subdomains, + // key, + // token + // } = props; + + // get feature base styling + // const baseStyle = (properties, zoom) => { + // if (_.isFunction(style)) { + // return style(properties); + // } else if (_.isObject(style)) { + // return style; + // } + // return { + // weight: 0.5, + // opacity: 1, + // color: '#ccc', + // fillColor: '#390870', + // fillOpacity: 0.6, + // fill: true, + // stroke: true + // }; + // }; + // this.highlight = null; + // this.active = null; + + + // const url = 'https://{s}.example.com/tiles/{z}/{x}/{y}.pbf'; + // const options = { + // // Specify zoom range in which tiles are loaded. Tiles will be + // // rendered from the same data for Zoom levels outside the range. + // minDetailZoom, // default undefined + // maxDetailZoom, // default undefined + + // // Styling options for L.Polyline or L.Polygon. If it is a function, it + // // will be passed the vector-tile feature and the layer name as + // // parameters. + // style, // default undefined + + // // This works like the same option for `Leaflet.VectorGrid`. + // vectorTileLayerStyle, // default undefined + // }; + + const layer = vectorTileLayer(url, options); + + // need to extract the mouse events from props. + // need to check if the mouse events are defined. + return layer + .on('mouseover', (e) => { + const { + properties + } = e.layer; + this._propagateEvent(onMouseover, e); + + // on mouseover styling + let st; + const featureId = this._getFeatureId(e.layer); + if (isFunction(hoverStyle)) { + st = hoverStyle(properties); + } else if (isObject(hoverStyle)) { + st = Object.assign({}, hoverStyle); + } + if (!isEmpty(st) && featureId) { + this.clearHighlight(); + this.highlight = featureId; + const base = Object.assign({}, baseStyle(properties)); + const hover = Object.assign(base, st); + this.setFeatureStyle(featureId, hover); + } + }) + .on('mouseout', (e) => { + this._propagateEvent(onMouseout, e); + this.clearHighlight(); + }) + .on('click', (e) => { + const { + properties + } = e.layer; + const featureId = this._getFeatureId(e.layer); + + this._propagateEvent(onClick, e); + + // set active style + let st; + if (isFunction(activeStyle)) { + st = activeStyle(properties); + } else if (isObject(activeStyle)) { + st = Object.assign({}, activeStyle); + } + if (!isEmpty(st) && featureId) { + this.clearActive(); + this.active = featureId; + const base = Object.assign({}, baseStyle(properties)); + const active = Object.assign(base, st); + this.setFeatureStyle(featureId, active); + } + }) + .on('dblclick', (e) => { + this._propagateEvent(onDblclick, e); + this.clearActive(); + }); + } + + componentDidMount() { + const { + layerContainer + } = this.props.leaflet || this.context; + const { + tooltipClassName = '', tooltip = null, popup = null + } = this.props; + this.leafletElement.addTo(layerContainer); + // bind tooltip + if (tooltip) { + this.leafletElement.bindTooltip((layer) => { + if (isFunction(tooltip)) { + return tooltip(layer); + } else if (isString(tooltip) && layer.properties.hasOwnProperty(tooltip)) { + return layer.properties[tooltip]; + } else if (isString(tooltip)) { + return tooltip; + } + return ''; + }, { + sticky: true, + direction: 'auto', + className: tooltipClassName + }); + } + // bind popup + if (popup) { + this.leafletElement.bindPopup((layer) => { + if (isFunction(popup)) { + return popup(layer); + } else if (isString(popup)) { + return popup; + } + return ''; + }); + } + } + + _getFeatureId(feature) { + const { + idField + } = this.props; + if (isFunction(idField)) { + return idField(feature); + } else if (isString(idField)) { + return feature.properties[idField]; + } + } + + _propagateEvent(eventHandler, e) { + if (!isFunction(eventHandler)) return; + const featureId = this._getFeatureId(e.layer); + const feature = this.getFeature(featureId); + const event = deepClone(e); + const mergedEvent = Object.assign(event.target, { + feature + }); + eventHandler(event); + } + + setFeatureStyle(id, style) { + this.leafletElement.setFeatureStyle(id, style); + } + + resetFeatureStyle(id) { + this.leafletElement.resetFeatureStyle(id); + } + + clearHighlight() { + if (this.highlight && this.highlight !== this.active) { + this.resetFeatureStyle(this.highlight); + } + this.highlight = null; + } + + clearActive() { + if (this.active) { + this.resetFeatureStyle(this.active); + } + this.active = null; + } + + getFeature(featureId) { + const { + data, + idField + } = this.props; + if (isEmpty(data)) return {}; + const feature = data.features.find(({ + properties + }) => properties[idField] === featureId); + return deepClone(feature); + } +} +*/ + +export default VectorTileLayer; diff --git a/opentreemap/frontend/js/src/fields/Diameter.js b/opentreemap/frontend/js/src/fields/Diameter.js new file mode 100644 index 000000000..036b87ad1 --- /dev/null +++ b/opentreemap/frontend/js/src/fields/Diameter.js @@ -0,0 +1,138 @@ +import React, { useEffect, useRef, useState, useMemo } from 'react'; + + +function DiameterCalculator(props) { + const { + value, + digits, + units, + identifier, + updateTreeData, + treeData } = props; + const [ diameter, setDiameter ] = useState(treeData[identifier] || null); + const [ circumference, setCircumference ] = useState(null); + + const onDiameterChange = (e) => { + const value = e.target.value; + setDiameter(value); + + const circumference = Math.round(value * Math.PI * 100) / 100; + setCircumference(circumference); + + updateTreeData(identifier, value); + } + + const onCircumferenceChange = (e) => { + const value = e.target.value; + setCircumference(value); + + const diameter = Math.round(value / Math.PI * 100) / 100; + setDiameter(diameter); + + updateTreeData(identifier, diameter); + } + + return ( + <> + <table id="diameter-calculator" className="table table-hover table-bordered"> + <thead> + <tr> + <th>Diameter</th> + <th>Circumference</th> + </tr> + </thead> + <tbody id="diameter-worksheet"> + <tr id="trunk-row"> + <td> + <div className="input-group"> + <input + className="input-sm form-control" + name="diameter" + type="text" + value={diameter} + onChange={onDiameterChange} + /> + <div className="input-group-append"> + <span className="input-group-text">{units}</span> + </div> + </div> + </td> + <td> + <div className="input-group"> + <input + className="input-sm form-control" + name="circumference" + type="text" + value={circumference} + onChange={onCircumferenceChange} + /> + <div className="input-group-append"> + <span className="input-group-text">{units}</span> + </div> + </div> + </td> + </tr> + </tbody> + <tfoot> + <tr> + <td colspan="2"> + <a id="add-trunk-row">Add another trunk?</a> + </td> + </tr> + </tfoot> + </table> + <p id="diameter-calculator-total-row" style={{display: 'none'}}> + Combined Diameter:<span id="diameter-calculator-total-reference" className="inline"></span>{units ? units : ''} + </p> + </> + ); +} + +export function Diameter(props) { + const { + choices, + length, + data_type, + digits, + display_value, + explanation, + identifier, + is_editable, + is_required, + is_visible, + label, + units, + errors, + value } = props; + + return ( + <div className={`form-group ${errors != null ? 'alert-danger' : ''}`}> + <label>* {label}</label> + {is_editable + ? <div className="field-edit"><DiameterCalculator {...props} /></div> + : ''} + {explanation + ? <p className="explanation">{explanation}</p> + : ''} + {errors != null + ? <div className="alert alert-danger text-info">{errors[0]}</div> + : '' + } + </div> + ); +} + +export function DiameterReadOnly(props) { + const { units, value } = props; + + const valueLabel = units != null + ? `${value} ${units}` + : `${value}`; + + return ( + <div className="form-group"> + <label>Trunk Diameter</label> + <div className="field-view"> {valueLabel} </div> + </div> + ); +} diff --git a/opentreemap/frontend/js/src/fields/FieldGroup.js b/opentreemap/frontend/js/src/fields/FieldGroup.js new file mode 100644 index 000000000..0599962c5 --- /dev/null +++ b/opentreemap/frontend/js/src/fields/FieldGroup.js @@ -0,0 +1,200 @@ +import React, { useEffect, useRef, useState, useMemo } from 'react'; + +import { Stewardship } from './Stewardship'; +import DatePicker from "react-datepicker"; + + +export function FieldGroup(props) { + const { errors, fieldGroup, filterFields, updateTreeData, treeData } = props; + return ( + <> + <h3>{fieldGroup.header}</h3> + <FieldTable + updateTreeData={updateTreeData} + treeData={treeData} + errors={errors} + fields={fieldGroup.fields.filter(x => filterFields.indexOf(x.identifier) == -1)} /> + <hr /> + {fieldGroup.collection_udf_keys.map((c, i) => [c, fieldGroup.collection_udfs[i]]) + .map((c,i) => <Stewardship + key={`${c[0]}.${i}`} + updateTreeData={updateTreeData} + treeData={treeData} + collectionUdfKey={c[0]} + collectionUdf={c[1]} + />) + } + </> + ); +} + + +export function Field(props) { + const { + data_type, + choices, + units, + identifier, + //value, + updateTreeData, + treeData } = props; + + const value = treeData[identifier] || ""; + + var input = null; + if (data_type == "bool") { + input = (<input + type="checkbox" + name={identifer} + checked={value || false} + onClick={(e) => updateTreeData(identifier, e.target.value)} + />); + } + else if (choices?.length > 0) { + const multiple = data_type == "multichoice" + ? {"multiple": "multiple"} + : {}; + // FIXME add selected + input = (<select + name={identifier} + value={value || ""} + onChange={(e) => updateTreeData(identifier, e.target.value)} + className="form-control"> + {choices.map((option, i) => <option + value={option.value} + >{option.display_value}</option>)} + </select>); + } + else if (data_type == "date" || data_type == "datetime") { + // FIXME add a datepicker + const startDate = new Date(); + input = (<DatePicker + selected={value} + startDate={startDate} + onChange={(e) => { + updateTreeData(identifier, e); + }} + dateFormat="yyyy-MM-dd" + />); + } + else if (data_type == "long_string") { + input = (<textarea + name={identifier} + className="form-control" + onChange={(e) => updateTreeData(identifier, e.target.value)} + value={value || ''} />); + } + else if (units) { + input = ( + <div className="input-group"> + <input + name={identifier} + className="form-control" + onChange={(e) => updateTreeData(identifier, e.target.value)} + value={value || ''}/> + <div className="input-group-append"> + <span className="input-group-text">{units}</span> + </div> + </div>); + } else { + input = (<input + name={identifier} + type="text" + onChange={(e) => updateTreeData(identifier, e.target.value)} + className="form-control" />); + } + + return ( + <div className="input-group">{input}</div> + ); +} + + +function FieldRow(props) { + const { + choices, + length, + data_type, + digits, + display_value, + explanation, + identifier, + is_editable, + is_required, + is_visible, + label, + units, + value, + errors, + updateTreeData, + treeData } = props; + + return ( + <tr className={errors != null ? "alert-danger" : ''}> + <td>{is_editable && is_required + ? '* ' : ''}{label} + {errors != null ? <ValidationError message={errors[0]} /> : ''} + </td> + {is_editable + ? <td><Field {...props} /></td> + : '' + } + </tr> + ); +} + + +function ValidationError(props) { + return ( + <div className="alert-danger text-info"><i>{props.message}</i></div> + ); +} + + +function FieldTable(props) { + const { errors, fields, updateTreeData, treeData } = props; + + return ( + <table className="table table-hover"> + <tbody> + {fields + .filter(x => x.is_visible) + .map((field, i) => {return ( + <> + <FieldRow + key={i} + updateTreeData={updateTreeData} + treeData={treeData} + errors={errors ? errors[field.identifier] : null} + {...field} /> + </> + )}) + } + </tbody> + </table> + ); +} + + +/** + * Display a field with a label and value, with optional units + */ +export function FieldReadOnly(props) { + const { label, units, value } = props; + + // round to 1 decimal place if this is a number + const valueFormatted = typeof(value) === 'number' + ? (Math.round(value * 10) / 10).toFixed(1) + : value; + + const valueLabel = units != null + ? `${valueFormatted} ${units}` + : `${valueFormatted}`; + + return ( + <div className="form-group"> + <label>{label}</label> + <div className="field-view"> {valueLabel} </div> + </div> + ); +} diff --git a/opentreemap/frontend/js/src/fields/Photos.js b/opentreemap/frontend/js/src/fields/Photos.js new file mode 100644 index 000000000..56d250cba --- /dev/null +++ b/opentreemap/frontend/js/src/fields/Photos.js @@ -0,0 +1,108 @@ +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import axios from 'axios'; +import { Alert, Button } from 'react-bootstrap'; + +// set an image limit of 20MB +const SIZE_LIMIT = 20 * 1024 * 1024; + + +export function Photos(props) { + const { addPhotos, clearPhotos, isEmptyTreePit, treeData, updateTreeData, errors } = props; + + const [showAlert, setShowAlert] = useState(false); + + // pass in the name of the element, the function to use for updating, + // and the treeDataVariable + const handleImageUpload = (name) => { + return (e) => { + const file = e.target.files[0]; + + if (file.size > SIZE_LIMIT) { + setShowAlert(true); + return; + } + + setShowAlert(false); + addPhotos(name, file); + } + } + + return ( + <> + {showAlert + ? (<Alert variant="danger" onClose={() => setShowAlert(false)} dismissible> + <Alert.Heading>Photo Error</Alert.Heading> + <p>The photo you submitted is too large</p> + </Alert>) + : '' + } + <div className={errors != null ? 'alert-danger' : ''}> + <label>* Tree Photos</label> + <table className="table table-hover"> + <tbody> + {!isEmptyTreePit + ? (<> + <PhotoRow + title="Add photo of tree shape" + buttonText="Add Shape" + name="shape" + hasPhoto={treeData['has_shape_photo'] == true} + handleImageUpload={handleImageUpload("shape")} + /> + <PhotoRow + title="Add photo of tree bark" + buttonText="Add Bark" + name="bark" + hasPhoto={treeData['has_bark_photo'] == true} + handleImageUpload={handleImageUpload("bark")} + /> + <PhotoRow + title="Add photo of tree leaf" + buttonText="Add Leaf" + name="leaf" + hasPhoto={treeData['has_leaf_photo'] == true} + handleImageUpload={handleImageUpload("leaf")} + /></>) + : (<PhotoRow + title="Add photo of site" + buttonText="Add Site" + name="site" + hasPhoto={treeData['has_site_photo'] == true} + handleImageUpload={handleImageUpload("site")} + />)} + </tbody> + </table> + + {errors != null + ? <div className="alert alert-danger text-info">{errors[0]}</div> + : '' + } + </div> + </> + ); +} + + +export function PhotoRow(props) { + const uploader = React.useRef(null); + const { name, handleImageUpload, title, buttonText, hasPhoto } = props; + + return ( + <tr className={`${hasPhoto ? 'photo-success' : ''}`}> + <td>{ title }</td> + <td><input + type="file" + accept="image/*" + multiple="false" + name={name} + ref={uploader} + onChange={handleImageUpload} + style={{display: "none"}} /> + <button + className="btn add-photos" + onClick={() => uploader.current.click()} + >{ buttonText }</button> + </td> + </tr> + ); +} diff --git a/opentreemap/frontend/js/src/fields/Species.js b/opentreemap/frontend/js/src/fields/Species.js new file mode 100644 index 000000000..7aaced254 --- /dev/null +++ b/opentreemap/frontend/js/src/fields/Species.js @@ -0,0 +1,128 @@ +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import axios from 'axios'; +import { Button } from 'react-bootstrap'; + +import { ClearButton, Typeahead } from 'react-bootstrap-typeahead'; +import 'react-bootstrap-typeahead/css/Typeahead.css'; + + +/** + * Show the title of the species using the common name and the scientific name + */ +export function SpeciesTitle(props) { + const { commonName, scientificName } = props; + + return ( + <div className="tree-details-species" data-class="display"> + <h3>{commonName}</h3> + <h5> + <em>{scientificName}</em> + </h5> + </div> + ); +} + + +export function Species(props) { + const { + identifier, + updateTreeData, + treeData, + shouldUseAllSpecies, + setShouldUseAllSpecies, + isEmptyTreePit, + setIsEmptyTreePit, + errors + } = props; + + const [ species, setSpecies ] = useState(treeData[identifier] || null); + const [ commonSpecies, setCommonSpecies ] = useState([]); + const [ allSpecies, setAllSpecies ] = useState([]); + + const ref = useRef(); + + useEffect(() => { + axios.get('/jerseycity/species/?is_common=true', {withCredential: true}) + .then(x => { + setCommonSpecies(x.data); + }).catch(x => { + console.log('Error getting species'); + }); + }, []); + + useEffect(() => { + if (shouldUseAllSpecies && allSpecies.length == 0){ + axios.get('/jerseycity/species', {withCredential: true}) + .then(x => { + setAllSpecies(x.data); + }).catch(x => { + console.log('Error getting all species'); + }); + } + }, [shouldUseAllSpecies]); + + const onChangeEmptyTreePit = (e) => { + const { checked, name } = e.target; + setIsEmptyTreePit(checked); + if (checked) { + setSpecies(null); + ref.current.clear(); + } + } + + return ( + <div className={errors != null ? 'alert-danger' : ''}> + <label>* Species</label> + <div> + <Typeahead + id="species-typeahead" + placeholder="Common or scientific name" + options={shouldUseAllSpecies ? allSpecies : commonSpecies} + labelKey="value" + disabled={isEmptyTreePit} + selected={species ? [species] : null} + renderMenuItemChildren={(option, props, index) => { + return ( + <div className="tt-suggestion tt-selectable"> + {option.common_name} + <small> + {option.scientific_name} + </small> + </div> + ); + }} + clearButton={true} + ref={ref} + onChange={(e) => { + const species = e[0]; + setSpecies(species); + updateTreeData(identifier, species); + }} + /> + <input + type="checkbox" + name="allSpecies" + id="allSpecies" + checked={shouldUseAllSpecies} + onChange={(e) => setShouldUseAllSpecies(e.target.checked)} + /> + <label htmlFor="allSpecies">Make all species available</label> + <br /> + <input + type="checkbox" + name="is_empty_site" + id="is-empty-site" + checked={isEmptyTreePit} + onChange={onChangeEmptyTreePit} + /> + <label htmlFor="is-empty-site">Is Empty Site</label> + </div> + + {errors != null + ? <div className="alert alert-danger text-info">{errors[0]}</div> + : '' + } + </div> + ); +} + diff --git a/opentreemap/frontend/js/src/fields/Stewardship.js b/opentreemap/frontend/js/src/fields/Stewardship.js new file mode 100644 index 000000000..1a7ec5766 --- /dev/null +++ b/opentreemap/frontend/js/src/fields/Stewardship.js @@ -0,0 +1,146 @@ +import React, { useState, useEffect } from 'react'; +import DatePicker from "react-datepicker"; + +import "react-datepicker/dist/react-datepicker.css"; + +/** + * Create initial stewardship from a list of datatypes + */ +function GetInitialStewardshipValue(datatypes) { + return datatypes.reduce((stewardship, datatype) => { + const value = datatype.type == "choice" + ? datatype.choices[0] + : datatype.type == "date" + ? new Date() + : null; + + stewardship[datatype.name] = value; + return stewardship; + }, {}); +} + + +function StewardshipField(props) { + const { type, name, choices, onChange, stewardship } = props + + if (type == "choice") { + return ( + <select + className="form-control" + name={name} + onChange={(e) => { + onChange(name, e.target.value); + }} + >{choices.map((value, i) => <option + value={value} + >{value}</option>)} + </select> + ); + } else if (type == "date") { + return ( + <DatePicker + className="form-control" + selected={stewardship[name]} + onChange={(e) => { + onChange(name, e); + }} + /> + ); + } +} + + +function StewardshipRow(props) { + const { datatypes, updateTreeData, addStewardship } = props; + const [ stewardship, setStewardship ] = useState(GetInitialStewardshipValue(datatypes)); + + const onChange = (name, value) => { + setStewardship({...stewardship, [name]: value}) + } + + return ( + <tr className="editrow"> + {datatypes.map((x, i) => (<td><StewardshipField + key={`stewardship_${x.name}_${i}`} + onChange={onChange} + stewardship={stewardship} + {...x} /></td>))} + <td><a className="btn add-row" onClick={() => addStewardship(stewardship)}> + </a></td> + </tr>) + ; +} + + +// the stewardship rows meant for reading +function StewardshipRowReadOnly(props) { + const { datatypes, stewardship, index, removeStewardship } = props; + + const formatValue = (value, type) => { + if (type == "date") { + return `${value.getDate()}/${value.getMonth() + 1}/${value.getFullYear()}`; + } + + return value; + } + + return ( + <tr className="editrow"> + {datatypes.map((x, i) => (<td>{formatValue(stewardship[x.name], x.type)}</td>))} + <td><a className="btn add-row" onClick={() => removeStewardship(index)}> X </a></td> + </tr>) + ; +} + + +export function Stewardship(props) { + const { collectionUdfKey, collectionUdf, updateTreeData, treeData } = props; + const [rows, setRows] = useState([]); + + const [stewardships, setStewardships] = useState(treeData[collectionUdfKey] || []); + const addStewardship = (s) => { + setStewardships([...stewardships, s]); + } + + useEffect(() => { + updateTreeData(collectionUdfKey, stewardships); + }, [stewardships]); + + const removeStewardship = (index) => { + var _stewardships = [...stewardships]; + _stewardships.splice(index, 1); + setStewardships(_stewardships); + } + + const title = `${collectionUdf.model_type} ${collectionUdf.name}`; + // this is a list of objects with type, name and maybe choices + const datatypes = JSON.parse(collectionUdf.datatype); + + return ( + <div> + <h4>{title}</h4> + <table className="table table-hover"> + <tbody> + <tr className="headerrow"> + {datatypes.map((x, i) => (<th>{x.name}</th>))} + <th></th> + </tr> + <StewardshipRow + datatypes={datatypes} + updateTreeData={updateTreeData} + addStewardship={addStewardship} + /> + {stewardships.map((x, i) => { + return <StewardshipRowReadOnly + removeStewardship={removeStewardship} + datatypes={datatypes} + stewardship={x} + index={i} /> + })} + </tbody> + </table> + </div> + ); +} + + + diff --git a/opentreemap/frontend/js/src/index.js b/opentreemap/frontend/js/src/index.js new file mode 100644 index 000000000..acae90368 --- /dev/null +++ b/opentreemap/frontend/js/src/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import MapMain from './map/MapMain'; + +ReactDOM.render( + <React.StrictMode> + <MapMain /> + </React.StrictMode>, + document.getElementById('app') +); diff --git a/opentreemap/frontend/js/src/map/Footer.js b/opentreemap/frontend/js/src/map/Footer.js new file mode 100644 index 000000000..c2f9395bc --- /dev/null +++ b/opentreemap/frontend/js/src/map/Footer.js @@ -0,0 +1,17 @@ +import React from 'react'; + +export function Footer(props) { + const isEmbedded = new URLSearchParams(window.location.search).get('embed') == "1"; + if (isEmbedded) return ''; + + return ( + <footer className="hidden-xs d-none d-sm-block"> + <div className="footer-inner"> + <ul className="list-inline pull-left"> + <li><a target="_blank" href="http://www.arborday.org/trees/whatTree/">Tree ID</a></li> + <li><a target="_blank" href="http://www.arborday.org/trees/whatTree/">Tree ID2</a></li> + </ul> + </div> + </footer> + ); +} diff --git a/opentreemap/frontend/js/src/map/Layers.js b/opentreemap/frontend/js/src/map/Layers.js new file mode 100644 index 000000000..d73049e3e --- /dev/null +++ b/opentreemap/frontend/js/src/map/Layers.js @@ -0,0 +1,132 @@ +import _ from 'lodash'; +import React, { useEffect, useRef, useState } from 'react'; +import config from 'treemap/lib/config'; +import { TileLayer, Marker, Popup } from "react-leaflet"; +import TreePopup from './TreePopup'; +import UtfGrid from './UtfGrid'; + + +export function PlotTileLayer(props) { + const { layerOptions, tilerArgs, geoRevHash } = props; + const [noSearchUrl, setNoSearchUrl] = useState(filterableLayer('treemap_mapfeature', 'png', options, tilerArgs || {}, geoRevHash)); + + const MAX_ZOOM_OPTION = {maxZoom: 21}; + // Min zoom level for detail layers + const MIN_ZOOM_OPTION = {minZoom: 15}; + + const FEATURE_LAYER_OPTION = {zIndex: 6}; + + const ref = useRef(null); + const options = _.extend(layerOptions || {}, MAX_ZOOM_OPTION, FEATURE_LAYER_OPTION); + + useEffect(() => { + const _noSearchUrl = filterableLayer('treemap_mapfeature', 'png', options, tilerArgs || {}, geoRevHash); + var tileLayer = ref.current; + setNoSearchUrl(_noSearchUrl); + tileLayer.setUrl(_noSearchUrl); + }, [geoRevHash]); + /* + useEffect(() => { + var t = ref; + }, [ref]); + */ + + return <TileLayer url={noSearchUrl} {...options} ref={ref} />; +} + + +export function BoundaryTileLayer(props) { + const { layerOptions, tilerArgs } = props; + + const MAX_ZOOM_OPTION = {maxZoom: 21}; + // Min zoom level for detail layers + const MIN_ZOOM_OPTION = {minZoom: 15}; + + const FEATURE_LAYER_OPTION = {zIndex: 7}; + + const ref = useRef(null); + const options = _.extend(layerOptions || {}, MAX_ZOOM_OPTION, FEATURE_LAYER_OPTION); + useEffect(() => { + var t = ref; + }, [ref]); + const noSearchUrl = filterableLayer('treemap_boundary', 'png', options, tilerArgs || {}, null); + + return <TileLayer url={noSearchUrl} {...options} ref={ref} />; +} + + +export function PlotUtfTileLayer(props) { + const { eventHandlers, geoRevHash } = props; + const MAX_ZOOM_OPTION = {maxZoom: 21}; + // Min zoom level for detail layers + const MIN_ZOOM_OPTION = {minZoom: 15}; + const [showMarker, setShowMarker] = useState(false); + const [latLng, setLatLng] = useState({ lat: null, lng: null}); + const [url, setUrl] = useState(filterableLayer('treemap_mapfeature', 'grid.json', options, {}, geoRevHash)); + + const FEATURE_LAYER_OPTION = {zIndex: 6}; + + const options = _.extend({resolution: 4}, MAX_ZOOM_OPTION, FEATURE_LAYER_OPTION); + //const url = getUrlMaker('treemap_mapfeature', 'grid.json')(); + + useEffect(() => { + const url = filterableLayer('treemap_mapfeature', 'grid.json', options, {}, geoRevHash); + setUrl(url); + }, [geoRevHash]); + + //return (<UtfGrid url={url} eventHandlers={eventHandlers} {...options}> + return (<UtfGrid url={url} eventHandlers={eventHandlers} {...options} />); +} + + +function filterableLayer(table, extension, layerOptions, tilerArgs, geoRevHash) { + var _geoRevHash = geoRevHash ?? config.instance.geoRevHash; + var revToUrl = getUrlMaker(table, extension, tilerArgs), + noSearchUrl = revToUrl(_geoRevHash), + searchBaseUrl = revToUrl(config.instance.universalRevHash); + //layer = L.tileLayer(noSearchUrl, layerOptions); + + /* + layer.setHashes = function(response) { + noSearchUrl = revToUrl(response.geoRevHash); + searchBaseUrl = revToUrl(response.universalRevHash); + + // Update tiles to reflect content changes. + var newLayerUrl = updateBaseUrl(layer._url, searchBaseUrl); + layer.setUrl(newLayerUrl); + }; + + layer.setFilter = function(filters) { + var fullUrl; + if (Search.isEmpty(filters)) { + fullUrl = noSearchUrl; + } else { + var query = Search.makeQueryStringFromFilters(filters); + var suffix = query ? '&' + query : ''; + fullUrl = searchBaseUrl + suffix; + } + layer.setUrl(fullUrl); + }; + */ + + //return (<TileLayer url={noSearchUrl} />); + return noSearchUrl; +} + + +function getUrlMaker(table, extension, tilerArgs) { + return function revToUrl(rev) { + var query = { + 'instance_id': config.instance.id, + 'restrict': JSON.stringify(config.instance.mapFeatureTypes) + }; + + if (tilerArgs) { + _.extend(query, tilerArgs); + } + + var paramString = new URLSearchParams(query).toString(); + return `${config.tileHost || ''}/tile/${rev}/database/otm/table/${table}/` + + `{z}/{x}/{y}.${extension}?${paramString}`; + }; +} diff --git a/opentreemap/frontend/js/src/map/Map.css b/opentreemap/frontend/js/src/map/Map.css new file mode 100644 index 000000000..e69de29bb diff --git a/opentreemap/frontend/js/src/map/Map.js b/opentreemap/frontend/js/src/map/Map.js new file mode 100644 index 000000000..a05151f23 --- /dev/null +++ b/opentreemap/frontend/js/src/map/Map.js @@ -0,0 +1,228 @@ +import React, { Component, useEffect } from 'react'; +import L from 'leaflet'; +import { Container, Col, Row } from 'react-bootstrap'; +import { LayersControl, LayerGroup, MapContainer, TileLayer, ImageOverlay, MapControl, Marker, Overlay, Popup, useMapEvents, useMap, WMSTileLayer } from "react-leaflet"; +import { ImageMapLayer, BasemapLayer, TiledMapLayer } from 'react-esri-leaflet'; +import ReactLeafletGoogleLayer from 'react-leaflet-google-layer'; +import { BingLayer } from '../util/bing/Bing'; +import { createSignature } from '../common/util/ApiRequest'; +import axios from 'axios'; +import config from 'treemap/lib/config'; +import { BoundaryTileLayer } from './Layers'; +import { PlotTileLayer, PlotUtfTileLayer } from './Layers'; +import { TreePopup } from './TreePopup'; +import { VectorTileLayer } from '../common/util/VectorTileLayer'; + +//import VectorGridDefault from 'react-leaflet-vectorgrid'; +//const VectorGrid = withLeaflet(VectorGridDefault); + +import 'leaflet/dist/leaflet.css'; +import './Map.css'; + +import icon from 'leaflet/dist/images/marker-icon.png'; +import iconShadow from 'leaflet/dist/images/marker-shadow.png'; + +// the below is a bugfix found in the following +// https://stackoverflow.com/questions/49441600/react-leaflet-marker-files-not-found +// https://github.com/Leaflet/Leaflet/issues/4968 +let DefaultIcon = L.icon({ + iconUrl: icon, + shadowUrl: iconShadow +}); + +L.Marker.prototype.options.icon = DefaultIcon; + + + +export default class Map extends Component { + constructor(props) { + super(props); + this.mapRef = React.createRef(); + this.state = { + startingLatitude: window.django.instance_center_y, + startingLongitude: window.django.instance_center_x, + loading: false + } + } + + /* + <VectorTileLayer + attribution='<a href="https://new.opengreenmap.org/browse/teams/601b4587b2de180100a4db38" target="_blank">© Sustainable JC<\/a>' + url="https://new.opengreenmap.org/api-v1/tiles/{z}/{x}/{y}?format=vt&map=601b463024942b0100cc57b3" + /> + */ + + render() { + const { loading, startingLongitude, startingLatitude, popupInfo, mapRef } = this.state; + const { popup, setMap, geoRevHash } = this.props; + + if (loading) return (<div>Loading...</div>); + + //attribution="https://new.opengreenmap.org/api-v1/tile/about.json?map=601b463024942b0100cc57b3&authorization=01d95994-8c91-22fc-5834-ccbbe5dbab9a" + const locateOptions = { + position: 'topleft', + strings: { + title: "Show me where I am" + }, + onActivate: () => {} + }; + + const onLocationFound = (e) => { + mapRef.flyTo(e.latlng, mapRef.getZoom()); + } + + if (mapRef != null) { + mapRef.on('locationfound', onLocationFound) + } + + const googleKey = window.django.googleApiKey; + const bingKey = window.django.bingApiKey; + + return ( + <> + <MapContainer + className="map" + center={[startingLatitude, startingLongitude]} + zoom={13} + whenCreated={setMap} + scrollWheelZoom={true} > + <LayersControl position='topright'> + <LayersControl.BaseLayer checked name='OpenStreetMap'> + <TileLayer + attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' + url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" + maxNativeZoom={19} + maxZoom={21} + zIndex={1} + /> + </LayersControl.BaseLayer> + <LayersControl.BaseLayer name='Google Hybrid'> + <ReactLeafletGoogleLayer + apiKey={googleKey} + type={'hybrid'} + maxNativeZoom={19} + maxZoom={21} + zIndex={1} + /> + </LayersControl.BaseLayer> + <LayersControl.BaseLayer name='Google Streets'> + <ReactLeafletGoogleLayer + apiKey={googleKey} + type={'roadmap'} + maxNativeZoom={19} + maxZoom={21} + zIndex={1} + /> + </LayersControl.BaseLayer> + <LayersControl.BaseLayer name='Esri Hybrid'> + <LayerGroup> + <BasemapLayer + name="Imagery" + maxNativeZoom={19} + maxZoom={21} + zIndex={1} + /> + <BasemapLayer + name="ImageryTransportation" + maxNativeZoom={19} + maxZoom={21} + zIndex={1} + /> + </LayerGroup> + </LayersControl.BaseLayer> + <LayersControl.BaseLayer name='Bing Hybrid'> + <BingLayer + maxNativeZoom={19} + maxZoom={21} + zIndex={1} + bingkey={bingKey} + type="AerialWithLabels" + /> + </LayersControl.BaseLayer> + </LayersControl> + <LayersControl position='topright' autoZIndex={false}> + <LayersControl.Overlay name='Wards'> + <BoundaryTileLayer tilerArgs={{'category': 'Ward'}} layerOptions={{'category': 'Ward'}}/> + </LayersControl.Overlay> + <LayersControl.Overlay name='Main Neighborhoods'> + <BoundaryTileLayer tilerArgs={{'category': 'Main Neighborhood'}} layerOptions={{'category': 'Main Neighborhood'}}/> + </LayersControl.Overlay> + <LayersControl.Overlay name='Neighborhoods'> + <BoundaryTileLayer tilerArgs={{'category': 'Neighborhood'}} layerOptions={{'category': 'Neighborhood'}}/> + </LayersControl.Overlay> + <LayersControl.Overlay name='Parks'> + <BoundaryTileLayer tilerArgs={{'category': 'Park'}} layerOptions={{'category': 'Park'}}/> + </LayersControl.Overlay> + <LayersControl.Overlay name='SIDs'> + <BoundaryTileLayer tilerArgs={{'category': 'SID'}} layerOptions={{'category': 'SID'}}/> + </LayersControl.Overlay> + <LayersControl.Overlay name='Tree Conditions'> + <PlotTileLayer tilerArgs={{'showTreeCondition': true}} geoRevHash={geoRevHash}/> + </LayersControl.Overlay> + </LayersControl> + <Legend /> + {this.props.children} + {popup} + </MapContainer> + </> + ); + } +} + +function Legend(props) { + const map = useMap(); + const legend = L.control({position: 'bottomright'}); + legend.onAdd = function (map) { + var div = L.DomUtil.create('div', 'info legend'); + var labels = [ + '<strong>Plot Colors</strong>', + '<i class="circle" style="background:#8BAA3C"></i> Healthy', + '<i class="circle" style="background:#8B1002"></i> Unhealthy', + '<i class="circle" style="background:#303031"></i> Dead', + ]; + div.innerHTML = labels.join('<br>'); + return div; + }; + + // only show this legend for the Tree Conditions layer + useMapEvents({ + overlayadd: (e) => { + if (e.name == "Tree Conditions") { + map.addControl(legend); + } + }, + overlayremove: (e) => { + if (e.name == "Tree Conditions") { + map.removeControl(legend); + } + }, + }); + + return null; +} + + +function LocateControl(props) { + const map = useMap(); + + const onLocationFound = (e) => { + map.flyTo(e.latlng, map.getZoom()); + } + + useEffect(() => { + if (map != null) { + map.on('locationfound', onLocationFound) + } + }, [map]); + + return ( + <div className="leaflet-top leaflet-left"> + <div className="leaflet-bar leaflet-control visible-xs-block d-block d-sm-none"> + <a className="leaflet-bar-part leaflet-bar-part-single" + title="Show me where I am" + onClick={() => map.locate()} > + <span className="icon icon-location"></span> + </a> + </div> + </div>); +} diff --git a/opentreemap/frontend/js/src/map/MapMain.js b/opentreemap/frontend/js/src/map/MapMain.js new file mode 100644 index 000000000..c4799811e --- /dev/null +++ b/opentreemap/frontend/js/src/map/MapMain.js @@ -0,0 +1,400 @@ +import React, { Component, useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import { Button, Container, Col, Row } from 'react-bootstrap'; +import axios from 'axios'; + +import L from 'leaflet'; +import config from 'treemap/lib/config'; +import reverse from 'reverse'; +import { useMapEvents } from "react-leaflet"; + +import Map from './Map'; +import './Map.css'; +import { TreePopup } from './TreePopup'; +import { Marker, Popup } from "react-leaflet"; + +import { PlotTileLayer, PlotUtfTileLayer } from './Layers'; +import { DetailSidebar } from '../sidebar/DetailSidebar'; +import { AddTreeSidebar } from '../sidebar/AddTreeSidebar'; +import { Footer } from './Footer'; + + +export default class MapMain extends Component { + constructor(props) { + super(props); + this.state = { + map: null, + showAddTree: window.django.shouldAddTree, + isAuthenticated: window.django.user.is_authenticated, + addTreeMarkerInfo: { + show: false, + latLng: { lat: null, lng: null }, + }, + popupInfo: { + ids: null, + show: false, + latLng: { lat: null, lng: null } + }, + addSelectedMarkerInfo: { + show: false, + latLng: { lat: null, lng: null } + }, + sidebarInfo: null, + benefits: null, + geoRevHash: config.instance.geoRevHash, + markerRef: null + }; + this.setShowAddTree = this.setShowAddTree.bind(this); + this.setShowEcobenefits = this.setShowEcobenefits.bind(this); + this.onMapClick = this.onMapClick.bind(this); + this.onUtfMapClick = this.onUtfMapClick.bind(this); + this.getPopup = this.getPopup.bind(this); + this.onAddMapFeature = this.onAddMapFeature.bind(this); + + } + + componentDidMount() { + var url = `/${window.django.instance_url}/benefit/search/api`; + axios.get(url, {withCredential: true}) + .then(res => { + this.setState({ + benefits: res.data + }); + }).catch(res => { + this.setState({ + benefits: null + }); + }); + } + + setShowAddTree(value) { + this.setState({ + showAddTree: value, + markerRef: null, + addTreeMarkerInfo: { + show: false, + latLng: { lat: null, lng: null } + }, + popupInfo: { + ids: null, + show: false, + latLng: { lat: null, lng: null } + }, + addSelectedMarkerInfo: { + show: false, + latLng: { lat: null, lng: null } + }, + }); + + // add the feature-selected to the body for legacy css purposes + document.body.classList.remove('feature-selected'); + } + + setShowEcobenefits(event) { + this.setState({ + showAddTree: false, + markerRef: null, + addTreeMarkerInfo: { + show: false, + latLng: { lat: null, lng: null } + }, + }); + } + + onMapClick(e) { + const { showAddTree } = this.state; + if (!showAddTree) return; + + this.setState({ + addTreeMarkerInfo: { + show: true, + latLng: e.latlng + }, + }); + } + + onUtfMapClick(e) { + if (e.id == null) { + this.setState({ + popupInfo: { + ids: null, + show: false, + latLng: { lat: null, lng: null } + }, + addSelectedMarkerInfo: { + show: false, + latLng: { lat: null, lng: null } + }, + sidebarInfo: null + }); + // add the feature-selected to the body for legacy css purposes + document.body.classList.remove('feature-selected'); + } else { + this.setState({ + popupInfo: { + // FIXME can there be multiple? + ids: [e.id], + show: true, + latLng: { lat: e.latlng.lat, lng: e.latlng.lng } + }, + addSelectedMarkerInfo: { + show: true, + latLng: { lat: e.latlng.lat, lng: e.latlng.lng } + } + }); + + const url = reverse.Urls.map_feature_accordion_api({ + instance_url_name: window.django.instance_url, + feature_id: e.id + }); + axios.get(url, {withCredential: true}) + .then(res => { + this.setState({ + sidebarInfo: res.data + }); + }).catch(err => { + this.setState({ + sidebarInfo: null + }); + }); + + // add the feature-selected to the body for legacy css purposes + document.body.classList.add('feature-selected'); + } + } + + onAddMapFeature = (data) => { + this.setState({ + geoRevHash: data.geoRevHash + }); + } + + getPopup() { + const { showAddTree, popupInfo, addTreeMarkerInfo, addSelectedMarkerInfo } = this.state; + if (popupInfo.show && !showAddTree) { + const marker = addSelectedMarkerInfo.show + ? (<ShowFeatureMarker {...addSelectedMarkerInfo} />) + : ''; + + return (<> + <TreePopup {...popupInfo} /> + {marker} + </>); + } + + if (addTreeMarkerInfo.show && showAddTree) { + return ( + <AddTreeMarker + latLng={addTreeMarkerInfo.latLng} + updateLatLng={(lat, lng) => { + this.setState({ + addTreeMarkerInfo: { + show: true, + latLng: {lat: lat, lng: lng} + } + }); + }} + updateMarkerRef={markerRef => { + this.setState({markerRef: markerRef}); + }} + /> + ); + } + + return null; + } + + render() { + const { + benefits, + showAddTree, + popupInfo, + sidebarInfo, + map, + addTreeMarkerInfo, + addSelectedMarkerInfo, + geoRevHash, + isAuthenticated, + markerRef + } = this.state; + + const utfEventHandlers = { + click: this.onUtfMapClick + } + + const popup = this.getPopup(); + + const sideBar = showAddTree + ? <AddTreeSidebar + onClose={() => this.setShowAddTree(false)} + addTreeMarkerInfo={addTreeMarkerInfo} + markerRef={markerRef} + clearLatLng={() => this.setState({ + addTreeMarkerInfo: { + show: false, + latLng: {lat: null, lng: null} + }})} + map={map} + onMapClick={this.onMapClick} + onAddMapFeature={this.onAddMapFeature} + /> + : <DetailSidebar + benefits={benefits} + sidebarInfo={sidebarInfo} + map={map} + addSelectedMarkerInfo={addSelectedMarkerInfo} + />; + + const plotTileLayer = (<PlotTileLayer geoRevHash={geoRevHash}/>); + const plotUtfTileLayer = (<PlotUtfTileLayer eventHandlers={utfEventHandlers} geoRevHash={geoRevHash} />); + const homeUrl = reverse.Urls.react_map({ + instance_url_name: window.django.instance_url, + }); + + const isEmbedded = new URLSearchParams(window.location.search).get('embed') == "1"; + + // FIXME use something less hacky for the navbar + return ( + <> + <div className="navbar navbar-expand fixed-top bg-dark navbar-dark" + style={{ width: '110px', zIndex: 10000 }}> + <ul className="navbar-nav nav mr-auto"> + <li className="nav-item"> + {isAuthenticated + ? (<a + className="nav-link" + onClick={() => this.setState({showAddTree: true})}>Add a Tree</a> + ) + : (<a + className="nav-link" + onClick={() => {}}>Please login to add a tree</a> + ) + } + </li> + </ul> + </div> + <div className={`header instance-header collapsed ${showAddTree ? "hide-search" : ""}`}> + <div className="logo"> + <a href={homeUrl}><img id="site-logo" src={window.django.logoUrl} alt="OpenTreeMap"></img> + </a> + </div> + <div className="toolbar-wrapper"></div> + <div className="search-wrapper"> + <div className="search-block-wrapper" style={{display: 'none'}}> + <label>Search by Species</label> + <div className="autocomplete-group"> + <input name="species.id" /> + </div> + </div> + </div> + </div> + <div className="subhead"> + <div className="advanced-search">Advanced Search</div> + <div className="stats-bar"> + <div className="stats-list"> + <div id="tree-and-planting-site-counts"> + <span>{benefits?.n_trees?.toLocaleString()}</span> trees, <span>{benefits?.n_empty_plots?.toLocaleString()}</span> empty sites + </div> + </div> + {isAuthenticated && !isEmbedded + ? (<div className="addBtn hidden-xs d-none d-sm-block"> + <Button onClick={() => this.setShowAddTree(true)}>+ Add a Tree</Button> + </div>) + : '' + } + </div> + </div> + <div className={`content explore-map ${showAddTree ? "hide-search" : ""}`}> + <Map + className="map" + popup={popup} + utfEventHandlers={utfEventHandlers} + setMap={(m) => this.setState({map: m})} + geoRevHash={geoRevHash} + > + <MapEventContainer + onClick={this.onMapClick} + /> + {plotUtfTileLayer} + {plotTileLayer} + </Map> + <div className="sidebar"> + { sideBar } + </div> + </div> + </>); + } +} + + +function MapEventContainer(props) { + const { onClick, setMap } = props; + const map = useMapEvents({ + click: onClick + }); + return null; +} + + +function AddTreeMarker(props) { + const { latLng, updateLatLng, updateMarkerRef } = props; + const [position, setPosition] = useState(latLng) + const markerRef = useRef(null) + + // FIXME use the window.settings.staticUrl variable + const addTreeIcon = new L.Icon({ + iconUrl: '/static/img/mapmarker_viewmode.png', + iconSize: [78, 75], + iconAnchor: [36, 62], + }); + + const eventHandlers = { + dragend: (e) => { + const marker = markerRef.current + if (marker != null) { + const latLng = e.target.getLatLng() + setPosition(latLng); + updateLatLng(latLng['lat'], latLng['lng']); + updateMarkerRef(marker); + } + } + }; + + useEffect(() => { + updateMarkerRef(markerRef.current); + }, []); + + return ( + <Marker + draggable={true} + eventHandlers={eventHandlers} + position={position} + icon={addTreeIcon} + ref={markerRef} /> + ); +} + + +function ShowFeatureMarker(props) { + const { latLng, updateLatLng } = props; + const [position, setPosition] = useState(latLng) + const markerRef = useRef(null) + + // this can happen if we click on a new marker + useEffect(() => { + setPosition(latLng); + }, [latLng]); + + // FIXME use the window.settings.staticUrl variable + const addTreeIcon = new L.Icon({ + iconUrl: '/static/img/mapmarker_editmode.png', + iconSize: [78, 75], + iconAnchor: [36, 62], + }); + + return ( + <Marker + position={position} + icon={addTreeIcon} + ref={markerRef} /> + ); +} diff --git a/opentreemap/frontend/js/src/map/TreePopup.js b/opentreemap/frontend/js/src/map/TreePopup.js new file mode 100644 index 000000000..90465aa54 --- /dev/null +++ b/opentreemap/frontend/js/src/map/TreePopup.js @@ -0,0 +1,74 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { TileLayer, Marker, Popup } from "react-leaflet"; +import axios from 'axios'; +import reverse from 'reverse'; + + +export function TreePopup(props) { + const { latLng, ids } = props; + const [title, setTitle] = useState(null); + const [canEdit, setCanEdit] = useState(false); + const instance_url = window.django.instance_url; + + useEffect(() => { + // clear the title before loading a new one + //var url = `/${instance_url}/features/${ids[0]}/popup_detail`; + const url = reverse.Urls.map_feature_popup_detail({ + instance_url_name: window.django.instance_url, + feature_id: ids[0] + }); + setTitle(null); + setCanEdit(false); + axios.get(url, {withCredential: true}) + .then(res => { + const feature = res.data.features[0]; + setTitle(res.data.features[0].title); + setCanEdit(window.django.user.is_authenticated + && feature.is_plot + && feature.is_editable); + }).catch(res => { + setTitle(null); + setCanEdit(false); + }); + }, [ids]); + + if (title == null) { + return ( + <Popup position={latLng}> + Loading... + </Popup> + ); + } + + const featureUrl = reverse.Urls.map_feature_detail({ + instance_url_name: instance_url, + feature_id: ids[0] + }); + + const featureEditUrl = reverse.Urls.map_feature_detail_edit({ + instance_url_name: instance_url, + feature_id: ids[0], + edit: 'edit' + }); + const isEmbedded = new URLSearchParams(window.location.search).get('embed') == "1"; + + return ( + <Popup position={latLng}> + <div id="map-feature-content"> + <div className="popup-content"> + <h4>{title}</h4> + <div className="popup-btns"> + {!isEmbedded + ? (<a href={featureUrl} className="btn btn-sm btn-secondary">More Details</a>) + : (<a href={featureUrl} target="_blank" className="btn btn-sm btn-secondary">More Details</a>) + } + { !isEmbedded && canEdit + ? <a href={featureEditUrl} className="btn btn-sm btn-info">Edit</a> + : "" + } + </div> + </div> + </div> + </Popup> + ); +} diff --git a/opentreemap/frontend/js/src/map/UtfGrid.js b/opentreemap/frontend/js/src/map/UtfGrid.js new file mode 100644 index 000000000..09edd72dd --- /dev/null +++ b/opentreemap/frontend/js/src/map/UtfGrid.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { withLeaflet } from 'react-leaflet'; +import { useLeafletContext, LayerProps, + createTileLayerComponent, + updateGridLayer, + withPane } from '@react-leaflet/core'; +import L from 'leaflet'; +import '../L.UTFGrid-min'; + + +export const UtfGrid = createTileLayerComponent( + function createTileLayer({ url, ...options }, context) { + const layer = new L.UTFGrid(url, options); + + layer.setUrl = function(url) { + layer._url = url; + layer._cache = {}; + layer._update(); + } + + layer.on('click', function(event) { + if (event.id == null) return; + //L.popup().setLatLng([event.latlng.lat, event.latlng.lng]).openOn(context.map); + }); + + //this.leafletElement = new L.UtfGrid(this.props.url, this.props.options); + return { + instance: layer, + context, + } + }, + (layer, props, prevProps) => { + const { url } = props; + updateGridLayer(layer, props, prevProps); + if (url != null && url !== prevProps.url) { + layer.setUrl(url); + layer.redraw(); + } + }); + //updateGridLayer); + +/* +export const UtfGrid = (props) => { + const context = useLeafletContext(); + + React.useEffect(() => { + const container = context.layerContainer || context.map; + const layer = new L.utfGrid(props.url, props.options); + + layer.setUrl = function(url) { + layer._url = url; + layer._cache = {}; + layer._update(); + } + + container.addLayer(layer); + + return () => { + container.removeLayer(layer); + }; + }); + + return null; +}; +*/ + +export default UtfGrid; diff --git a/opentreemap/frontend/js/src/sidebar/AddTreeSidebar.js b/opentreemap/frontend/js/src/sidebar/AddTreeSidebar.js new file mode 100644 index 000000000..849d58150 --- /dev/null +++ b/opentreemap/frontend/js/src/sidebar/AddTreeSidebar.js @@ -0,0 +1,563 @@ +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import axios from 'axios'; +import { Button } from 'react-bootstrap'; +import L from 'leaflet'; + +import { Diameter } from '../fields/Diameter'; +import { FieldGroup } from '../fields/FieldGroup'; +import { Species } from '../fields/Species'; +import { Photos } from '../fields/Photos'; +import { GeolocateTypeahead } from '../common/GeolocateTypeahead'; + +import { useMapEvents } from "react-leaflet"; + +import reverse from 'reverse'; + +import utility from 'treemap/lib/utility'; +import lonLatToWebMercator from 'treemap/lib/utility'; + + +export function AddTreeSidebar(props) { + + const instance_url = window.django.instance_url; + const csrfToken = window.django.csrf; + + const { onClose, map, onMapClick, clearLatLng, onAddMapFeature, markerRef } = props; + const latLng = markerRef?.getLatLng() ?? {lat: null, lng: null}; + const [stepNumber, setStepNumber] = useState(1); + const [ fieldGroups, setFieldGroups ] = useState(null); + + const [ treeData, setTreeData ] = useState({}); + const [ photos, setPhotos ] = useState({}); + + const [ saving, setSaving ] = useState(false); + + const [ shouldUseAllSpecies, setShouldUseAllSpecies ] = useState(false); + const [ isEmptyTreePit, setIsEmptyTreePit ] = useState(false); + + // these are the validation errors from the server + const [errors, setErrors] = useState({}); + + const [featureId, setFeatureId] = useState(0); + + // these are our post-creation pieces + const [postCreateAction, setPostCreateAction] = useState("addtree-done"); + + // clear out only the geometry for adding a new one + const addTreeSame = (data) => { + setStepNumber(1); + clearPhotos(); + clearLatLng(); + } + + // clear our everything to add a new one + const addTreeNew = (data) => { + setStepNumber(1); + clearLatLng(); + setTreeData({}); + } + + const addTreeViewDetails = (data) => { + // redirect + const url = reverse.Urls.map_feature_popup_detail({ + instance_url_name: window.django.instance_url, + feature_id: featureId + }); + window.location.href = url; + } + + const addTreeDone = (e) => { + onClose(); + } + + const updatePostCreateFunction = (e) => { + setPostCreateAction(e.target.value); + } + + const postCreateActionMap = { + 'addtree-addsame': (e) => addTreeSame(e), + 'addtree-addnew': (e) => addTreeNew(e), + 'addtree-viewdetails': (e) => addTreeViewDetails(e), + 'addtree-done': (e) => addTreeDone(e) + } + + const handleErrors = (errors) => { + const { fieldErrors, globalErrors } = errors; + + setErrors(fieldErrors); + setStepNumber(2); + } + + const updateTreeData = (identifier, value) => { + setTreeData({ + ...treeData, + [identifier]: value, + }); + } + + const addPhotos = (name, image) => { + const treeDataVariable = `has_${name}_photo`; + updateTreeData(treeDataVariable, true); + + setPhotos({ + ...photos, + [name]: image + }); + } + + /** + * Clear out all photo info in the tree data + */ + const clearPhotos = () => { + const treeDataUpdated = Object + .keys(treeData) + .filter(x => x.match(/has_\w*_photo/)) + .reduce((obj, key) => {obj[key] = false; return obj}, {...treeData}) + + setPhotos({}); + setTreeData(treeDataUpdated); + } + + useEffect(() => { + clearPhotos(); + }, [isEmptyTreePit]) + + /** + * Submit photos if available. + * Always return back the original data, which contains + * the original geo_rev_hash of the created element. + * The geo_rev_hash is used for map updating + */ + const submitPhotos = (data) => { + const featureId = data.featureId; + if (isEmptyTreePit) { + const url = reverse.Urls.add_photo_to_map_feature({ + instance_url_name: window.django.instance_url, + feature_id: featureId + }); + const label = "site"; + + if (!(label in photos)) { + return Promise.resolve(data); + } + + const formData = new FormData(); + formData.append('label', label); + formData.append('file', photos[label]); + return axios.post(url, formData, + {withCredential: true, + headers: {'X-CSRFToken': csrfToken, 'Content-Type': 'multipart/form-data'} + }).then(response => data); + } + + const url = reverse.Urls.add_photo_to_tree_with_label({ + instance_url_name: window.django.instance_url, + feature_id: featureId, + tree_id: data.treeId + }); + + return axios.all(Object.keys(photos).map(x => { + const formData = new FormData(); + formData.append('label', x); + formData.append('file', photos[x]); + return axios.post(url, formData, + {withCredential: true, + headers: {'X-CSRFToken': csrfToken, 'Content-Type': 'multipart/form-data'} + }); + })).then(response => data); + } + + useEffect(() => { + var url = `/${instance_url}/fields/`; + axios.get(url, {withCredential: true}) + .then(res => { + let fieldGroups = res.data.field_groups; + setFieldGroups(fieldGroups); + + // set our initial tree data as all empty + let initialFields = fieldGroups + .flatMap(x => x.fields) + .reduce((_map, field) => { + _map[field.identifier] = field.value || ""; + return _map; + }, {}); + // zip together the collection_udf_keys and the collection_udfs + let collectionUdfs = fieldGroups + .flatMap(fg => fg.collection_udf_keys + .map((k, i) => [k, fg.collection_udfs[i]])) + .reduce((_map, udf) => { + _map[udf[0]] = udf[1].iscollection ? [] : ""; + return _map; + }, {}); + + setTreeData({ + ...initialFields, + ...collectionUdfs + }); + }).catch(res => { + }); + }, []); + + + const closeHandler = (e) => { + e.preventDefault(); + onClose(); + } + + const lonLatToWebMercator = (lon, lat) => { + var originShift = (2.0 * Math.PI * (6378137.0 / 2.0)) / 180.0; + return { + x: originShift * lon, + y: originShift * (Math.log(Math.tan((90.0 + lat) * (Math.PI / 360.0)))) / (Math.PI / 180.0) + }; + }; + + const onComplete = () => { + const position = markerRef.getLatLng(); + const treeDataFormatted = { + // fix the dates, because we don't want to fix them on the backend yet + ...Object.keys(treeData).reduce((obj, key) => { + const value = treeData[key]; + obj[key] = value instanceof Date + ? value.toISOString().split('T')[0] + : value; + return obj + }, treeData), + "plot.geom": lonLatToWebMercator(position.lng, position.lat), + // species for the server is just the ID + "tree.species": treeData["tree.species"]?.id, + "is_empty_site": isEmptyTreePit + }; + + // hacky, and this should happen on the backend + if (!isEmptyTreePit) { + delete treeDataFormatted['has_site_photo']; + } + + const url = reverse.Urls.addPlot(instance_url); + setSaving(true); + axios.post(url, + treeDataFormatted, + {withCredential: true, headers: {"X-CSRFToken": csrfToken}}, + ).then(res => { + setFeatureId(res.data.featureId); + return submitPhotos(res.data); + }).then(data => { + setFeatureId(0); + setSaving(false); + postCreateActionMap[postCreateAction](data); + onAddMapFeature(data); + if (data.treeId) { + var url = reverse.Urls.inaturalist_create_observation_for_tree({ + instance_url_name: instance_url, + tree_id: data.treeId + }); + return axios.post( + url, + {withCredential: true, headers: {"X-CSRFToken": csrfToken}}); + } + return new Promise((resolve, reject) => {}); + }).catch(err => { + if (err?.response?.data != null) { + handleErrors(err.response.data); + } + setFeatureId(0); + setSaving(false); + }); + } + + const step = stepNumber == 1 + ? <FirstStep + closeHandler={onClose} + map={map} + onMapClick={onMapClick} + latLng={latLng} + onNext={() => setStepNumber(2)} /> + : stepNumber == 2 + ? <SecondStep + latLng={latLng} + closeHandler={onClose} + fieldGroups={fieldGroups} + updateTreeData={updateTreeData} + treeData={treeData} + addPhotos={addPhotos} + clearPhotos={clearPhotos} + shouldUseAllSpecies={shouldUseAllSpecies} + setShouldUseAllSpecies={setShouldUseAllSpecies} + isEmptyTreePit={isEmptyTreePit} + setIsEmptyTreePit={setIsEmptyTreePit} + errors={errors} + //onComplete={() => onComplete()} + onNext={() => setStepNumber(3)} + onPrevious={() => setStepNumber(1)}/> + : stepNumber == 3 + ? <ThirdStep + closeHandler={onClose} + treeData={treeData} + updatePostCreateFunction={updatePostCreateFunction} + postCreateAction={postCreateAction} + onComplete={() => onComplete()} + onPrevious={() => setStepNumber(2)} + saving={saving} + /> + : null; + + return ( + <div id="sidebar-add-tree"> + <div className="sidebar-inner"> + <a + className="close cancelBtn small d-none d-sm-block" + style={{zIndex: 99}} + onClick={closeHandler}>×</a> + <h3>Add a Tree</h3> + {step} + </div> + </div> + ); +} + + +function Step(props) { + const { closeHandler, latLng, stepNumber } = props; + // disable going to the next step until we complete our location + const nextDisabled = stepNumber == 1 && latLng.lat == null && latLng.lng == null; + return ( + <div className="add-step-container"> + <div className={`add-step ${props.withMap ? "with-map" : ""} active`}> + <div className="add-step-header"> + {props.stepHeader || ''} + <a + className="close cancelBtn small d-block d-sm-none" + style={{zIndex: 99}} + onClick={closeHandler}>×</a> + </div> + <div className="add-step-content"> {props.children} </div> + <div className="add-step-footer"> + + <ul className="pagination justify-content-center"> + <li className="page-item"> + {stepNumber != 1 + ? <a className="btn btn-primary page-link" onClick={props.onPrevious}>Back</a> + : '' + } + </li> + <li className="page-item disabled"> + <span className="page-link">Step {stepNumber} of 3</span> + </li> + <li className="page-item"> + {props.stepNumber != 3 + ? <a + className={`page-link btn ${nextDisabled ? "btn-secondary" : "btn-primary"}`} + onClick={nextDisabled ? null : props.onNext}>Next</a> + : <a className="page-link btn btn-primary" onClick={props.onComplete}>Done</a> + } + </li> + </ul> + </div> + </div> + </div> + ); + +} + + +function FirstStep(props) { + const { map, onMapClick } = props; + const [ errors, setErrors ] = useState(null); + + const onLocationFound = (e) => { + map.flyTo(e.latlng, map.getZoom()); + onMapClick(e); + } + const ref = useRef(); + + useEffect(() => { + if (map != null) { + map.on('locationfound', onLocationFound) + } + }, [map]); + + const stepHeader = "1. Set the tree's location"; + return ( + <Step + stepNumber={1} + stepHeader={stepHeader} + withMap={true} + {...props} + > + + <GeolocateTypeahead + onLocationFound={onLocationFound} + handleErrors={(e) => setErrors(e)} + /> + + <div> + <a className="geolocate" onClick={() => map.locate()}><i className="icon-direction"></i> Use current location</a> + </div> + + <div className="alert alert-info move-market-message hidden-xs d-none d-sm-block"> + Choose a point on the map or select "Use Current Location" + </div> + + <div className="alert alert-info move-market-message hidden-xs d-none d-sm-block"> + Please move marker to the exact location of the tree. + </div> + </Step> + ); +} + + +function SecondStep(props) { + const stepHeader = "2. Add species and additional info"; + const { + fieldGroups, + updateTreeData, + treeData, + addPhotos, + clearPhotos, + errors, + shouldUseAllSpecies, + setShouldUseAllSpecies, + isEmptyTreePit, + setIsEmptyTreePit + } = props; + + if (!fieldGroups) return (<div>Loading...</div>); + + const fieldGroupsTree = fieldGroups.find(x => x.model == 'tree') + + // filter these out diameter and species and handle them separately + const diameter = 'tree.diameter'; + const diameterFieldProps = fieldGroupsTree?.fields.find(x => x.identifier == diameter); + const diameterField = diameterFieldProps + ? <Diameter + identifier={diameter} + updateTreeData={updateTreeData} + treeData={treeData} + errors={errors != null ? errors[diameter] : null} + {...diameterFieldProps} /> + : ''; + + const species = 'tree.species'; + const speciesFieldProps = fieldGroupsTree?.fields.filter(x => x.identifier == diameter) + const speciesField = speciesFieldProps + ? <Species + identifier={species} + updateTreeData={updateTreeData} + treeData={treeData} + shouldUseAllSpecies={shouldUseAllSpecies} + setShouldUseAllSpecies={setShouldUseAllSpecies} + isEmptyTreePit={isEmptyTreePit} + setIsEmptyTreePit={setIsEmptyTreePit} + errors={errors != null ? errors[species] : null} + {...speciesFieldProps} /> + : ''; + + const isErrorsEmpty = errors && Object.keys(errors).length === 0 && errors.constructor === Object; + + return ( + <Step + stepNumber={2} + stepHeader={stepHeader} + {...props} + > + {!isErrorsEmpty + ? <div className="alert-danger">Please correct all errors (scroll below)</div> + : '' + } + <Photos + updateTreeData={updateTreeData} + treeData={treeData} + isEmptyTreePit={isEmptyTreePit} + addPhotos={addPhotos} + clearPhotos={clearPhotos} + errors={errors != null ? errors['tree.photos'] : null} + /> + + <hr /> + + {speciesField} + + <hr /> + + {isEmptyTreePit ? '' : diameterField} + + <hr /> + + {fieldGroups + .filter(x => x.model == 'plot' || (x.model == 'tree' && !isEmptyTreePit)) + .map((fieldGroup, i) => <FieldGroup + key={i} + fieldGroup={fieldGroup} + updateTreeData={updateTreeData} + treeData={treeData} + errors={errors} + filterFields={[diameter, species]} />)} + </Step> + ); +} + + +function ThirdStep(props) { + const stepHeader = "3. Finalize this tree" + const { updatePostCreateFunction, postCreateAction, saving } = props + + return ( + <Step + stepNumber={3} + stepHeader={stepHeader} + {...props} + > + <hr /> + <label>After I add this tree...</label> + {saving + ? <div><img className="spinner" src='/static/img/spinner.gif' />Saving...</div> + : '' + } + <div> + <input + type="radio" + name="addFeatureOptions" + id="addtree-addsame" + value="addtree-addsame" + checked={postCreateAction == "addtree-addsame"} + onChange={updatePostCreateFunction} + /> + <label htmlFor="addtree-addsame">Add another tree with these details</label> + </div> + <div> + <input + type="radio" + name="addFeatureOptions" + id="addtree-addnew" + value="addtree-addnew" + checked={postCreateAction == "addtree-addnew"} + onChange={updatePostCreateFunction} + /> + <label htmlFor="addtree-addnew">Add another tree with new details</label> + </div> + <div> + <input + type="radio" + name="addFeatureOptions" + id="addtree-viewdetails" + value="addtree-viewdetails" + checked={postCreateAction == "addtree-viewdetails"} + onChange={updatePostCreateFunction} + /> + <label htmlFor="addtree-viewdetails">Continue editing this tree</label> + </div> + <div> + <input + type="radio" + name="addFeatureOptions" + id="addtree-done" + value="addtree-done" + checked={postCreateAction == "addtree-done"} + onChange={updatePostCreateFunction} + /> + <label htmlFor="addtree-done">I'm done!</label> + </div> + </Step> + ); +} diff --git a/opentreemap/frontend/js/src/sidebar/DetailSidebar.js b/opentreemap/frontend/js/src/sidebar/DetailSidebar.js new file mode 100644 index 000000000..d1d200f68 --- /dev/null +++ b/opentreemap/frontend/js/src/sidebar/DetailSidebar.js @@ -0,0 +1,134 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Accordion, Card, useAccordionToggle } from 'react-bootstrap'; +import axios from 'axios'; + +import { EcobenefitsPanel } from './EcobenefitsSideBar'; +import { MapFeatureDetailAccordion } from './MapFeatureDetailAccordion'; + + +export function DetailSidebar(props) { + const { benefits, sidebarInfo, map, addSelectedMarkerInfo } = props; + const accordionRef = useRef(null); + + const [activeId, setActiveId] = useState('1'); + const [expandedDetails, setExpandedDetails] = useState(false); + const [expandedBenefits, setExpandedBenefits] = useState(false); + + // to keep track of previous center for expanding and collapsing + const [mapCenter, setMapCenter] = useState(false); + + useEffect(() => { + const ac = accordionRef; + setActiveId(sidebarInfo != null + ? '0' + : '1' + ); + setExpandedDetails(false); + setExpandedBenefits(false); + }, [sidebarInfo]); + + const toggleActiveId = (id) => { + setActiveId(id == activeId + ? null + : id + ); + } + + const toggleExpandedDetails = () => { + var expandedDetailsNew = !expandedDetails; + var newMapCenter = null; + if (expandedDetailsNew){ + // save the previous center, move to this data point + setMapCenter(map.getCenter()); + newMapCenter = addSelectedMarkerInfo.latLng; + document.body.classList.add('open'); + document.body.classList.add('hide-search'); + } else { + newMapCenter = mapCenter; + setMapCenter(null); + document.body.classList.remove('open'); + document.body.classList.remove('hide-search'); + } + + setExpandedDetails(expandedDetailsNew); + + setTimeout(() => { + map.invalidateSize(); + map.panTo( + newMapCenter, { + animate: true, + duration: 0.4, + easeLinearity: 0.1 + }); + }, 500); + } + + const toggleExpandedBenefits = () => { + var expandedBenefitsNew = !expandedBenefits; + if (expandedBenefitsNew){ + document.body.classList.add('open'); + document.body.classList.add('hide-search'); + } else { + document.body.classList.remove('open'); + document.body.classList.remove('hide-search'); + } + + setExpandedBenefits(expandedBenefitsNew); + } + + return (<> + <Accordion defaultActiveKey={activeId} className="panel" ref={accordionRef} activeKey={activeId}> + <Card className={`panel-group ${expandedDetails ? 'expanded with-map' : ''}`}> + <Accordion.Toggle + as={Card.Header} + onClick={() => toggleActiveId('0')} + eventKey="0" + className="panel-heading" + > + <a className="panel-toggle"> + Details + <span className="arrow pull-right"> + <i className="icon-right-open"></i> + </span> + </a> + </Accordion.Toggle> + <Accordion.Collapse + eventKey="0" + className="" + > + {sidebarInfo != null + ? <MapFeatureDetailAccordion + onToggleClick={() => toggleExpandedDetails()} + {...sidebarInfo} + /> + : <div></div> + } + </Accordion.Collapse> + </Card> + <Card className={`panel-group ${expandedBenefits ? 'expanded' : ''}`}> + <Accordion.Toggle + as={Card.Header} + eventKey="1" + className="panel-heading" + onClick={() => toggleActiveId('1')} + > + <a className="panel-toggle"> + Eco Benefits + <span className="arrow pull-right"> + <i className="icon-right-open"></i> + </span> + </a> + </Accordion.Toggle> + <Accordion.Collapse + eventKey="1" + > + <EcobenefitsPanel + benefits={benefits?.benefits} + basis={benefits?.basis} + onToggleClick={() => toggleExpandedBenefits()} + /> + </Accordion.Collapse> + </Card> + </Accordion> + </>); +} diff --git a/opentreemap/frontend/js/src/sidebar/EcobenefitsSideBar.js b/opentreemap/frontend/js/src/sidebar/EcobenefitsSideBar.js new file mode 100644 index 000000000..e58ab8698 --- /dev/null +++ b/opentreemap/frontend/js/src/sidebar/EcobenefitsSideBar.js @@ -0,0 +1,98 @@ +import React, { useEffect, useRef, useState } from 'react'; +import axios from 'axios'; +import { Accordion, Card, useAccordionToggle } from 'react-bootstrap'; + +export function EcobenefitsSideBar(props) { + const { benefits, onToggleClick } = props; + const [expanded, setExpanded] = useState(false); + //const instance_url = window.django.instance_url; + //const [benefits, setBenefits] = useState(null); + + /* + useEffect(() => { + // clear the title before loading a new one + var url = `/${instance_url}/benefit/search/api`; + axios.get(url, {withCredential: true}) + .then(res => { + setBenefits(res.data); + console.log(res); + }).catch(res => { + console.log('error'); + console.log(res); + }); + }, []); + */ + + if (benefits == null) return "Loading.."; + + return ( + <div id="sidebar-browse-trees"> + <div className="panel"> + <div className="panel-group"> + <div className="panel-heading"> + <a className="panel-toggle"> + Detail / Eco Benefits + <span className="arrow pull-right"> + <i className="icon-right-open"></i> + </span> + </a> + </div> + <div className="collape in panel-body"> + <EcobenefitsPanel benefits={benefits} onToggleClick={onToggleClick} /> + </div> + </div> + </div> + </div> + ); +} + +export function EcobenefitsPanel(props) { + const { benefits, basis, onToggleClick } = props; + if (benefits == null) return (<div></div>); + + return ( + <div className="panel-body"> + <div className="panel-inner benefit-values"> + <a + className="sidebar-panel-toggle visible-xs-block d-block d-sm-none" + onClick={() => onToggleClick()} + > + <i className="icon-right-open"></i> + </a> + <BenefitRow isTotal={true} {...benefits.all.totals} /> + <div className="benefit-value-title">Tree Benefits</div> + {Object.values(benefits.plot).map((x, i) => { + return <BenefitRow key={i} {...x} />; + })} + <div className="benefit-tree-count"> + Based on {basis?.plot?.n_objects_used?.toLocaleString()} out of {basis?.plot?.n_total.toLocaleString()} total trees. + </div> + </div> + </div> + ); + +} + +function BenefitRow(props){ + const iconClass = props.icon != null ? `icon-${props.icon}` : "icon-sun-filled"; + var benefitContent = null; + if (props.value != null) { + benefitContent = `${props.value} ${props.unit}`; + if (props.currency_saved) { + benefitContent += ` saved ${props.currency_saved}`; + } + } else if (props.currency_saved) { + benefitContent = `${props.currency_saved} saved`; + } + + if (benefitContent == null) return ""; + return ( + <div className={`benefit-value-row ${props.isTotal ? 'benefit-total' : ''}`}> + <div className="benefit-icon"><i className={iconClass} /></div> + <h3 className="benefit-label">{props.label}</h3> + <span className="benefit-content"> + {benefitContent} + </span> + </div> + ); +} diff --git a/opentreemap/frontend/js/src/sidebar/MapFeatureDetailAccordion.js b/opentreemap/frontend/js/src/sidebar/MapFeatureDetailAccordion.js new file mode 100644 index 000000000..c971edbbd --- /dev/null +++ b/opentreemap/frontend/js/src/sidebar/MapFeatureDetailAccordion.js @@ -0,0 +1,64 @@ +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import { Accordion, Card, useAccordionToggle } from 'react-bootstrap'; + +import reverse from 'reverse'; + +import { SpeciesTitle } from '../fields/Species'; +import { EcobenefitsPanel } from './EcobenefitsSideBar'; +import { DiameterReadOnly } from '../fields/Diameter'; +import { FieldReadOnly } from '../fields/FieldGroup'; + + +export function MapFeatureDetailAccordion(props) { + const { benefits, tree, feature, plot, units, onToggleClick } = props; + const hasTree = tree.id != null; + + const featureUrl = reverse.Urls.map_feature_detail({ + instance_url_name: window.django.instance_url, + feature_id: feature.id + }); + + //<a className="btn" id="full-details-btn" href={featureUrl}>More Details</a> + const isEmbedded = new URLSearchParams(window.location.search).get('embed') == "1"; + + return (<div className="panel-body"> + <div className="panel-body-buttons-wrapper"> + <div className="panel-body-buttons"> + {!isEmbedded + ? (<a className="btn" id="full-details-btn" href={featureUrl}>More Details</a>) + : '' + } + </div> + </div> + <div className="panel-inner" onClick={() => onToggleClick()}> + <Accordion.Toggle + as={Card.Header} + className="visible-xs-block feature-info d-block d-sm-none" + onClick={() => onToggleClick()} + > + <h4>{hasTree ? tree.species.common_name : "Empty Tree Pit"}</h4> + </Accordion.Toggle> + + {hasTree + ? <form id="details-form"> + <SpeciesTitle + commonName={tree.species.common_name} + scientificName={tree.species.scientific_name} + /> + + <FieldReadOnly + label={"Trunk Diameter"} + units={units['tree.diameter']} + value={tree.diameter} /> + <FieldReadOnly + label={"Tree Height"} + units={units['tree.height']} + value={tree.height} /> + + <EcobenefitsPanel benefits={benefits} /> + </form> + : ''} + </div> + + </div>); +} diff --git a/opentreemap/frontend/js/src/util/bing/Bing.js b/opentreemap/frontend/js/src/util/bing/Bing.js new file mode 100644 index 000000000..47f4ac1a5 --- /dev/null +++ b/opentreemap/frontend/js/src/util/bing/Bing.js @@ -0,0 +1,11 @@ +import { createLayerComponent } from '@react-leaflet/core'; +import {bingLayer} from './leaflet.bing'; + +const createLeafletElement = (props) => { + + const instance = L.bingLayer(props.bingkey, props); + + return { instance }; + } + +export const BingLayer = createLayerComponent(createLeafletElement); diff --git a/opentreemap/frontend/js/src/util/bing/README.txt b/opentreemap/frontend/js/src/util/bing/README.txt new file mode 100644 index 000000000..a09f9c1ed --- /dev/null +++ b/opentreemap/frontend/js/src/util/bing/README.txt @@ -0,0 +1 @@ +All code came from https://github.com/TA-Geoforce/react-leaflet-bing-v2/ diff --git a/opentreemap/frontend/js/src/util/bing/index.js b/opentreemap/frontend/js/src/util/bing/index.js new file mode 100644 index 000000000..46c36cd24 --- /dev/null +++ b/opentreemap/frontend/js/src/util/bing/index.js @@ -0,0 +1 @@ +export { BingLayer } from "./Bing"; diff --git a/opentreemap/frontend/js/src/util/bing/leaflet.bing.js b/opentreemap/frontend/js/src/util/bing/leaflet.bing.js new file mode 100644 index 000000000..a540e1063 --- /dev/null +++ b/opentreemap/frontend/js/src/util/bing/leaflet.bing.js @@ -0,0 +1,132 @@ +L.BingLayer = L.TileLayer.extend({ + options: { + subdomains: [0, 1, 2, 3], + type: 'Aerial', + attribution: 'Bing', + culture: '', + style: '' + }, + + initialize: function (bing_key, options) { + L.Util.setOptions(this, options); + + this._bing_key = bing_key; + this._url = null; + this._providers = []; + this.metaRequested = false; + }, + + tile2quad: function (x, y, z) { + var quad = ''; + for (var i = z; i > 0; i--) { + var digit = 0; + var mask = 1 << (i - 1); + if ((x & mask) !== 0) digit += 1; + if ((y & mask) !== 0) digit += 2; + quad = quad + digit; + } + return quad; + }, + + getTileUrl: function (tilePoint) { + var zoom = this._getZoomForUrl(); + var subdomains = this.options.subdomains, + s = this.options.subdomains[Math.abs((tilePoint.x + tilePoint.y) % subdomains.length)]; + return this._url.replace('{subdomain}', s) + .replace('{quadkey}', this.tile2quad(tilePoint.x, tilePoint.y, zoom)) + .replace('{culture}', this.options.culture); + }, + + loadMetadata: function () { + if (this.metaRequested) return; + this.metaRequested = true; + var _this = this; + var cbid = '_bing_metadata_' + L.Util.stamp(this); + window[cbid] = function (meta) { + window[cbid] = undefined; + var e = document.getElementById(cbid); + e.parentNode.removeChild(e); + if (meta.errorDetails) { + console.log(meta.errorDetails); + return; + } + _this.initMetadata(meta); + }; + var urlScheme = (document.location.protocol === 'file:') ? 'http' : document.location.protocol.slice(0, -1); + var url = urlScheme + '://dev.virtualearth.net/REST/v1/Imagery/Metadata/' + + this.options.type + '?include=ImageryProviders&jsonp=' + cbid + + '&key=' + this._bing_key + '&UriScheme=' + urlScheme + '&culture=' + this.options.culture + '&style=' + this.options.style; + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + script.id = cbid; + document.getElementsByTagName('head')[0].appendChild(script); + }, + + initMetadata: function (meta) { + var r = meta.resourceSets[0].resources[0]; + this.options.subdomains = r.imageUrlSubdomains; + this._url = r.imageUrl; + if (r.imageryProviders) { + for (var i = 0; i < r.imageryProviders.length; i++) { + var p = r.imageryProviders[i]; + for (var j = 0; j < p.coverageAreas.length; j++) { + var c = p.coverageAreas[j]; + var coverage = {zoomMin: c.zoomMin, zoomMax: c.zoomMax, active: false}; + var bounds = new L.LatLngBounds( + new L.LatLng(c.bbox[0]+0.01, c.bbox[1]+0.01), + new L.LatLng(c.bbox[2]-0.01, c.bbox[3]-0.01) + ); + coverage.bounds = bounds; + coverage.attrib = p.attribution; + this._providers.push(coverage); + } + } + } + this._update(); + }, + + _update: function () { + if (this._url === null || !this._map) return; + this._update_attribution(); + L.TileLayer.prototype._update.apply(this, []); + }, + + _update_attribution: function () { + var bounds = this._map.getBounds(); + var zoom = this._map.getZoom(); + for (var i = 0; i < this._providers.length; i++) { + var p = this._providers[i]; + if ((zoom <= p.zoomMax && zoom >= p.zoomMin) && + bounds.intersects(p.bounds)) { + if (!p.active && this._map.attributionControl) + this._map.attributionControl.addAttribution(p.attrib); + p.active = true; + } else { + if (p.active && this._map.attributionControl) + this._map.attributionControl.removeAttribution(p.attrib); + p.active = false; + } + } + }, + + onAdd: function (map) { + this.loadMetadata(); + L.TileLayer.prototype.onAdd.apply(this, [map]); + }, + + onRemove: function (map) { + for (var i = 0; i < this._providers.length; i++) { + var p = this._providers[i]; + if (p.active && this._map.attributionControl) { + this._map.attributionControl.removeAttribution(p.attrib); + p.active = false; + } + } + L.TileLayer.prototype.onRemove.apply(this, [map]); + } +}); + +L.bingLayer = function (bing_key, options) { + return new L.BingLayer(bing_key, options); +}; diff --git a/opentreemap/frontend/migrations/__init__.py b/opentreemap/frontend/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opentreemap/frontend/models.py b/opentreemap/frontend/models.py new file mode 100644 index 000000000..1dfab7604 --- /dev/null +++ b/opentreemap/frontend/models.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. diff --git a/opentreemap/frontend/templates/frontend/account_dashboard.html b/opentreemap/frontend/templates/frontend/account_dashboard.html new file mode 100644 index 000000000..9a1675df6 --- /dev/null +++ b/opentreemap/frontend/templates/frontend/account_dashboard.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +{% extends "instance_base.html" %} +{% load render_bundle from webpack_loader %} + +{% block header %} +{% endblock header %} +{% block subhead %} +{% endblock subhead %} + +{% block content %} +<script> + window.django = { + csrf: "{{ csrf_token }}", + instance_url: "{{ request.instance.url_name }}", + user: { + username: "{{ request.user.username }}", + full_name: "{{ request.user.get_full_name }}", + last_login: "{{ request.user.last_login }}", + is_authenticated: "{{ request.user.is_authenticated|yesno:"true,false" }}" == "true", + }, + logoUrl: "{{ request.instance.logo.url }}" + }; +</script> +<div id="app" style="height: 100%;" class="app-wrapper"></div> + {% render_bundle 'js/frontend/account_profile' %} +{% endblock content %} diff --git a/opentreemap/frontend/templates/frontend/index.html b/opentreemap/frontend/templates/frontend/index.html new file mode 100644 index 000000000..dd75d2414 --- /dev/null +++ b/opentreemap/frontend/templates/frontend/index.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +{% extends "instance_base.html" %} +{% load render_bundle from webpack_loader %} + +{% block header %} +{% endblock header %} +{% block subhead %} +{% endblock subhead %} + +{% block content %} +<script> + window.django = { + csrf: "{{ csrf_token }}", + instance_url: "{{ request.instance.url_name }}", + instance_center_x: "{{ request.instance.center_lat_lng.x }}", + instance_center_y: "{{ request.instance.center_lat_lng.y }}", + user: { + username: "{{ request.user.username }}", + full_name: "{{ request.user.get_full_name }}", + last_login: "{{ request.user.last_login }}", + is_authenticated: "{{ request.user.is_authenticated|yesno:"true,false" }}" == "true", + }, + shouldAddTree: "{{ shouldAddTree|yesno:"true,false"}}" == "true", + googleApiKey: "{{settings.GOOGLE_MAPS_API_KEY}}", + bingApiKey: "{{settings.BING_API_KEY}}", + logoUrl: "{{ request.instance.logo.url }}" + }; +</script> +<div id="app" style="height: 100%;" class="app-wrapper"></div> + {% render_bundle 'js/frontend/index' %} +{% endblock content %} diff --git a/opentreemap/frontend/tests.py b/opentreemap/frontend/tests.py new file mode 100644 index 000000000..5982e6bcd --- /dev/null +++ b/opentreemap/frontend/tests.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.test import TestCase + +# Create your tests here. diff --git a/opentreemap/frontend/urls.py b/opentreemap/frontend/urls.py new file mode 100644 index 000000000..2b9346073 --- /dev/null +++ b/opentreemap/frontend/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url +from treemap.urls import USERNAME_PATTERN + +from . import views + +urlpatterns = [ + #url('', render_template('frontend/index.html')()), + #url('', views.index_page), + url(r'^$', views.react_map_page, name='react_map_index'), + url(r'^map/$', views.react_map_page, name='react_map'), + url(r'^user-dashboard/$', views.user_dashboard, name='user_dashboard'), +] diff --git a/opentreemap/frontend/views.py b/opentreemap/frontend/views.py new file mode 100644 index 000000000..e3e95af3b --- /dev/null +++ b/opentreemap/frontend/views.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import collections +from functools import partial + +from django.conf import settings +from django.http import HttpResponse, HttpResponseRedirect +from django.http.request import QueryDict +from django_tinsel.decorators import route, render_template, username_matches_request_user +from django_tinsel.utils import decorate as do +from django.shortcuts import render, get_object_or_404 +from django.utils.translation import ugettext as _ +from django.urls import reverse + +from treemap.decorators import get_instance_or_404, instance_request, login_or_401 +from treemap.lib.user import get_audits, get_user_instances, get_audits_params +from treemap.models import User + + +def get_map_view_context(request, instance): + # the add tree link goes to this page with the query parameter m for mode + return { + "shouldAddTree": request.GET.get('m', '') == 'addTree', + "googleApiKey": settings.GOOGLE_MAPS_API_KEY + } + + +def index(request, instance): + return HttpResponseRedirect(reverse('react_map', kwargs={'instance_url_name': instance.url_name})) + + +index_page = instance_request(index) + + +def user_info(request, instance): + user = request.user + instance_id = request.GET.get('instance_id', None) + + instance = (get_instance_or_404(pk=instance_id) + if instance_id else None) + + query_vars = QueryDict(mutable=True) + if instance_id: + query_vars['instance_id'] = instance_id + + audit_dict = get_audits(request.user, instance, query_vars, + user=user, should_count=True) + + reputation = user.get_reputation(instance) if instance else None + + public_fields = [] + private_fields = [] + + return {'user': user, + 'its_me': user.id == request.user.id, + 'reputation': reputation, + 'instance_id': instance_id, + 'instances': get_user_instances(request.user, user, instance), + 'total_edits': audit_dict['total_count'], + 'audits': audit_dict['audits'], + } + + +######## +# Move this to a routes file +######## +react_map_page = do( + instance_request, + #ensure_csrf_cookie, + render_template('frontend/index.html'), + get_map_view_context) + + +#user = route( +# GET=do( +# #username_matches_request_user, +# render_template('treemap/user.html'), +# user_info) +# ) + +user_dashboard = route( + GET=do( + instance_request, + #username_matches_request_user, + login_or_401, + render_template('frontend/account_dashboard.html'), + user_info) + ) diff --git a/opentreemap/geocode/tests.py b/opentreemap/geocode/tests.py index e7cca0474..6dc89cb62 100644 --- a/opentreemap/geocode/tests.py +++ b/opentreemap/geocode/tests.py @@ -1,11 +1,9 @@ -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json import requests import os -from urllib import urlencode +from urllib.parse import urlencode from unittest import skipIf from django.http import HttpResponse diff --git a/opentreemap/geocode/urls.py b/opentreemap/geocode/urls.py index b66fdbe70..67c3653cd 100644 --- a/opentreemap/geocode/urls.py +++ b/opentreemap/geocode/urls.py @@ -1,6 +1,4 @@ -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import url from geocode.views import geocode_view, get_esri_token_view diff --git a/opentreemap/geocode/views.py b/opentreemap/geocode/views.py index 8c2e32e04..6628895c9 100644 --- a/opentreemap/geocode/views.py +++ b/opentreemap/geocode/views.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json @@ -87,13 +85,15 @@ def geocode(request): Search for specified address, returning candidates with lat/long """ key = request.GET.get('key') - address = request.GET.get('address').encode('utf-8') + address = request.GET.get('address') for_storage = 'forStorage' in request.GET if key: # See settings.OMGEO_SETTINGS for configuration pq = PlaceQuery(query=address, key=key, for_storage=for_storage) geocode_result = geocoder.geocode(pq) + g = Geocoder() + geocode_result = g.geocode(address) candidates = geocode_result.get('candidates', None) if candidates: # There should only be one candidate since the user already chose a diff --git a/opentreemap/importer/errors.py b/opentreemap/importer/errors.py index ee5b2e954..b9249ef8f 100644 --- a/opentreemap/importer/errors.py +++ b/opentreemap/importer/errors.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.utils.translation import ugettext_lazy as _ diff --git a/opentreemap/importer/fields.py b/opentreemap/importer/fields.py index 024e0f0d6..961e3f214 100644 --- a/opentreemap/importer/fields.py +++ b/opentreemap/importer/fields.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from treemap.models import Species @@ -169,7 +167,12 @@ class trees(object): # TODO: READONLY restore when implemented # Note: this is a tuple and not a set so it will be ordered in exports - ALL = tuple([p[1] for p in EXPORTER_PAIRS if p[1] not in IGNORED]) + # List comprehensions create a scope that does not have access to the + # previously defined class variables, so we use a lambda to bind the + # variables and make them available + # https://stackoverflow.com/a/13913933 + ALL = (lambda IGNORED=IGNORED, EXPORTER_PAIRS=EXPORTER_PAIRS: + tuple([p[1] for p in EXPORTER_PAIRS if p[1] not in IGNORED]))() def title_case(field_names): diff --git a/opentreemap/importer/js/src/bulkUpload.js b/opentreemap/importer/js/src/bulkUpload.js index 2d84736cf..4b2912110 100644 --- a/opentreemap/importer/js/src/bulkUpload.js +++ b/opentreemap/importer/js/src/bulkUpload.js @@ -8,5 +8,5 @@ var $ = require('jquery'), adminPage.init(); require('importer/lib/importsList.js').init({ - startImportUrl: reverse['importer:start_import'](config.instance.url_name) + startImportUrl: reverse.Urls['importer:start_import'](config.instance.url_name) }); diff --git a/opentreemap/importer/migrations/0001_initial.py b/opentreemap/importer/migrations/0001_initial.py index 71fd66802..2375be7a3 100644 --- a/opentreemap/importer/migrations/0001_initial.py +++ b/opentreemap/importer/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations @@ -64,7 +64,7 @@ class Migration(migrations.Migration): ('finished', models.BooleanField(default=False)), ('errors', models.TextField(default='')), ('status', models.IntegerField(default=3)), - ('import_event', models.ForeignKey(to='importer.TreeImportEvent')), + ('import_event', models.ForeignKey(on_delete=models.CASCADE, to='importer.TreeImportEvent')), ], ), ] diff --git a/opentreemap/importer/migrations/0002_auto_20150630_1556.py b/opentreemap/importer/migrations/0002_auto_20150630_1556.py index 4c2a159da..a5c56961d 100644 --- a/opentreemap/importer/migrations/0002_auto_20150630_1556.py +++ b/opentreemap/importer/migrations/0002_auto_20150630_1556.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations from django.conf import settings @@ -17,36 +17,36 @@ class Migration(migrations.Migration): migrations.AddField( model_name='treeimportrow', name='plot', - field=models.ForeignKey(blank=True, to='treemap.Plot', null=True), + field=models.ForeignKey(on_delete=models.CASCADE, blank=True, to='treemap.Plot', null=True), ), migrations.AddField( model_name='treeimportevent', name='instance', - field=models.ForeignKey(to='treemap.Instance'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance'), ), migrations.AddField( model_name='treeimportevent', name='owner', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='speciesimportrow', name='import_event', - field=models.ForeignKey(to='importer.SpeciesImportEvent'), + field=models.ForeignKey(on_delete=models.CASCADE, to='importer.SpeciesImportEvent'), ), migrations.AddField( model_name='speciesimportrow', name='species', - field=models.ForeignKey(blank=True, to='treemap.Species', null=True), + field=models.ForeignKey(on_delete=models.CASCADE, blank=True, to='treemap.Species', null=True), ), migrations.AddField( model_name='speciesimportevent', name='instance', - field=models.ForeignKey(to='treemap.Instance'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance'), ), migrations.AddField( model_name='speciesimportevent', name='owner', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL), ), ] diff --git a/opentreemap/importer/migrations/0003_add_lost_job_bookkeeping_fields.py b/opentreemap/importer/migrations/0003_add_lost_job_bookkeeping_fields.py index 87385fbf2..55ec70490 100644 --- a/opentreemap/importer/migrations/0003_add_lost_job_bookkeeping_fields.py +++ b/opentreemap/importer/migrations/0003_add_lost_job_bookkeeping_fields.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/importer/migrations/0004_make_new_fields_nonnullable.py b/opentreemap/importer/migrations/0004_make_new_fields_nonnullable.py index 350a4a855..5287d182d 100644 --- a/opentreemap/importer/migrations/0004_make_new_fields_nonnullable.py +++ b/opentreemap/importer/migrations/0004_make_new_fields_nonnullable.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/importer/migrations/0005_add_import_event_schema_version.py b/opentreemap/importer/migrations/0005_add_import_event_schema_version.py index 25aeecac1..0e2b1eadc 100644 --- a/opentreemap/importer/migrations/0005_add_import_event_schema_version.py +++ b/opentreemap/importer/migrations/0005_add_import_event_schema_version.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/importer/migrations/0006_add_index_to_tree_import_row.py b/opentreemap/importer/migrations/0006_add_index_to_tree_import_row.py index b555d336c..ae6228142 100644 --- a/opentreemap/importer/migrations/0006_add_index_to_tree_import_row.py +++ b/opentreemap/importer/migrations/0006_add_index_to_tree_import_row.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.4 on 2017-08-28 16:30 -from __future__ import unicode_literals + from django.db import migrations diff --git a/opentreemap/importer/models/base.py b/opentreemap/importer/models/base.py index eb67c7e2f..f8677d736 100644 --- a/opentreemap/importer/models/base.py +++ b/opentreemap/importer/models/base.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json from datetime import datetime, timedelta @@ -43,8 +41,8 @@ class Meta: field_order = models.TextField(default='') # Metadata about this particular import - owner = models.ForeignKey(User) - instance = models.ForeignKey(Instance) + owner = models.ForeignKey(User, on_delete=models.CASCADE) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) created = models.DateTimeField(auto_now=True) completed = models.DateTimeField(null=True, blank=True) @@ -321,7 +319,7 @@ def append_error(self, err, fields, data=None): # If you give append_error a single field # there is no need to get angry - if isinstance(fields, basestring): + if isinstance(fields, str): fields = (fields,) # make into tuple self.errors = json.dumps( @@ -384,7 +382,7 @@ def safe_pos_float(self, fld): return i def convert_units(self, data, converts): - for fld, factor in converts.iteritems(): + for fld, factor in converts.items(): if fld in data and factor != 1.0: data[fld] = float(data[fld]) * factor diff --git a/opentreemap/importer/models/species.py b/opentreemap/importer/models/species.py index 5578db69d..5e69b8fdb 100644 --- a/opentreemap/importer/models/species.py +++ b/opentreemap/importer/models/species.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import itertools @@ -50,7 +48,7 @@ def instance_region_codes(self): def row_set(self): return self.speciesimportrow_set - def __unicode__(self): + def __str__(self): return _('Species Import #%s') % self.pk def status_description(self): @@ -94,10 +92,10 @@ class SpeciesImportRow(GenericImportRow): )) # Species reference - species = models.ForeignKey(Species, null=True, blank=True) + species = models.ForeignKey(Species, on_delete=models.CASCADE, null=True, blank=True) merged = models.BooleanField(default=False) - import_event = models.ForeignKey(SpeciesImportEvent) + import_event = models.ForeignKey(SpeciesImportEvent, on_delete=models.CASCADE) class Meta: app_label = 'importer' @@ -122,7 +120,7 @@ def diff_from_species(self, species): data = self.cleaned diffs = {} - for (model_key, row_key) in SpeciesImportRow.SPECIES_MAP.iteritems(): + for (model_key, row_key) in SpeciesImportRow.SPECIES_MAP.items(): row_data = data.get(row_key, None) model_data = getattr(species, model_key) @@ -332,18 +330,18 @@ def _prepare_merge_data(self): species = Species.objects.filter(pk__in=possible_matches) diffs = [self.diff_from_species(s) for s in species] - if all(diff.keys() == ['id'] for diff in diffs): + if all(list(diff.keys()) == ['id'] for diff in diffs): # Imported data differs only in ID field (None vs. something) identical_to_existing = True self.merged = True else: # Filter out diffs whose "model value" is empty - filtered_diffs = [{k: v for k, v in diff.iteritems() + filtered_diffs = [{k: v for k, v in diff.items() if v[0] is not None and v[0] != ''} for diff in diffs] - diff_keys = [diff.keys() for diff in filtered_diffs] + diff_keys = [list(diff.keys()) for diff in filtered_diffs] diff_keys = set(itertools.chain(*diff_keys)) diff_keys.remove('id') @@ -392,7 +390,7 @@ def commit_row(self): self.import_event.max_tree_height_conversion_factor }) - for modelkey, datakey in SpeciesImportRow.SPECIES_MAP.iteritems(): + for modelkey, datakey in SpeciesImportRow.SPECIES_MAP.items(): importdata = data.get(datakey, None) if importdata is not None: diff --git a/opentreemap/importer/models/trees.py b/opentreemap/importer/models/trees.py index f01b750f7..6bb22472e 100644 --- a/opentreemap/importer/models/trees.py +++ b/opentreemap/importer/models/trees.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json from django.core.exceptions import ValidationError, MultipleObjectsReturned from django.contrib.gis.db import models from django.contrib.gis.geos import Point, Polygon +from django.contrib.gis.db.models.functions import Distance from django.utils.translation import ugettext as _ from django.db import transaction @@ -41,7 +40,7 @@ class Meta: def row_set(self): return self.treeimportrow_set - def __unicode__(self): + def __str__(self): return _('Tree Import #%s') % self.pk def get_udf_column_name(self, udf_def): @@ -114,10 +113,10 @@ class TreeImportRow(GenericImportRow): } # plot that was created from this row - plot = models.ForeignKey(Plot, null=True, blank=True) + plot = models.ForeignKey(Plot, on_delete=models.CASCADE, null=True, blank=True) # The main import event - import_event = models.ForeignKey(TreeImportEvent) + import_event = models.ForeignKey(TreeImportEvent, on_delete=models.CASCADE) class Meta: app_label = 'importer' @@ -207,7 +206,7 @@ def _import_value_to_udf_value(self, udf_def, value): def _commit_plot_data(self, data, plot): plot_edited = False - for plot_attr, field_name in TreeImportRow.PLOT_MAP.iteritems(): + for plot_attr, field_name in TreeImportRow.PLOT_MAP.items(): value = data.get(field_name, None) if value: plot_edited = True @@ -228,7 +227,7 @@ def _commit_plot_data(self, data, plot): plot.update_updated_fields(ie.owner) def _commit_tree_data(self, data, plot, tree, tree_edited): - for tree_attr, field_name in TreeImportRow.TREE_MAP.iteritems(): + for tree_attr, field_name in TreeImportRow.TREE_MAP.items(): value = data.get(field_name, None) if value: tree_edited = True @@ -349,7 +348,7 @@ def validate_proximity(self, point): .exclude(pk__in=already_committed)\ .filter(geom__intersects=nearby_bbox) - nearby = nearby.distance(point).order_by('distance')[:5] + nearby = nearby.annotate(distance=Distance('geom', point)).order_by('distance')[:5] if len(nearby) > 0: flds = (fields.trees.POINT_X, fields.trees.POINT_Y) @@ -434,7 +433,7 @@ def validate_user_defined_fields(self): message = str(ve) if isinstance(ve.message_dict, dict): message = '\n'.join( - [unicode(m) for m in ve.message_dict.values()]) + [str(m) for m in list(ve.message_dict.values())]) self.append_error( errors.INVALID_UDF_VALUE, column_name, message) diff --git a/opentreemap/importer/routes.py b/opentreemap/importer/routes.py index df814a8c7..a10c51136 100644 --- a/opentreemap/importer/routes.py +++ b/opentreemap/importer/routes.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django_tinsel.utils import decorate as do from django_tinsel.decorators import render_template, json_api_call diff --git a/opentreemap/importer/tasks.py b/opentreemap/importer/tasks.py index 5d7512615..1a5b11409 100644 --- a/opentreemap/importer/tasks.py +++ b/opentreemap/importer/tasks.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json @@ -24,7 +22,7 @@ def _create_rows_for_event(ie, csv_file): # so we can show progress. Caller does manual cleanup if necessary. reader = utf8_file_to_csv_dictreader(csv_file) - field_names = [f.strip().decode('utf-8') for f in reader.fieldnames + field_names = [f.strip() for f in reader.fieldnames if f.strip().lower() not in ie.ignored_fields()] ie.field_order = json.dumps(field_names) ie.save() @@ -54,7 +52,8 @@ def _create_rows(ie, reader): for row in reader: data = clean_row_data(row) - if len(filter(None, data.values())) > 0: # skip blank rows + # check for truthiness to skip blank rows + if len([_f for _f in list(data.values()) if _f]) > 0: data = json.dumps(data) rows.append(RowModel(data=data, import_event=ie, idx=idx)) @@ -94,7 +93,7 @@ def run_import_event_validation(import_type, import_event_id, file_obj): try: validation_tasks = [] - for i in xrange(0, ie.row_count, settings.IMPORT_BATCH_SIZE): + for i in range(0, ie.row_count, settings.IMPORT_BATCH_SIZE): validation_tasks.append(_validate_rows.s(import_type, ie.id, i)) final_task = _finalize_validation.si(import_type, import_event_id) @@ -164,7 +163,7 @@ def commit_import_event(import_type, import_event_id): commit_tasks = [ _commit_rows.s(import_type, import_event_id, i) - for i in xrange(0, ie.row_count, settings.IMPORT_BATCH_SIZE)] + for i in range(0, ie.row_count, settings.IMPORT_BATCH_SIZE)] finalize_task = _finalize_commit.si(import_type, import_event_id) diff --git a/opentreemap/importer/templates/importer/partials/imports.html b/opentreemap/importer/templates/importer/partials/imports.html index fd63b7876..b5da1b528 100644 --- a/opentreemap/importer/templates/importer/partials/imports.html +++ b/opentreemap/importer/templates/importer/partials/imports.html @@ -1,5 +1,5 @@ {% load i18n %} -{% load staticfiles %} +{% load static %} <div class="contained topper"> <div> diff --git a/opentreemap/importer/tests.py b/opentreemap/importer/tests.py index 070a7ad57..208aea991 100644 --- a/opentreemap/importer/tests.py +++ b/opentreemap/importer/tests.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- # flake8: noqa -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import tempfile import csv @@ -10,7 +8,7 @@ import psycopg2 from datetime import date -from StringIO import StringIO +from io import StringIO from unittest.case import skip, skipIf from django.conf import settings @@ -204,7 +202,6 @@ def test_species_diameter_and_height(self): self.assertHasError(i, errors.SPECIES_DBH_TOO_HIGH) # 15 > 12 self.assertNotHasError(i, errors.SPECIES_HEIGHT_TOO_HIGH) # 25 < 30 - def test_proximity(self): p1 = mkPlot(self.instance, self.user, geom=Point(25.0000001, 25.0000001, srid=4326)) @@ -464,7 +461,7 @@ def test_date_udf(self): i = self.mkrow(row) i.validate_row() self.assertHasError(i, errors.INVALID_UDF_VALUE, - data="[u'Test date must be formatted as YYYY-MM-DD']") + data="['Test date must be formatted as YYYY-MM-DD']") def test_choice_udf(self): UserDefinedFieldDefinition.objects.create( @@ -537,13 +534,13 @@ def test_multichoice_udf(self): i = self.mkrow(row) i.validate_row() self.assertHasError(i, errors.INVALID_UDF_VALUE, - data="[u'Test multichoice must be valid JSON']") + data="['Test multichoice must be valid JSON']") row['planting site: test multichoice'] = '"a","b"' i = self.mkrow(row) i.validate_row() self.assertHasError(i, errors.INVALID_UDF_VALUE, - data="[u'Test multichoice must be valid JSON']") + data="['Test multichoice must be valid JSON']") class SpeciesValidationTest(ValidationTest): @@ -1022,7 +1019,7 @@ def extract_errors(self, json): if 'errors' not in json: return errors - for k, v in json['errors'].iteritems(): + for k, v in json['errors'].items(): errors[k] = [] for e in v: d = e['data'] @@ -1251,7 +1248,6 @@ def test_faulty_data1(self): self.assertEqual(ierrors['1'], [(errors.INVALID_GEOM[0], gflds, None)]) - self.assertNotIn('2', ierrors) self.assertNotIn('3', ierrors) self.assertEqual(ierrors['4'], @@ -1322,7 +1318,6 @@ def test_no_files(self): self.assertTrue(isinstance(response, HttpResponseBadRequest)) - def test_unit_changes(self): csv = ("| point x | point y | tree height | canopy height | " "diameter | planting site width | planting site length |\n" @@ -1498,7 +1493,6 @@ def test_override_with_opentreemap_id(self): self.assertEqual(int(p1_geom.x*100), 4553) self.assertEqual(int(p1_geom.y*100), 3109) - def test_import_updates_updated_at_fields(self): original_creator = make_admin_user(self.instance, username='original_creator') p1 = mkPlot(self.instance, original_creator) @@ -1528,7 +1522,6 @@ def test_import_updates_updated_at_fields(self): self.assertGreater(p1.updated_at, p1_original_updated_at) self.assertGreater(t1.plot.updated_at, p2_original_updated_at) - def test_swap_locations_using_otm_id(self): center = self.instance.center self.assertEqual(3857, center.srid) diff --git a/opentreemap/importer/urls.py b/opentreemap/importer/urls.py index 5f6dca874..c08902b7d 100644 --- a/opentreemap/importer/urls.py +++ b/opentreemap/importer/urls.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import url diff --git a/opentreemap/importer/util.py b/opentreemap/importer/util.py index a76cae049..da14db32c 100644 --- a/opentreemap/importer/util.py +++ b/opentreemap/importer/util.py @@ -1,25 +1,23 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import codecs import csv - +from io import StringIO def _clean_string(s): s = s.strip() - if not isinstance(s, unicode): - s = unicode(s, 'utf-8') + if not isinstance(s, str): + s = str(s, 'utf-8') return s def clean_row_data(h): h2 = {} - for (k, v) in h.iteritems(): + for (k, v) in h.items(): k = clean_field_name(k) if k != 'ignore': - if isinstance(v, basestring): + if isinstance(v, str): v = _clean_string(v) h2[k] = v @@ -36,7 +34,7 @@ def _as_utf8(f): def _guess_dialect_and_reset_read_pointer(f): - dialect = csv.Sniffer().sniff(_as_utf8(f).read(4096), delimiters=',\t') + dialect = csv.Sniffer().sniff(_as_utf8(f).read(4096).decode(), delimiters=',\t') f.seek(0) return dialect @@ -47,5 +45,11 @@ def utf8_file_to_csv_dictreader(f): # CSV standard "" to escape a double quote. Excel and LibreOffice # use this escape by default when saving as CSV. dialect.doublequote = True - return csv.DictReader(_as_utf8(f), + + # at this point, the binary filestream must be repackaged to the csv spec, + # that is an iterator of unicode strings. time limited a more elegant solution + # than allocating memory for up to two copies of the data but we anticipate having + # sufficient RAM + csv_body = StringIO(f.read().decode()) + return csv.DictReader(csv_body, dialect=dialect) diff --git a/opentreemap/importer/views.py b/opentreemap/importer/views.py index b366d1cd7..838906b8c 100644 --- a/opentreemap/importer/views.py +++ b/opentreemap/importer/views.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json import io @@ -14,7 +12,7 @@ from django.db.models import Q from django.shortcuts import get_object_or_404, render from django.core.paginator import Paginator, Page, EmptyPage -from django.core.urlresolvers import reverse +from django.urls import reverse from django.http import HttpResponse, HttpResponseBadRequest from django.utils.translation import ugettext as _ @@ -80,7 +78,7 @@ def start_import(request, instance): } kwargs = {k: 1 / storage_to_instance_units_factor(instance, v[0], v[1]) - for (k, v) in factors.items()} + for (k, v) in list(factors.items())} process_csv(request, instance, import_type, **kwargs) @@ -260,7 +258,7 @@ def update_row(request, instance, import_type, row_id): fields.trees.COMMON_NAME: species.common_name }) - for k, v in request.POST.iteritems(): + for k, v in request.POST.items(): if k in basedata: basedata[k] = v @@ -433,7 +431,7 @@ def _get_page(self, *args, **kwargs): def _add_species_resolver_to_fields(collected_fields, row): - species_error_fields = ((f, v) for f, v in collected_fields.items() + species_error_fields = ((f, v) for f, v in list(collected_fields.items()) if f in fields.trees.SPECIES_FIELDS and v.get('css_class')) @@ -706,10 +704,10 @@ def commit(request, instance, import_type, import_event_id): def process_csv(request, instance, import_type, **kwargs): files = request.FILES - filename = files.keys()[0] + filename = list(files.keys())[0] file_obj = files[filename] - file_obj = io.BytesIO(decode(file_obj.read()).encode('utf-8')) + file_obj = io.BytesIO((file_obj.read().encode('utf-8'))) owner = request.user ImportEventModel = get_import_event_model(import_type) @@ -754,7 +752,7 @@ def cancel(request, instance, import_type, import_event_id): @queryset_as_exported_csv def export_all_species(request, instance): - field_names = SpeciesImportRow.SPECIES_MAP.keys() + field_names = list(SpeciesImportRow.SPECIES_MAP.keys()) field_names.remove('id') return Species.objects.filter(instance_id=instance.id).values(*field_names) diff --git a/opentreemap/manage.py b/opentreemap/manage.py index f90f70aa9..6871f369b 100755 --- a/opentreemap/manage.py +++ b/opentreemap/manage.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3.7 import os import sys diff --git a/opentreemap/manage_treemap/js/src/basicInfo.js b/opentreemap/manage_treemap/js/src/basicInfo.js index 4a29bb6e7..5b13a4e3f 100644 --- a/opentreemap/manage_treemap/js/src/basicInfo.js +++ b/opentreemap/manage_treemap/js/src/basicInfo.js @@ -8,6 +8,6 @@ var adminPage = require('manage_treemap/lib/adminPage.js'), adminPage.init(); inlineEditForm.init({ - updateUrl: reverse.site_config(config.instance.url_name), + updateUrl: reverse.Urls.site_config(config.instance.url_name), section: '#basic-info', }); diff --git a/opentreemap/manage_treemap/js/src/benefits.js b/opentreemap/manage_treemap/js/src/benefits.js index ca2e23771..0ab8607d4 100644 --- a/opentreemap/manage_treemap/js/src/benefits.js +++ b/opentreemap/manage_treemap/js/src/benefits.js @@ -11,7 +11,7 @@ var $ = require('jquery'), adminPage.init(); var form = inlineEditForm.init({ - updateUrl: reverse.benefits(config.instance.url_name), + updateUrl: reverse.Urls.benefits(config.instance.url_name), section: '#benefits', errorCallback: alerts.errorCallback }), diff --git a/opentreemap/manage_treemap/js/src/branding.js b/opentreemap/manage_treemap/js/src/branding.js index db1f8a581..e6d78d16f 100644 --- a/opentreemap/manage_treemap/js/src/branding.js +++ b/opentreemap/manage_treemap/js/src/branding.js @@ -30,9 +30,9 @@ var dom = { adminPage.init(); -var cssUrl = reverse.scss() + '?' + config.instance.scssQuery, +var cssUrl = reverse.Urls.scss() + '?' + config.instance.scssQuery, form = inlineEditForm.init({ - updateUrl: reverse.branding(config.instance.url_name), + updateUrl: reverse.Urls.branding(config.instance.url_name), section: '#branding', errorCallback: alerts.errorCallback }); diff --git a/opentreemap/manage_treemap/js/src/greenInfrastructure.js b/opentreemap/manage_treemap/js/src/greenInfrastructure.js index b0d0a7167..78bb6d888 100644 --- a/opentreemap/manage_treemap/js/src/greenInfrastructure.js +++ b/opentreemap/manage_treemap/js/src/greenInfrastructure.js @@ -11,7 +11,7 @@ var inlineEditForm = require('treemap/lib/inlineEditForm.js'), adminPage.init(); var form = inlineEditForm.init({ - updateUrl: reverse.green_infrastructure(config.instance.url_name), + updateUrl: reverse.Urls.green_infrastructure(config.instance.url_name), section: '#gsi', errorCallback: alerts.errorCallback }); diff --git a/opentreemap/manage_treemap/js/src/groups.js b/opentreemap/manage_treemap/js/src/groups.js new file mode 100644 index 000000000..1c66657b5 --- /dev/null +++ b/opentreemap/manage_treemap/js/src/groups.js @@ -0,0 +1,170 @@ +"use strict"; + +var $ = require('jquery'), + _ = require('lodash'), + Bacon = require('baconjs'), + toastr = require('toastr'), + BU = require('treemap/lib/baconUtils.js'), + U = require('treemap/lib/utility.js'), + buttonEnabler = require('treemap/lib/buttonEnabler.js'), + errors = require('manage_treemap/lib/errors.js'), + simpleEditForm = require('treemap/lib/simpleEditForm.js'), + adminPage = require('manage_treemap/lib/adminPage.js'), + config = require('treemap/lib/config.js'), + Chart = require('Chart'), + reverse = require('reverse'); + +var dom = { + selects: 'select[data-name]', + radios: ':radio:checked[data-name]', + roleIds: '[data-roles]', + createNewRole: '#create_new_role', + newRoleName: '#new_role_name', + roles: '#role-info', + edit: '.editBtn', + save: '.saveBtn', + cancel: '.cancelBtn', + addRole: '.addRoleBtn', + addRoleModal: '#add-role-modal', + spinner: '.spinner', + rolesTableContainer: '#role-info .role-table-scroll', + newFieldsAlert: '#new-fields-alert', + newFieldsDismiss: '#new-fields-dismiss', + chart: '#group-chart canvas', +}; + +var url = reverse.Urls.roles_endpoint(config.instance.url_name), + updateStream = $(dom.save) + .asEventStream('click') + .doAction(function(e) { + e.preventDefault(); + $(dom.spinner).show(); + $(dom.save).prop("disabled", true); + }) + .map(getRolePermissions) + .flatMap(BU.jsonRequest('PUT', url)), + resultsStream = BU.jsonRequest( + 'GET', + reverse.Urls.get_groups_data(config.instance.url_name, 'neighborhood') + )(); + +simpleEditForm.init({ + edit: dom.edit, + cancel: dom.cancel, + save: dom.save, + saveStream: updateStream +}); + +function enableSave() { + $(dom.spinner).hide(); + $(dom.save).prop("disabled", false); +} + +function showError(resp) { + enableSave(); + toastr.error(resp.responseText); +} + +updateStream.onError(showError); + +updateStream.onValue(enableSave); + +resultsStream.onError(showError); +resultsStream.onValue(function (results) { + var chart = new Chart($(dom.chart), { + type: 'bar', + data: { + labels: results['data'].map(x => x['neighborhood']), + datasets: [{ + label: 'Trees', + data: results['data'].map(x => x['total']) + }] + }, + }); +}); + +buttonEnabler.run(); +U.modalsFocusOnFirstInputWhenShown(); +$(dom.addRole).on('click', function () { + $(dom.addRoleModal).modal('show'); +}); + + +var newRoleStream = $(dom.createNewRole) + .asEventStream('click') + .doAction(function () { $(dom.spinner).show(); }) + .map($(dom.newRoleName)) + .map('.val') + .flatMap(getNewRoleHtml(url)); + +newRoleStream.onError(showError); + +newRoleStream + .onValue(addNewRole); + +var alertDismissStream = $(dom.newFieldsDismiss).asEventStream('click') + .doAction('.preventDefault') + .map(undefined) + .flatMap(BU.jsonRequest('POST', $(dom.newFieldsDismiss).attr('href'))); + +alertDismissStream.onValue(function() { + $(dom.newFieldsAlert).hide(); + $(dom.roles).find('tr.active').removeClass('active'); +}); + +adminPage.init(Bacon.mergeAll(updateStream, alertDismissStream)); + +function getRolePermissions() { + var roleIds = $(dom.roleIds).data('roles').split(','); + var roles = _.zipObject(roleIds, _.times(roleIds.length, function () { + return { + 'fields': {}, 'models': {} + }; + })); + $(dom.selects).each(function(i, select) { + var $select = $(select); + var roleId = $select.attr('data-role-id'); + var field = $select.attr('data-name'); + var perm = $select.val(); + + roles[roleId].fields[field] = perm; + }); + $(dom.radios).each(function(i, radio) { + var $radio = $(radio), + roleId = $radio.data('role-id'), + permissionName = $radio.data('name'), + hasPermission = $radio.data('value'); + roles[roleId].models[permissionName] = hasPermission; + }); + + return roles; +} + +function createRoleOptionElement(role) { + return $('<option>') + .attr('value', role) + .html(role); +} + +function addNewRole(html) { + $(dom.spinner).hide(); + + $(dom.newRoleName).val(''); + + $(dom.roles).html(html); + // Scroll over to the new role for easier editing of it + $(dom.rolesTableContainer).animate({scrollLeft: $(dom.rolesTableContainer)[0].scrollWidth}); +} + +function getNewRoleHtml(url) { + return function(role) { + var req = $.ajax({ + url: url, + method: 'POST', + contentType: 'application/json', + data: JSON.stringify({'name': role}) + }); + + return Bacon.fromPromise(req); + }; +} diff --git a/opentreemap/manage_treemap/js/src/link.js b/opentreemap/manage_treemap/js/src/link.js index 031a00595..c6fd1a405 100644 --- a/opentreemap/manage_treemap/js/src/link.js +++ b/opentreemap/manage_treemap/js/src/link.js @@ -7,6 +7,6 @@ var adminPage = require('manage_treemap/lib/adminPage.js'), adminPage.init(); inlineEditForm.init({ - updateUrl: reverse.external_link(config.instance.url_name), + updateUrl: reverse.Urls.external_link(config.instance.url_name), section: '#external-links', }); diff --git a/opentreemap/manage_treemap/js/src/mobileConfigs.js b/opentreemap/manage_treemap/js/src/mobileConfigs.js index 47b29a113..c777dca3a 100644 --- a/opentreemap/manage_treemap/js/src/mobileConfigs.js +++ b/opentreemap/manage_treemap/js/src/mobileConfigs.js @@ -28,7 +28,7 @@ var attrs = { adminPage.init(); reorderFields.handle({ - url: reverse.set_field_configs(config.instance.url_name), + url: reverse.Urls.set_field_configs(config.instance.url_name), container: dom.container, editButton: dom.edit, saveButton: dom.save, diff --git a/opentreemap/manage_treemap/js/src/reports.js b/opentreemap/manage_treemap/js/src/reports.js new file mode 100644 index 000000000..da7793b70 --- /dev/null +++ b/opentreemap/manage_treemap/js/src/reports.js @@ -0,0 +1,496 @@ +"use strict"; + +var $ = require('jquery'), + _ = require('lodash'), + Bacon = require('baconjs'), + toastr = require('toastr'), + BU = require('treemap/lib/baconUtils.js'), + U = require('treemap/lib/utility.js'), + buttonEnabler = require('treemap/lib/buttonEnabler.js'), + errors = require('manage_treemap/lib/errors.js'), + simpleEditForm = require('treemap/lib/simpleEditForm.js'), + adminPage = require('manage_treemap/lib/adminPage.js'), + config = require('treemap/lib/config.js'), + Chart = require('Chart'), + reverse = require('reverse'); + +var dom = { + spinner: '.spinner', + newFieldsAlert: '#new-fields-alert', + newFieldsDismiss: '#new-fields-dismiss', + aggregationLevelDropdown: '#select-aggregation', + + neighborhoodDropdownContainer: '#select-neighborhoods-container', + wardDropdownContainer: '#select-wards-container', + parkDropdownContainer: '#select-parks-container', + sidDropdownContainer: '#select-sids-container', + + neighborhoodDropdown: '#select-neighborhoods', + wardDropdown: '#select-wards', + parkDropdown: '#select-parks', + sidDropdown: '#select-sids', + + chart: '#group-chart canvas', + treeCountsChart: '#tree-counts-chart canvas', + speciesChart: '#species-chart canvas', + treeConditionsChart: '#tree-conditions-chart canvas', + treeDiametersChart: '#tree-diameters-chart canvas', + + ecobenefitsByWardTableHeader: '#ecobenefits-by-ward-table thead', + ecobenefitsByWardTableBody: '#ecobenefits-by-ward-table tbody', + ecobenefitsByWardTotal: '#ecobenefits-by-ward-total' +}; + +var charts = { + treeCountsChart: null, + speciesChart: null, + treeConditionsChart: null, + treeDiametersChart: null, + + ecobenefitsByWardTableHeader: null, + ecobenefitsByWardTableBody: null, + ecobenefitsByWardTotal: null +}; + +// a cache to hold our data +var dataCache = { + treeCountsChart: null, + speciesChart: null, + treeConditionsChart: null, + treeDiametersChart: null, + ecobenefits: null, +}; + +var onValueFunctions = { + treeCountsChart: null, + speciesChart: null, + treeConditionsChart: null, + treeDiametersChart: null, + ecobenefits: null, +} + +var url = reverse.Urls.roles_endpoint(config.instance.url_name); + +function loadData() { + + var aggregationLevel = $(dom.aggregationLevelDropdown).val(); + var treeCountStream = BU.jsonRequest( + 'GET', + reverse.Urls.get_reports_data(config.instance.url_name, 'count', aggregationLevel) + )(); + treeCountStream.onError(showError); + treeCountStream.onValue(onValueFunctions.treeCountsChart); + + var speciesStream = BU.jsonRequest( + 'GET', + reverse.Urls.get_reports_data(config.instance.url_name, 'species', aggregationLevel) + )(); + speciesStream.onError(showError); + speciesStream.onValue(onValueFunctions.speciesChart); + + var treeConditionsStream = BU.jsonRequest( + 'GET', + reverse.Urls.get_reports_data(config.instance.url_name, 'condition', aggregationLevel) + )(); + treeConditionsStream.onError(showError); + treeConditionsStream.onValue(onValueFunctions.treeConditionsChart); + + var treeDiametersStream = BU.jsonRequest( + 'GET', + reverse.Urls.get_reports_data(config.instance.url_name, 'diameter', aggregationLevel) + )(); + treeDiametersStream.onError(showError); + treeDiametersStream.onValue(onValueFunctions.treeDiametersChart); + + $(dom.ecobenefitsByWardTotal).html(''); + $(dom.spinner).show(); + var ecobenefitsStream = BU.jsonRequest( + 'GET', + reverse.Urls.get_reports_data(config.instance.url_name, 'ecobenefits', aggregationLevel) + )(); + ecobenefitsStream.onError(showError); + ecobenefitsStream.onValue(onValueFunctions.ecobenefits); +} + + +function showError(resp) { + enableSave(); + toastr.error(resp.responseText); +} + +var chartColors = { + orange: 'rgb(255, 159, 64)', + yellow: 'rgb(255, 205, 86)', + green: 'rgb(75, 192, 192)', + blue: 'rgb(54, 162, 235)', + purple: 'rgb(153, 102, 255)', + grey: 'rgb(201, 203, 207)', + + // a less saturated red + red: '#8b1002', + + // a softer black + black: '#303031' +}; + +// theme from https://learnui.design/tools/data-color-picker.html +// starting with #8baa3d, which is the otm-green color in +// _base.scss +var otmGreen = '#8baa3d'; +var otmLimeGreen = '#add142'; +var chartColorTheme = [ + '#003f5c', + '#00506b', + '#006274', + '#007374', + '#00836c', + '#1c935f', + '#59a04e', + '#8baa3d' +]; + + +onValueFunctions.treeCountsChart = function (results) { + var data = results['data'] + dataCache.treeCountsChart = data; + + if (charts.treeCountsChart == null) { + var chart = new Chart($(dom.treeCountsChart), { + type: 'bar', + data: { + labels: [], + datasets: [] + } + }); + + charts.treeCountsChart = chart; + } + + updateTreeCountsData(data); +}; + +function updateTreeCountsData(data) { + var chart = charts.treeCountsChart; + if (chart == null) { + return; + } + + chart.data.labels = data.map(x => x['name']); + chart.data.datasets = [{ + label: 'Trees', + borderColor: otmLimeGreen, + backgroundColor: otmGreen, + data: data.map(x => x['count']) + }]; + chart.update(); +} + +onValueFunctions.speciesChart = function (results) { + var data = results['data']; + dataCache.speciesChart = data; + + updateSpeciesData(data); +} + +function updateSpeciesData(data) { + var chart = charts.speciesChart; + if (chart != null) { + chart.destroy(); + } + + // reduce the species and counts, as there are multiple given the aggregation + var reduceFunc = function(acc, value) { + acc[value['species_name']] = acc[value['species_name']] + value['count'] + || value['count']; + return acc; + } + var dataObj = data.reduce(reduceFunc, {}); + // make into a list of items and sort descending + data = Object.keys(dataObj).map(k => {return {name: k, count: dataObj[k]}}) + .sort((first, second) => second['count'] - first['count']); + + // take the first N and aggregate the rest + var finalData = data.slice(0, 5); + var otherSum = data.slice(5).reduce((acc, val) => acc + val['count'], 0); + finalData.push({name: 'Other', count: otherSum}) + + var chart = new Chart($(dom.speciesChart), { + type: 'pie', + data: { + labels: finalData.map(x => x['name']), + datasets: [{ + data: finalData.map(x => x['count']), + backgroundColor: finalData.map((x, i) => chartColorTheme[i]), + borderColor: 'rgba(200, 200, 200, 0.75)', + hoverBorderColor: 'rgba(200, 200, 200, 1)', + }] + } + }); + charts.speciesChart = chart; + chart.update(); +} + +onValueFunctions.treeConditionsChart = function (results) { + var data = results['data']; + dataCache.treeConditionsChart = data; + + if (charts.treeConditionsChart == null) { + var chart = new Chart($(dom.treeConditionsChart), { + type: 'bar', + options: { + scales: { + xAxes: [{ + stacked: true, + }], + yAxes: [{ + stacked: true + }] + } + }, + data: { + labels: [], + datasets: [] + } + }); + charts.treeConditionsChart = chart; + } + + updateTreeConditionsChart(data); +} + +function updateTreeConditionsChart(data) { + var chart = charts.treeConditionsChart; + if (chart == null) { + return; + } + + chart.data.labels = data.map(x => x['name']); + chart.data.datasets = [ + { + label: 'Healthy', + data: data.map(x => x['healthy']), + backgroundColor: otmGreen + }, + { + label: 'Unhealthy', + data: data.map(x => x['unhealthy']), + backgroundColor: chartColors.red + }, + { + label: 'Dead', + data: data.map(x => x['dead']), + backgroundColor: chartColors.black + } + ]; + chart.update(); +} + +onValueFunctions.treeDiametersChart = function (results) { + var data = results['data']; + dataCache.treeDiametersChart = data; + updateTreeDiametersChart(data); +} + +function updateTreeDiametersChart(data) { + var chart = charts.treeDiametersChart; + if (chart != null) { + chart.destroy(); + } + // + // reduce the species and counts, as there are multiple given the aggregation + var reduceFunc = function(acc, value) { + var diameter = value['diameter']; + if (diameter <= 5) { + acc['< 5 in.'] = acc['< 5 in.'] + 1 || 1; + } else if (diameter > 5 && diameter < 25){ + acc['> 5 in. and < 25 in.'] = acc['> 5 in. and < 25 in.'] + 1 || 1; + } else { + acc['> 25 in.'] = acc['> 25 in.'] + 1 || 1; + } + return acc; + } + var dataObj = data.reduce(reduceFunc, {}); + // make into a list of items and sort descending + data = Object.keys(dataObj).map(k => {return {name: k, count: dataObj[k]}}); + + var colors = chartColorTheme.reverse(); + var chart = new Chart($(dom.treeDiametersChart), { + type: 'pie', + data: { + labels: data.map(x => x['name']), + datasets: [{ + data: data.map(x => x['count']), + backgroundColor: data.map((x, i) => colors[i * 2]), + borderColor: 'rgba(200, 200, 200, 0.75)', + hoverBorderColor: 'rgba(200, 200, 200, 1)', + }] + }, + }); + charts.treeDiametersChart = chart; +} + +onValueFunctions.ecobenefits = function (results) { + var data = results['data']; + dataCache.ecobenefits = data; + $(dom.spinner).hide(); + updateEcobenefits(data); +} + +function updateEcobenefits(data) { + var columns = data['columns']; + var columnHtml = '<tr>' + columns.map(x => '<th>' + x + '</th>').join('') + '</tr>'; + var dataHtml = data['data'].map(row => '<tr>' + row.map((x, i) => { + if (row[0] == 'Total') { + return '<td><b>' + formatColumn(x, columns[i]) + '</b></td>'; + } + return '<td>' + formatColumn(x, columns[i]) + '</td>'; + }).join('') + '</tr>').join(''); + + $(dom.ecobenefitsByWardTableHeader).html(columnHtml); + $(dom.ecobenefitsByWardTableBody).html(dataHtml); + + // compute the totals + var total = data['data'].flatMap(row => row.map((x, i) => { + if (row[0] == 'Total') { + return 0; + } + if (columns[i].indexOf('$') != -1) { + return x; + } else { + return 0; + } + })).reduce((a, b) => a + b, 0); + + $(dom.ecobenefitsByWardTotal) + .html('<b>Total Annual Benefits: $' + total.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }) + '</b>'); +} + +function formatColumn(column, columnName) { + if (column == null) + return ''; + if (typeof column == 'number' && columnName.indexOf('$') != -1) + return '$' + column.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + if (typeof column == 'number' && column < 0.00001) + return ''; + if (typeof column == 'number') + return column.toLocaleString(undefined, { + maximumFractionDigits: 4 + }); + return column; +} + +$(dom.neighborhoodDropdown).change(function(event) { + var name = $(this).val(); + filterDataByAggregation(name); +}); + +$(dom.wardDropdown).change(function(event) { + var name = $(this).val(); + filterDataByAggregation(name); +}); + +$(dom.parkDropdown).change(function(event) { + var name = $(this).val(); + filterDataByAggregation(name); +}); + +$(dom.sidDropdown).change(function(event) { + var name = $(this).val(); + filterDataByAggregation(name); +}); + +function filterDataByAggregation(name) { + var data = dataCache.treeCountsChart; + + updateTreeCountsData( + data.filter(x => name == 'all' || name.includes(x['name'])) + ); + + data = dataCache.speciesChart; + updateSpeciesData( + data.filter(x => name == 'all' || name.includes(x['name'])) + ); + + data = dataCache.treeConditionsChart; + updateTreeConditionsChart( + data.filter(x => name == 'all' || name.includes(x['name'])) + ); + + data = dataCache.treeDiametersChart; + updateTreeDiametersChart( + data.filter(x => name == 'all' || name.includes(x['name'])) + ); + + data = dataCache.ecobenefits; + updateEcobenefits({ + columns: data['columns'], + data: data['data'].filter(x => name == 'all' || name.includes(x[0])) + }); +} + +$(dom.aggregationLevelDropdown).change(function(event) { + var aggregationLevel = $(dom.aggregationLevelDropdown).val(); + + $(dom.wardDropdownContainer + " option:selected").removeAttr("selected"); + $(dom.neighborhoodDropdownContainer + " option:selected").removeAttr("selected"); + $(dom.parkDropdownContainer + " option:selected").removeAttr("selected"); + $(dom.sidDropdownContainer + " option:selected").removeAttr("selected"); + + // could probably do toggle, but i'm paranoid something will break + if (aggregationLevel == "ward") { + $(dom.wardDropdownContainer).show(); + $(dom.wardDropdownContainer + " option[value=all]").attr("selected", true); + $(dom.neighborhoodDropdownContainer).hide(); + $(dom.parkDropdownContainer).hide(); + $(dom.sidDropdownContainer).hide(); + } else if (aggregationLevel == "neighborhood"){ + $(dom.wardDropdownContainer).hide(); + $(dom.neighborhoodDropdownContainer).show(); + $(dom.neighborhoodDropdownContainer + " option[value=all]").attr("selected", true); + $(dom.parkDropdownContainer).hide(); + $(dom.sidDropdownContainer).hide(); + } else if (aggregationLevel == "park"){ + $(dom.wardDropdownContainer).hide(); + $(dom.neighborhoodDropdownContainer).hide(); + $(dom.parkDropdownContainer).show(); + $(dom.parkDropdownContainer + " option[value=all]").attr("selected", true); + $(dom.sidDropdownContainer).hide(); + } else if (aggregationLevel == "sid"){ + $(dom.wardDropdownContainer).hide(); + $(dom.neighborhoodDropdownContainer).hide(); + $(dom.parkDropdownContainer).hide(); + $(dom.sidDropdownContainer).show(); + $(dom.sidDropdownContainer + " option[value=all]").attr("selected", true); + } + + loadData(); +}); + + +buttonEnabler.run(); +U.modalsFocusOnFirstInputWhenShown(); + +var alertDismissStream = $(dom.newFieldsDismiss).asEventStream('click') + .doAction('.preventDefault') + .map(undefined) + .flatMap(BU.jsonRequest('POST', $(dom.newFieldsDismiss).attr('href'))); + +alertDismissStream.onValue(function() { + $(dom.newFieldsAlert).hide(); +}); + +adminPage.init(Bacon.mergeAll(alertDismissStream)); + +// initially, show by Ward +$(dom.wardDropdownContainer).show(); +$(dom.neighborhoodDropdownContainer).hide(); +$(dom.parkDropdownContainer).hide(); +$(dom.sidDropdownContainer).hide(); +$(dom.wardDropdownContainer + " option[value=all]").attr("selected", true); +loadData(); diff --git a/opentreemap/manage_treemap/js/src/roles.js b/opentreemap/manage_treemap/js/src/roles.js index 6e1f84b3e..cc2cbe738 100644 --- a/opentreemap/manage_treemap/js/src/roles.js +++ b/opentreemap/manage_treemap/js/src/roles.js @@ -31,7 +31,7 @@ var dom = { newFieldsDismiss: '#new-fields-dismiss' }; -var url = reverse.roles_endpoint(config.instance.url_name), +var url = reverse.Urls.roles_endpoint(config.instance.url_name), updateStream = $(dom.save) .asEventStream('click') .doAction(function(e) { diff --git a/opentreemap/manage_treemap/js/src/searchConfig.js b/opentreemap/manage_treemap/js/src/searchConfig.js index 1fdc81aa8..13b8f9df1 100644 --- a/opentreemap/manage_treemap/js/src/searchConfig.js +++ b/opentreemap/manage_treemap/js/src/searchConfig.js @@ -24,7 +24,7 @@ var attrs = { adminPage.init(); reorderFields.handle({ - url: reverse.search_config(config.instance.url_name), + url: reverse.Urls.search_config(config.instance.url_name), container: dom.container, editButton: dom.edit, saveButton: dom.save, diff --git a/opentreemap/manage_treemap/js/src/udfs.js b/opentreemap/manage_treemap/js/src/udfs.js index ffeb073f0..b90ebb3b0 100644 --- a/opentreemap/manage_treemap/js/src/udfs.js +++ b/opentreemap/manage_treemap/js/src/udfs.js @@ -60,7 +60,7 @@ var $udfType = $('#udf-type'), saveConfirmModelTemplate = _.template($('#confirmer-model-template').html()), saveConfirmChangeTemplate = _.template($('#confirmer-change-template').html()), - url = reverse.udfs(config.instance.url_name); + url = reverse.Urls.udfs(config.instance.url_name); var saveModal = (function() { function hide() { @@ -160,7 +160,7 @@ newUdfStream }); var getUdfUrlForId = function(id) { - return reverse.udfs_change({ + return reverse.Urls.udfs_change({ instance_url_name: config.instance.url_name, udf_id: id }); diff --git a/opentreemap/manage_treemap/js/src/units.js b/opentreemap/manage_treemap/js/src/units.js index 9ee18c47b..476a003f9 100644 --- a/opentreemap/manage_treemap/js/src/units.js +++ b/opentreemap/manage_treemap/js/src/units.js @@ -8,7 +8,7 @@ var inlineEditForm = require('treemap/lib/inlineEditForm.js'), adminPage.init(); inlineEditForm.init({ - updateUrl: reverse.units_endpoint(config.instance.url_name) + '?update_universal_rev=1', + updateUrl: reverse.Urls.units_endpoint(config.instance.url_name) + '?update_universal_rev=1', section: '#units', errorCallback: alerts.errorCallback }); diff --git a/opentreemap/manage_treemap/js/src/users.js b/opentreemap/manage_treemap/js/src/users.js index 82f82237a..f08a08be9 100644 --- a/opentreemap/manage_treemap/js/src/users.js +++ b/opentreemap/manage_treemap/js/src/users.js @@ -39,7 +39,7 @@ var dom = { adminPage.init(); -var url = reverse.user_roles(config.instance.url_name), +var url = reverse.Urls.user_roles(config.instance.url_name), $container = $(dom.container), $addUser = $(dom.addUser), $addUserModal = $(dom.addUserModal), @@ -209,7 +209,7 @@ $(dom.removeInvite).on('click', function() { $row = $removeInviteModal.data('row'); $.ajax({ - 'url': reverse.user_invite(config.instance.url_name, id), + 'url': reverse.Urls.user_invite(config.instance.url_name, id), 'method': 'DELETE', }) .done(function() { diff --git a/opentreemap/manage_treemap/lib/email.py b/opentreemap/manage_treemap/lib/email.py index 3241fd84b..98cbe2d6c 100644 --- a/opentreemap/manage_treemap/lib/email.py +++ b/opentreemap/manage_treemap/lib/email.py @@ -1,6 +1,6 @@ -from django.core.mail import get_connection, EmailMultiAlternatives from django.template.loader import render_to_string from django.conf import settings +from django.core.mail import send_mail def _get_email_subj_and_body(tmpl, ctxt): @@ -9,20 +9,23 @@ def _get_email_subj_and_body(tmpl, ctxt): subject = ''.join(subject.splitlines()) message = render_to_string( + 'manage_treemap/emails/%s.plaintext.txt' % tmpl, ctxt) + + html_message = render_to_string( 'manage_treemap/emails/%s.body.txt' % tmpl, ctxt) - return subject, message + return subject, message, html_message def send_email(tmpl, ctxt, recipient_list): from_email = settings.DEFAULT_FROM_EMAIL - subject, message = _get_email_subj_and_body(tmpl, ctxt) - - connection = get_connection(fail_silently=True) - mail = EmailMultiAlternatives(subject, message, - from_email, recipient_list, - connection=connection) - - mail.attach_alternative(message, 'text/html') - - return mail.send() + subject, message, html_message = _get_email_subj_and_body(tmpl, ctxt) + + return send_mail( + subject, + message, + '"Sustainable JC Green Team" <info@sustainablejc.org>', + recipient_list, + fail_silently=False, + html_message=html_message + ) diff --git a/opentreemap/manage_treemap/migrations/0001_initial.py b/opentreemap/manage_treemap/migrations/0001_initial.py index 2c2608e7d..3163c7494 100644 --- a/opentreemap/manage_treemap/migrations/0001_initial.py +++ b/opentreemap/manage_treemap/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models from django.conf import settings @@ -24,9 +24,9 @@ class Migration(migrations.Migration): ('updated', models.DateField(auto_now=True)), ('accepted', models.BooleanField(default=False)), ('activation_key', models.CharField(unique=True, max_length=40)), - ('created_by', models.ForeignKey(to=settings.AUTH_USER_MODEL)), - ('instance', models.ForeignKey(to='treemap.Instance')), - ('role', models.ForeignKey(to='treemap.Role')), + ('created_by', models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL)), + ('instance', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance')), + ('role', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Role')), ], ), migrations.AlterUniqueTogether( diff --git a/opentreemap/manage_treemap/models.py b/opentreemap/manage_treemap/models.py index 929adbf6d..bceea092b 100644 --- a/opentreemap/manage_treemap/models.py +++ b/opentreemap/manage_treemap/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import random import string @@ -28,12 +26,12 @@ class InstanceInvitation(models.Model): email = models.CharField(max_length=255, validators=[validate_email]) - instance = models.ForeignKey(Instance) - role = models.ForeignKey(Role) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) + role = models.ForeignKey(Role, on_delete=models.CASCADE) admin = models.BooleanField(default=False) created = models.DateField(auto_now_add=True) - created_by = models.ForeignKey(User) + created_by = models.ForeignKey(User, on_delete=models.CASCADE) updated = models.DateField(auto_now=True) accepted = models.BooleanField(default=False) diff --git a/opentreemap/manage_treemap/routes.py b/opentreemap/manage_treemap/routes.py index 0b22bd5e7..de0fd838d 100644 --- a/opentreemap/manage_treemap/routes.py +++ b/opentreemap/manage_treemap/routes.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals + from functools import partial @@ -14,19 +12,22 @@ import manage_treemap.views.photo as photo_views import manage_treemap.views.green_infrastructure as green_infrastructure_views -from exporter.views import begin_export_users +from exporter.views import begin_export_groups, begin_export_users from importer.views import list_imports from manage_treemap.views import update_instance_fields_with_validator from manage_treemap.views.roles import roles_list, roles_update, roles_create +from manage_treemap.views.groups import groups_list, get_groups_data +from manage_treemap.views.reports import get_reports_data as _get_reports_data, reports, check_report_query_parameters, include_user from manage_treemap.views.udf import (udf_bulk_update, udf_create, udf_list, udf_delete_popup, udf_delete, udf_update_choice, remove_udf_notifications) from manage_treemap.views.user_roles import ( - user_roles_list, update_user_roles, create_user_role, + user_roles_list, user_roles_list_api, user_roles_list_active, user_roles_list_invited, + update_user_roles, create_user_role, remove_invited_user_from_instance) from treemap.decorators import (require_http_method, admin_instance_request, - return_400_if_validation_errors) + return_400_if_validation_errors, instance_request) admin_route = lambda **kwargs: admin_instance_request(route(**kwargs)) @@ -109,6 +110,24 @@ create_user_role) ) +user_roles_invited = do( + json_api_call, + instance_request, + user_roles_list_invited +) + +user_roles_active = do( + json_api_call, + instance_request, + user_roles_list_active +) + +user_roles_api = do( + json_api_call, + instance_request, + user_roles_list_api +) + user_roles_partial = admin_route( GET=do(render_template('manage_treemap/partials/user_roles.html'), user_roles_list) @@ -121,6 +140,11 @@ admin_instance_request, begin_export_users) +begin_export_groups = do( + json_api_call, + admin_instance_request, + begin_export_groups) + roles = admin_route( GET=do(render_template('manage_treemap/roles.html'), roles_list), PUT=do(roles_update), @@ -203,3 +227,42 @@ 'manage_treemap/partials/fields/field_groups.html'), field_views.set_fields_page)) ) + +groups = admin_route( + GET=do(render_template('manage_treemap/groups.html'), groups_list), + #PUT=do(groups_update), + #POST=do(render_template('manage_treemap/partials/groups.html'), + # groups_create) +) + +get_groups_data = do( + json_api_call, + admin_instance_request, + get_groups_data) + +reports = do( + instance_request, + route( + GET=do(render_template('manage_treemap/reports.html'), reports), + ) + #PUT=do(groups_update), + #POST=do(render_template('manage_treemap/partials/groups.html'), + # groups_create) +) + +# FIXME move to API +get_reports_data = do( + json_api_call, + instance_request, + #admin_instance_request, + _get_reports_data +) + + +get_reports_user_data = do( + json_api_call, + instance_request, + check_report_query_parameters, + include_user, + _get_reports_data +) diff --git a/opentreemap/manage_treemap/templates/manage_treemap/dashboard_base.html b/opentreemap/manage_treemap/templates/manage_treemap/dashboard_base.html new file mode 100644 index 000000000..09f413fe7 --- /dev/null +++ b/opentreemap/manage_treemap/templates/manage_treemap/dashboard_base.html @@ -0,0 +1,26 @@ +{% extends "instance_base.html" %} +{% load l10n %} +{% load i18n %} +{% load auth_extras %} +{% load instance_config %} +{% load is_current_view %} + +{% block page_title %} | {% trans "Dashboard" %} {% endblock %} + +{% block activeexplore %} +{% endblock %} + +{% block activemanagement %} +{% endblock %} + +{% block activedashboard %} + active +{% endblock %} + +{% block header %} +{% endblock header %} +{% block subhead %} +{% endblock subhead %} + +{% block content %} +{% endblock content %} diff --git a/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_existing_user.plaintext.txt b/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_existing_user.plaintext.txt new file mode 100644 index 000000000..666c00fe5 --- /dev/null +++ b/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_existing_user.plaintext.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% trans "You've been added to a tree map!" %} diff --git a/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.body.txt b/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.body.txt index d06d6ecdb..6b0841328 100644 --- a/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.body.txt +++ b/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.body.txt @@ -1,24 +1,86 @@ {% load i18n %} +Welcome New SJC Citizen Scientist Tree Mapper! -{% blocktrans with instance_name=instance.name %} -You've been invited to join the {{ instance_name }} tree map! -{% endblocktrans %} +<p> +Thanks so much for joining the historic Jersey City Tree Mapping Census and we appreciate the time you have already put in participating in the WebEx Training we’ve put together for the program. +</p> -{% if instance.is_public %} -{% trans "You can look at it now by going to:" %} -<a href="{{ request.is_secure|yesno:"https,http"}}://{{ request.get_host }}{% url 'map' instance_url_name=instance.url_name %}"> - {{ request.is_secure|yesno:"https,http"}}://{{ request.get_host }}{% url 'map' instance_url_name=instance.url_name %} -</a> - -{% trans "But you'll have to register before you can edit or add trees. You can register at" %} -{% else %} - {% trans "Before you can access the tree map you'll have to register at" %} -{% endif %} +<p> +Below are the steps to help you get started <b>Setting Up a User Account</b> with the <b>SJC OpenTreeMap (OTM) Platform</b>, so you can begin mapping Trees in your neighborhood, and the <b>GroupMe OTM Text App</b>, so you can be in touch with your Neighborhood Captain. The GroupMe OTM Text App is important to download right away so communications about the Tree Mapping Census get to you in a timely way. The GroupMe OTM Text App also helps us to field any and all questions you may have about your field mapping experience. It is your direct connect to your Neighborhood Captain. +</p> -<a href="{{ request.is_secure|yesno:"https,http"}}://{{ request.get_host }}{% url 'registration_register' %}?join=true&key={{ invite.activation_key }}"> +<h3>Getting Started With OpenTreeMap</h3> +<p> +Before you can get started as a Tree Mapper you will need to <b>Set Up A User Account</b>. Please go to the link below to Register on the platform. Please note, this link only works for your email address and cannot be shared with others. +<br><a href="{{ request.is_secure|yesno:"https,http"}}://{{ request.get_host }}{% url 'registration_register' %}?join=true&key={{ invite.activation_key }}"> {{ request.is_secure|yesno:"https,http"}}://{{ request.get_host }}{% url 'registration_register' %}?join=true&key={{ invite.activation_key }} </a> +</p> + +<p> +Once you have Set Up and OTM User Account, you can go to the OTM platform on your smartphone to participate in the JC Tree Mapping Census for your neighborhood. +<a href="https://opentreemap.sustainablejc.org/">https://opentreemap.sustainablejc.org/</a>. +</p> + +<p style="text-indent: 0; margin: 1em;"> +<b>Since there is NOT an SJC OTM App you can download, here is a way to Set Up the SJC OTM Website Icon on your Android or iPhone for quick access - </b> +<ul> +<li><a href="https://www.howtogeek.com/196087/how-to-add-websites-to-the-home-screen-on-any-smartphone-or-tablet/">How To Add Website Icons To Your Smartphone Home Screens</a></li> +<li><a href="https://opentreemap.sustainablejc.org/jerseycity/map/">Noting the OpenTreeMap platform website here which you want to call up on your smartphone</a>, refer to the instructions above to put a shortcut icon on your smartphone like an App icon to quickly call up the website and get to the SJC OTM right away.</li> +</ul> +</p> + +<h3>Getting Started With The GroupMe (OTM) Text App</h3> +<p> +The GroupMe Text App will be your tool to talk, coordinate, and keep in touch with your Neighborhood Captain, Ward Captain, and Sustainable Jersey City. We are also connecting you with your fellow neighborhood Tree Mappers who have the GroupMe OTM Text App. You can send pictures, ask questions, get activated with your neighborhood! +</p> + +<p> +<b>First Step</b>: Download the GroupMe App on your smartphone (Google Playstore or iPhone), so you can create an account that will allow you to connect on the OTM Network we set up in JC for GroupMe OTM Text App with other Tree Mappers in your neighborhood and your Neighborhood Captain. +</p> + +<p> +<b>Second Step</b>: You will enter your specific neighborhood on the GroupMe OTM Text App via the dedicated Neighborhood Hyperlink Invite sent to you in an email from your Neighborhood Captain, so stay tuned to your inbox. +<br><span style="text-indent: 0; margin: 1em;"><b><i>*You can also access GroupMe on your computer via your web browser ! Just enter www.GroupMe.com and enter your account information.</b></i></span> +</p> + +<p> +If you’re having trouble with creating a GroupMe account or finding your Neighborhood Captain on the SJC GroupMe OTM Network we’ve set up for Jersey City, please reach out to us directly to Erika Bruce / OTM Tree Mapping Census Program Lead at Enbruce96@gmail.com - please provide your GroupMe account username, OTM User Account username, and the Neighborhood you live you in, which is the SAME NEIGHBORHOOD you indicated when you <a href="https://docs.google.com/forms/d/e/1FAIpQLSc-TjdqCR3f2_mVf-jP5R68YYJd2hrjTqjU1tUVCOg31UzM3A/viewform">Signed Up on the Census Registry.</a>! +</p> + +<br> + +<p> +<b><u>Other Important Resources For You</u></b><b><a href="https://www.sustainablejc.org/opentreemap">(available on SJC OTM Webpage)</a></b> +</p> + +<p> +<b><u>Sustainable JC OTM webpage link</u></b><a href="https://www.sustainablejc.org/opentreemap">https://www.sustainablejc.org/opentreemap</a></b> +</p> + +<a href="https://static1.squarespace.com/static/59eb5534b7411cf368c81ad3/t/5ebb1a7a8cf11f6e3c2fd36b/1589320315209/new+OTM+Tip+Sheet+2020+final+05122020.pdf">Download the new SJC OpenTreeMap (OTM) Tip Sheet here</a> + +<h3>Tree Species ID Resources</h3> +<p> +<ul> +<li><b><a href="https://rucore.libraries.rutgers.edu/rutgers-lib/54233/">Trees of NJ and the Mid-Atlantic States Guidebook pdf download</a>, recommended by the NJ Tree Foundation + How To Use This Guidebook ! (print and take with you when you go Tree Mapping - letter size pages)</b></li> +<li><b>Self-directed <a href="https://ellalhv.org/video-archive/">ELLA Webinar - Tree Species ID by Their Leaves</a> with Companion Pocket Guide <a href="https://www.amazon.com/Tree-Finder-Manual-Identification-Eastern/dp/0912550015">TREE FINDER available on Amazon for only $5.95</a> (recommended by our Neighborhood Captains 👍 !)</b></li> +<li><b>Neighborhood Captain Chris Lamm <a href="https://drive.google.com/file/d/1CKKSs8wtajvWU2EKU5rK9_ZKCwtOyzgy/view">JC Common Trees List</a> !</b></li> +</ul> +</p> + +<p> +<a href="https://drive.google.com/file/d/1g70Vlzy960xH9TVfCRAJCfESGzrbjKp9/view?usp=sharing">Here is replay of part of the WebEx Training you will find helpful</a> - <b>Tree Science & Climate Benefit of Trees by Ray Baylon / LSP Naturalist</b> - we recorded this at our July Training; please note the recording is off to a rough start but picks up nicely and is a good refresher for you as new Citizen Scientist Tree Mapper in Jersey City. +</p> + +<p> +The SJC OpenTreeMap Website Platform for Jersey City is in active development and will be available in the Apple App Store and the Google Play Store soon, so stay tuned for updates. +</p> + +<p> +Thanks and Happy Tree Mapping! +</p> -<br/> -<br/> -<br/> +<p> +Sustainable JC Green Team +</p> diff --git a/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.plaintext.txt b/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.plaintext.txt new file mode 100644 index 000000000..bb3e2db06 --- /dev/null +++ b/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.plaintext.txt @@ -0,0 +1,38 @@ +Welcome New SJC Citizen Scientist Tree Mapper! + +Thanks so much for joining the historic Jersey City Tree Mapping Census and we appreciate the time you have already put in participating in the WebEx Training we’ve put together for the program. + +Below are the steps to help you get started with the SJC OpenTreeMap (OTM) App, so you can begin mapping Trees in your neighborhood, and the GroupMe OTM Text App, so you can be in touch with your Neighborhood Captain. The GroupMe OTM App is important so communications about the Tree Mapping Census get to you in a timely way. The app also helps us to field any and all questions you may have about your field mapping experience. It is your direct connect to your Neighborhood Captain. + +Getting Started With OpenTreeMap + +Before you can get started as a Tree Mapper you will need a User Account. Please go to this link to Register on the platform. Please note, this link only works for your email address and cannot be shared with others. +{{ request.is_secure|yesno:"https,http"}}://{{ request.get_host }}{% url 'registration_register' %}?join=true&key={{ invite.activation_key }} + +Once you have set up an account you can go to the OTM platform on your phone to participate in the JC Tree Mapping Census for your neighborhood. +https://opentreemap.sustainablejc.org/ + +You can download the SJC OTM Tip Sheet available on our website at https://www.sustainablejc.org/opentreemap + +The SJC OpenTreeMap App for Jersey City is in active development and will be available in the Apple App Store and the Google Play Store soon, so stay tuned for updates. + +Here is a useful Tree Species ID resource ________ for you to download to take with you when you start Tree Mapping in your neighborhood. Remember, as a new Citizen Scientist learning how to ID Tree Species is part of the fun, so don’t worry - between this pdf download and the iNaturalist feature verifying your photos, we have you covered! + + +Getting Started With The GroupMe Text App + +The GroupMe Text App will be your tool to talk, coordinate, and keep in touch with your Neighborhood Captain, Ward Captain, and Sustainable Jersey City. We are also connecting you with your fellow neighborhood Tree Mappers who have the GroupMe OTM App. You can send pictures, ask questions, get activated with your neighborhood! + +First Step: Download the GroupMe App on your phone, so you can create an account that will allow you to connect with other Tree Mappers in your neighborhood and your Neighborhood Captain. +Second Step: You will enter your specific neighborhood GroupMe via a Hyperlink Invite sent to you in an email from your Neighborhood Captain, so stay tuned to your inbox. + +*You can access GroupMe via your web browser too! Just enter www.GroupMe.com and enter your account information. + +The SJC OpenTreeMap platform will officially be turned on for you to start mapping Trees in your neighborhood on Saturday May 16th. Between now and then you have plenty of time to set up your OTM User Account and connect with your Neighborhood Captains on the GroupMe OTM. + +If you’re having trouble with creating a GroupMe account or finding your Neighborhood Captain on GroupMe OTM please reach out to us at info@sustainablejc.org - please provide your GroupMe account username, OTM User Account username, and your neighborhood! + + +Thanks and Happy Tree Mapping! + +Sustainable JC Green Team diff --git a/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.subj.txt b/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.subj.txt index 666c00fe5..dcb2c807b 100644 --- a/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.subj.txt +++ b/opentreemap/manage_treemap/templates/manage_treemap/emails/invite_to_new_user.subj.txt @@ -1,2 +1,2 @@ {% load i18n %} -{% trans "You've been added to a tree map!" %} +{% trans "Invitation To Participate In SJC's OpenTreeMap For Jersey City!" %} diff --git a/opentreemap/manage_treemap/templates/manage_treemap/emails/user_joined_instance.plaintext.txt b/opentreemap/manage_treemap/templates/manage_treemap/emails/user_joined_instance.plaintext.txt new file mode 100644 index 000000000..c8ae2214f --- /dev/null +++ b/opentreemap/manage_treemap/templates/manage_treemap/emails/user_joined_instance.plaintext.txt @@ -0,0 +1,15 @@ +{% load i18n %} + +{% with username=user.username %} + {% blocktrans %} + {{ username }} has accepted an invitation to join your tree map! + Check out their profile at + {% endblocktrans %} + <a href="{{ request.is_secure|yesno:"https,http"}}://{{ request.get_host }}{% url 'user' username=user.username %}"> + {{ request.is_secure|yesno:"https,http"}}://{{ request.get_host }}{% url 'user' username=user.username %} + </a> +{% endwith %} + +<br/> +<br/> +<br/> diff --git a/opentreemap/manage_treemap/templates/manage_treemap/embed.html b/opentreemap/manage_treemap/templates/manage_treemap/embed.html index 16da888e0..2c0f96fc6 100644 --- a/opentreemap/manage_treemap/templates/manage_treemap/embed.html +++ b/opentreemap/manage_treemap/templates/manage_treemap/embed.html @@ -23,19 +23,19 @@ <h3>{% trans "Embed this tree map on another site" %}</h3> <form> <fieldset> <ul class="frame-size-choice list-inline container-fluid"> - <li class="pull-left"> + <li class="pull-left list-inline-item"> <label class="checkbox"> <input type="radio" name="frame-size" id="frame-size-standard" value="{{ iframe_standard }}" checked="checked"> <span>{% trans "Standard" %}</span> <span class="reduced">{{ iframe_standard_width }}x{{ iframe_standard_height }}</span> </label> </li> - <li class="pull-left"> + <li class="pull-left list-inline-item"> <label class="checkbox"> <input type="radio" name="frame-size" id="frame-size-wide" value="{{ iframe_wide }}"> <span>{% trans "Wide" %}</span> <span class="reduced">{{ iframe_wide_width }}x{{ iframe_wide_height }}</span> </label> </li> - <li class="pull-left"> + <li class="pull-left list-inline-item"> <label class="checkbox"> <input type="radio" name="frame-size" id="frame-size-custom" value="{{ iframe_custom }}"> <span>{% trans "Custom " %}</span> diff --git a/opentreemap/manage_treemap/templates/manage_treemap/groups.html b/opentreemap/manage_treemap/templates/manage_treemap/groups.html new file mode 100644 index 000000000..f7a014189 --- /dev/null +++ b/opentreemap/manage_treemap/templates/manage_treemap/groups.html @@ -0,0 +1,57 @@ +{% extends "manage_treemap/management_base.html" %} +{% load render_bundle from webpack_loader %} +{% load i18n %} +{% load instance_config %} + +{% block admin_title %}{% trans "Groups" %}{% endblock %} + +{% block tab_content %} + <div id="roles-pane"> + <div class="page-header"> + <div class="page-header-toggles"> + <i class="icon-menu" id="toggle-sidebar"></i> + </div> + <div class="page-header-title"> + <h1>{% trans "Groups" %}</h1> + </div> + {% include "manage_treemap/partials/form_buttons_groups.html" %} + </div> + + <div class="content"> + <div id="group-info"> + {% include "manage_treemap/partials/groups.html" %} + </div> + </div> + </div> +{% endblock %} + +{% block endbody %} + <div id="add-group-modal" class="modal modal-primary fade"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>{% trans "Add Group" %}</h3> + </div> + <div class="modal-body"> + <div class="form-group"> + <label class="control-label">{% trans "Name of new group:" %}</label> + <input id="new_group_name" type="text" class="form-control" style="width:100%;"> + </div> + </div> + <div class="modal-footer"> + <button class="btn dismiss-cancel" data-dismiss="modal"> + {% trans "Cancel" %} + </button> + <button id="create_new_group" class="btn btn-primary pull-right" data-dismiss="modal"> + {% trans "Create Group" %} + </button> + </div> + </div> + </div> + </div> +{% endblock endbody %} + +{% block scripts %} + {% render_bundle 'js/manage_treemap/groups' %} +{% endblock scripts %} diff --git a/opentreemap/manage_treemap/templates/manage_treemap/management_base.html b/opentreemap/manage_treemap/templates/manage_treemap/management_base.html index 2addb9558..93029cb12 100644 --- a/opentreemap/manage_treemap/templates/manage_treemap/management_base.html +++ b/opentreemap/manage_treemap/templates/manage_treemap/management_base.html @@ -79,6 +79,10 @@ {% include "manage_treemap/partials/admin_link.html" with view="roles_endpoint" title=_("Roles") icon="icon-lock" notification="udfs" %} + {% include "manage_treemap/partials/admin_link.html" with view="groups_endpoint" title=_("Groups") icon="icon-lock" %} + + {% include "manage_treemap/partials/admin_link.html" with view="reports_endpoint" title=_("Reports") icon="icon-chart-line" %} + {% with is_active=request|is_current_view:"comment_moderation_admin photo_review_admin" %} <tr {% if is_active %}class="active"{% endif %}> <td> diff --git a/opentreemap/manage_treemap/templates/manage_treemap/partials/fields/udf_row.html b/opentreemap/manage_treemap/templates/manage_treemap/partials/fields/udf_row.html index 2bb58326a..2bc5f8919 100644 --- a/opentreemap/manage_treemap/templates/manage_treemap/partials/fields/udf_row.html +++ b/opentreemap/manage_treemap/templates/manage_treemap/partials/fields/udf_row.html @@ -7,7 +7,7 @@ data-model-type="{{ udf.model_type }}" data-is-collection="{{ udf.iscollection|yesno }}" data-datatype="{{ udf.datatype_dict.type }}"> - <td>{{ udf.model_type|display_name:request.instance }} {{ udf.name }}</td> + <td>{% if udf.isrequired %}* {% endif %}{{ udf.model_type|display_name:request.instance }} {{ udf.name }}</td> <td>{{ datatype }}</td> <td class="udf-choices"> {% if not udf.iscollection and not udf.datatype_dict.choices %} diff --git a/opentreemap/manage_treemap/templates/manage_treemap/partials/form_buttons_groups.html b/opentreemap/manage_treemap/templates/manage_treemap/partials/form_buttons_groups.html new file mode 100644 index 000000000..5721421ae --- /dev/null +++ b/opentreemap/manage_treemap/templates/manage_treemap/partials/form_buttons_groups.html @@ -0,0 +1,30 @@ +{% extends "treemap/partials/form_buttons.html" %} +{% load i18n %} +{% load instance_config %} + +{% block other_buttons %} + <button data-class="display" class="btn btn-info addGroupBtn"> + {% trans "Add User To Group" %} + </button> + <div class="btn-group"> + <button data-class="display" type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + {% trans "Export" %} <span class="caret"></span> + </button> + <ul class="dropdown-menu dropdown-menu-right"> + <li> + <a href="javascript:;" + data-export-start-url="{% url 'management_begin_export_groups' instance_url_name=instance.url_name aggregation_level='neighborhood' %}"> + {% trans "Export Trees By Neighborhood" %} + </a> + </li> + <!-- + <li> + <a href="javascript:;" + data-export-start-url="{% url 'management_begin_export_groups' instance_url_name=instance.url_name aggregation_level='user' %}"> + {% trans "Export Trees By User" %} + </a> + </li> + --> + </ul> + </div> +{% endblock other_buttons %} diff --git a/opentreemap/manage_treemap/templates/manage_treemap/partials/groups.html b/opentreemap/manage_treemap/templates/manage_treemap/partials/groups.html new file mode 100644 index 000000000..f6b752362 --- /dev/null +++ b/opentreemap/manage_treemap/templates/manage_treemap/partials/groups.html @@ -0,0 +1,34 @@ +<div> + <!-- + {% for group in role_groups %} + {% include "manage_treemap/partials/role_group_model.html" %} + {% endfor %} + --> + <h3>Trees By Neighborhood</h3> + <div id="group-chart"> + <canvas height="50"></canvas> + </div> + + <br><br> + <h3>Groups</h3> + <div class="role-table-scroll"> + <table class="role-table table admin-table"> + <thead> + <tr> + <th>Ward</th> + <th>Neighborhood</th> + <th>Email</th> + </tr> + </thead> + <tbody> + {% for group, user in user_groups %} + <tr> + <td>{{ group.ward }}</td> + <td>{{ group.neighborhood }}</td> + <td>{{ user.email }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> +</div> diff --git a/opentreemap/manage_treemap/templates/manage_treemap/partials/reports.html b/opentreemap/manage_treemap/templates/manage_treemap/partials/reports.html new file mode 100644 index 000000000..d31de3ace --- /dev/null +++ b/opentreemap/manage_treemap/templates/manage_treemap/partials/reports.html @@ -0,0 +1,91 @@ +<div id="management" style="padding-top: 60px;"> + <div class="dashboard" style="width: 85%; margin-bottom: 50px;"> + <h3>Trees Counts</h3> + <div id="tree-counts-chart"> + <canvas height="50"></canvas> + </div> + + <h3>Trees By Species</h3> + <div id="species-chart"> + <canvas height="50"></canvas> + </div> + + <h3>Tree Conditions</h3> + <div id="tree-conditions-chart"> + <canvas height="50"></canvas> + </div> + + <h3>Tree Diameters</h3> + <div id="tree-diameters-chart"> + <canvas height="50"></canvas> + </div> + + <h3>Eco Benefits</h3> + <img src="/static/img/spinner.gif" class="spinner pull-right" style="display:none;"> + <table class="table table-hover" id="ecobenefits-by-ward-table"> + <thead></thead> + <tbody style="white-space: nowrap;" ></tbody> + </table> + <div id="ecobenefits-by-ward-total"></div> + <p><i>* Total trees based on trees used for Eco Benefits calculator</i></p> + </div> + + <div class="sidebar" style="width: 15%; padding-top: 60px; padding-left: 15px; padding-right: 15px;"> + <div class="container-fluid"> + <div class="form-group"> + <label class="control-label" style="color: white">Aggregation Level</label> + <select id="select-aggregation" class="form-control"> + <option value="ward">Ward</option> + <option value="neighborhood">Neighborhood</option> + <option value="park">Parks</option> + <option value="sid">SIDs</option> + </select> + </div> + + <div id="select-wards-container" class="form-group"> + <label class="control-label" style="color: white">Aggregation</label> + <select id="select-wards" multiple="multiple" class="form-control" size="15"> + <option value="all">All</option> + {% for ward in wards %} + <option>{{ ward.name }}</option> + {% endfor %} + </select> + <p style="color: white"><i>* Hold Control + Click to select muliple</i></p> + </div> + + <div id="select-neighborhoods-container" class="form-group"> + <label class="control-label" style="color: white">Aggregation</label> + <select id="select-neighborhoods" multiple="multiple" class="form-control" size="15"> + <option value="all">All</option> + {% for neighborhood in neighborhoods %} + <option>{{ neighborhood.name }}</option> + {% endfor %} + </select> + <p style="color: white"><i>* Hold Control + Click to select muliple</i></p> + </div> + + <div id="select-parks-container" class="form-group"> + <label class="control-label" style="color: white">Aggregation</label> + <select id="select-parks" multiple="multiple" class="form-control" size="15"> + <option value="all">All</option> + {% for park in parks %} + <option>{{ park.name }}</option> + {% endfor %} + </select> + <p style="color: white"><i>* Hold Control + Click to select muliple</i></p> + </div> + + <div id="select-sids-container" class="form-group"> + <label class="control-label" style="color: white">Aggregation</label> + <select id="select-sids" multiple="multiple" class="form-control" size="15"> + <option value="all">All</option> + {% for sid in sids %} + <option>{{ sid.name }}</option> + {% endfor %} + </select> + <p style="color: white"><i>* Hold Control + Click to select muliple</i></p> + </div> + </div> + + </div> +</div> diff --git a/opentreemap/manage_treemap/templates/manage_treemap/reports.html b/opentreemap/manage_treemap/templates/manage_treemap/reports.html new file mode 100644 index 000000000..c965adab9 --- /dev/null +++ b/opentreemap/manage_treemap/templates/manage_treemap/reports.html @@ -0,0 +1,28 @@ +{% extends "manage_treemap/dashboard_base.html" %} +{% load render_bundle from webpack_loader %} +{% load i18n %} +{% load instance_config %} + +{% block content %} + <div id="roles-pane" class="tab-content management-content"> + <div class="page-header"> + <div class="page-header-title"> + <h1>{% trans "Dashboard" %}</h1> + </div> + </div> + + <div class="content"> + <div id="group-info"> + {% include "manage_treemap/partials/reports.html" %} + </div> + </div> + </div> +{% endblock %} + +{% block endbody %} + <!-- this is where a modal would go for things like parameters --> +{% endblock endbody %} + +{% block scripts %} + {% render_bundle 'js/manage_treemap/reports' %} +{% endblock scripts %} diff --git a/opentreemap/manage_treemap/templatetags/is_current_view.py b/opentreemap/manage_treemap/templatetags/is_current_view.py index a88a07ee2..eabff8020 100644 --- a/opentreemap/manage_treemap/templatetags/is_current_view.py +++ b/opentreemap/manage_treemap/templatetags/is_current_view.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django import template -from django.core.urlresolvers import reverse +from django.urls import reverse register = template.Library() diff --git a/opentreemap/manage_treemap/tests/test_ecobenefits.py b/opentreemap/manage_treemap/tests/test_ecobenefits.py index a8c3c8910..7a12dd88d 100644 --- a/opentreemap/manage_treemap/tests/test_ecobenefits.py +++ b/opentreemap/manage_treemap/tests/test_ecobenefits.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals + import json diff --git a/opentreemap/manage_treemap/tests/test_fields.py b/opentreemap/manage_treemap/tests/test_fields.py index dc1c08387..0f7d42427 100644 --- a/opentreemap/manage_treemap/tests/test_fields.py +++ b/opentreemap/manage_treemap/tests/test_fields.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json import copy diff --git a/opentreemap/manage_treemap/tests/test_general.py b/opentreemap/manage_treemap/tests/test_general.py index 5515558df..6b0761c01 100644 --- a/opentreemap/manage_treemap/tests/test_general.py +++ b/opentreemap/manage_treemap/tests/test_general.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals + import json diff --git a/opentreemap/manage_treemap/tests/test_gsi.py b/opentreemap/manage_treemap/tests/test_gsi.py index ce9bbbc7c..8e6874e17 100644 --- a/opentreemap/manage_treemap/tests/test_gsi.py +++ b/opentreemap/manage_treemap/tests/test_gsi.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json diff --git a/opentreemap/manage_treemap/tests/test_instance_invitations.py b/opentreemap/manage_treemap/tests/test_instance_invitations.py index f99326482..1832557be 100644 --- a/opentreemap/manage_treemap/tests/test_instance_invitations.py +++ b/opentreemap/manage_treemap/tests/test_instance_invitations.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from registration.models import RegistrationProfile @@ -71,7 +69,7 @@ def test_normal_registration_without_invite(self): user = users[0] self.assertFalse(user.is_active) - self.assertEquals(len(InstanceUser.objects.filter(user=user)), 0) + self.assertEqual(len(InstanceUser.objects.filter(user=user)), 0) success_url = rv.get_success_url(user) self.assertEqual(success_url, 'registration_complete') @@ -115,7 +113,7 @@ def assert_user_was_invited(self, view, new_user): success_url, __, __ = view.get_success_url(new_user) self.assertEqual(success_url, '/%s/map/' % self.instance.url_name) - self.assertEquals(len(mail.outbox), 1) + self.assertEqual(len(mail.outbox), 1) msg = mail.outbox[0] # Make sure we have some chars and the correct receivers @@ -123,7 +121,7 @@ def assert_user_was_invited(self, view, new_user): self.assertGreater(len(msg.body), 10) to = set(msg.to) expected_to = {self.user.email} - self.assertEquals(to, expected_to) + self.assertEqual(to, expected_to) # Disable plug-in function to ensure we are testing core behavior @override_settings(INVITATION_ACCEPTED_NOTIFICATION_EMAILS=None) @@ -154,10 +152,10 @@ def test_does_not_redirect_when_email_different(self): # We should get an activation email, and no others, because the emails # did not match - self.assertEquals(len(mail.outbox), 1) + self.assertEqual(len(mail.outbox), 1) msg = mail.outbox[0] - self.assertEquals(tuple(msg.to), (new_user.email,)) + self.assertEqual(tuple(msg.to), (new_user.email,)) def test_does_not_redirect_when_key_does_not_match(self): rv = self._invite_and_register("some@email.com", key_matches=False) @@ -177,10 +175,10 @@ def test_does_not_redirect_when_key_does_not_match(self): # We should get an activation email, and no others, because the emails # did not match - self.assertEquals(len(mail.outbox), 1) + self.assertEqual(len(mail.outbox), 1) msg = mail.outbox[0] - self.assertEquals(tuple(msg.to), (new_user.email,)) + self.assertEqual(tuple(msg.to), (new_user.email,)) # Disable plug-in function to ensure we are testing core behavior @override_settings(INVITATION_ACCEPTED_NOTIFICATION_EMAILS=None) diff --git a/opentreemap/manage_treemap/tests/test_roles.py b/opentreemap/manage_treemap/tests/test_roles.py index 36c5fef28..6e05a48a2 100644 --- a/opentreemap/manage_treemap/tests/test_roles.py +++ b/opentreemap/manage_treemap/tests/test_roles.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json @@ -80,7 +78,7 @@ def test_add_user_to_instance(self): self.assertGreater(len(msg.subject), 10) self.assertGreater(len(msg.body), 10) - self.assertEquals(tuple(msg.to), (self.user4.email,)) + self.assertEqual(tuple(msg.to), (self.user4.email,)) def test_email_not_found_creates_invite(self): self.assertEqual(InstanceInvitation.objects.count(), 0) @@ -114,7 +112,7 @@ def test_email_not_found_creates_invite(self): self.assertGreater(len(msg.subject), 10) self.assertGreater(len(msg.body), 10) - self.assertEquals(tuple(msg.to), (email,)) + self.assertEqual(tuple(msg.to), (email,)) def test_invalid_email(self): body = {'email': 'asdfasdf@'} @@ -296,7 +294,7 @@ def test_model_assignment(self): 'MapFeaturePhoto.delete_bioswalephoto'] self.request_updates( - dict(zip(permissions, [True] * len(permissions)))) + dict(list(zip(permissions, [True] * len(permissions))))) for existing in permissions: __, codename = dotted_split(existing, 2, maxsplit=1) diff --git a/opentreemap/manage_treemap/tests/test_udf.py b/opentreemap/manage_treemap/tests/test_udf.py index 3c1df2247..9a599ae8f 100644 --- a/opentreemap/manage_treemap/tests/test_udf.py +++ b/opentreemap/manage_treemap/tests/test_udf.py @@ -1,6 +1,4 @@ -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals + import datetime import json @@ -52,7 +50,7 @@ def setUp(self): def _make_put_request(self, params): request = RequestFactory().put( 'does/not/matter/', json.dumps(params), - content_type=u'application/json') + content_type='application/json') request.method = 'PUT' setattr(request, 'user', self.user) setattr(request, 'instance', self.instance) @@ -152,7 +150,7 @@ def test_cant_delete_stewardship(self): self.instance.config['plot_stewardship_udf_id'] = self.cudf.pk self.instance.save() resp = udf_delete(make_request(), self.instance, self.cudf.pk) - self.assertEquals(resp.status_code, 400) + self.assertEqual(resp.status_code, 400) def test_delete_udf_deletes_perms(self): body = {'udf.name': 'cool udf', @@ -189,7 +187,7 @@ def test_delete_scalar_udf(self): .exists()) resp = udf_delete(make_request(), self.instance, self.udf.pk) - self.assertEquals(resp.status_code, 200) + self.assertEqual(resp.status_code, 200) self.assertFalse(Audit.objects.filter(instance=self.instance) .filter(model=self.udf.model_type) @@ -214,7 +212,7 @@ def test_cant_delete_collection_udf(self): .exists()) resp = udf_delete(make_request(), self.instance, self.cudf.pk) - self.assertEquals(resp.status_code, 400) + self.assertEqual(resp.status_code, 400) self.assertTrue(Audit.objects.filter(instance=self.instance) .filter(model='udf:%s' % self.cudf.pk) diff --git a/opentreemap/manage_treemap/tests/test_units.py b/opentreemap/manage_treemap/tests/test_units.py index 7b3ad1cd0..86425cd5b 100644 --- a/opentreemap/manage_treemap/tests/test_units.py +++ b/opentreemap/manage_treemap/tests/test_units.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.test.utils import override_settings diff --git a/opentreemap/manage_treemap/tests/test_urls.py b/opentreemap/manage_treemap/tests/test_urls.py index e8c42e860..c2a57c5c7 100644 --- a/opentreemap/manage_treemap/tests/test_urls.py +++ b/opentreemap/manage_treemap/tests/test_urls.py @@ -1,4 +1,4 @@ -from django.core.urlresolvers import reverse +from django.urls import reverse from treemap.tests import make_instance, make_admin_user from treemap.tests.test_urls import UrlTestCase diff --git a/opentreemap/manage_treemap/urls.py b/opentreemap/manage_treemap/urls.py index d93f802db..b565fe843 100644 --- a/opentreemap/manage_treemap/urls.py +++ b/opentreemap/manage_treemap/urls.py @@ -1,6 +1,4 @@ -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import url @@ -28,7 +26,12 @@ url(r'^photo-review/approve-reject/(?P<action>(approve)|(reject))$', routes.approve_or_reject_photos, name='approve_or_reject_photos'), + url(r'^user-roles-api/$', routes.user_roles_api, name='user_roles'), url(r'^user-roles/$', routes.user_roles, name='user_roles'), + + url(r'^user-roles/invited/$', routes.user_roles_invited, name='user_roles_invited'), + url(r'^user-roles/active/$', routes.user_roles_active, name='user_roles_active'), + url(r'^user-roles-partial/$', routes.user_roles_partial, name='user_roles_partial'), url(r'^user-invite/(?P<invite_id>\d+)$', routes.user_invites, @@ -36,6 +39,14 @@ url(r'^roles/$', routes.roles, name='roles_endpoint'), url(r'^export/user/(?P<data_format>(csv|json))/$', routes.begin_export_users, name='management_begin_export_users'), + + # groups export + url(r'^export/groups/(?P<aggregation_level>(neighborhood|user))/$', + routes.begin_export_groups, name='management_begin_export_groups'), + + url(r'^groups/(?P<aggregation_level>(neighborhood|user))/$', + routes.get_groups_data, name='get_groups_data'), + url(r'^clear-udf-notifications/$', routes.clear_udf_notifications, name='clear_udf_notifications'), @@ -53,4 +64,11 @@ name='field_configs'), url(r'^set-fields/$', routes.set_field_configs, name='set_field_configs'), + + url(r'^groups/$', routes.groups, name='groups_endpoint'), + + url(r'^reports/$', routes.reports, name='reports_endpoint'), + url(r'^reports/(?P<data_set>\w+)/(?P<aggregation_level>(neighborhood|ward|park|sid|none))/$', + routes.get_reports_data, name='get_reports_data'), + url(r'^reports-user-data/$', routes.get_reports_user_data, name='get_reports_user_data'), ] diff --git a/opentreemap/manage_treemap/views/__init__.py b/opentreemap/manage_treemap/views/__init__.py index 862bf5c43..b85bcd213 100644 --- a/opentreemap/manage_treemap/views/__init__.py +++ b/opentreemap/manage_treemap/views/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.core.exceptions import ValidationError @@ -31,7 +29,7 @@ def update_instance_fields(request, instance, validation_fn=None): def _update_instance_fields(json_data, instance, validation_fn=None, should_update_universal_rev=False): error_dict = {} - for identifier, value in json_data.iteritems(): + for identifier, value in json_data.items(): model, field_name = dotted_split(identifier, 2, maxsplit=1) obj = instance @@ -55,7 +53,7 @@ def _update_instance_fields(json_data, instance, validation_fn=None, if should_update_universal_rev: instance.update_universal_rev() return {'ok': True} - except ValidationError, ve: + except ValidationError as ve: validation_error = ve raise ValidationError(package_field_errors('instance', validation_error)) diff --git a/opentreemap/manage_treemap/views/fields.py b/opentreemap/manage_treemap/views/fields.py index 3ae6de250..f76b6c868 100644 --- a/opentreemap/manage_treemap/views/fields.py +++ b/opentreemap/manage_treemap/views/fields.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from copy import deepcopy @@ -76,7 +74,7 @@ def set_search_config(request, instance): search_fields = json_from_request(request) for prop in ('search_config', 'mobile_search_fields'): config = deepcopy(getattr(instance, prop)) - for key, val in search_fields[prop].iteritems(): + for key, val in search_fields[prop].items(): config[key] = search_fields[prop][key] setattr(instance, prop, config) @@ -202,7 +200,7 @@ def field_context(identifier): if ALERT_IDENTIFIER_PATTERN.match(identifier): return get_alert_field_info(identifier, instance) else: - return set_search_field_label(instance, {'identifier': field_name}) + return set_search_field_label(instance, {'identifier': identifier}) return [field_context(field_name) for field_name in field_names] diff --git a/opentreemap/manage_treemap/views/green_infrastructure.py b/opentreemap/manage_treemap/views/green_infrastructure.py index a2e14709a..506fbabbd 100644 --- a/opentreemap/manage_treemap/views/green_infrastructure.py +++ b/opentreemap/manage_treemap/views/green_infrastructure.py @@ -45,7 +45,8 @@ def _get_form_fields(defaults, thing): return form_fields terminology_fields = {thing: _get_form_fields(defaults, thing) - for thing, defaults in REPLACEABLE_TERMS.items()} + for thing, defaults + in list(REPLACEABLE_TERMS.items())} __, annual_rainfall_display_value = get_display_value( instance, 'greenInfrastructure', 'rainfall', @@ -165,7 +166,7 @@ def _map_feature_cross_validator(field_name, value, instance): def _terminology_validator(field_name, value, instance): - acceptable_terms = REPLACEABLE_TERMS.keys() + [ + acceptable_terms = list(REPLACEABLE_TERMS.keys()) + [ Cls.__name__ for Cls in _get_replaceable_models(instance)] __, terms, term, form = dotted_split(field_name, 4, maxsplit=3) @@ -219,7 +220,7 @@ def _set_map_feature_config(field_name, value, instance): def _validate_and_set_individual_values(json_data, instance, error_dict): errors = None INVALID_KEY_MESSAGE = _("An invalid key was sent in the request") - for identifier, value in json_data.iteritems(): + for identifier, value in json_data.items(): if not '.' in identifier: error_dict[identifier] = [INVALID_KEY_MESSAGE] __, field_name = dotted_split(identifier, 2, maxsplit=1) @@ -251,7 +252,7 @@ def _validate_and_set_individual_values(json_data, instance, error_dict): # mutates error_dict def _cross_validate_values(json_data, instance, error_dict): errors = None - for identifier, value in json_data.iteritems(): + for identifier, value in json_data.items(): __, field_name = dotted_split(identifier, 2, maxsplit=1) if field_name in error_dict: continue @@ -268,7 +269,7 @@ def green_infrastructure(request, instance): json_data = json_from_request(request) new_data = {} increment_universal_rev = False - for identifier, value in json_data.iteritems(): + for identifier, value in json_data.items(): model, field_name = dotted_split(identifier, 2, maxsplit=1) if field_name.startswith('config.map_feature_types') or \ field_name.startswith('config.map_feature_config'): diff --git a/opentreemap/manage_treemap/views/groups.py b/opentreemap/manage_treemap/views/groups.py new file mode 100644 index 000000000..cd4ff074f --- /dev/null +++ b/opentreemap/manage_treemap/views/groups.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +from copy import deepcopy + +from exporter.group import get_neighborhood_count +from treemap.models import NeighborhoodGroup +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError, ObjectDoesNotExist +from django.db import transaction +from django.http import HttpResponse, HttpResponseBadRequest +from django.utils.functional import Promise +from django.utils.translation import ugettext_lazy as _ + + +def groups_update(request, instance): + role_perms = json_from_request(request) + _update_perms_from_object(role_perms, instance) + return HttpResponse(_('Updated roles')) + + +def groups_list(request, instance): + + user_groups = [] + groups = NeighborhoodGroup.objects.all() + + for group in groups: + for user in group.user_set.all(): + user_groups.append((group, user)) + + return { + 'user_groups': user_groups, + 'instance': instance, + } + + +@transaction.atomic +def groups_create(request, instance): + params = json_from_request(request) + + group_name = params.get('name', None) + + if not group_name: + return HttpResponseBadRequest( + _("Must provide a name for the new role.")) + + role, created = Role.objects.get_or_create(name=role_name, + instance=instance, + rep_thresh=0) + + if created is False: + return HttpResponseBadRequest( + _("A role with name '%(role_name)s' already exists") % + {'role_name': role_name}) + + add_default_permissions(instance, roles=[role]) + + return roles_list(request, instance) + + +def get_groups_data(request, instance, aggregation_level): + return { + 'data': list(get_neighborhood_count(instance).all()) + } diff --git a/opentreemap/manage_treemap/views/management.py b/opentreemap/manage_treemap/views/management.py index b2225bac3..e15b7a962 100644 --- a/opentreemap/manage_treemap/views/management.py +++ b/opentreemap/manage_treemap/views/management.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import locale import re from django.conf import settings from django.core.exceptions import ValidationError -from django.core.urlresolvers import reverse +from django.urls import reverse from django.core.validators import URLValidator, validate_email from django.shortcuts import redirect from django.utils.translation import ugettext as _ @@ -243,9 +241,9 @@ def benefits_convs(request, instance): pfx = ('<span class="currency-value">' + conv.currency_symbol + '</span> ') - for group_title, fields in field_groups.iteritems(): + for group_title, fields in field_groups.items(): fields_with_pfx = [((pfx + label), value) - for label, value in fields.iteritems()] + for label, value in fields.items()] field_groups[group_title] = fields_with_pfx return {'benefitCurrencyConversion': conv, @@ -271,7 +269,7 @@ def update_benefits(request, instance): updated_values = json_from_request(request) - for field, value in updated_values.iteritems(): + for field, value in updated_values.items(): if field in valid_fields: field_part = dotted_split(field, 2)[1] setattr(conv, field_part, value) diff --git a/opentreemap/manage_treemap/views/photo.py b/opentreemap/manage_treemap/views/photo.py index 655dc9d16..64178bf4e 100644 --- a/opentreemap/manage_treemap/views/photo.py +++ b/opentreemap/manage_treemap/views/photo.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.core.paginator import Paginator, EmptyPage from django.db import transaction diff --git a/opentreemap/manage_treemap/views/reports.py b/opentreemap/manage_treemap/views/reports.py new file mode 100644 index 000000000..7758a5a48 --- /dev/null +++ b/opentreemap/manage_treemap/views/reports.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +from __future__ import unicode_literals + +from copy import deepcopy +from functools import wraps, partial +import json + +from django.db import IntegrityError, connection, transaction +from django.contrib.contenttypes.models import ContentType +from django_tinsel.utils import LazyEncoder +from django.http import (HttpResponse, HttpResponseBadRequest, + HttpResponseRedirect) + +from treemap.ecobenefits import get_benefits_for_filter +from treemap.search import Filter +from treemap.models import Boundary, NeighborhoodGroup, MapFeature + + +def add_boundary_clause(aggregation_level, alias='m', join='boundary_join', where='boundary_where'): + format_args = {} + args = [] + if not aggregation_level: + format_args[join] = "cross join (select 'ALL'::varchar as name) b" + format_args[where] = 'and 1=1' + else: + format_args[join] = ''' + left join treemap_boundary b on + (st_within({alias}.the_geom_webmercator, b.the_geom_webmercator)) + '''.format(alias=alias) + format_args[where] = ''' + and b.name is not null + and lower(b.category) = %s + ''' + args.append(aggregation_level) + + return (format_args, args) + + +def add_user_clause(user, alias='m', where='user_where'): + format_args = {} + args = [] + if user: + format_args[where] = 'and {alias}.updated_by_id = %s'.format(alias=alias) + args.append(user.id) + else: + format_args[where] = 'and 1=1' + + return (format_args, args) + + +def check_report_query_parameters(view_fn): + @wraps(view_fn) + def f(request, instance, *args, **kwargs): + data_set = request.GET.get('data_set') + if data_set not in _REPORTS: + message_dict = { 'globalErrors': ['Please choose a correct report'] } + return HttpResponseBadRequest( + json.dumps(message_dict, cls=LazyEncoder)) + + aggregation_level = request.GET.get('aggregation_level') + return view_fn(request, instance, data_set, aggregation_level, *args, **kwargs) + + #raise PermissionDenied + return f + + +def include_user(view_fn): + @wraps(view_fn) + def f(request, *args, **kwargs): + return view_fn(request, *args, include_user=True, **kwargs) + return f + + +def reports(request, instance): + neighborhoods = Boundary.objects.filter(category__iexact='neighborhood').values('name').order_by('name').all() + wards = Boundary.objects.filter(category__iexact='ward').order_by('name').values('name').all() + parks = Boundary.objects.filter(category__iexact='park').order_by('name').values('name').distinct().all() + sids = Boundary.objects.filter(category__iexact='sid').order_by('name').values('name').distinct().all() + + return { + 'instance': instance, + 'neighborhoods': neighborhoods, + 'wards': wards, + 'parks': parks, + 'sids': sids + } + + +def get_reports_data(request, instance, data_set, aggregation_level, include_user=False): + data_set_funcs = { + 'count': get_tree_count, + 'count_over_time': get_tree_count_over_time, + 'species': get_species_count, + 'condition': get_tree_conditions, + 'diameter': get_tree_diameters, + 'ecobenefits': get_ecobenefits, + 'ecobenefits_by_user': get_ecobenefits_by_user + } + if data_set in data_set_funcs: + return {'data': data_set_funcs[data_set]( + aggregation_level, + instance, + request.user if include_user else None)} + + return None + + +def get_tree_count(aggregation_level, instance, user): + _query = """ + select b.name as "name", + count(1) as "count" + from treemap_mapfeature m + join treemap_tree t on m.id = t.plot_id + {boundary_join} + where 1=1 + {boundary_where} + {user_where} + group by b.name + """ + + (format_boundary, args_boundary) = add_boundary_clause(aggregation_level) + (format_user, args_user) = add_user_clause(user) + + query = _query.format(**format_boundary, **format_user) + args = [*args_boundary, *args_user] + + columns = ['name', 'count'] + with connection.cursor() as cursor: + cursor.execute(query, args) + results = cursor.fetchall() + return [dict(zip(columns, r)) for r in results] + + +def get_tree_count_over_time(aggregation_level, instance, user): + _query = """ + WITH week_range as ( + select date_trunc('week', i) as "date" + from generate_series(current_date - interval '3 months', + current_date, '1 week'::interval) i + ), trees as ( + select date_trunc('week', m.updated_at::date) as "date", + count(1) as "count" + from treemap_mapfeature m + join treemap_tree t on m.id = t.plot_id + where 1=1 + {user_where} + group by date_trunc('week', m.updated_at::date) + ) + SELECT w.date, t.count + FROM week_range w + LEFT JOIN trees t on t.date = w.date + """ + + (format_user, args) = add_user_clause(user) + query = _query.format(**format_user) + + #import ipdb; ipdb.set_trace() # BREAKPOINT + columns = ['name', 'count'] + with connection.cursor() as cursor: + cursor.execute(query, args) + results = cursor.fetchall() + return [dict(zip(columns, r)) for r in results] + + +def get_species_count(aggregation_level, instance, user): + query = """ + SELECT s.common_name as species_name, + b.name as name, + count(1) as "count" + from treemap_mapfeature m + join treemap_tree t on m.id = t.plot_id + left join treemap_species s on s.id = t.species_id + left join treemap_boundary b on + (st_within(m.the_geom_webmercator, b.the_geom_webmercator)) + WHERE 1=1 + and s.common_name is not null + and b.name is not null + and lower(b.category) = %s + group by s.common_name, b.name + """ + columns = ['species_name', 'name', 'count'] + with connection.cursor() as cursor: + cursor.execute(query, [aggregation_level]) + results = cursor.fetchall() + return [dict(zip(columns, r)) for r in results] + + +def get_tree_conditions(aggregation_level, instance, user): + query = """ + select b.name, + sum(case when t.udfs -> 'Condition' = 'Healthy' then 1 else 0 end) as healthy, + sum(case when t.udfs -> 'Condition' = 'Unhealthy' then 1 else 0 end) as unhealthy, + sum(case when t.udfs -> 'Condition' = 'Dead' then 1 else 0 end) as dead, + sum(case when t.udfs -> 'JC Forester - Roots Sidewalk Issue' = 'Yes' then 1 else 0 end) as sidewalk_issue, + sum(case when t.udfs -> 'JC Forester - Canopy Power Lines Issue' = 'Yes' then 1 else 0 end) as power_lines_issue + from treemap_mapfeature m + join treemap_tree t on m.id = t.plot_id + left JOIN treemap_boundary b on (ST_Within(m.the_geom_webmercator, b.the_geom_webmercator)) + where 1=1 + and lower(b.category) = %s + and b.name is not null + group by b.name + """ + columns = [ + 'name', + 'healthy', + 'unhealthy', + 'dead', + 'sidewalk_issue', + 'power_lines_issue' + ] + with connection.cursor() as cursor: + cursor.execute(query, [aggregation_level]) + results = cursor.fetchall() + return [dict(zip(columns, r)) for r in results] + + +def get_tree_diameters(aggregation_level, instance, user): + query = """ + with tstats as ( + select min(diameter) as min, + max(diameter) as max + from treemap_mapfeature m + left join treemap_tree t on m.id = t.plot_id + left join treemap_species s on s.id = t.species_id + where 1=1 + and diameter is not null + and s.common_name is not null + -- this is otherwise probably just wrong data + and diameter >= 2.5 + ) + select diameter as diameter, + b.name as name, + tstats.min as "min", + tstats.max as "max" + from treemap_mapfeature m + cross join tstats + left join treemap_tree t on m.id = t.plot_id + left join treemap_species s on s.id = t.species_id + left JOIN treemap_boundary b on (ST_Within(m.the_geom_webmercator, b.the_geom_webmercator)) + where 1=1 + and lower(b.category) = %s + and b.name is not null + and diameter is not null + and s.common_name is not null + -- this is otherwise probably just wrong data + and diameter >= 2.5 + """ + + columns = ['diameter', 'name', 'min', 'max'] + with connection.cursor() as cursor: + cursor.execute(query, [aggregation_level]) + results = cursor.fetchall() + return [dict(zip(columns, r)) for r in results] + + +def get_ecobenefits(aggregation_level, instance, user): + """ + Get the ecobenefits as a flattened table. + We will have two columns per label, one is the value per year, + one is the units per year + """ + columns = ['Name'] + data = [] + boundaries = Boundary.objects.filter(category__iexact=aggregation_level).order_by('name').all() + + _filter = Filter(None, None, instance) + benefits_all, basis_all = get_benefits_for_filter(_filter) + data_all = ['Total'] + for (_, value) in benefits_all['plot'].items(): + label = value['label'] + columns.extend(['{} {}/year'.format(label, value['unit']), '{} ($)'.format(label)]) + data_all.extend([value['value'], value['currency']]) + columns.append('Total Trees') + data_all.append(basis_all['plot']['n_objects_used']) + + boundary_filters = [] + + # iterate over the boundaries and get the benefits for each one + for boundary in boundaries: + # for now, skip this one + if boundary.name == 'Liberty State Park': + continue + + boundary_filter = json.dumps({'plot.geom': + {'IN_BOUNDARY': boundary.pk}}) + _filter = Filter(boundary_filter, None, instance) + benefits_boundary, basis_boundary = get_benefits_for_filter(_filter) + + data_boundary = [boundary.name] + for (_, value) in benefits_boundary['plot'].items(): + label = value['label'] + data_boundary.extend([value['value'], value['currency']]) + data_boundary.append(basis_boundary['plot']['n_objects_used']) + + data.append(data_boundary) + + # add our totals at the end + data.append(data_all) + return {'columns': columns, 'data': data} + + +def get_ecobenefits_by_user(aggregation_level, instance, user): + """ + Get the ecobenefits as a flattened table. + We will have two columns per label, one is the value per year, + one is the units per year + """ + columns = ['Name'] + data = [] + + user_filter = json.dumps({'plot.updated_by_id': user.id}) + + _filter = Filter(user_filter, None, instance) + benefits_all, basis_all = get_benefits_for_filter(_filter) + data_all = ['Total'] + for (_, value) in benefits_all['plot'].items(): + label = value['label'] + columns.extend(['{} {}/year'.format(label, value['unit']), '{} ($)'.format(label)]) + data_all.extend([value['value'], value['currency']]) + columns.append('Total Trees') + data_all.append(basis_all['plot']['n_objects_used']) + + return {'columns': columns, 'data': data_all} + + +_REPORTS = { + 'count': get_tree_count, + 'count_over_time': get_tree_count_over_time, + 'species': get_species_count, + 'condition': get_tree_conditions, + 'diameter': get_tree_diameters, + 'ecobenefits': get_ecobenefits, + 'ecobenefits_by_user': get_ecobenefits_by_user +} diff --git a/opentreemap/manage_treemap/views/roles.py b/opentreemap/manage_treemap/views/roles.py index 24c38719b..872b0d4dd 100644 --- a/opentreemap/manage_treemap/views/roles.py +++ b/opentreemap/manage_treemap/views/roles.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from copy import deepcopy @@ -23,6 +21,7 @@ from treemap.plugin import get_instance_permission_spec from manage_treemap.views import remove_udf_notification +from functools import reduce WRITE_PERM = (_('Full Write Access'), @@ -70,19 +69,19 @@ def _update_perms_from_object(role_perms, instance): roles_by_id = {role.pk: role for role in Role.objects.filter(instance=instance)} field_perms = {(role, perm.full_name): perm - for role in roles_by_id.itervalues() + for role in roles_by_id.values() for perm in deepcopy(role_field_permissions(role))} - input_role_ids = [int(role_id) for role_id in role_perms.iterkeys()] + input_role_ids = [int(role_id) for role_id in role_perms.keys()] for role_id in input_role_ids: if role_id not in roles_by_id: raise ValidationError("Unrecognized role id [%s]" % role_id) input_roles = [roles_by_id[role_id] for role_id in input_role_ids] - input_role_fields = zip(input_roles, [ - role_inputs['fields'] for role_inputs in role_perms.itervalues()]) - input_role_models = zip(input_roles, [ - role_inputs['models'] for role_inputs in role_perms.itervalues()]) + input_role_fields = list(zip(input_roles, [ + role_inputs['fields'] for role_inputs in role_perms.values()])) + input_role_models = list(zip(input_roles, [ + role_inputs['models'] for role_inputs in role_perms.values()])) def validate_model_name(model_name, valid_names): if model_name not in valid_names: @@ -91,7 +90,7 @@ def validate_model_name(model_name, valid_names): (", ".join(valid_names), model_name)) def validate_and_save_field_perm(role, field_perm): - for model_field_name, perm_type in field_perm.iteritems(): + for model_field_name, perm_type in field_perm.items(): model_name, field_name = dotted_split(model_field_name, 2) validate_model_name(model_name, valid_field_model_names) @@ -145,7 +144,7 @@ def validate_permission_assignment(codename, should_be_assigned, Model): def validate_and_save_model_perm(role, model_perm): unassign = [] - for model_perm_name, should_be_assigned in model_perm.iteritems(): + for model_perm_name, should_be_assigned in model_perm.items(): model_name, codename = dotted_split(model_perm_name, 2) validate_model_name( model_name, set(valid_perm_models_by_name.keys())) @@ -235,8 +234,8 @@ def get_role_photo_perms(role, Model): def get_instance_perms(roles): def translated(spec): - return {k: unicode(v) if isinstance(v, Promise) else v - for k, v in spec.iteritems()} + return {k: str(v) if isinstance(v, Promise) else v + for k, v in spec.items()} def combine(base_dict, additional_dict): combination = deepcopy(base_dict) @@ -263,13 +262,16 @@ def combine(base_dict, additional_dict): for spec in specs] def role_field_perms(Model): - return zip(*[get_field_perms(role, Model) for role in roles]) + return list( + zip(*[get_field_perms(role, Model) for role in roles])) def role_model_perms(Model): - return zip(*[get_role_model_perms(role, Model) for role in roles]) + return list( + zip(*[get_role_model_perms(role, Model) for role in roles])) def role_photo_perms(Model): - return zip(*[get_role_photo_perms(role, Model) for role in roles]) + return list( + zip(*[get_role_photo_perms(role, Model) for role in roles])) groups = [{ 'role_model_perms': role_model_perms(Model), diff --git a/opentreemap/manage_treemap/views/udf.py b/opentreemap/manage_treemap/views/udf.py index d2474f73c..70082ce45 100644 --- a/opentreemap/manage_treemap/views/udf.py +++ b/opentreemap/manage_treemap/views/udf.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json @@ -66,7 +64,7 @@ def udf_bulk_update(request, instance): choice_map = {int(param['id']): param['changes'] for param in choice_changes} udfds = [udf for udf in udf_defs(instance) - if udf.pk in choice_map.keys()] + if udf.pk in list(choice_map.keys())] # Update one at a time rather than doing bulk_update. # There won't be that many of them, and we need to go through diff --git a/opentreemap/manage_treemap/views/user_roles.py b/opentreemap/manage_treemap/views/user_roles.py index b801cbcf6..5193b0e5c 100644 --- a/opentreemap/manage_treemap/views/user_roles.py +++ b/opentreemap/manage_treemap/views/user_roles.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.paginator import Paginator, EmptyPage from django.contrib.auth import login, authenticate from django.db import transaction from django.dispatch import receiver +from django.forms.models import model_to_dict from django.http import HttpResponse from django.utils.translation import ugettext as _ from django.shortcuts import get_object_or_404 @@ -34,7 +33,7 @@ def _extract_role_updates(post): mapping them to the values """ updates = {} - for key, value in post.iteritems(): + for key, value in post.items(): if key.startswith("iuser_") and key.endswith("_role"): iuser_id = key[6:-5] updates[iuser_id] = value @@ -62,6 +61,65 @@ def create_user_role(request, instance): return user_roles_list(request, instance) +def user_roles_list_invited(request, instance): + + def invite_user_context(invites): + for invite in invites: + yield { + 'id': str(invite.pk), + 'username': invite.email, + 'role_id': invite.role.pk, + 'role_name': invite.role.name, + 'admin': invite.admin, + } + + invited_users = instance.instanceinvitation_set \ + .select_related('role') \ + .filter(accepted=False) + + return { + 'invited_users': list(invite_user_context(invited_users)), + 'instance_roles': list(Role.objects.filter(instance_id=instance.pk).values()) + } + + +def user_roles_list_active(request, instance): + + def instance_user_context(users): + for instance_user in users: + user = instance_user.user + yield { + 'id': str(instance_user.pk), + 'username': user.username, + 'role_id': instance_user.role.pk, + 'role_name': instance_user.role.name, + 'admin': instance_user.admin, + 'is_owner': does_user_own_instance(instance, user) + } + + active_users = instance.instanceuser_set \ + .select_related('role', 'user') \ + .exclude(user=User.system_user()) + + return { + 'active_users': list(instance_user_context(active_users)), + 'instance_roles': list(Role.objects.filter(instance_id=instance.pk).values()), + } + + +def user_roles_list_api(request, instance): + user_roles = user_roles_list(request, instance) + + user_roles['instance'] = model_to_dict(user_roles['instance']) + + user_roles['paged_instance_users'] = list(user_roles['paged_instance_users'].object_list.values()) + user_roles['invited_users'] = list(user_roles['invited_users']) + user_roles['instance_roles'] = list(user_roles['instance_roles'].values()) + user_roles['instance_users'] = list(user_roles['instance_users']) + + return user_roles + + def user_roles_list(request, instance): page = int(request.GET.get('page', '1')) user_sort = request.GET.get('user_sort', 'user__username') @@ -137,9 +195,10 @@ def invite_user_with_email_to_instance(request, email, instance): email__iexact=email, instance=instance) if existing_invites.exists(): - raise ValidationError( - _("A user with email address '%s' has already been invited.") % - email) + ctxt = {'request': request, + 'instance': instance, + 'invite': existing_invites} + send_email('invite_to_new_user', ctxt, (email,)) invite = InstanceInvitation.objects.create(instance=instance, email=email, @@ -190,7 +249,7 @@ def update_user_roles(request, instance): (InstanceInvitation, 'invites')): updates = role_updates.get(key, {}) - for pk, updated_info in updates.iteritems(): + for pk, updated_info in updates.items(): model = Model.objects.get(pk=pk) updated_role = int(updated_info.get('role', model.role_id)) diff --git a/opentreemap/modeling/migrations/0001_initial.py b/opentreemap/modeling/migrations/0001_initial.py index 8489fe40b..6bfad64ff 100644 --- a/opentreemap/modeling/migrations/0001_initial.py +++ b/opentreemap/modeling/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations from django.conf import settings @@ -26,8 +26,8 @@ class Migration(migrations.Migration): ('prioritization_params', treemap.json_field.JSONField()), ('scenarios', treemap.json_field.JSONField(null=True, blank=True)), ('currentScenarioId', models.IntegerField(null=True, blank=True)), - ('instance', models.ForeignKey(to='treemap.Instance')), - ('owner', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('instance', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance')), + ('owner', models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), ] diff --git a/opentreemap/modeling/migrations/0002_remove_plan_currentscenarioid.py b/opentreemap/modeling/migrations/0002_remove_plan_currentscenarioid.py index 13c759551..3bd3338aa 100644 --- a/opentreemap/modeling/migrations/0002_remove_plan_currentscenarioid.py +++ b/opentreemap/modeling/migrations/0002_remove_plan_currentscenarioid.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/modeling/migrations/0003_plan_zoom_lat_lng.py b/opentreemap/modeling/migrations/0003_plan_zoom_lat_lng.py index ab758bab3..76f4238d9 100644 --- a/opentreemap/modeling/migrations/0003_plan_zoom_lat_lng.py +++ b/opentreemap/modeling/migrations/0003_plan_zoom_lat_lng.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models import treemap.json_field diff --git a/opentreemap/modeling/migrations/0004_plan_revision.py b/opentreemap/modeling/migrations/0004_plan_revision.py index b5588dc95..73188912e 100644 --- a/opentreemap/modeling/migrations/0004_plan_revision.py +++ b/opentreemap/modeling/migrations/0004_plan_revision.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/modeling/models.py b/opentreemap/modeling/models.py index 977fa28a0..5c74fa7fb 100644 --- a/opentreemap/modeling/models.py +++ b/opentreemap/modeling/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.db import models from django.core.exceptions import ValidationError @@ -13,8 +11,8 @@ class Plan(models.Model): revision = models.IntegerField(default=0) - instance = models.ForeignKey(Instance) - owner = models.ForeignKey(User) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) + owner = models.ForeignKey(User, on_delete=models.CASCADE) name = models.TextField() description = models.TextField(blank=True) is_published = models.BooleanField(default=False) @@ -37,7 +35,7 @@ def to_json(self): } def update(self, plan_dict): - for key, value in plan_dict.iteritems(): + for key, value in plan_dict.items(): if key in ('revision', 'name', 'description', 'is_published', 'scenarios', 'zoom_lat_lng'): setattr(self, key, value) @@ -53,5 +51,5 @@ def save(self, *args, **kwargs): def clean(self): pass - def __unicode__(self): + def __str__(self): return self.name diff --git a/opentreemap/modeling/run_model/GrowthAndMortalityModel.py b/opentreemap/modeling/run_model/GrowthAndMortalityModel.py index 5d46b5e60..8a5ec63c7 100644 --- a/opentreemap/modeling/run_model/GrowthAndMortalityModel.py +++ b/opentreemap/modeling/run_model/GrowthAndMortalityModel.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import random from copy import copy diff --git a/opentreemap/modeling/run_model/GrowthModelUrbanTreeDatabase.py b/opentreemap/modeling/run_model/GrowthModelUrbanTreeDatabase.py index 4a9ed7dea..728a87f3c 100644 --- a/opentreemap/modeling/run_model/GrowthModelUrbanTreeDatabase.py +++ b/opentreemap/modeling/run_model/GrowthModelUrbanTreeDatabase.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- # flake8: noqa -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from copy import deepcopy from operator import itemgetter @@ -99,7 +97,7 @@ def _get_species_for_planting(instance): return [] itree_region = itree_regions[0] - otm_codes = _growth_data[itree_region.code].keys() + otm_codes = list(_growth_data[itree_region.code].keys()) species = [species_for_otm_code(otm_code) for otm_code in otm_codes] species = sorted(species, key=itemgetter('common_name')) @@ -136,7 +134,6 @@ def bisect(f, x_lo, x_hi, y_target, max_iterations, tolerance): raise Exception("Max iterations exceeded") - # Growth functions from TS4_Growth_eqn_forms.csv # Note that 'mse' is 'mean-squared-error', which is listed in column 'c' of # TS6_Growth_coefficients.csv diff --git a/opentreemap/modeling/run_model/MortalityModelUrbanTreeDatabase.py b/opentreemap/modeling/run_model/MortalityModelUrbanTreeDatabase.py index 5daa5eced..283e5db3c 100644 --- a/opentreemap/modeling/run_model/MortalityModelUrbanTreeDatabase.py +++ b/opentreemap/modeling/run_model/MortalityModelUrbanTreeDatabase.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from copy import deepcopy from random import random @@ -82,7 +80,7 @@ def init_tree(self, tree): def kill_trees(self, trees, remainders): categories = self._categorize(trees) new_remainders = {} - for key, c in categories.iteritems(): + for key, c in categories.items(): remainder = remainders.get(key, 0) float_to_kill = c.mortality * len(c.trees) + remainder int_to_kill = int(round(float_to_kill)) diff --git a/opentreemap/modeling/run_model/Tree.py b/opentreemap/modeling/run_model/Tree.py index f6f622f27..83bbe669a 100644 --- a/opentreemap/modeling/run_model/Tree.py +++ b/opentreemap/modeling/run_model/Tree.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division class Tree(object): diff --git a/opentreemap/modeling/run_model/schema_helpers.py b/opentreemap/modeling/run_model/schema_helpers.py index 0dfdf5a1b..0b9ced22c 100644 --- a/opentreemap/modeling/run_model/schema_helpers.py +++ b/opentreemap/modeling/run_model/schema_helpers.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + # Helpers for building a JSON schema @@ -18,8 +16,9 @@ def obj(properties, optional_properties={}): return { 'type': 'object', 'additionalProperties': False, - 'required': properties.keys(), - 'properties': dict(properties.items() + optional_properties.items()) + 'required': list(properties.keys()), + 'properties': + dict(list(properties.items()) + list(optional_properties.items())) } diff --git a/opentreemap/modeling/templates/modeling/partials/plans/plans.html b/opentreemap/modeling/templates/modeling/partials/plans/plans.html index 1eaca8a08..d41aef174 100644 --- a/opentreemap/modeling/templates/modeling/partials/plans/plans.html +++ b/opentreemap/modeling/templates/modeling/partials/plans/plans.html @@ -2,7 +2,7 @@ {% load l10n %} {% load humanize %} {% load util %} -{% load staticfiles %} +{% load static %} <div class="plans-backdrop"></div> <div class="plans-content"> diff --git a/opentreemap/modeling/templates/modeling/partials/scenarios/clientside_templates.html b/opentreemap/modeling/templates/modeling/partials/scenarios/clientside_templates.html index 753ce4d31..ef6e5f1a6 100644 --- a/opentreemap/modeling/templates/modeling/partials/scenarios/clientside_templates.html +++ b/opentreemap/modeling/templates/modeling/partials/scenarios/clientside_templates.html @@ -14,7 +14,9 @@ <label>{% trans "Diameter" %}</label> <div class="input-group"> <input class="form-control diameter" type="text" value="<%- tree.diameter() %>"> - <span class="input-group-addon">{{ diameter_units }}</span> + <div class="input-group-append"> + <span class="input-group-text">{{ diameter_units }}</span> + </div> </div> </div> <div class="col-sm-3"> @@ -69,7 +71,9 @@ <label>{% trans "Diameter" %}</label> <div class="input-group"> <input class="form-control diameter" type="text" value="<%- tree.diameter() %>"> - <span class="input-group-addon">{{ diameter_units }}</span> + <div class="input-group-append"> + <span class="input-group-text">{{ diameter_units }}</span> + </div> </div> </div> <div class="col-sm-1"> @@ -200,7 +204,9 @@ <h4><b> value="<%= mortalityRates[i] || 0 %>" data-code="<%= otmCode %>" data-index="<%= i %>" /> - <span class="input-group-addon">%</span> + <div class="input-group-append"> + <span class="input-group-text">%</span> + </div> </div> </td> <% }) %> diff --git a/opentreemap/modeling/templates/modeling/partials/scenarios/sidebar.html b/opentreemap/modeling/templates/modeling/partials/scenarios/sidebar.html index 751627f50..0ca0d249b 100644 --- a/opentreemap/modeling/templates/modeling/partials/scenarios/sidebar.html +++ b/opentreemap/modeling/templates/modeling/partials/scenarios/sidebar.html @@ -47,7 +47,9 @@ <h3 role="button" data-toggle="collapse" href="#tree-mortality-container" aria-e <div class="form-group"> <div class="input-group"> <input type="number" class="form-control" data-category="default"> - <div class="input-group-addon">{% trans "%" %}</div> + <div class="input-group-append"> + <span class="input-group-text">{% trans "%" %}</div> + </div> </div> </div> </div> @@ -87,7 +89,9 @@ <h3 role="button" data-toggle="collapse" href="#replanting-growth-container" ari <div class="col-sm-5" style="padding-left: 0"> <div class="input-group"> <input class="form-control" type="number" value="2"> - <div class="input-group-addon">{% trans "years" %}</div> + <div class="input-group-append"> + <div class="input-group-text:">{% trans "years" %}</div> + </div> </div> </div> </div> diff --git a/opentreemap/modeling/tests.py b/opentreemap/modeling/tests.py index ade817c9b..fa65186a0 100644 --- a/opentreemap/modeling/tests.py +++ b/opentreemap/modeling/tests.py @@ -59,7 +59,7 @@ def _delete_plan(self, user): delete_plan(request, self.instance, self.plan_id) def _assert_plans_match(self, spec, result): - for key, value in spec.iteritems(): + for key, value in spec.items(): self.assertEqual(value, result[key], "Mismatch in plan field '%s'" % key) @@ -207,7 +207,7 @@ def run_model(self, expected_n_trees=1, expected_n_years=1): class TestBisect(SimpleTestCase): def setUp(self): - self.choices = range(10) + self.choices = list(range(10)) self.identity = lambda n: n self.tolerance = 0 self.max_iterations = 15 diff --git a/opentreemap/modeling/urls.py b/opentreemap/modeling/urls.py index 06ece43c9..4741d349c 100644 --- a/opentreemap/modeling/urls.py +++ b/opentreemap/modeling/urls.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import url diff --git a/opentreemap/modeling/views.py b/opentreemap/modeling/views.py index bee6a33b9..1ac190cdd 100644 --- a/opentreemap/modeling/views.py +++ b/opentreemap/modeling/views.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf import settings from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator -from django.core.urlresolvers import reverse +from django.urls import reverse from django.db import transaction from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404 diff --git a/opentreemap/opentreemap/__init__.py b/opentreemap/opentreemap/__init__.py index ae51bd34f..648edb358 100644 --- a/opentreemap/opentreemap/__init__.py +++ b/opentreemap/opentreemap/__init__.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals + from .celery import app as celery_app # NOQA diff --git a/opentreemap/opentreemap/celery.py b/opentreemap/opentreemap/celery.py index 16a317f02..828436437 100644 --- a/opentreemap/opentreemap/celery.py +++ b/opentreemap/opentreemap/celery.py @@ -14,7 +14,8 @@ # Using a string here means the worker will not have to # pickle the object when using Windows. -app.config_from_object('django.conf:settings', namespace='CELERY') +app.config_from_object('django.conf:settings') +#app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks() rollbar_setup = False diff --git a/opentreemap/opentreemap/context_processors.py b/opentreemap/opentreemap/context_processors.py index a48764aaa..ed5e0b94e 100644 --- a/opentreemap/opentreemap/context_processors.py +++ b/opentreemap/opentreemap/context_processors.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import copy from datetime import datetime @@ -26,7 +24,7 @@ def global_settings(request): last_instance = get_last_visited_instance(request) - if hasattr(request, 'user') and request.user.is_authenticated(): + if hasattr(request, 'user') and request.user.is_authenticated: last_effective_instance_user =\ request.user.get_effective_instance_user(last_instance) _update_last_seen(last_effective_instance_user) diff --git a/opentreemap/opentreemap/integrations/__init__.py b/opentreemap/opentreemap/integrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opentreemap/opentreemap/integrations/inaturalist.py b/opentreemap/opentreemap/integrations/inaturalist.py new file mode 100644 index 000000000..965e47056 --- /dev/null +++ b/opentreemap/opentreemap/integrations/inaturalist.py @@ -0,0 +1,343 @@ +import csv +from collections import Counter +import dateutil.parser +import datetime +import time +import logging + +from celery import shared_task, chord +import requests +from django.conf import settings +from django.db import connection +from django.core.cache import cache + +from treemap.models import INaturalistObservation, Species, MapFeaturePhotoLabel, INaturalistPhoto +from treemap.lib.map_feature import get_map_feature_or_404 + +base_url = settings.INATURALIST_URL + + +def get_inaturalist_auth_token(): + + payload = { + 'client_id': settings.INATURALIST_APP_ID, + 'client_secret': settings.INATURALIST_APP_SECRET, + 'grant_type': 'password', + 'username': settings.INATURALIST_USERNAME, + 'password': settings.INATURALIST_PASSWORD + } + + r = requests.post( + url="{base_url}/oauth/token".format(base_url=base_url), + data=payload + ) + token = r.json()['access_token'] + return token + + +def set_observation_to_captive(token, observation_id): + metric = 'wild' + + headers = {'Authorization': 'Bearer {}'.format(token)} + params = { + 'id': observation_id, + 'metric': metric, + 'agree': 'false' + } + + response = requests.post( + url="{base_url}/observations/{observation_id}/quality/{metric}".format( + base_url=base_url, + observation_id=observation_id, + metric=metric), + json=params, + headers=headers + ) + + return response.ok + + +def create_observation(token, latitude, longitude, species): + + headers = {'Authorization': 'Bearer {}'.format(token)} + params = {'observation': { + 'observed_on_string': datetime.datetime.now().isoformat(), + 'latitude': latitude, + 'longitude': longitude, + 'species_guess': species + } + } + + response = requests.post( + url="{base_url}/observations.json".format(base_url=base_url), + json=params, + headers=headers + ) + + observation = response.json()[0] + + set_observation_to_captive(token, observation['id']) + + return observation + + +def add_photo_to_observation(token, observation_id, photo): + + headers = {'Authorization': 'Bearer {}'.format(token)} + data = {'observation_photo[observation_id]': observation_id} + file_data = {'file': photo.image.file.file} + + response = requests.post( + url="{base_url}/observation_photos".format(base_url=base_url), + headers=headers, + data=data, + files=file_data + ) + return response.json() + + +def get_majority_identification(identifications): + """ + Find the majority of an identification + """ + _identifications = [{ + 'user': identification['user']['login'], + 'taxon_id': identification['taxon']['id'], + 'taxon': identification['taxon']['name'] + } for identification in identifications] + + most_common = Counter([i['taxon'] for i in _identifications]).most_common() + (taxon, count) = most_common[0] if most_common else ('', 0) + + users = [i['user'] for i in _identifications if i['taxon'] == taxon] + + return { + 'taxon': taxon, + 'users': ','.join(users), + 'total_count': len(_identifications), + 'count': count + } + + +def is_species_match(taxon_otm, taxon_inaturalist): + """ + Tons of weird mismatches, so we need a mapping + """ + taxon_inaturalist_map = { + 'prunus × yedoensis': 'prunus yedoensis' + } + _taxon_inaturalist = taxon_inaturalist_map.get(taxon_inaturalist, taxon_inaturalist) + return taxon_otm in _taxon_inaturalist or _taxon_inaturalist in taxon_otm + + +def sync_identifications(): + """ + Goes through all unidentified observations and updates them with taxonomy on iNaturalist + """ + o9n_models = INaturalistObservation.objects.filter(is_identified=False) + + observations = get_all_observations([o.observation_id for o in o9n_models]) + id_info = [] + + for o9n_model in o9n_models: + observation = observations.get(o9n_model.observation_id) + if not observation: + continue + + identifications = observation['identifications'] + identification_majority = get_majority_identification(identifications) + if not o9n_model.tree or not o9n_model.tree.species: + continue + + taxon_otm = o9n_model.tree.species.scientific_name + + species_match = is_species_match(taxon_otm.lower(), identification_majority['taxon'].lower()) + + try: + id_info.append({ + 'map_feature_id': o9n_model.map_feature.id, + 'tree_id': o9n_model.tree.id, + 'matches': species_match, + 'species_otm': taxon_otm, + 'species_inaturalist': identification_majority['taxon'], + 'users_majority': identification_majority['users'], + 'sjc_in_majority': 'sustainablejc' in identification_majority['users'], + 'count_identification': len(identifications), + 'count_most_votes': identification_majority['total_count'], + 'num_identification_agreements_inat': observation['num_identification_agreements'], + 'num_identification_disagreements_inat': observation['num_identification_disagreements'], + 'identifications_most_disagree_inat': observation['identifications_most_disagree'], + 'taxons_inaturalist': ','.join([i['taxon']['name'] for i in identifications]) + }) + except Exception as e: + continue + + #_set_identification(o9n_model, taxonomy) + + with open('inaturalist_compare.csv', 'w') as csvfile: + fieldnames = id_info[0].keys() + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(id_info) + + +def get_all_observations(observation_ids): + """ + Retrieve iNaturalist observation by ID + SWAGGER docs: https://api.inaturalist.org/v1 + + We want to retrieve all observations that have at least two observations in agreement + + :param o9n_id: observation ID + :return: observation JSON as a dict + """ + identifications = {} + page = 1 + per_page = 100 + base_url = 'https://api.inaturalist.org/v1' + for observation_batch in [observation_ids[x:x+per_page] for x in range(0, len(observation_ids), per_page)]: + data = requests.get( + url="{base_url}/observations/{ids}".format( + base_url=base_url, + ids=','.join([str(o) for o in observation_batch])), + ).json() + + if (not data): + break + + results = data['results'] + for result in results: + try: + identifications[result['id']] = { + 'identifications': result['identifications'], + 'num_identification_agreements': result['num_identification_agreements'], + 'num_identification_disagreements': result['num_identification_disagreements'], + 'identifications_most_disagree': result['identifications_most_disagree'] + } + except Exception as e: + pass + + time.sleep(5) + + return identifications + + +def get_o9n(o9n_id): + """ + Retrieve iNaturalist observation by ID + API docs: https://www.inaturalist.org/pages/api+reference#get-observations-id + :param o9n_id: observation ID + :return: observation JSON as a dict + """ + return requests.get( + url="{base_url}/observations/{o9n_id}.json".format( + base_url=base_url, o9n_id=o9n_id) + ).json() + + +def _set_identification(o9n_model, taxon): + o9n_model.tree.species = Species(common_name=taxon['taxon']['common_name']['name']) + o9n_model.identified_at = dateutil.parser.parse(taxon['updated_at']) + o9n_model.is_identified = True + o9n_model.save() + + +def get_features_for_inaturalist(tree_id=None): + """ + Get all the features that have a label and can be submitted to iNaturalist + """ + tree_filter_clause = "1=1" + if tree_id: + tree_filter_clause = "t.id = {}".format(tree_id) + + query = """ + SELECT photo.map_feature_id, photo.instance_id, t.id as tree_id + FROM treemap_mapfeaturephoto photo + JOIN treemap_mapfeaturephotolabel label on label.map_feature_photo_id = photo.id + JOIN treemap_tree t on t.plot_id = photo.map_feature_id + LEFT JOIN treemap_inaturalistobservation inat on inat.map_feature_id = photo.map_feature_id + where 1=1 + and inat.id is null + + -- these could be empty tree pits + and t.species_id is not null + + -- we also cannot get the species to dead trees + and coalesce(t.udfs -> 'Condition', '') != 'Dead' + + -- if we should filter a specific tree, do it here + and {} + + group by photo.map_feature_id, photo.instance_id, t.id + having sum(case when label.name = 'shape' then 1 else 0 end) > 0 + and sum(case when label.name = 'bark' then 1 else 0 end) > 0 + and sum(case when label.name = 'leaf' then 1 else 0 end) > 0 + """.format(tree_filter_clause) + + with connection.cursor() as cursor: + # FIXME use parameters for a tree id + cursor.execute(query) + results = cursor.fetchall() + + return [{'feature_id': r[0], + 'instance_id': r[1], + 'tree_id': r[2]} + for r in results] + + +@shared_task() +def create_observations(instance, tree_id=None): + logger = logging.getLogger('iNaturalist') + logger.info('Creating observations') + + features = get_features_for_inaturalist(tree_id) + if not features: + return + + token = get_inaturalist_auth_token() + + for feature in features: + feature = get_map_feature_or_404(feature['feature_id'], instance) + tree = feature.safe_get_current_tree() + + if not tree: + continue + + photos = tree.photos().prefetch_related('mapfeaturephotolabel_set').all() + if len(photos) != 3: + continue + + # we want to submit the leaf first, so sort by leaf + photos = sorted(photos, key=lambda x: 0 if x.has_label('leaf') else 1) + (longitude, latitude) = feature.latlon.coords + + # create the observation + _observation = create_observation( + token, + latitude, + longitude, + tree.species.common_name + ) + observation = INaturalistObservation( + observation_id=_observation['id'], + map_feature=feature, + tree=tree, + submitted_at=datetime.datetime.now() + ) + observation.save() + + for photo in photos: + time.sleep(10) + photo_info = add_photo_to_observation(token, _observation['id'], photo) + + photo_observation = INaturalistPhoto( + tree_photo=photo, + observation=observation, + inaturalist_photo_id=photo_info['photo_id'] + ) + photo_observation.save() + + # let's not get rate limited + time.sleep(30) + + logger.info('Finished creating observations') diff --git a/opentreemap/opentreemap/integrations/tests/__init__.py b/opentreemap/opentreemap/integrations/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opentreemap/opentreemap/integrations/tests/fixtures/__init__.py b/opentreemap/opentreemap/integrations/tests/fixtures/__init__.py new file mode 100644 index 000000000..3839ec09c --- /dev/null +++ b/opentreemap/opentreemap/integrations/tests/fixtures/__init__.py @@ -0,0 +1,15 @@ +import copy +import json + +# API reference: https://www.inaturalist.org/pages/api+reference#get-observations-id +with open('opentreemap/integrations/tests/fixtures/observation.json') as json_file: + _o9n = json.loads(json_file.read()) + + +def get_inaturalist_o9n(o9n_id=None): + o9n_copy = copy.deepcopy(_o9n) + + if o9n_id: + o9n_copy['id'] = o9n_id + + return o9n_copy diff --git a/opentreemap/opentreemap/integrations/tests/fixtures/observation.json b/opentreemap/opentreemap/integrations/tests/fixtures/observation.json new file mode 100644 index 000000000..c579c7dfa --- /dev/null +++ b/opentreemap/opentreemap/integrations/tests/fixtures/observation.json @@ -0,0 +1,301 @@ +{ + "id": 32189837, + "observed_on": "2019-09-05", + "description": null, + "latitude": "42.355992389", + "longitude": "-74.1652624123", + "map_scale": null, + "timeframe": null, + "species_guess": null, + "user_id": 0, + "taxon_id": 48678, + "created_at": "2019-09-05T23:14:55.466Z", + "updated_at": "2019-09-05T23:17:25.894Z", + "place_guess": "Nowhere, NY, US", + "id_please": false, + "observed_on_string": "Thu Sep 05 2019 19:12:11 GMT-0400 (EDT)", + "iconic_taxon_id": 47126, + "num_identification_agreements": 0, + "num_identification_disagreements": 0, + "time_observed_at": "2019-09-05T23:12:11.000Z", + "time_zone": "Eastern Time (US & Canada)", + "location_is_exact": false, + "delta": false, + "positional_accuracy": 30, + "private_latitude": null, + "private_longitude": null, + "private_positional_accuracy": null, + "geoprivacy": null, + "quality_grade": "needs_id", + "positioning_method": null, + "positioning_device": null, + "out_of_range": null, + "license": null, + "uri": "https://www.inaturalist.org/observations/1", + "observation_photos_count": 1, + "comments_count": 0, + "zic_time_zone": "America/New_York", + "oauth_application_id": 3, + "observation_sounds_count": 0, + "identifications_count": 1, + "captive": false, + "community_taxon_id": null, + "site_id": 1, + "old_uuid": null, + "public_positional_accuracy": 30, + "mappable": true, + "cached_votes_total": 0, + "last_indexed_at": "2020-03-22T12:38:47.845Z", + "private_place_guess": null, + "uuid": "b4722d45-eab0-476a-b0ed-e99ac521ec6e", + "taxon_geoprivacy": null, + "user_login": "anonymous", + "iconic_taxon_name": "Plantae", + "captive_flag": false, + "created_at_utc": "2019-09-05T23:14:55.466Z", + "updated_at_utc": "2019-09-05T23:17:25.894Z", + "time_observed_at_utc": "2019-09-05T23:12:11.000Z", + "faves_count": 0, + "owners_identification_from_vision": true, + "user": { + "id": 2132915, + "login": "anonymous", + "name": "Anon", + "observations_count": 44, + "identifications_count": 0, + "user_icon_url": "https://static.inaturalist.org/attachments/users/icons/0/thumb.jpeg?123456789", + "medium_user_icon_url": "https://static.inaturalist.org/attachments/users/icons/0/medium.jpeg?123456789", + "original_user_icon_url": "https://static.inaturalist.org/attachments/users/icons/0/original.jpeg?123456789" + }, + "observation_field_values": [], + "project_observations": [ + { + "id": 31875070, + "project_id": 4034, + "observation_id": 32189837, + "created_at": "2019-09-05T23:30:51.927Z", + "updated_at": "2019-09-05T23:30:51.927Z", + "curator_identification_id": null, + "tracking_code": null, + "user_id": null, + "uuid": "507a7703-b307-498d-bbac-d4dbbb49f06c", + "project": { + "id": 4034, + "title": "New York Wildflower Monitoring Project", + "description": "Much remains to be discovered about the flora of New York and why plants grow where and how they do. This project was initiated in 2015 to expand upon efforts to document plant observations during class field trips. The primary goal was to build an online photographic guide to common flowering plants across New York while exploring ways to connect citizen scientists, improve accuracy in plant monitoring, and identify limitations to implementing monitoring projects. \r\n\r\nIn September 2017, we will no longer be tracking this project regularly; however, any observations of flowering plants in New York will still be automatically added to the project. We appreciate your interest and enthusiasm for learning more about the different plant communities of New York and hope this continues to be a useful educational tool moving forward. \r\n", + "icon_url": "https://static.inaturalist.org/projects/4034-icon-span2.JPG?1505523585" + } + } + ], + "observation_photos": [ + { + "id": 46563675, + "observation_id": 32189837, + "photo_id": 50463281, + "position": 0, + "created_at": "2019-09-05T23:17:25.843Z", + "updated_at": "2019-09-05T23:17:25.843Z", + "old_uuid": null, + "uuid": "eabe8724-ea94-42a6-b2b9-b82a70e8f51c", + "photo": { + "id": 50463281, + "square_url": "https://static.inaturalist.org/photos/50463281/square.jpg?1567725442", + "thumb_url": "https://static.inaturalist.org/photos/50463281/thumb.jpg?1567725442", + "small_url": "https://static.inaturalist.org/photos/50463281/small.jpg?1567725442", + "medium_url": "https://static.inaturalist.org/photos/50463281/medium.jpg?1567725442", + "large_url": "https://static.inaturalist.org/photos/50463281/large.jpg?1567725442", + "created_at": "2019-09-05T23:17:24.222Z", + "updated_at": "2019-09-05T23:17:24.222Z", + "native_page_url": "https://www.inaturalist.org/photos/50463281", + "native_username": "anon", + "license": 0, + "subtype": null, + "native_original_image_url": null, + "uuid": "b1a4ab8b-f0ba-48f7-be5b-4d83d111c610", + "license_code": "C", + "attribution": "(c) Anon, all rights reserved", + "license_name": "Copyright", + "license_url": "http://en.wikipedia.org/wiki/Copyright", + "type": "LocalPhoto" + } + } + ], + "comments": [], + "taxon": { + "id": 48678, + "name": "Solidago", + "rank": "genus", + "source_id": 1, + "created_at": "2008-11-07T06:14:11.000Z", + "updated_at": "2019-12-02T22:14:42.762Z", + "iconic_taxon_id": 47126, + "is_iconic": false, + "name_provider": "ColNameProvider", + "observations_count": 74165, + "listed_taxa_count": 19714, + "rank_level": 20, + "unique_name": "gyldenris", + "wikipedia_summary": "<i><b>Solidago</b></i>, commonly called <b>goldenrods</b>, is a genus of about 100 to 120 species of flowering plants in the aster family, Asteraceae. Most are herbaceous perennial species found in open areas such as meadows, prairies, and savannas. They are mostly native to North America, including Mexico; a few species are native to South America and Eurasia. Some American species have also been introduced into Europe and other parts of the world.", + "wikipedia_title": "", + "ancestry": "48460/47126/211194/47125/47124/47605/47604/632790/461542/972606", + "conservation_status": null, + "conservation_status_source_id": null, + "conservation_status_source_identifier": null, + "is_active": true, + "complete": null, + "complete_rank": null, + "taxon_framework_relationship_id": 316691, + "uuid": "ca854959-47e8-4d7e-b4af-156fc9d00236", + "default_name": { + "id": 221777, + "name": "goldenrods", + "is_valid": true, + "lexicon": "English" + }, + "photo_url": "https://static.inaturalist.org/photos/30546/square.jpg?1545409047", + "iconic_taxon_name": "Plantae", + "conservation_status_name": null, + "image_url": "https://static.inaturalist.org/photos/30546/square.jpg?1545409047", + "common_name": { + "id": 221777, + "name": "goldenrods", + "is_valid": true, + "lexicon": "English" + }, + "taxon_photos": [ + { + "id": 15264, + "taxon_id": 48678, + "photo_id": 30546, + "position": null, + "created_at": null, + "updated_at": null, + "photo": { + "id": 30546, + "user_id": null, + "native_photo_id": "4032129702", + "square_url": "https://static.inaturalist.org/photos/30546/square.jpg?1545409047", + "thumb_url": "https://static.inaturalist.org/photos/30546/thumb.jpg?1545409047", + "small_url": "https://static.inaturalist.org/photos/30546/small.jpg?1545409047", + "medium_url": "https://static.inaturalist.org/photos/30546/medium.jpg?1545409047", + "large_url": "https://static.inaturalist.org/photos/30546/large.jpg?1545409047", + "created_at": "2011-05-02T04:50:39.000Z", + "updated_at": "2018-12-21T16:17:29.590Z", + "native_page_url": "http://www.flickr.com/photos/35478170@N08/4032129702", + "native_username": "Anon", + "native_realname": "Anon", + "license": 5, + "subtype": "FlickrPhoto", + "native_original_image_url": "https://farm3.staticflickr.com/2565/4032129702_b52cdb7ba3_o.jpg", + "uuid": "8a980b17-04b7-46c7-be44-5a02d322809a", + "license_code": "CC-BY-SA", + "attribution": "(c) Anon, some rights reserved (CC BY-SA)", + "license_name": "Creative Commons Attribution-ShareAlike License", + "license_url": "http://creativecommons.org/licenses/by-sa/4.0/", + "type": "LocalPhoto" + } + } + ] + }, + "identifications": [ + { + "id": 69767601, + "observation_id": 32189837, + "taxon_id": 48678, + "user_id": 2132915, + "body": null, + "created_at": "2019-09-05T23:14:55.509Z", + "updated_at": "2019-09-05T23:14:55.509Z", + "current": true, + "taxon_change_id": null, + "category": "leading", + "uuid": "9ad16252-4ac8-4d6b-9781-0ea99f26d07c", + "blind": null, + "previous_observation_taxon_id": 48678, + "disagreement": false, + "user": { + "id": 2132915, + "login": "anon", + "name": "Anon", + "user_icon_url": "https://static.inaturalist.org/attachments/users/icons/0/thumb.jpeg?123456789" + }, + "taxon": { + "id": 48678, + "name": "Solidago", + "rank": "genus", + "source_id": 1, + "created_at": "2008-11-07T06:14:11.000Z", + "updated_at": "2019-12-02T22:14:42.762Z", + "iconic_taxon_id": 47126, + "is_iconic": false, + "name_provider": "ColNameProvider", + "observations_count": 74165, + "listed_taxa_count": 19714, + "rank_level": 20, + "unique_name": "gyldenris", + "wikipedia_summary": "<i><b>Solidago</b></i>, commonly called <b>goldenrods</b>, is a genus of about 100 to 120 species of flowering plants in the aster family, Asteraceae. Most are herbaceous perennial species found in open areas such as meadows, prairies, and savannas. They are mostly native to North America, including Mexico; a few species are native to South America and Eurasia. Some American species have also been introduced into Europe and other parts of the world.", + "wikipedia_title": "", + "ancestry": "48460/47126/211194/47125/47124/47605/47604/632790/461542/972606", + "conservation_status": null, + "conservation_status_source_id": null, + "conservation_status_source_identifier": null, + "is_active": true, + "complete": null, + "complete_rank": null, + "taxon_framework_relationship_id": 316691, + "uuid": "ca854959-47e8-4d7e-b4af-156fc9d00236", + "default_name": { + "id": 221777, + "name": "goldenrods", + "is_valid": true, + "lexicon": "English" + }, + "photo_url": "https://static.inaturalist.org/photos/30546/square.jpg?1545409047", + "iconic_taxon_name": "Plantae", + "conservation_status_name": null, + "image_url": "https://static.inaturalist.org/photos/30546/square.jpg?1545409047", + "common_name": { + "id": 221777, + "name": "goldenrods", + "is_valid": true, + "lexicon": "English" + }, + "taxon_photos": [ + { + "id": 15264, + "taxon_id": 48678, + "photo_id": 30546, + "position": null, + "created_at": null, + "updated_at": null, + "photo": { + "id": 30546, + "user_id": null, + "native_photo_id": "4032129702", + "square_url": "https://static.inaturalist.org/photos/30546/square.jpg?1545409047", + "thumb_url": "https://static.inaturalist.org/photos/30546/thumb.jpg?1545409047", + "small_url": "https://static.inaturalist.org/photos/30546/small.jpg?1545409047", + "medium_url": "https://static.inaturalist.org/photos/30546/medium.jpg?1545409047", + "large_url": "https://static.inaturalist.org/photos/30546/large.jpg?1545409047", + "created_at": "2011-05-02T04:50:39.000Z", + "updated_at": "2018-12-21T16:17:29.590Z", + "native_page_url": "http://www.flickr.com/photos/35478170@N08/4032129702", + "native_username": "Anon", + "native_realname": "Anon", + "license": 5, + "subtype": "FlickrPhoto", + "native_original_image_url": "https://farm3.staticflickr.com/2565/4032129702_b52cdb7ba3_o.jpg", + "uuid": "8a980b17-04b7-46c7-be44-5a02d322809a", + "license_code": "CC-BY-SA", + "attribution": "(c) Anon, some rights reserved (CC BY-SA)", + "license_name": "Creative Commons Attribution-ShareAlike License", + "license_url": "http://creativecommons.org/licenses/by-sa/4.0/", + "type": "LocalPhoto" + } + } + ] + } + } + ], + "faves": [] +} diff --git a/opentreemap/opentreemap/integrations/tests/test_inaturalist.py b/opentreemap/opentreemap/integrations/tests/test_inaturalist.py new file mode 100644 index 000000000..b58dc7785 --- /dev/null +++ b/opentreemap/opentreemap/integrations/tests/test_inaturalist.py @@ -0,0 +1,87 @@ +from django.contrib.gis.geos import Point +from mock import patch + +from opentreemap.integrations import inaturalist +from opentreemap.integrations.tests import fixtures +from treemap.models import MapFeature, INaturalistObservation, Tree, Plot +from treemap.tests.base import OTMTestCase +from treemap.tests import (make_instance, make_commander_user) + + +class TestINaturalist(OTMTestCase): + instance = None + commander_user = None + + GET_O9N_TARGET = 'opentreemap.integrations.inaturalist.get_o9n' + + def setUp(self): + self.instance = make_instance() + self.commander_user = make_commander_user(self.instance) + + def _create_observation(self, o9n_id=32189837, is_identified=False): + plot = Plot(geom=Point(0, 0), instance=self.instance) + plot.save_with_user(self.commander_user) + + tree = Tree(instance=self.instance, plot=plot) + tree.save_with_user(self.commander_user) + + o = INaturalistObservation(is_identified=is_identified, + observation_id=o9n_id, + map_feature=plot, + tree=tree) + o.save() + return o + + def test_no_observations(self): + self.assertEqual(INaturalistObservation.objects.filter(is_identified=False).count(), 0) + + with patch(TestINaturalist.GET_O9N_TARGET) as get_o9n_mock: + inaturalist.sync_identifications() + + get_o9n_mock.assert_not_called() + self.assertEqual(INaturalistObservation.objects.filter(is_identified=False).count(), 0) + + def test_identified(self): + self._create_observation(is_identified=True) + + self.assertEqual(INaturalistObservation.objects.filter(is_identified=False).count(), 0) + + with patch(TestINaturalist.GET_O9N_TARGET) as get_o9n_mock: + inaturalist.sync_identifications() + + get_o9n_mock.assert_not_called() + self.assertEqual(INaturalistObservation.objects.filter(is_identified=False).count(), 0) + + def test_unidentified(self): + o9n_id = 1 + + self._create_observation(o9n_id) + + self.assertEqual(INaturalistObservation.objects.filter(is_identified=False).count(), 1) + + with patch(TestINaturalist.GET_O9N_TARGET, + return_value=fixtures.get_inaturalist_o9n(o9n_id)) as get_o9n_mock: + inaturalist.sync_identifications() + + get_o9n_mock.assert_called_once_with(o9n_id) + self.assertEqual(INaturalistObservation.objects.filter(is_identified=False).count(), 0) + + +class TestINaturalistPost(OTMTestCase): + """ + A set of test cases for writing to iNaturalist + + Uncomment these and fill in blanks for actual testing + + TODO make mock testing better + """ + + def test_set_observation_to_captive(self): + # pick an observation at random for this + token = inaturalist.get_inaturalist_auth_token() + observation_id = 51288919 + + inaturalist.set_observation_to_captive(token, observation_id) + + def test_get_all_observations(self): + data = inaturalist.get_all_observations() diff --git a/opentreemap/opentreemap/settings/default_settings.py b/opentreemap/opentreemap/settings/default_settings.py index 400572b63..743427dc0 100644 --- a/opentreemap/opentreemap/settings/default_settings.py +++ b/opentreemap/opentreemap/settings/default_settings.py @@ -222,6 +222,7 @@ MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -294,6 +295,7 @@ 'django.contrib.postgres', 'django_js_reverse', 'webpack_loader', + 'frontend', ) I18N_APPS = ( @@ -402,13 +404,27 @@ # For django-recaptcha https://github.com/praekelt/django-recaptcha # Setting NOCAPTCHA to True enables v2 -NOCAPTCHA = True +NOCAPTCHA = False +SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error'] if os.environ.get('RECAPTCHA_PUBLIC_KEY', '') != '': # We use an if block here because django-recaptcha will only use a default # test key if these settings are undefined. - RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', None) - RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', None) + #RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', None) + #RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', None) USE_RECAPTCHA = True else: USE_RECAPTCHA = False + + +DEFAULT_INSTANCE = 'JerseyCity' + +INATURALIST_URL = '' + +CORS_ORIGIN_WHITELIST = ( + 'http://localhost:3000', + 'http://localhost:8080' +) + +BROKER_URL = 'redis://localhost:6379/' +CELERY_RESULT_BACKEND = 'redis://localhost:6379/' diff --git a/opentreemap/opentreemap/urls.py b/opentreemap/opentreemap/urls.py index e94ea4f8c..0dd9f164f 100644 --- a/opentreemap/opentreemap/urls.py +++ b/opentreemap/opentreemap/urls.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf import settings from django.conf.urls import include, url from django.contrib import admin -from django.contrib.auth.views import logout +from django.contrib.auth.views import LogoutView from django.views.generic import RedirectView -from django.views.i18n import javascript_catalog +from django.views.i18n import JavaScriptCatalog from treemap import routes from treemap.ecobenefits import within_itree_regions_view @@ -28,7 +26,13 @@ # For URLs included via <app>.urls, see <app>/tests # For "top level" URLs defined here, see treemap/tests/urls.py (RootUrlTests) -urlpatterns = [ +root_url = [] +if hasattr(settings, 'DEFAULT_INSTANCE') and settings.DEFAULT_INSTANCE: + root_url.append(url(r'^$', RedirectView.as_view(url='/{}/ui/'.format(settings.DEFAULT_INSTANCE)))) +else: + root_url.append(url(r'^$', routes.landing_page)) + +urlpatterns = root_url + [ url(r'^robots.txt$', RedirectView.as_view( url='/static/robots.txt', permanent=True)), # Setting permanent=False in case we want to allow customizing favicons @@ -38,19 +42,19 @@ url('^comments/', include('django_comments.urls')), url(r'^', include('geocode.urls')), url(r'^stormwater/', include('stormwater.urls')), - url(r'^$', routes.landing_page), + #url(r'^$', routes.landing_page), url(r'^config/settings.js$', routes.root_settings_js), url(r'^users/%s/$' % USERNAME_PATTERN, routes.user, name='user'), url(r'^users/%s/edits/$' % USERNAME_PATTERN, routes.user_audits, name='user_audits'), - url(r'^users/%s/photo/$' % USERNAME_PATTERN, - routes.upload_user_photo, name='user_photo'), + #url(r'^users/%s/photo/$' % USERNAME_PATTERN, + # routes.upload_user_photo, name='user_photo'), url(r'^api/v(?P<version>\d+)/', include('api.urls')), # The profile view is handled specially by redirecting to # the page of the currently logged in user url(r'^accounts/profile/$', routes.profile_to_user_page, name='profile'), - url(r'^accounts/logout/$', logout, {'next_page': '/'}), + url(r'^accounts/logout/$', LogoutView.as_view(), name='logout'), url(r'^accounts/forgot-username/$', routes.forgot_username, name='forgot_username'), url(r'^accounts/resend-activation-email/$', routes.resend_activation_email, @@ -71,8 +75,9 @@ url(instance_pattern + r'/accounts/register/$', RegistrationView.as_view(), name='instance_registration_register'), + url(instance_pattern + r'/ui/', include('frontend.urls')), url(instance_pattern + r'/', include('treemap.urls')), - url(instance_pattern + r'/importer/', include('importer.urls', + url(instance_pattern + r'/importer/', include(('importer.urls', 'importer'), namespace='importer')), url(instance_pattern + r'/export/', include('exporter.urls')), url(instance_pattern + r'/comments/', include('otm_comments.urls')), @@ -80,6 +85,7 @@ url(r'', include('modeling.urls')), ] + if settings.USE_JS_I18N: js_i18n_info_dict = { 'domain': 'djangojs', @@ -87,7 +93,7 @@ } urlpatterns = [ - url(r'^jsi18n/$', javascript_catalog, js_i18n_info_dict) + url(r'^jsi18n/$', JavaScriptCatalog, js_i18n_info_dict) ] + urlpatterns if settings.EXTRA_URLS: @@ -97,7 +103,7 @@ ] + urlpatterns if settings.DEBUG: - urlpatterns = [url(r'^admin/', include(admin.site.urls))] + urlpatterns + urlpatterns = [url(r'^admin/', admin.site.urls)] + urlpatterns handler404 = 'treemap.routes.error_404_page' handler500 = 'treemap.routes.error_500_page' diff --git a/opentreemap/opentreemap/util.py b/opentreemap/opentreemap/util.py index 8558f4851..56d0bfb04 100644 --- a/opentreemap/opentreemap/util.py +++ b/opentreemap/opentreemap/util.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json import logging @@ -129,7 +127,7 @@ def extent_as_json(extent): def extent_intersection(*extents): - extents = zip(*extents) + extents = list(zip(*extents)) xmins, ymins, xmaxes, ymaxes = extents return (max(xmins), max(ymins), min(xmaxes), min(ymaxes)) diff --git a/opentreemap/otm1_migrator/data_util.py b/opentreemap/otm1_migrator/data_util.py index 29d623c44..484b70d36 100644 --- a/opentreemap/otm1_migrator/data_util.py +++ b/opentreemap/otm1_migrator/data_util.py @@ -71,7 +71,7 @@ def dict_to_model(config, model_name, data_dict, instance): for field in (common_fields .union(renamed_fields) - .union(dependency_fields.values())): + .union(list(dependency_fields.values()))): transform_fn = (config[model_name] .get('value_transformers', {}) .get(field, None)) @@ -85,7 +85,7 @@ def dict_to_model(config, model_name, data_dict, instance): model.udfs[transformed_field[4:]] = transformed_value else: suffix = ('_id' - if transformed_field in dependency_fields.values() + if transformed_field in list(dependency_fields.values()) else '') setattr(model, transformed_field + suffix, transformed_value) @@ -128,7 +128,7 @@ def add_udfs_to_migration_rules(migration_rules, udfs, instance): model_rules = migration_rules[model] model_rules['removed_fields'] -= set(udfs[model].keys()) - for field, field_rules in udfs[model].items(): + for field, field_rules in list(udfs[model].items()): prefixed = 'udf:' + field_rules['udf.name'] model_rules['renamed_fields'][field] = prefixed @@ -143,13 +143,14 @@ def add_udfs_to_migration_rules(migration_rules, udfs, instance): def create_udfs(udfs, instance): - for model, model_rules in udfs.items(): - for field, field_rules in model_rules.items(): + for model, model_rules in list(udfs.items()): + for field, field_rules in list(model_rules.items()): # convert the migrator udf schema # to the udf-lib friendly schema name = field_rules['udf.name'] model_type = to_model_name(model) + import ipdb; ipdb.set_trace() # BREAKPOINT choices = field_rules.get('udf.choices') datatype_type = field_rules.get( 'udf.type', 'choice' if choices else 'string') @@ -162,7 +163,7 @@ def create_udfs(udfs, instance): } if not udf_lib.udf_exists(udf_params, instance): - print "Creating udf %s" % name + print("Creating udf %s" % name) udf_lib.udf_create(udf_params, instance) diff --git a/opentreemap/otm1_migrator/management/commands/perform_migration.py b/opentreemap/otm1_migrator/management/commands/perform_migration.py index 5ead6598f..e408b64fb 100644 --- a/opentreemap/otm1_migrator/management/commands/perform_migration.py +++ b/opentreemap/otm1_migrator/management/commands/perform_migration.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import os import importlib @@ -48,10 +46,9 @@ def save_objects(migration_rules, model_name, model_dicts, relic_ids, if dict['pk'] not in model_key_map) for model_dict in dicts_to_save: - dependencies = (migration_rules + dependencies = (list(migration_rules .get(model_name, {}) - .get('dependencies', {}) - .items()) + .get('dependencies', {}).items())) old_model_dict = model_dict.copy() old_model_dict['fields'] = model_dict['fields'].copy() @@ -145,7 +142,7 @@ def handle(self, *args, **options): if options['config_file']: config_data = json.load(open(options['config_file'], 'r')) - for k, v in config_data.items(): + for k, v in list(config_data.items()): if not options.get(k, None): options[k] = v diff --git a/opentreemap/otm1_migrator/management/commands/post_migrate_validation.py b/opentreemap/otm1_migrator/management/commands/post_migrate_validation.py index 1f9b660cc..7c6b54035 100644 --- a/opentreemap/otm1_migrator/management/commands/post_migrate_validation.py +++ b/opentreemap/otm1_migrator/management/commands/post_migrate_validation.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf import settings from django.contrib.contenttypes.models import ContentType diff --git a/opentreemap/otm1_migrator/migration_rules/tampa.py b/opentreemap/otm1_migrator/migration_rules/tampa.py index ff032869d..77b65b07b 100644 --- a/opentreemap/otm1_migrator/migration_rules/tampa.py +++ b/opentreemap/otm1_migrator/migration_rules/tampa.py @@ -62,9 +62,9 @@ def create_override(species_obj, species_dict): itree_code = species_dict['fields'].get('itree_code', None) if not itree_code: sci_name = species_dict['fields'].get('scientific_name', '').lower() - print('No itree_code for "%d: %s"' % (species_dict['pk'], sci_name)) + print(('No itree_code for "%d: %s"' % (species_dict['pk'], sci_name))) itree_code = meta_species.get(sci_name, '') - print('Looked up meta species "%s"' % itree_code) + print(('Looked up meta species "%s"' % itree_code)) override = ITreeCodeOverride( instance_species_id=species_obj.pk, region=ITreeRegion.objects.get(code=TAMPA_ITREE_REGION_CODE), diff --git a/opentreemap/otm1_migrator/migrations/0001_initial.py b/opentreemap/otm1_migrator/migrations/0001_initial.py index ba310284a..57320c953 100644 --- a/opentreemap/otm1_migrator/migrations/0001_initial.py +++ b/opentreemap/otm1_migrator/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/otm1_migrator/migrations/0002_auto_20150630_1556.py b/opentreemap/otm1_migrator/migrations/0002_auto_20150630_1556.py index 79aa14a21..fb30e9824 100644 --- a/opentreemap/otm1_migrator/migrations/0002_auto_20150630_1556.py +++ b/opentreemap/otm1_migrator/migrations/0002_auto_20150630_1556.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations @@ -15,32 +15,32 @@ class Migration(migrations.Migration): migrations.AddField( model_name='otm1userrelic', name='instance', - field=models.ForeignKey(to='treemap.Instance'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance'), ), migrations.AddField( model_name='otm1userrelic', name='migration_event', - field=models.ForeignKey(blank=True, to='otm1_migrator.MigrationEvent', null=True), + field=models.ForeignKey(on_delete=models.CASCADE, blank=True, to='otm1_migrator.MigrationEvent', null=True), ), migrations.AddField( model_name='otm1modelrelic', name='instance', - field=models.ForeignKey(to='treemap.Instance'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance'), ), migrations.AddField( model_name='otm1modelrelic', name='migration_event', - field=models.ForeignKey(blank=True, to='otm1_migrator.MigrationEvent', null=True), + field=models.ForeignKey(on_delete=models.CASCADE, blank=True, to='otm1_migrator.MigrationEvent', null=True), ), migrations.AddField( model_name='otm1commentrelic', name='instance', - field=models.ForeignKey(to='treemap.Instance'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance'), ), migrations.AddField( model_name='otm1commentrelic', name='migration_event', - field=models.ForeignKey(blank=True, to='otm1_migrator.MigrationEvent', null=True), + field=models.ForeignKey(on_delete=models.CASCADE, blank=True, to='otm1_migrator.MigrationEvent', null=True), ), migrations.AlterUniqueTogether( name='otm1userrelic', diff --git a/opentreemap/otm1_migrator/model_processors.py b/opentreemap/otm1_migrator/model_processors.py index 47aa21d71..fc863205f 100644 --- a/opentreemap/otm1_migrator/model_processors.py +++ b/opentreemap/otm1_migrator/model_processors.py @@ -1,11 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import os import pytz -from exceptions import NotImplementedError from django.db.transaction import atomic from django.contrib.contenttypes.models import ContentType @@ -140,7 +137,7 @@ def process_userprofile(migration_rules, migration_event, photo_full_path = os.path.join(photo_basepath, photo_path) try: - photo_data = open(photo_full_path) + photo_data = open(photo_full_path, 'rb') except IOError: print("Failed to read photo %s ... SKIPPING USER %s %s" % (photo_full_path, user.id, user.username)) @@ -166,7 +163,7 @@ def save_treephoto(migration_rules, migration_event, treephoto_path, pk = models.UNBOUND_MODEL_ID else: image = open(os.path.join(treephoto_path, - model_dict['fields']['photo'])) + model_dict['fields']['photo']), 'rb') treephoto_obj.set_image(image) treephoto_obj.map_feature_id = (Tree .objects diff --git a/opentreemap/otm1_migrator/models.py b/opentreemap/otm1_migrator/models.py index 65d5914c8..76dff05c4 100644 --- a/opentreemap/otm1_migrator/models.py +++ b/opentreemap/otm1_migrator/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.core.exceptions import MultipleObjectsReturned @@ -27,9 +25,9 @@ class MigrationEvent(models.Model): class AbstractRelic(models.Model): - migration_event = models.ForeignKey(MigrationEvent, + migration_event = models.ForeignKey(MigrationEvent, on_delete=models.CASCADE, null=True, blank=True) - instance = models.ForeignKey(Instance) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) otm1_model_id = models.IntegerField() otm2_model_id = models.IntegerField() diff --git a/opentreemap/otm1_migrator/tests.py b/opentreemap/otm1_migrator/tests.py index 26c0fca4c..19a1d7c2a 100644 --- a/opentreemap/otm1_migrator/tests.py +++ b/opentreemap/otm1_migrator/tests.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json from copy import copy diff --git a/opentreemap/otm1_migrator/views.py b/opentreemap/otm1_migrator/views.py index e434f1c9e..01c03573b 100644 --- a/opentreemap/otm1_migrator/views.py +++ b/opentreemap/otm1_migrator/views.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import csv @@ -10,7 +8,7 @@ from treemap.util import get_csv_response from treemap.models import User -from models import OTM1UserRelic +from .models import OTM1UserRelic # assumptions: # * there are n userrelics and m users, such that n >= m diff --git a/opentreemap/otm_comments/__init__.py b/opentreemap/otm_comments/__init__.py index b6a6aa870..b0795e0cf 100644 --- a/opentreemap/otm_comments/__init__.py +++ b/opentreemap/otm_comments/__init__.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division # The contrib.comments app will load whichever app is specified in the settings diff --git a/opentreemap/otm_comments/forms.py b/opentreemap/otm_comments/forms.py index 4b005cae3..83c73b28a 100644 --- a/opentreemap/otm_comments/forms.py +++ b/opentreemap/otm_comments/forms.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from threadedcomments.forms import ThreadedCommentForm diff --git a/opentreemap/otm_comments/migrations/0001_initial.py b/opentreemap/otm_comments/migrations/0001_initial.py index 28abb58b7..306f54021 100644 --- a/opentreemap/otm_comments/migrations/0001_initial.py +++ b/opentreemap/otm_comments/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations import treemap.audit @@ -15,7 +15,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='EnhancedThreadedComment', fields=[ - ('threadedcomment_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='threadedcomments.ThreadedComment')), + ('threadedcomment_ptr', models.OneToOneField(on_delete=models.CASCADE, parent_link=True, auto_created=True, primary_key=True, serialize=False, to='threadedcomments.ThreadedComment')), ('is_archived', models.BooleanField(default=False)), ], options={ @@ -29,7 +29,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('flagged_at', models.DateTimeField(auto_now_add=True)), ('hidden', models.BooleanField(default=False)), - ('comment', models.ForeignKey(to='otm_comments.EnhancedThreadedComment')), + ('comment', models.ForeignKey(on_delete=models.CASCADE, to='otm_comments.EnhancedThreadedComment')), ], bases=(models.Model, treemap.audit.Auditable), ), diff --git a/opentreemap/otm_comments/migrations/0002_auto_20150630_1556.py b/opentreemap/otm_comments/migrations/0002_auto_20150630_1556.py index ae1bdac14..c4b63190a 100644 --- a/opentreemap/otm_comments/migrations/0002_auto_20150630_1556.py +++ b/opentreemap/otm_comments/migrations/0002_auto_20150630_1556.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations from django.conf import settings @@ -17,11 +17,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='enhancedthreadedcommentflag', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='enhancedthreadedcomment', name='instance', - field=models.ForeignKey(to='treemap.Instance'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance'), ), ] diff --git a/opentreemap/otm_comments/migrations/0003_auto_20160923_1413.py b/opentreemap/otm_comments/migrations/0003_auto_20160923_1413.py index da9a256d3..b5db494d9 100644 --- a/opentreemap/otm_comments/migrations/0003_auto_20160923_1413.py +++ b/opentreemap/otm_comments/migrations/0003_auto_20160923_1413.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/otm_comments/migrations/0004_auto_20170907_0937.py b/opentreemap/otm_comments/migrations/0004_auto_20170907_0937.py index 020d51e6b..b563a28e6 100644 --- a/opentreemap/otm_comments/migrations/0004_auto_20170907_0937.py +++ b/opentreemap/otm_comments/migrations/0004_auto_20170907_0937.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.4 on 2017-09-07 14:37 -from __future__ import unicode_literals + from django.db import migrations diff --git a/opentreemap/otm_comments/models.py b/opentreemap/otm_comments/models.py index 6684053bf..b2bbb50ef 100644 --- a/opentreemap/otm_comments/models.py +++ b/opentreemap/otm_comments/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from threadedcomments.models import ThreadedComment @@ -21,7 +19,7 @@ class EnhancedThreadedComment(ThreadedComment): # We could retrieve this through the GenericForeignKey on ThreadedComment, # but it makes things simpler to record instance here. - instance = models.ForeignKey('treemap.Instance') + instance = models.ForeignKey('treemap.Instance', on_delete=models.CASCADE) @property def is_flagged(self): @@ -58,8 +56,8 @@ class Meta: class EnhancedThreadedCommentFlag(models.Model, Auditable): - comment = models.ForeignKey(EnhancedThreadedComment) - user = models.ForeignKey(settings.AUTH_USER_MODEL) + comment = models.ForeignKey(EnhancedThreadedComment, on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) flagged_at = models.DateTimeField(auto_now_add=True) # whether the flag itself was hidden, NOT the related # comment. That is decided by `comment.is_removed` diff --git a/opentreemap/otm_comments/tests.py b/opentreemap/otm_comments/tests.py index 829a20083..0047f6c18 100644 --- a/opentreemap/otm_comments/tests.py +++ b/opentreemap/otm_comments/tests.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from datetime import datetime, timedelta diff --git a/opentreemap/otm_comments/uitests.py b/opentreemap/otm_comments/uitests.py index 86e672452..f9fdc3217 100644 --- a/opentreemap/otm_comments/uitests.py +++ b/opentreemap/otm_comments/uitests.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from time import sleep from selenium.webdriver.support.wait import WebDriverWait -from django.core.urlresolvers import reverse +from django.urls import reverse from treemap.instance import create_stewardship_udfs from treemap.tests.ui import TreemapUITestCase diff --git a/opentreemap/otm_comments/urls.py b/opentreemap/otm_comments/urls.py index 0465a37de..609b036f8 100644 --- a/opentreemap/otm_comments/urls.py +++ b/opentreemap/otm_comments/urls.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import url diff --git a/opentreemap/otm_comments/views.py b/opentreemap/otm_comments/views.py index a868e7cd5..3456e8a22 100644 --- a/opentreemap/otm_comments/views.py +++ b/opentreemap/otm_comments/views.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from functools import partial diff --git a/opentreemap/registration_backend/urls.py b/opentreemap/registration_backend/urls.py index 5f3958b07..b2e816e99 100644 --- a/opentreemap/registration_backend/urls.py +++ b/opentreemap/registration_backend/urls.py @@ -1,15 +1,18 @@ from django.conf.urls import include from django.conf.urls import url -from django.contrib.auth.views import login +from django.urls import path +from django.contrib.auth import login +from django.contrib.auth import views as auth_views from django.views.generic.base import TemplateView -from views import (RegistrationView, ActivationView, LoginForm, - PasswordResetView) +from .views import (RegistrationView, ActivationView, LoginForm, + PasswordResetView) urlpatterns = [ - url(r'^login/$', login, {'authentication_form': LoginForm}, name='login'), + path(r'^login/$', auth_views.LoginView.as_view( + authentication_form=LoginForm), name='login'), url(r'^activation-complete/$', TemplateView.as_view(template_name='registration/activation_complete.html'), # NOQA name='registration_activation_complete'), diff --git a/opentreemap/registration_backend/views.py b/opentreemap/registration_backend/views.py index 30c977263..9155f62ad 100644 --- a/opentreemap/registration_backend/views.py +++ b/opentreemap/registration_backend/views.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django import forms from django.core.exceptions import ValidationError from django.conf import settings from django.contrib.auth.forms import AuthenticationForm from django.utils.translation import ugettext_lazy as _ -from django.core.urlresolvers import reverse +from django.urls import reverse from django.contrib.sites.requests import RequestSite from django.contrib.auth.views import\ PasswordResetView as DefaultPasswordResetView @@ -32,7 +30,7 @@ class LoginForm(AuthenticationForm): def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) - for field_name, field in self.fields.items(): + for field_name, field in list(self.fields.items()): field.widget.attrs['class'] = 'input-xlarge form-control' @@ -74,7 +72,7 @@ def __init__(self, *args, **kwargs): self.fields['email'].label = _('Email') self.fields['password2'].label = _('Confirm Password') - for field_name, field in self.fields.items(): + for field_name, field in list(self.fields.items()): if not isinstance(field, forms.BooleanField): field.widget.attrs['class'] = 'form-control' diff --git a/opentreemap/stderr.txt b/opentreemap/stderr.txt new file mode 100644 index 000000000..8e7ca1045 --- /dev/null +++ b/opentreemap/stderr.txt @@ -0,0 +1,76 @@ +Creating test database for alias 'default'... +Got an error creating the test database: database "test_otm" already exists + +Traceback (most recent call last): + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/utils.py", line 82, in _execute + return self.cursor.execute(sql) +psycopg2.errors.DuplicateDatabase: database "test_otm" already exists + + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/base/creation.py", line 188, in _create_test_db + self._execute_create_test_db(cursor, test_db_params, keepdb) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/postgresql/creation.py", line 42, in _execute_create_test_db + super()._execute_create_test_db(cursor, parameters, keepdb) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/base/creation.py", line 174, in _execute_create_test_db + cursor.execute('CREATE DATABASE %(dbname)s %(suffix)s' % parameters) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/utils.py", line 66, in execute + return self._execute_with_wrappers(sql, params, many=False, executor=self._execute) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers + return executor(sql, params, many, context) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute + return self.cursor.execute(sql, params) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/utils.py", line 90, in __exit__ + raise dj_exc_value.with_traceback(traceback) from exc_value + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/utils.py", line 82, in _execute + return self.cursor.execute(sql) +django.db.utils.ProgrammingError: database "test_otm" already exists + + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "manage.py", line 10, in <module> + execute_from_command_line(sys.argv) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line + utility.execute() + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/core/management/__init__.py", line 395, in execute + self.fetch_command(subcommand).run_from_argv(self.argv) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/core/management/commands/test.py", line 23, in run_from_argv + super().run_from_argv(argv) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/core/management/base.py", line 330, in run_from_argv + self.execute(*args, **cmd_options) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/core/management/base.py", line 371, in execute + output = self.handle(*args, **options) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/core/management/commands/test.py", line 53, in handle + failures = test_runner.run_tests(test_labels) + File "/home/tzinckgraf/code/opentreemap/otm-core-python3/otm-core/opentreemap/treemap/tests/__init__.py", line 33, in run_tests + return super(OTM2TestRunner, self).run_tests(*args, **kwargs) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/test/runner.py", line 695, in run_tests + old_config = self.setup_databases(aliases=databases) + File "/home/tzinckgraf/code/opentreemap/otm-core-python3/otm-core/opentreemap/treemap/tests/__init__.py", line 38, in setup_databases + result = super(OTM2TestRunner, self).setup_databases(*args, **kwargs) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/test/runner.py", line 614, in setup_databases + return _setup_databases( + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/test/utils.py", line 170, in setup_databases + connection.creation.create_test_db( + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/base/creation.py", line 55, in create_test_db + self._create_test_db(verbosity, autoclobber, keepdb) + File "/home/tzinckgraf/.local/share/virtualenvs/otm-core-python3-ZpRnQKto/lib/python3.8/site-packages/django/db/backends/base/creation.py", line 197, in _create_test_db + confirm = input( + File "/usr/lib/python3.8/unittest/signals.py", line 36, in __call__ + self.default_handler(signum, frame) +KeyboardInterrupt + +If you suspect this is an IPython 7.19.0 bug, please report it at: + https://github.com/ipython/ipython/issues +or send an email to the mailing list at ipython-dev@python.org + +You can print a more detailed traceback right now with "%tb", or use "%debug" +to interactively debug it. + +Extra-detailed tracebacks for bug-reporting purposes can be enabled via: + %config Application.verbose_crash=True + diff --git a/opentreemap/stdout.txt b/opentreemap/stdout.txt new file mode 100644 index 000000000..aafbace04 --- /dev/null +++ b/opentreemap/stdout.txt @@ -0,0 +1 @@ +Type 'yes' if you would like to try deleting the test database 'test_otm', or 'no' to cancel: \ No newline at end of file diff --git a/opentreemap/stormwater/benefits.py b/opentreemap/stormwater/benefits.py index 21fb4afd6..aa59666b9 100644 --- a/opentreemap/stormwater/benefits.py +++ b/opentreemap/stormwater/benefits.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.db.models import Sum from django.utils.translation import ugettext_lazy as _ diff --git a/opentreemap/stormwater/migrations/0001_initial.py b/opentreemap/stormwater/migrations/0001_initial.py index 2f66cada4..665e6ada4 100644 --- a/opentreemap/stormwater/migrations/0001_initial.py +++ b/opentreemap/stormwater/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations import django.contrib.gis.db.models.fields @@ -15,7 +15,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='PolygonalMapFeature', fields=[ - ('mapfeature_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='treemap.MapFeature')), + ('mapfeature_ptr', models.OneToOneField(on_delete=models.CASCADE, parent_link=True, auto_created=True, primary_key=True, serialize=False, to='treemap.MapFeature')), ('polygon', django.contrib.gis.db.models.fields.MultiPolygonField(srid=3857)), ], options={ @@ -26,7 +26,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Bioswale', fields=[ - ('polygonalmapfeature_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='stormwater.PolygonalMapFeature')), + ('polygonalmapfeature_ptr', models.OneToOneField(on_delete=models.CASCADE, parent_link=True, auto_created=True, primary_key=True, serialize=False, to='stormwater.PolygonalMapFeature')), ], options={ 'abstract': False, diff --git a/opentreemap/stormwater/migrations/0002_raingarden.py b/opentreemap/stormwater/migrations/0002_raingarden.py index ba1789614..edc0fc4fb 100644 --- a/opentreemap/stormwater/migrations/0002_raingarden.py +++ b/opentreemap/stormwater/migrations/0002_raingarden.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations @@ -14,7 +14,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='RainGarden', fields=[ - ('polygonalmapfeature_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='stormwater.PolygonalMapFeature')), + ('polygonalmapfeature_ptr', models.OneToOneField(on_delete=models.CASCADE, parent_link=True, auto_created=True, primary_key=True, serialize=False, to='stormwater.PolygonalMapFeature')), ], options={ 'abstract': False, diff --git a/opentreemap/stormwater/migrations/0003_rainbarrel.py b/opentreemap/stormwater/migrations/0003_rainbarrel.py index c43ed9195..82f74f01a 100644 --- a/opentreemap/stormwater/migrations/0003_rainbarrel.py +++ b/opentreemap/stormwater/migrations/0003_rainbarrel.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations @@ -15,7 +15,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='RainBarrel', fields=[ - ('mapfeature_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='treemap.MapFeature')), + ('mapfeature_ptr', models.OneToOneField(on_delete=models.CASCADE, parent_link=True, auto_created=True, primary_key=True, serialize=False, to='treemap.MapFeature')), ('capacity', models.FloatField(help_text='Capacity')), ], options={ diff --git a/opentreemap/stormwater/migrations/0004_auto_20151021_1600.py b/opentreemap/stormwater/migrations/0004_auto_20151021_1600.py index 305b6921d..235632388 100644 --- a/opentreemap/stormwater/migrations/0004_auto_20151021_1600.py +++ b/opentreemap/stormwater/migrations/0004_auto_20151021_1600.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/stormwater/migrations/0005_help_text_to_verbose_name.py b/opentreemap/stormwater/migrations/0005_help_text_to_verbose_name.py index 404de2764..c1a8cdefb 100644 --- a/opentreemap/stormwater/migrations/0005_help_text_to_verbose_name.py +++ b/opentreemap/stormwater/migrations/0005_help_text_to_verbose_name.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/stormwater/migrations/0006_stormwater_drainage_area.py b/opentreemap/stormwater/migrations/0006_stormwater_drainage_area.py index 90cc108e6..93861db6a 100644 --- a/opentreemap/stormwater/migrations/0006_stormwater_drainage_area.py +++ b/opentreemap/stormwater/migrations/0006_stormwater_drainage_area.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/stormwater/migrations/0007_drainage_area_permissions.py b/opentreemap/stormwater/migrations/0007_drainage_area_permissions.py index 0d82c0b65..d14ccbeab 100644 --- a/opentreemap/stormwater/migrations/0007_drainage_area_permissions.py +++ b/opentreemap/stormwater/migrations/0007_drainage_area_permissions.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations from django.db.models import F diff --git a/opentreemap/stormwater/migrations/0008_benefits-calc-cache-flush.py b/opentreemap/stormwater/migrations/0008_benefits-calc-cache-flush.py index 728234197..6f9001830 100644 --- a/opentreemap/stormwater/migrations/0008_benefits-calc-cache-flush.py +++ b/opentreemap/stormwater/migrations/0008_benefits-calc-cache-flush.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations from django.db.models import F diff --git a/opentreemap/stormwater/migrations/0009_drainage_area_imperial_units.py b/opentreemap/stormwater/migrations/0009_drainage_area_imperial_units.py index 13d6257e2..2b4b53c3a 100644 --- a/opentreemap/stormwater/migrations/0009_drainage_area_imperial_units.py +++ b/opentreemap/stormwater/migrations/0009_drainage_area_imperial_units.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations from django.db.models import F diff --git a/opentreemap/stormwater/migrations/0010_stormwater_blank_true.py b/opentreemap/stormwater/migrations/0010_stormwater_blank_true.py index 529d440e5..7125ac49e 100644 --- a/opentreemap/stormwater/migrations/0010_stormwater_blank_true.py +++ b/opentreemap/stormwater/migrations/0010_stormwater_blank_true.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/stormwater/models.py b/opentreemap/stormwater/models.py index d3a753934..e3aafff8d 100644 --- a/opentreemap/stormwater/models.py +++ b/opentreemap/stormwater/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.contrib.gis.db import models from django.db import connection @@ -12,6 +10,9 @@ from treemap.models import MapFeature, ValidationMixin from treemap.ecobenefits import CountOnlyBenefitCalculator +# these are all built-in directly to Django +from django.db.models import Manager as GeoManager + class PolygonalMapFeature(MapFeature): area_field_name = 'polygon' @@ -19,7 +20,7 @@ class PolygonalMapFeature(MapFeature): polygon = models.MultiPolygonField(srid=3857) - objects = models.GeoManager() + objects = GeoManager() @classproperty def always_writable(cls): @@ -81,7 +82,7 @@ def calculate_area(self): class Bioswale(PolygonalMapFeature, ValidationMixin): - objects = models.GeoManager() + objects = GeoManager() drainage_area = models.FloatField( null=True, blank=True, @@ -143,7 +144,7 @@ def clean(self): class RainGarden(PolygonalMapFeature, ValidationMixin): - objects = models.GeoManager() + objects = GeoManager() drainage_area = models.FloatField( null=True, blank=True, @@ -205,7 +206,7 @@ def clean(self): class RainBarrel(MapFeature): - objects = models.GeoManager() + objects = GeoManager() capacity = models.FloatField( verbose_name=_("Capacity"), error_messages={'invalid': _("Please enter a number.")}) diff --git a/opentreemap/stormwater/routes.py b/opentreemap/stormwater/routes.py index ec859e049..ab4bff834 100644 --- a/opentreemap/stormwater/routes.py +++ b/opentreemap/stormwater/routes.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django_tinsel.decorators import json_api_call, route from django_tinsel.utils import decorate as do diff --git a/opentreemap/stormwater/tests.py b/opentreemap/stormwater/tests.py index 538b30bcc..9f34eff74 100644 --- a/opentreemap/stormwater/tests.py +++ b/opentreemap/stormwater/tests.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from treemap.ecobenefits import (FEET_SQ_PER_METER_SQ, FEET_PER_INCH, GALLONS_PER_CUBIC_FT) diff --git a/opentreemap/stormwater/urls.py b/opentreemap/stormwater/urls.py index 3a05da2e1..db770966c 100644 --- a/opentreemap/stormwater/urls.py +++ b/opentreemap/stormwater/urls.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import url diff --git a/opentreemap/stormwater/views.py b/opentreemap/stormwater/views.py index 85a65d1ae..69aa79a45 100644 --- a/opentreemap/stormwater/views.py +++ b/opentreemap/stormwater/views.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf import settings from django.contrib.gis.geos import Point from django.contrib.gis.measure import D +from django.contrib.gis.db.models.functions import Distance from django_tinsel.exceptions import HttpBadRequestException @@ -24,7 +23,8 @@ def polygon_for_point(request, instance): raise HttpBadRequestException( 'The distance parameter must be a number') - features = PolygonalMapFeature.objects.distance(point)\ + features = PolygonalMapFeature.objects\ + .annotate(distance=Distance('geom', point))\ .filter(instance=instance)\ .filter(polygon__distance_lte=(point, D(m=distance)))\ .order_by('distance')[0:1] diff --git a/opentreemap/test_result.txt b/opentreemap/test_result.txt new file mode 100644 index 000000000..299862464 --- /dev/null +++ b/opentreemap/test_result.txt @@ -0,0 +1,2 @@ +Installed 16 object(s) from 1 fixture(s) + diff --git a/opentreemap/treemap/DotDict.py b/opentreemap/treemap/DotDict.py index 8dce65070..d48521ecc 100644 --- a/opentreemap/treemap/DotDict.py +++ b/opentreemap/treemap/DotDict.py @@ -41,10 +41,30 @@ def __setitem__(self, key, value): dict.__setitem__(self, key, value) def __getitem__(self, key): + # An implementation note about the try blocks below. + + # Django 1.11.17 calls `hasattr(a_dotdict_field, 'resolve_expression')` + # https://bit.ly/2ZWLoWF + + # The Python 3 documentation says that this ends up calling `getattr` + # and returning False if an `AttributeError` is raised + # https://docs.python.org/3/library/functions.html#hasattr + + # For dictionaries, `getattr` ends up calling `__getitem__`. Our + # implementation of `__getitem__` correctly raises KeyError, and we + # were relying on a bug in Python 2 where any exception raised during a + # `hasattr` check would result in False. Now that Python 3 explicitly + # checks for `AttributeError` we need this workaround if '.' not in key: - return dict.__getitem__(self, key) + try: + return dict.__getitem__(self, key) + except KeyError as ke: + raise AttributeError(ke) myKey, restOfKey = key.split('.', 1) - target = dict.__getitem__(self, myKey) + try: + target = dict.__getitem__(self, myKey) + except KeyError as ke: + raise AttributeError(ke) self._ensure_dot_dict(target, restOfKey, myKey) return target[restOfKey] diff --git a/opentreemap/treemap/admin.py b/opentreemap/treemap/admin.py index 5f3707b05..f94bec415 100644 --- a/opentreemap/treemap/admin.py +++ b/opentreemap/treemap/admin.py @@ -2,8 +2,8 @@ from django.contrib.auth import models as auth_models from django.contrib.auth.signals import user_logged_in -import models -import udf +from . import models +from . import udf user_logged_in.disconnect(auth_models.update_last_login) diff --git a/opentreemap/treemap/audit.py b/opentreemap/treemap/audit.py index fa59df7ff..1aa0bd9b3 100644 --- a/opentreemap/treemap/audit.py +++ b/opentreemap/treemap/audit.py @@ -1,6 +1,4 @@ -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json import hashlib @@ -17,7 +15,7 @@ from django.dispatch import receiver from django.db import models as django_models from django.db.models.signals import post_save, post_delete -from django.db.models.fields import FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import IntegrityError, connection, transaction from django.conf import settings @@ -84,7 +82,7 @@ def _reserve_model_id(model_class): cursor.execute("select nextval('%s');" % id_seq_name) results = cursor.fetchone() model_id = results[0] - assert(type(model_id) in [int, long]) + assert(type(model_id) in [int, int]) except: msg = "There was a database error while retrieving a unique audit ID." raise IntegrityError(msg) @@ -106,7 +104,7 @@ def _reserve_model_id_range(model_class, num): {'seq': id_seq_name, 'num': num}) model_ids = [row[0] for row in cursor] - assert(type(model_id) in [int, long] for model_id in model_ids) + assert(type(model_id) in [int, int] for model_id in model_ids) except: msg = "There was a database error while retrieving a unique audit ID." raise IntegrityError(msg) @@ -530,7 +528,7 @@ def as_dict(self): @property def hash(self): - values = ['%s:%s' % (k, v) for (k, v) in self.as_dict().iteritems()] + values = ['%s:%s' % (k, v) for (k, v) in self.as_dict().items()] string = '|'.join(values).encode('utf-8') return hashlib.md5(string).hexdigest() @@ -583,7 +581,12 @@ def apply_change(self, key, orig_value): # TODO: if a field has a default value, don't # set the original value when the original value # is none, set it to the default value of the field. - setattr(self, key, orig_value) + try: + setattr(self, key, orig_value) + except AttributeError: + # if we hit this exception, then we are trying to set a property + # so we can skip it + pass @classproperty def do_not_track(cls): @@ -608,12 +611,12 @@ def tracked_fields(self): def _direct_updates(self, updates, user): pending_fields = self.get_pending_fields(user) - return {key: val for key, val in updates.iteritems() + return {key: val for key, val in updates.items() if key not in pending_fields} def _pending_updates(self, updates, user): pending_fields = self.get_pending_fields(user) - return {key: val for key, val in updates.iteritems() + return {key: val for key, val in updates.items() if key in pending_fields} def _updated_fields(self): @@ -663,12 +666,12 @@ def is_user_administrator(self, user): instance = self.get_instance() except AuthorizeException: return False - if not user or not user.is_authenticated(): + if not user or not user.is_authenticated: return False return user.get_role(instance).name == Role.ADMINISTRATOR def fields(self): - return self.as_dict().keys() + return list(self.as_dict().keys()) def get_previous_state(self): return self._previous_state @@ -687,7 +690,7 @@ def populate_previous_state(self): # "initial" state is empty so we clear it here self.clear_previous_state() else: - self._previous_state = {k: v for k, v in self.as_dict().iteritems() + self._previous_state = {k: v for k, v in self.as_dict().items() if k not in self._do_not_track} def get_pending_fields(self, user=None): @@ -708,8 +711,8 @@ def get_pending_fields(self, user=None): class FieldPermission(models.Model): model_name = models.CharField(max_length=255) field_name = models.CharField(max_length=255) - role = models.ForeignKey('Role') - instance = models.ForeignKey('Instance') + role = models.ForeignKey('Role', on_delete=models.CASCADE) + instance = models.ForeignKey('Instance', on_delete=models.CASCADE) NONE = 0 READ_ONLY = 1 @@ -725,7 +728,7 @@ class FieldPermission(models.Model): class Meta: unique_together = ('model_name', 'field_name', 'role', 'instance') - def __unicode__(self): + def __str__(self): return "%s.%s - %s - %s" % (self.model_name, self.field_name, self.role, @@ -786,7 +789,7 @@ def save(self, *args, **kwargs): class RoleManager(models.Manager): def get_role(self, instance, user=None): - if user is None or user.is_anonymous(): + if user is None or user.is_anonymous: return instance.default_role return user.get_role(instance) @@ -799,7 +802,7 @@ class Role(models.Model): objects = RoleManager() name = models.CharField(max_length=255) - instance = models.ForeignKey('Instance', null=True, blank=True) + instance = models.ForeignKey('Instance', on_delete=models.CASCADE, null=True, blank=True) default_permission_level = models.IntegerField( db_column='default_permission', @@ -854,7 +857,7 @@ def has_permission(self, codename, Model=None): qs = qs.filter(content_type=content_type) return qs.exists() - def __unicode__(self): + def __str__(self): return '{} ({})'.format(self.name, self.pk) @@ -1133,7 +1136,7 @@ def make_audit(field, prev_val, cur_val): requires_auth=False, ref=None) - for [field, (prev_value, next_value)] in direct_updates.iteritems(): + for [field, (prev_value, next_value)] in direct_updates.items(): yield make_audit(field, prev_value, next_value) @property @@ -1156,7 +1159,7 @@ def hash(self): string_to_hash = '%s:%s:%s' % (self._model_name, self.pk, audit_string) - return hashlib.md5(string_to_hash).hexdigest() + return hashlib.md5(string_to_hash.encode()).hexdigest() @classmethod def action_format_string_for_audit(clz, audit): @@ -1234,7 +1237,7 @@ def save_with_user(self, user, auth_bypass=False, *args, **kwargs): # Before saving we need to restore any pending values to their # previous state - for pending_field, (old_val, __) in pending_updates.iteritems(): + for pending_field, (old_val, __) in pending_updates.items(): try: self.apply_change(pending_field, old_val) except ValueError: @@ -1289,7 +1292,7 @@ def make_pending_audit(field, prev_val, cur_val): requires_auth=True, ref=None) - for [field, (prev_value, next_value)] in pending_updates.iteritems(): + for [field, (prev_value, next_value)] in pending_updates.items(): yield make_pending_audit(field, prev_value, next_value) @@ -1314,13 +1317,13 @@ class Audit(models.Model): model = models.CharField(max_length=255, null=True, db_index=True) model_id = models.IntegerField(null=True, db_index=True) instance = models.ForeignKey( - 'Instance', null=True, blank=True, db_index=True) + 'Instance', on_delete=models.CASCADE, null=True, blank=True, db_index=True) field = models.CharField(max_length=255, null=True) previous_value = models.TextField(null=True) current_value = models.TextField(null=True, db_index=True) - user = models.ForeignKey('treemap.User') + user = models.ForeignKey('treemap.User', on_delete=models.CASCADE) action = models.IntegerField() """ @@ -1356,7 +1359,7 @@ def __init__(self, *args, **kwargs): self.current_value = json.dumps(self.current_value) requires_auth = models.BooleanField(default=False) - ref = models.ForeignKey('Audit', null=True) + ref = models.ForeignKey('Audit', on_delete=models.CASCADE, null=True) created = models.DateTimeField(auto_now_add=True, db_index=True) updated = models.DateTimeField(auto_now=True, db_index=True) @@ -1436,7 +1439,7 @@ def _deserialize_value(self, value): if isinstance(field_cls, models.GeometryField): field_modified_value = GEOSGeometry(field_modified_value) elif isinstance(field_cls, models.ForeignKey): - if isinstance(field_modified_value, (str, unicode)): + try: # sometimes audit records have descriptive string values # stored in what should be a foreign key field. # these cannot be resolved to foreign key models. @@ -1448,12 +1451,13 @@ def _deserialize_value(self, value): # parsing fails, it should be the case that a readable # string is stored instead of a PK, so return that # without trying to resolve a foreign key model. - try: + if isinstance(field_modified_value, str): pk = int(field_modified_value) - field_modified_value = field_cls.rel.to.objects.get( - pk=pk) - except ValueError: - pass + else: + pk = field_modified_value + field_modified_value = field_cls.related_model.objects.get(pk=pk) + except ValueError: + pass return field_modified_value @@ -1576,8 +1580,8 @@ def dict(self): 'ref': self.ref.pk if self.ref else None, 'created': str(self.created)} - def __unicode__(self): - return u"pk=%s - action=%s - %s.%s:(%s) - %s => %s" % \ + def __str__(self): + return "pk=%s - action=%s - %s.%s:(%s) - %s => %s" % \ (self.pk, self.TYPES[self.action], self.model, self.field, self.model_id, self.previous_value, self.current_value) @@ -1592,14 +1596,14 @@ class ReputationMetric(models.Model): how many reputation points are awarded/deducted for an approved/denied audit. """ - instance = models.ForeignKey('Instance') + instance = models.ForeignKey('Instance', on_delete=models.CASCADE) model_name = models.CharField(max_length=255) action = models.CharField(max_length=255) direct_write_score = models.IntegerField(null=True, blank=True) approval_score = models.IntegerField(null=True, blank=True) denial_score = models.IntegerField(null=True, blank=True) - def __unicode__(self): + def __str__(self): return "%s - %s - %s" % (self.instance, self.model_name, self.action) @staticmethod @@ -1640,7 +1644,7 @@ def apply_adjustment(*audits): elif not audit.requires_auth: iuser.reputation += rm.direct_write_score - for iuser in iusers.itervalues(): + for iuser in iusers.values(): iuser.save_base() @@ -1654,7 +1658,7 @@ def _get_model_class(class_dict, cls, model_name): Convert a model name (as a string) into the model class """ if model_name.startswith('udf:'): - from udf import UserDefinedCollectionValue + from .udf import UserDefinedCollectionValue return UserDefinedCollectionValue if not class_dict: diff --git a/opentreemap/treemap/decorators.py b/opentreemap/treemap/decorators.py index 6fd253c45..71bb39575 100644 --- a/opentreemap/treemap/decorators.py +++ b/opentreemap/treemap/decorators.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json from functools import wraps @@ -10,7 +8,7 @@ from django.core.exceptions import PermissionDenied from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseRedirect) -from django.core.urlresolvers import reverse +from django.urls import reverse from django.core.exceptions import ValidationError from django.views.decorators.http import require_http_methods from django.contrib.auth.decorators import login_required @@ -37,7 +35,7 @@ def wrapper(request, instance_url_name, *args, **kwargs): request.instance_supports_ecobenefits = instance.has_itree_region() user = request.user - if user.is_authenticated(): + if user.is_authenticated: instance_user = user.get_instance_user(instance) request.instance_user = instance_user @@ -49,7 +47,7 @@ def wrapper(request, instance_url_name, *args, **kwargs): if request_is_embedded(request): return HttpResponseRedirect( reverse('instance_not_available') + '?embed=1') - elif request.user.is_authenticated(): + elif request.user.is_authenticated: return HttpResponseRedirect( reverse('instance_not_available')) else: @@ -65,7 +63,7 @@ def user_must_be_admin(view_fn): def f(request, instance, *args, **kwargs): user = request.user - if user.is_authenticated(): + if user.is_authenticated: user_instance = user.get_instance_user(instance) is_admin = user_instance and user_instance.admin @@ -192,7 +190,7 @@ def login_or_401(view_fn): """ @wraps(view_fn) def wrapper(request, *args, **kwargs): - if request.user.is_authenticated(): + if request.user.is_authenticated: return view_fn(request, *args, **kwargs) else: return HttpResponse('Unauthorized', status=401) diff --git a/opentreemap/treemap/ecobackend.py b/opentreemap/treemap/ecobackend.py index 0efcc60e1..7b07d1100 100644 --- a/opentreemap/treemap/ecobackend.py +++ b/opentreemap/treemap/ecobackend.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division -import urllib2 -import urllib + +import urllib.request +import urllib.parse +import urllib.error import json import re import sys @@ -92,7 +91,7 @@ def json_benefits_call(endpoint, params, post=False, convert_params=True): hexstring = ''.join('%02X' % ord(x) for x in bytestring) v = "ST_GeomFromEWKT('%s')" % GEOSGeometry(hexstring).ewkt - elif not isinstance(v, unicode): + elif not isinstance(v, str): v = str(v) if k == "param": @@ -103,15 +102,15 @@ def json_benefits_call(endpoint, params, post=False, convert_params=True): else: paramdata[k] = v - data = json.dumps(paramdata) + data = json.dumps(paramdata).encode('utf-8') else: - data = json.dumps(params) - req = urllib2.Request(url, - data, - {'Content-Type': 'application/json'}) + data = json.dumps(params).encode('utf-8') + + req = urllib.request.Request( + url, data, {'Content-Type': 'application/json'}) else: - paramString = "&".join(["%s=%s" % (urllib.quote_plus(str(name)), - urllib.quote_plus(str(val))) + paramString = "&".join(["%s=%s" % (urllib.parse.quote_plus(str(name)), + urllib.parse.quote_plus(str(val))) for (name, val) in params]) # A get request is assumed by urllib2 @@ -124,13 +123,15 @@ def json_benefits_call(endpoint, params, post=False, convert_params=True): general_unhandled_struct = (None, UNKNOWN_ECO_FAILURE) try: - result = urllib2.urlopen(req).read() + result = urllib.request.urlopen(req).read() if result: result = json.loads(result) return result, None - except urllib2.HTTPError as e: - error_body = e.fp.read() - for code, patterns in ECOBENEFIT_FAILURE_CODES_AND_PATTERNS.items(): + except urllib.error.HTTPError as e: + error_body = e.fp.read().decode('UTF-8') + codes_and_patterns = \ + list(ECOBENEFIT_FAILURE_CODES_AND_PATTERNS.items()) + for code, patterns in codes_and_patterns: for pattern in patterns: match = re.match(pattern, error_body) if match: @@ -162,6 +163,6 @@ def json_benefits_call(endpoint, params, post=False, convert_params=True): LOG_FUNCTION_FOR_FAILURE_CODE[UNKNOWN_ECO_FAILURE]( "ECOBENEFIT FAILURE: " + error_body) return general_unhandled_struct - except urllib2.URLError: + except urllib.error.URLError: logger.error("Error connecting to ecoservice", exc_info=sys.exc_info()) return general_unhandled_struct diff --git a/opentreemap/treemap/ecobenefits.py b/opentreemap/treemap/ecobenefits.py index 9e6b7fd31..9afa7ff4f 100644 --- a/opentreemap/treemap/ecobenefits.py +++ b/opentreemap/treemap/ecobenefits.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.utils.translation import ugettext_lazy as _ from django.contrib.gis.geos.point import Point @@ -113,7 +111,7 @@ def _make_sql_from_query(self, query): # Returning a unicode SQL string ensures that any string # replacements done to query string will not raise # UnicodeDecodeError - return unicode(cursor.mogrify(sql, params), 'utf-8') + return str(cursor.mogrify(sql, params), 'utf-8') def benefits_for_filter(self, instance, item_filter): from treemap.models import Plot, Tree @@ -176,7 +174,7 @@ def benefits_for_filter(self, instance, item_filter): # does that we use "plot__geom" above and then # do this rather dubious string manipulation below if not region_code: - targetGeomField = '"treemap_mapfeature"."the_geom_webmercator"' + targetGeomField = '"treemap_mapfeature"."the_geom_webmercator"::bytea' xyGeomFields = 'ST_X(%s), ST_Y(%s)' % \ (targetGeomField, targetGeomField) @@ -187,7 +185,7 @@ def benefits_for_filter(self, instance, item_filter): 'region': region_code or ""} rawb, err = ecobackend.json_benefits_call( - 'eco_summary.json', params.iteritems(), post=True) + 'eco_summary.json', iter(params.items()), post=True) if err: raise Exception(err) @@ -237,7 +235,7 @@ def benefits_for_object(self, instance, plot): 'speciesid': tree.species.pk} rawb, err = ecobackend.json_benefits_call( - 'eco.json', params.iteritems()) + 'eco.json', iter(params.items())) if err: rslt = {'error': err} @@ -314,7 +312,7 @@ def compute_currency_and_transform_units(instance, benefits): rslt = {} - for group, (unit, keys) in groups.iteritems(): + for group, (unit, keys) in groups.items(): valuetotal = currencytotal = 0 for key in keys: @@ -343,7 +341,7 @@ def _sum_dict(d1, d2): return d1 dsum = {} - for k in d1.keys() + d2.keys(): + for k in list(d1.keys()) + list(d2.keys()): if k in d1 and k not in d2: dsum[k] = d1[k] elif k in d2 and k not in d1: @@ -370,8 +368,8 @@ def _combine_benefit_basis(basis, new_basis_groups): def _combine_grouped_benefits(benefits, new_benefit_groups): - for group, ft_benefits in new_benefit_groups.iteritems(): - for ft_benefit_key, ft_benefit in ft_benefits.iteritems(): + for group, ft_benefits in new_benefit_groups.items(): + for ft_benefit_key, ft_benefit in ft_benefits.items(): if group not in benefits: benefits[group] = {} @@ -403,7 +401,7 @@ def _combine_grouped_benefits(benefits, new_benefit_groups): def _annotate_basis_with_extra_stats(basis): # Basis groups just have # calc and # discarded # annotate with some more info - for abasis in basis.values(): + for abasis in list(basis.values()): total = (abasis['n_objects_used'] + abasis['n_objects_discarded']) @@ -476,7 +474,7 @@ def _ensure_itree_codes_fetched(): _itree_codes_by_region = result['Codes'] _all_itree_codes = set( - itertools.chain(*_itree_codes_by_region.values())) + itertools.chain(*list(_itree_codes_by_region.values()))) within_itree_regions_view = json_api_call(within_itree_regions) diff --git a/opentreemap/treemap/ecocache.py b/opentreemap/treemap/ecocache.py index ec93205a5..2b3a13c7b 100644 --- a/opentreemap/treemap/ecocache.py +++ b/opentreemap/treemap/ecocache.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + + import hashlib from django.conf import settings diff --git a/opentreemap/treemap/exceptions.py b/opentreemap/treemap/exceptions.py index bc4505a85..1d084362d 100644 --- a/opentreemap/treemap/exceptions.py +++ b/opentreemap/treemap/exceptions.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json from django.http import HttpResponseForbidden diff --git a/opentreemap/treemap/images.py b/opentreemap/treemap/images.py index 1593a407b..5a0064e7b 100644 --- a/opentreemap/treemap/images.py +++ b/opentreemap/treemap/images.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from PIL import Image import hashlib import os -from cStringIO import StringIO +from io import BytesIO from django.conf import settings from django.core.exceptions import ValidationError @@ -29,7 +27,7 @@ def _rotate_image_based_on_exif(img): def _get_file_for_image(image, filename, format): - temp = StringIO() + temp = BytesIO() image.save(temp, format=format) temp.seek(0) return SimpleUploadedFile(filename, temp.read(), @@ -51,6 +49,8 @@ def save_uploaded_image(image_data, name_prefix, thumb_size=None, # have to treat it as a file-like object if type(image_data) is str: image_data = StringIO(image_data) + elif type(image_data) is bytes: + image_data = BytesIO(image_data) image_data.seek(0, os.SEEK_END) file_size = image_data.tell() diff --git a/opentreemap/treemap/instance.py b/opentreemap/treemap/instance.py index 667cdb741..89d838fc0 100644 --- a/opentreemap/treemap/instance.py +++ b/opentreemap/treemap/instance.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from copy import deepcopy @@ -17,9 +15,12 @@ from django.db.models import F from django.utils.translation import ugettext_lazy as _ +# these are all built-in directly to Django +from django.db.models import Manager as GeoManager + import hashlib import json -from urllib import urlencode +from urllib.parse import urlencode from opentreemap.util import extent_intersection, extent_as_json @@ -128,7 +129,7 @@ def get_instance_permission_spec(instance=None): class InstanceBounds(models.Model): """ Center of the map when loading the instance """ geom = models.MultiPolygonField(srid=3857) - objects = models.GeoManager() + objects = GeoManager() @classmethod def create_from_point(cls, x, y, half_edge=50000): @@ -155,7 +156,7 @@ def create_from_geojson(cls, geojson): try: geojson_dict = json.loads(geojson) except ValueError as e: - raise ValidationError(e.message) + raise ValidationError(str(e)) if geojson_dict['type'] != 'FeatureCollection': raise ValidationError('GeoJSON must contain a FeatureCollection') @@ -254,7 +255,9 @@ class Instance(models.Model): eco_rev = models.IntegerField(default=_DEFAULT_REV) eco_benefits_conversion = models.ForeignKey( - 'BenefitCurrencyConversion', null=True, blank=True) + 'BenefitCurrencyConversion', + on_delete=models.CASCADE, + null=True, blank=True) """ Center of the map when loading the instance """ bounds = models.OneToOneField(InstanceBounds, @@ -267,7 +270,7 @@ class Instance(models.Model): """ center_override = models.PointField(srid=3857, null=True, blank=True) - default_role = models.ForeignKey('Role', related_name='default_role') + default_role = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='default_role') users = models.ManyToManyField('User', through='InstanceUser') @@ -307,9 +310,9 @@ class Instance(models.Model): canopy_boundary_category = models.CharField(max_length=255, default='', blank=True) - objects = models.GeoManager() + objects = GeoManager() - def __unicode__(self): + def __str__(self): return self.name def _make_config_property(prop, default=None): @@ -432,11 +435,11 @@ def center(self): @property def geo_rev_hash(self): - return hashlib.md5(str(self.geo_rev)).hexdigest() + return hashlib.md5(str(self.geo_rev).encode()).hexdigest() @property def universal_rev_hash(self): - return hashlib.md5(str(self.universal_rev)).hexdigest() + return hashlib.md5(str(self.universal_rev).encode()).hexdigest() @property def center_lat_lng(self): @@ -455,7 +458,8 @@ def factor_conversions(self): @property def scss_query_string(self): - scss_vars = ({k: val for k, val in self.scss_variables.items() if val} + scss_vars = ({k: val for k, val + in list(self.scss_variables.items()) if val} if self.scss_variables else {}) return urlencode(scss_vars) @@ -583,7 +587,7 @@ def add_map_feature_types(self, types): for type, clz in zip(types, classes): settings = (getattr(clz, 'udf_settings', {})) - for udfc_name, udfc_settings in settings.items(): + for udfc_name, udfc_settings in list(settings.items()): if udfc_settings.get('defaults'): get_or_create_udf(self, type, udfc_name) @@ -725,7 +729,7 @@ def raise_errors(errors): raise_errors([INSTANCE_FIELD_ERRORS['no_field_groups']]) for group in field_groups: - if not _truthy_of_type(group.get('header'), basestring): + if not _truthy_of_type(group.get('header'), str): errors.add(INSTANCE_FIELD_ERRORS['group_has_no_header']) if ((not isinstance(group.get('collection_udf_keys'), list) diff --git a/opentreemap/treemap/js/src/lib/MapManager.js b/opentreemap/treemap/js/src/lib/MapManager.js index 5198de174..6337f6d0e 100644 --- a/opentreemap/treemap/js/src/lib/MapManager.js +++ b/opentreemap/treemap/js/src/lib/MapManager.js @@ -16,6 +16,9 @@ var $ = require('jquery'), config = require('treemap/lib/config.js'), reverse = require('reverse'), + shp = require('shpjs'), + leafletShpfile = require('treemap/lib/leaflet.shpfile.js'), + MIN_ZOOM_OPTION = layersLib.MIN_ZOOM_OPTION, MAX_ZOOM_OPTION = layersLib.MAX_ZOOM_OPTION, BASE_PANE_OPTION = layersLib.BASE_PANE_OPTION; @@ -29,6 +32,7 @@ require('esri-leaflet'); require('leaflet.locatecontrol'); var MapManager = function() {}; // constructor +window.shp = shp; function monkeyPatchLeafletLayersControlForMobileSafari(layersControl) { /* @@ -122,9 +126,11 @@ MapManager.prototype = { var hasPolygons = getDomMapBool('has-polygons', options.domId), hasBoundaries = getDomMapBool('has-boundaries', options.domId), plotLayer = layersLib.createPlotTileLayer(), - allPlotsLayer = layersLib.createPlotTileLayer(), + plotImportedLayer = layersLib.createPlotTileImportedLayer(), + allPlotsLayer = layersLib.createPlotTileLayer({}), utfLayer = layersLib.createPlotUTFLayer(); this._plotLayer = plotLayer; + this._plotImportedLayer = plotImportedLayer; this._allPlotsLayer = allPlotsLayer; this._utfLayer = utfLayer; allPlotsLayer.setOpacity(0.3); @@ -136,7 +142,24 @@ MapManager.prototype = { this.layersControl.addOverlay(plotLayer, 'OpenTreeMap Trees'); } else { map.addLayer(plotLayer); + map.addLayer(utfLayer); + + // add the legend and the imported plot layer together + var legend = createLegend(); + this.layersControl.addOverlay(plotImportedLayer, 'Imported Trees'); + map.on('overlayadd', function(e) { + if (e.layer === plotImportedLayer) { + map.addControl(legend); + } + }); + + map.on('overlayremove', function(e) { + if (e.layer === plotImportedLayer) { + map.removeControl(legend); + } + }); + var baseUtfEventStream = BU.leafletEventStream(utfLayer, 'click'); if (hasPolygons) { @@ -165,7 +188,7 @@ MapManager.prototype = { // halving with every zoom level. I arrived at 20 // meters at zoom level 15 through trial and error dist = 20 / Math.pow(2, map.getZoom() - MIN_ZOOM_OPTION.minZoom), - url = reverse.polygon_for_point({instance_url_name: config.instance.url_name}); + url = reverse.Urls.polygon_for_point({instance_url_name: config.instance.url_name}); return url + format('?lng=%d&lat=%d&distance=%d', lng, lat, dist); }).flatMap(BU.getJsonFromUrl); @@ -266,11 +289,31 @@ MapManager.prototype = { visible = _.keys(basemapMapping)[0]; } map.addLayer(basemapMapping[visible]); - this.layersControl = L.control.layers(basemapMapping, null, { - autoZIndex: false - }); - monkeyPatchLeafletLayersControlForMobileSafari(this.layersControl); + var wardLayer = layersLib.createBoundariesbyCategoryTileLayer('Ward'); + var mainNeighborhoodLayer = layersLib.createBoundariesbyCategoryTileLayer('Main Neighborhood'); + var neighborhoodLayer = layersLib.createBoundariesbyCategoryTileLayer('Neighborhood'); + var parksLayer = layersLib.createBoundariesbyCategoryTileLayer('Park'); + var parcelsLayer = layersLib.createBoundariesbyCategoryTileLayer('Parcels'); + var namedZonesLayer = layersLib.createBoundariesbyCategoryTileLayer('Zones'); + var sidLayer = layersLib.createBoundariesbyCategoryTileLayer('SID'); + + this.layersControl = L.control.layers( + basemapMapping, + { + 'Wards': wardLayer, + 'Main Neighborhoods + LSP': mainNeighborhoodLayer, + 'Neighborhoods': neighborhoodLayer, + 'Parks': parksLayer, + 'SIDs': sidLayer, + 'Parcels': parcelsLayer, + 'Zones': namedZonesLayer, + }, { + autoZIndex: false + } + ); + + //monkeyPatchLeafletLayersControlForMobileSafari(this.layersControl); this.layersControl.addTo(map); map.on('baselayerchange', function(e) { @@ -311,6 +354,7 @@ MapManager.prototype = { updateRevHashes: function (response) { this._utfLayer.setHashes(response); this._plotLayer.setHashes(response); + this._plotImportedLayer.setHashes(response); this._allPlotsLayer.setHashes(response); if (this._hasPolygons) { @@ -321,6 +365,7 @@ MapManager.prototype = { setFilter: function (filter) { this._plotLayer.setFilter(filter); + this._plotImportedLayer.setFilter(filter); if (this._hasPolygons) { this._polygonLayer.setFilter(filter); @@ -412,6 +457,12 @@ function getBasemapLayers(type) { return L.esri.basemapLayer(key, options); } + return { + 'Bing': makeBingLayer('AerialWithLabels'), + 'Google': makeGoogleLayer('hybrid'), + 'Streets': makeGoogleLayer('roadmap'), + }; + if (type === 'bing') { return { 'Road': makeBingLayer('Road'), @@ -490,4 +541,21 @@ function addCustomLayer(mapManager, layerInfo) { } } +function createLegend() { + var legend = L.control({position: 'bottomright'}); + legend.onAdd = function (map) { + var div = L.DomUtil.create('div', 'info legend'); + var labels = [ + '<strong>Plot Colors</strong>', + '<i class="circle" style="background:#8BAA3C"></i> User Added Tree', + '<i class="circle" style="background:#E1C6FF"></i> Empty Plot', + '<i class="circle" style="background:#85B4ED"></i> Imported Tree', + ]; + div.innerHTML = labels.join('<br>'); + return div; + }; + + return legend; +} + module.exports = MapManager; diff --git a/opentreemap/treemap/js/src/lib/boundarySelect.js b/opentreemap/treemap/js/src/lib/boundarySelect.js index dfbec0d96..0d76f4c98 100644 --- a/opentreemap/treemap/js/src/lib/boundarySelect.js +++ b/opentreemap/treemap/js/src/lib/boundarySelect.js @@ -29,7 +29,7 @@ function showBoundaryGeomOnMapLayerAndZoom(map, boundaryGeoJsonLayer) { } function instanceBoundaryIdToUrl(id) { - return reverse.boundaries_geojson({ + return reverse.Urls.boundaries_geojson({ instance_url_name: config.instance.url_name, boundary_id: id }); diff --git a/opentreemap/treemap/js/src/lib/export.js b/opentreemap/treemap/js/src/lib/export.js index e9daf306c..dfc702a3f 100644 --- a/opentreemap/treemap/js/src/lib/export.js +++ b/opentreemap/treemap/js/src/lib/export.js @@ -66,7 +66,7 @@ function getJobStartStream () { function makeJobCheckStream (attrStream) { function poll (jobId) { jobManager.start(jobId); - var url = reverse.check_export({ + var url = reverse.Urls.check_export({ instance_url_name: config.instance.url_name, job_id: jobId }); diff --git a/opentreemap/treemap/js/src/lib/fieldHelpers.js b/opentreemap/treemap/js/src/lib/fieldHelpers.js index 97b0feefa..dc9e64c77 100644 --- a/opentreemap/treemap/js/src/lib/fieldHelpers.js +++ b/opentreemap/treemap/js/src/lib/fieldHelpers.js @@ -128,7 +128,7 @@ exports.formToDictionary = function ($form, $editFields, $displayFields) { var name = elem.name, value = $(elem).val() || [], display = getDisplayValue('multichoice', name); - display = (display === "null") ? [] : JSON.parse(display); + display = (display === undefined) ? [] : JSON.parse(display); if (! _.isEqual(value.sort(), display.sort())) { // Value of multichoice field has changed result[name] = value; diff --git a/opentreemap/treemap/js/src/lib/geocoder.js b/opentreemap/treemap/js/src/lib/geocoder.js index 008d3d5f0..b5679c233 100644 --- a/opentreemap/treemap/js/src/lib/geocoder.js +++ b/opentreemap/treemap/js/src/lib/geocoder.js @@ -47,7 +47,7 @@ exports = module.exports = function () { } return Bacon.fromPromise( $.ajax({ - url: reverse.geocode(), + url: reverse.Urls.geocode(), type: 'GET', data: data, dataType: 'json' @@ -77,7 +77,7 @@ exports = module.exports = function () { return Bacon.fromPromise($.ajax(opts)); } else { return Bacon.fromPromise( - $.getJSON(reverse.get_geocode_token()) + $.getJSON(reverse.Urls.get_geocode_token()) .then(function(response) { token = response.token; opts.data.token = token; diff --git a/opentreemap/treemap/js/src/lib/imageLightbox.js b/opentreemap/treemap/js/src/lib/imageLightbox.js index e453c053d..8be0c9156 100644 --- a/opentreemap/treemap/js/src/lib/imageLightbox.js +++ b/opentreemap/treemap/js/src/lib/imageLightbox.js @@ -1,10 +1,12 @@ "use strict"; var $ = require('jquery'), + BU = require('treemap/lib/baconUtils.js'), toastr = require('toastr'), format = require('util').format, _ = require('lodash'), config = require('treemap/lib/config.js'), + reverse = require('reverse'), photoCarousel = require('treemap/lib/photoCarousel.js'); @@ -25,7 +27,7 @@ module.exports.init = function(options) { $imageContainer.on('slide', function(e) { var $thumbnailList = $imageContainer.find('.carousel-indicators'), $thumbnailListContainer = $thumbnailList.parent(), - index = $imageContainer.find('.carousel-inner .item').index(e.relatedTarget), + index = $imageContainer.find('.carousel-inner .carousel-item').index(e.relatedTarget), $thumbnail = $thumbnailList.find('[data-slide-to="' + index + '"]'), // The $thumbnailListContainer has overflow-x:auto on it, and @@ -45,8 +47,8 @@ module.exports.init = function(options) { // To make the thumbnail scrolling work we need to prevent wrapping // from the first to the last item, and vice versa $imageContainer.on('slid', function(e) { - var isFirst = $imageContainer.find('.carousel-inner .item:first').hasClass('active'), - isLast = $imageContainer.find('.carousel-inner .item:last').hasClass('active'); + var isFirst = $imageContainer.find('.carousel-inner .carousel-item:first').hasClass('active'), + isLast = $imageContainer.find('.carousel-inner .carousel-item:last').hasClass('active'); $imageContainer .find('.carousel-control') .attr('href', '#' + $imageContainer.attr('id')) @@ -76,7 +78,7 @@ module.exports.init = function(options) { } function isPhotoDeletable () { - var $deleteControl = $imageContainer.find('.item.active .delete-photo'); + var $deleteControl = $imageContainer.find('.carousel-item.active .delete-photo'); return 0 < $deleteControl.length; } @@ -84,14 +86,21 @@ module.exports.init = function(options) { // $imageContainer.on('click', '[href="' + options.lightbox + '"]', function(e) { $lightbox.on('show.bs.modal', function(e) { var $toggle = $(e.relatedTarget), - $active = $toggle.parents('.item.active'), + $active = $toggle.parents('.carousel-item.active'), $endpointEl = $active.find('[data-endpoint]'), $deleteToggleEl = $active.find('.delete-photo'), mode = $toggle.is($deleteToggleEl) ? 'delete' : 'view', endpoint = $endpointEl.attr('data-endpoint'), modeSelector = '[data-class="' + mode + '"]', notModeSelector = '[data-class]:not(' + modeSelector + ')', - $keepControl = $lightbox.find('[data-photo-keep]'); + $keepControl = $lightbox.find('[data-photo-keep]'), + + label = $endpointEl.attr('data-label'), + photoId = $endpointEl.attr('data-map-feature-photo-id'), + featureId = $endpointEl.attr('data-map-feature-id'), + labelSelector = '[data-class="label"]', + labelViewSelector = '.photo-label-view', + labelEditSelectSelector = '#photo-label-btn'; $keepControl.off('click.delete-mode'); currentRotation = 0; @@ -104,6 +113,40 @@ module.exports.init = function(options) { $lightbox.find(notModeSelector).hide(); $lightbox.find('[data-photo-save]').attr('data-photo-save', endpoint); + var labelEl = $lightbox.find(labelSelector); + // set the label if it exists + if(label !== undefined && label !== "") { + var labelViewEl = $lightbox.find(labelViewSelector); + labelViewEl.html(label); + $lightbox.find(labelEditSelectSelector).val(label); + + $lightbox.find(labelEditSelectSelector).on('change', function(e) { + var value = e.target.value; + var url = reverse.Urls.map_feature_photo({ + instance_url_name: config.instance.url_name, + feature_id: featureId, + photo_id: photId + }) + '/label'; + + var stream = BU.jsonRequest('POST', url)({'label': value}); + stream.onValue(function() { + console.log("done"); + }); + }); + + if (mode === 'edit'){ + labelViewEl.hide(); + } else { + labelViewEl.show(); + } + + labelEl.show(); + + } else { + labelEl.hide(); + } + + // tzinckgraf if (1 === $deleteToggleEl.length) { $lightbox.find('[data-photo-confirm]').attr('data-photo-confirm', endpoint); $lightbox.find('[data-class="delete"] button').prop('disabled', false); @@ -124,6 +167,11 @@ module.exports.init = function(options) { $lightbox.find('[data-class="edit"]').show(); }); + $lightbox.on('click', '[data-photo-save-cancel]', function() { + $lightbox.find('[data-class]:not([data-class="view"])').hide(); + $lightbox.find('[data-class="view"]').show(); + }); + $lightbox.on('click', '[data-photo-rotate]', function(e) { var $target = $(e.currentTarget), $saveButton = $target diff --git a/opentreemap/treemap/js/src/lib/inlineEditForm.js b/opentreemap/treemap/js/src/lib/inlineEditForm.js index 349d11455..73d2ffa57 100644 --- a/opentreemap/treemap/js/src/lib/inlineEditForm.js +++ b/opentreemap/treemap/js/src/lib/inlineEditForm.js @@ -90,6 +90,14 @@ exports.init = function(options) { $("table[data-udf-id] .placeholder").css('display', 'none'); }, + removeRequiredIndicator = function() { + //$('span.required-indicator').html(''); + }, + + displayRequiredIndicator = function() { + //$('span.required-indicator').html('* '); + }, + getDataToSave = options.getDataToSave || function() { var data = FH.formToDictionary($(form), $(editFields), $(displayFields)); @@ -310,8 +318,10 @@ exports.init = function(options) { editStartStream.onValue(editForm.displayValuesToFormFields); editStartStream.onValue(showCollectionUdfs); + editStartStream.onValue(displayRequiredIndicator); eventsLandingInDisplayModeStream.onValue(resetCollectionUdfs); + eventsLandingInDisplayModeStream.onValue(removeRequiredIndicator); return { // immutable access to all actions diff --git a/opentreemap/treemap/js/src/lib/layers.js b/opentreemap/treemap/js/src/lib/layers.js index ba8c756ee..910a4e57b 100644 --- a/opentreemap/treemap/js/src/lib/layers.js +++ b/opentreemap/treemap/js/src/lib/layers.js @@ -52,6 +52,12 @@ exports.createBoundariesTileLayer = function () { return L.tileLayer(url, options); }; +exports.createBoundariesbyCategoryTileLayer = function (category) { + var options = _.extend({category: category}, MAX_ZOOM_OPTION, FEATURE_LAYER_OPTION); + return filterableLayer( + 'treemap_boundary', 'png', options, {category: category}); +}; + exports.getCanopyBoundariesTileLayerUrl = function(tilerArgs) { var revToUrl = getUrlMaker('treemap_canopy_boundary', 'png', tilerArgs); return revToUrl(config.instance.geoRevHash); @@ -63,10 +69,21 @@ exports.createCanopyBoundariesTileLayer = function () { return L.tileLayer(url, options); }; -exports.createPlotTileLayer = function () { +exports.createPlotTileLayer = function (tilerArgs) { + var options = _.extend({}, MAX_ZOOM_OPTION, FEATURE_LAYER_OPTION); + return filterableLayer( + 'treemap_mapfeature', 'png', options, tilerArgs); +}; + +exports.createPlotTileImportedLayer = function () { + // this query means all ID's >= 0, which is all ID's + var tilerArgs = { + 'importer': '1', + 'q': JSON.stringify({"treeRowImport.id": {"MIN": 0, "MAX": ""}}) + }; var options = _.extend({}, MAX_ZOOM_OPTION, FEATURE_LAYER_OPTION); return filterableLayer( - 'treemap_mapfeature', 'png', options); + 'importer_treerowimport', 'png', options, tilerArgs); }; exports.createPolygonTileLayer = function () { @@ -160,8 +177,8 @@ function updateBaseUrl(url, newBaseUrl) { return newBase + '?' + oldQueryString; } -function filterableLayer (table, extension, layerOptions) { - var revToUrl = getUrlMaker(table, extension), +function filterableLayer (table, extension, layerOptions, tilerArgs) { + var revToUrl = getUrlMaker(table, extension, tilerArgs), noSearchUrl = revToUrl(config.instance.geoRevHash), searchBaseUrl = revToUrl(config.instance.universalRevHash), layer = L.tileLayer(noSearchUrl, layerOptions); diff --git a/opentreemap/treemap/js/src/lib/leaflet.shpfile.js b/opentreemap/treemap/js/src/lib/leaflet.shpfile.js new file mode 100644 index 000000000..a528b1b3e --- /dev/null +++ b/opentreemap/treemap/js/src/lib/leaflet.shpfile.js @@ -0,0 +1,63 @@ +'use strict'; + +/* global cw, shp */ +L.Shapefile = L.GeoJSON.extend({ + options: { + importUrl: 'shp.js' + }, + + initialize: function(file, options) { + L.Util.setOptions(this, options); + if (typeof cw !== 'undefined') { + /*eslint-disable no-new-func*/ + if (!options.isArrayBuffer) { + this.worker = cw(new Function('data', 'cb', 'importScripts("' + this.options.importUrl + '");shp(data).then(cb);')); + } else { + this.worker = cw(new Function('data', 'importScripts("' + this.options.importUrl + '"); return shp.parseZip(data);')); + } + /*eslint-enable no-new-func*/ + } + L.GeoJSON.prototype.initialize.call(this, { + features: [] + }, options); + this.addFileData(file); + }, + + addFileData: function(file) { + var self = this; + this.fire('data:loading'); + if (typeof file !== 'string' && !('byteLength' in file)) { + var data = this.addData(file); + this.fire('data:loaded'); + return data; + } + if (!this.worker) { + shp(file).then(function(data) { + self.addData(data); + self.fire('data:loaded'); + }).catch(function(err) { + self.fire('data:error', err); + }) + return this; + } + var promise; + if (this.options.isArrayBufer) { + promise = this.worker.data(file, [file]); + } else { + promise = this.worker.data(cw.makeUrl(file)); + } + + promise.then(function(data) { + self.addData(data); + self.fire('data:loaded'); + self.worker.close(); + }).then(function() {}, function(err) { + self.fire('data:error', err); + }) + return this; + } +}); + +L.shapefile = function(a, b, c) { + return new L.Shapefile(a, b, c); +}; diff --git a/opentreemap/treemap/js/src/lib/mapFeatureDelete.js b/opentreemap/treemap/js/src/lib/mapFeatureDelete.js index c396e0ea6..2c7d1970a 100644 --- a/opentreemap/treemap/js/src/lib/mapFeatureDelete.js +++ b/opentreemap/treemap/js/src/lib/mapFeatureDelete.js @@ -50,6 +50,6 @@ function mapPageUrl() { zoom = (new MapManager()).ZOOM_PLOT, zoomLatLng = _.extend({zoom: zoom}, latlng), query = U.makeZoomLatLngQuery(zoomLatLng), - url = reverse.map(config.instance.url_name) + '?z=' + query; + url = reverse.Urls.map(config.instance.url_name) + '?z=' + query; return url; -} \ No newline at end of file +} diff --git a/opentreemap/treemap/js/src/lib/mapFeatureUdf.js b/opentreemap/treemap/js/src/lib/mapFeatureUdf.js index 45e22b5bf..890cf7f65 100644 --- a/opentreemap/treemap/js/src/lib/mapFeatureUdf.js +++ b/opentreemap/treemap/js/src/lib/mapFeatureUdf.js @@ -11,7 +11,7 @@ var $ = require('jquery'), '<% _.each(fields, function (field) { %>' + '<td data-value="<%= field.value %>"> <%= field.display %> </td>' + '<% }) %>' + - '<td></td>' + + '<td><a data-udf-id="<%= id %>" class="btn remove">x</a></td>' + '</tr>'), resolveButtonMarkup = '<a href="javascript:;" ' + @@ -58,15 +58,26 @@ exports.init = function(form) { } // Wire up collection udfs - $('a[data-udf-id]').on('click', function() { + $('a.add-row[data-udf-id]').on('click', function() { var id = $(this).data('udf-id'), selector = format('table[data-udf-id="%s"] * [data-field-name]', id), fields = $(selector).toArray(), data = _.map(fields, formatField); - $(this).closest('table').append(udfRowTemplate({ fields: data })); + $(this).closest('table').append(udfRowTemplate({ fields: data, id: id })); }); - form.inEditModeProperty.filter(R.equals(true)).onValue(addResolveAlertButtons); + $('table[data-udf-id]').on('click', 'a.remove[data-udf-id]', function() { + var id = $(this).data('udf-id'), + selector = format('table[data-udf-id="%s"] * [data-field-name]', id), + fields = $(selector).toArray(), + data = _.map(fields, formatField); + + $(this).closest('tr').remove(); //.append(udfRowTemplate({ fields: data })); + }); + + if (form != null && form.hasOwnProperty('inEditModeProperty')) { + form.inEditModeProperty.filter(R.equals(true)).onValue(addResolveAlertButtons); + } }; diff --git a/opentreemap/treemap/js/src/lib/plotDetail.js b/opentreemap/treemap/js/src/lib/plotDetail.js index 5c65b19b0..26ce332bc 100644 --- a/opentreemap/treemap/js/src/lib/plotDetail.js +++ b/opentreemap/treemap/js/src/lib/plotDetail.js @@ -32,7 +32,7 @@ exports.init = function(form) { '.responseData.treeId'); if (treeId) { - var deleteUrl = reverse.delete_tree({ + var deleteUrl = reverse.Urls.delete_tree({ instance_url_name: config.instance.url_name, feature_id: window.otm.mapFeature.featureId, tree_id: treeId @@ -47,7 +47,7 @@ exports.init = function(form) { otmTypeahead.create({ name: "species", - url: reverse.species_list_view(config.instance.url_name), + url: reverse.Urls.species_list_view(config.instance.url_name), input: "#plot-species-typeahead", template: "#species-element-template", hidden: "#plot-species-hidden", diff --git a/opentreemap/treemap/js/src/lib/search.js b/opentreemap/treemap/js/src/lib/search.js index a7d9244c8..58c3c3c83 100644 --- a/opentreemap/treemap/js/src/lib/search.js +++ b/opentreemap/treemap/js/src/lib/search.js @@ -79,7 +79,7 @@ function executeSearch(filters) { var query = makeQueryStringFromFilters(filters); var search = $.ajax({ - url: reverse.benefit_search(config.instance.url_name), + url: reverse.Urls.benefit_search(config.instance.url_name), data: query, type: 'GET', dataType: 'html' diff --git a/opentreemap/treemap/js/src/lib/searchBar.js b/opentreemap/treemap/js/src/lib/searchBar.js index 20b904ba0..180793871 100644 --- a/opentreemap/treemap/js/src/lib/searchBar.js +++ b/opentreemap/treemap/js/src/lib/searchBar.js @@ -78,7 +78,7 @@ function redirectToSearchPage(filters, latLng) { if (filters.address) { query += '&a=' + filters.address; } - window.location.href = reverse.map(config.instance.url_name) + '?' + query; + window.location.href = reverse.Urls.map(config.instance.url_name) + '?' + query; } function initSearchUi(searchStream) { @@ -333,7 +333,7 @@ module.exports = exports = { var speciesTypeahead = otmTypeahead.create({ name: "species", - url: reverse.species_list_view(config.instance.url_name), + url: reverse.Urls.species_list_view(config.instance.url_name), input: "#species-typeahead", template: "#species-element-template", hidden: "#search-species", @@ -341,7 +341,7 @@ module.exports = exports = { }), locationTypeahead = otmTypeahead.create({ name: "boundaries", - url: reverse.boundary_list(config.instance.url_name), + url: reverse.Urls.boundary_list(config.instance.url_name), input: dom.locationSearchTypeahead, template: "#boundary-element-template", hidden: "#boundary", diff --git a/opentreemap/treemap/js/src/lib/socialMediaSharing.js b/opentreemap/treemap/js/src/lib/socialMediaSharing.js index 769d4f510..7ed1f7fbd 100644 --- a/opentreemap/treemap/js/src/lib/socialMediaSharing.js +++ b/opentreemap/treemap/js/src/lib/socialMediaSharing.js @@ -6,28 +6,44 @@ var $ = require('jquery'), Bacon = require('baconjs'), + BU = require('treemap/lib/baconUtils.js'), R = require('ramda'), _ = require('lodash'), + reverse = require('reverse'), + config = require('treemap/lib/config.js'), _DONT_SHOW_AGAIN_KEY = 'social-media-sharing-dont-show-again', _SHARE_CONTAINER_SIZE = 300, + // for iNaturalist + _APP_ID = 'db6db69ef86d5a21a4c9876bcaebad059db3b1ed90f30255c6d9e8bdaebf0513', + + photoInfo = { + PhotoDetailUrl: '', + PhotoUrl: '' + }, + attrs = { dataUrlTemplate: 'data-url-template', dataClass: 'data-class', + mapFeatureId: 'data-map-feature-id', + mapFeaturePhotoId: 'data-map-feature-photo-id', mapFeaturePhotoDetailAbsoluteUrl: 'data-map-feature-photo-detail-absolute-url', mapFeaturePhotoImageAbsoluteUrl: 'data-map-feature-photo-image-absolute-url', - mapFeaturePhotoPreview: 'data-map-feature-photo-thumbnail' + mapFeaturePhotoPreview: 'data-map-feature-photo-thumbnail', }, dom = { dontShowAgain: '[' + attrs.dataClass + '="' + _DONT_SHOW_AGAIN_KEY + '"]', photoModal: '#social-media-sharing-photo-upload-modal', - photoPreview: '#social-media-sharing-photo-upload-preview', + photoPreview: '#label-photo-upload-preview', shareLinkSelector: '[' + attrs.dataUrlTemplate + ']', + mapFeatureId: '[' + attrs.mapFeatureId + ']', + mapFeaturePhotoId: '[' + attrs.mapFeaturePhotoId + ']', mapFeaturePhotoDetailAbsoluteUrl: '[' + attrs.mapFeaturePhotoDetailAbsoluteUrl + ']', toggle: '.share', - container: '.js-container' + container: '.js-container', + photoLabel: '#photo-label' }, generateHref = R.curry( @@ -60,28 +76,64 @@ function renderPhotoModal (imageData) { $photo = $carousel.find(dom.mapFeaturePhotoDetailAbsoluteUrl), photoDetailUrl = $photo.attr(attrs.mapFeaturePhotoDetailAbsoluteUrl), photoUrl = $photo.attr(attrs.mapFeaturePhotoImageAbsoluteUrl), + + mapFeatureId = $photo.attr(attrs.mapFeatureId), + mapFeaturePhotoId = $photo.attr(attrs.mapFeaturePhotoId), + $photoPreview = $(dom.photoPreview); + photoInfo.PhotoDetailUrl = photoDetailUrl; + photoInfo.PhotoUrl = photoUrl; + + // Validation errors (image invalid, image too big) are only returned as DOM // elements. In order to skip showing the share dialog we need to check the // dialog markup for the error message element. - if ($(imageData.data.result).filter('[data-photo-upload-failed]').length > 0) { - return; - } + //if ($(imageData.data.result).filter('[data-photo-upload-failed]').length > 0) { + // return; + //} $photoModal.modal('show'); $photoPreview.attr('src', $photo.attr(attrs.mapFeaturePhotoPreview)); _.each($anchors, generateHref(photoDetailUrl, photoUrl)); + + // remove the old handlers and reset the value + $(dom.photoLabel).off('change'); + $(dom.photoLabel).val(''); + + $(dom.photoLabel).on('change', function(e) { + var value = e.target.value; + var url = reverse.Urls.map_feature_photo({ + instance_url_name: config.instance.url_name, + feature_id: window.otm.mapFeature.featureId, + photo_id: mapFeaturePhotoId + }) + '/label'; + + var stream = BU.jsonRequest('POST', url)({'label': value}); + stream.onValue(function() { + console.log("done"); + }); + + /* + var addStream = $addUser + .asEventStream('click') + .map(function () { + return {'email': $addUserEmail.val()}; + }) + .flatMap(BU.jsonRequest('POST', url)); + */ + + }); } + module.exports.init = function(options) { var imageFinishedStream = options.imageFinishedStream || Bacon.never(); $(dom.toggle).on('click', function () { $(dom.container).toggle(_SHARE_CONTAINER_SIZE); }); - imageFinishedStream - .filter(shouldShowSharingModal) - .onValue(renderPhotoModal); + imageFinishedStream.onValue(renderPhotoModal); + //.filter(shouldShowSharingModal) $(dom.dontShowAgain).on('click', setDontShowAgainVal); }; diff --git a/opentreemap/treemap/js/src/lib/uploadPanelAddTreePhoto.js b/opentreemap/treemap/js/src/lib/uploadPanelAddTreePhoto.js new file mode 100644 index 000000000..3c5b97602 --- /dev/null +++ b/opentreemap/treemap/js/src/lib/uploadPanelAddTreePhoto.js @@ -0,0 +1,177 @@ +// Manage panel for file uploading +"use strict"; + +var $ = require('jquery'), + toastr = require('toastr'), + Bacon = require('baconjs'), + U = require('treemap/lib/utility.js'), + reverse = require('reverse'), + _ = require('lodash'), + config = require('treemap/lib/config.js'); + +// For modal dialog on jquery +require('bootstrap'); + +// jQuery-File-Upload and its dependencies +require('jqueryIframeTransport'); +require('jqueryFileUpload'); + + +module.exports.init = function(options) { + options = options || {}; + var $panel = $(options.panelId || '#upload-panel'), + $image = $(options.imageElement), + $error = $(options.error || '.js-upload-error'), + dataType = options.dataType || 'json', + addMapFeatureBus = options.addMapFeatureBus, + + $chooser = $panel.find('.fileChooser'), + $progressBar = $panel.find('.progress').children().first(), + callback, + unsubscribeFromAdd = $.noop, + finishedStream = new Bacon.EventStream(function(subscribe) { + callback = subscribe; + + return function() { + callback = null; + }; + }); + + var fileupload = $chooser.fileupload({ + dataType: dataType, + start: function () { + $error.hide(); + }, + add: function(e, data) { + unsubscribeFromAdd(); + + // keep track of this for visibility + + var input = $(this).closest(".fileChooser"); + var row = $(input.data('row-id')); + + // once we finish adding the tree, we can use that + // result to send the photo and label + unsubscribeFromAdd = addMapFeatureBus.onValue(function (result) { + var label = input.data('label'); + + // either we have an empty site, so we want to use the empty site photo + // or we don't have an empty site and we want every other photo + var isEmptySite = !result.feature.has_tree; + + var callback_data = {}; + + var url = null; + if (isEmptySite && label == 'empty site') { + + url = reverse.Urls.add_photo_to_map_feature({ + instance_url_name: config.instance.url_name, + feature_id: result.featureId, + }); + callback_data['feature_id'] = result.featureId; + + } else if (!isEmptySite && label != 'empty site') { + + url = reverse.Urls.add_photo_to_tree_with_label({ + instance_url_name: config.instance.url_name, + feature_id: result.featureId, + tree_id: result.treeId + }); + callback_data['feature_id'] = result.featureId; + callback_data['tree_id'] = result.treeId; + } + // this handles the case of a tree photo that might accidentally be added + // on an empty site + else { + return; + } + + data.formData = {'label': label} + data.url = url; + + // push to the stream once this is done uploading + callback_data['label'] = label; + data.submit().done(function(e) { + callback(new Bacon.Next(callback_data)); + }); + }); + row.addClass('bg-success') + + $(input.data('checkbox-id')).prop('checked', true); + $panel.modal('hide'); + }, + progressall: function (e, data) { + var progress = parseInt(data.loaded / data.total * 100, 10); + $progressBar.width(progress + '%'); + }, + always: function (e, data) { + $progressBar.width('0%'); + }, + done: function (e, data) { + $panel.modal('hide'); + if ($image.length > 0) { + $image.attr('src', data.result.url); + } + + // clear everything + var input = $(this).closest(".fileChooser"); + var row = $(input.data('row-id')); + row.removeClass('bg-success') + + $(input.data('checkbox-id')).prop('checked', false); + data.files = []; + unsubscribeFromAdd(); + + if (callback) { + // Downstream users will be opening modals, which leads to + // style errors if that is done before a modal closes + //callback(new Bacon.Next({event: e, data: data})); + //$panel.one('hidden.bs.modal', function() { + // callback(new Bacon.Next({event: e, data: data})); + //}); + } + }, + fail: function (e, data) { + // If the datatype is not JSON we expect the endpoint to return + // error messages inside the HTML fragment it gives back + if (dataType == 'json') { + var json = data.jqXHR.responseJSON, + message; + + if (json && json.error) { + U.warnDeprecatedErrorMessage(json); + message = json.error; + } else if (json && json.globalErrors) { + message = json.globalErrors.join(','); + } else { + message = "Upload failed"; + } + $error.text(message).show(); + } + } + }); + + fileupload.on('fileuploadadd', function(e, data) { + data.process(function() { + var defer = $.Deferred(); + _.each(data.files, function(file) { + if (file.size >= options.maxImageSize) { + var mb = options.maxImageSize / 1024 / 1024, + message = config.trans.fileExceedsMaximumFileSize + .replace('{0}', file.name) + .replace('{1}', mb + ' MB'); + toastr.error(message); + defer.reject([data]); + } + }); + defer.resolve([data]); + return defer.promise(); + }); + }); + + $panel.on('hidden.bs.modal', function() { + $error.hide(); + }); + + return finishedStream; +}; diff --git a/opentreemap/treemap/js/src/mapFeatureDetail.js b/opentreemap/treemap/js/src/mapFeatureDetail.js index d745caa46..b664321a7 100644 --- a/opentreemap/treemap/js/src/mapFeatureDetail.js +++ b/opentreemap/treemap/js/src/mapFeatureDetail.js @@ -66,6 +66,7 @@ function init() { dataType: 'html' }); + // tzinckgraf imageLightbox.init({ imageFinishedStream: imageFinishedStream, imageContainer: '#photo-carousel', @@ -112,18 +113,19 @@ function init() { initDetail(form); - var refreshDetailUrl = reverse.map_feature_detail_partial({ + var refreshDetailUrl = reverse.Urls.map_feature_detail_partial({ instance_url_name: config.instance.url_name, feature_id: window.otm.mapFeature.featureId }), - refreshSidebarUrl = reverse.map_feature_sidebar({ + refreshSidebarUrl = reverse.Urls.map_feature_sidebar({ instance_url_name: config.instance.url_name, feature_id: window.otm.mapFeature.featureId }); form.saveOkStream.merge(imageFinishedStream) .onValue(function () { - $(dom.detail).load(refreshDetailUrl, initDetailAfterRefresh); - $(dom.sidebar).load(refreshSidebarUrl); + //$(dom.detail).load(refreshDetailUrl, initDetailAfterRefresh); + //$(dom.sidebar).load(refreshSidebarUrl); + location.reload(); }); form.inEditModeProperty.onValue(function(inEditMode) { @@ -192,18 +194,20 @@ function init() { if (config.instance.basemap.type === 'google') { var $streetViewContainer = $(dom.streetView); $streetViewContainer.show(); + /* var panorama = streetView.create({ streetViewElem: $streetViewContainer[0], noStreetViewText: config.trans.noStreetViewText, location: window.otm.mapFeature.location.point }); + */ form.saveOkStream .onValue(function () { // If location is an array, we are editing a polygonal map // feature. The page triggers a full postback after editing a // polygon map feature. if (!_.isArray(currentMover.location)) { - panorama.update(currentMover.location); + //panorama.update(currentMover.location); } }); } diff --git a/opentreemap/treemap/js/src/mapPage/addMapFeature.js b/opentreemap/treemap/js/src/mapPage/addMapFeature.js index 080506f30..46f0744c6 100644 --- a/opentreemap/treemap/js/src/mapPage/addMapFeature.js +++ b/opentreemap/treemap/js/src/mapPage/addMapFeature.js @@ -31,7 +31,7 @@ function init(options) { formSelector = options.formSelector, indexOfSetLocationStep = options.indexOfSetLocationStep, addFeatureRadioOptions = options.addFeatureRadioOptions, - addFeatureUrl = reverse.add_plot(config.instance.url_name), + addFeatureUrl = reverse.Urls.add_plot(config.instance.url_name), onSaveBefore = options.onSaveBefore || _.identity, stepControls = require('treemap/mapPage/stepControls.js').init($sidebar), @@ -43,12 +43,13 @@ function init(options) { $geolocateError = U.$find('.geolocate-error', $sidebar), $pointInStreamError = U.$find('.pointnotinmap-error', $sidebar), triggerSearchBus = options.triggerSearchBus, + addMapFeatureBus = options.addMapFeatureBus, $form = U.$find(formSelector, $sidebar), editFields = formSelector + ' [data-class="edit"]', validationFields = options.validationFields || formSelector + ' [data-class="error"]', $placeMarkerMessage = U.$find('.place-marker-message', $sidebar), - $moveMarkerMessage = U.$find('.move-marker-message', $sidebar), + $moveMarkerMessage = U.$find('.move-marker-message', $sidebar), boundsGeoJson = L.geoJson(config.instance.bounds); $(editFields).show(); @@ -243,12 +244,17 @@ function init(options) { } }); + var featureBusOnSuccess = [onAddFeatureSuccess, triggerSearchBus.push]; + if (addMapFeatureBus != null){ + featureBusOnSuccess.push(addMapFeatureBus.push) + } + $.ajax({ url: addFeatureUrl, type: 'POST', contentType: "application/json", data: JSON.stringify(data), - success: [onAddFeatureSuccess, triggerSearchBus.push], + success: featureBusOnSuccess, error: onAddFeatureError }); } @@ -289,7 +295,7 @@ function init(options) { break; case 'edit': close(); - var url = reverse.map_feature_detail_edit({ + var url = reverse.Urls.map_feature_detail_edit({ instance_url_name: config.instance.url_name, feature_id: result.featureId, edit: 'edit' diff --git a/opentreemap/treemap/js/src/mapPage/addResourceMode.js b/opentreemap/treemap/js/src/mapPage/addResourceMode.js index 1570b05ca..5941a0a67 100644 --- a/opentreemap/treemap/js/src/mapPage/addResourceMode.js +++ b/opentreemap/treemap/js/src/mapPage/addResourceMode.js @@ -55,7 +55,7 @@ function init(options) { skipDetailForm = $option.data('skip-detail-form') == 'True', enableDetailNext = $option.data('enable-detail-next') == 'True', enableContinueEditing = $option.data('is-editable') == 'True', - addFeatureUrl = reverse.add_map_feature({ + addFeatureUrl = reverse.Urls.add_map_feature({ instance_url_name: config.instance.url_name, type: type }); @@ -86,7 +86,7 @@ function init(options) { } $.ajax({ - url: reverse.add_map_feature({ + url: reverse.Urls.add_map_feature({ instance_url_name: config.instance.url_name, type: type }), diff --git a/opentreemap/treemap/js/src/mapPage/addTreeMode.js b/opentreemap/treemap/js/src/mapPage/addTreeMode.js index 01061af8c..0b56d21b7 100644 --- a/opentreemap/treemap/js/src/mapPage/addTreeMode.js +++ b/opentreemap/treemap/js/src/mapPage/addTreeMode.js @@ -1,12 +1,19 @@ "use strict"; var $ = require('jquery'), + Bacon = require('baconjs'), + BU = require('treemap/lib/baconUtils.js'), _ = require('lodash'), + FH = require('treemap/lib/fieldHelpers.js'), U = require('treemap/lib/utility.js'), + reverse = require('reverse'), addMapFeature = require('treemap/mapPage/addMapFeature.js'), + mapFeatureUdf = require('treemap/lib/mapFeatureUdf.js'), otmTypeahead = require('treemap/lib/otmTypeahead.js'), plotMarker = require('treemap/lib/plotMarker.js'), - diameterCalculator = require('treemap/lib/diameterCalculator.js'); + uploadPanel = require('treemap/lib/uploadPanelAddTreePhoto.js'), + diameterCalculator = require('treemap/lib/diameterCalculator.js'), + config = require('treemap/lib/config.js'); var activateMode = _.identity, deactivateMode = _.identity, @@ -15,15 +22,23 @@ var activateMode = _.identity, STEP_FINAL = 2; function init(options) { + var mapFeatureBus = new Bacon.Bus(); + options.addMapFeatureBus = mapFeatureBus; var $sidebar = $(options.sidebar), $speciesTypeahead = U.$find('#add-tree-species-typeahead', $sidebar), $summaryHead = U.$find('.summaryHead', $sidebar), + $isEmptySite = U.$find('#is-empty-site', $sidebar), $summarySubhead = U.$find('.summarySubhead', $sidebar), typeahead = otmTypeahead.create(options.typeahead), + clearEditControls = function() { typeahead.clear(); }, - manager = addMapFeature.init(_.extend({clearEditControls: clearEditControls}, options)); + manager = addMapFeature.init( + _.extend({ + clearEditControls: clearEditControls, + onSaveBefore: onSaveBefore}, options) + ); activateMode = function() { manager.activate(); @@ -31,6 +46,11 @@ function init(options) { plotMarker.useTreeIcon(true); plotMarker.enablePlacing(); $('body').addClass('add-feature'); + + // this is a hard reset on these fields + $('#empty-site-photos').hide(); + $('#tree-photos').show(); + $isEmptySite.prop('checked', false); }; deactivateMode = function() { @@ -38,6 +58,82 @@ function init(options) { manager.deactivate(); }; + var shapeImageFinishedStream = uploadPanel.init({ + panelId: '#shape-photo-upload', + dataType: 'html', + addMapFeatureBus: mapFeatureBus + }); + + var barkImageFinishedStream = uploadPanel.init({ + panelId: '#bark-photo-upload', + dataType: 'html', + addMapFeatureBus: mapFeatureBus + }); + + var leafImageFinishedStream = uploadPanel.init({ + panelId: '#leaf-photo-upload', + dataType: 'html', + addMapFeatureBus: mapFeatureBus + }); + + var emptySiteImageFinishedStream = uploadPanel.init({ + panelId: '#empty-site-photo-upload', + dataType: 'html', + addMapFeatureBus: mapFeatureBus + }); + // if we just mapped an empty site, then redo the photoUpload stream + // in case we partially mapped this + emptySiteImageFinishedStream.onValue(function(value) { + photoUploads = Bacon.combineAsArray( + shapeImageFinishedStream, + barkImageFinishedStream, + leafImageFinishedStream + ); + + photoUploads.onValue(function(value){ + photoUploads() + photoUploads = Bacon.combineAsArray( + shapeImageFinishedStream, + barkImageFinishedStream, + leafImageFinishedStream + ); + }); + }); + + var photoUploads = Bacon.combineAsArray( + shapeImageFinishedStream, + barkImageFinishedStream, + leafImageFinishedStream + ); + var photoUploadSubscription = photoUploads.onValue(function(value){ + if (value.length != 3 || !value.every(x => x['tree_id'] == value[0]['tree_id'])) + return; + + var treeId = value[0]['tree_id']; + + var url = reverse.Urls.inaturalist_create_observation_for_tree({ + instance_url_name: config.instance.url_name, + tree_id: treeId + }); + + var stream = BU.jsonRequest('POST', url)(); + stream.onValue(function() {console.log('submitted to iNat'); }); + }); + + // start off hidden + $('#empty-site-photos').hide(); + $isEmptySite.on('change', function() { + if(this.checked){ + $('#empty-site-photos').show(); + $('#tree-photos').hide(); + } else { + $('#empty-site-photos').hide(); + $('#tree-photos').show(); + } + }); + + mapFeatureUdf.init(null); + diameterCalculator({ formSelector: options.formSelector, cancelStream: manager.deactivateStream, saveOkStream: manager.addFeatureStream }); @@ -55,9 +151,59 @@ function init(options) { function aTreeFieldIsSet() { var data = manager.getFormData(); - return _.some(data, function (value, key) { - return key && key.indexOf('tree') === 0 && value; + var treeValueSet = _.some(data, function (value, key) { + return key && key.indexOf('tree') === 0 && value && value !== ""; + }); + + // either we have a tree value set, or we have not explicitly + // said this is an empty site + return treeValueSet || !$isEmptySite.is(":checked"); + } + + /// This is for the Stewardship + // By default collection udfs have their input row + // hidden, so show that row + $("table[data-udf-id] .editrow").css('display', ''); + // The header row may also be hidden if there are no + // items so show that as well + $("table[data-udf-id] .headerrow").css('display', ''); + $("table[data-udf-id] .placeholder").css('display', 'none'); + + // we also always want to show the indicator + $('span.required-indicator').html('* '); + + // before we save the data, grab any UDF data, such s Stewardship + function onSaveBefore(data) { + // Extract data for all rows of the collection, + // whether entered in this session or pre-existing. + $('table[data-udf-name]').map(function() { + var $table = $(this); + var name = $table.data('udf-name'); + + var headers = $table.find('tr.headerrow th') + .map(function() { + return $(this).html(); + }); + + headers = _.compact(headers); + + data[name] = + _.map($table.find('tr[data-value-id]').toArray(), function(row) { + var $row = $(row), + $tds = $row.find('td'), + id = $row.attr('data-value-id'), + + rowData = _.zipObject(headers, $tds + .map(function() { + return $.trim($(this).attr('data-value')); + })); + if (! _.isEmpty(id)) { + rowData.id = id; + } + return rowData; + }); }); + return data; } // In case we're adding another tree, make user move the marker diff --git a/opentreemap/treemap/js/src/mapPage/browseTreesMode.js b/opentreemap/treemap/js/src/mapPage/browseTreesMode.js index 9dc6e1180..7f84a24c6 100644 --- a/opentreemap/treemap/js/src/mapPage/browseTreesMode.js +++ b/opentreemap/treemap/js/src/mapPage/browseTreesMode.js @@ -28,7 +28,7 @@ var dom = { function idToPlotDetailUrl(id) { if (id) { - return reverse.map_feature_detail({ + return reverse.Urls.map_feature_detail({ instance_url_name: config.instance.url_name, feature_id: id }); @@ -127,14 +127,14 @@ function getPopupContent(utfGridEvent) { featureId = data ? data[config.utfGrid.mapfeatureIdKey] : null; if (featureId) { - return getPopup(reverse.map_feature_popup({ + return getPopup(reverse.Urls.map_feature_popup({ instance_url_name: config.instance.url_name, feature_id: featureId })); } else if (config.instance.canopyEnabled) { var latlng = utfGridEvent.latlng; - return getPopup(reverse.canopy_popup(config.instance.url_name) + + return getPopup(reverse.Urls.canopy_popup(config.instance.url_name) + format('?lng=%d&lat=%d', latlng.lng, latlng.lat)); } else { @@ -160,6 +160,7 @@ function makePopup(latLon, html) { var $popup = $(html); if (embed) { $popup.find('a').attr('target', '_blank'); + $popup.find('.popup-btns').css('display', 'none'); } var $popupContents = $($popup[0].outerHTML); @@ -207,7 +208,7 @@ function togglePopup(newPopup) { function getPlotAccordionContent(id) { var search = $.ajax({ - url: reverse.map_feature_accordion({ + url: reverse.Urls.map_feature_accordion({ instance_url_name: config.instance.url_name, feature_id: id }), diff --git a/opentreemap/treemap/js/src/mapPage/locationSearchUI.js b/opentreemap/treemap/js/src/mapPage/locationSearchUI.js index d4f8dcb0c..0a06054a3 100644 --- a/opentreemap/treemap/js/src/mapPage/locationSearchUI.js +++ b/opentreemap/treemap/js/src/mapPage/locationSearchUI.js @@ -25,7 +25,7 @@ var dom = { var map, polygon, customAreaSearchBus, - createAnonymousBoundary = BU.jsonRequest('POST', reverse.anonymous_boundary()); + createAnonymousBoundary = BU.jsonRequest('POST', reverse.Urls.anonymous_boundary()); function init(options) { var mapManager = options.mapManager; diff --git a/opentreemap/treemap/js/src/mapPage/modes.js b/opentreemap/treemap/js/src/mapPage/modes.js index fced17767..c5a1fb23d 100644 --- a/opentreemap/treemap/js/src/mapPage/modes.js +++ b/opentreemap/treemap/js/src/mapPage/modes.js @@ -203,13 +203,13 @@ function init(mapManager, triggerSearchBus, embed, completedSearchStream) { function getSpeciesTypeaheadOptions(idPrefix) { return { name: "species", - url: reverse.species_list_view(config.instance.url_name), + url: reverse.Urls.species_list_view(config.instance.url_name), input: "#" + idPrefix + "-typeahead", template: "#species-element-template", hidden: "#" + idPrefix + "-hidden", reverse: "id", forceMatch: true, - minLength: 1 + minLength: 0 }; } diff --git a/opentreemap/treemap/json_field.py b/opentreemap/treemap/json_field.py index c1a224ffd..1651c1f0e 100644 --- a/opentreemap/treemap/json_field.py +++ b/opentreemap/treemap/json_field.py @@ -8,7 +8,7 @@ class JSONField(models.TextField): def to_python(self, value): - if isinstance(value, basestring): + if isinstance(value, str): obj = json.loads(value or "{}") return DotDict(obj) if isinstance(obj, dict) else obj else: @@ -27,7 +27,7 @@ def value_to_string(self, obj): value = self._get_val_from_obj(obj) return self.get_prep_value(value) - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection, context=None): return self.to_python(value) diff --git a/opentreemap/treemap/lib/__init__.py b/opentreemap/treemap/lib/__init__.py index 74eb39bba..153f05102 100644 --- a/opentreemap/treemap/lib/__init__.py +++ b/opentreemap/treemap/lib/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import re @@ -24,8 +22,8 @@ def format_benefits(instance, benefits, basis, digits=None): # FYI: this mutates the underlying benefit dictionaries total_currency = 0 - for benefit_group in benefits.values(): - for key, benefit in benefit_group.iteritems(): + for benefit_group in list(benefits.values()): + for key, benefit in benefit_group.items(): if benefit['currency'] is not None: # TODO: Use i18n/l10n to format currency benefit['currency_saved'] = currency_symbol + number_format( diff --git a/opentreemap/treemap/lib/external_link.py b/opentreemap/treemap/lib/external_link.py index 0ebd3b75b..e128689b7 100644 --- a/opentreemap/treemap/lib/external_link.py +++ b/opentreemap/treemap/lib/external_link.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import re from django.utils.translation import ugettext_lazy as _ +from functools import reduce # Utilities for external links @@ -34,7 +33,7 @@ def _re_group_count(compiled, text): return reduce( lambda ac, groupdict: { k: v if groupdict[k] is None else v + 1 - for k, v in ac.iteritems()}, + for k, v in ac.items()}, [m.groupdict() for m in compiled.finditer(text)], totals) @@ -48,4 +47,4 @@ def get_url_tokens_for_display(in_bold=False): show = lambda t: ('<b>#{%s}</b>' if in_bold else '#{%s}') % t return (', '.join(show(token) for token in _valid_url_tokens[:-1]) + - unicode(_(' or ')) + show(_valid_url_tokens[-1])) + str(_(' or ')) + show(_valid_url_tokens[-1])) diff --git a/opentreemap/treemap/lib/hide_at_zoom.py b/opentreemap/treemap/lib/hide_at_zoom.py index 27a353c94..58134c435 100644 --- a/opentreemap/treemap/lib/hide_at_zoom.py +++ b/opentreemap/treemap/lib/hide_at_zoom.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from math import floor diff --git a/opentreemap/treemap/lib/map_feature.py b/opentreemap/treemap/lib/map_feature.py index ba386ce3d..bd7f44f3f 100644 --- a/opentreemap/treemap/lib/map_feature.py +++ b/opentreemap/treemap/lib/map_feature.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import datetime from string import Template from django.conf import settings -from django.core.urlresolvers import reverse +from django.urls import reverse from django.core.exceptions import PermissionDenied from django.http import Http404 from django.shortcuts import get_object_or_404 @@ -198,6 +196,7 @@ def context_dict_for_plot(request, plot, tree_id=None, **kwargs): if tree: tree.convert_to_display_units() + context['units'] = {**(tree.units() if tree else {}), **plot.units()} if tree is not None: photos = tree.photos() # can't send a regular photo qs because the API will @@ -278,7 +277,7 @@ class UrlTemplate(Template): 'feature_id': plot.pk} if tree: url_name = 'add_photo_to_tree' - url_kwargs = dict(url_kwargs.items() + [('tree_id', tree.pk)]) + url_kwargs = dict(list(url_kwargs.items()) + [('tree_id', tree.pk)]) else: url_name = 'add_photo_to_plot' @@ -327,23 +326,24 @@ def context_dict_for_resource(request, resource, **kwargs): if has_photos: completed_progress_items += 1 - context['upload_photo_endpoint'] = reverse( - 'add_photo_to_map_feature', - kwargs={'instance_url_name': instance.url_name, - 'feature_id': resource.pk}) + if resource.pk: + context['upload_photo_endpoint'] = reverse( + 'add_photo_to_map_feature', + kwargs={'instance_url_name': instance.url_name, + 'feature_id': resource.pk}) - context['progress_percent'] = int(100 * ( - completed_progress_items / total_progress_items) + .5) + context['progress_percent'] = int(100 * ( + completed_progress_items / total_progress_items) + .5) - context['progress_messages'] = [] - if not has_photos: - context['progress_messages'].append(_('Add a photo')) + context['progress_messages'] = [] + if not has_photos: + context['progress_messages'].append(_('Add a photo')) - audits = _map_feature_audits(request.user, request.instance, resource) + audits = _map_feature_audits(request.user, request.instance, resource) - _add_audits_to_context(audits, context) + _add_audits_to_context(audits, context) - _add_share_context(context, request, photos) + _add_share_context(context, request, photos) object_name_alias = to_object_name(context['feature'].__class__.__name__) # some features that were originally written to support plot and tree @@ -386,7 +386,7 @@ def context_dict_for_map_feature(request, feature, edit=False): feature.instance = instance # save a DB lookup user = request.user - if user and user.is_authenticated(): + if user and user.is_authenticated: favorited = Favorite.objects \ .filter(map_feature=feature, user=user).exists() else: @@ -396,7 +396,7 @@ def context_dict_for_map_feature(request, feature, edit=False): # which prevents the Favorite query above from ever returning # True. To avoid that we need to do the field masking after # setting the favorited flag. - if user and user.is_authenticated(): + if user and user.is_authenticated: feature.mask_unauthorized_fields(user) feature.convert_to_display_units() diff --git a/opentreemap/treemap/lib/object_caches.py b/opentreemap/treemap/lib/object_caches.py index 6e5d0d391..af327f763 100644 --- a/opentreemap/treemap/lib/object_caches.py +++ b/opentreemap/treemap/lib/object_caches.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf import settings from django.db.models import F diff --git a/opentreemap/treemap/lib/page_of_items.py b/opentreemap/treemap/lib/page_of_items.py index 2995ed1f7..2df0ff618 100644 --- a/opentreemap/treemap/lib/page_of_items.py +++ b/opentreemap/treemap/lib/page_of_items.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division -from django.core.urlresolvers import reverse + +from django.urls import reverse class UrlParams(object): @@ -17,8 +15,8 @@ def params(self, *keys): def url(self, *keys, **overrides): params = self._params if overrides: - params = dict(params.items() + overrides.items()) - keys = params.keys() + params = dict(list(params.items()) + list(overrides.items())) + keys = list(params.keys()) return self._url + self._param_string(keys, params) @staticmethod diff --git a/opentreemap/treemap/lib/perms.py b/opentreemap/treemap/lib/perms.py index c10fa6e37..8b461eb67 100644 --- a/opentreemap/treemap/lib/perms.py +++ b/opentreemap/treemap/lib/perms.py @@ -1,6 +1,4 @@ -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import inspect diff --git a/opentreemap/treemap/lib/photo.py b/opentreemap/treemap/lib/photo.py index f1c1bea7d..bb12f9a7a 100644 --- a/opentreemap/treemap/lib/photo.py +++ b/opentreemap/treemap/lib/photo.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division -from urlparse import urlparse, urlunparse -from django.core.urlresolvers import reverse +from urllib.parse import urlparse, urlunparse + +from django.urls import reverse def _drop_querystring(url): @@ -34,6 +32,16 @@ def context_dict_for_photo(request, photo): photo_dict['thumbnail'] = thumbnail_url photo_dict['raw'] = photo + # add the label + # TODO use a OneToOne mapping + labels = photo.mapfeaturephotolabel_set.all() + if labels: + photo_dict['has_label'] = True + photo_dict['label'] = labels[0].name + photo_dict['label_id'] = labels[0].id + else: + photo_dict['has_label'] = False + url = reverse( 'map_feature_photo_detail', kwargs={'instance_url_name': photo.map_feature.instance.url_name, diff --git a/opentreemap/treemap/lib/tree.py b/opentreemap/treemap/lib/tree.py index fb9a84281..faf4d3bae 100644 --- a/opentreemap/treemap/lib/tree.py +++ b/opentreemap/treemap/lib/tree.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.http import Http404 diff --git a/opentreemap/treemap/lib/user.py b/opentreemap/treemap/lib/user.py index 2a9f87a13..ef8b608e7 100644 --- a/opentreemap/treemap/lib/user.py +++ b/opentreemap/treemap/lib/user.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.db.models import Q @@ -186,7 +184,7 @@ def get_audits_params(request): def user_accessible_instance_filter(logged_in_user): public = Q(is_public=True) - if logged_in_user is not None and not logged_in_user.is_anonymous(): + if logged_in_user is not None and not logged_in_user.is_anonymous: private_with_access = Q(instanceuser__user=logged_in_user) instance_filter = public | private_with_access diff --git a/opentreemap/treemap/management/commands/create_instance.py b/opentreemap/treemap/management/commands/create_instance.py index 9a5dff2f5..1d3ba8a92 100755 --- a/opentreemap/treemap/management/commands/create_instance.py +++ b/opentreemap/treemap/management/commands/create_instance.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import logging diff --git a/opentreemap/treemap/management/commands/create_system_user.py b/opentreemap/treemap/management/commands/create_system_user.py index 6b28f48e2..48beb8722 100644 --- a/opentreemap/treemap/management/commands/create_system_user.py +++ b/opentreemap/treemap/management/commands/create_system_user.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.core.management.base import BaseCommand from django.conf import settings diff --git a/opentreemap/treemap/management/commands/delete_data.py b/opentreemap/treemap/management/commands/delete_data.py index 88bcbcce7..0f88ccc96 100644 --- a/opentreemap/treemap/management/commands/delete_data.py +++ b/opentreemap/treemap/management/commands/delete_data.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from treemap.management.util import InstanceDataCommand diff --git a/opentreemap/treemap/management/commands/migrate_to_s3.py b/opentreemap/treemap/management/commands/migrate_to_s3.py new file mode 100755 index 000000000..6fbd9c5b0 --- /dev/null +++ b/opentreemap/treemap/management/commands/migrate_to_s3.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +import logging + +from django.core.management.base import BaseCommand +from django.db import transaction + +from django.contrib.gis.geos import GEOSGeometry, Point + +from treemap.models import MapFeaturePhoto + +from django.conf import settings + +logger = logging.getLogger('') + + +class Command(BaseCommand): + """ + Migrate all images from the file + """ + + def add_arguments(self, parser): + pass + + @transaction.atomic + def handle(self, *args, **options): + photos = MapFeaturePhoto.objects.all() + + for photo in photos: + try: + filename = '{}/{}'.format(settings.MEDIA_ROOT, photo.image.name) + with open(filename) as image: + photos.set_image(image) + photos.save() + logger.info("Migrated id {}".format(photo.id)) + except Exception as e: + logger.exception("Could not load id {}, {}".format(photo.id, filename)) diff --git a/opentreemap/treemap/management/commands/random_trees.py b/opentreemap/treemap/management/commands/random_trees.py index ddc3b66bc..2f636d9b7 100644 --- a/opentreemap/treemap/management/commands/random_trees.py +++ b/opentreemap/treemap/management/commands/random_trees.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import random import math @@ -74,7 +72,7 @@ def handle(self, *args, **options): ct = 0 cp = 0 - for i in xrange(0, n): + for i in range(0, n): mktree = random.random() < tree_prob radius = random.gauss(0.0, max_radius) theta = random.random() * 2.0 * math.pi diff --git a/opentreemap/treemap/management/commands/set_mapfeature_updated_at.py b/opentreemap/treemap/management/commands/set_mapfeature_updated_at.py index aaf407bf1..06688aee0 100644 --- a/opentreemap/treemap/management/commands/set_mapfeature_updated_at.py +++ b/opentreemap/treemap/management/commands/set_mapfeature_updated_at.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.core.management.base import BaseCommand diff --git a/opentreemap/treemap/management/commands/set_mapfeature_updated_by.py b/opentreemap/treemap/management/commands/set_mapfeature_updated_by.py index e0e2dceaf..d69f7a0a0 100644 --- a/opentreemap/treemap/management/commands/set_mapfeature_updated_by.py +++ b/opentreemap/treemap/management/commands/set_mapfeature_updated_by.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.core.management.base import BaseCommand diff --git a/opentreemap/treemap/management/commands/setup_groups.py b/opentreemap/treemap/management/commands/setup_groups.py new file mode 100755 index 000000000..30ed6ef27 --- /dev/null +++ b/opentreemap/treemap/management/commands/setup_groups.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +import csv +import logging +from tempfile import TemporaryFile + +from django.core.management.base import BaseCommand +from django.db import transaction + +from django.contrib.gis.geos import GEOSGeometry, Point + +from treemap.instance import (Instance, InstanceBounds, + create_stewardship_udfs, + add_species_to_instance) +from treemap.models import (InstanceUser, User, NeighborhoodGroup) +from treemap.audit import (Role, FieldPermission, add_default_permissions, + add_instance_permissions) + +from exporter.group import write_groups + +# FIXME should this be an InstanceGroup? With only one instance, no need +from django.contrib.auth.models import Group + + +logger = logging.getLogger('') + + +class Command(BaseCommand): + """ + Create a new instance with a single editing role. + """ + def add_arguments(self, parser): + parser.add_argument( + 'instance_name', + help='Specify instance name'), + parser.add_argument( + '--filename', + dest='filename', + help='File for setting up groups'), + parser.add_argument( + '--report', + action='store_true', + dest='report', + help='Run a sample report'), + + @transaction.atomic + def handle(self, *args, **options): + instance_name = options['instance_name'] + instance = Instance.objects.get(name=instance_name) + + if options.get('report'): + self.run_report(instance) + return + + filename = options['filename'] + with open(filename, mode='r') as csv_file: + csv_reader = csv.DictReader(csv_file) + line_count = 0 + for row in csv_reader: + try: + user = User.objects.get(email=row['Email']) + except: + continue + group, _ = NeighborhoodGroup.objects.get_or_create( + name='{} - {}'.format(row['Ward'], row['Neighborhood']), + ward=row['Ward'], + neighborhood=row['Neighborhood'] + ) + group.user_set.add(user) + group.save() + + def run_report(self, instance): + filename = 'groups.csv' + file_obj = TemporaryFile() + write_groups(file_obj, instance) diff --git a/opentreemap/treemap/management/commands/submit_to_inaturalist.py b/opentreemap/treemap/management/commands/submit_to_inaturalist.py new file mode 100755 index 000000000..826c8bd5a --- /dev/null +++ b/opentreemap/treemap/management/commands/submit_to_inaturalist.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +import logging +import time + +from django.core.management.base import BaseCommand +from django.db import transaction + +from treemap.instance import Instance +from opentreemap.integrations import inaturalist +from django.db import IntegrityError, connection, transaction + +from django.conf import settings + +logger = logging.getLogger('') + +class Command(BaseCommand): + """ + Migrate all images from the file + """ + + def add_arguments(self, parser): + parser.add_argument( + 'instance_name', + help='Specify instance name'), + + @transaction.atomic + def handle(self, *args, **options): + instance_name = options['instance_name'] + instance = Instance.objects.get(name=instance_name) + + trees = inaturalist.get_features_for_inaturalist() + + logger.debug('{} trees to add'.format(len(trees))) + inaturalist.create_observations(instance, tree_id=654) + + for tree in trees: + try: + inaturalist.create_observations(instance, tree_id=tree['tree_id']) + except Exception as e: + logger.exception('Could not run tree_id {tree_id} plot {plot_id}'.format(**tree), e) + pass diff --git a/opentreemap/treemap/management/commands/upload_boundary.py b/opentreemap/treemap/management/commands/upload_boundary.py new file mode 100755 index 000000000..caf3fafb0 --- /dev/null +++ b/opentreemap/treemap/management/commands/upload_boundary.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +import csv +import json +import requests +import logging + +from django.core.management.base import BaseCommand +from django.db import transaction + +from django.contrib.gis.utils import LayerMapping + +from django.contrib.gis.db import models +from django.contrib.gis.geos import GEOSGeometry, Point, MultiPolygon + +from treemap.instance import (Instance, InstanceBounds, + create_stewardship_udfs, + add_species_to_instance) +from treemap.models import (Boundary, InstanceUser, User, + BenefitCurrencyConversion) +from treemap.audit import (Role, FieldPermission, add_default_permissions, + add_instance_permissions) + +logger = logging.getLogger('') + + +class JCNeighborhoodsOTM(models.Model): + nghbhd = models.CharField(max_length=50) + main_nghbh = models.CharField(max_length=25) + geom = models.MultiPolygonField(srid=4326) + + +class Command(BaseCommand): + """ + Create a new instance with a single editing role. + """ + + def add_arguments(self, parser): + parser.add_argument( + 'instance_name', + help='Specify instance name'), + parser.add_argument( + '--filename', + dest='filename', + help=('Specify a boundary via a geojson file. Must be ' + 'projected in EPSG:4326')), + parser.add_argument( + '--park', + action='store_true', + help='Is a parks file from JC OpenData website'), + + @transaction.atomic + def handle(self, *args, **options): + instance_name = options['instance_name'] + instance = Instance.objects.get(name=instance_name) + + #self.create_neighborhoods(instance) + self.create_park_boundary(instance) + + + def create_park_boundary(self, instance): + + url = 'https://data.jerseycitynj.gov/api/records/1.0/search/?rows={rows}&location=13,40.72164,-74.06642&basemap=jawg.light&start={start}&fields=park,govt,area,acre,geo_point_2d,geo_shape&dataset=jersey-city-parks-map&timezone=America%2FNew_York&lang=en' + row_count = 20 + offset = 0 + + records = [] + + while True: + request = requests.get(url.format(rows=row_count, start=offset)) + if not request.ok: + raise Exception('Problem with request') + break + + records_request = request.json()['records'] + records.extend(records_request) + + if not records_request: + break + + offset += len(records_request) + + boundaries = [] + for record in records: + fields = record['fields'] + geojson = '{}'.format(fields['geo_shape']) + geom = GEOSGeometry(json.dumps(fields['geo_shape']), srid=4326) + if geom.geom_type == 'Polygon': + geom = MultiPolygon(geom, srid=4326) + boundary = Boundary( + geom=geom, + name=fields.get('park', 'Missing Name'), + category='Park', + searchable=True, + sort_order=0 + ) + boundaries.append(boundary) + + """ + ** query for turning this into a GeoJSON file + + SELECT row_to_json(fc) + FROM ( + SELECT 'FeatureCollection' As type, array_to_json(array_agg(f)) As features + FROM ( + SELECT 'Feature' As type + , ST_AsGeoJSON(ST_Transform(lg.the_geom_webmercator, 4326))::json As geometry + , row_to_json(lp) As properties + FROM treemap_boundary As lg + INNER JOIN (SELECT id, name FROM treemap_boundary) As lp + ON lg.id = lp.id + where lg.category = 'Park' + and lg.name ilike '%lincoln%park%' + ) As f + ) As fc + """ + Boundary.objects.bulk_create(boundaries) + + def create_neighborhoods(self, instance): + """ + python manage.py ogrinspect ~/code/opentreemap/otm-core/data/JCNeighborhoodsOTM.shp JCNeighborhoodsOTM --srid=4326 --mapping --multi + """ + jcneighborhoodsotm_mapping = { + 'nghbhd' : 'Nghbhd', + 'main_nghbh' : 'Main_Nghbh', + 'geom' : 'MULTIPOLYGON', + + } + + shapefile = '/home/tzinckgraf/code/opentreemap/otm-core/data/JCNeighborhoodsOTM.shp' + lm = LayerMapping( + JCNeighborhoodsOTM, + shapefile, + jcneighborhoodsotm_mapping, + transform=False + ) + + import ipdb; ipdb.set_trace() # BREAKPOINT + pass diff --git a/opentreemap/treemap/management/util.py b/opentreemap/treemap/management/util.py index e474b1030..3c2036cad 100644 --- a/opentreemap/treemap/management/util.py +++ b/opentreemap/treemap/management/util.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.core.management.base import BaseCommand from django.db import connection, transaction diff --git a/opentreemap/treemap/migrations/0001_initial.py b/opentreemap/treemap/migrations/0001_initial.py index ca430dfab..8c0ddabf5 100644 --- a/opentreemap/treemap/migrations/0001_initial.py +++ b/opentreemap/treemap/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations import re @@ -140,7 +140,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('reputation', models.IntegerField(default=0)), ('admin', models.BooleanField(default=False)), - ('instance', models.ForeignKey(to='treemap.Instance')), + ('instance', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance')), ], bases=(treemap.audit.Auditable, models.Model), ), @@ -197,7 +197,7 @@ class Migration(migrations.Migration): ('direct_write_score', models.IntegerField(null=True, blank=True)), ('approval_score', models.IntegerField(null=True, blank=True)), ('denial_score', models.IntegerField(null=True, blank=True)), - ('instance', models.ForeignKey(to='treemap.Instance')), + ('instance', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance')), ], ), migrations.CreateModel( @@ -207,7 +207,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=255)), ('default_permission', models.IntegerField(default=0, choices=[(0, 'None'), (1, 'Read Only'), (2, 'Write with Audit'), (3, 'Write Directly')])), ('rep_thresh', models.IntegerField()), - ('instance', models.ForeignKey(blank=True, to='treemap.Instance', null=True)), + ('instance', models.ForeignKey(on_delete=models.CASCADE, blank=True, to='treemap.Instance', null=True)), ], ), migrations.CreateModel( @@ -233,7 +233,7 @@ class Migration(migrations.Migration): ('max_diameter', models.IntegerField(default=200)), ('max_height', models.IntegerField(default=800)), ('updated_at', models.DateTimeField(db_index=True, auto_now=True, null=True)), - ('instance', models.ForeignKey(to='treemap.Instance')), + ('instance', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance')), ], options={ 'verbose_name_plural': 'Species', @@ -246,7 +246,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=100)), ('content', models.TextField()), - ('instance', models.ForeignKey(to='treemap.Instance')), + ('instance', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance')), ], ), migrations.CreateModel( @@ -260,8 +260,8 @@ class Migration(migrations.Migration): ('canopy_height', models.FloatField(help_text='Canopy Height', null=True, blank=True)), ('date_planted', models.DateField(help_text='Date Planted', null=True, blank=True)), ('date_removed', models.DateField(help_text='Date Removed', null=True, blank=True)), - ('instance', models.ForeignKey(to='treemap.Instance')), - ('species', models.ForeignKey(blank=True, to='treemap.Species', help_text='Species', null=True)), + ('instance', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance')), + ('species', models.ForeignKey(on_delete=models.CASCADE, blank=True, to='treemap.Species', help_text='Species', null=True)), ], options={ 'abstract': False, @@ -285,13 +285,13 @@ class Migration(migrations.Migration): ('datatype', models.TextField()), ('iscollection', models.BooleanField()), ('name', models.CharField(max_length=255)), - ('instance', models.ForeignKey(to='treemap.Instance')), + ('instance', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance')), ], ), migrations.CreateModel( name='Plot', fields=[ - ('mapfeature_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='treemap.MapFeature')), + ('mapfeature_ptr', models.OneToOneField(on_delete=models.CASCADE, parent_link=True, auto_created=True, primary_key=True, serialize=False, to='treemap.MapFeature')), ('width', models.FloatField(help_text='Plot Width', null=True, blank=True)), ('length', models.FloatField(help_text='Plot Length', null=True, blank=True)), ('owner_orig_id', models.CharField(max_length=255, null=True, blank=True)), @@ -304,60 +304,60 @@ class Migration(migrations.Migration): migrations.CreateModel( name='TreePhoto', fields=[ - ('mapfeaturephoto_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='treemap.MapFeaturePhoto')), - ('tree', models.ForeignKey(to='treemap.Tree')), + ('mapfeaturephoto_ptr', models.OneToOneField(on_delete=models.CASCADE, parent_link=True, auto_created=True, primary_key=True, serialize=False, to='treemap.MapFeaturePhoto')), + ('tree', models.ForeignKey(on_delete=models.CASCADE, to='treemap.Tree')), ], bases=('treemap.mapfeaturephoto',), ), migrations.AddField( model_name='userdefinedcollectionvalue', name='field_definition', - field=models.ForeignKey(to='treemap.UserDefinedFieldDefinition'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.UserDefinedFieldDefinition'), ), migrations.AddField( model_name='mapfeaturephoto', name='instance', - field=models.ForeignKey(to='treemap.Instance'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance'), ), migrations.AddField( model_name='mapfeaturephoto', name='map_feature', - field=models.ForeignKey(to='treemap.MapFeature'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.MapFeature'), ), migrations.AddField( model_name='mapfeature', name='instance', - field=models.ForeignKey(to='treemap.Instance'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance'), ), migrations.AddField( model_name='itreecodeoverride', name='instance_species', - field=models.ForeignKey(to='treemap.Species'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Species'), ), migrations.AddField( model_name='itreecodeoverride', name='region', - field=models.ForeignKey(to='treemap.ITreeRegion'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.ITreeRegion'), ), migrations.AddField( model_name='instanceuser', name='role', - field=models.ForeignKey(to='treemap.Role'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Role'), ), migrations.AddField( model_name='instanceuser', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='instance', name='default_role', - field=models.ForeignKey(related_name='default_role', to='treemap.Role'), + field=models.ForeignKey(on_delete=models.CASCADE, related_name='default_role', to='treemap.Role'), ), migrations.AddField( model_name='instance', name='eco_benefits_conversion', - field=models.ForeignKey(blank=True, to='treemap.BenefitCurrencyConversion', null=True), + field=models.ForeignKey(on_delete=models.CASCADE, blank=True, to='treemap.BenefitCurrencyConversion', null=True), ), migrations.AddField( model_name='instance', @@ -367,42 +367,42 @@ class Migration(migrations.Migration): migrations.AddField( model_name='fieldpermission', name='instance', - field=models.ForeignKey(to='treemap.Instance'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Instance'), ), migrations.AddField( model_name='fieldpermission', name='role', - field=models.ForeignKey(to='treemap.Role'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Role'), ), migrations.AddField( model_name='favorite', name='map_feature', - field=models.ForeignKey(to='treemap.MapFeature'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.MapFeature'), ), migrations.AddField( model_name='favorite', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='audit', name='instance', - field=models.ForeignKey(blank=True, to='treemap.Instance', null=True), + field=models.ForeignKey(on_delete=models.CASCADE, blank=True, to='treemap.Instance', null=True), ), migrations.AddField( model_name='audit', name='ref', - field=models.ForeignKey(to='treemap.Audit', null=True), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Audit', null=True), ), migrations.AddField( model_name='audit', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='tree', name='plot', - field=models.ForeignKey(to='treemap.Plot'), + field=models.ForeignKey(on_delete=models.CASCADE, to='treemap.Plot'), ), migrations.AlterUniqueTogether( name='species', diff --git a/opentreemap/treemap/migrations/0002_add_itree_regions_20150701_1809.py b/opentreemap/treemap/migrations/0002_add_itree_regions_20150701_1809.py index 6da71bf1b..5903e8d2c 100644 --- a/opentreemap/treemap/migrations/0002_add_itree_regions_20150701_1809.py +++ b/opentreemap/treemap/migrations/0002_add_itree_regions_20150701_1809.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.core.management import call_command from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0003_change_audit_id_to_big_int_20150708_1612.py b/opentreemap/treemap/migrations/0003_change_audit_id_to_big_int_20150708_1612.py index 90ae9b23f..6d983b78a 100644 --- a/opentreemap/treemap/migrations/0003_change_audit_id_to_big_int_20150708_1612.py +++ b/opentreemap/treemap/migrations/0003_change_audit_id_to_big_int_20150708_1612.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0004_auto_20150720_1523.py b/opentreemap/treemap/migrations/0004_auto_20150720_1523.py index db189ff18..f042d542d 100644 --- a/opentreemap/treemap/migrations/0004_auto_20150720_1523.py +++ b/opentreemap/treemap/migrations/0004_auto_20150720_1523.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0005_auto_20150729_1046.py b/opentreemap/treemap/migrations/0005_auto_20150729_1046.py index da57a2c58..826cdc509 100644 --- a/opentreemap/treemap/migrations/0005_auto_20150729_1046.py +++ b/opentreemap/treemap/migrations/0005_auto_20150729_1046.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0006_stop_tracking_polygonal_mapfeature_ptr.py b/opentreemap/treemap/migrations/0006_stop_tracking_polygonal_mapfeature_ptr.py index fb95de372..125d5b32b 100644 --- a/opentreemap/treemap/migrations/0006_stop_tracking_polygonal_mapfeature_ptr.py +++ b/opentreemap/treemap/migrations/0006_stop_tracking_polygonal_mapfeature_ptr.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0007_auto_20150902_1534.py b/opentreemap/treemap/migrations/0007_auto_20150902_1534.py index 537fe45cc..1a54cdb78 100644 --- a/opentreemap/treemap/migrations/0007_auto_20150902_1534.py +++ b/opentreemap/treemap/migrations/0007_auto_20150902_1534.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0008_instance_eco_rev.py b/opentreemap/treemap/migrations/0008_instance_eco_rev.py index 577a95413..e14033b10 100644 --- a/opentreemap/treemap/migrations/0008_instance_eco_rev.py +++ b/opentreemap/treemap/migrations/0008_instance_eco_rev.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0009_restructure_replaceable_terms.py b/opentreemap/treemap/migrations/0009_restructure_replaceable_terms.py index f4a53b2fa..a24e903ec 100644 --- a/opentreemap/treemap/migrations/0009_restructure_replaceable_terms.py +++ b/opentreemap/treemap/migrations/0009_restructure_replaceable_terms.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations diff --git a/opentreemap/treemap/migrations/0010_eliminate_warnings_fields_w340.py b/opentreemap/treemap/migrations/0010_eliminate_warnings_fields_w340.py index c08de0a88..fd39ad74c 100644 --- a/opentreemap/treemap/migrations/0010_eliminate_warnings_fields_w340.py +++ b/opentreemap/treemap/migrations/0010_eliminate_warnings_fields_w340.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations from django.conf import settings diff --git a/opentreemap/treemap/migrations/0011_instance_universal_rev.py b/opentreemap/treemap/migrations/0011_instance_universal_rev.py index 4ebd903e7..fe3abe17d 100644 --- a/opentreemap/treemap/migrations/0011_instance_universal_rev.py +++ b/opentreemap/treemap/migrations/0011_instance_universal_rev.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0012_help_text_to_verbose_name.py b/opentreemap/treemap/migrations/0012_help_text_to_verbose_name.py index 76c59aa2f..87a953ded 100644 --- a/opentreemap/treemap/migrations/0012_help_text_to_verbose_name.py +++ b/opentreemap/treemap/migrations/0012_help_text_to_verbose_name.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations import django.utils.timezone @@ -70,6 +70,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='tree', name='species', - field=models.ForeignKey(verbose_name='Species', blank=True, to='treemap.Species', null=True), + field=models.ForeignKey(on_delete=models.CASCADE, verbose_name='Species', blank=True, to='treemap.Species', null=True), ), ] diff --git a/opentreemap/treemap/migrations/0013_mapfeature_hide_at_zoom.py b/opentreemap/treemap/migrations/0013_mapfeature_hide_at_zoom.py index f16bf2cb8..909da4cc7 100644 --- a/opentreemap/treemap/migrations/0013_mapfeature_hide_at_zoom.py +++ b/opentreemap/treemap/migrations/0013_mapfeature_hide_at_zoom.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0014_change_empty_multichoice_values.py b/opentreemap/treemap/migrations/0014_change_empty_multichoice_values.py index 84551d661..8d1cfd94e 100644 --- a/opentreemap/treemap/migrations/0014_change_empty_multichoice_values.py +++ b/opentreemap/treemap/migrations/0014_change_empty_multichoice_values.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations from treemap.udf import UDFDictionary @@ -28,9 +28,9 @@ def set_empty_multichoice_values_to_none(apps, schema_editor): super(UDFDictionary, obj.udfs).__setitem__(udfd.name, None) obj.save_base() if len(objs) > 0: - print('Updated %s empty multichoice values for %s udf "%s" (%s)' + print(('Updated %s empty multichoice values for %s udf "%s" (%s)' % (len(objs), udfd.model_type, udfd.name, - udfd.instance.url_name)) + udfd.instance.url_name))) class Migration(migrations.Migration): diff --git a/opentreemap/treemap/migrations/0015_add_separate_instance_bounds_model.py b/opentreemap/treemap/migrations/0015_add_separate_instance_bounds_model.py index a25ff75df..3e497425c 100644 --- a/opentreemap/treemap/migrations/0015_add_separate_instance_bounds_model.py +++ b/opentreemap/treemap/migrations/0015_add_separate_instance_bounds_model.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models import django.contrib.gis.db.models.fields @@ -22,6 +22,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='instance', name='bounds_obj', - field=models.OneToOneField(null=True, blank=True, to='treemap.InstanceBounds'), + field=models.OneToOneField(on_delete=models.CASCADE, null=True, blank=True, to='treemap.InstanceBounds'), ), ] diff --git a/opentreemap/treemap/migrations/0015_instanceuser_last_seen.py b/opentreemap/treemap/migrations/0015_instanceuser_last_seen.py index ebdea84a3..dad1dc605 100644 --- a/opentreemap/treemap/migrations/0015_instanceuser_last_seen.py +++ b/opentreemap/treemap/migrations/0015_instanceuser_last_seen.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0016_make_bounds_nullable.py b/opentreemap/treemap/migrations/0016_make_bounds_nullable.py index 92f520b1c..58a3ab044 100644 --- a/opentreemap/treemap/migrations/0016_make_bounds_nullable.py +++ b/opentreemap/treemap/migrations/0016_make_bounds_nullable.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models import django.contrib.gis.db.models.fields diff --git a/opentreemap/treemap/migrations/0017_copy_bounds_to_separate_model.py b/opentreemap/treemap/migrations/0017_copy_bounds_to_separate_model.py index eae80f788..b6b07d5fb 100644 --- a/opentreemap/treemap/migrations/0017_copy_bounds_to_separate_model.py +++ b/opentreemap/treemap/migrations/0017_copy_bounds_to_separate_model.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0018_add_species_field_names.py b/opentreemap/treemap/migrations/0018_add_species_field_names.py index 4c90485cd..7002348e1 100644 --- a/opentreemap/treemap/migrations/0018_add_species_field_names.py +++ b/opentreemap/treemap/migrations/0018_add_species_field_names.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0018_merge.py b/opentreemap/treemap/migrations/0018_merge.py index 080559c3f..118fc8a1c 100644 --- a/opentreemap/treemap/migrations/0018_merge.py +++ b/opentreemap/treemap/migrations/0018_merge.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0019_merge.py b/opentreemap/treemap/migrations/0019_merge.py index e684064e8..71aaed998 100644 --- a/opentreemap/treemap/migrations/0019_merge.py +++ b/opentreemap/treemap/migrations/0019_merge.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations diff --git a/opentreemap/treemap/migrations/0020_remove_instance_bounds.py b/opentreemap/treemap/migrations/0020_remove_instance_bounds.py index 17e91080f..a3158d7f0 100644 --- a/opentreemap/treemap/migrations/0020_remove_instance_bounds.py +++ b/opentreemap/treemap/migrations/0020_remove_instance_bounds.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0021_rename_bounds_obj_to_bounds.py b/opentreemap/treemap/migrations/0021_rename_bounds_obj_to_bounds.py index 10cb29165..8782ef0d6 100644 --- a/opentreemap/treemap/migrations/0021_rename_bounds_obj_to_bounds.py +++ b/opentreemap/treemap/migrations/0021_rename_bounds_obj_to_bounds.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0022_add_esri_basemap_type_choice.py b/opentreemap/treemap/migrations/0022_add_esri_basemap_type_choice.py index ea8e051eb..da43270e3 100644 --- a/opentreemap/treemap/migrations/0022_add_esri_basemap_type_choice.py +++ b/opentreemap/treemap/migrations/0022_add_esri_basemap_type_choice.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0022_remove_null_from_eco_rev.py b/opentreemap/treemap/migrations/0022_remove_null_from_eco_rev.py index 7656473f3..1bafe358e 100644 --- a/opentreemap/treemap/migrations/0022_remove_null_from_eco_rev.py +++ b/opentreemap/treemap/migrations/0022_remove_null_from_eco_rev.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0023_merge.py b/opentreemap/treemap/migrations/0023_merge.py index d547cc4d5..0cc3e1b9b 100644 --- a/opentreemap/treemap/migrations/0023_merge.py +++ b/opentreemap/treemap/migrations/0023_merge.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0024_add_species_verbose_names.py b/opentreemap/treemap/migrations/0024_add_species_verbose_names.py index 4e33c0b34..27d5a830a 100644 --- a/opentreemap/treemap/migrations/0024_add_species_verbose_names.py +++ b/opentreemap/treemap/migrations/0024_add_species_verbose_names.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0025_remove_null_from_boundary_updated_at.py b/opentreemap/treemap/migrations/0025_remove_null_from_boundary_updated_at.py index 534867f68..419fb0732 100644 --- a/opentreemap/treemap/migrations/0025_remove_null_from_boundary_updated_at.py +++ b/opentreemap/treemap/migrations/0025_remove_null_from_boundary_updated_at.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0026_add_canopy_fields.py b/opentreemap/treemap/migrations/0026_add_canopy_fields.py index 215ccedc6..cf7df4a7d 100644 --- a/opentreemap/treemap/migrations/0026_add_canopy_fields.py +++ b/opentreemap/treemap/migrations/0026_add_canopy_fields.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0027_boundary_searchable.py b/opentreemap/treemap/migrations/0027_boundary_searchable.py index bca42170c..d36df2f00 100644 --- a/opentreemap/treemap/migrations/0027_boundary_searchable.py +++ b/opentreemap/treemap/migrations/0027_boundary_searchable.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0027_make_instance_canopy_fields_non_null.py b/opentreemap/treemap/migrations/0027_make_instance_canopy_fields_non_null.py index b038c149c..5fe76e191 100644 --- a/opentreemap/treemap/migrations/0027_make_instance_canopy_fields_non_null.py +++ b/opentreemap/treemap/migrations/0027_make_instance_canopy_fields_non_null.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0028_make_boundary_searchable_non_null.py b/opentreemap/treemap/migrations/0028_make_boundary_searchable_non_null.py index 2763ce397..62364ed67 100644 --- a/opentreemap/treemap/migrations/0028_make_boundary_searchable_non_null.py +++ b/opentreemap/treemap/migrations/0028_make_boundary_searchable_non_null.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0029_merge.py b/opentreemap/treemap/migrations/0029_merge.py index cc3ba1a95..cba58e83c 100644 --- a/opentreemap/treemap/migrations/0029_merge.py +++ b/opentreemap/treemap/migrations/0029_merge.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0030_add_verbose_name_to_owner_orig_id.py b/opentreemap/treemap/migrations/0030_add_verbose_name_to_owner_orig_id.py index f6ca218a5..9f2484d28 100644 --- a/opentreemap/treemap/migrations/0030_add_verbose_name_to_owner_orig_id.py +++ b/opentreemap/treemap/migrations/0030_add_verbose_name_to_owner_orig_id.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0030_backfill_canopy_boundary_category_column.py b/opentreemap/treemap/migrations/0030_backfill_canopy_boundary_category_column.py index 96f248fa4..9eb289cb7 100644 --- a/opentreemap/treemap/migrations/0030_backfill_canopy_boundary_category_column.py +++ b/opentreemap/treemap/migrations/0030_backfill_canopy_boundary_category_column.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0031_add_custom_id_to_default_search_fields.py b/opentreemap/treemap/migrations/0031_add_custom_id_to_default_search_fields.py index f96d8c29e..0e3e04efd 100644 --- a/opentreemap/treemap/migrations/0031_add_custom_id_to_default_search_fields.py +++ b/opentreemap/treemap/migrations/0031_add_custom_id_to_default_search_fields.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from copy import deepcopy @@ -8,7 +8,7 @@ from treemap.DotDict import DotDict -identifier = u'plot.owner_orig_id' +identifier = 'plot.owner_orig_id' def update_config_property(apps, update_fn, *categories): @@ -24,9 +24,9 @@ def add_to_config(config, *categories): for category in categories: lookup = '.'.join(['search_config', category]) specs = config.setdefault(lookup, []) - if True not in [identifier in v for s in specs for v in s.values()]: + if True not in [identifier in v for s in specs for v in list(s.values())]: # mutates config[lookup] - specs.append({u'identifier': identifier}) + specs.append({'identifier': identifier}) return config @@ -42,7 +42,7 @@ def remove_from_config(config, *categories): specs = config.get(lookup) if specs: for index, spec in enumerate(specs): - if identifier in spec.values(): + if identifier in list(spec.values()): break if index < len(specs): specs.pop(index) diff --git a/opentreemap/treemap/migrations/0032_add_udfs_to_web_detail_fields.py b/opentreemap/treemap/migrations/0032_add_udfs_to_web_detail_fields.py index 01fa0cb39..a8ec9df08 100644 --- a/opentreemap/treemap/migrations/0032_add_udfs_to_web_detail_fields.py +++ b/opentreemap/treemap/migrations/0032_add_udfs_to_web_detail_fields.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from copy import deepcopy diff --git a/opentreemap/treemap/migrations/0032_rename_to_role_default_permission_level.py b/opentreemap/treemap/migrations/0032_rename_to_role_default_permission_level.py index 72b0a716f..0b99460e2 100644 --- a/opentreemap/treemap/migrations/0032_rename_to_role_default_permission_level.py +++ b/opentreemap/treemap/migrations/0032_rename_to_role_default_permission_level.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0033_instance_permissions.py b/opentreemap/treemap/migrations/0033_instance_permissions.py index 239c462fe..90eb5ed57 100644 --- a/opentreemap/treemap/migrations/0033_instance_permissions.py +++ b/opentreemap/treemap/migrations/0033_instance_permissions.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0034_add_permission_view_external_link.py b/opentreemap/treemap/migrations/0034_add_permission_view_external_link.py index 14e83c876..bcf3a9dd5 100644 --- a/opentreemap/treemap/migrations/0034_add_permission_view_external_link.py +++ b/opentreemap/treemap/migrations/0034_add_permission_view_external_link.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations diff --git a/opentreemap/treemap/migrations/0035_merge.py b/opentreemap/treemap/migrations/0035_merge.py index f9121c53a..ae468fa47 100644 --- a/opentreemap/treemap/migrations/0035_merge.py +++ b/opentreemap/treemap/migrations/0035_merge.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0036_assign_role_add_delete_permissions.py b/opentreemap/treemap/migrations/0036_assign_role_add_delete_permissions.py index 7145fd85e..c65653fb8 100644 --- a/opentreemap/treemap/migrations/0036_assign_role_add_delete_permissions.py +++ b/opentreemap/treemap/migrations/0036_assign_role_add_delete_permissions.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations from django.db.models import Q +from functools import reduce _WRITE_DIRECTLY = 3 @@ -71,7 +72,7 @@ def remove_permission(apps, schema_editor): ct_query = reduce(lambda q1, q2: q1 | q2, [ Q(app_label=label, model__in=app_models) - for label, app_models in app_labels.iteritems()]) + for label, app_models in app_labels.items()]) photo_query = Q(app_label='treemap', model='mapfeaturephoto') diff --git a/opentreemap/treemap/migrations/0037_fix_plot_add_delete_permission_labels.py b/opentreemap/treemap/migrations/0037_fix_plot_add_delete_permission_labels.py index ba9689c44..98fc55fed 100644 --- a/opentreemap/treemap/migrations/0037_fix_plot_add_delete_permission_labels.py +++ b/opentreemap/treemap/migrations/0037_fix_plot_add_delete_permission_labels.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations diff --git a/opentreemap/treemap/migrations/0038_update_delete_perm_labels.py b/opentreemap/treemap/migrations/0038_update_delete_perm_labels.py index 2dce0a829..e1a0afb72 100644 --- a/opentreemap/treemap/migrations/0038_update_delete_perm_labels.py +++ b/opentreemap/treemap/migrations/0038_update_delete_perm_labels.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations diff --git a/opentreemap/treemap/migrations/0038_updated_by.py b/opentreemap/treemap/migrations/0038_updated_by.py index 1cff830a2..b60aeed16 100644 --- a/opentreemap/treemap/migrations/0038_updated_by.py +++ b/opentreemap/treemap/migrations/0038_updated_by.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models from django.conf import settings @@ -21,7 +21,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='mapfeature', name='updated_by', - field=models.ForeignKey(blank=True, null=True, + field=models.ForeignKey(on_delete=models.CASCADE, blank=True, null=True, to=settings.AUTH_USER_MODEL), ), ] diff --git a/opentreemap/treemap/migrations/0039_merge.py b/opentreemap/treemap/migrations/0039_merge.py index 24a183179..fd7d3e238 100644 --- a/opentreemap/treemap/migrations/0039_merge.py +++ b/opentreemap/treemap/migrations/0039_merge.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0040_expand_itree_regions.py b/opentreemap/treemap/migrations/0040_expand_itree_regions.py index d7dd33a98..49d5f79bb 100644 --- a/opentreemap/treemap/migrations/0040_expand_itree_regions.py +++ b/opentreemap/treemap/migrations/0040_expand_itree_regions.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + import os diff --git a/opentreemap/treemap/migrations/0041_search_by_user.py b/opentreemap/treemap/migrations/0041_search_by_user.py index d6a7ce5cf..cf7a701be 100644 --- a/opentreemap/treemap/migrations/0041_search_by_user.py +++ b/opentreemap/treemap/migrations/0041_search_by_user.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from copy import deepcopy @@ -8,7 +8,7 @@ from treemap.DotDict import DotDict -identifier = u'mapFeature.updated_by' +identifier = 'mapFeature.updated_by' def update_config_property(apps, update_fn, *categories): @@ -24,9 +24,9 @@ def add_to_config(config, *categories): for category in categories: lookup = '.'.join(['search_config', category]) specs = config.setdefault(lookup, []) - if 0 == len([v for s in specs for v in s.values() if v == identifier]): + if 0 == len([v for s in specs for v in list(s.values()) if v == identifier]): # mutates config[lookup] - specs.append({u'identifier': identifier}) + specs.append({'identifier': identifier}) return config @@ -42,7 +42,7 @@ def remove_from_config(config, *categories): specs = config.get(lookup) if specs: find_index = [i for i, s in enumerate(specs) - if identifier in s.values()] + if identifier in list(s.values())] if 0 < len(find_index): specs.pop(find_index[0]) diff --git a/opentreemap/treemap/migrations/0042_auto_20170112_1603.py b/opentreemap/treemap/migrations/0042_auto_20170112_1603.py index c929c7c00..1d489fa77 100644 --- a/opentreemap/treemap/migrations/0042_auto_20170112_1603.py +++ b/opentreemap/treemap/migrations/0042_auto_20170112_1603.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models from django.conf import settings @@ -15,6 +15,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='mapfeature', name='updated_by', - field=models.ForeignKey(verbose_name='Last Updated By', blank=True, to=settings.AUTH_USER_MODEL, null=True), + field=models.ForeignKey(on_delete=models.CASCADE, verbose_name='Last Updated By', blank=True, to=settings.AUTH_USER_MODEL, null=True), ), ] diff --git a/opentreemap/treemap/migrations/0043_species_not_udf_model.py b/opentreemap/treemap/migrations/0043_species_not_udf_model.py index d842876ae..88efcddcd 100644 --- a/opentreemap/treemap/migrations/0043_species_not_udf_model.py +++ b/opentreemap/treemap/migrations/0043_species_not_udf_model.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/opentreemap/treemap/migrations/0043_udfs_default.py b/opentreemap/treemap/migrations/0043_udfs_default.py index 9017cb0e0..450ba7cef 100644 --- a/opentreemap/treemap/migrations/0043_udfs_default.py +++ b/opentreemap/treemap/migrations/0043_udfs_default.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations import treemap.udf diff --git a/opentreemap/treemap/migrations/0044_hstorefield.py b/opentreemap/treemap/migrations/0044_hstorefield.py index 3c1c53da4..d874bcfac 100644 --- a/opentreemap/treemap/migrations/0044_hstorefield.py +++ b/opentreemap/treemap/migrations/0044_hstorefield.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import migrations import treemap.udf diff --git a/opentreemap/treemap/migrations/0045_add_modeling_permission.py b/opentreemap/treemap/migrations/0045_add_modeling_permission.py index 697143c87..0952ee44f 100644 --- a/opentreemap/treemap/migrations/0045_add_modeling_permission.py +++ b/opentreemap/treemap/migrations/0045_add_modeling_permission.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.4 on 2017-08-28 15:47 -from __future__ import unicode_literals + from django.db import migrations from django.db.utils import IntegrityError diff --git a/opentreemap/treemap/migrations/0046_auto_20170907_0937.py b/opentreemap/treemap/migrations/0046_auto_20170907_0937.py index aaaa95e61..602b58140 100644 --- a/opentreemap/treemap/migrations/0046_auto_20170907_0937.py +++ b/opentreemap/treemap/migrations/0046_auto_20170907_0937.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.4 on 2017-09-07 14:37 -from __future__ import unicode_literals + from django.db import migrations import treemap.DotDict diff --git a/opentreemap/treemap/migrations/0047_auto_20200217_1043.py b/opentreemap/treemap/migrations/0047_auto_20200217_1043.py new file mode 100644 index 000000000..e98f5207c --- /dev/null +++ b/opentreemap/treemap/migrations/0047_auto_20200217_1043.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2020-02-17 16:43 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('treemap', '0046_auto_20170907_0937'), + ] + + operations = [ + migrations.CreateModel( + name='MapFeaturePhotoLabel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ('map_feature_photo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='treemap.MapFeaturePhoto')), + ], + ), + migrations.AlterUniqueTogether( + name='mapfeaturephotolabel', + unique_together=set([('map_feature_photo', 'name')]), + ), + ] diff --git a/opentreemap/treemap/migrations/0047_inaturalistobservation_inaturalistphoto.py b/opentreemap/treemap/migrations/0047_inaturalistobservation_inaturalistphoto.py new file mode 100644 index 000000000..bf0db6f5c --- /dev/null +++ b/opentreemap/treemap/migrations/0047_inaturalistobservation_inaturalistphoto.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2020-01-11 17:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('treemap', '0046_auto_20170907_0937'), + ] + + operations = [ + migrations.CreateModel( + name='INaturalistObservation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('observation_id', models.IntegerField()), + ('map_feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='treemap.MapFeature')), + ('tree', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='treemap.Tree')), + ], + ), + migrations.CreateModel( + name='INaturalistPhoto', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('inaturalist_photo_id', models.IntegerField()), + ('observation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='treemap.INaturalistObservation')), + ('tree_photo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='treemap.TreePhoto')), + ], + ), + ] diff --git a/opentreemap/treemap/migrations/0047_instance_itree_region_default_choices_to_list.py b/opentreemap/treemap/migrations/0047_instance_itree_region_default_choices_to_list.py new file mode 100644 index 000000000..4bd438483 --- /dev/null +++ b/opentreemap/treemap/migrations/0047_instance_itree_region_default_choices_to_list.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2020-01-08 19:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('treemap', '0046_auto_20170907_0937'), + ] + + operations = [ + migrations.AlterField( + model_name='instance', + name='itree_region_default', + field=models.CharField(blank=True, choices=[('CaNCCoJBK', 'Northern California Coast'), ('CenFlaXXX', 'Central Florida'), ('GulfCoCHS', 'Coastal Plain'), ('InlEmpCLM', 'Inland Empire'), ('InlValMOD', 'Inland Valleys'), ('InterWABQ', 'Interior West'), ('LoMidWXXX', 'Lower Midwest'), ('MidWstMSP', 'Midwest'), ('NMtnPrFNL', 'North'), ('NoEastXXX', 'Northeast'), ('PacfNWLOG', 'Pacific Northwest'), ('PiedmtCLT', 'South'), ('SoCalCSMA', 'Southern California Coast'), ('SWDsrtGDL', 'Southwest Desert'), ('TpIntWBOI', 'Temperate Interior West'), ('TropicPacXXX', 'Tropical')], max_length=20, null=True), + ), + ] diff --git a/opentreemap/treemap/migrations/0048_merge_20200229_1923.py b/opentreemap/treemap/migrations/0048_merge_20200229_1923.py new file mode 100644 index 000000000..eda7ec88d --- /dev/null +++ b/opentreemap/treemap/migrations/0048_merge_20200229_1923.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2020-03-01 01:23 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('treemap', '0047_auto_20200217_1043'), + ('treemap', '0047_inaturalistobservation_inaturalistphoto'), + ] + + operations = [ + ] diff --git a/opentreemap/treemap/migrations/0049_auto_20200229_2200.py b/opentreemap/treemap/migrations/0049_auto_20200229_2200.py new file mode 100644 index 000000000..337e8516c --- /dev/null +++ b/opentreemap/treemap/migrations/0049_auto_20200229_2200.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2020-03-01 04:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('treemap', '0048_merge_20200229_1923'), + ] + + operations = [ + migrations.AddField( + model_name='inaturalistobservation', + name='identified_at', + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name='inaturalistobservation', + name='is_identified', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='inaturalistobservation', + name='submitted_at', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/opentreemap/treemap/migrations/0050_userdefinedfielddefinition_isrequired.py b/opentreemap/treemap/migrations/0050_userdefinedfielddefinition_isrequired.py new file mode 100644 index 000000000..e15c280ab --- /dev/null +++ b/opentreemap/treemap/migrations/0050_userdefinedfielddefinition_isrequired.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2020-05-08 01:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('treemap', '0049_auto_20200229_2200'), + ] + + operations = [ + migrations.AddField( + model_name='userdefinedfielddefinition', + name='isrequired', + field=models.BooleanField(default=False), + ), + ] diff --git a/opentreemap/treemap/migrations/0051_auto_20200718_1914.py b/opentreemap/treemap/migrations/0051_auto_20200718_1914.py new file mode 100644 index 000000000..ac05a5a8f --- /dev/null +++ b/opentreemap/treemap/migrations/0051_auto_20200718_1914.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2020-07-19 00:14 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ('treemap', '0050_userdefinedfielddefinition_isrequired'), + ] + + operations = [ + migrations.CreateModel( + name='NeighborhoodGroup', + fields=[ + ('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.Group')), + ('ward', models.CharField(max_length=100)), + ('neighborhood', models.CharField(max_length=200)), + ], + options={ + 'verbose_name': 'Neighborhood', + }, + bases=('auth.group',), + ), + migrations.AlterField( + model_name='mapfeaturephoto', + name='thumbnail', + field=models.ImageField(editable=False, upload_to='trees-thumbs/%Y/%m/%d'), + ), + migrations.AlterUniqueTogether( + name='neighborhoodgroup', + unique_together=set([('ward', 'neighborhood')]), + ), + ] diff --git a/opentreemap/treemap/migrations/0052_merge_20210227_1454.py b/opentreemap/treemap/migrations/0052_merge_20210227_1454.py new file mode 100644 index 000000000..b12791886 --- /dev/null +++ b/opentreemap/treemap/migrations/0052_merge_20210227_1454.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.7 on 2021-02-27 20:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('treemap', '0051_auto_20200718_1914'), + ('treemap', '0047_instance_itree_region_default_choices_to_list'), + ] + + operations = [ + ] diff --git a/opentreemap/treemap/migrations/0053_auto_20210227_1456.py b/opentreemap/treemap/migrations/0053_auto_20210227_1456.py new file mode 100644 index 000000000..f5462de8a --- /dev/null +++ b/opentreemap/treemap/migrations/0053_auto_20210227_1456.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.7 on 2021-02-27 20:56 + +import django.contrib.auth.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('treemap', '0052_merge_20210227_1454'), + ] + + operations = [ + migrations.AlterModelManagers( + name='neighborhoodgroup', + managers=[ + ('objects', django.contrib.auth.models.GroupManager()), + ], + ), + migrations.AddField( + model_name='species', + name='is_common', + field=models.NullBooleanField(verbose_name='Is Common Species'), + ), + ] diff --git a/opentreemap/treemap/models.py b/opentreemap/treemap/models.py index afd65ce06..94cfb653f 100644 --- a/opentreemap/treemap/models.py +++ b/opentreemap/treemap/models.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division import hashlib @@ -22,9 +19,12 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import (UserManager, AbstractBaseUser, - PermissionsMixin) + PermissionsMixin, Group) from django.template.loader import get_template +# these are all built-in directly to Django +from django.db.models import Manager as GeoManager + from treemap.species.codes import ITREE_REGIONS, get_itree_code from treemap.audit import Auditable, Role, Dictable, Audit, PendingAuditable # Import this even though it's not referenced, so Django can find it @@ -70,7 +70,7 @@ def _action_format_string_for_readonly(action, readonly): class StaticPage(models.Model): - instance = models.ForeignKey(Instance) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) name = models.CharField(max_length=100) content = models.TextField() @@ -121,7 +121,7 @@ def _get_built_in_name(page_name): return name return None - def __unicode__(self): + def __str__(self): return self.name @@ -235,7 +235,7 @@ def get_default_for_region(cls, region_code): if config: benefits_conversion = cls() benefits_conversion.currency_symbol = '$' - for field, conversion in config.iteritems(): + for field, conversion in config.items(): setattr(benefits_conversion, field, conversion) return benefits_conversion else: @@ -363,7 +363,7 @@ def created(self): @property def email_hash(self): - return hashlib.sha512(self.email).hexdigest() + return hashlib.sha512(self.email.encode()).hexdigest() def dict(self): return {'id': self.pk, @@ -430,6 +430,15 @@ def save(self, *args, **kwargs): self.save_with_user(system_user, *args, **kwargs) +class NeighborhoodGroup(Group): + ward = models.CharField(max_length=100) + neighborhood = models.CharField(max_length=200) + + class Meta: + verbose_name = "Neighborhood" + unique_together = ('ward', 'neighborhood') + + class Species(PendingAuditable, models.Model): """ http://plants.usda.gov/adv_search.html @@ -439,7 +448,7 @@ class Species(PendingAuditable, models.Model): DEFAULT_MAX_HEIGHT = 800 # Base required info - instance = models.ForeignKey(Instance) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) # ``otm_code`` is the key used to link this instance # species row to a cannonical species. An otm_code # is usually the USDA code, but this is not guaranteed. @@ -480,7 +489,10 @@ class Species(PendingAuditable, models.Model): updated_at = models.DateTimeField( # TODO: remove null=True null=True, auto_now=True, editable=False, db_index=True) - objects = models.GeoManager() + # Let's us differentiate between common species and uncommon species in an area. + is_common = models.NullBooleanField(verbose_name='Is Common Species') + + objects = GeoManager() def __init__(self, *args, **kwargs): super(Species, self).__init__(*args, **kwargs) @@ -553,7 +565,7 @@ def get_itree_code(self, region_code=None): else: return get_itree_code(region_code, self.otm_code) - def __unicode__(self): + def __str__(self): return self.display_name class Meta: @@ -564,9 +576,9 @@ class Meta: class InstanceUser(Auditable, models.Model): - instance = models.ForeignKey(Instance) - user = models.ForeignKey(User) - role = models.ForeignKey(Role) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + role = models.ForeignKey(Role, on_delete=models.CASCADE) reputation = models.IntegerField(default=0) admin = models.BooleanField(default=False) last_seen = models.DateField(null=True, blank=True) @@ -592,7 +604,7 @@ def save(self, *args, **kwargs): def do_not_track(cls): return Auditable.do_not_track | {'last_seen'} - def __unicode__(self): + def __str__(self): # protect against not being logged in username = '' if getattr(self, 'user', None) is not None: @@ -610,7 +622,7 @@ def __unicode__(self): # before PendingAuditable. class MapFeature(Convertible, UDFModel, PendingAuditable): "Superclass for map feature subclasses like Plot, RainBarrel, etc." - instance = models.ForeignKey(Instance) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) geom = models.PointField(srid=3857, db_column='the_geom_webmercator') address_street = models.CharField(max_length=255, blank=True, null=True, @@ -627,14 +639,14 @@ class MapFeature(Convertible, UDFModel, PendingAuditable): # efficient. updated_at = models.DateTimeField(default=timezone.now, verbose_name=_("Last Updated")) - updated_by = models.ForeignKey(User, null=True, blank=True, + updated_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Last Updated By")) - objects = models.GeoManager() + objects = GeoManager() # subclass responsibilities area_field_name = None - is_editable = None + is_editable = True # When querying MapFeatures (as opposed to querying a subclass like Plot), # we get a heterogenous collection (some Plots, some RainBarrels, etc.). @@ -666,7 +678,7 @@ def __init__(self, *args, **kwargs): @classproperty def do_not_track(cls): return PendingAuditable.do_not_track | UDFModel.do_not_track | { - 'feature_type', 'mapfeature_ptr', 'hide_at_zoom'} + 'is_editable', 'is_plot', 'title', 'feature_type', 'mapfeature_ptr', 'hide_at_zoom'} @property def _is_generic(self): @@ -680,12 +692,18 @@ def geom_field_name(cls): def latlon(self): latlon = Point(self.geom.x, self.geom.y, srid=3857) latlon.transform(4326) + #latlon = Point(self.geom.x, self.geom.y, srid=4326) return latlon @property def is_plot(self): return getattr(self, 'feature_type', None) == 'Plot' + @property + def inaturalist_observation_url(self): + observation = self.inaturalistobservation_set.first() + return observation.url if observation else None + def update_updated_fields(self, user): """Changing a child object of a map feature (tree, photo, etc.) demands that we update the updated_at field on the @@ -825,7 +843,7 @@ def hash(self): for feature in self.nearby_map_features(): string_to_hash += "," + str(feature.pk) - return hashlib.md5(string_to_hash).hexdigest() + return hashlib.md5(string_to_hash.encode()).hexdigest() def title(self): # Cast allows the map feature subclass to handle generating @@ -898,7 +916,7 @@ def nearby_map_features(self, distance_in_meters=None): .filter(instance=self.instance)\ .exclude(pk=self.pk) - def __unicode__(self): + def __str__(self): geom = getattr(self, 'geom', None) x = geom and geom.x or '?' y = geom and geom.y or '?' @@ -910,6 +928,18 @@ def __unicode__(self): text = "%s (%s, %s) %s" % (feature_type, x, y, address) return text + def as_dict(self): + """ + Add additional fields to the dictionary representation. + This is what gets returned from the API + """ + data = super(MapFeature, self).as_dict() + data['title'] = self.title() + + data['is_editable'] = self.is_editable + data['is_plot'] = self.is_plot + return data + @classproperty def _terminology(cls): return {'singular': cls.__name__} @@ -973,8 +1003,9 @@ class Plot(MapFeature, ValidationMixin): owner_orig_id = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Custom ID")) - objects = models.GeoManager() + objects = GeoManager() is_editable = True + REQUIRED_FIELDS = ['width', 'length'] _terminology = {'singular': _('Planting Site'), 'plural': _('Planting Sites')} @@ -1070,10 +1101,10 @@ class Tree(Convertible, UDFModel, PendingAuditable, ValidationMixin): """ Represents a single tree, belonging to an instance """ - instance = models.ForeignKey(Instance) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) - plot = models.ForeignKey(Plot) - species = models.ForeignKey(Species, null=True, blank=True, + plot = models.ForeignKey(Plot, on_delete=models.CASCADE) + species = models.ForeignKey(Species, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Species")) readonly = models.BooleanField(default=False) @@ -1089,8 +1120,10 @@ class Tree(Convertible, UDFModel, PendingAuditable, ValidationMixin): verbose_name=_("Date Removed")) users_can_delete_own_creations = True + REQUIRED_FIELDS = ['diameter', 'height', 'canopy_height'] + is_editable = True - objects = models.GeoManager() + objects = GeoManager() _stewardship_choices = ['Watered', 'Pruned', @@ -1132,7 +1165,7 @@ def always_writable(cls): _terminology = {'singular': _('Tree'), 'plural': _('Trees')} - def __unicode__(self): + def __str__(self): diameter_str = getattr(self, 'diameter', '') species_str = getattr(self, 'species', '') if not diameter_str and not species_str: @@ -1205,7 +1238,7 @@ def hash(self): photos = [str(photo.pk) for photo in self.treephoto_set.all()] string_to_hash += ":" + ",".join(photos) - return hashlib.md5(string_to_hash).hexdigest() + return hashlib.md5(string_to_hash.encode()).hexdigest() def add_photo(self, image, user): tp = TreePhoto(tree=self, instance=self.instance) @@ -1235,8 +1268,8 @@ def delete_with_user(self, user, *args, **kwargs): class Favorite(models.Model): - user = models.ForeignKey(User) - map_feature = models.ForeignKey(MapFeature) + user = models.ForeignKey(User, on_delete=models.CASCADE) + map_feature = models.ForeignKey(MapFeature, on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) class Meta: @@ -1244,15 +1277,16 @@ class Meta: class MapFeaturePhoto(models.Model, PendingAuditable, Convertible): - map_feature = models.ForeignKey(MapFeature) + map_feature = models.ForeignKey(MapFeature, on_delete=models.CASCADE) image = models.ImageField( upload_to='trees/%Y/%m/%d', editable=False) thumbnail = models.ImageField( - upload_to='trees_thumbs/%Y/%m/%d', editable=False) + upload_to='trees-thumbs/%Y/%m/%d', editable=False) + #upload_to='trees_thumbs/%Y/%m/%d', editable=False) created_at = models.DateTimeField(auto_now_add=True) - instance = models.ForeignKey(Instance) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) users_can_delete_own_creations = True _terminology = {'singular': _('Photo'), 'plural': _('Photos')} @@ -1346,12 +1380,17 @@ def _user_can_do(self, user, action): Clz = self.map_feature.__class__ if callable(getattr(self.map_feature, 'cast_to_subtype', None)): Clz = self.map_feature.cast_to_subtype().__class__ + + # Plot doesn't actually have permissions, but MapFeaturePhoto does + if Clz.__name__.lower() == 'plot': + Clz = MapFeature + codename = Role.permission_codename(Clz, action, photo=True) return role.has_permission(codename, Model=MapFeaturePhoto) class TreePhoto(MapFeaturePhoto): - tree = models.ForeignKey(Tree) + tree = models.ForeignKey(Tree, on_delete=models.CASCADE) @classproperty def always_writable(cls): @@ -1407,8 +1446,11 @@ def as_dict(self): return data + def has_label(self, label_name): + return self.mapfeaturephotolabel_set.filter(name__iexact=label_name).exists() + -class BoundaryManager(models.GeoManager): +class BoundaryManager(GeoManager): """ By default, exclude anonymous boundaries from queries. """ @@ -1446,9 +1488,9 @@ class Boundary(models.Model): objects = BoundaryManager() # Allows access to anonymous boundaries - all_objects = models.GeoManager() + all_objects = GeoManager() - def __unicode__(self): + def __str__(self): return self.name @classmethod @@ -1469,7 +1511,7 @@ def anonymous(cls, polygon=None): class ITreeRegionAbstract(object): - def __unicode__(self): + def __str__(self): "printed representation, used in templates" return "%s (%s)" % (self.code, ITREE_REGIONS.get(self.code, {}).get('name')) @@ -1492,12 +1534,12 @@ class ITreeRegion(ITreeRegionAbstract, models.Model): code = models.CharField(max_length=40, unique=True) geometry = models.MultiPolygonField(srid=3857) - objects = models.GeoManager() + objects = GeoManager() class ITreeCodeOverride(models.Model, Auditable): - instance_species = models.ForeignKey(Species) - region = models.ForeignKey(ITreeRegion) + instance_species = models.ForeignKey(Species, on_delete=models.CASCADE) + region = models.ForeignKey(ITreeRegion, on_delete=models.CASCADE) itree_code = models.CharField(max_length=100) class Meta: @@ -1506,3 +1548,40 @@ class Meta: def __init__(self, *args, **kwargs): super(ITreeCodeOverride, self).__init__(*args, **kwargs) self.populate_previous_state() + + +class MapFeaturePhotoLabel(models.Model): + """ + Provide a tag for a photo + """ + map_feature_photo = models.ForeignKey(MapFeaturePhoto, on_delete=models.CASCADE) + name = models.CharField(max_length=40) + + class Meta: + unique_together = ('map_feature_photo', 'name') + + +class INaturalistObservation(models.Model): + # this is the observation_id from iNaturalist + observation_id = models.IntegerField() + + map_feature = models.ForeignKey(MapFeature, on_delete=models.CASCADE) + tree = models.ForeignKey(Tree, on_delete=models.CASCADE) + + is_identified = models.BooleanField(default=False) + submitted_at = models.DateTimeField(default=timezone.now) + identified_at = models.DateTimeField(null=True) + + @property + def url(self): + return '{base_url}/observations/{observation_id}'.format( + base_url=settings.INATURALIST_URL, + observation_id=self.observation_id + ) + +class INaturalistPhoto(models.Model): + tree_photo = models.ForeignKey(TreePhoto, on_delete=models.CASCADE) + observation = models.ForeignKey(INaturalistObservation, on_delete=models.CASCADE) + inaturalist_photo_id = models.IntegerField() + + diff --git a/opentreemap/treemap/plugin.py b/opentreemap/treemap/plugin.py index 35c2703c5..3b4019e09 100644 --- a/opentreemap/treemap/plugin.py +++ b/opentreemap/treemap/plugin.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf import settings from django.db.models import Q diff --git a/opentreemap/treemap/routes.py b/opentreemap/treemap/routes.py index 99f9e22a8..ad99baba4 100644 --- a/opentreemap/treemap/routes.py +++ b/opentreemap/treemap/routes.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from functools import partial from django.conf import settings from django.contrib.auth.decorators import login_required from django.views.decorators.http import etag -from django.views.decorators.csrf import ensure_csrf_cookie +from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt from django_tinsel.utils import decorate as do from django_tinsel.decorators import (route, json_api_call, render_template, @@ -46,11 +44,12 @@ index_page = instance_request(misc_views.index) -map_page = do( - instance_request, - ensure_csrf_cookie, - render_template('treemap/map.html'), - misc_views.get_map_view_context) +map_page = instance_request(misc_views.index) +#map_page = do( +# instance_request, + #ensure_csrf_cookie, +# render_template('treemap/map.html'), +# misc_views.get_map_view_context) static_page = do( instance_request, @@ -104,11 +103,21 @@ instance_request, misc_views.species_list) +species_list_common = do( + json_api_call, + instance_request, + misc_views.species_list_common) + compile_scss = do( require_http_method("GET"), string_to_response("text/css"), misc_views.compile_scss) +fields = do( + json_api_call, + instance_request, + misc_views.get_plot_field_groups) + ##################################### # mapfeature ##################################### @@ -143,6 +152,11 @@ render_template('treemap/partials/map_feature_accordion.html'), feature_views.context_map_feature_detail) +map_feature_accordion_api = do( + instance_request, + json_api_call, + feature_views.context_map_feature_detail_api) + get_map_feature_sidebar = do( instance_request, etag(feature_views.map_feature_hash), @@ -155,6 +169,11 @@ render_template('treemap/partials/map_feature_popup.html'), feature_views.map_feature_popup) +map_feature_popup_detail = do( + instance_request, + json_api_call, + feature_views.map_feature_popup) + canopy_popup = do( instance_request, feature_views.canopy_popup) @@ -173,6 +192,24 @@ POST=add_map_feature_photo_do(feature_views.rotate_map_feature_photo), DELETE=delete_photo) +add_map_feature_photo_label = add_map_feature_photo_do( + feature_views.add_map_feature_photo_label) + +add_map_feature_photo_do = partial( + do, + require_http_method("POST"), + login_or_401, + instance_request, + creates_instance_user, + render_template('treemap/partials/photo_carousel.html')) + +map_feature_photo_detail = do( + instance_request, + require_http_method('GET'), + render_template('treemap/map_feature_photo_detail.html'), + feature_views.map_feature_photo_detail) + +# tzinckgraf map_feature_photo_detail = do( instance_request, require_http_method('GET'), @@ -220,6 +257,11 @@ tree_detail = instance_request(tree_views.tree_detail) +search_tree_benefits_api = do( + instance_request, + json_api_call, + tree_views.search_tree_benefits) + search_tree_benefits = do( instance_request, etag(tree_views.ecobenefits_hash), @@ -227,6 +269,7 @@ tree_views.search_tree_benefits) add_tree_photo = add_map_feature_photo_do(tree_views.add_tree_photo) +add_tree_photo_with_label = add_map_feature_photo_do(tree_views.add_tree_photo_with_label) ##################################### # user @@ -239,7 +282,10 @@ profile_to_user_page = user_views.profile_to_user user = route( - GET=render_template('treemap/user.html')(user_views.user), + GET=do( + username_matches_request_user, + render_template('treemap/user.html'), + user_views.user), PUT=do( require_http_method("PUT"), username_matches_request_user, @@ -279,3 +325,32 @@ json_api_call, return_400_if_validation_errors, user_views.users) + + +### INATURALIST +inaturalist_create_observations = do( + csrf_exempt, + instance_request, + require_http_method('POST'), + json_api_call, + feature_views.inaturalist_create_observations) + +inaturalist_create_observation_for_tree = do( + csrf_exempt, + instance_request, + require_http_method('POST'), + json_api_call, + feature_views.inaturalist_create_observation_for_tree) + +inaturalist_sync = do( + csrf_exempt, + instance_request, + require_http_method('POST'), + json_api_call, + feature_views.inaturalist_sync) + +inaturalist_add = do( + instance_request, + require_http_method('POST'), + json_api_call, + feature_views.inaturalist_add) diff --git a/opentreemap/treemap/search.py b/opentreemap/treemap/search.py index 54de5d5bc..63dd04631 100644 --- a/opentreemap/treemap/search.py +++ b/opentreemap/treemap/search.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from json import loads from datetime import datetime @@ -118,14 +116,23 @@ def __init__(self, *args, **kwargs): del kwargs['basekey'] else: self.basekeys = set() - super(FilterContext, self).__init__(*args, **kwargs) def add(self, thing, conn): if thing.basekeys: self.basekeys = self.basekeys | thing.basekeys - return super(FilterContext, self).add(thing, conn) + q = super(FilterContext, self).add(thing, conn) + q.basekeys = self.basekeys + return q + + def __and__(self, other): + """ + Under the hood, this does a deepcopy, which does not preserve attributes + """ + q = super(FilterContext, self).__and__(other) + q.basekeys = self.basekeys + return q def convert_filter_units(instance, filter_dict): @@ -133,7 +140,7 @@ def convert_filter_units(instance, filter_dict): Convert the values in a filter dictionary from display units to database units. Mutates the `filter_dict` argument and returns it. """ - for field_name, value in filter_dict.iteritems(): + for field_name, value in filter_dict.items(): if field_name not in ['tree.diameter', 'tree.height', 'tree.canopy_height', 'plot.width', 'plot.length', 'bioswale.drainage_area', @@ -245,7 +252,7 @@ def parse_scalar_predicate_pair(key, value, mapping): query = {} - for pred, props in props_by_pred.iteritems(): + for pred, props in props_by_pred.items(): lookup_tail, rhs = _parse_prop(props, value, pred, value[pred]) @@ -260,7 +267,7 @@ def parse_scalar_predicate_pair(key, value, mapping): return FilterContext(basekey=model, **query) qs = [parse_scalar_predicate_pair(*kv, mapping=mapping) - for kv in query.iteritems()] + for kv in query.items()] return _apply_combinator('AND', qs) @@ -284,7 +291,7 @@ def _parse_by_is_collection_udf(query_dict, mapping): } ''' query_dict_list = [dict(value=v, **_parse_by_key_type(k, mapping=mapping)) - for k, v in query_dict.items()] + for k, v in list(query_dict.items())] grouped = groupby(sorted(query_dict_list, key=lambda qd: qd['type']), lambda qd: qd['type']) return {k: list(v) for k, v in grouped} @@ -306,8 +313,8 @@ def _parse_by_key_type(key, mapping): def _unparse_scalars(scalars): - return dict(zip([qd['key'] for qd in scalars], - [qd['value'] for qd in scalars])) + return dict(list(zip([qd['key'] for qd in scalars], + [qd['value'] for qd in scalars]))) def _parse_collections(by_type, mapping): @@ -334,7 +341,7 @@ def parse_collection_subquery(identifier, field_parts, mapping): return _apply_combinator( 'AND', [parse_collection_subquery(identifier, field_parts, mapping) - for identifier, field_parts in by_type.items()]) + for identifier, field_parts in list(by_type.items())]) def _parse_udf_collection(udfd_id, query_parts): @@ -354,7 +361,7 @@ def parse_collection_udf_dict(key, value): if isinstance(value, dict): preds = parse_udf_dict_value(value) query = {_lookup_key('data__', field, k): - v for (k, v) in preds.iteritems()} + v for (k, v) in preds.items()} else: query = {_lookup_key('data__', field): value} return query @@ -415,7 +422,7 @@ def _parse_predicate_key(key, mapping): if mapping_model not in mapping: raise ModelParseException( 'Valid models are: %s or a collection UDF, not "%s"' % - (mapping.keys(), model)) + (list(mapping.keys()), model)) return model, mapping[mapping_model], field @@ -594,7 +601,7 @@ def _parse_props(props, valuesdict): params = {} - for key, val in valuesdict.items(): + for key, val in list(valuesdict.items()): lookup, rhs = _parse_prop(props[key], valuesdict, key, val) params[lookup] = rhs @@ -606,12 +613,12 @@ def _parse_prop(predicate_props, valuesdict, key, val): if not valid_values.issuperset(set(valuesdict.keys())): raise ParseException( 'Cannot use these keys together: %s vs %s' % - (valuesdict.keys(), valid_values)) + (list(valuesdict.keys()), valid_values)) predicate_builder = predicate_props['predicate_builder'] param_pair = predicate_builder(val) # Return a 2-tuple rather than a single-key dict - return param_pair.items()[0] + return list(param_pair.items())[0] def _parse_dict_props_for_mapping(mapping, valuesdict): diff --git a/opentreemap/treemap/search_fields.py b/opentreemap/treemap/search_fields.py index c0c2f9513..6d7dc672e 100644 --- a/opentreemap/treemap/search_fields.py +++ b/opentreemap/treemap/search_fields.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ @@ -151,7 +149,7 @@ def get_visible_fields(field_infos, user): fields = copy.deepcopy(instance.search_config) fields = {category: get_visible_fields(field_infos, user) - for category, field_infos in fields.iteritems()} + for category, field_infos in fields.items()} for field_info in fields.get('missing', []): _set_missing_search_label(instance, field_info) @@ -165,7 +163,7 @@ def get_visible_fields(field_infos, user): for feature in sorted(instance.map_feature_types) if feature != 'Plot'] num = 0 - for filters in fields.itervalues(): + for filters in fields.values(): for field in filters: # It makes styling easier if every field has an identifier id = "%s_%s" % (field.get('identifier', ''), num) @@ -290,7 +288,7 @@ def get_udfc_search_fields(instance, user): model_name = clz.__name__ if model_name not in ['Tree'] + instance.map_feature_types: continue - for k, v in clz.collection_udf_settings.items(): + for k, v in list(clz.collection_udf_settings.items()): udfds = (u for u in udf_defs(instance, model_name) if u.name == k) for udfd in udfds: if udf_write_level(iu, udfd) in (READ, WRITE): diff --git a/opentreemap/treemap/species/codes.py b/opentreemap/treemap/species/codes.py index f20f5c230..0800f4b6c 100644 --- a/opentreemap/treemap/species/codes.py +++ b/opentreemap/treemap/species/codes.py @@ -2,7 +2,7 @@ def all_itree_region_codes(): - return _CODES.keys() + return list(_CODES.keys()) def all_species_codes(): return species_codes_for_regions(all_itree_region_codes()) @@ -3518,4 +3518,4 @@ def get_itree_code(region_code, otm_code): } -ITREE_REGION_CHOICES = [(code, conf['name']) for code, conf in ITREE_REGIONS.items()] +ITREE_REGION_CHOICES = [(code, conf['name']) for code, conf in list(ITREE_REGIONS.items())] diff --git a/opentreemap/treemap/templates/base.html b/opentreemap/treemap/templates/base.html index ac50b02c3..5d3351ddc 100644 --- a/opentreemap/treemap/templates/base.html +++ b/opentreemap/treemap/templates/base.html @@ -3,6 +3,7 @@ {% load render_bundle from webpack_loader %} {% load instance_config %} {% load js_reverse %} +{% load static %} <!DOCTYPE html> <!-- @@ -12,6 +13,7 @@ <meta charset="utf-8"> <title>{% block title %}OpenTreeMap{% block instance_title %}{% endblock %}{% block page_title %}{% endblock %}{% endblock title %}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <script src="{% static 'js/shim/reverse.js' %}"></script> <link rel="icon" type="image/png" href="/favicon.png" /> {% block application_css %} {% render_bundle 'js/treemap/base' 'css' %} @@ -75,29 +77,33 @@ <div {% block outermost_atts %}{% endblock outermost_atts %} class="wrapper{% if embed %} embed{% endif %}"> {% block topnav %} <!-- Top Nav --> - <div class="navbar navbar-inverse navbar-fixed-top"> + <nav class="navbar navbar-expand fixed-top bg-dark navbar-dark"> <div class="navbar-collapse" id="otm-navbar-collapse"> - <ul class="pull-left nav navbar-nav navbar-left"> + <ul class="navbar-nav nav mr-auto"> {# Since the active style changes on a page-by-page basis we use nested blocks below. These nested blocks can then be overriden to change which tab is active. #} {% block instancetopnav %} {% if last_instance %} - <li class="explore-trees {% block activeexplore %}active{% endblock %}"><a href="{% url 'map' instance_url_name=last_instance.url_name %}">{% trans "Explore Trees" %}</a></li> - {% if last_instance|feature_enabled:'add_plot' and last_effective_instance_user %} - <li data-feature="add_plot"> - <a data-class='add-tree' + <li class="nav-item explore-trees {% block activeexplore %}active{% endblock %}"> + <a class="nav-link" href="{% url 'map' instance_url_name=last_instance.url_name %}">{% trans "Explore Trees" %}</a></li> + {% if last_instance|feature_enabled:'add_plot' and last_effective_instance_user and not embed %} + <li data-feature="add_plot" class="nav-item"> + <a class="nav-link" data-class='add-tree' data-always-enable='{{ last_effective_instance_user|plot_is_creatable }}' data-disabled-title='{% trans "Adding trees is not available to all users" %}' - data-href="{% url 'map' instance_url_name=last_instance.url_name %}?m=addTree" + data-href="{% url 'react_map' instance_url_name=last_instance.url_name %}?m=addTree" + href="{% url 'react_map' instance_url_name=last_instance.url_name %}?m=addTree" disabled='disabled'>{% trans "Add a Tree" %}</a> </li> {% endif %} {% endif %} {% endblock instancetopnav %} </ul> - <ul class="pull-right nav navbar-nav navbar-right"> + </div> + <div class="navbar-collapse collapse" id="otm-navbar-collapse"> + <ul class="navbar-nav nav ml-auto"> {% if request.user.is_authenticated %} - <li class="hidden-xs {% block activeuser %}{% endblock %}"> - <a href="{% url 'profile' %}">{% trans "My Account" %} + <li class="nav-item hidden-xs d-none d-sm-block {% block activeuser %}{% endblock %}"> + <a class="nav-link" href="{% url 'profile' %}">{% trans "My Account" %} {% if last_instance %} {% if reputation %} <span class="reputation">({{ last_effective_instance_user.reputation }} rep)</span> @@ -105,9 +111,12 @@ {% endif %} </a> </li> - <li class="hidden-xs"><a href="{% url 'auth_logout' %}">{% trans "Logout" %}</a></li> - <li class="user-img hidden-xs"> - <a href="{% url 'profile' %}"> + <li class="nav-item hidden-xs d-none d-sm-block"> + <a class="nav-link" href="{% url 'user_dashboard' instance_url_name=request.instance.url_name %}">{% trans "My Dashboard" %}</a> + </li> + <li class="nav-item hidden-xs d-none d-sm-block"><a class="nav-link" href="{% url 'auth_logout' %}">{% trans "Logout" %}</a></li> + <li class="nav-item user-img hidden-xs d-none d-sm-block"> + <a class="nav-link" href="{% url 'profile' %}"> {% if request.user.thumbnail %} <img src="{{ request.user.thumbnail.url }}"> {% else %} @@ -115,13 +124,13 @@ {% endif %} </a> </li> - <li class="add-menu dropdown visible-xs-inline-block"> - <a class="dropdown-toggle" data-toggle="dropdown"> + <li class="nav-item add-menu dropdown visible-xs-inline-block d-block d-sm-none"> + <a class="nav-link dropdown-toggle" data-toggle="dropdown"> <i class="icon-cog"></i> </a> <ul class="dropdown-menu dropdown-pull-left"> <li> - <a href="{% url 'profile' %}">{% trans "My Account" %} + <a class="nav-link" href="{% url 'profile' %}">{% trans "My Account" %} {% if last_instance %} {% if reputation %} <span class="reputation">({{ last_effective_instance_user.reputation }} rep)</span> @@ -129,29 +138,18 @@ {% endif %} </a> </li> - <li><a href="{% url 'auth_logout' %}">{% trans "Logout" %}</a></li> + <li> + <a class="nav-link" href="{% url 'user_dashboard' instance_url_name=request.instance.url_name %}">{% trans "My Dashboard" %}</a> + </li> + <li class="nav-item"><a class="nav-link" href="{% url 'auth_logout' %}">{% trans "Logout" %}</a></li> </ul> </li> {% else %} - <li class="hidden-xs"><a id="login" href="{% url 'auth_login' %}{% login_forward %}">{% trans "Login" %}</a></li> - {% block signup %} - <li class="hidden-xs"><a href="{% url 'registration_register' %}">{% trans "Sign Up" %}</a></li> - {% endblock signup %} - <li class="add-menu dropdown visible-xs-inline-block"> - <a class="dropdown-toggle" data-toggle="dropdown"> - <i class="icon-cog"></i> - </a> - <ul class="dropdown-menu dropdown-pull-left"> - <li><a id="login" href="{% url 'auth_login' %}{% login_forward %}">{% trans "Login" %}</a></li> - {% block signup_small %} - <li><a href="{% url 'registration_register' %}">{% trans "Sign Up" %}</a></li> - {% endblock signup_small %} - </ul> - </li> + <li><a class="nav-link" id="login" href="{% url 'auth_login' %}{% login_forward %}">{% trans "Login" %}</a></li> {% endif %} </ul> </div> - </div> + </nav> {% endblock topnav %} {% block header %} @@ -196,7 +194,7 @@ </div> {% if not embed %} - <footer class="hidden-xs">{% block footer %}{% endblock footer %}</footer> + <footer class="hidden-xs d-none d-sm-block">{% block footer %}{% endblock footer %}</footer> {% endif %} {% block config_scripts %} @@ -217,7 +215,7 @@ {% js_reverse_inline %} </script> {% endif %} - {% render_bundle 'js/treemap/base' 'js' %} + {% render_bundle 'js/treemap/base-chunk' 'js' %} {% endblock global_scripts %} {% block templates %} diff --git a/opentreemap/treemap/templates/instance_base.html b/opentreemap/treemap/templates/instance_base.html index fe4735963..09df1edee 100644 --- a/opentreemap/treemap/templates/instance_base.html +++ b/opentreemap/treemap/templates/instance_base.html @@ -37,17 +37,18 @@ {% block instancetopnav %} {% if request.instance|feature_enabled:'add_plot' and last_effective_instance_user %} {% if request.instance.has_resources %} - <li class="add-menu dropdown" data-feature="add_plot"> - <a class="dropdown-toggle" data-toggle="dropdown"> + <li class="nav-item add-menu dropdown" data-feature="add_plot"> + <a class="nav-link dropdown-toggle" data-toggle="dropdown"> <i class="icon-plus-circled"></i> </a> {% include "treemap/partials/add_feature_menu.html" with dropdown_class="dropdown-menu-left" include_tree=True %} </li> {% else %} - <li data-feature="add_plot"> - <a data-always-enable='{{ last_effective_instance_user|plot_is_creatable }}' + <li class="nav-item" data-feature="add_plot"> + <a class="nav-link" data-always-enable='{{ last_effective_instance_user|plot_is_creatable }}' data-disabled-title='{% trans "Adding trees is not available to all users" %}' - data-href="{% url 'map' instance_url_name=request.instance.url_name %}?m=addTree" + data-href="{% url 'react_map' instance_url_name=request.instance.url_name %}?m=addTree" + href="{% url 'react_map' instance_url_name=request.instance.url_name %}?m=addTree" {% if embed %} target="_blank" {% else %} @@ -59,17 +60,18 @@ {% endif %} {% endif %} -<li class="explore-map {% block activeexplore %}active{% endblock %}"> - <a href="{% url 'map' instance_url_name=request.instance.url_name %}{% block map_query %}{% endblock %}" +<li class="nav-item explore-map {% block activeexplore %}active{% endblock %}"> + <a class="nav-link" href="{% url 'react_map' instance_url_name=request.instance.url_name %}" {% if embed %}target="_blank"{% endif %}> - <span class="hidden-xs">{% trans "Explore Map" %}</span> + <span class="hidden-xs d-none d-sm-block">{% trans "Explore Map" %}</span> <!-- We hide the logo on mobile, so we show the instance name instead of "Explore Map" to give context about which map you are on --> - <span class="visible-xs-inline">{{ request.instance.name }}</span> + <span class="visible-xs-inline d-block d-sm-none">{{ request.instance.name }}</span> + </a> </li> {% if last_effective_instance_user|has_permission:'modeling' %} - <li class="{% block activemodeling %}{% endblock %} hidden-xs"> + <li class="{% block activemodeling %}{% endblock %} hidden-xs d-none d-sm-block"> <a href="{% url 'model_trees' instance_url_name=request.instance.url_name %}"> <span>{% trans "Plan" %}</span> </a> @@ -77,20 +79,25 @@ {% endif %} {% if request.instance|feature_enabled:'recent_edits_report' %} -<li class="hidden-xs {% block activerecentedits %}{% endblock %}"> - <a href="{% url 'edits' instance_url_name=request.instance.url_name %}">{% trans "View Edits" %}</a> +<li class="nav-item hidden-xs d-none d-sm-block {% block activerecentedits %}{% endblock %}"> + <a class="nav-link" href="{% url 'edits' instance_url_name=request.instance.url_name %}">{% trans "View Edits" %}</a> </li> {% endif %} {% if last_effective_instance_user.admin %} -<li class="hidden-xs {% block activemanagement %}{% endblock %}"> - <a href="{% url "management" instance_url_name=request.instance.url_name %}">{% trans "Manage" %}</a> +<li class="nav-item hidden-xs d-none d-sm-block {% block activemanagement %}{% endblock %}"> + <a class="nav-link" href="{% url "management" instance_url_name=request.instance.url_name %}">{% trans "Manage" %}</a> </li> {% endif %} + +<li class="nav-item hidden-xs d-none d-sm-block {% block activedashboard %}{% endblock %}"> + <a class="nav-link" href="{% url "reports_endpoint" instance_url_name=request.instance.url_name %}">{% trans "Dashboard" %}</a> +</li> {% endblock instancetopnav %} +<!-- {% block signup %} -<li class="hidden-xs"> +<li class="hidden-xs d-none d-sm-block"> <a href="{% url 'instance_registration_register' instance_url_name=request.instance.url_name %}">{% trans "Sign Up" %}</a> </li> {% endblock signup %} @@ -99,6 +106,7 @@ <a href="{% url 'instance_registration_register' instance_url_name=request.instance.url_name %}">{% trans "Sign Up" %}</a> </li> {% endblock signup_small %} +--!> {% block subhead %} <div class="subhead"> @@ -134,7 +142,7 @@ {% if not embed %} {% block subhead_exports %} {% if request.instance|export_enabled_for:request.user %} - <a href="javascript:;" class="btn btn-primary btn-xs exportBtn hidden-xs" + <a href="javascript:;" class="btn btn-primary btn-xs exportBtn hidden-xs d-none d-sm-block" data-export-start-url="{% url 'begin_export' instance_url_name=request.instance.url_name model='tree' %}"> <i class="icon-export"></i> {% trans "Export Search Results" %} </a> @@ -142,8 +150,8 @@ {% endblock subhead_exports %} {% endif %} </div> - <div class="addBtn hidden-xs"> - {% if request.instance|feature_enabled:'add_plot' and last_effective_instance_user %} + <div class="addBtn hidden-xs d-none d-sm-block"> + {% if request.instance|feature_enabled:'add_plot' and last_effective_instance_user and not embed %} {% if request.instance.has_resources %} <div class="btn-group"> {% include "treemap/partials/add_plot_btn.html" %} @@ -180,8 +188,8 @@ <div class="btn-group"> <button id="search-advanced" class="btn btn-default btn-sm" data-event-category="search" data-event-action="toggle-advanced"> - <span class="hidden-xs">{% trans "Advanced" %}</span> - <span class="visible-xs-inline"> + <span class="hidden-xs d-none d-sm-block">{% trans "Advanced" %}</span> + <span class="visible-xs-inline d-block d-sm-none"> <span class="text">{% trans "Advanced" %}</span> <i class="icon-cancel"></i> </span> @@ -202,14 +210,14 @@ <div class="footer-inner"> {% with linkData=request.instance|instance_config:"linkData" %} <ul class="list-inline pull-left"> - <li><a target="_blank" + <li class="list-inline-item"><a target="_blank" href="{% include "treemap/partials/treekey_url.html" %}" >{% trans "Tree ID" %}</a></li> {% for name in request.instance.static_page_names %} - <li><a href="{% url 'static_page' instance_url_name=request.instance.url_name page=name %}">{{ name }}</a></li> + <li class="list-inline-item"><a href="{% url 'static_page' instance_url_name=request.instance.url_name page=name %}">{{ name }}</a></li> {% endfor %} {% if 'contact' in linkData.keys and linkData.contact %} - <li><a href="mailto:{{ linkData.contact }}">{% trans "Contact" %}</a></li> + <li class="list-inline-item"><a href="mailto:{{ linkData.contact }}">{% trans "Contact" %}</a></li> {% endif %} </ul> {% endwith %} diff --git a/opentreemap/treemap/templates/registration/login.html b/opentreemap/treemap/templates/registration/login.html index 5d87b5357..99bf4a0e3 100644 --- a/opentreemap/treemap/templates/registration/login.html +++ b/opentreemap/treemap/templates/registration/login.html @@ -54,10 +54,12 @@ </fieldset> </form> </div> + <!-- <div class="well login-signup-panel signup-option"> <h3>{% trans "Don't have an account?" %}</h3> <a class="btn btn-lg btn-primary" href="{% url 'registration_register' %}">{% trans "Register" %}</a> </div> + --!> </div> {% endblock content %} diff --git a/opentreemap/treemap/templates/treemap/edits.html b/opentreemap/treemap/templates/treemap/edits.html index f0ed064d3..9233e5108 100644 --- a/opentreemap/treemap/templates/treemap/edits.html +++ b/opentreemap/treemap/templates/treemap/edits.html @@ -149,22 +149,22 @@ <h1> {% endfor %} </table> - <ul class="pager"> + <ul class="pager pagination"> {% if prev_page %} - <li class="previous"> + <li class="previous page-item"> <a href="{{ prev_page }}"> {% else %} - <li class="previous disabled"> + <li class="previous disabled page-item"> <a> {% endif %} ← {% trans "Previous" %}</a> </li> {% if next_page %} - <li class="next"> + <li class="next page-item"> <a href="{{ next_page }}"> {% else %} - <li class="next disabled"> + <li class="next disabled page-item"> <a> {% endif %} {% trans "Next" %} →</a> diff --git a/opentreemap/treemap/templates/treemap/field/inputs.html b/opentreemap/treemap/templates/treemap/field/inputs.html index 8c3c3f740..b6e41fce7 100644 --- a/opentreemap/treemap/templates/treemap/field/inputs.html +++ b/opentreemap/treemap/templates/treemap/field/inputs.html @@ -49,7 +49,9 @@ value="{{ field.value|default_if_none:""|unlocalize }}" {% endif %} {{ extra|default:"" }} /> - <span class="input-group-addon" {{ unit_extra|default:"" }}>{{ field.units }}</span> + <div class="input-group-append"> + <span class="input-group-text" {{ unit_extra|default:"" }}>{{ field.units }}</span> + </div> </div> {% else %} <input name="{{ field.identifier }}" diff --git a/opentreemap/treemap/templates/treemap/field/search.html b/opentreemap/treemap/templates/treemap/field/search.html index dcdec9653..e1e146d15 100644 --- a/opentreemap/treemap/templates/treemap/field/search.html +++ b/opentreemap/treemap/templates/treemap/field/search.html @@ -8,11 +8,11 @@ {% with 'data-class=search data-search-type=MIN id=min'|add:field.id as extra %} {% include "treemap/field/inputs.html" %} {% endwith %} - <span class="hidden-xs"> + <span class="hidden-xs d-none d-sm-block"> {# Translators: This is in-between two date boxes for a range search #} {% trans "through" %} </span> - <span class="visible-xs-inline">-</span> + <span class="visible-xs-inline d-block d-sm-none">-</span> {% with 'data-class=search data-search-type=MAX id=max'|add:field.id as extra %} {% include "treemap/field/inputs.html" %} {% endwith %} diff --git a/opentreemap/treemap/templates/treemap/field/species_div.html b/opentreemap/treemap/templates/treemap/field/species_div.html index c9e7a898a..0184bd9f1 100644 --- a/opentreemap/treemap/templates/treemap/field/species_div.html +++ b/opentreemap/treemap/templates/treemap/field/species_div.html @@ -10,9 +10,13 @@ <h5> </div> {% if field.is_editable %} <div style="display: none;" data-class="edit" data-field="{{ field.identifier }}"> - <label>{% trans "Species" %}</label> + <label>* {% trans "Species" %}</label> <div> {% include "treemap/field/species_typeahead.html" %} + + <input name="is_empty_site" + id="is-empty-site" + type="checkbox" /> <label for="is-empty-site">Is Empty Site</label> </div> <div class="alert alert-danger text-danger" style="display: none;" diff --git a/opentreemap/treemap/templates/treemap/field/tr.html b/opentreemap/treemap/templates/treemap/field/tr.html index c05aa6761..c3e4e5d28 100644 --- a/opentreemap/treemap/templates/treemap/field/tr.html +++ b/opentreemap/treemap/templates/treemap/field/tr.html @@ -1,7 +1,7 @@ {# vim: set filetype=htmldjango : #} {% if field.is_visible %} <tr> - <td>{{ field.label }}</td> + <td>{% if field.is_editable and field.is_required %}<span style="display: inline;" class="required-indicator"></span>{% endif %}{{ field.label }}</td> <td {% include "treemap/field/attrs.html" with class="display" %}> {{ field.display_value|default_if_none:"" }} </td> diff --git a/opentreemap/treemap/templates/treemap/map-add-tree.html b/opentreemap/treemap/templates/treemap/map-add-tree.html index 5bf1726ed..b1dddb88e 100644 --- a/opentreemap/treemap/templates/treemap/map-add-tree.html +++ b/opentreemap/treemap/templates/treemap/map-add-tree.html @@ -1,27 +1,128 @@ {% load form_extras %} {% load i18n %} +{% trans "Add a Photo" as upload_title %} +{% include "treemap/partials/upload_file.html" with title=upload_title panel_id="shape-photo-upload" label="shape" row_id="add-shape" checkbox_id="has-shape-photo" %} +{% include "treemap/partials/upload_file.html" with title=upload_title panel_id="bark-photo-upload" label="bark" row_id="add-bark" checkbox_id="has-bark-photo" %} +{% include "treemap/partials/upload_file.html" with title=upload_title panel_id="leaf-photo-upload" label="leaf" row_id="add-leaf" checkbox_id="has-leaf-photo" %} +{% include "treemap/partials/upload_file.html" with title=upload_title panel_id="empty-site-photo-upload" label="empty site" row_id="add-empty-site" checkbox_id="has-empty-site-photo"%} + {% with nsteps=3 %} <div class="sidebar-inner"> - <a href="javascript:;" class="close cancelBtn small hidden-xs">×</a> + <a href="javascript:;" class="close cancelBtn small hidden-xs d-none d-sm-block">×</a> <h3>{% trans "Add a Tree" %}</h3> <div class="add-step-container" id="add-tree-container"> {% include "treemap/partials/step_set_location.html" with first=True feature_name="tree" %} + <div class="add-step"> <div class="add-step-header"> {% trans "Add species and additional info" %} - <a href="javascript:;" class="close cancelBtn small visible-xs-block">×</a> + <a href="javascript:;" class="close cancelBtn small visible-xs-block d-block d-sm-none">×</a> </div> <div class="add-step-content"> + <div id="tree-photos"> + <label>* {% trans "Tree Photos" %}</label> + <table class="table table-hover"> + <tbody> + <tr id="add-shape"> + <td>Add photo of tree shape</td> + <td> + <button data-toggle="modal" + data-target="#shape-photo-upload" + class="btn add-photos">{% trans "Add Shape" %}</button> + </td> + </tr> + <tr id="add-leaf"> + <td>Add photo of tree leaf</td> + <td><button data-toggle="modal" + data-target="#leaf-photo-upload" + class="btn add-photos">{% trans "Add Leaf" %}</button> + </td> + </tr> + <tr id="add-bark"> + <td>Add photo of tree bark</td> + <td><button data-toggle="modal" + data-target="#bark-photo-upload" + class="btn add-photos">{% trans "Add Bark" %}</button> + </td> + </tr> + </tbody> + </table> + </div> + <div id="empty-site-photos"> + <label>{% trans "Tree Photos" %}</label> + <table class="table table-hover"> + <tbody> + <tr id="add-empty-site"> + <td>Add photo of tree site</td> + <td> + <button data-toggle="modal" + data-target="#empty-site-photo-upload" + class="btn add-photos">{% trans "Add Empty Site" %}</button> + </td> + </tr> + </tbody> + </table> + </div> <form id="add-tree-form" onsubmit="return false;"> + <div style="display: none;"> + <input name="has_shape_photo" + id="has-shape-photo" + type="checkbox" /> + <input name="has_bark_photo" + id="has-bark-photo" + type="checkbox" /> + <input name="has_leaf_photo" + id="has-leaf-photo" + type="checkbox" /> + </div> + {# The "add-tree-species" label is used as an id prefix in "species_ul.html" #} {% create "add-tree-species" from "Tree.species" in request.instance withtemplate "treemap/field/species_div.html" %} - {% trans "Trunk Diameter" as diameter %} + + {% trans "* Trunk Diameter" as diameter %} {% create diameter from "Tree.diameter" in request.instance withtemplate "treemap/field/diameter_div.html" %} - {% for label, identifier in fields_for_add_tree %} - {% create label from identifier in request.instance withtemplate "treemap/field/div.html" %} - {% endfor %} + {% for group in field_groups %} + {% if group.model == "plot" %} + <h3>{% trans "Planting Site Information" %}</h3> + <table class="table table-hover"> + <tbody> + {% for tuple in group.fields %} + {% with field=tuple.0 label=tuple.1 template=tuple.2 %} + {% create label from field in request.instance withtemplate template %} + {% endwith %} + {% endfor %} + </tbody> + </table> + + {% for udf in group.collection_udfs %} + {% with title_prefix="Planting Site" %} + {% include "treemap/partials/collectionudf.html" with udf=udf title_prefix=title_prefix model=tree values=values %} + {% endwith %} + {% endfor %} + + {% elif group.model == "tree" %} + <h3>{% trans "Tree Information" %}</h3> + <table class="table table-hover"> + <tbody> + {% for tuple in group.fields %} + {% with field=tuple.0 label=tuple.1 template=tuple.2 %} + {% create label from field in request.instance withtemplate template %} + {% endwith %} + {% endfor %} + </tbody> + </table> + + {% for udf in group.collection_udfs %} + {% with title_prefix=udf.model_type %} + {% include "treemap/partials/collectionudf.html" with udf=udf title_prefix=title_prefix model=tree values=values %} + {% endwith %} + {% endfor %} + + {% endif %} + + {% endfor %} {% include 'treemap/partials/hidden_address.html' with object_name='plot' %} </form> @@ -31,7 +132,7 @@ <h3>{% trans "Add a Tree" %}</h3> <div class="add-step"> <div class="add-step-header"> {% trans "Finalize this tree" %} - <a href="javascript:;" class="close cancelBtn small visible-xs-block">×</a> + <a href="javascript:;" class="close cancelBtn small visible-xs-block d-block d-sm-none">×</a> </div> <div class="add-step-content"> <h3 class="summaryHead"></h3> diff --git a/opentreemap/treemap/templates/treemap/map-browse-trees.html b/opentreemap/treemap/templates/treemap/map-browse-trees.html index 17286bf53..4bb5743f5 100644 --- a/opentreemap/treemap/templates/treemap/map-browse-trees.html +++ b/opentreemap/treemap/templates/treemap/map-browse-trees.html @@ -4,7 +4,7 @@ <div class="panel" id="map-info"> <div class="panel-group" id="feature-panel"> <div class="panel-heading"> - <a class="panel-toggle collapsed" data-toggle="collapse" data-parent="#map-info" href="#tree-detail">{% trans "Details" %} <span class="arrow pull-right"><i class="icon-right-open"></i></span></a> + <a class="panel-toggle collapsed" data-toggle="collapse" data-parent="#map-info" data-target="#tree-detail">{% trans "Details" %} <span class="arrow pull-right"><i class="icon-right-open"></i></span></a> </div> <div id="tree-detail" class="panel-body collapse"> <div class="panel-body-buttons-wrapper"> @@ -50,9 +50,9 @@ {% if request.instance_supports_ecobenefits %} <div class="panel-group" id="eco-panel"> <div class="panel-heading"> - <a class="panel-toggle" data-toggle="collapse" data-parent="#map-info" href="#yearlyEco">{% trans "Eco Benefits" %} <span class="arrow pull-right"><i class="icon-right-open"></i></span></a> + <a class="panel-toggle" data-toggle="collapse" role="button" data-parent="#map-info" data-target="#yearlyEco">{% trans "Eco Benefits" %} <span class="arrow pull-right"><i class="icon-right-open"></i></span></a> </div> - <div id="yearlyEco" class="panel-body collapse in"> + <div id="yearlyEco" class="panel-body collapse in show"> <div class="panel-inner benefit-values" id="benefit-values"> <div class="benefit-value-row benefit-loading"> <h3 class="benefit-label">{% trans "Loading Ecobenefits" %}</h3> diff --git a/opentreemap/treemap/templates/treemap/map_feature_detail.html b/opentreemap/treemap/templates/treemap/map_feature_detail.html index 808f5c99b..e43b2ac8a 100644 --- a/opentreemap/treemap/templates/treemap/map_feature_detail.html +++ b/opentreemap/treemap/templates/treemap/map_feature_detail.html @@ -29,8 +29,8 @@ {% trans "Add a Photo" as upload_title %} {% include "treemap/partials/upload_file.html" with title=upload_title upload_url=upload_photo_endpoint %} + {% include "treemap/partials/photo_social_media_sharing.html" %} {% if request.instance.is_public %} - {% include "treemap/partials/photo_social_media_sharing.html" %} {% endif %} {# Modal for viewing and rotating photos #} @@ -53,9 +53,26 @@ <div class="pull-left" data-class="edit"> <a class="btn btn-small pull-left btn-rotate" data-class="edit" data-photo-rotate="-90"><i class="icon-ccw"></i></a> <a class="btn btn-small pull-left btn-rotate" data-class="edit" data-photo-rotate="90"><i class="icon-cw"></i></a> + + {# Button and dropdown for label, depending on edit or view #} + <div class="pull-left"> + <label for="photo-label">Choose a photo label</label> + <select id="photo-label"> + <option value=""></value> + <option value="leaf">Leaf</value> + <option value="bark">Bark</value> + <option value="shape">Shape</value> + </select> + </div> + </div> + + <button class="btn btn-xs photo-label-view pull-left" data-class="view" disabled="true"></button> + <div class="pull-right" data-class="edit"> + <button disabled="disabled" class="btn btn-small pull-right" + data-photo-save="">{% trans "Save" %}</button> + <button class="btn btn-small pull-right" + data-photo-keep="">{% trans "Cancel" %}</button> </div> - <button data-class="edit" disabled="disabled" class="btn btn-small pull-right" - data-photo-save="">{% trans "Save" %}</button> <div class="pull-right" data-class="delete"> <button disabled="disabled" class="btn btn-small btn-danger" data-photo-confirm="">{% trans "Yes" %}</button> <button class="btn btn-small" data-photo-keep="">{% trans "No" %}</button> @@ -70,14 +87,13 @@ <div class="row"> <div class="col-md-3"> <div class="photo-container"> - <div id="photo-carousel" class="carousel slide" data-interval=""> + <div id="photo-carousel" class="carousel slide" data-interval="0"> {% include "treemap/partials/photo_carousel.html" %} </div> <button data-toggle="modal" data-target="#upload-panel" data-href="{{ request.get_full_path }}" data-always-enable="{{ last_effective_instance_user|photo_is_addable:feature }}" - disabled="disabled" {% if feature.is_plot %} data-disabled-title="{% trans "Adding tree photos is not available to all users" %}" {% else %} @@ -91,26 +107,26 @@ </div> <div class="col-md-9"> - <div class="row"> - <div id="mapFeaturePartial"> + <div> + <div id="mapFeaturePartial" class="row"> {% include map_feature_partial %} - </div> - <!-- Maps --> - <div class="col-md-4"> - <div id="map" class="map-small" - data-has-boundaries="False" - data-has-polygons="{{ has_polygons }}"> + <!-- Maps --> + <div class="col-md-4"> + <div id="map" class="map-small" + data-has-boundaries="False" + data-has-polygons="{{ has_polygons }}"> + </div> + {% if feature.is_editable %} + <button + data-href="{{ request.get_full_path }}" + style="display:none" + id="edit-location" + class="btn btn-block btn-sm btn-otmsecondary">{% trans "Move Location" %}</button> + <button class="btn btn-block btn-sm btn-alert" style="display:none" id="cancel-edit-location">{% trans "Cancel Move Location" %}</button> + {% endif %} + <br> + <div id="street-view" class="street-view-small" style="display: none;"></div> </div> - {% if feature.is_editable %} - <button - data-href="{{ request.get_full_path }}" - style="display:none" - id="edit-location" - class="btn btn-block btn-sm btn-otmsecondary">{% trans "Move Location" %}</button> - <button class="btn btn-block btn-sm btn-alert" style="display:none" id="cancel-edit-location">{% trans "Cancel Move Location" %}</button> - {% endif %} - <br> - <div id="street-view" class="street-view-small" style="display: none;"></div> </div> </div> </div> diff --git a/opentreemap/treemap/templates/treemap/partials/Resources.html b/opentreemap/treemap/templates/treemap/partials/Resources.html index c0322569c..0520feca2 100644 --- a/opentreemap/treemap/templates/treemap/partials/Resources.html +++ b/opentreemap/treemap/templates/treemap/partials/Resources.html @@ -1,11 +1,22 @@ <h2>Resources</h2> <h3>Want to find out more about trees and urban forestry?</h3> <p>Visit the sites below for more information.</p> + <h4>SJC Links</h4> + <ul> + <li><a href="https://drive.google.com/file/d/1xpvAuJm7iikqzM_DH6mqd7bhHhNrcOej/view">Jersey City 2015 - 2020 Tree Canopy Report - Another 6+% Deterioration!</a></li> + <li><a href="https://static1.squarespace.com/static/59eb5534b7411cf368c81ad3/t/5ebb1a7a8cf11f6e3c2fd36b/1589320315209/new+OTM+Tip+Sheet+2020+final+05122020.pdf">SJC OTM Tip Sheet</a></li> + <li>Self-directed <a href="https://ellalhv.org/video-archive/">ELLA Webinar - Tree Species ID by Their Leaves</a> with Companion Pocket Guide <a href="https://www.amazon.com/Tree-Finder-Manual-Identification-Eastern/dp/0912550015">TREE FINDER available on Amazon for only $5.95</a> (recommended by our Neighborhood Captains!) </li> + <li>Neighborhood Captain Chris Lamm <a href="https://drive.google.com/file/d/1CKKSs8wtajvWU2EKU5rK9_ZKCwtOyzgy/view">JC Common Trees List!</a></li> + <li><a href="https://docs.google.com/spreadsheets/d/1RLWjvq3-dbZqfFbRlX0GsV0CPBVwN5Fcipq-SfrS1a4/edit">Resource Log for NCs</a></li> + </ul> + <h4>Species Identification</h4> <ul> <li><a href="https://www.arborday.org/trees/whatTree/">Arbor Day Foundation's What Tree is That?</a></li> <li><a href="http://www.mobot.org/gardeninghelp/plantfinder/Alpha.asp">Missouri Botanical Garden PlantFinder</a></li> <li><a href="http://plants.usda.gov/java/">USDA PLANTS Database</a></li> + <li><a href="https://rucore.libraries.rutgers.edu/rutgers-lib/54233/">Trees of NJ and the Mid-Atlantic States Guidebook pdf download</a>, recommended by the NJ Tree Foundation + <a href="https://www.sustainablejc.org/s/Key-Guide_Trees-of-NJ-and-the-Mid-Atlantic-States.pdf">How To Use This Guidebook</a>! (print and take with you when you go Tree Mapping - letter size pages) + <li><a href="https://drive.google.com/file/d/1e6BXNHcna-2BRsFEIYL0zEbchxNIfxPQ/view">Tree Identification and Tree Hazards</a></li> </ul> <h4>Environmental Benefits</h4> diff --git a/opentreemap/treemap/templates/treemap/partials/collectionudf.html b/opentreemap/treemap/templates/treemap/partials/collectionudf.html index 2d4730ca4..cf222e57a 100644 --- a/opentreemap/treemap/templates/treemap/partials/collectionudf.html +++ b/opentreemap/treemap/templates/treemap/partials/collectionudf.html @@ -34,7 +34,7 @@ <h4>{{ udf.name }}</h4> </td> {% if forloop.last %} <td> - <a data-udf-id="{{ udf.pk|unlocalize }}" class="btn">+</a> + <a data-udf-id="{{ udf.pk|unlocalize }}" class="btn add-row">+</a> </td> {% endif %} {% endfor %} diff --git a/opentreemap/treemap/templates/treemap/partials/diameter_calculator.html b/opentreemap/treemap/templates/treemap/partials/diameter_calculator.html index ae5dea0c9..499709653 100644 --- a/opentreemap/treemap/templates/treemap/partials/diameter_calculator.html +++ b/opentreemap/treemap/templates/treemap/partials/diameter_calculator.html @@ -18,7 +18,9 @@ data-value="{{ field.value|unlocalize }}" data-digits="{{ field.digits }}" /> - <span class="input-group-addon" {{ unit_extra|default:"" }}>{{ field.units }}</span> + <div class="input-group-append"> + <span class="input-group-text" {{ unit_extra|default:"" }}>{{ field.units }}</span> + </div> </div> </td> <td> @@ -27,7 +29,9 @@ data-class="circumference-input" type="text" /> - <span class="input-group-addon" {{ unit_extra|default:"" }}>{{ field.units }}</span> + <div class="input-group-append"> + <span class="input-group-text" {{ unit_extra|default:"" }}>{{ field.units }}</span> + </div> </div> </td> </tr> diff --git a/opentreemap/treemap/templates/treemap/partials/eco_benefits.html b/opentreemap/treemap/templates/treemap/partials/eco_benefits.html index c8d2e9ae1..ba2fbae99 100644 --- a/opentreemap/treemap/templates/treemap/partials/eco_benefits.html +++ b/opentreemap/treemap/templates/treemap/partials/eco_benefits.html @@ -3,10 +3,9 @@ {% if request.instance_supports_ecobenefits %} <div {% if not hidecounts %}id="benefit-values"{% endif %} class="benefit-values panel-inner"> - <a class="sidebar-panel-toggle visible-xs-block" + <a class="sidebar-panel-toggle visible-xs-block d-block d-sm-none" id="{% if hidecounts %}feature-panel-toggle{% else %}eco-panel-toggle{% endif %}"> <i class="icon-right-open"></i> - <i class="icon-cancel"></i> </a> {% with all_benefits=benefits.all %} {% include "treemap/partials/benefit_value_row.html" with benefit=all_benefits.totals total=True %} diff --git a/opentreemap/treemap/templates/treemap/partials/map_add_resource.html b/opentreemap/treemap/templates/treemap/partials/map_add_resource.html index 83505497f..9056cc46e 100644 --- a/opentreemap/treemap/templates/treemap/partials/map_add_resource.html +++ b/opentreemap/treemap/templates/treemap/partials/map_add_resource.html @@ -4,13 +4,13 @@ {% with nsteps=5 %} <div class="sidebar-inner"> - <a href="javascript:;" class="close cancelBtn small hidden-xs">×</a> + <a href="javascript:;" class="close cancelBtn small hidden-xs d-none d-sm-block">×</a> <h3>{% trans "Add a" %} {{ term.Resource.singular }}</h3> <div class="add-step-container" id="add-resource-container"> <div class="add-step active"> <div class="add-step-header"> {% blocktrans with Resource=terms.Resource.singular %} Select {{ Resource }} Type {% endblocktrans %} - <a href="javascript:;" class="close cancelBtn small visible-xs-block">×</a> + <a href="javascript:;" class="close cancelBtn small d-block d-sm-none visible-xs-block">×</a> </div> <div class="add-step-content"> <label>{{ term.Resource.singular }} {% trans "Type" %}</label> @@ -40,7 +40,7 @@ <h3>{% trans "Add a" %} {{ term.Resource.singular }}</h3> {% if cls.area_field_name == 'roof_geometry' %} <div class="add-step-header"> {% trans "Indicate Nearby Roof Area" %} - <a href="javascript:;" class="close cancelBtn small visible-xs-block">×</a> + <a href="javascript:;" class="close cancelBtn small visible-xs-block d-block d-sm-none">×</a> </div> <div class="add-step-content"> <div class="alert alert-info">Move the squares on the blue polygon to define the borders of the roof area that will drain into your {{ term.Resource.singular.lower }}. @@ -49,7 +49,7 @@ <h3>{% trans "Add a" %} {{ term.Resource.singular }}</h3> {% else %} <div class="add-step-header"> {% trans "Indicate Polygon Area" %} - <a href="javascript:;" class="close cancelBtn small visible-xs-block">×</a> + <a href="javascript:;" class="close cancelBtn small visible-xs-block d-block d-sm-none">×</a> </div> <div class="add-step-content"> <div class="alert alert-info">Move the squares on the blue polygon to define the borders of your {{ term.Resource.singular.lower }}. @@ -64,7 +64,7 @@ <h3>{% trans "Add a" %} {{ term.Resource.singular }}</h3> <div class="add-step"> <div class="add-step-header"> {% trans "About this" %} {{ term.Resource.singular }} - <a href="javascript:;" class="close cancelBtn small visible-xs-block">×</a> + <a href="javascript:;" class="close cancelBtn small visible-xs-block d-block d-sm-none">×</a> </div> <div class="add-step-content"> <form id="add-resource-form" onsubmit="return false;"> @@ -76,7 +76,7 @@ <h3>{% trans "Add a" %} {{ term.Resource.singular }}</h3> <div class="add-step"> <div class="add-step-header"> {% trans "Finalize this" %} {{ term.Resource.singular }} - <a href="javascript:;" class="close cancelBtn small visible-xs-block">×</a> + <a href="javascript:;" class="close cancelBtn small visible-xs-block d-block d-sm-none">×</a> </div> <div class="add-step-content"> <h3 class="summaryHead"></h3> diff --git a/opentreemap/treemap/templates/treemap/partials/map_feature_accordion.html b/opentreemap/treemap/templates/treemap/partials/map_feature_accordion.html index 4a340d6af..459f1b4d0 100644 --- a/opentreemap/treemap/templates/treemap/partials/map_feature_accordion.html +++ b/opentreemap/treemap/templates/treemap/partials/map_feature_accordion.html @@ -3,7 +3,7 @@ {% load auth_extras %} {% load form_extras %} -<div class="visible-xs-block feature-info"> +<div class="visible-xs-block feature-info d-block d-sm-none"> <h4>{{ title }}</h4> {% if feature.address_full %} <div> diff --git a/opentreemap/treemap/templates/treemap/partials/map_feature_detail_base.html b/opentreemap/treemap/templates/treemap/partials/map_feature_detail_base.html index b4f6f9903..731783421 100644 --- a/opentreemap/treemap/templates/treemap/partials/map_feature_detail_base.html +++ b/opentreemap/treemap/templates/treemap/partials/map_feature_detail_base.html @@ -3,11 +3,11 @@ {% load i18n %} {% load l10n %} {% load util %} -{% load staticfiles %} +{% load static %} {% load auth_extras %} {% load comment_sequence %} -<div class="detail-header"> +<div class="detail-header col-md-12"> {% if request.instance.is_public %} <div class="js-container pull-right" style="display: none"> <a target="_blank" href="http://www.facebook.com/sharer/sharer.php?u={{ share.url }}"> @@ -44,10 +44,10 @@ <h4 class="address" id="map-feature-address">{{ feature.address_full }}</h4> </div> <div class="col-md-8"> <p> + {% if request.user.is_authenticated %} {% if feature.is_plot or feature.is_editable %} <button id="edit-map-feature" data-class="display" - disabled="disabled" data-always-enable="{{ last_effective_instance_user|map_feature_is_writable:feature }}" data-href="{{ request.get_full_path }}" {% if feature.is_plot %} @@ -62,7 +62,6 @@ <h4 class="address" id="map-feature-address">{{ feature.address_full }}</h4> {% endif %} <button id="delete-object" data-class="display" - disabled="disabled" {% if has_tree %} {# TODO: this will not work quite right when a user adds a tree without refreshing #} data-always-enable="{{ last_effective_instance_user|is_deletable:tree }}" @@ -78,6 +77,7 @@ <h4 class="address" id="map-feature-address">{{ feature.address_full }}</h4> data-href="{{ request.get_full_path }}" class="btn btn-sm btn-danger">{% trans "Delete" %}</button> <img class="spinner" src="{% static "img/spinner.gif" %}" style="display: none;"> + {% endif %} </p> <!-- Alerts --> @@ -136,8 +136,10 @@ <h3>{% trans "Comments" %}</h3> {% for close in comment.close %}</li></ul>{% endfor %} {% endfor %} {% if not request.user.is_authenticated %} - <p><a href="{% url 'registration_register' %}">{% trans "Sign Up" %}</a> - {% trans "or" %} <a href="{% url 'auth_login' %}{% login_forward %}">{% trans "log in" %}</a> + <p> + <!--<a href="{% url 'registration_register' %}">{% trans "Sign Up" %}</a> + {% trans "or" %}--> + <a href="{% url 'auth_login' %}{% login_forward %}">{% trans "log in" %}</a> {% trans "to add comments" %}</p> {% endif %} </div> diff --git a/opentreemap/treemap/templates/treemap/partials/map_feature_popup.html b/opentreemap/treemap/templates/treemap/partials/map_feature_popup.html index da2a57ff1..e2d0a1ad5 100644 --- a/opentreemap/treemap/templates/treemap/partials/map_feature_popup.html +++ b/opentreemap/treemap/templates/treemap/partials/map_feature_popup.html @@ -19,6 +19,7 @@ <h4>{{ feature.title }}</h4> <a href="{% url 'map_feature_detail' instance_url_name=request.instance.url_name feature_id=feature.pk %}" class="btn btn-sm btn-secondary">{% trans "More Details" %}</a> + {% if request.user.is_authenticated %} <a disabled="disabled" {% if not feature.is_plot and not feature.is_editable %} style="visibility: hidden;" @@ -28,6 +29,7 @@ <h4>{{ feature.title }}</h4> data-href="{% url 'map_feature_detail_edit' instance_url_name=request.instance.url_name feature_id=feature.pk edit='edit' %}" class="btn btn-sm btn-info">{% trans "Edit" %}</a> + {% endif %} {% if features|length > 1 %} <div class="popup-paging"> <button class="btn btn-sm prev" {% if forloop.first %}disabled=disabled{% endif %}> diff --git a/opentreemap/treemap/templates/treemap/partials/photo_carousel.html b/opentreemap/treemap/templates/treemap/partials/photo_carousel.html index dd8fb235b..77ab19a16 100644 --- a/opentreemap/treemap/templates/treemap/partials/photo_carousel.html +++ b/opentreemap/treemap/templates/treemap/partials/photo_carousel.html @@ -5,11 +5,14 @@ <div class="carousel-inner"> {% for photo in photos %} - <div class="item {{ forloop.first|yesno:"active," }}"> + <div class="carousel-item {{ forloop.first|yesno:"active," }}"> <a data-toggle="modal" href="#photo-lightbox" class="inspect-photo" data-photo-src="{{ photo.image }}" {% if forloop.first %} {# this is the *actual* photo for the map feature and will be used by the sharing code when a new upload takes place #} + data-map-feature-photo-id="{{ photo.id }}" + data-map-feature-id="{{ photo.map_feature_id }}" + data-label="{{ photo.label }}" data-map-feature-photo-detail-absolute-url="{{ photo.absolute_detail_url }}" data-map-feature-photo-image-absolute-url="{{ photo.absolute_image }}" data-map-feature-photo-thumbnail="{{ photo.thumbnail }}" @@ -18,10 +21,14 @@ <img src="{{ photo.thumbnail }}" alt="{% trans 'Photo number' %} {{ forloop.counter }}"> </a> {% if last_effective_instance_user|photo_is_deletable:photo.raw %} + <!-- tzinckgraf --> <a class="delete-photo" title="{% trans 'Delete photo number' %} {{ forloop.counter }}" data-toggle="modal" href="#photo-lightbox"><i class="icon-trash-1"></i> </a> {% endif %} + {% if photo.has_label %} + <button class="btn btn-xs photo-label" disabled="true">{{ photo.label }}</button> + {% endif %} </div> {% empty %} {% if feature.is_plot %} @@ -31,20 +38,22 @@ {% endif %} {% endfor %} </div> +{% if photos|length > 1 %} +<!--a class="carousel-control-prev left" href="#photo-carousel" data-slide="prev"><i class="icon-left-circled"></i></a--> +<a class="carousel-control-prev" href="#photo-carousel" role="button" data-slide="prev"><i class="icon-left-circled"></i></a> +<!--a class="carousel-control right" href="#photo-carousel" data-slide="next"><i class="icon-right-circled"></i></a--> +<a class="carousel-control-next" href="#photo-carousel" role="button" data-slide="next"><i class="icon-right-circled"></i></a> +{% endif %} <div id="tree-photo-thumbnails"> <div> - <ol class="carousel-indicators">{% for photo in photos %} + <ol class="carousel-indicators list-inline">{% for photo in photos %} <li data-target="#photo-carousel" data-slide-to="{{ forloop.counter0 }}" - class="{{ forloop.first|yesno:"active," }}"> + class="{{ forloop.first|yesno:"active," }} list-inline-item"> <img src="{{ photo.thumbnail }}" alt=""> </li> {% endfor %}</ol> </div> </div> -{% if photos|length > 1 %} -<a class="carousel-control left" href="#photo-carousel" data-slide="prev"><i class="icon-left-circled"></i></a> -<a class="carousel-control right" href="#photo-carousel" data-slide="next"><i class="icon-right-circled"></i></a> -{% endif %} {% if error %} {# The data-photo-upload-failed attribute must appear on an element if there was a validation error #} <div class="alert alert-danger" data-photo-upload-failed>{{ error }}</div> diff --git a/opentreemap/treemap/templates/treemap/partials/photo_social_media_sharing.html b/opentreemap/treemap/templates/treemap/partials/photo_social_media_sharing.html index 84ef93bd2..e128f074f 100644 --- a/opentreemap/treemap/templates/treemap/partials/photo_social_media_sharing.html +++ b/opentreemap/treemap/templates/treemap/partials/photo_social_media_sharing.html @@ -7,25 +7,19 @@ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h2>{% trans "Photo Added!" %}</h2> </div> - <div class="modal-body" data-class="share-links"> - <p>{% trans "Would you like to share this photo?" %}</p> - <img id="social-media-sharing-photo-upload-preview"/> - <a data-class="facebook" - data-url-template="http://www.facebook.com/sharer/sharer.php?u=<%= photoDetailUrl %>" - target="_blank"><img src="/static/img/facebook_32.png"></a> - <a data-class="twitter" - data-url-template="http://twitter.com/share?&text={{ photo_upload_share_text }}&url=<%= photoDetailUrl %>" - target="_blank"><img src="/static/img/twitter_32.png"></a> - <a data-class="google" - data-url-template="https://plus.google.com/share?url=<%= photoDetailUrl %>" - target="_blank"><img src="/static/img/googleplus_32.png"></a> - <a data-class="pinterest" - data-url-template="https://www.pinterest.com/pin/create/button/?&media=<%= photoUrl %>&description={{ photo_upload_share_text }}&url=<%= photoDetailUrl %>" - target="_blank"><img src="/static/img/pinterest_32.png"></a> + <div class="modal-body"> + <p>{% trans "Would you to label this photo?" %}</p> + <img id="label-photo-upload-preview"/> </div> <div class="modal-footer"> - <label class="pull-left"><input data-class="social-media-sharing-dont-show-again" type="checkbox"/>{% trans "Don't show me again" %}</label> - <a href="javascript:;" class="btn" data-dismiss="modal">{% trans "No Thanks" %}</a> + <label class="pull-left" for="photo-label">Choose a photo label</label> + <select class="pull-left" id="photo-label"> + <option value=""></value> + <option value="leaf">Leaf</value> + <option value="bark">Bark</value> + <option value="shape">Shape</value> + </select> + <a href="javascript:;" class="btn" data-dismiss="modal">{% trans "Done" %}</a> </div> </div> </div> diff --git a/opentreemap/treemap/templates/treemap/partials/plot_detail.html b/opentreemap/treemap/templates/treemap/partials/plot_detail.html index 0b56015f2..710eb8edf 100644 --- a/opentreemap/treemap/templates/treemap/partials/plot_detail.html +++ b/opentreemap/treemap/templates/treemap/partials/plot_detail.html @@ -18,7 +18,7 @@ <!-- Ecosystem Benefits --> <div id="ecobenefits"> - <h3>{% trans "Yearly Ecosystem Services" %}</h3> + <h3>{% trans "Annual Ecosystem Services" %}</h3> {% if request.instance_supports_ecobenefits %} {% include "treemap/partials/plot_eco.html" %} {% else %} diff --git a/opentreemap/treemap/templates/treemap/partials/sidebar.html b/opentreemap/treemap/templates/treemap/partials/sidebar.html index 7ab57ce21..69b174dfc 100644 --- a/opentreemap/treemap/templates/treemap/partials/sidebar.html +++ b/opentreemap/treemap/templates/treemap/partials/sidebar.html @@ -37,19 +37,13 @@ <h5>{{ user.username }}</h5> </div> {% endif %} -{% if feature.is_plot %} +{% with observation_url=feature.inaturalist_observation_url %} <div class="well"> - <h3>{% trans "Nearby Trees" %}</h3> - {% with nearby=plot.nearby_plots %} - {% if nearby %} - </ul> - {% for plot in nearby %} - <li><a href="{% url 'map_feature_detail' instance_url_name=request.instance.url_name feature_id=plot.pk %}">{{ plot.pk }}</a></li> - {% endfor %} - </ul> + <h3>{% trans "iNaturalist" %}</h3> + {% if observation_url %} + <h5><a href="{{ observation_url }}">Go to observation</a></h5> {% else %} - <p class="text-muted"><em>{% trans "There are no trees nearby" %}</em></p> + <p class="text-muted"><em>This has not been submitted to iNaturalist</em></p> {% endif %} </div> {% endwith %} -{% endif %} diff --git a/opentreemap/treemap/templates/treemap/partials/step_controls.html b/opentreemap/treemap/templates/treemap/partials/step_controls.html index 81852a7ab..91d426020 100644 --- a/opentreemap/treemap/templates/treemap/partials/step_controls.html +++ b/opentreemap/treemap/templates/treemap/partials/step_controls.html @@ -9,14 +9,24 @@ {% trans "of" %} <span class='footer-total-steps'>{{ nsteps }}</span> </span> - <ul class="pager"> + <ul class="pager pagination justify-content-center"> {% if not first %} - <li class="previous"> - <a class="btn btn-primary" href="javascript:;">{% trans "« Back" %}</a> + <li class="previous page-item"> + <a class="btn btn-primary page-link" href="javascript:;">{% trans "« Back" %}</a> </li> {% endif %} - <li class="next"> - <a class="btn btn-primary" href="javascript:;"{% if feature_name and last %} data-event-category="add-{{ feature_name }}" data-event-action="confirm-done"{% endif %}> + <li class="page-item disabled"> + <span class="page-link"> + <strong> + {% trans "Step" %} + <span class='footer-step-number'></span> + </strong> + {% trans "of" %} + <span class='footer-total-steps'>{{ nsteps }}</span> + </span> + </li> + <li class="next page-item"> + <a class="btn btn-primary page-link" href="javascript:;"{% if feature_name and last %} data-event-category="add-{{ feature_name }}" data-event-action="confirm-done"{% endif %}> {% if last %}{% trans "Done" %}{% else %}{% trans "Next »" %}{% endif %} </a> </li> diff --git a/opentreemap/treemap/templates/treemap/partials/step_set_location.html b/opentreemap/treemap/templates/treemap/partials/step_set_location.html index 27e01e969..2d01b4a80 100644 --- a/opentreemap/treemap/templates/treemap/partials/step_set_location.html +++ b/opentreemap/treemap/templates/treemap/partials/step_set_location.html @@ -2,7 +2,7 @@ <div class="add-step with-map"> <div class="add-step-header"> {% trans "Set the" %} {{ feature_name }}'s {% trans "location" %} - <a href="javascript:;" class="close cancelBtn small visible-xs-block">×</a> + <a href="javascript:;" class="close cancelBtn small visible-xs-block d-block d-sm-none">×</a> </div> <div class="add-step-content"> {# Making onsubmit return false prevents the form from being submitted. We want to run JS code instead #} @@ -12,21 +12,21 @@ </form> <a class="geolocate"><i class="icon-direction"></i> {% trans "Use Current Location" %}</a> <div class="alert alert-info place-marker-message"> - <span class="visible-xs-inline"> + <span class="visible-xs-inline d-block d-sm-none"> {% trans 'Tap on the map, enter an address, or select "Use Current Location."' %} </span> - <span class="hidden-xs"> + <span class="hidden-xs d-none d-sm-block"> {% trans 'Choose a point on the map, enter an address with city, or select "Use Current Location."' %} </span> </div> <div class="alert alert-danger text-danger geocode-error" style="display: none;"> - <span class="visible-xs-inline">{% trans "Unable to locate this address." %}</span> - <span class="hidden-xs">{% trans "Unable to locate this address." %}</span> + <span class="visible-xs-inline d-block d-sm-none">{% trans "Unable to locate this address." %}</span> + <span class="hidden-xs d-none d-sm-block">{% trans "Unable to locate this address." %}</span> </div> <div class="alert alert-danger text-danger geolocate-error" style="display: none;"> {% trans "Unable to determine current location" %} </div> - <div class="alert alert-info move-marker-message hidden-xs" style="display: none;"> + <div class="alert alert-info move-marker-message hidden-xs d-none d-sm-block" style="display: none;"> {% trans "Please move marker to exact location of the" %} {{ feature_name }} </div> <div class="alert alert-danger text-danger pointnotinmap-error" style="display: none;"> diff --git a/opentreemap/treemap/templates/treemap/partials/upload_file.html b/opentreemap/treemap/templates/treemap/partials/upload_file.html index 87d896ebc..e031dd2bb 100644 --- a/opentreemap/treemap/templates/treemap/partials/upload_file.html +++ b/opentreemap/treemap/templates/treemap/partials/upload_file.html @@ -1,7 +1,7 @@ {% load i18n %} -<div id="{{ panel_id|default:"upload-panel" }}" class="modal fade"> - <div class="modal-dialog"> +<div id="{{ panel_id|default:"upload-panel" }}" class="modal fade" role="dialog" tabindex="-1"> + <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> @@ -16,6 +16,9 @@ <h3>{{ title }}</h3> <p> <input class="fileChooser" type="file" name="file" data-url="{{ upload_url }}" + data-label="{{ label }}" + data-row-id="#{{ row_id }}" + data-checkbox-id="#{{ checkbox_id }}" accept={{ accept|default:"image/*" }}> </p> <div class="progress"> diff --git a/opentreemap/treemap/templates/treemap/recent_user_edits.html b/opentreemap/treemap/templates/treemap/recent_user_edits.html index 2adfe044a..70f0be7b0 100644 --- a/opentreemap/treemap/templates/treemap/recent_user_edits.html +++ b/opentreemap/treemap/templates/treemap/recent_user_edits.html @@ -32,16 +32,16 @@ {% endfor %} </tbody> </table> -<ul class="pager"> +<ul class="pager pagination"> {% if prev_page %} - <li class="previous"> + <li class="previous page-item"> <a id="recent-user-edits-prev" href="{% url 'user_audits' username=user.username %}{{prev_page }}"> ← {% trans "Previous" %} </a> </li> {% endif %} {% if next_page %} - <li class="next"> + <li class="next page-item"> <a id="recent-user-edits-next" href="{% url 'user_audits' username=user.username %}{{ next_page }}"> {% trans "Next" %} → </a> diff --git a/opentreemap/treemap/templatetags/auth_extras.py b/opentreemap/treemap/templatetags/auth_extras.py index 1ec82eba3..e7bf17712 100644 --- a/opentreemap/treemap/templatetags/auth_extras.py +++ b/opentreemap/treemap/templatetags/auth_extras.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django import template from django.conf import settings @@ -135,7 +133,7 @@ def render(self, context): req_user = template.Variable('request.user').resolve(context) model = self.model_variable.resolve(context) - if (model and req_user and req_user.is_authenticated() + if (model and req_user and req_user.is_authenticated and model.user_can_create(req_user)): content = self.nodelist.render(context) else: @@ -182,7 +180,7 @@ def usercontent_tag(parser, token): 'expected format is: ' 'usercontent for {user_identifier}') - if isinstance(user_identifier, (int, long)): + if isinstance(user_identifier, int): user_identifier = user_identifier else: if user_identifier[0] == '"'\ @@ -211,10 +209,10 @@ def render(self, context): user_identifier = self.user_identifier user_content = self.nodelist.render(context) - if isinstance(user_identifier, (int, long)): + if isinstance(user_identifier, int): if req_user.pk == user_identifier: return user_content - elif isinstance(user_identifier, basestring): + elif isinstance(user_identifier, str): if req_user.username == user_identifier: return user_content else: @@ -239,7 +237,7 @@ def login_forward(context, query_prefix='?'): """ request = template.Variable('request').resolve(context) - if getattr(request, 'user', None) and request.user.is_authenticated(): + if getattr(request, 'user', None) and request.user.is_authenticated: raise ValidationError( _('Can\'t forward login if already logged in')) # urlparse chokes on lazy objects in Python 3, force to str diff --git a/opentreemap/treemap/templatetags/form_extras.py b/opentreemap/treemap/templatetags/form_extras.py index ec4da9ad7..667a4759e 100644 --- a/opentreemap/treemap/templatetags/form_extras.py +++ b/opentreemap/treemap/templatetags/form_extras.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json import re @@ -10,14 +8,14 @@ from django import template from django.template.loader import get_template from django.core.exceptions import ObjectDoesNotExist -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import dateformat from django.utils.translation import ugettext as _ from django.conf import settings from opentreemap.util import dotted_split -from treemap.util import get_model_for_instance, to_object_name, num_format +from treemap.util import get_model_for_instance, to_object_name, num_format, get_field from treemap.json_field import (is_json_field_reference, get_attr_from_json_field) from treemap.units import (get_digits_if_formattable, get_units_if_convertible, @@ -55,25 +53,25 @@ FOREIGN_KEY_PREDICATE = 'IS' -VALID_FIELD_KEYS = ','.join(FIELD_MAPPINGS.keys()) +VALID_FIELD_KEYS = ','.join(list(FIELD_MAPPINGS.keys())) class Variable(Grammar): - grammar = (G(b'"', WORD(b'^"'), b'"') | G(b"'", WORD(b"^'"), b"'") - | WORD(b"a-zA-Z_", b"a-zA-Z0-9_.")) - + grammar = (G('"', WORD('^"'), '"') | G("'", WORD("^'"), "'") + | WORD("a-zA-Z_", "a-zA-Z0-9_.")) + grammar_whitespace_mode = 'optional' class Label(Grammar): - grammar = (G(b'_("', WORD(b'^"'), b'")') | G(b"_('", WORD(b"^'"), b"')") + grammar = (G('_("', WORD('^"'), '")') | G("_('", WORD("^'"), "')") | Variable) - + grammar_whitespace_mode = 'optional' class InlineEditGrammar(Grammar): - grammar = (OR(G(OR(b"field", b"create"), OPTIONAL(Label)), b"search"), - b"from", Variable, OPTIONAL(b"for", Variable), - OPTIONAL(b"in", Variable), b"withtemplate", Variable, - OPTIONAL(b"withhelp", Label)) - grammar_whitespace = True + grammar = (OR(G(OR("field", "create"), OPTIONAL(Label)), "search"), + "from", Variable, OPTIONAL("for", Variable), + OPTIONAL("in", Variable), "withtemplate", Variable, + OPTIONAL("withhelp", Label)) + grammar_whitespace_mode = 'optional' _inline_edit_parser = InlineEditGrammar.parser() @@ -188,8 +186,8 @@ def inline_edit_tag(tag, Node): """ def tag_parser(parser, token): try: - results = _inline_edit_parser.parse_string(token.contents, - reset=True, eof=True) + results = _inline_edit_parser.parse_text(token.contents, + reset=True, eof=True) except ParseError as e: raise template.TemplateSyntaxError( 'expected format: %s [{label}] from {model.property}' @@ -226,7 +224,7 @@ def _token_to_variable(token): elif token[0] == '"' and token[0] == token[-1] and len(token) >= 2: return token[1:-1] else: - return template.Variable(token) + return template.Variable(token.strip()) def _resolve_variable(variable, context): @@ -280,7 +278,7 @@ def field_type_label_choices(model, field_name, label=None, label = label if label else field.verbose_name explanation = explanation if explanation else field.help_text choices = [{'value': choice[0], 'display_value': choice[1]} - for choice in field.choices] + for choice in field.choices] if field.choices else [] if choices and field.null: choices = [{'value': '', 'display_value': ''}] + choices else: @@ -333,125 +331,30 @@ def render(self, context): field_template = get_template(_resolve_variable( self.field_template, context)).template - if not isinstance(identifier, basestring)\ + if not isinstance(identifier, str)\ or not _identifier_regex.match(identifier): raise template.TemplateSyntaxError( 'expected a string with the format "object_name.property" ' 'to follow "from" %s' % identifier) - model_name_or_object_name, field_name = dotted_split(identifier, 2, - maxsplit=1) + model_name_or_object_name, field_name = dotted_split(identifier, 2, maxsplit=1) model = self.get_model(context, model_name_or_object_name, instance) - object_name = to_object_name(model_name_or_object_name) - - identifier = "%s.%s" % (object_name, field_name) - - def _field_value(model, field_name, data_type): - udf_field_name = field_name.replace('udf:', '') - val = None - if field_name in [f.name for f in model._meta.get_fields()]: - try: - val = getattr(model, field_name) - except (ObjectDoesNotExist, AttributeError): - pass - elif _is_udf(model, udf_field_name): - if udf_field_name in model.udfs: - val = model.udfs[udf_field_name] - # multichoices place a json serialized data-value - # on the dom element and client-side javascript - # processes it into a view table and edit widget - if data_type == 'multichoice': - val = json.dumps(val) - elif data_type == 'multichoice': - val = '[]' - else: - raise ValueError('Could not find field: %s' % field_name) - - return val - - if is_json_field_reference(field_name): - field_value = get_attr_from_json_field(model, field_name) - choices = None - is_visible = is_editable = True - data_type = "string" - else: - add_blank = (ADD_BLANK_ALWAYS if self.treat_multichoice_as_choice - else ADD_BLANK_IF_CHOICE_FIELD) - data_type, label, explanation, choices = field_type_label_choices( - model, field_name, label, explanation=explanation, - add_blank=add_blank) - field_value = _field_value(model, field_name, data_type) - - if user is not None and hasattr(model, 'field_is_visible'): - is_visible = model.field_is_visible(user, field_name) - is_editable = model.field_is_editable(user, field_name) - else: - # This tag can be used without specifying a user. In that case - # we assume that the content is visible and upstream code is - # responsible for only showing the content to the appropriate - # user - is_visible = True - is_editable = True - - digits = units = '' - - if hasattr(model, 'instance'): - digits = get_digits_if_formattable( - model.instance, object_name, field_name) - - units = get_units_if_convertible( - model.instance, object_name, field_name) - if units != '': - units = get_unit_abbreviation(units) - - if data_type == 'foreign_key': - # rendered clientside - display_val = '' - elif field_value is None: - display_val = None - elif data_type in ['date', 'datetime']: - fmt = (model.instance.short_date_format if model.instance - else settings.SHORT_DATE_FORMAT) - display_val = dateformat.format(field_value, fmt) - elif is_convertible_or_formattable(object_name, field_name): - display_val = format_value( - model.instance, object_name, field_name, field_value) - if units != '': - display_val += (' %s' % units) - elif data_type == 'bool': - display_val = _('Yes') if field_value else _('No') - elif data_type == 'multichoice': - # this is rendered clientside from data attributes so - # there's no meaningful intermediate value to send - # without rendering the same markup server-side. - display_val = None - elif choices: - display_vals = [choice['display_value'] for choice in choices - if choice['value'] == field_value] - display_val = display_vals[0] if display_vals else field_value - elif data_type == 'float': - display_val = num_format(field_value) - else: - display_val = unicode(field_value) - - context['field'] = { - 'label': label, - 'explanation': explanation, - 'identifier': identifier, - 'value': field_value, - 'display_value': display_val, - 'units': units, - 'digits': digits, - 'data_type': data_type, - 'is_visible': is_visible, - 'is_editable': is_editable, - 'choices': choices, - } + context['field'] = get_field( + context, + label, + identifier, + user, + instance, + explanation, + treat_multichoice_as_choice=self.treat_multichoice_as_choice, + model=model, + ) self.get_additional_context( context['field'], model, field_name, context.get('q', '')) - return field_template.render(context) + _field = field_template.render(context) + return _field class FieldNode(AbstractNode): @@ -535,7 +438,7 @@ def get_additional_context(self, field, model, field_name, search_query): def update_field(settings): # Identifier is lower-cased above to match the calling convention # of update endpoints, so we shouldn't overwrite it :( - field.update({k: v for k, v in settings.items() + field.update({k: v for k, v in list(settings.items()) if v is not None and k != 'identifier'}) search_settings = getattr(model, 'search_settings', {}).get(field_name) diff --git a/opentreemap/treemap/templatetags/instance_config.py b/opentreemap/treemap/templatetags/instance_config.py index 4f32fa92a..cf41cbd29 100644 --- a/opentreemap/treemap/templatetags/instance_config.py +++ b/opentreemap/treemap/templatetags/instance_config.py @@ -1,6 +1,4 @@ -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json diff --git a/opentreemap/treemap/templatetags/urls.py b/opentreemap/treemap/templatetags/urls.py index 129bd9176..aba48d6fe 100644 --- a/opentreemap/treemap/templatetags/urls.py +++ b/opentreemap/treemap/templatetags/urls.py @@ -1,4 +1,4 @@ -import urlparse +import urllib.parse from django import template from django.http.request import QueryDict @@ -9,13 +9,13 @@ class UrlHelper(object): def __init__(self, full_path): - url = urlparse.urlparse(full_path) + url = urllib.parse.urlparse(full_path) self.path = url.path self.fragment = url.fragment self.query_dict = QueryDict(url.query, mutable=True) def update_query_data(self, **kwargs): - for key, val in kwargs.iteritems(): + for key, val in kwargs.items(): if hasattr(val, '__iter__'): self.query_dict.setlist(key, val) else: diff --git a/opentreemap/treemap/templatetags/util.py b/opentreemap/treemap/templatetags/util.py index 50f14c526..f518576d9 100644 --- a/opentreemap/treemap/templatetags/util.py +++ b/opentreemap/treemap/templatetags/util.py @@ -1,5 +1,5 @@ from django import template -from django.core.urlresolvers import reverse +from django.urls import reverse from django.core.exceptions import ObjectDoesNotExist import json @@ -110,7 +110,7 @@ def audit_detail_link(audit): """ model = audit.model - if model in MapFeature.subclass_dict().keys(): + if model in list(MapFeature.subclass_dict().keys()): model = 'mapfeature' model = model.lower() @@ -134,7 +134,7 @@ def terminology(model, instance): @register.filter def display_name(audit_or_model_or_name, instance=None): audit = None - if isinstance(audit_or_model_or_name, (Audit, basestring)): + if isinstance(audit_or_model_or_name, (Audit, str)): if isinstance(audit_or_model_or_name, Audit): audit = audit_or_model_or_name name = audit.model diff --git a/opentreemap/treemap/tests/__init__.py b/opentreemap/treemap/tests/__init__.py index 02f1eeb5c..0fb79fb3d 100644 --- a/opentreemap/treemap/tests/__init__.py +++ b/opentreemap/treemap/tests/__init__.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import logging -from cStringIO import StringIO +from io import BytesIO import subprocess import shutil import tempfile @@ -367,7 +365,9 @@ def make_request(params=None, user=None, instance=None, extra = {} if body: - body_stream = StringIO(body) + body_stream = BytesIO(body) \ + if type(body) == bytes \ + else BytesIO(body.encode()) extra['wsgi.input'] = body_stream extra['CONTENT_LENGTH'] = len(body) @@ -432,7 +432,7 @@ def resource_path(self, name): return path def load_resource(self, name): - return file(self.resource_path(name)) + return open(self.resource_path(name), 'rb') def tearDown(self): shutil.rmtree(self.photoDir) diff --git a/opentreemap/treemap/tests/test_audit.py b/opentreemap/treemap/tests/test_audit.py index 8b6a12e85..628462100 100644 --- a/opentreemap/treemap/tests/test_audit.py +++ b/opentreemap/treemap/tests/test_audit.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import psycopg2 import json @@ -10,7 +8,7 @@ from django.test.client import RequestFactory from django.core.exceptions import (FieldError, ValidationError, ObjectDoesNotExist) -from django.core.urlresolvers import reverse +from django.urls import reverse from django.db import IntegrityError, connection from django.contrib.gis.geos import Point @@ -92,12 +90,12 @@ def test_scope_model_method(self): method_instance_2_trees = list(self.instance2.scope_model(Tree)) # Test that it returns the same as using the ORM - self.assertEquals(orm_instance_1_trees, method_instance_1_trees) - self.assertEquals(orm_instance_2_trees, method_instance_2_trees) + self.assertEqual(orm_instance_1_trees, method_instance_1_trees) + self.assertEqual(orm_instance_2_trees, method_instance_2_trees) # Test that it didn't grab all trees - self.assertNotEquals(list(all_trees), method_instance_1_trees) - self.assertNotEquals(list(all_trees), method_instance_2_trees) + self.assertNotEqual(list(all_trees), method_instance_1_trees) + self.assertNotEqual(list(all_trees), method_instance_2_trees) # Models that do not have any relation to Instance should # raise an error if you attempt to scope them. @@ -144,13 +142,13 @@ def assertAuditsEqual(self, exps, acts): raise AssertionError('Missing audit record for %s' % act) def make_audit(self, pk, field, old, new, - action=Audit.Type.Insert, user=None, model=u'Tree'): + action=Audit.Type.Insert, user=None, model='Tree'): if field: - field = unicode(field) + field = str(field) if old: - old = unicode(old) + old = str(old) if new: - new = unicode(new) + new = str(new) user = user or self.user1 @@ -531,7 +529,7 @@ def test_can_create_obj_even_if_some_fields_are_pending(self): .filter(field_name__in=['id', 'geom', 'readonly'])\ .update(permission_level=FieldPermission.WRITE_DIRECTLY) - self.assertEquals(Plot.objects.count(), 0) + self.assertEqual(Plot.objects.count(), 0) # new_plot should be created, but there should be # a pending record for length (and it should not be @@ -542,7 +540,7 @@ def test_can_create_obj_even_if_some_fields_are_pending(self): new_plot.save_with_user(self.pending_user) - self.assertEquals(Plot.objects.count(), 1) + self.assertEqual(Plot.objects.count(), 1) @skip("Insert pending approval not implemented at this time") def test_insert_writes_when_approved(self): @@ -553,8 +551,8 @@ def test_insert_writes_when_approved(self): new_tree = Tree(plot=new_plot, instance=self.instance) new_tree.save_with_user(self.pending_user) - self.assertEquals(Plot.objects.count(), 0) - self.assertEquals(Tree.objects.count(), 0) + self.assertEqual(Plot.objects.count(), 0) + self.assertEqual(Tree.objects.count(), 0) approve_or_reject_audits_and_apply( list(new_tree.audits()) + list(new_plot.audits()), diff --git a/opentreemap/treemap/tests/test_auth.py b/opentreemap/treemap/tests/test_auth.py index ecd9279e6..cea67520d 100644 --- a/opentreemap/treemap/tests/test_auth.py +++ b/opentreemap/treemap/tests/test_auth.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from treemap.tests import (RequestTestCase, make_instance, make_user) @@ -76,7 +74,7 @@ def test_private_instance_is_not_accessible_without_login(self): self.make_instance_private() res = self.client.get('/%s/map/' % self.instance.url_name) self.assertRedirects(res, - 'http://testserver/accounts/login/?next=/%s/map/' + '/accounts/login/?next=/%s/map/' % self.instance.url_name) def test_private_instance_is_not_accessible_by_non_instance_user(self): @@ -85,7 +83,7 @@ def test_private_instance_is_not_accessible_by_non_instance_user(self): {'username': 'user', 'password': 'password'}) res = self.client.get('/%s/map/' % self.instance.url_name) - self.assertRedirects(res, 'http://testserver/not-available') + self.assertRedirects(res, '/not-available') def test_private_instance_accessible_by_instance_user(self): self.make_instance_private() diff --git a/opentreemap/treemap/tests/test_cached_audit_info.py b/opentreemap/treemap/tests/test_cached_audit_info.py index eda49634b..a4ec919eb 100644 --- a/opentreemap/treemap/tests/test_cached_audit_info.py +++ b/opentreemap/treemap/tests/test_cached_audit_info.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import pytz @@ -30,7 +28,7 @@ def setUp(self): self.plot.save_with_user(self.user) def max_audit_for_model_type(self, models): - if isinstance(models, basestring): + if isinstance(models, str): models = [models] audits = Audit.objects.filter(model__in=models)\ .order_by('-created') diff --git a/opentreemap/treemap/tests/test_dates.py b/opentreemap/treemap/tests/test_dates.py index dc4bc5d99..d79e9f792 100644 --- a/opentreemap/treemap/tests/test_dates.py +++ b/opentreemap/treemap/tests/test_dates.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division from datetime import datetime diff --git a/opentreemap/treemap/tests/test_ecobenefits.py b/opentreemap/treemap/tests/test_ecobenefits.py index 6f6ed0ff7..db0cc6ae7 100644 --- a/opentreemap/treemap/tests/test_ecobenefits.py +++ b/opentreemap/treemap/tests/test_ecobenefits.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json diff --git a/opentreemap/treemap/tests/test_hide_at_zoom.py b/opentreemap/treemap/tests/test_hide_at_zoom.py index bbf09da32..a5e36b140 100644 --- a/opentreemap/treemap/tests/test_hide_at_zoom.py +++ b/opentreemap/treemap/tests/test_hide_at_zoom.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.contrib.gis.geos import Point from django.db.models import Count diff --git a/opentreemap/treemap/tests/test_images.py b/opentreemap/treemap/tests/test_images.py index b6da0468d..f06635a07 100644 --- a/opentreemap/treemap/tests/test_images.py +++ b/opentreemap/treemap/tests/test_images.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from PIL import Image diff --git a/opentreemap/treemap/tests/test_instance.py b/opentreemap/treemap/tests/test_instance.py index e4bebf6be..1305106db 100644 --- a/opentreemap/treemap/tests/test_instance.py +++ b/opentreemap/treemap/tests/test_instance.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json diff --git a/opentreemap/treemap/tests/test_json_field.py b/opentreemap/treemap/tests/test_json_field.py index 0784ba468..e98194caf 100644 --- a/opentreemap/treemap/tests/test_json_field.py +++ b/opentreemap/treemap/tests/test_json_field.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from treemap.tests import make_instance from treemap.instance import Instance @@ -58,8 +56,8 @@ def test_contains_lookup(self): self.instance.config = ['a', 'b', 'c'] self.instance.save() - self.assertEquals(set(Instance.objects.filter(config__contains='a')), - {self.instance}) + self.assertEqual(set(Instance.objects.filter(config__contains='a')), + {self.instance}) - self.assertEquals(set(Instance.objects.filter(config__contains='x')), - set()) + self.assertEqual(set(Instance.objects.filter(config__contains='x')), + set()) diff --git a/opentreemap/treemap/tests/test_management.py b/opentreemap/treemap/tests/test_management.py index 763187fb8..0088d750b 100644 --- a/opentreemap/treemap/tests/test_management.py +++ b/opentreemap/treemap/tests/test_management.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division -from StringIO import StringIO + +from io import StringIO from django.core.management import call_command diff --git a/opentreemap/treemap/tests/test_map_feature.py b/opentreemap/treemap/tests/test_map_feature.py index 019e6fa02..04b0590dc 100644 --- a/opentreemap/treemap/tests/test_map_feature.py +++ b/opentreemap/treemap/tests/test_map_feature.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from contextlib import contextmanager from unittest.case import skip @@ -281,11 +279,12 @@ def test_all_ecobenefits(self): plot_currencies = { cat: benefit.get('currency', None) - for cat, benefit in plot_benefits.items()} + for cat, benefit in list(plot_benefits.items())} self.assertIsNotNone(min(plot_currencies.values())) - expected_total_currency = sum( - [benefit['currency'] for benefit in plot_benefits.values()]) - \ + plot_benefits = [ + benefit['currency'] for benefit in list(plot_benefits.values())] + expected_total_currency = sum(plot_benefits) - \ plot_benefits[BenefitCategory.CO2STORAGE]['currency'] + \ benefits['resource'][BenefitCategory.STORMWATER]['currency'] diff --git a/opentreemap/treemap/tests/test_middleware.py b/opentreemap/treemap/tests/test_middleware.py index 1179fc7b0..102430340 100644 --- a/opentreemap/treemap/tests/test_middleware.py +++ b/opentreemap/treemap/tests/test_middleware.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf import settings from django.http import HttpResponseRedirect @@ -27,7 +25,7 @@ def __init__(self, http_user_agent=None, path_info='/', other_params=None): 'PATH_INFO': path_info } if other_params: - for k, v in other_params.items(): + for k, v in list(other_params.items()): self.META[k] = v @@ -40,7 +38,7 @@ def _request_with_agent(self, *args, **kwargs): def _assert_redirects(self, response, expected_url): self.assertTrue(isinstance(response, HttpResponseRedirect)) - self.assertEquals(expected_url, response["Location"]) + self.assertEqual(expected_url, response["Location"]) def test_detects_ie(self): req, __ = self._request_with_agent(USER_AGENT_STRINGS.IE_7) @@ -61,43 +59,43 @@ def test_sets_version_and_does_not_redirect_for_ie_11(self): req, res = self._request_with_agent(USER_AGENT_STRINGS.IE_11) self.assertIsNone(res, 'Expected the middleware to return a None ' 'response (no redirect) for IE 11') - self.assertEquals(11, req.ie_version, 'Expected the middleware to ' - 'set "ie_version" to 11') + self.assertEqual(11, req.ie_version, 'Expected the middleware to ' + 'set "ie_version" to 11') def test_sets_version_and_redirects_ie_10(self): req, res = self._request_with_agent(USER_AGENT_STRINGS.IE_10) self._assert_redirects(res, settings.IE_VERSION_UNSUPPORTED_REDIRECT_PATH) - self.assertEquals(10, req.ie_version, 'Expected the middleware to set ' - '"ie_version" to 10') + self.assertEqual(10, req.ie_version, 'Expected the middleware to set ' + '"ie_version" to 10') def test_sets_version_and_redirects_ie_9(self): req, res = self._request_with_agent(USER_AGENT_STRINGS.IE_9) self._assert_redirects(res, settings.IE_VERSION_UNSUPPORTED_REDIRECT_PATH) - self.assertEquals(9, req.ie_version, 'Expected the middleware to set ' - '"ie_version" to 9') + self.assertEqual(9, req.ie_version, 'Expected the middleware to set ' + '"ie_version" to 9') def test_sets_version_and_redirects_ie_8(self): req, res = self._request_with_agent(USER_AGENT_STRINGS.IE_8) self._assert_redirects(res, settings.IE_VERSION_UNSUPPORTED_REDIRECT_PATH) - self.assertEquals(8, req.ie_version, 'Expected the middleware to set ' - '"ie_version" to 8') + self.assertEqual(8, req.ie_version, 'Expected the middleware to set ' + '"ie_version" to 8') def test_sets_version_and_redirects_ie_7(self): req, res = self._request_with_agent(USER_AGENT_STRINGS.IE_7) self._assert_redirects(res, settings.IE_VERSION_UNSUPPORTED_REDIRECT_PATH) - self.assertEquals(7, req.ie_version, 'Expected the middleware to set ' - '"ie_version" to 7') + self.assertEqual(7, req.ie_version, 'Expected the middleware to set ' + '"ie_version" to 7') def test_sets_version_and_redirects_ie_6(self): req, res = self._request_with_agent(USER_AGENT_STRINGS.IE_6) self._assert_redirects(res, settings.IE_VERSION_UNSUPPORTED_REDIRECT_PATH) - self.assertEquals(6, req.ie_version, 'Expected the middleware to set ' - '"ie_version" to 6') + self.assertEqual(6, req.ie_version, 'Expected the middleware to set ' + '"ie_version" to 6') def test_detects_json_and_does_not_redirect(self): params = {'HTTP_ACCEPT': 'application/json;q=0.9,*/*;q=0.8'} diff --git a/opentreemap/treemap/tests/test_models.py b/opentreemap/treemap/tests/test_models.py index 3bb0eda35..69430b7ec 100644 --- a/opentreemap/treemap/tests/test_models.py +++ b/opentreemap/treemap/tests/test_models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.test import SimpleTestCase from django.test.utils import override_settings @@ -75,19 +73,19 @@ def test_changing_fields_changes_hash(self): class SpeciesModelTests(OTMTestCase): def test_scientific_name_genus(self): s = Species(genus='Ulmus') - self.assertEquals(s.scientific_name, 'Ulmus') + self.assertEqual(s.scientific_name, 'Ulmus') def test_scientific_name_genus_species(self): s = Species(genus='Ulmus', species='rubra') - self.assertEquals(s.scientific_name, 'Ulmus rubra') + self.assertEqual(s.scientific_name, 'Ulmus rubra') def test_scientific_name_genus_cultivar(self): s = Species(genus='Ulmus', cultivar='Columella') - self.assertEquals(s.scientific_name, "Ulmus 'Columella'") + self.assertEqual(s.scientific_name, "Ulmus 'Columella'") def test_scientific_name_all(self): s = Species(genus='Ulmus', species='rubra', cultivar='Columella') - self.assertEquals(s.scientific_name, "Ulmus rubra 'Columella'") + self.assertEqual(s.scientific_name, "Ulmus rubra 'Columella'") class ModelUnicodeTests(OTMTestCase): @@ -142,42 +140,42 @@ def setUp(self): self.reputation_metric.save_base() def test_instance_model(self): - self.assertEqual(unicode(self.instance), "Test Instance") + self.assertEqual(str(self.instance), "Test Instance") def test_species_model(self): self.assertEqual( - unicode(self.species), + str(self.species), "Test Common Name [Test Genus Test Species 'Test Cultivar']") def test_user_model(self): - self.assertEqual(unicode(self.user), 'commander') + self.assertEqual(str(self.user), 'commander') def test_plot_model(self): - self.assertEqual(unicode(self.plot), + self.assertEqual(str(self.plot), 'Plot (1.0, 1.0) 123 Main Street') def test_tree_model(self): - self.assertEqual(unicode(self.tree), '') + self.assertEqual(str(self.tree), '') def test_boundary_model(self): - self.assertEqual(unicode(self.boundary), 'Test Boundary') + self.assertEqual(str(self.boundary), 'Test Boundary') def test_role_model(self): - self.assertEqual(unicode(self.role), 'Test Role (%s)' % self.role.pk) + self.assertEqual(str(self.role), 'Test Role (%s)' % self.role.pk) def test_field_permission_model(self): - self.assertEqual(unicode(self.field_permission), + self.assertEqual(str(self.field_permission), 'Tree.readonly - Test Role (%s) - Read Only' % self.role.pk) def test_audit_model(self): self.assertEqual( - unicode(self.audit), + str(self.audit), 'pk=%s - action=Update - Tree.readonly:(1) - True => False' % self.audit.pk) def test_reputation_metric_model(self): - self.assertEqual(unicode(self.reputation_metric), + self.assertEqual(str(self.reputation_metric), 'Test Instance - Tree - Test Action') @@ -422,6 +420,7 @@ def test_url_name_allows_lcase_numbers_and_hyphens(self): def test_url_name_must_be_unique(self): make_instance(url_name='philly') + #django.core.exceptions.ValidationError self.assertRaises(make_instance, url_name='philly') def test_has_itree_region_with_nothing(self): diff --git a/opentreemap/treemap/tests/test_perms.py b/opentreemap/treemap/tests/test_perms.py index 41b371d4b..e619a5c0b 100644 --- a/opentreemap/treemap/tests/test_perms.py +++ b/opentreemap/treemap/tests/test_perms.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType diff --git a/opentreemap/treemap/tests/test_search.py b/opentreemap/treemap/tests/test_search.py index 8201c4e1a..3d6842f72 100644 --- a/opentreemap/treemap/tests/test_search.py +++ b/opentreemap/treemap/tests/test_search.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json import psycopg2 @@ -111,14 +109,14 @@ def _create_tree_and_plot(instance, user, point, plot = Plot(geom=point, instance=instance) if plotudfs: - for k, v in plotudfs.iteritems(): + for k, v in plotudfs.items(): plot.udfs[k] = v plot.save_with_user(user) tree = Tree(plot=plot, instance=instance) if treeudfs: - for k, v in treeudfs.iteritems(): + for k, v in treeudfs.items(): tree.udfs[k] = v tree.save_with_user(user) @@ -528,14 +526,14 @@ def create_tree_and_plot(self, plotudfs=None, treeudfs=None): plot = Plot(geom=self.p1, instance=self.instance) if plotudfs: - for k, v in plotudfs.iteritems(): + for k, v in plotudfs.items(): plot.udfs[k] = v plot.save_with_user(self.commander) tree = Tree(plot=plot, instance=self.instance) if treeudfs: - for k, v in treeudfs.iteritems(): + for k, v in treeudfs.items(): tree.udfs[k] = v tree.save_with_user(self.commander) diff --git a/opentreemap/treemap/tests/test_search_fields.py b/opentreemap/treemap/tests/test_search_fields.py index 0403a4d11..1b924100c 100644 --- a/opentreemap/treemap/tests/test_search_fields.py +++ b/opentreemap/treemap/tests/test_search_fields.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json @@ -19,17 +17,17 @@ def setUp(self): def assert_search_present(self, **groups): search = self.instance.advanced_search_fields(self.user) - for group_name, field in groups.iteritems(): + for group_name, field in groups.items(): self.assertIn(group_name, search) search_group = search[group_name] field_info = search_group[0] if 'label' in field_info: - field_info['label'] = unicode(field_info['label']) + field_info['label'] = str(field_info['label']) if 'id' in field_info: del field_info['id'] - self.assertEquals(field_info, field) + self.assertEqual(field_info, field) def assert_search_absent(self, group_name): search = self.instance.advanced_search_fields(self.user) @@ -86,15 +84,15 @@ def setUp(self): def assert_search_present(self, **groups): search = mobile_search_fields(self.instance) - for group_name, field in groups.iteritems(): + for group_name, field in groups.items(): self.assertIn(group_name, search) search_group = search[group_name] field_info = search_group[0] if 'label' in field_info: - field_info['label'] = unicode(field_info['label']) + field_info['label'] = str(field_info['label']) - self.assertEquals(field_info, field) + self.assertEqual(field_info, field) def test_missing_filters(self): self.instance.mobile_search_fields = { diff --git a/opentreemap/treemap/tests/test_species.py b/opentreemap/treemap/tests/test_species.py index e80f91c95..cdc0fbd9c 100644 --- a/opentreemap/treemap/tests/test_species.py +++ b/opentreemap/treemap/tests/test_species.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from treemap.tests.base import OTMTestCase from treemap.species import species_for_otm_code, species_for_scientific_name diff --git a/opentreemap/treemap/tests/test_templatetags.py b/opentreemap/treemap/tests/test_templatetags.py index 6291e778e..e51db8c6f 100644 --- a/opentreemap/treemap/tests/test_templatetags.py +++ b/opentreemap/treemap/tests/test_templatetags.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from copy import deepcopy import tempfile @@ -619,11 +617,11 @@ def test_create_uses_new_model(self): content = template.render(Context({ 'request': {'user': self.observer, 'instance': self.instance} })).strip() - self.assertEqual(content, unicode(Plot().length)) + self.assertEqual(content, str(Plot().length)) def test_search_uses_new_model(self): self.assert_search_context_value( - self.observer, 'field.value', unicode(Plot().length), + self.observer, 'field.value', str(Plot().length), {'identifier': 'Plot.length'}) def test_search_adds_field_config(self): @@ -646,7 +644,7 @@ def test_search_adds_field_config(self): def test_search_gets_default_label_when_none_given(self): self.assert_search_context_value( self.observer, 'field.label', - unicode(Plot._meta.get_field('length').verbose_name), + str(Plot._meta.get_field('length').verbose_name), {'identifier': 'Plot.length', 'label': None}) def test_search_fields_get_added_only_for_valid_json_matches(self): diff --git a/opentreemap/treemap/tests/test_udfs.py b/opentreemap/treemap/tests/test_udfs.py index bf8621fe6..0e32a50d0 100644 --- a/opentreemap/treemap/tests/test_udfs.py +++ b/opentreemap/treemap/tests/test_udfs.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import json from random import shuffle @@ -67,9 +65,9 @@ def setUp(self): def test_set_item_to_none_removes_key(self): self.d['Test choice'] = 'a' - self.assertEqual(1, len(self.d.keys())) + self.assertEqual(1, len(list(self.d.keys()))) self.d['Test choice'] = None - self.assertEqual(0, len(self.d.keys())) + self.assertEqual(0, len(list(self.d.keys()))) def test_setting_nonexistant_key_to_none_is_a_noop(self): # Should not raise an error @@ -147,7 +145,7 @@ def setUp(self): def create_and_save_with_choice(c, n=1): plots = [] - for i in xrange(n): + for i in range(n): plot = Plot(geom=self.p, instance=self.instance) plot.udfs['Test choice'] = c plot.save_with_user(self.commander_user) @@ -1200,7 +1198,7 @@ def test_can_delete(self): all_new_stews = reloaded_plot.udfs['Stewardship'] # Keep only 'prune' (note that UDF collection values are unordered) - new_stews = filter(lambda v: v['action'] == 'prune', all_new_stews) + new_stews = [v for v in all_new_stews if v['action'] == 'prune'] reloaded_plot.udfs['Stewardship'] = new_stews reloaded_plot.save_with_user(self.commander_user) @@ -1304,7 +1302,7 @@ def test_delete_udf_deletes_mobile_api_field(self): udf_def.delete() updated_instance = Instance.objects.get(pk=self.instance.pk) - self.assertEquals( + self.assertEqual( 0, len(updated_instance.mobile_api_fields[0]['field_keys'])) def test_delete_cudf_deletes_mobile_api_field_group(self): @@ -1341,13 +1339,13 @@ def test_delete_cudf_deletes_mobile_api_field_group(self): tree_udf_def.delete() updated_instance = Instance.objects.get(pk=self.instance.pk) - self.assertEquals(1, len( + self.assertEqual(1, len( updated_instance.mobile_api_fields[1]['collection_udf_keys'])) plot_udf_def.delete() updated_instance = Instance.objects.get(pk=self.instance.pk) - self.assertEquals(1, len(updated_instance.mobile_api_fields)) + self.assertEqual(1, len(updated_instance.mobile_api_fields)) class UdfCRUTestCase(OTMTestCase): diff --git a/opentreemap/treemap/tests/test_units.py b/opentreemap/treemap/tests/test_units.py index c78c2e392..4e750c912 100644 --- a/opentreemap/treemap/tests/test_units.py +++ b/opentreemap/treemap/tests/test_units.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.test.utils import override_settings diff --git a/opentreemap/treemap/tests/test_urls.py b/opentreemap/treemap/tests/test_urls.py index a47884a06..6eb7249a6 100644 --- a/opentreemap/treemap/tests/test_urls.py +++ b/opentreemap/treemap/tests/test_urls.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import os import json -from django.core.urlresolvers import reverse +from django.urls import reverse from django.test.utils import override_settings from treemap.models import Plot, Tree @@ -66,7 +64,7 @@ def assert_redirects_to_static_file(self, url, expected_url): self.assert_static_file_exists(expected_url) def assert_static_file_exists(self, url): - self.assertEquals(url[:8], '/static/') + self.assertEqual(url[:8], '/static/') path = os.path.join(STATIC_ROOT, url[8:]) self.assertTrue(os.path.exists(path)) diff --git a/opentreemap/treemap/tests/test_util.py b/opentreemap/treemap/tests/test_util.py index 73c117a0e..57a8240bf 100644 --- a/opentreemap/treemap/tests/test_util.py +++ b/opentreemap/treemap/tests/test_util.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.contrib.sessions.middleware import SessionMiddleware from django.test.utils import override_settings @@ -47,29 +45,29 @@ def test_session(self): # processor (that should be inserted by the session) # # By default, nothing is in session - self.assertEqual(self.client.get('/test-last-instance').content, '') + self.assertEqual(self.client.get('/test-last-instance').content.decode('UTF-8'), '') # Going to an instance sets the context variable self.client.get('/%s/map/' % self.instance1.url_name) - self.assertEqual(self.client.get('/test-last-instance').content, + self.assertEqual(self.client.get('/test-last-instance').content.decode('UTF-8'), self._format(self.instance1.pk)) # Going to a non-public instance doesn't update it self.client.get('/%s/map/' % self.instance3.url_name) - self.assertEqual(self.client.get('/test-last-instance').content, + self.assertEqual(self.client.get('/test-last-instance').content.decode('UTF-8'), self._format(self.instance1.pk)) # Going to a private instance while not logged in # also doesn't update self.client.get('/%s/map/' % self.instance4.url_name) - self.assertEqual(self.client.get('/test-last-instance').content, + self.assertEqual(self.client.get('/test-last-instance').content.decode('UTF-8'), self._format(self.instance1.pk)) self.client.login(username='joe', password='joe') # But should change after logging in self.client.get('/%s/map/' % self.instance4.url_name) - self.assertEqual(self.client.get('/test-last-instance').content, + self.assertEqual(self.client.get('/test-last-instance').content.decode('UTF-8'), self._format(self.instance4.pk)) def test_get_last_instance(self): diff --git a/opentreemap/treemap/tests/test_views.py b/opentreemap/treemap/tests/test_views.py index 32b26a9c1..8c3509b60 100644 --- a/opentreemap/treemap/tests/test_views.py +++ b/opentreemap/treemap/tests/test_views.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import os import json -from StringIO import StringIO +from io import StringIO import psycopg2 from django.test.utils import override_settings @@ -156,7 +154,7 @@ def test_boundary_to_geojson_view(self): self.instance, boundary.pk) - self.assertEqual(response.content, + self.assertEqual(response.content.decode('utf-8'), boundary.geom.transform(4326, clone=True).geojson) self._assert_response_is_srid_3857_distance(response, distance) @@ -170,7 +168,7 @@ def test_anonymous_boundary_to_geojson_view(self): self.instance, boundary.pk) - self.assertEqual(response.content, + self.assertEqual(response.content.decode('utf-8'), boundary.geom.transform(4326, clone=True).geojson) self._assert_response_is_srid_3857_distance(response, distance) @@ -179,7 +177,7 @@ def test_add_anonymous_boundary_view(self): distance3857 = 1.0 point3857 = Point(distance3857, distance3857, srid=3857) point4326 = point3857.transform(4326, clone=True) - n = point4326.get_x() + n = point4326.x request_dict = { 'polygon': [[n, n], [n, n+1], [n+1, n+1], [n+1, n], [n, n]] } @@ -195,7 +193,7 @@ def test_add_anonymous_boundary_view(self): self.instance, boundary_id) - self.assertEqual(gjs_response.content, + self.assertEqual(gjs_response.content.decode('utf-8'), anonymous_boundary.geom.transform( 4326, clone=True).geojson) @@ -228,8 +226,8 @@ def _assert_response_is_srid_3857_distance(self, response, distance): json_response = json.loads(response.content) response_upper_left = Point(json_response['coordinates'][0][0][0], srid=4326) - self.assertAlmostEqual(response_upper_left.get_x(), - upper_left_4326.get_x()) + self.assertAlmostEqual(response_upper_left.x, + upper_left_4326.x) class TreePhotoTestCase(LocalMediaTestCase): @@ -682,12 +680,13 @@ def test_does_not_create_tree_if_tree_field_value_is_an_empty_string(self): 'plot.readonly': False, 'tree.udf:Test choice': ''} - created_plot, __ = update_map_feature(update, self.user, plot) + with self.assertRaises(ValidationError): + created_plot, __ = update_map_feature(update, self.user, plot) - created_plot_update = Plot.objects.get(pk=created_plot.pk) - self.assertIsNone(created_plot_update.current_tree()) + created_plot_update = Plot.objects.get(pk=created_plot.pk) + self.assertIsNone(created_plot_update.current_tree()) - created_plot_update.delete_with_user(self.user) + created_plot_update.delete_with_user(self.user) def test_does_create_tree_when_one_tree_field_is_non_empty(self): plot = Plot(instance=self.instance) @@ -979,7 +978,7 @@ def test_plot_with_tree(self): .has_itree_region() context = plot_detail(request, self.instance, plot_w_tree.pk) - self.assertEquals(plot_w_tree, context['plot']) + self.assertEqual(plot_w_tree, context['plot']) self.assertIn('benefits', context) def test_plot_without_tree(self): @@ -988,7 +987,7 @@ def test_plot_without_tree(self): context = self.get_plot_context(plot_wo_tree) - self.assertEquals(plot_wo_tree, context['plot']) + self.assertEqual(plot_wo_tree, context['plot']) self.assertNotIn('benefits', context) def test_system_user_hidden_from_audit_history(self): @@ -1056,8 +1055,8 @@ def test_progress_starts_at_25(self): # Having a plot location counts at 25% context = self.get_plot_context(self.plot_wo_tree) - self.assertEquals(25, context['progress_percent']) - self.assertEquals(4, len(context['progress_messages'])) + self.assertEqual(25, context['progress_percent']) + self.assertEqual(4, len(context['progress_messages'])) def test_progress_messages_decrease_when_plot_has_tree(self): wo_tree_context = self.get_plot_context(self.plot_wo_tree) @@ -1327,7 +1326,7 @@ def _assert_dicts_equal(self, expected, actual): self.assertEqual(len(expected), len(actual), "Number of dicts") for expected, generated in zip(expected, actual): - for k, v in expected.iteritems(): + for k, v in expected.items(): self.assertEqual(v, generated[k], "key [%s]" % k) def check_audits(self, url, dicts, user=None): @@ -1591,21 +1590,21 @@ def test_udf_collection_audits_appear(self): "ref": None, "action": Audit.Type.Insert, "previous_value": None, - "current_value": "water", + "current_value": "343", "requires_auth": False, "user_id": self.commander.pk, "instance_id": self.instance.pk, - "field": "udf:action" + "field": "udf:height" }, { "model": "udf:%s" % cudf.pk, "ref": None, "action": Audit.Type.Insert, "previous_value": None, - "current_value": "343", + "current_value": "water", "requires_auth": False, "user_id": self.commander.pk, "instance_id": self.instance.pk, - "field": "udf:height" + "field": "udf:action" }], user=self.officer) @@ -1706,6 +1705,7 @@ def setUp(self): js_species = self.species_json[i] js_species['id'] = species.id js_species['common_name'] = species.common_name + js_species['is_common'] = None js_species['scientific_name'] = species.scientific_name js_species['value'] = species.display_name js_species['genus'] = species.genus @@ -1714,12 +1714,24 @@ def setUp(self): js_species['other_part_of_name'] = species.other_part_of_name def test_get_species_list(self): - self.assertEquals(species_list(make_request(), self.instance), - self.species_json) + species = species_list(make_request(), self.instance) + + # cannot compare tokens yet + tokens_request = [s.pop('tokens') for s in species] + tokens_json = [s.pop('tokens') for s in self.species_json] + + self.assertEqual(species, + self.species_json) def test_get_species_list_max_items(self): - self.assertEquals( - species_list(make_request({'max_items': 3}), self.instance), + species = species_list(make_request({'max_items': 3}), self.instance) + + # cannot compare tokens yet + tokens_request = [s.pop('tokens') for s in species] + tokens_json = [s.pop('tokens') for s in self.species_json[:3]] + + self.assertEqual( + species, self.species_json[:3]) @@ -1735,11 +1747,11 @@ def setUp(self): def test_get_by_username(self): context = user(make_request(), self.joe.username) - self.assertEquals(self.joe.username, context['user'].username, - 'the user view should return a dict with user with ' - '"username" set to %s ' % self.joe.username) - self.assertEquals(list, type(context['audits']), - 'the user view should return a list of audits') + self.assertEqual(self.joe.username, context['user'].username, + 'the user view should return a dict with user with ' + '"username" set to %s ' % self.joe.username) + self.assertEqual(list, type(context['audits']), + 'the user view should return a list of audits') def test_get_with_invalid_username_returns_404(self): self.assertRaises(Http404, user, make_request(), @@ -1781,7 +1793,7 @@ def setUp(self): def assertOk(self, response): if (issubclass(response.__class__, HttpResponse)): context = json.loads(response.content) - self.assertEquals(200, response.status_code) + self.assertEqual(200, response.status_code) else: context = response self.assertTrue('ok' in context) @@ -1790,7 +1802,7 @@ def assertOk(self, response): def assertBadRequest(self, response): self.assertTrue(issubclass(response.__class__, HttpResponse)) - self.assertEquals(400, response.status_code) + self.assertEqual(400, response.status_code) context = json.loads(response.content) self.assertFalse('ok' in context) self.assertTrue('globalErrors' in context) @@ -1805,9 +1817,9 @@ def test_change_first_name(self): update = b'{"user.first_name": "Joseph"}' self.assertOk(update_user( make_request(user=self.joe, body=update), self.joe)) - self.assertEquals('Joseph', - User.objects.get(username='joe').first_name, - 'The first_name was not updated') + self.assertEqual('Joseph', + User.objects.get(username='joe').first_name, + 'The first_name was not updated') def test_expects_keys_prefixed_with_user(self): self.joe.name = 'Joe' @@ -1851,11 +1863,11 @@ def test_get_by_username_redirects(self): self.commander.username) expected_url = '/users/%s/?instance_id=%d' %\ (self.commander.username, self.instance.id) - self.assertEquals(res.status_code, 302, "should be a 302 Found \ + self.assertEqual(res.status_code, 302, "should be a 302 Found \ temporary redirect") - self.assertEquals(expected_url, res['Location'], - 'the view should redirect to %s not %s ' % - (expected_url, res['Location'])) + self.assertEqual(expected_url, res['Location'], + 'the view should redirect to %s not %s ' % + (expected_url, res['Location'])) def test_get_with_invalid_username_redirects(self): test_username = 'no_way_username' @@ -1864,11 +1876,11 @@ def test_get_with_invalid_username_redirects(self): test_username) expected_url = '/users/%s/?instance_id=%d' %\ (test_username, self.instance.id) - self.assertEquals(res.status_code, 302, "should be a 302 Found \ + self.assertEqual(res.status_code, 302, "should be a 302 Found \ temporary redirect") - self.assertEquals(expected_url, res['Location'], - 'the view should redirect to %s not %s ' % - (expected_url, res['Location'])) + self.assertEqual(expected_url, res['Location'], + 'the view should redirect to %s not %s ' % + (expected_url, res['Location'])) class SettingsJsViewTests(ViewTestCase): @@ -1893,12 +1905,12 @@ def setUp(self): @override_settings(TILE_HOST=None) def test_none_tile_hosts_omits_tilehosts_setting(self): self.assertNotInResponse('otm.settings.tileHosts', - self.get_response()) + self.get_response().content.decode('utf-8')) @override_settings(TILE_HOST='{s}.a') def test_single_tile_host_in_tilehosts_setting(self): self.assertInResponse('otm.settings.tileHost = "{s}.a";', - self.get_response()) + self.get_response().content.decode('utf-8')) class InstanceSettingsJsViewTests(SettingsJsViewTests): @@ -1998,18 +2010,18 @@ def test_sends_email_for_existing_user(self): resp = forgot_username(make_request({'email': self.user.email}, method='POST')) - self.assertEquals(resp, {'email': self.user.email}) + self.assertEqual(resp, {'email': self.user.email}) - self.assertEquals(len(mail.outbox), 1) + self.assertEqual(len(mail.outbox), 1) self.assertIn(self.user.username, mail.outbox[0].body) def test_no_email_if_doesnt_exist(self): resp = forgot_username(make_request({'email': 'doesnt@exist.co.uk'}, method='POST')) - self.assertEquals(resp, {'email': 'doesnt@exist.co.uk'}) + self.assertEqual(resp, {'email': 'doesnt@exist.co.uk'}) - self.assertEquals(len(mail.outbox), 0) + self.assertEqual(len(mail.outbox), 0) class UserInstancesViewTests(OTMTestCase): @@ -2037,17 +2049,17 @@ def setUp(self): def test_a_views_a(self): # User a views their own instances instances = get_user_instances(self.user_a, self.user_a, self.c) - self.assertEquals(list(instances), [self.a, self.ab, self.c]) + self.assertEqual(list(instances), [self.a, self.ab, self.c]) def test_a_views_b(self): # User a views b's instances instances = get_user_instances(self.user_a, self.user_b, self.c) - self.assertEquals(list(instances), [self.ab, self.b_public]) + self.assertEqual(list(instances), [self.ab, self.b_public]) def test_anonymous_views_b(self): # User anonymous views b's instances instances = get_user_instances(None, self.user_b, self.c) - self.assertEquals(list(instances), [self.b_public]) + self.assertEqual(list(instances), [self.b_public]) @override_settings(VIEWABLE_INSTANCES_FUNCTION=None) diff --git a/opentreemap/treemap/tests/ui/__init__.py b/opentreemap/treemap/tests/ui/__init__.py index 5a425853a..aae98e267 100644 --- a/opentreemap/treemap/tests/ui/__init__.py +++ b/opentreemap/treemap/tests/ui/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import importlib from time import sleep @@ -179,7 +177,7 @@ def wait_for_input_value(self, element_or_selector, value, timeout=10): return element def _get_element(self, element_or_selector): - if isinstance(element_or_selector, basestring): + if isinstance(element_or_selector, str): return self.find(element_or_selector) else: return element_or_selector @@ -188,7 +186,7 @@ def _get_element(self, element_or_selector): @override_settings(**test_settings) class TreemapUITestCase(UITestCase): def assertElementVisibility(self, element, visible): - if isinstance(element, basestring): + if isinstance(element, str): element = self.find_id(element) wait = (self.wait_until_visible if visible else self.wait_until_invisible) diff --git a/opentreemap/treemap/tests/ui/plot_detail/cases.py b/opentreemap/treemap/tests/ui/plot_detail/cases.py index 8b244b025..bb3cd99ed 100644 --- a/opentreemap/treemap/tests/ui/plot_detail/cases.py +++ b/opentreemap/treemap/tests/ui/plot_detail/cases.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from selenium.common.exceptions import ElementNotVisibleException diff --git a/opentreemap/treemap/tests/ui/plot_detail/uitest_add.py b/opentreemap/treemap/tests/ui/plot_detail/uitest_add.py index 66d63141e..8bfd131c3 100644 --- a/opentreemap/treemap/tests/ui/plot_detail/uitest_add.py +++ b/opentreemap/treemap/tests/ui/plot_detail/uitest_add.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from treemap.models import Tree -from cases import PlotDetailUITestCase +from .cases import PlotDetailUITestCase class PlotAddTest(PlotDetailUITestCase): diff --git a/opentreemap/treemap/tests/ui/plot_detail/uitest_delete.py b/opentreemap/treemap/tests/ui/plot_detail/uitest_delete.py index f561b85ec..585bc7905 100644 --- a/opentreemap/treemap/tests/ui/plot_detail/uitest_delete.py +++ b/opentreemap/treemap/tests/ui/plot_detail/uitest_delete.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from unittest.case import skip from treemap.models import Tree -from cases import PlotDetailDeleteUITestCase +from .cases import PlotDetailDeleteUITestCase class PlotEditDeleteTest(PlotDetailDeleteUITestCase): diff --git a/opentreemap/treemap/tests/ui/plot_detail/uitest_edit.py b/opentreemap/treemap/tests/ui/plot_detail/uitest_edit.py index 0d353d4e0..56df901df 100644 --- a/opentreemap/treemap/tests/ui/plot_detail/uitest_edit.py +++ b/opentreemap/treemap/tests/ui/plot_detail/uitest_edit.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from treemap.models import Plot, Tree -from cases import PlotDetailUITestCase +from .cases import PlotDetailUITestCase class PlotEditTest(PlotDetailUITestCase): diff --git a/opentreemap/treemap/tests/ui/ui_test_urls.py b/opentreemap/treemap/tests/ui/ui_test_urls.py index 493f0a711..c26a4448b 100644 --- a/opentreemap/treemap/tests/ui/ui_test_urls.py +++ b/opentreemap/treemap/tests/ui/ui_test_urls.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import include, url from django.http import HttpResponse diff --git a/opentreemap/treemap/tests/ui/uitest_map.py b/opentreemap/treemap/tests/ui/uitest_map.py index 829a72919..3758897ec 100644 --- a/opentreemap/treemap/tests/ui/uitest_map.py +++ b/opentreemap/treemap/tests/ui/uitest_map.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from time import sleep from unittest.case import skip diff --git a/opentreemap/treemap/tests/ui/uitest_registration_views.py b/opentreemap/treemap/tests/ui/uitest_registration_views.py index 081535dba..f4c255f21 100644 --- a/opentreemap/treemap/tests/ui/uitest_registration_views.py +++ b/opentreemap/treemap/tests/ui/uitest_registration_views.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from time import sleep -from django.core.urlresolvers import reverse +from django.urls import reverse from django.core import mail from registration.models import RegistrationProfile diff --git a/opentreemap/treemap/tests/unit_test_urls.py b/opentreemap/treemap/tests/unit_test_urls.py index 9043cb541..85a17fbe0 100644 --- a/opentreemap/treemap/tests/unit_test_urls.py +++ b/opentreemap/treemap/tests/unit_test_urls.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import include, url from django.template import Template, RequestContext diff --git a/opentreemap/treemap/udf.py b/opentreemap/treemap/udf.py index 862c66304..155637219 100644 --- a/opentreemap/treemap/udf.py +++ b/opentreemap/treemap/udf.py @@ -48,9 +48,6 @@ http://stackoverflow.com/a/43745677/14405 ''' -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division import json import copy @@ -61,7 +58,6 @@ from decimal import Decimal from django.core.exceptions import ValidationError -from django.utils import six from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.contrib.gis.db import models @@ -164,11 +160,11 @@ class UserDefinedCollectionValue(UserTrackable, models.Model): particular collection field. We audit all of the fields on this object and expand the audits in the same way that scalar udfs work. """ - field_definition = models.ForeignKey('UserDefinedFieldDefinition') + field_definition = models.ForeignKey('UserDefinedFieldDefinition', on_delete=models.CASCADE) model_id = models.IntegerField() data = HStoreField() - def __unicode__(self): + def __str__(self): return repr(self.data) def __init__(self, *args, **kwargs): @@ -276,7 +272,7 @@ def as_dict(self, *args, **kwargs): base_model_dict = super( UserDefinedCollectionValue, self).as_dict(*args, **kwargs) - for field, value in self.data.iteritems(): + for field, value in self.data.items(): base_model_dict['udf:' + field] = value return base_model_dict @@ -323,7 +319,7 @@ def save_with_user(self, user, *args, **kwargs): if field_perm.permission_level == FieldPermission.WRITE_WITH_AUDIT: model_id = _reserve_model_id(UserDefinedCollectionValue) pending = True - for field, (oldval, __) in updated_fields.iteritems(): + for field, (oldval, __) in updated_fields.items(): self.apply_change(field, oldval) else: pending = False @@ -334,7 +330,7 @@ def save_with_user(self, user, *args, **kwargs): if audit_type == Audit.Type.Insert: updated_fields['id'] = [None, model_id] - for field, (old_val, new_val) in updated_fields.iteritems(): + for field, (old_val, new_val) in updated_fields.items(): Audit.objects.create( current_value=new_val, previous_value=old_val, @@ -358,7 +354,7 @@ class UserDefinedFieldDefinition(models.Model): """ The instance that this field is bound to """ - instance = models.ForeignKey(Instance) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) """ The type of model that this should bind to @@ -402,10 +398,15 @@ class UserDefinedFieldDefinition(models.Model): """ name = models.CharField(max_length=255) + """ + Whether or not this is a required field + """ + isrequired = models.BooleanField(default=False) + class Meta: unique_together = ('instance', 'model_type', 'name') - def __unicode__(self): + def __str__(self): return ('%s.%s%s' % (self.model_type, self.name, ' (collection)' if self.iscollection else '')) @@ -423,7 +424,7 @@ def _validate_and_update_choice( self, datatype, old_choice_value, new_choice_value): # Prevent validation errors when the choice value is numeric. - old_choice_value = unicode(old_choice_value) + old_choice_value = str(old_choice_value) if datatype['type'] not in ('choice', 'multichoice'): raise ValidationError( @@ -532,10 +533,8 @@ def _update_choices_on_audits( def _list_replace_or_remove(self, l, old, new): if l is None: return None - new_l = filter( - None, - [(new if choice == old else choice) - for choice in l]) + new_l = [_f for _f + in [(new if choice == old else choice) for choice in l] if _f] return new_l or None def add_choice(self, new_choice_value, name=None): @@ -547,7 +546,7 @@ def add_choice(self, new_choice_value, name=None): datatypes = {d['name']: d for d in self.datatype_dict} datatypes[name]['choices'].append(new_choice_value) - self.datatype = json.dumps(datatypes.values()) + self.datatype = json.dumps(list(datatypes.values())) self.save() else: if name is not None: @@ -569,7 +568,7 @@ def replace_collection_field_choices(self, field_name, new_choices): if choice not in new_choices: self.delete_choice(choice, field_name) field['choices'] = new_choices - self.datatype = json.dumps(datatypes.values()) + self.datatype = json.dumps(list(datatypes.values())) self.save() @transaction.atomic @@ -590,7 +589,7 @@ def update_choice( datatypes[name], old_choice_value, new_choice_value) datatypes[name] = datatype - self.datatype = json.dumps(datatypes.values()) + self.datatype = json.dumps(list(datatypes.values())) self.save() vals = UserDefinedCollectionValue\ @@ -735,7 +734,7 @@ def _validate_single_datatype(self, datatype): raise ValidationError(_('Missing choices key')) for choice in choices: - if not isinstance(choice, basestring): + if not isinstance(choice, str): raise ValidationError(_('Choice must be a string')) if choice is None or choice.strip() == '': raise ValidationError(_('Empty choice is not allowed')) @@ -871,16 +870,16 @@ def reverse_clean(self, value, key=None, datatype_dict=None): datatype = dtd['type'] if datatype == 'date': - if isinstance(value, six.string_types): + if isinstance(value, str): return value elif isinstance(value, datetime): return value.strftime(DATETIME_FORMAT) elif isinstance(value, bool): return force_text(value).lower() - elif isinstance(value, six.integer_types + (float, Decimal)): + elif isinstance(value, (int, float, Decimal)): return force_text(value) # Order matters. Strings are Iterable. - elif isinstance(value, six.string_types): + elif isinstance(value, str): return value elif isinstance(value, Iterable): return force_text(json.dumps(value, cls=DecimalEncoder)) @@ -976,11 +975,11 @@ def _validate(val): fieldname=self.name) if values is None: return None - if isinstance(values, basestring): + if isinstance(values, str): # A single string is valid JSON. Wrap as a list for # consistency values = [values] - map(_validate, values) + list(map(_validate, values)) return values else: return value @@ -992,7 +991,7 @@ def clean_collection(self, data): errors = {} for entry in data: - for subfield_name, subfield_val in entry.iteritems(): + for subfield_name, subfield_val in entry.items(): if subfield_name == 'id': continue @@ -1048,7 +1047,7 @@ def __init__(self, **kwargs): if not isinstance(self.default, UDFDictionary): self.default = UDFDictionary - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection, context=None): return UDFDictionary(value) def to_python(self, value): @@ -1059,7 +1058,7 @@ def to_python(self, value): if isinstance(value, UDFDictionary): return value - if isinstance(value, six.string_types): + if isinstance(value, str): value = super(UDFPostgresField, self).to_python(value) if value is None: @@ -1087,7 +1086,7 @@ def get_prep_value(self, value): return {key: udfds[key].reverse_clean(val) for (key, val) - in super(UDFDictionary, udf_dict).iteritems()} + in super(UDFDictionary, udf_dict).items()} return value @@ -1213,7 +1212,7 @@ def _prefixed_name(self, key): def __contains__(self, key): if super(UDFDictionary, self).__contains__(key): return True - return key in self.collection_fields.keys() + return key in list(self.collection_fields.keys()) def __getitem__(self, key): udfd = self._get_udf_or_error(key) @@ -1264,7 +1263,7 @@ def get(self, key, default, do_not_clean=False): def iteritems(self): model_instance = getattr(self, 'instance', None) - for k, v in super(UDFDictionary, self).iteritems(): + for k, v in super(UDFDictionary, self).items(): if v is not None: yield k, v if model_instance is not None: @@ -1281,10 +1280,10 @@ def __repr__(self): model_type = getattr(self, '_model_type', self.__class__.__name__) return '{}.udfs({})'.format( - model_type, pformat(dict(self.items()))) + model_type, pformat(dict(list(self.items())))) -class UDFModel(UserTrackable, models.Model): +class UDFModel(UserTrackable, models.Model, metaclass=UDFModelBase): """ Classes that extend this model gain support for scalar UDF fields via the `udfs` field. @@ -1294,8 +1293,6 @@ class UDFModel(UserTrackable, models.Model): Authorizable mixins """ - __metaclass__ = UDFModelBase - udfs = UDFPostgresField( db_index=True, blank=True, @@ -1427,6 +1424,10 @@ def save(self, *args, **kwargs): def udf_field_names(self): return [field.name for field in self.get_user_defined_fields()] + @property + def udf_required_fields(self): + return [field.name for field in self.get_user_defined_fields() if field.isrequired] + @property def scalar_udf_names_and_fields(self): model_name = to_object_name(self.__class__.__name__) @@ -1471,7 +1472,7 @@ def visible_collection_udfs_audit_names(self, user): def collection_udf_settings(cls): return { k: v for k, v in - getattr(cls, 'udf_settings', {}).items() + list(getattr(cls, 'udf_settings', {}).items()) if v.get('iscollection')} @property @@ -1498,7 +1499,7 @@ def _format_value(value): # For collection UDFs, we need to format each subvalue # inside each dictionary value = [{k: _format_value(val) - for k, val in sub_dict.iteritems()} + for k, val in sub_dict.items()} for sub_dict in value] else: value = _format_value(value) @@ -1518,13 +1519,13 @@ def save_with_user(self, user, *args, **kwargs): collection_fields = self.udfs._base_collection_fields(clean=False) dirty_collection_values = { field_name: values - for field_name, values in collection_fields.iteritems() + for field_name, values in collection_fields.items() if field_name in self.dirty_collection_udfs} fields = {field.name: field for field in self.get_user_defined_fields()} - for field_name, values in dirty_collection_values.iteritems(): + for field_name, values in dirty_collection_values.items(): field = fields[field_name] ids_specified = [] @@ -1534,7 +1535,7 @@ def save_with_user(self, user, *args, **kwargs): if udcv.data != value_dict: udcv.data = { key: field.reverse_clean(val, key=key) - for key, val in value_dict.items()} + for key, val in list(value_dict.items())} udcv.save_with_user(user) ids_specified.append(udcv.pk) @@ -1574,11 +1575,11 @@ def clean_udfs(self): errors = {} keys_to_delete = [ - key for key, field in scalar_fields.iteritems() + key for key, field in scalar_fields.items() if field is None] for key in keys_to_delete: del self.udfs[key] - for key, field in scalar_fields.iteritems(): + for key, field in scalar_fields.items(): val = self.udfs.get(key, None, do_not_clean=True) try: field.clean_value(val) @@ -1590,7 +1591,7 @@ def clean_udfs(self): # without the attribute `collection_data_loaded`, so use `getattr`. if getattr(self.udfs, 'collection_data_loaded', None): collection_data = self.udfs.collection_fields - for collection_field_name, data in collection_data.iteritems(): + for collection_field_name, data in collection_data.items(): collection_field = collection_fields.get( collection_field_name, None) diff --git a/opentreemap/treemap/units.py b/opentreemap/treemap/units.py index 4e4a9affd..74ae7c9b9 100644 --- a/opentreemap/treemap/units.py +++ b/opentreemap/treemap/units.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import copy @@ -35,6 +33,20 @@ def terminology(cls, instance=None): def display_name(cls, instance): return cls.terminology(instance)['singular'] + def units(self): + """ + Get the units for all fields + """ + from treemap.util import to_object_name + unit_dict = {} + model = to_object_name(self._meta.object_name) + for field in self._meta.get_fields(): + if self.instance and is_convertible(model, field.name): + unit_abbreviation = get_unit_abbreviation(get_units(self.instance, model, field.name)) + unit_dict['{}.{}'.format(model, field.name)] = unit_abbreviation + + return unit_dict + def _mutate_convertable_fields(self, f): from treemap.util import to_object_name # note that `to_object_name` is a helper function we use @@ -130,10 +142,10 @@ def convert_to_database_units(self): "sq_m": {"sq_m": 1, "sq_ft": 10.7639} } _unit_conversions["ft"] = {u: v * 12 for (u, v) - in _unit_conversions["in"].iteritems()} + in _unit_conversions["in"].items()} _unit_conversions["sq_ft"] = { u: v / _unit_conversions["sq_m"]["sq_ft"] - for u, v in _unit_conversions["sq_m"].iteritems()} + for u, v in _unit_conversions["sq_m"].items()} def get_unit_name(abbrev): @@ -146,7 +158,7 @@ def get_unit_abbreviation(abbrev): def get_convertible_units(category_name, value_name): abbrev = _get_display_default(category_name, value_name, 'units') - return _unit_conversions[abbrev].keys() + return list(_unit_conversions[abbrev].keys()) def _get_display_default(category_name, value_name, key): @@ -212,7 +224,7 @@ def _is_configured_for(keys, category_name, value_name): defaults = settings.DISPLAY_DEFAULTS return (category_name in defaults and value_name in defaults[category_name] - and keys & defaults[category_name][value_name].viewkeys()) + and keys & defaults[category_name][value_name].keys()) is_convertible_or_formattable = partial(_is_configured_for, @@ -232,7 +244,7 @@ def storage_to_instance_units_factor(instance, category_name, value_name): instance_unit = get_units(instance, category_name, value_name) conversion_dict = _unit_conversions.get(storage_unit) - if instance_unit not in conversion_dict.keys(): + if instance_unit not in list(conversion_dict.keys()): raise Exception("Cannot convert from [%s] to [%s]" % (storage_unit, instance_unit)) diff --git a/opentreemap/treemap/urls.py b/opentreemap/treemap/urls.py index a148d4a41..608f561b1 100644 --- a/opentreemap/treemap/urls.py +++ b/opentreemap/treemap/urls.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + from django.conf.urls import url @@ -23,6 +21,9 @@ url(r'^boundaries/$', routes.boundary_autocomplete, name='boundary_list'), url(r'^edits/$', routes.edits_page, name='edits'), url(r'^species/$', routes.species_list, name="species_list_view"), + url(r'^species-common/$', routes.species_list_common, name="species_list_common_view"), + url(r'^fields/$', routes.fields, name='fields'), + url(r'^map/$', routes.map_page, name='map'), url(r'^features/(?P<feature_id>\d+)/$', @@ -35,17 +36,25 @@ routes.edit_map_feature_detail, name='map_feature_detail_edit'), url(r'^features/(?P<feature_id>\d+)/popup$', routes.map_feature_popup, name='map_feature_popup'), + url(r'^features/(?P<feature_id>\d+)/popup_detail$', + routes.map_feature_popup_detail, name='map_feature_popup_detail'), url(r'^canopy-popup$', routes.canopy_popup, name='canopy_popup'), url(r'^features/(?P<feature_id>\d+)/trees/(?P<tree_id>\d+)/$', routes.delete_tree, name='delete_tree'), url(r'^features/(?P<feature_id>\d+)/sidebar$', routes.get_map_feature_sidebar, name='map_feature_sidebar'), + url(r'^features/(?P<feature_id>\d+)/photo$', routes.add_map_feature_photo, name='add_photo_to_map_feature'), + url(r'^features/(?P<feature_id>\d+)/accordion$', routes.map_feature_accordion, name='map_feature_accordion'), + url(r'^features/(?P<feature_id>\d+)/accordion_api$', + routes.map_feature_accordion_api, name='map_feature_accordion_api'), url('^features/(?P<feature_id>\d+)/photo/(?P<photo_id>\d+)/detail$', routes.map_feature_photo_detail, name='map_feature_photo_detail'), + url('^features/(?P<feature_id>\d+)/photo/(?P<photo_id>\d+)/label$', + routes.add_map_feature_photo_label, name='map_feature_photo_label'), url('^features/(?P<feature_id>\d+)/photo/(?P<photo_id>\d+)$', routes.map_feature_photo, name='map_feature_photo'), url(r'^features/(?P<feature_id>\d+)/favorite$', @@ -68,14 +77,35 @@ routes.add_tree_photo, name='add_photo_to_plot'), url(r'^plots/(?P<feature_id>\d+)/tree/(?P<tree_id>\d+)/photo$', routes.add_tree_photo, name='add_photo_to_tree'), + url(r'^plots/(?P<feature_id>\d+)/tree/(?P<tree_id>\d+)/photo/label$', + routes.add_tree_photo_with_label, name='add_photo_to_tree_with_label'), url(r'^config/settings.js$', routes.instance_settings_js, name='settings'), url(r'^benefit/search$', routes.search_tree_benefits, name='benefit_search'), - url(r'^users/%s/$' % USERNAME_PATTERN, routes.instance_user_page, - name="user_profile"), + url(r'^benefit/search/api$', routes.search_tree_benefits_api, + name='benefit_search_api'), + #url(r'^users/%s/$' % USERNAME_PATTERN, routes.instance_user_page, + # name="user_profile"), url(r'^users/%s/edits/$' % USERNAME_PATTERN, routes.instance_user_audits), url(r'^users/$', routes.users, name="users"), + + #url(r'^inaturalist/$', + # routes.inaturalist, name='inaturalist'), + + url(r'^inaturalist/create$', + routes.inaturalist_create_observations, + name='inaturalist_create_observations'), + + url(r'^inaturalist/create/(?P<tree_id>\d+)$', + routes.inaturalist_create_observation_for_tree, + name='inaturalist_create_observation_for_tree'), + + url(r'^inaturalist-add/$', + routes.inaturalist_add, name='inaturalist_add'), + + url(r'^inaturalist/sync$', + routes.inaturalist_sync, name='inaturalist_sync'), ] diff --git a/opentreemap/treemap/util.py b/opentreemap/treemap/util.py index e2ffc1d0c..7edf77d44 100644 --- a/opentreemap/treemap/util.py +++ b/opentreemap/treemap/util.py @@ -1,23 +1,28 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import datetime from collections import OrderedDict +import json -from urlparse import urlparse +from urllib.parse import urlparse from django.apps import apps from django.shortcuts import get_object_or_404, resolve_url from django.http import HttpResponse +from django.utils import dateformat from django.utils.encoding import force_str from django.contrib.auth import REDIRECT_FIELD_NAME from django.conf import settings -from django.core.exceptions import ValidationError, MultipleObjectsReturned +from django.core.exceptions import ValidationError, MultipleObjectsReturned, ObjectDoesNotExist from django.utils.translation import ugettext_lazy as _ -from opentreemap.util import dict_pop +from opentreemap.util import dict_pop, dotted_split +from treemap.json_field import (is_json_field_reference, + get_attr_from_json_field) +from treemap.units import (get_digits_if_formattable, get_units_if_convertible, + is_convertible_or_formattable, format_value, + get_unit_abbreviation) from treemap.instance import Instance @@ -74,7 +79,7 @@ def add_visited_instance(request, instance): visited_instances[instance.pk] = stamp # turn back into a list of tuples - request.session['visited_instances'] = visited_instances.items() + request.session['visited_instances'] = list(visited_instances.items()) request.session.modified = True @@ -134,7 +139,7 @@ def package_field_errors(model_name, validation_error): Return a version keyed by "objectname.fieldname" instead of "fieldname". """ dict = {'%s.%s' % (to_object_name(model_name), field): msgs - for (field, msgs) in validation_error.message_dict.iteritems()} + for (field, msgs) in validation_error.message_dict.items()} return dict @@ -193,7 +198,7 @@ def get_csv_response(filename): # add BOM to support CSVs in MS Excel # http://en.wikipedia.org/wiki/Byte_order_mark - response.write(u'\ufeff'.encode('utf8')) + response.write('\ufeff'.encode('utf8')) return response @@ -238,3 +243,217 @@ def num_format(num): # Allow for up to 10 digits of precision, but strip trailing '0' or '.' return '{0:.10f}'.format(num).rstrip('0').rstrip('.') return num + + +# Field Utilities +FIELD_MAPPINGS = { + 'IntegerField': 'int', + 'ForeignKey': 'foreign_key', + 'OneToOneField': 'int', + 'AutoField': 'int', + 'FloatField': 'float', + 'TextField': 'long_string', + 'CharField': 'string', + 'DateTimeField': 'datetime', + 'DateField': 'date', + 'BooleanField': 'bool', + 'NullBooleanField': 'bool', + 'FileField': 'string', + 'PointField': 'point', + 'MultiPolygonField': 'multipolygon', +} + +VALID_FIELD_KEYS = ','.join(FIELD_MAPPINGS.keys()) + +# Should a blank choice be added for choice and multichoice fields? +ADD_BLANK_ALWAYS = 0 +ADD_BLANK_NEVER = 1 +ADD_BLANK_IF_CHOICE_FIELD = 2 + + +def field_type_label_choices(model, field_name, label=None, + explanation=None, + add_blank=ADD_BLANK_IF_CHOICE_FIELD): + choices = None + udf_field_name = field_name.replace('udf:', '') + if not _is_udf(model, udf_field_name): + field = model._meta.get_field(field_name) + field_type = field.get_internal_type() + try: + field_type = FIELD_MAPPINGS[field_type] + except KeyError: + raise Exception('This template tag only supports %s not %s' + % (VALID_FIELD_KEYS, + field_type)) + label = label if label else field.verbose_name + explanation = explanation if explanation else field.help_text + choices = [{'value': choice[0], 'display_value': choice[1]} + for choice in field.choices or []] + if choices and field.null: + choices = [{'value': '', 'display_value': ''}] + choices + else: + udf_dict = _udf_dict(model, field_name) + field_type = udf_dict['type'] + label = label if label else udf_field_name + if 'choices' in udf_dict: + choices = [{'value': value, 'display_value': value} + for value in udf_dict['choices']] + if add_blank == ADD_BLANK_ALWAYS or ( + add_blank == ADD_BLANK_IF_CHOICE_FIELD and + field_type == 'choice' + ): + choices.insert(0, {'value': "", 'display_value': ""}) + + return field_type, label, explanation, choices + +def _get_model(context, object_name, instance=None): + return context[object_name] + +def _is_udf(model, udf_field_name): + return (hasattr(model, 'udf_field_names') and + udf_field_name in model.udf_field_names) + +def _udf_dict(model, field_name): + matches = [field.datatype_dict + for field in model.get_user_defined_fields() + if field.name == field_name.replace('udf:', '')] + if matches: + return matches[0] + else: + raise Exception("Datatype for field %s not found" % field_name) + + +def get_field(context, label, identifier, user, instance, + explanation=None, treat_multichoice_as_choice=False, model=None): + is_required = False + + """ + if not isinstance(identifier, basestring)\ + or not _identifier_regex.match(identifier): + raise template.TemplateSyntaxError( + 'expected a string with the format "object_name.property" ' + 'to follow "from" %s' % identifier) + """ + + model_name_or_object_name, field_name = dotted_split(identifier, 2, maxsplit=1) + + if not model: + model = _get_model(context, model_name_or_object_name, instance) + + object_name = to_object_name(model_name_or_object_name) + + identifier = "%s.%s" % (object_name, field_name) + + def _field_is_required(model, field_name): + udf_field_name = field_name.replace('udf:', '') + if _is_udf(model, udf_field_name): + return udf_field_name in model.udf_required_fields + return False + + def _field_value(model, field_name, data_type): + udf_field_name = field_name.replace('udf:', '') + val = None + if field_name in [f.name for f in model._meta.get_fields()]: + try: + val = getattr(model, field_name) + except (ObjectDoesNotExist, AttributeError): + pass + elif _is_udf(model, udf_field_name): + if udf_field_name in model.udfs: + val = model.udfs[udf_field_name] + # multichoices place a json serialized data-value + # on the dom element and client-side javascript + # processes it into a view table and edit widget + if data_type == 'multichoice': + val = json.dumps(val) + elif data_type == 'multichoice': + val = '[]' + + else: + raise ValueError('Could not find field: %s' % field_name) + + return val + + if is_json_field_reference(field_name): + field_value = get_attr_from_json_field(model, field_name) + choices = None + is_visible = is_editable = True + data_type = "string" + else: + add_blank = (ADD_BLANK_ALWAYS if treat_multichoice_as_choice + else ADD_BLANK_IF_CHOICE_FIELD) + data_type, label, explanation, choices = field_type_label_choices( + model, field_name, label, explanation=explanation, + add_blank=add_blank) + is_required = _field_is_required(model, field_name) + field_value = _field_value(model, field_name, data_type) + + if user is not None and hasattr(model, 'field_is_visible'): + is_visible = model.field_is_visible(user, field_name) + is_editable = model.field_is_editable(user, field_name) + else: + # This tag can be used without specifying a user. In that case + # we assume that the content is visible and upstream code is + # responsible for only showing the content to the appropriate + # user + is_visible = True + is_editable = True + + digits = units = '' + + if hasattr(model, 'instance'): + digits = get_digits_if_formattable( + model.instance, object_name, field_name) + + units = get_units_if_convertible( + model.instance, object_name, field_name) + if units != '': + units = get_unit_abbreviation(units) + + if data_type == 'foreign_key': + # rendered clientside + display_val = '' + elif field_value is None: + display_val = None + elif data_type in ['date', 'datetime']: + fmt = (model.instance.short_date_format if model.instance + else settings.SHORT_DATE_FORMAT) + display_val = dateformat.format(field_value, fmt) + elif is_convertible_or_formattable(object_name, field_name): + display_val = format_value( + model.instance, object_name, field_name, field_value) + if units != '': + display_val += (' %s' % units) + elif data_type == 'bool': + display_val = _('Yes') if field_value else _('No') + elif data_type == 'multichoice': + # this is rendered clientside from data attributes so + # there's no meaningful intermediate value to send + # without rendering the same markup server-side. + display_val = None + elif choices: + display_vals = [choice['display_value'] for choice in choices + if choice['value'] == field_value] + display_val = display_vals[0] if display_vals else field_value + elif data_type == 'float': + display_val = num_format(field_value) + else: + display_val = str(field_value) + + if hasattr(model, 'REQUIRED_FIELDS'): + is_required = is_required or field_name in model.REQUIRED_FIELDS + + return { + 'label': label, + 'explanation': explanation, + 'identifier': identifier, + 'value': field_value, + 'display_value': display_val, + 'units': units, + 'digits': digits, + 'data_type': data_type, + 'is_visible': is_visible, + 'is_editable': is_editable, + 'is_required': is_required, + 'choices': choices, + } diff --git a/opentreemap/treemap/views/map_feature.py b/opentreemap/treemap/views/map_feature.py index b3cd7f007..c03ee86ff 100644 --- a/opentreemap/treemap/views/map_feature.py +++ b/opentreemap/treemap/views/map_feature.py @@ -1,38 +1,40 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + +import datetime import json import hashlib +import re +import time from functools import wraps -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.core.exceptions import ValidationError from django.conf import settings -from django.db import transaction +from django.db import connection, transaction from django.contrib.gis.geos import Point, MultiPolygon, Polygon from django.contrib.gis.db.models import GeometryField from django.utils.translation import ugettext as _ from opentreemap.util import dotted_split +from opentreemap.integrations import inaturalist from treemap.lib.hide_at_zoom import (update_hide_at_zoom_after_move, update_hide_at_zoom_after_delete) from treemap.units import Convertible from treemap.models import (Tree, Species, MapFeature, - MapFeaturePhoto, TreePhoto, Favorite) + MapFeaturePhoto, TreePhoto, Favorite, + MapFeaturePhotoLabel, INaturalistPhoto, INaturalistObservation) from treemap.util import (package_field_errors, to_object_name) from treemap.images import get_image_from_request from treemap.lib.photo import context_dict_for_photo -from treemap.lib.object_caches import udf_defs from treemap.lib.map_feature import (get_map_feature_or_404, raise_non_instance_404, context_dict_for_plot, context_dict_for_resource) -from treemap.views.misc import add_map_info_to_context +from treemap.views.misc import add_map_info_to_context, add_plot_field_groups def _request_to_update_map_feature(request, feature): @@ -104,7 +106,7 @@ def _map_feature_detail_context(request, instance, feature_id, edit=False): if feature.is_plot: partial = 'treemap/partials/plot_detail.html' - _add_plot_field_groups(context, instance) + add_plot_field_groups(context, instance) else: app = feature.__module__.split('.')[0] partial = '%s/%s_detail.html' % (app, feature.feature_type) @@ -112,42 +114,6 @@ def _map_feature_detail_context(request, instance, feature_id, edit=False): return context, partial -def _add_plot_field_groups(context, instance): - templates = { - "tree.id": "treemap/field/tree_id_tr.html", - "tree.species": "treemap/field/species_tr.html", - "tree.diameter": "treemap/field/diameter_tr.html" - } - - labels = { - # 'plot-species' is used as the "label" in the 'field' tag, - # but ulitmately gets used as an identifier in the template - "tree.species": "plot-species", - "tree.diameter": _("Trunk Diameter") - } - labels.update({ - v: k for k, v in context['tree'].scalar_udf_names_and_fields}) - labels.update({ - v: k for k, v in context['plot'].scalar_udf_names_and_fields}) - - def info(group): - group['fields'] = [ - (field, labels.get(field), - templates.get(field, "treemap/field/tr.html")) - for field in group.get('field_keys', []) - ] - group['collection_udfs'] = [ - next(udf for udf in udf_defs(instance) - if udf.full_name == udf_name) - for udf_name in group.get('collection_udf_keys', []) - ] - - return group - - context['field_groups'] = [ - info(group) for group in instance.web_detail_fields] - - def render_map_feature_detail_partial(request, instance, feature_id, **kwargs): context, partial = _map_feature_detail_context( request, instance, feature_id) @@ -163,6 +129,16 @@ def context_map_feature_detail(request, instance, feature_id, **kwargs): return map_feature_detail(request, instance, feature_id, should_render=False, **kwargs) +def context_map_feature_detail_api(request, instance, feature_id, **kwargs): + details = context_map_feature_detail(request, instance, feature_id, **kwargs) + + # exclude anything that cannot easily be converted to JSON + keys_to_exclude = ['field_groups', 'photos'] + + details_filtered = dict([(key, value) for key, value in details.items() if key not in keys_to_exclude]) + + return details_filtered + def map_feature_photo_detail(request, instance, feature_id, photo_id): feature = get_map_feature_or_404(feature_id, instance) @@ -179,11 +155,15 @@ def plot_detail(request, instance, feature_id, edit=False, tree_id=None): def render_map_feature_add(request, instance, type): if type in instance.map_feature_types[1:]: app = MapFeature.get_subclass(type).__module__.split('.')[0] + feature = MapFeature.get_subclass(type)() + feature.instance = instance + context = context_dict_for_resource(request, feature) try: template = '%s/%s_add.html' % (app, type) except: template = 'treemap/resource_add.html' - return render(request, template, {'object_name': to_object_name(type)}) + context['object_name'] = to_object_name(type) + return render(request, template, context) else: raise_non_instance_404(type) @@ -284,6 +264,66 @@ def save_and_return_errors(thing, user): except ValidationError as e: return package_field_errors(thing._model_name, e) + def check_if_species_is_set(request_dict, is_empty_site): + # If we have a field that explicitly checks for empty site, + # called is_empty_site, and that is False, and either + # tree.species is empty or not set, then we have a problem + if is_empty_site: + return + + # if we don't remove it, we will get failures as OTM tries to find + # this field on the model + tree_species = request_dict.get('tree.species') + if not is_empty_site and not tree_species: + raise ValidationError( + {'tree.species': 'Either set a species or set to an empty planting site'} + ) + + # if both are set, that is also a problem + if is_empty_site and tree_species: + raise ValidationError( + {'tree.species': 'Cannot set both species and empty planting site'} + ) + + def check_all_photos(request_dict, is_empty_site): + # check that we have a shape, bark and leaf photo + # we need all to be valid, or it can be an empty site + has_shape_photo = request_dict.pop('has_shape_photo', False) + has_bark_photo = request_dict.pop('has_bark_photo', False) + has_leaf_photo = request_dict.pop('has_leaf_photo', False) + + has_site_photo = request_dict.pop('has_site_photo', False) + + if is_empty_site and not has_site_photo: + raise ValidationError( + {'tree.photos': 'Please submit empty site photo'} + ) + + if not is_empty_site and not (has_shape_photo and has_bark_photo and has_leaf_photo): + # FIXME eventually, do not put a validation error on the species + raise ValidationError( + {'tree.photos': 'Please submit all photos'} + ) + + def check_required_fields(request_dict, feature, tree, is_empty_site): + errors = {} + + # skip validations for empty sites + if is_empty_site: + return + + if feature.feature_type == 'Plot': + for field in feature.REQUIRED_FIELDS: + if not getattr(feature, field): + errors['plot.{}'.format(field)] = 'Missing required field'.format(field) + if tree: + for field in tree.REQUIRED_FIELDS: + if not getattr(tree, field): + errors['tree.{}'.format(field)] = 'Missing required field'.format(field) + + if errors: + raise ValidationError(errors) + def skip_setting_value_on_tree(value, tree): # If the tree is not None, we always set a value. If the tree # is None (meaning that we would be creating a new Tree @@ -294,9 +334,16 @@ def skip_setting_value_on_tree(value, tree): tree = None errors = {} + # validate species before checking any fields + # but only validate on creation, which means the feature.id is not set + if (not feature.id and feature.feature_type == 'Plot'): + is_empty_site = request_dict.pop('is_empty_site', False) + check_if_species_is_set(request_dict, is_empty_site) + check_all_photos(request_dict, is_empty_site) + rev_updates = ['universal_rev'] old_geom = feature.geom - for (identifier, value) in request_dict.iteritems(): + for (identifier, value) in request_dict.items(): split_template = 'Malformed request - invalid field %s' object_name, field = dotted_split(identifier, 2, failure_format_string=split_template) @@ -322,7 +369,7 @@ def skip_setting_value_on_tree(value, tree): if field == 'species' and value: value = get_object_or_404(Species, instance=feature.instance, pk=value) - elif field == 'plot' and value == unicode(feature.pk): + elif field == 'plot' and value == str(feature.pk): value = feature else: raise ValueError( @@ -338,6 +385,10 @@ def skip_setting_value_on_tree(value, tree): elif identifier in ['tree.species', 'tree.diameter']: rev_updates.append('eco_rev') + is_empty_site = request_dict.pop('is_empty_site', False) + if not feature.id: + check_required_fields(request_dict, feature, tree, is_empty_site) + if feature.fields_were_updated(): errors.update(save_and_return_errors(feature, user)) if tree and tree.fields_were_updated(): @@ -373,7 +424,8 @@ def map_feature_hash(request, instance, feature_id, edit=False, tree_id=None): if request.user: pk = request.user.pk or '' - return hashlib.md5(feature.hash + ':' + str(pk)).hexdigest() + string_to_hash = feature.hash + ':' + str(pk) + return hashlib.md5(string_to_hash.encode()).hexdigest() @get_photo_context_and_errors @@ -398,6 +450,19 @@ def rotate_map_feature_photo(request, instance, feature_id, photo_id): mf_photo.save_with_user(request.user) +@get_photo_context_and_errors +def add_map_feature_photo_label(request, instance, feature_id, photo_id): + feature = get_map_feature_or_404(feature_id, instance) + photo_class = TreePhoto if feature.is_plot else MapFeaturePhoto + mf_photo = get_object_or_404(photo_class, pk=photo_id, map_feature=feature) + label_dict = json.loads(request.body) + map_feature_photo_label = MapFeaturePhotoLabel() + map_feature_photo_label.map_feature_photo = mf_photo + map_feature_photo_label.name = label_dict['label'] + map_feature_photo_label.save() + return + + @get_photo_context_and_errors def delete_photo(request, instance, feature_id, photo_id): feature = get_map_feature_or_404(feature_id, instance) @@ -450,3 +515,47 @@ def unfavorite_map_feature(request, instance, feature_id): Favorite.objects.filter(user=request.user, map_feature=feature).delete() return {'success': True} + + +def get_photo_id_from_photo_detail_url(url, feature_id): + """ + """ + return int(re.match(r'.*/{}/photo/(\d+)/detail'.format(feature_id), url).groups()[0]) + + +def inaturalist_add(request, instance, *args, **kwargs): + try: + token = request.session['inaturalist_token'] + except KeyError: + return {'success': False} + + # INaturalistPhoto, INaturalistObservation + + body = json.loads(request.body) + feature_id = body['featureId'] + feature = get_map_feature_or_404(feature_id, instance) + tree = feature.safe_get_current_tree() + photo_id = get_photo_id_from_photo_detail_url(body['photoDetailUrl'], feature_id) + photo_class = TreePhoto if feature.is_plot else MapFeaturePhoto + photo = get_object_or_404(photo_class, pk=photo_id, map_feature=feature) + + (longitude, latitude) = feature.latlon.coords + + observation = inaturalist.create_observation(token, latitude, longitude) + photo_info = inaturalist.add_photo_to_observation(token, observation['id'], photo) + + return {'success': True} + + +def inaturalist_create_observations(request, instance, *args, **kwargs): + inaturalist.create_observations.delay(instance) + return {'success': True} + + +def inaturalist_create_observation_for_tree(request, instance, tree_id=None): + inaturalist.create_observations.delay(instance, tree_id=tree_id) + return {'success': True} + + +def inaturalist_sync(request, instance): + inaturalist.sync_identifications() diff --git a/opentreemap/treemap/views/misc.py b/opentreemap/treemap/views/misc.py index 608bac6f7..1f9710430 100644 --- a/opentreemap/treemap/views/misc.py +++ b/opentreemap/treemap/views/misc.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import string import re @@ -9,24 +7,27 @@ import json from django.utils.translation import ugettext as _ -from django.core.urlresolvers import reverse +from django.urls import reverse from django.conf import settings from django.contrib.gis.geos import Polygon from django.core.exceptions import ValidationError +from django.forms.models import model_to_dict from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render, get_object_or_404 from stormwater.models import PolygonalMapFeature -from treemap.models import User, Species, StaticPage, Instance, Boundary +from treemap.models import User, Species, StaticPage, Instance, Boundary, Tree, Plot from treemap.plugin import get_viewable_instances_filter from treemap.lib.user import get_audits, get_audits_params from treemap.lib import COLOR_RE from treemap.lib.perms import model_is_creatable +from treemap.lib.object_caches import udf_defs from treemap.units import get_unit_abbreviation, get_units -from treemap.util import leaf_models_of_class +from treemap.util import leaf_models_of_class, get_field +from treemap.images import get_image_from_request _SCSS_VAR_NAME_RE = re.compile('^[_a-zA-Z][-_a-zA-Z0-9]*$') @@ -66,12 +67,14 @@ def edits(request, instance): def index(request, instance): - return HttpResponseRedirect(reverse('map', kwargs={ + return HttpResponseRedirect(reverse('react_map_index', kwargs={ 'instance_url_name': instance.url_name})) + #return HttpResponseRedirect(reverse('map', kwargs={ + # 'instance_url_name': instance.url_name})) def get_map_view_context(request, instance): - if request.user and not request.user.is_anonymous(): + if request.user and not request.user.is_anonymous: iuser = request.user.get_effective_instance_user(instance) resource_classes = [resource for resource in instance.resource_classes if model_is_creatable(iuser, resource)] @@ -79,19 +82,44 @@ def get_map_view_context(request, instance): resource_classes = [] context = { - 'fields_for_add_tree': [ - (_('Tree Height'), 'Tree.height') - ], 'resource_classes': resource_classes, 'only_one_resource_class': len(resource_classes) == 1, 'polygon_area_units': get_unit_abbreviation( get_units(instance, 'greenInfrastructure', 'area')), 'q': request.GET.get('q'), } + add_plot_field_groups( + context, + instance, + filter_fields=['tree.id', 'tree.species', 'tree.diameter'] + ) + add_map_info_to_context(context, instance) return context +def get_plot_field_groups(request, instance): + context = {} + _get_plot_field_groups( + context, + instance, + request.user, + filter_fields=['tree.id'] + #filter_fields=['tree.id', 'tree.species', 'tree.diameter'] + ) + return context + + +def map_save_image_with_label(request, instance, label): + """ + Save an image with this label in the session. + This is needed when a user is creating a new tree from the website + """ + data = get_image_from_request(request) + request.session[label] = data + return + + def add_map_info_to_context(context, instance): all_polygon_types = {c.map_feature_type for c in leaf_models_of_class(PolygonalMapFeature)} @@ -130,6 +158,7 @@ def add_anonymous_boundary(request): def boundary_autocomplete(request, instance): max_items = request.GET.get('max_items', None) + max_items = int(max_items) if max_items else None boundaries = instance.boundaries \ .filter(searchable=True) \ @@ -144,13 +173,25 @@ def boundary_autocomplete(request, instance): for boundary in boundaries] -def species_list(request, instance): +def species_list_common(request, instance): + return species_list(request, instance, is_common=True) + + +def species_list(request, instance, is_common=False): max_items = request.GET.get('max_items', None) + max_items = int(max_items) if max_items else None species_qs = instance.scope_model(Species)\ .order_by('common_name')\ .values('common_name', 'genus', 'species', 'cultivar', - 'other_part_of_name', 'id') + 'other_part_of_name', 'is_common', 'id') + + is_common = request.GET.get('is_common', None) + is_common = bool(is_common) if is_common else False + + # if this is false, we want to grab everything + if is_common: + species_qs = species_qs.filter(is_common=True) if max_items: species_qs = species_qs[:max_items] @@ -181,7 +222,7 @@ def annotate_species_dict(sdict): display_name = "%s [%s]" % (sdict['common_name'], sci_name) - tokens = tokenize(species) + tokens = tokenize(sdict) sdict.update({ 'scientific_name': sci_name, @@ -207,7 +248,7 @@ def compile_scss(request): scss = "$staticUrl: '/static/';\n" # We can probably be a bit looser with what we allow here in the future if # we need to, but we must do some checking so that libsass doesn't explode - for key, value in request.GET.items(): + for key, value in list(request.GET.items()): if _SCSS_VAR_NAME_RE.match(key) and COLOR_RE.match(value): scss += '$%s: #%s;\n' % (key, value) elif key == 'url': @@ -249,7 +290,7 @@ def instance_geojson(instance): def error_page(status_code): template = '%s.html' % status_code - def inner_fn(request): + def inner_fn(request, exception=None): reasons = { 404: _('URL or resource not found'), 500: _('An unhandled error occured'), @@ -269,3 +310,95 @@ def inner_fn(request): return response return inner_fn + + +def add_plot_field_groups(context, instance, filter_fields=None, json=False): + _filter_fields = filter_fields or [] + templates = { + "tree.id": "treemap/field/tree_id_tr.html", + "tree.species": "treemap/field/species_tr.html", + "tree.diameter": "treemap/field/diameter_tr.html" + } + + labels = { + # 'plot-species' is used as the "label" in the 'field' tag, + # but ulitmately gets used as an identifier in the template + "tree.species": "plot-species", + "tree.diameter": _("Trunk Diameter") + } + + # use the tree if it exists to get the UDF fields, otherwise use a blank tree + _tree = context.get('tree', Tree()) + labels.update({ + v: k for k, v in _tree.scalar_udf_names_and_fields}) + + # use the plot if it exists to get the UDF fields, otherwise use a blank plot + _plot = context.get('plot', Plot()) + labels.update({ + v: k for k, v in _plot.scalar_udf_names_and_fields}) + + def info(group): + group['fields'] = [ + (field, labels.get(field), + templates.get(field, "treemap/field/tr.html")) + for field in group.get('field_keys', []) if field not in _filter_fields + ] + group['collection_udfs'] = [ + next(udf for udf in udf_defs(instance) + if udf.full_name == udf_name) + for udf_name in group.get('collection_udf_keys', []) + ] + + # the UDF model by default is not serializable, so for API calls + # we force it to be serializable + if json: + group['collection_udfs'] = [ + model_to_dict(udf) for udf in group['collection_udfs']] + + return group + + context['field_groups'] = [ + info(group) for group in instance.web_detail_fields] + + +def _get_plot_field_groups(context, instance, user, filter_fields=None): + _filter_fields = filter_fields or [] + + labels = { + # 'plot-species' is used as the "label" in the 'field' tag, + # but ulitmately gets used as an identifier in the template + "tree.species": "plot-species", + "tree.diameter": _("Trunk Diameter") + } + + # use the tree if it exists to get the UDF fields, otherwise use a blank tree + _tree = context.get('tree', Tree(instance=instance)) + labels.update({ + v: k for k, v in _tree.scalar_udf_names_and_fields}) + + # use the plot if it exists to get the UDF fields, otherwise use a blank plot + _plot = context.get('plot', Plot(instance=instance)) + labels.update({ + v: k for k, v in _plot.scalar_udf_names_and_fields}) + + def info(group): + model = _tree if group['model'] == 'tree' else _plot + + # this is the case when creating these fields for some reason + user = None + group['fields'] = [ + get_field(context, labels.get(field), field, user, instance, model=model) + for field in group.get('field_keys', []) if field not in _filter_fields + ] + + # the UDF model by default is not serializable, so for API calls + # we force it to be serializable + group['collection_udfs'] = [ + next(model_to_dict(udf) for udf in udf_defs(instance) + if udf.full_name == udf_name) + for udf_name in group.get('collection_udf_keys', []) + ] + return group + + context['field_groups'] = [ + info(group) for group in instance.web_detail_fields] diff --git a/opentreemap/treemap/views/tree.py b/opentreemap/treemap/views/tree.py index a70304a85..f33609a34 100644 --- a/opentreemap/treemap/views/tree.py +++ b/opentreemap/treemap/views/tree.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import hashlib from django.core.exceptions import ValidationError -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils.translation import ungettext from django.shortcuts import get_object_or_404 from django.db import transaction from django.http import HttpResponseRedirect from treemap.search import Filter -from treemap.models import Tree, Plot +from treemap.models import Tree, Plot, MapFeaturePhotoLabel from treemap.ecobenefits import get_benefits_for_filter from treemap.ecocache import get_cached_plot_count from treemap.lib import format_benefits @@ -27,6 +25,37 @@ def tree_detail(request, instance, feature_id, tree_id): 'feature_id': feature_id})) +def create_map_feature_photo_label(photo, label): + map_feature_photo_label = MapFeaturePhotoLabel() + map_feature_photo_label.map_feature_photo = photo + map_feature_photo_label.name = label + map_feature_photo_label.save() + return map_feature_photo_label + + +def add_tree_photo_with_label(request, instance, feature_id, tree_id=None): + error = None + try: + label = request.POST['label'] + photo, tree = add_tree_photo_helper( + request, instance, feature_id, tree_id) + + create_map_feature_photo_label(photo, label) + + photos = tree.photos() + except ValidationError as e: + trees = Tree.objects.filter(pk=tree_id) + if len(trees) == 1: + photos = trees[0].photos() + else: + photos = [] + # TODO: Better display error messages in the view + error = '; '.join(e.messages) + return {'photos': [context_dict_for_photo(request, photo) + for photo in photos], + 'error': error} + + def add_tree_photo(request, instance, feature_id, tree_id=None): error = None try: @@ -134,4 +163,4 @@ def ecobenefits_hash(request, instance): string_to_hash = universal_rev + ":" + eco_str + ":" + map_features - return hashlib.md5(string_to_hash).hexdigest() + return hashlib.md5(string_to_hash.encode()).hexdigest() diff --git a/opentreemap/treemap/views/user.py b/opentreemap/treemap/views/user.py index b7b731820..19651c3d8 100644 --- a/opentreemap/treemap/views/user.py +++ b/opentreemap/treemap/views/user.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division + import collections @@ -10,9 +8,9 @@ from django.conf import settings from django.contrib.sites.requests import RequestSite from django.core.exceptions import ValidationError -from django.core.urlresolvers import reverse +from django.urls import reverse from django.db.models.expressions import RawSQL -from django.db.models.functions import Length +from django.db.models.functions import Length, Lower from django.http import HttpResponseRedirect from django.http.request import QueryDict from django.shortcuts import render, get_object_or_404 @@ -227,7 +225,7 @@ def user(request, username): public_fields = [] private_fields = [] - for field in USER_PROFILE_FIELDS.values(): + for field in list(USER_PROFILE_FIELDS.values()): field_tuple = (field['label'], field['identifier'], field.get('template', "treemap/field/div.html")) if field['visibility'] == 'public' and user.make_info_public is True: @@ -254,7 +252,7 @@ def users(request, instance): users_qs = InstanceUser.objects \ .filter(instance=instance)\ - .order_by('user__username')\ + .order_by(Lower('user__username'))\ .values('user_id', 'user__username', 'user__first_name', 'user__last_name', 'user__make_info_public') diff --git a/package.json b/package.json index 46a54bdcb..6233cb441 100644 --- a/package.json +++ b/package.json @@ -4,48 +4,88 @@ "directories": { "doc": "doc" }, - "//": ["!!! WARNING !!!", - "If you upgrade Leaflet you must update the patch in MapManager.js", - "!!! WARNING !!!"], + "//": [ + "!!! WARNING !!!", + "If you upgrade Leaflet you must update the patch in MapManager.js", + "!!! WARNING !!!" + ], "dependencies": { "autotrack": "^0.6.5", + "axios": "^0.21.1", "baconjs": "~0.6", + "bootstrap": "^4.5.3", + "chart.js": "^3.4.1", "console-browserify": "~1.0.1", + "crypto-js": "^4.0.0", "dragula": "^2.0.2", "es6-promise": "~4.1.0", - "esri-leaflet": "~1.0.2", - "leaflet": "~1.0.3", + "esri-leaflet": "^3.0.1", + "fibers": "^5.0.0", + "jquery": "^3.5.1", + "leaflet": "^1.7.1", "leaflet-draw": "~0.4.9", "leaflet-pip": "~1.0.0", + "leaflet-utfgrid": "^0.3.0", + "leaflet-vector-tile-layer": "^0.4.0", "leaflet.gridlayer.googlemutant": "~0.4.3", "leaflet.locatecontrol": "^0.62.0", - "lodash": "^4.17.4", + "lodash": "^4.17.20", "moment": "2.19.3", "mustache": "~2.2.1", "numeral": "1.5.3", + "popper.js": "^1.16.1", "ramda": "^0.23.0", - "sanitize-filename": "~1.4.2" + "react": "^17.0.1", + "react-bootstrap": "^1.4.0", + "react-bootstrap-typeahead": "^5.1.4", + "react-chartjs-2": "^3.0.3", + "react-datepicker": "^3.5.0", + "react-dom": "^17.0.1", + "react-esri-leaflet": "^1.0.3", + "react-leaflet": "^3.1.0", + "react-leaflet-google-layer": "^2.0.3", + "react-leaflet-vectorgrid": "^2.2.1", + "react-router-dom": "^5.2.0", + "react-table": "^7.6.2", + "sanitize-filename": "~1.4.2", + "shpjs": "3.4.2", + "toastr": "^2.1.4", + "yarn": "^1.22.10" }, "devDependencies": { + "@babel/core": "^7.12.10", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-transform-react-jsx": "^7.12.12", + "@babel/preset-env": "^7.12.11", + "@babel/preset-react": "^7.12.10", "autoprefixer": "^6.5.3", + "babel-loader": "^8.2.2", "chai": "~1.8.1", - "css-loader": "^0.23.1", + "clean-webpack-plugin": "^3.0.0", + "css-loader": "^5.0.1", "exports-loader": "^0.6.3", - "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.9.0", "glob": "^7.0.5", - "imports-loader": "^0.6.5", + "import-local": "^3.0.2", + "imports-loader": "^1.2.0", "jshint": "^2.9.2", + "mini-css-extract-plugin": "^1.3.3", "mocha": "~1.14.0", - "node-sass": "^3.13.1", - "postcss-loader": "^1.1.1", - "sass-loader": "^4.1.1", - "style-loader": "^0.13.1", + "node-sass": "^4.11.0", + "postcss": "^8.2.2", + "postcss-loader": "^4.1.0", + "react-dev-tools": "0.0.1", + "react-leaflet-bing-v2": "^5.0.1", + "sass": "^1.32.0", + "sass-loader": "^10.1.0", + "sass-resources-loader": "^2.1.1", + "style-loader": "^0.13.2", "testem": "^0.9.2", - "url-loader": "^0.5.7", - "webpack": "^1.13.1", - "webpack-bundle-tracker": "0.0.93", - "webpack-dev-server": "^1.14.1" + "url-loader": "^4.1.1", + "webpack": "^4.44.2", + "webpack-bundle-tracker": "^0.4.3", + "webpack-cli": "^4.3.0", + "webpack-dev-server": "^3.11.1" }, "repository": { "type": "git", @@ -55,6 +95,7 @@ "test": "testem ci", "check": "jshint opentreemap/*/js/src/*", "build": "webpack --config webpack.prod.config.js", + "build-profile": "webpack --config webpack.prod.config.js --progress=profile", "build-dev": "webpack --config webpack.dev.config.js", "build-test": "webpack -d --config webpack.test.config.js", "watch": "webpack-dev-server --config webpack.dev.config.js --host 0.0.0.0 --port 6062 --content-base static/" diff --git a/requirements.txt b/requirements.txt index 8affc6203..79018d121 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,49 +1,72 @@ -# For outdated module report see https://requires.io/github/OpenTreeMap/otm-core/requirements/?branch=develop -# Changelog URL shown below if not linked on above page - -amqp==2.2.1 +amqp==5.0.5 anyjson==0.3.3 -billiard==3.5.0.3 -boto==2.48.0 # http://docs.pythonboto.org/en/latest/releasenotes/v2.39.0.html -celery==4.1.0 -certifi==2017.7.27.1 +appdirs==1.4.3 +asgiref==3.3.1 +attrs==20.3.0 +billiard==3.6.3.0 +boto==2.49.0 +CacheControl==0.12.6 +celery==5.0.5 +certifi==2019.11.28 chardet==3.0.4 -# https://docs.djangoproject.com/en/1.10/releases/#id2 -Django==1.11.16 # rq.filter: >=1.11,<1.12 -django-apptemplates==1.3 -django-contrib-comments==1.8.0 -django-js-reverse==0.7.3 -django-queryset-csv==1.0.2 # https://github.com/azavea/django-queryset-csv/commits/master -django-recaptcha==1.4.0 -django-redis==4.8.0 -django-registration-redux==1.7 -django-storages==1.6.5 -django-threadedcomments==1.1 -django-tinsel==1.0.1 -django-webpack-loader==0.5.0 # https://github.com/owais/django-webpack-loader/releases -flake8==2.0 # rq.filter: ==2.0 -functools32==3.2.3.post2 -gunicorn==19.7.1 # http://docs.gunicorn.org/en/stable/news.html -hiredis==0.2.0 -idna==2.5 -jsonschema==2.6.0 -kombu==4.1.0 -libsass==0.11.2 +click==7.1.2 +click-didyoumean==0.0.3 +click-plugins==1.1.1 +click-repl==0.1.6 +colorama==0.4.3 +contextlib2==0.6.0 +distlib==0.3.0 +distro==1.4.0 +Django==3.1.7 +django-apptemplates==1.5 +django-contrib-comments==2.0.0 +django-cors-headers==3.7.0 +django-js-reverse==0.9.1 +django-queryset-csv==1.1.0 +django-recaptcha==2.0.6 +django-redis==4.12.1 +django-registration-redux==2.9 +django-storages==1.11.1 +django-threadedcomments==1.2 +django-tinsel==1.0.2 +django-webpack-loader==0.7.0 +flake8==3.8.4 +gunicorn==20.0.4 +hiredis==1.1.0 +html5lib==1.0.1 +idna==2.8 +ipaddr==2.2.0 +jsonschema==3.2.0 +kombu==5.0.2 +libsass==0.20.1 +lockfile==0.12.2 mccabe==0.6.1 -# Modgrammar-py2 has a 0.9.2 release on PyPi, but no artifacts for the release -modgrammar-py2==0.9.1 # rq.filter: !=0.9.2 -olefile==0.44 -pep8==1.4.6 # rq.filter: ==1.4.6 -Pillow==4.2.1 -psycopg2==2.7.3.2 # http://initd.org/psycopg/docs/news.html -pyflakes==1.6.0 -python-dateutil==2.6.1 # https://github.com/dateutil/dateutil/blob/master/NEWS -python-omgeo==5.1.0 -pytz==2017.2 # https://launchpad.net/pytz/+announcements -redis==2.10.5 -requests==2.20.0 -rollbar==0.13.12 # https://github.com/rollbar/pyrollbar/blob/master/CHANGELOG.md -six==1.10.0 +modgrammar==0.10 +msgpack==0.6.2 +olefile==0.46 +packaging==20.3 +pep517==0.8.2 +pep8==1.7.1 +Pillow==8.1.0 +progress==1.5 +prompt-toolkit==3.0.16 +psycopg2==2.8.6 +pycodestyle==2.6.0 +pyflakes==2.2.0 +pyparsing==2.4.6 +pyrsistent==0.17.3 +python-dateutil==2.8.1 +python-omgeo==6.0.4 +pytoml==0.1.21 +pytz==2021.1 +redis==3.5.3 +requests==2.22.0 +retrying==1.3.3 +rollbar==0.15.2 +six==1.14.0 +sqlparse==0.4.1 unicodecsv==0.14.1 -urllib3==1.23 -wsgiref==0.1.2 +urllib3==1.25.8 +vine==5.0.0 +wcwidth==0.2.5 +webencodings==0.5.1 diff --git a/scripts/hmaccurl.py b/scripts/hmaccurl.py new file mode 100644 index 000000000..0ae651b52 --- /dev/null +++ b/scripts/hmaccurl.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +import argparse +import base64 +import datetime +import hashlib +import hmac +import os +import subprocess + +try: + from urllib.parse import urlparse, quote, parse_qs +except ImportError: + from urllib import quote + from urlparse import urlparse, parse_qs + +from pytz import timezone + +SIG_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S" + + +def main(): + parser = argparse.ArgumentParser( + description=('Make an HMAC signed request via cURL with support for ' + 'small subset of cURL options. Set ACCESS_KEY and ' + 'SECRET_KEY variables in the environment before ' + 'executing.')) + parser.add_argument('url', help='The URL to be requested') + parser.add_argument('-X', + '--request', + default='GET', + help='The HTTP method. Default is GET') + parser.add_argument('-I', + '--head', + action='store_true', + help='Show document info only') + parser.add_argument('-s', + '--silent', + action='store_true', + help=('Silent or quiet mode. Don''t show progress ' + 'meter or error messages.')) + parser.add_argument('-d', '--data', help='The request body') + parser.add_argument('--debug', + action='store_true', + help='Print debugging information') + parser.add_argument('--access_key', + help='Access key') + parser.add_argument('--secret_key', + help='Secret key') + args = parser.parse_args() + + access_key = args.access_key or os.environ.get('ACCESS_KEY') + secret_key = args.secret_key or os.environ.get('SECRET_KEY') + if not access_key or not secret_key: + raise Exception( + 'You must set ACCESS_KEY and SECRET_KEY environment variables') + + timestamp = datetime.datetime.utcnow().strftime(SIG_TIMESTAMP_FORMAT) + #timestamp = datetime.datetime.strptime('2020-12-28 03-02-52', '%Y-%m-%d %H-%M-%S').strftime(SIG_TIMESTAMP_FORMAT) + + verb = args.request + url = urlparse(args.url) + host = url.netloc + path = url.path + params = parse_qs(url.query) + params['access_key'] = access_key + params['timestamp'] = timestamp + + def stringify_param_value(value): + if isinstance(value, list): + return quote(value[0]) + return quote(value) + + sorted_params = [ + '{}={}'.format(k, stringify_param_value(params[k])) + for k in sorted(params.keys()) + ] + + param_string = '&'.join(sorted_params) + if args.debug: + print(param_string) + + string_to_sign = '\n'.join([verb, host, path, param_string]) + if args.debug: + print(string_to_sign) + + if args.data: + data = base64.b64encode(args.data.encode()) + if data: + string_to_sign += data.decode() + + signature = base64.b64encode( + hmac.new(secret_key.encode(), string_to_sign.encode(), + hashlib.sha256).digest()) + + signed_url = '{}://{}{}?{}&signature={}'.format(url.scheme, url.netloc, + url.path, param_string, + signature.decode()) + import ipdb; ipdb.set_trace() # BREAKPOINT + if args.debug: + print(signed_url) + + verb_arg = '-X {} '.format(verb) + head_arg = '-I ' if args.head else '' + head_arg = '-s ' if args.silent else '' + data_arg = "--data '{}' ".format(args.data) if args.data else '' + + command = 'curl {}{}{}"{}"'.format(head_arg, verb_arg, data_arg, + signed_url) + if args.debug: + print(command) + + p = subprocess.Popen(command, shell=True) + p.wait() + + +if __name__ == "__main__": + main() diff --git a/scripts/sample_create_plot.json b/scripts/sample_create_plot.json new file mode 100644 index 000000000..cad413dba --- /dev/null +++ b/scripts/sample_create_plot.json @@ -0,0 +1,45 @@ +{ + "has_shape_photo": true, + "has_bark_photo": true, + "has_leaf_photo": true, + "tree.species": "40", + "tree.diameter": "1", + "tree.height": "4", + "tree.canopy_height": "3", + "tree.udf:Condition": "Healthy", + "tree.udf:JC Forester - Roots Sidewalk Issue": "", + "tree.udf:JC Forester - Canopy Power Lines Issue": "", + "tree.udf:JC Forester - Oak Wilt": "", + "plot.udf:Surveyor Id": "", + "plot.width": "3", + "plot.length": "4", + "plot.address_street": "", + "plot.address_city": "", + "plot.address_zip": "", + "plot.owner_orig_id": "", + "plot.udf:Install Tree Pit": "", + "plot.udf:Tree Pit Concreted Over": "", + "plot.udf:Steel Grate": "", + "plot.udf:Powerlines Overhead": "", + "plot.udf:Tree Stump": "", + "is_empty_site": false, + "tree.udf:Stewardship": [ + { + "Action": "Watered", + "Date": "2021-03-10" + }, + { + "Action": "Mulched, Had Compost Added, or Soil Amended", + "Date": "2021-03-14" + }, + { + "Action": "Mulched, Had Compost Added, or Soil Amended", + "Date": "2021-03-14" + } + ], + "plot.udf:Stewardship": [], + "plot.geom": { + "x": -8238688.656689439, + "y": 4971996.239469761 + } +} diff --git a/test-requirements.txt b/test-requirements.txt index 679ced8a5..e00f587ee 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ coverage==4.2 # https://coverage.readthedocs.io/en/coverage-4.2/changes.html PyVirtualDisplay==0.2 # https://github.com/ponty/PyVirtualDisplay/releases -# We use Firefox and selenium for running UI tests. But using the latest +# We use Firefox and selenium for running UI tests. But using the latest # versions is fragile because every few months a Firefox release is incompatible # with the current selenium. Since our UI tests don't depend on the latest features # of either we now use specific versions known to work together. @@ -11,3 +11,5 @@ PyVirtualDisplay==0.2 # https://github.com/ponty/PyVirtualDisplay/release # https://github.com/SeleniumHQ/selenium/blob/master/py/CHANGES selenium==2.53.6 # rq.filter: ==2.53.6 + +mock==3.0.5 diff --git a/webpack.common.config.js b/webpack.common.config.js index b7875dfbb..9804fa31f 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -5,7 +5,7 @@ var Webpack = require('webpack'), path = require('path'), _ = require('lodash'), BundleTracker = require('webpack-bundle-tracker'), - ExtractTextPlugin = require("extract-text-webpack-plugin"), + MiniCssExtractPlugin = require('mini-css-extract-plugin'), autoprefixer = require('autoprefixer'); function d(path) { @@ -16,6 +16,12 @@ function d(path) { function getEntries() { var files = glob.sync('./opentreemap/*/js/src/*.js'), entries = {}; + + //var files = [ + // ...glob.sync('./opentreemap/treemap/js/src/*.js'), + // ...glob.sync('./opentreemap/frontend/js/src/*.js'), + // ], + // entries = {}; files.forEach(function(file) { var app = file.split(path.sep)[2], basename = path.basename(file, '.js'); @@ -42,7 +48,7 @@ var shimmed = { leafletbing: d('assets/js/shim/leaflet.bing.js'), utfgrid: d('assets/js/shim/leaflet.utfgrid.js'), typeahead: d('assets/js/shim/typeahead.jquery.js'), - bootstrap: d('assets/js/shim/bootstrap.js'), + //bootstrap: d('assets/js/shim/bootstrap.js'), jqueryFileUpload: d('assets/js/shim/jquery.fileupload.js'), jqueryIframeTransport: d('assets/js/shim/jquery.iframe-transport.js'), jqueryUiWidget: d('assets/js/shim/jquery.ui.widget.js'), @@ -54,34 +60,79 @@ var shimmed = { module.exports = { entry: getEntries(), + /* + externals: { + "jquery": "jQuery", + //jQuery: "jquery", + //jQuery: d("assets/js/vendor/jquery"), + //jquery: d("assets/js/vendor/jquery"), + "window.jQuery": "jquery", + L: "leaflet" + }, + */ output: { filename: '[name].js', path: d('static'), sourceMapFilename: '[file].map' }, module: { - loaders: [{ + //loaders: [{ + rules: [ + { + test: /\.js$/, + include: [d('opentreemap/frontend/js/src/')], + use: ['babel-loader'] + /* + }, { include: [shimmed["bootstrap-datepicker"], shimmed["bootstrap-multiselect"]], - loader: "imports?bootstrap" + use: [ {loader: "imports-loader?bootstrap"} ] + */ }, { - test: /\.scss$/, - loader: ExtractTextPlugin.extract(['css', 'postcss-loader', 'sass'], {extract: true}) + test: /\.(sa|sc|c)ss$/, + use: [ + MiniCssExtractPlugin.loader, + { loader: 'css-loader', options: { sourceMap: false } }, + //{ loader: 'postcss-loader', options: { plugins: () => [autoprefixer({ browsers: ['last 2 versions'] })] } }, + 'sass-loader' + ] + /* + test: /\.(css|scss)$/, + use: [ + //MiniCssExtractPlugin.loader, + 'style-loader', + 'css-loader', + 'postcss-loader', + 'sass-loader', + ] + */ }, { test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)|\.svg($|\?)/, - loader: 'url', + use: [ {loader: 'url-loader'}], }, { test: /\.(jpg|png|gif)$/, - loader: 'url?limit=25000', + use: [ {loader: 'url-loader?limit=25000'} ], }] }, resolve: { alias: getAliases(), // Look in node_modules, our shared asset 'js/vendor/' and each Django // app's 'js/vendor/' for modules that support a module system - root: [d("assets/js/vendor"), d("node_modules")].concat(glob.sync(d('opentreemap/*/js/vendor/'))) + //modules: [d("assets/js/vendor"), d("node_modules")].concat(glob.sync(d('opentreemap/*/js/vendor/'))) + modules: [d("assets/js/vendor"), d("node_modules")].concat(glob.sync(d('opentreemap/*/js/vendor/'))) + //roots: [d("assets/js/vendor"), d("node_modules")].concat(glob.sync(d('opentreemap/*/js/vendor/'))) }, resolveLoader: { - root: d("node_modules") + roots: [d("node_modules")] + }, + optimization: { + /* + runtimeChunk: 'single', + */ + splitChunks: { + chunks: 'all', + name: "js/treemap/base-chunk", + minChunks: 2, + } }, plugins: [ // Provide jquery and Leaflet as global variables, which gets rid of @@ -89,9 +140,11 @@ module.exports = { // NOTE: the test configuration relies on this being the first plugin new Webpack.ProvidePlugin({ jQuery: "jquery", + $: 'jquery', "window.jQuery": "jquery", - L: "leaflet" + L: "leaflet", }), + /* new Webpack.optimize.CommonsChunkPlugin({ // Inlude the treemap/base entry module as part of the common module name: "js/treemap/base", @@ -99,10 +152,9 @@ module.exports = { // Chunks are moved to the common bundle if they are used in 2 or more entry bundles minChunks: 2, }), - new ExtractTextPlugin('css/main-[chunkhash].css', {allChunks: true}), + new MiniCssExtractPlugin({fiename: 'css/main-[chunkhash].css', {allChunks: true}), + */ + new MiniCssExtractPlugin({filename: 'css/style-[name].css', chunkFilename: "[name].css"}), new BundleTracker({path: d('static'), filename: 'webpack-stats.json'}) - ], - postcss: function () { - return [autoprefixer]; - } + ] }; diff --git a/webpack.dev.config.js b/webpack.dev.config.js index f3330ade2..9960096ee 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -2,6 +2,7 @@ var webpack = require('webpack'), config = require('./webpack.common.config.js'), + reversePath = __dirname + '/assets/js/shim/reverse.js'; host = process.env.WEBPACK_DEV_SERVER || 'http://localhost:6062/'; @@ -12,10 +13,18 @@ config.entry['js/treemap/base'] = [ './opentreemap/treemap/js/src/base.js' ]; -config.output.publicPath = host + 'static/'; -config.output.pathInfo = true; +//config.output.publicPath = host + 'static/'; +config.output.publicPath = '/static/'; +//config.output.pathInfo = true; +config.output.pathinfo = true; -config.debug = true; +config.module.rules.push({ + include: reversePath, + //loader: 'imports?this=>window!exports?Urls' + use: ['imports?this=>window!exports?Urls'] +}); + +//config.debug = true; config.devtool = 'eval'; diff --git a/webpack.prod.config.js b/webpack.prod.config.js index bcf2c9ae1..3fe9ebf7f 100644 --- a/webpack.prod.config.js +++ b/webpack.prod.config.js @@ -9,13 +9,44 @@ config.output.filename = '[name]-[chunkhash].js'; // Allows require-ing the static file created by django-js-reverse config.resolve.alias.reverse = reversePath; -config.devtool = 'source-map'; - -config.module.loaders.push({ - include: reversePath, - loader: 'imports?this=>window!exports?Urls' +config.devtool = false; +config.mode = 'production'; +//config.devtool = 'eval-cheap-source-map'; +//config.devtool = 'eval'; +//config.devtool = 'inline-source-map'; +//config.mode = 'development'; + +/* +config.watch = true; +config.watchOptions = { + poll: 1000, +}; +*/ + +//config.module.loaders.push({ +config.module.rules.push({ + //include: reversePath, + test: reversePath, + //loader: 'imports-loader?this=>window!exports-loader?Urls' + use: [//'imports-loader?this=>window!exports-loader?exports=default|Urls' + /*{ + loader: 'imports-loader', + options: { + wrapper: 'window' + //additionalCode: 'this = window;' + } + }, + */ + { + loader: 'exports-loader', + options: { + exports: 'Urls' + } + } + ] }); +/* config.plugins.concat([ new webpack.optimize.UglifyJsPlugin({ mangle: { @@ -24,6 +55,7 @@ config.plugins.concat([ }), new webpack.optimize.OccurrenceOrderPlugin() ]); +*/ config.output.publicPath = '/static/';