diff --git a/.gitignore b/.gitignore index 0fb3c32c0..d12d7e6eb 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ src/Handler/Course.SnapCustom.hs tags test.log *.dump-splices +/.stack-work.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe06bfac..e1e07c871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,53 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [2.1.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v2.1.0...v2.1.1) (2019-07-10) + + +### Bug Fixes + +* **assign correctors:** also show names of unenlisted correctors ([de49a77](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/de49a77)) +* **build:** fix build ([49dc413](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/49dc413)) + + + +## [2.1.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v2.0.0...v2.1.0) (2019-07-10) + + +### Bug Fixes + +* **corrector handling:** show correctors by a consistent order ([9c5ed5f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9c5ed5f)) +* **translation:** fix typos in translations; add bug to known bugs ([ac3f7bb](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ac3f7bb)) + + +### Features + +* **csv:** introduce csv export ([631bbef](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/631bbef)) + + + +## [2.0.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v1.4.1...v2.0.0) (2019-07-10) + + +### Bug Fixes + +* **correction:** comment column made wide in online correction form ([d83b1f6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d83b1f6)), closes [#373](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/373) +* **number-input-fields:** number inputs made HTML5 compatible ([6098215](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/6098215)), closes [#412](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/412) +* **ratings:** disallow ratings for graded sheets without point value ([c0b90c4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c0b90c4)) +* **tooltips:** fixes font-color when used in tableheaders ([f4bb70e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/f4bb70e)) + + +### Features + +* **exams:** show study features of registered users ([04bea76](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/04bea76)) + + +### BREAKING CHANGES + +* **exams:** E.isInfixOf and E.hasInfix + + + ### [1.4.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v1.4.0...v1.4.1) (2019-07-04) diff --git a/build.sh b/build.sh index f2958c7de..f35d369b2 100755 --- a/build.sh +++ b/build.sh @@ -2,6 +2,8 @@ set -e +[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || : + echo "Building..." stack build --fast --flag uniworx:-library-only --flag uniworx:dev $@ echo "Done." diff --git a/clean.sh b/clean.sh index 2c9c71212..2b9f5bfc7 100755 --- a/clean.sh +++ b/clean.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +set -e + +[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || : + case $1 in "") exec -- stack clean diff --git a/db.sh b/db.sh index 3d80bf68f..a66af88ba 100755 --- a/db.sh +++ b/db.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash # Options: see /test/Database.hs (Main) + set -e +[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || : + stack build --fast --flag uniworx:-library-only --flag uniworx:dev stack exec uniworxdb -- $@ diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 56f0d5164..000000000 --- a/deploy.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env sh - -configFile="" - -case "$1" in - test) - ln -svf "keter_testworx.yml" config/keter.yml - - yesod keter - ;; - production) - ln -svf "keter_uni2work.yml" config/keter.yml - - yesod keter && git tag -f live && git push origin live - ;; - *) - echo "Usage: $0 (test|production)" >&2 - exit 2 - ;; -esac - diff --git a/frontend/vendor/fontawesome.css b/frontend/vendor/fontawesome.css index 70c4ba2dd..deb5b4a59 100644 --- a/frontend/vendor/fontawesome.css +++ b/frontend/vendor/fontawesome.css @@ -1,5 +1,5 @@ /*! - * Font Awesome Free 5.1.0 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */ -.fa,.fab,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:a 2s infinite linear}.fa-pulse{animation:a 1s infinite steps(8)}@keyframes a{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=1)';transform:rotate(90deg)}.fa-rotate-180{-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=2)';transform:rotate(180deg)}.fa-rotate-270{-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)';transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)';transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)'}.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:'\f26e'}.fa-accessible-icon:before{content:'\f368'}.fa-accusoft:before{content:'\f369'}.fa-address-book:before{content:'\f2b9'}.fa-address-card:before{content:'\f2bb'}.fa-adjust:before{content:'\f042'}.fa-adn:before{content:'\f170'}.fa-adversal:before{content:'\f36a'}.fa-affiliatetheme:before{content:'\f36b'}.fa-algolia:before{content:'\f36c'}.fa-align-center:before{content:'\f037'}.fa-align-justify:before{content:'\f039'}.fa-align-left:before{content:'\f036'}.fa-align-right:before{content:'\f038'}.fa-allergies:before{content:'\f461'}.fa-amazon:before{content:'\f270'}.fa-amazon-pay:before{content:'\f42c'}.fa-ambulance:before{content:'\f0f9'}.fa-american-sign-language-interpreting:before{content:'\f2a3'}.fa-amilia:before{content:'\f36d'}.fa-anchor:before{content:'\f13d'}.fa-android:before{content:'\f17b'}.fa-angellist:before{content:'\f209'}.fa-angle-double-down:before{content:'\f103'}.fa-angle-double-left:before{content:'\f100'}.fa-angle-double-right:before{content:'\f101'}.fa-angle-double-up:before{content:'\f102'}.fa-angle-down:before{content:'\f107'}.fa-angle-left:before{content:'\f104'}.fa-angle-right:before{content:'\f105'}.fa-angle-up:before{content:'\f106'}.fa-angry:before{content:'\f556'}.fa-angrycreative:before{content:'\f36e'}.fa-angular:before{content:'\f420'}.fa-app-store:before{content:'\f36f'}.fa-app-store-ios:before{content:'\f370'}.fa-apper:before{content:'\f371'}.fa-apple:before{content:'\f179'}.fa-apple-pay:before{content:'\f415'}.fa-archive:before{content:'\f187'}.fa-archway:before{content:'\f557'}.fa-arrow-alt-circle-down:before{content:'\f358'}.fa-arrow-alt-circle-left:before{content:'\f359'}.fa-arrow-alt-circle-right:before{content:'\f35a'}.fa-arrow-alt-circle-up:before{content:'\f35b'}.fa-arrow-circle-down:before{content:'\f0ab'}.fa-arrow-circle-left:before{content:'\f0a8'}.fa-arrow-circle-right:before{content:'\f0a9'}.fa-arrow-circle-up:before{content:'\f0aa'}.fa-arrow-down:before{content:'\f063'}.fa-arrow-left:before{content:'\f060'}.fa-arrow-right:before{content:'\f061'}.fa-arrow-up:before{content:'\f062'}.fa-arrows-alt:before{content:'\f0b2'}.fa-arrows-alt-h:before{content:'\f337'}.fa-arrows-alt-v:before{content:'\f338'}.fa-assistive-listening-systems:before{content:'\f2a2'}.fa-asterisk:before{content:'\f069'}.fa-asymmetrik:before{content:'\f372'}.fa-at:before{content:'\f1fa'}.fa-atlas:before{content:'\f558'}.fa-audible:before{content:'\f373'}.fa-audio-description:before{content:'\f29e'}.fa-autoprefixer:before{content:'\f41c'}.fa-avianex:before{content:'\f374'}.fa-aviato:before{content:'\f421'}.fa-award:before{content:'\f559'}.fa-aws:before{content:'\f375'}.fa-backspace:before{content:'\f55a'}.fa-backward:before{content:'\f04a'}.fa-balance-scale:before{content:'\f24e'}.fa-ban:before{content:'\f05e'}.fa-band-aid:before{content:'\f462'}.fa-bandcamp:before{content:'\f2d5'}.fa-barcode:before{content:'\f02a'}.fa-bars:before{content:'\f0c9'}.fa-baseball-ball:before{content:'\f433'}.fa-basketball-ball:before{content:'\f434'}.fa-bath:before{content:'\f2cd'}.fa-battery-empty:before{content:'\f244'}.fa-battery-full:before{content:'\f240'}.fa-battery-half:before{content:'\f242'}.fa-battery-quarter:before{content:'\f243'}.fa-battery-three-quarters:before{content:'\f241'}.fa-bed:before{content:'\f236'}.fa-beer:before{content:'\f0fc'}.fa-behance:before{content:'\f1b4'}.fa-behance-square:before{content:'\f1b5'}.fa-bell:before{content:'\f0f3'}.fa-bell-slash:before{content:'\f1f6'}.fa-bezier-curve:before{content:'\f55b'}.fa-bicycle:before{content:'\f206'}.fa-bimobject:before{content:'\f378'}.fa-binoculars:before{content:'\f1e5'}.fa-birthday-cake:before{content:'\f1fd'}.fa-bitbucket:before{content:'\f171'}.fa-bitcoin:before{content:'\f379'}.fa-bity:before{content:'\f37a'}.fa-black-tie:before{content:'\f27e'}.fa-blackberry:before{content:'\f37b'}.fa-blender:before{content:'\f517'}.fa-blind:before{content:'\f29d'}.fa-blogger:before{content:'\f37c'}.fa-blogger-b:before{content:'\f37d'}.fa-bluetooth:before{content:'\f293'}.fa-bluetooth-b:before{content:'\f294'}.fa-bold:before{content:'\f032'}.fa-bolt:before{content:'\f0e7'}.fa-bomb:before{content:'\f1e2'}.fa-bong:before{content:'\f55c'}.fa-book:before{content:'\f02d'}.fa-book-open:before{content:'\f518'}.fa-bookmark:before{content:'\f02e'}.fa-bowling-ball:before{content:'\f436'}.fa-box:before{content:'\f466'}.fa-box-open:before{content:'\f49e'}.fa-boxes:before{content:'\f468'}.fa-braille:before{content:'\f2a1'}.fa-briefcase:before{content:'\f0b1'}.fa-briefcase-medical:before{content:'\f469'}.fa-broadcast-tower:before{content:'\f519'}.fa-broom:before{content:'\f51a'}.fa-brush:before{content:'\f55d'}.fa-btc:before{content:'\f15a'}.fa-bug:before{content:'\f188'}.fa-building:before{content:'\f1ad'}.fa-bullhorn:before{content:'\f0a1'}.fa-bullseye:before{content:'\f140'}.fa-burn:before{content:'\f46a'}.fa-buromobelexperte:before{content:'\f37f'}.fa-bus:before{content:'\f207'}.fa-bus-alt:before{content:'\f55e'}.fa-buysellads:before{content:'\f20d'}.fa-calculator:before{content:'\f1ec'}.fa-calendar:before{content:'\f133'}.fa-calendar-alt:before{content:'\f073'}.fa-calendar-check:before{content:'\f274'}.fa-calendar-minus:before{content:'\f272'}.fa-calendar-plus:before{content:'\f271'}.fa-calendar-times:before{content:'\f273'}.fa-camera:before{content:'\f030'}.fa-camera-retro:before{content:'\f083'}.fa-cannabis:before{content:'\f55f'}.fa-capsules:before{content:'\f46b'}.fa-car:before{content:'\f1b9'}.fa-caret-down:before{content:'\f0d7'}.fa-caret-left:before{content:'\f0d9'}.fa-caret-right:before{content:'\f0da'}.fa-caret-square-down:before{content:'\f150'}.fa-caret-square-left:before{content:'\f191'}.fa-caret-square-right:before{content:'\f152'}.fa-caret-square-up:before{content:'\f151'}.fa-caret-up:before{content:'\f0d8'}.fa-cart-arrow-down:before{content:'\f218'}.fa-cart-plus:before{content:'\f217'}.fa-cc-amazon-pay:before{content:'\f42d'}.fa-cc-amex:before{content:'\f1f3'}.fa-cc-apple-pay:before{content:'\f416'}.fa-cc-diners-club:before{content:'\f24c'}.fa-cc-discover:before{content:'\f1f2'}.fa-cc-jcb:before{content:'\f24b'}.fa-cc-mastercard:before{content:'\f1f1'}.fa-cc-paypal:before{content:'\f1f4'}.fa-cc-stripe:before{content:'\f1f5'}.fa-cc-visa:before{content:'\f1f0'}.fa-centercode:before{content:'\f380'}.fa-certificate:before{content:'\f0a3'}.fa-chalkboard:before{content:'\f51b'}.fa-chalkboard-teacher:before{content:'\f51c'}.fa-chart-area:before{content:'\f1fe'}.fa-chart-bar:before{content:'\f080'}.fa-chart-line:before{content:'\f201'}.fa-chart-pie:before{content:'\f200'}.fa-check:before{content:'\f00c'}.fa-check-circle:before{content:'\f058'}.fa-check-double:before{content:'\f560'}.fa-check-square:before{content:'\f14a'}.fa-chess:before{content:'\f439'}.fa-chess-bishop:before{content:'\f43a'}.fa-chess-board:before{content:'\f43c'}.fa-chess-king:before{content:'\f43f'}.fa-chess-knight:before{content:'\f441'}.fa-chess-pawn:before{content:'\f443'}.fa-chess-queen:before{content:'\f445'}.fa-chess-rook:before{content:'\f447'}.fa-chevron-circle-down:before{content:'\f13a'}.fa-chevron-circle-left:before{content:'\f137'}.fa-chevron-circle-right:before{content:'\f138'}.fa-chevron-circle-up:before{content:'\f139'}.fa-chevron-down:before{content:'\f078'}.fa-chevron-left:before{content:'\f053'}.fa-chevron-right:before{content:'\f054'}.fa-chevron-up:before{content:'\f077'}.fa-child:before{content:'\f1ae'}.fa-chrome:before{content:'\f268'}.fa-church:before{content:'\f51d'}.fa-circle:before{content:'\f111'}.fa-circle-notch:before{content:'\f1ce'}.fa-clipboard:before{content:'\f328'}.fa-clipboard-check:before{content:'\f46c'}.fa-clipboard-list:before{content:'\f46d'}.fa-clock:before{content:'\f017'}.fa-clone:before{content:'\f24d'}.fa-closed-captioning:before{content:'\f20a'}.fa-cloud:before{content:'\f0c2'}.fa-cloud-download-alt:before{content:'\f381'}.fa-cloud-upload-alt:before{content:'\f382'}.fa-cloudscale:before{content:'\f383'}.fa-cloudsmith:before{content:'\f384'}.fa-cloudversify:before{content:'\f385'}.fa-cocktail:before{content:'\f561'}.fa-code:before{content:'\f121'}.fa-code-branch:before{content:'\f126'}.fa-codepen:before{content:'\f1cb'}.fa-codiepie:before{content:'\f284'}.fa-coffee:before{content:'\f0f4'}.fa-cog:before{content:'\f013'}.fa-cogs:before{content:'\f085'}.fa-coins:before{content:'\f51e'}.fa-columns:before{content:'\f0db'}.fa-comment:before{content:'\f075'}.fa-comment-alt:before{content:'\f27a'}.fa-comment-dots:before{content:'\f4ad'}.fa-comment-slash:before{content:'\f4b3'}.fa-comments:before{content:'\f086'}.fa-compact-disc:before{content:'\f51f'}.fa-compass:before{content:'\f14e'}.fa-compress:before{content:'\f066'}.fa-concierge-bell:before{content:'\f562'}.fa-connectdevelop:before{content:'\f20e'}.fa-contao:before{content:'\f26d'}.fa-cookie:before{content:'\f563'}.fa-cookie-bite:before{content:'\f564'}.fa-copy:before{content:'\f0c5'}.fa-copyright:before{content:'\f1f9'}.fa-couch:before{content:'\f4b8'}.fa-cpanel:before{content:'\f388'}.fa-creative-commons:before{content:'\f25e'}.fa-creative-commons-by:before{content:'\f4e7'}.fa-creative-commons-nc:before{content:'\f4e8'}.fa-creative-commons-nc-eu:before{content:'\f4e9'}.fa-creative-commons-nc-jp:before{content:'\f4ea'}.fa-creative-commons-nd:before{content:'\f4eb'}.fa-creative-commons-pd:before{content:'\f4ec'}.fa-creative-commons-pd-alt:before{content:'\f4ed'}.fa-creative-commons-remix:before{content:'\f4ee'}.fa-creative-commons-sa:before{content:'\f4ef'}.fa-creative-commons-sampling:before{content:'\f4f0'}.fa-creative-commons-sampling-plus:before{content:'\f4f1'}.fa-creative-commons-share:before{content:'\f4f2'}.fa-credit-card:before{content:'\f09d'}.fa-crop:before{content:'\f125'}.fa-crop-alt:before{content:'\f565'}.fa-crosshairs:before{content:'\f05b'}.fa-crow:before{content:'\f520'}.fa-crown:before{content:'\f521'}.fa-css3:before{content:'\f13c'}.fa-css3-alt:before{content:'\f38b'}.fa-cube:before{content:'\f1b2'}.fa-cubes:before{content:'\f1b3'}.fa-cut:before{content:'\f0c4'}.fa-cuttlefish:before{content:'\f38c'}.fa-d-and-d:before{content:'\f38d'}.fa-dashcube:before{content:'\f210'}.fa-database:before{content:'\f1c0'}.fa-deaf:before{content:'\f2a4'}.fa-delicious:before{content:'\f1a5'}.fa-deploydog:before{content:'\f38e'}.fa-deskpro:before{content:'\f38f'}.fa-desktop:before{content:'\f108'}.fa-deviantart:before{content:'\f1bd'}.fa-diagnoses:before{content:'\f470'}.fa-dice:before{content:'\f522'}.fa-dice-five:before{content:'\f523'}.fa-dice-four:before{content:'\f524'}.fa-dice-one:before{content:'\f525'}.fa-dice-six:before{content:'\f526'}.fa-dice-three:before{content:'\f527'}.fa-dice-two:before{content:'\f528'}.fa-digg:before{content:'\f1a6'}.fa-digital-ocean:before{content:'\f391'}.fa-digital-tachograph:before{content:'\f566'}.fa-discord:before{content:'\f392'}.fa-discourse:before{content:'\f393'}.fa-divide:before{content:'\f529'}.fa-dizzy:before{content:'\f567'}.fa-dna:before{content:'\f471'}.fa-dochub:before{content:'\f394'}.fa-docker:before{content:'\f395'}.fa-dollar-sign:before{content:'\f155'}.fa-dolly:before{content:'\f472'}.fa-dolly-flatbed:before{content:'\f474'}.fa-donate:before{content:'\f4b9'}.fa-door-closed:before{content:'\f52a'}.fa-door-open:before{content:'\f52b'}.fa-dot-circle:before{content:'\f192'}.fa-dove:before{content:'\f4ba'}.fa-download:before{content:'\f019'}.fa-draft2digital:before{content:'\f396'}.fa-drafting-compass:before{content:'\f568'}.fa-dribbble:before{content:'\f17d'}.fa-dribbble-square:before{content:'\f397'}.fa-dropbox:before{content:'\f16b'}.fa-drum:before{content:'\f569'}.fa-drum-steelpan:before{content:'\f56a'}.fa-drupal:before{content:'\f1a9'}.fa-dumbbell:before{content:'\f44b'}.fa-dyalog:before{content:'\f399'}.fa-earlybirds:before{content:'\f39a'}.fa-ebay:before{content:'\f4f4'}.fa-edge:before{content:'\f282'}.fa-edit:before{content:'\f044'}.fa-eject:before{content:'\f052'}.fa-elementor:before{content:'\f430'}.fa-ellipsis-h:before{content:'\f141'}.fa-ellipsis-v:before{content:'\f142'}.fa-ember:before{content:'\f423'}.fa-empire:before{content:'\f1d1'}.fa-envelope:before{content:'\f0e0'}.fa-envelope-open:before{content:'\f2b6'}.fa-envelope-square:before{content:'\f199'}.fa-envira:before{content:'\f299'}.fa-equals:before{content:'\f52c'}.fa-eraser:before{content:'\f12d'}.fa-erlang:before{content:'\f39d'}.fa-ethereum:before{content:'\f42e'}.fa-etsy:before{content:'\f2d7'}.fa-euro-sign:before{content:'\f153'}.fa-exchange-alt:before{content:'\f362'}.fa-exclamation:before{content:'\f12a'}.fa-exclamation-circle:before{content:'\f06a'}.fa-exclamation-triangle:before{content:'\f071'}.fa-expand:before{content:'\f065'}.fa-expand-arrows-alt:before{content:'\f31e'}.fa-expeditedssl:before{content:'\f23e'}.fa-external-link-alt:before{content:'\f35d'}.fa-external-link-square-alt:before{content:'\f360'}.fa-eye:before{content:'\f06e'}.fa-eye-dropper:before{content:'\f1fb'}.fa-eye-slash:before{content:'\f070'}.fa-facebook:before{content:'\f09a'}.fa-facebook-f:before{content:'\f39e'}.fa-facebook-messenger:before{content:'\f39f'}.fa-facebook-square:before{content:'\f082'}.fa-fast-backward:before{content:'\f049'}.fa-fast-forward:before{content:'\f050'}.fa-fax:before{content:'\f1ac'}.fa-feather:before{content:'\f52d'}.fa-feather-alt:before{content:'\f56b'}.fa-female:before{content:'\f182'}.fa-fighter-jet:before{content:'\f0fb'}.fa-file:before{content:'\f15b'}.fa-file-alt:before{content:'\f15c'}.fa-file-archive:before{content:'\f1c6'}.fa-file-audio:before{content:'\f1c7'}.fa-file-code:before{content:'\f1c9'}.fa-file-contract:before{content:'\f56c'}.fa-file-download:before{content:'\f56d'}.fa-file-excel:before{content:'\f1c3'}.fa-file-export:before{content:'\f56e'}.fa-file-image:before{content:'\f1c5'}.fa-file-import:before{content:'\f56f'}.fa-file-invoice:before{content:'\f570'}.fa-file-invoice-dollar:before{content:'\f571'}.fa-file-medical:before{content:'\f477'}.fa-file-medical-alt:before{content:'\f478'}.fa-file-pdf:before{content:'\f1c1'}.fa-file-powerpoint:before{content:'\f1c4'}.fa-file-prescription:before{content:'\f572'}.fa-file-signature:before{content:'\f573'}.fa-file-upload:before{content:'\f574'}.fa-file-video:before{content:'\f1c8'}.fa-file-word:before{content:'\f1c2'}.fa-fill:before{content:'\f575'}.fa-fill-drip:before{content:'\f576'}.fa-film:before{content:'\f008'}.fa-filter:before{content:'\f0b0'}.fa-fingerprint:before{content:'\f577'}.fa-fire:before{content:'\f06d'}.fa-fire-extinguisher:before{content:'\f134'}.fa-firefox:before{content:'\f269'}.fa-first-aid:before{content:'\f479'}.fa-first-order:before{content:'\f2b0'}.fa-first-order-alt:before{content:'\f50a'}.fa-firstdraft:before{content:'\f3a1'}.fa-fish:before{content:'\f578'}.fa-flag:before{content:'\f024'}.fa-flag-checkered:before{content:'\f11e'}.fa-flask:before{content:'\f0c3'}.fa-flickr:before{content:'\f16e'}.fa-flipboard:before{content:'\f44d'}.fa-flushed:before{content:'\f579'}.fa-fly:before{content:'\f417'}.fa-folder:before{content:'\f07b'}.fa-folder-open:before{content:'\f07c'}.fa-font:before{content:'\f031'}.fa-font-awesome:before{content:'\f2b4'}.fa-font-awesome-alt:before{content:'\f35c'}.fa-font-awesome-flag:before{content:'\f425'}.fa-font-awesome-logo-full:before{content:'\f4e6'}.fa-fonticons:before{content:'\f280'}.fa-fonticons-fi:before{content:'\f3a2'}.fa-football-ball:before{content:'\f44e'}.fa-fort-awesome:before{content:'\f286'}.fa-fort-awesome-alt:before{content:'\f3a3'}.fa-forumbee:before{content:'\f211'}.fa-forward:before{content:'\f04e'}.fa-foursquare:before{content:'\f180'}.fa-free-code-camp:before{content:'\f2c5'}.fa-freebsd:before{content:'\f3a4'}.fa-frog:before{content:'\f52e'}.fa-frown:before{content:'\f119'}.fa-frown-open:before{content:'\f57a'}.fa-fulcrum:before{content:'\f50b'}.fa-futbol:before{content:'\f1e3'}.fa-galactic-republic:before{content:'\f50c'}.fa-galactic-senate:before{content:'\f50d'}.fa-gamepad:before{content:'\f11b'}.fa-gas-pump:before{content:'\f52f'}.fa-gavel:before{content:'\f0e3'}.fa-gem:before{content:'\f3a5'}.fa-genderless:before{content:'\f22d'}.fa-get-pocket:before{content:'\f265'}.fa-gg:before{content:'\f260'}.fa-gg-circle:before{content:'\f261'}.fa-gift:before{content:'\f06b'}.fa-git:before{content:'\f1d3'}.fa-git-square:before{content:'\f1d2'}.fa-github:before{content:'\f09b'}.fa-github-alt:before{content:'\f113'}.fa-github-square:before{content:'\f092'}.fa-gitkraken:before{content:'\f3a6'}.fa-gitlab:before{content:'\f296'}.fa-gitter:before{content:'\f426'}.fa-glass-martini:before{content:'\f000'}.fa-glass-martini-alt:before{content:'\f57b'}.fa-glasses:before{content:'\f530'}.fa-glide:before{content:'\f2a5'}.fa-glide-g:before{content:'\f2a6'}.fa-globe:before{content:'\f0ac'}.fa-globe-africa:before{content:'\f57c'}.fa-globe-americas:before{content:'\f57d'}.fa-globe-asia:before{content:'\f57e'}.fa-gofore:before{content:'\f3a7'}.fa-golf-ball:before{content:'\f450'}.fa-goodreads:before{content:'\f3a8'}.fa-goodreads-g:before{content:'\f3a9'}.fa-google:before{content:'\f1a0'}.fa-google-drive:before{content:'\f3aa'}.fa-google-play:before{content:'\f3ab'}.fa-google-plus:before{content:'\f2b3'}.fa-google-plus-g:before{content:'\f0d5'}.fa-google-plus-square:before{content:'\f0d4'}.fa-google-wallet:before{content:'\f1ee'}.fa-graduation-cap:before{content:'\f19d'}.fa-gratipay:before{content:'\f184'}.fa-grav:before{content:'\f2d6'}.fa-greater-than:before{content:'\f531'}.fa-greater-than-equal:before{content:'\f532'}.fa-grimace:before{content:'\f57f'}.fa-grin:before{content:'\f580'}.fa-grin-alt:before{content:'\f581'}.fa-grin-beam:before{content:'\f582'}.fa-grin-beam-sweat:before{content:'\f583'}.fa-grin-hearts:before{content:'\f584'}.fa-grin-squint:before{content:'\f585'}.fa-grin-squint-tears:before{content:'\f586'}.fa-grin-stars:before{content:'\f587'}.fa-grin-tears:before{content:'\f588'}.fa-grin-tongue:before{content:'\f589'}.fa-grin-tongue-squint:before{content:'\f58a'}.fa-grin-tongue-wink:before{content:'\f58b'}.fa-grin-wink:before{content:'\f58c'}.fa-grip-horizontal:before{content:'\f58d'}.fa-grip-vertical:before{content:'\f58e'}.fa-gripfire:before{content:'\f3ac'}.fa-grunt:before{content:'\f3ad'}.fa-gulp:before{content:'\f3ae'}.fa-h-square:before{content:'\f0fd'}.fa-hacker-news:before{content:'\f1d4'}.fa-hacker-news-square:before{content:'\f3af'}.fa-hand-holding:before{content:'\f4bd'}.fa-hand-holding-heart:before{content:'\f4be'}.fa-hand-holding-usd:before{content:'\f4c0'}.fa-hand-lizard:before{content:'\f258'}.fa-hand-paper:before{content:'\f256'}.fa-hand-peace:before{content:'\f25b'}.fa-hand-point-down:before{content:'\f0a7'}.fa-hand-point-left:before{content:'\f0a5'}.fa-hand-point-right:before{content:'\f0a4'}.fa-hand-point-up:before{content:'\f0a6'}.fa-hand-pointer:before{content:'\f25a'}.fa-hand-rock:before{content:'\f255'}.fa-hand-scissors:before{content:'\f257'}.fa-hand-spock:before{content:'\f259'}.fa-hands:before{content:'\f4c2'}.fa-hands-helping:before{content:'\f4c4'}.fa-handshake:before{content:'\f2b5'}.fa-hashtag:before{content:'\f292'}.fa-hdd:before{content:'\f0a0'}.fa-heading:before{content:'\f1dc'}.fa-headphones:before{content:'\f025'}.fa-headphones-alt:before{content:'\f58f'}.fa-headset:before{content:'\f590'}.fa-heart:before{content:'\f004'}.fa-heartbeat:before{content:'\f21e'}.fa-helicopter:before{content:'\f533'}.fa-highlighter:before{content:'\f591'}.fa-hips:before{content:'\f452'}.fa-hire-a-helper:before{content:'\f3b0'}.fa-history:before{content:'\f1da'}.fa-hockey-puck:before{content:'\f453'}.fa-home:before{content:'\f015'}.fa-hooli:before{content:'\f427'}.fa-hornbill:before{content:'\f592'}.fa-hospital:before{content:'\f0f8'}.fa-hospital-alt:before{content:'\f47d'}.fa-hospital-symbol:before{content:'\f47e'}.fa-hot-tub:before{content:'\f593'}.fa-hotel:before{content:'\f594'}.fa-hotjar:before{content:'\f3b1'}.fa-hourglass:before{content:'\f254'}.fa-hourglass-end:before{content:'\f253'}.fa-hourglass-half:before{content:'\f252'}.fa-hourglass-start:before{content:'\f251'}.fa-houzz:before{content:'\f27c'}.fa-html5:before{content:'\f13b'}.fa-hubspot:before{content:'\f3b2'}.fa-i-cursor:before{content:'\f246'}.fa-id-badge:before{content:'\f2c1'}.fa-id-card:before{content:'\f2c2'}.fa-id-card-alt:before{content:'\f47f'}.fa-image:before{content:'\f03e'}.fa-images:before{content:'\f302'}.fa-imdb:before{content:'\f2d8'}.fa-inbox:before{content:'\f01c'}.fa-indent:before{content:'\f03c'}.fa-industry:before{content:'\f275'}.fa-infinity:before{content:'\f534'}.fa-info:before{content:'\f129'}.fa-info-circle:before{content:'\f05a'}.fa-instagram:before{content:'\f16d'}.fa-internet-explorer:before{content:'\f26b'}.fa-ioxhost:before{content:'\f208'}.fa-italic:before{content:'\f033'}.fa-itunes:before{content:'\f3b4'}.fa-itunes-note:before{content:'\f3b5'}.fa-java:before{content:'\f4e4'}.fa-jedi-order:before{content:'\f50e'}.fa-jenkins:before{content:'\f3b6'}.fa-joget:before{content:'\f3b7'}.fa-joint:before{content:'\f595'}.fa-joomla:before{content:'\f1aa'}.fa-js:before{content:'\f3b8'}.fa-js-square:before{content:'\f3b9'}.fa-jsfiddle:before{content:'\f1cc'}.fa-key:before{content:'\f084'}.fa-keybase:before{content:'\f4f5'}.fa-keyboard:before{content:'\f11c'}.fa-keycdn:before{content:'\f3ba'}.fa-kickstarter:before{content:'\f3bb'}.fa-kickstarter-k:before{content:'\f3bc'}.fa-kiss:before{content:'\f596'}.fa-kiss-beam:before{content:'\f597'}.fa-kiss-wink-heart:before{content:'\f598'}.fa-kiwi-bird:before{content:'\f535'}.fa-korvue:before{content:'\f42f'}.fa-language:before{content:'\f1ab'}.fa-laptop:before{content:'\f109'}.fa-laravel:before{content:'\f3bd'}.fa-lastfm:before{content:'\f202'}.fa-lastfm-square:before{content:'\f203'}.fa-laugh:before{content:'\f599'}.fa-laugh-beam:before{content:'\f59a'}.fa-laugh-squint:before{content:'\f59b'}.fa-laugh-wink:before{content:'\f59c'}.fa-leaf:before{content:'\f06c'}.fa-leanpub:before{content:'\f212'}.fa-lemon:before{content:'\f094'}.fa-less:before{content:'\f41d'}.fa-less-than:before{content:'\f536'}.fa-less-than-equal:before{content:'\f537'}.fa-level-down-alt:before{content:'\f3be'}.fa-level-up-alt:before{content:'\f3bf'}.fa-life-ring:before{content:'\f1cd'}.fa-lightbulb:before{content:'\f0eb'}.fa-line:before{content:'\f3c0'}.fa-link:before{content:'\f0c1'}.fa-linkedin:before{content:'\f08c'}.fa-linkedin-in:before{content:'\f0e1'}.fa-linode:before{content:'\f2b8'}.fa-linux:before{content:'\f17c'}.fa-lira-sign:before{content:'\f195'}.fa-list:before{content:'\f03a'}.fa-list-alt:before{content:'\f022'}.fa-list-ol:before{content:'\f0cb'}.fa-list-ul:before{content:'\f0ca'}.fa-location-arrow:before{content:'\f124'}.fa-lock:before{content:'\f023'}.fa-lock-open:before{content:'\f3c1'}.fa-long-arrow-alt-down:before{content:'\f309'}.fa-long-arrow-alt-left:before{content:'\f30a'}.fa-long-arrow-alt-right:before{content:'\f30b'}.fa-long-arrow-alt-up:before{content:'\f30c'}.fa-low-vision:before{content:'\f2a8'}.fa-luggage-cart:before{content:'\f59d'}.fa-lyft:before{content:'\f3c3'}.fa-magento:before{content:'\f3c4'}.fa-magic:before{content:'\f0d0'}.fa-magnet:before{content:'\f076'}.fa-mailchimp:before{content:'\f59e'}.fa-male:before{content:'\f183'}.fa-mandalorian:before{content:'\f50f'}.fa-map:before{content:'\f279'}.fa-map-marked:before{content:'\f59f'}.fa-map-marked-alt:before{content:'\f5a0'}.fa-map-marker:before{content:'\f041'}.fa-map-marker-alt:before{content:'\f3c5'}.fa-map-pin:before{content:'\f276'}.fa-map-signs:before{content:'\f277'}.fa-marker:before{content:'\f5a1'}.fa-mars:before{content:'\f222'}.fa-mars-double:before{content:'\f227'}.fa-mars-stroke:before{content:'\f229'}.fa-mars-stroke-h:before{content:'\f22b'}.fa-mars-stroke-v:before{content:'\f22a'}.fa-mastodon:before{content:'\f4f6'}.fa-maxcdn:before{content:'\f136'}.fa-medal:before{content:'\f5a2'}.fa-medapps:before{content:'\f3c6'}.fa-medium:before{content:'\f23a'}.fa-medium-m:before{content:'\f3c7'}.fa-medkit:before{content:'\f0fa'}.fa-medrt:before{content:'\f3c8'}.fa-meetup:before{content:'\f2e0'}.fa-megaport:before{content:'\f5a3'}.fa-meh:before{content:'\f11a'}.fa-meh-blank:before{content:'\f5a4'}.fa-meh-rolling-eyes:before{content:'\f5a5'}.fa-memory:before{content:'\f538'}.fa-mercury:before{content:'\f223'}.fa-microchip:before{content:'\f2db'}.fa-microphone:before{content:'\f130'}.fa-microphone-alt:before{content:'\f3c9'}.fa-microphone-alt-slash:before{content:'\f539'}.fa-microphone-slash:before{content:'\f131'}.fa-microsoft:before{content:'\f3ca'}.fa-minus:before{content:'\f068'}.fa-minus-circle:before{content:'\f056'}.fa-minus-square:before{content:'\f146'}.fa-mix:before{content:'\f3cb'}.fa-mixcloud:before{content:'\f289'}.fa-mizuni:before{content:'\f3cc'}.fa-mobile:before{content:'\f10b'}.fa-mobile-alt:before{content:'\f3cd'}.fa-modx:before{content:'\f285'}.fa-monero:before{content:'\f3d0'}.fa-money-bill:before{content:'\f0d6'}.fa-money-bill-alt:before{content:'\f3d1'}.fa-money-bill-wave:before{content:'\f53a'}.fa-money-bill-wave-alt:before{content:'\f53b'}.fa-money-check:before{content:'\f53c'}.fa-money-check-alt:before{content:'\f53d'}.fa-monument:before{content:'\f5a6'}.fa-moon:before{content:'\f186'}.fa-mortar-pestle:before{content:'\f5a7'}.fa-motorcycle:before{content:'\f21c'}.fa-mouse-pointer:before{content:'\f245'}.fa-music:before{content:'\f001'}.fa-napster:before{content:'\f3d2'}.fa-neuter:before{content:'\f22c'}.fa-newspaper:before{content:'\f1ea'}.fa-nimblr:before{content:'\f5a8'}.fa-nintendo-switch:before{content:'\f418'}.fa-node:before{content:'\f419'}.fa-node-js:before{content:'\f3d3'}.fa-not-equal:before{content:'\f53e'}.fa-notes-medical:before{content:'\f481'}.fa-npm:before{content:'\f3d4'}.fa-ns8:before{content:'\f3d5'}.fa-nutritionix:before{content:'\f3d6'}.fa-object-group:before{content:'\f247'}.fa-object-ungroup:before{content:'\f248'}.fa-odnoklassniki:before{content:'\f263'}.fa-odnoklassniki-square:before{content:'\f264'}.fa-old-republic:before{content:'\f510'}.fa-opencart:before{content:'\f23d'}.fa-openid:before{content:'\f19b'}.fa-opera:before{content:'\f26a'}.fa-optin-monster:before{content:'\f23c'}.fa-osi:before{content:'\f41a'}.fa-outdent:before{content:'\f03b'}.fa-page4:before{content:'\f3d7'}.fa-pagelines:before{content:'\f18c'}.fa-paint-brush:before{content:'\f1fc'}.fa-paint-roller:before{content:'\f5aa'}.fa-palette:before{content:'\f53f'}.fa-palfed:before{content:'\f3d8'}.fa-pallet:before{content:'\f482'}.fa-paper-plane:before{content:'\f1d8'}.fa-paperclip:before{content:'\f0c6'}.fa-parachute-box:before{content:'\f4cd'}.fa-paragraph:before{content:'\f1dd'}.fa-parking:before{content:'\f540'}.fa-passport:before{content:'\f5ab'}.fa-paste:before{content:'\f0ea'}.fa-patreon:before{content:'\f3d9'}.fa-pause:before{content:'\f04c'}.fa-pause-circle:before{content:'\f28b'}.fa-paw:before{content:'\f1b0'}.fa-paypal:before{content:'\f1ed'}.fa-pen:before{content:'\f304'}.fa-pen-alt:before{content:'\f305'}.fa-pen-fancy:before{content:'\f5ac'}.fa-pen-nib:before{content:'\f5ad'}.fa-pen-square:before{content:'\f14b'}.fa-pencil-alt:before{content:'\f303'}.fa-pencil-ruler:before{content:'\f5ae'}.fa-people-carry:before{content:'\f4ce'}.fa-percent:before{content:'\f295'}.fa-percentage:before{content:'\f541'}.fa-periscope:before{content:'\f3da'}.fa-phabricator:before{content:'\f3db'}.fa-phoenix-framework:before{content:'\f3dc'}.fa-phoenix-squadron:before{content:'\f511'}.fa-phone:before{content:'\f095'}.fa-phone-slash:before{content:'\f3dd'}.fa-phone-square:before{content:'\f098'}.fa-phone-volume:before{content:'\f2a0'}.fa-php:before{content:'\f457'}.fa-pied-piper:before{content:'\f2ae'}.fa-pied-piper-alt:before{content:'\f1a8'}.fa-pied-piper-hat:before{content:'\f4e5'}.fa-pied-piper-pp:before{content:'\f1a7'}.fa-piggy-bank:before{content:'\f4d3'}.fa-pills:before{content:'\f484'}.fa-pinterest:before{content:'\f0d2'}.fa-pinterest-p:before{content:'\f231'}.fa-pinterest-square:before{content:'\f0d3'}.fa-plane:before{content:'\f072'}.fa-plane-arrival:before{content:'\f5af'}.fa-plane-departure:before{content:'\f5b0'}.fa-play:before{content:'\f04b'}.fa-play-circle:before{content:'\f144'}.fa-playstation:before{content:'\f3df'}.fa-plug:before{content:'\f1e6'}.fa-plus:before{content:'\f067'}.fa-plus-circle:before{content:'\f055'}.fa-plus-square:before{content:'\f0fe'}.fa-podcast:before{content:'\f2ce'}.fa-poo:before{content:'\f2fe'}.fa-portrait:before{content:'\f3e0'}.fa-pound-sign:before{content:'\f154'}.fa-power-off:before{content:'\f011'}.fa-prescription:before{content:'\f5b1'}.fa-prescription-bottle:before{content:'\f485'}.fa-prescription-bottle-alt:before{content:'\f486'}.fa-print:before{content:'\f02f'}.fa-procedures:before{content:'\f487'}.fa-product-hunt:before{content:'\f288'}.fa-project-diagram:before{content:'\f542'}.fa-pushed:before{content:'\f3e1'}.fa-puzzle-piece:before{content:'\f12e'}.fa-python:before{content:'\f3e2'}.fa-qq:before{content:'\f1d6'}.fa-qrcode:before{content:'\f029'}.fa-question:before{content:'\f128'}.fa-question-circle:before{content:'\f059'}.fa-quidditch:before{content:'\f458'}.fa-quinscape:before{content:'\f459'}.fa-quora:before{content:'\f2c4'}.fa-quote-left:before{content:'\f10d'}.fa-quote-right:before{content:'\f10e'}.fa-r-project:before{content:'\f4f7'}.fa-random:before{content:'\f074'}.fa-ravelry:before{content:'\f2d9'}.fa-react:before{content:'\f41b'}.fa-readme:before{content:'\f4d5'}.fa-rebel:before{content:'\f1d0'}.fa-receipt:before{content:'\f543'}.fa-recycle:before{content:'\f1b8'}.fa-red-river:before{content:'\f3e3'}.fa-reddit:before{content:'\f1a1'}.fa-reddit-alien:before{content:'\f281'}.fa-reddit-square:before{content:'\f1a2'}.fa-redo:before{content:'\f01e'}.fa-redo-alt:before{content:'\f2f9'}.fa-registered:before{content:'\f25d'}.fa-rendact:before{content:'\f3e4'}.fa-renren:before{content:'\f18b'}.fa-reply:before{content:'\f3e5'}.fa-reply-all:before{content:'\f122'}.fa-replyd:before{content:'\f3e6'}.fa-researchgate:before{content:'\f4f8'}.fa-resolving:before{content:'\f3e7'}.fa-retweet:before{content:'\f079'}.fa-ribbon:before{content:'\f4d6'}.fa-road:before{content:'\f018'}.fa-robot:before{content:'\f544'}.fa-rocket:before{content:'\f135'}.fa-rocketchat:before{content:'\f3e8'}.fa-rockrms:before{content:'\f3e9'}.fa-rss:before{content:'\f09e'}.fa-rss-square:before{content:'\f143'}.fa-ruble-sign:before{content:'\f158'}.fa-ruler:before{content:'\f545'}.fa-ruler-combined:before{content:'\f546'}.fa-ruler-horizontal:before{content:'\f547'}.fa-ruler-vertical:before{content:'\f548'}.fa-rupee-sign:before{content:'\f156'}.fa-sad-cry:before{content:'\f5b3'}.fa-sad-tear:before{content:'\f5b4'}.fa-safari:before{content:'\f267'}.fa-sass:before{content:'\f41e'}.fa-save:before{content:'\f0c7'}.fa-schlix:before{content:'\f3ea'}.fa-school:before{content:'\f549'}.fa-screwdriver:before{content:'\f54a'}.fa-scribd:before{content:'\f28a'}.fa-search:before{content:'\f002'}.fa-search-minus:before{content:'\f010'}.fa-search-plus:before{content:'\f00e'}.fa-searchengin:before{content:'\f3eb'}.fa-seedling:before{content:'\f4d8'}.fa-sellcast:before{content:'\f2da'}.fa-sellsy:before{content:'\f213'}.fa-server:before{content:'\f233'}.fa-servicestack:before{content:'\f3ec'}.fa-share:before{content:'\f064'}.fa-share-alt:before{content:'\f1e0'}.fa-share-alt-square:before{content:'\f1e1'}.fa-share-square:before{content:'\f14d'}.fa-shekel-sign:before{content:'\f20b'}.fa-shield-alt:before{content:'\f3ed'}.fa-ship:before{content:'\f21a'}.fa-shipping-fast:before{content:'\f48b'}.fa-shirtsinbulk:before{content:'\f214'}.fa-shoe-prints:before{content:'\f54b'}.fa-shopping-bag:before{content:'\f290'}.fa-shopping-basket:before{content:'\f291'}.fa-shopping-cart:before{content:'\f07a'}.fa-shopware:before{content:'\f5b5'}.fa-shower:before{content:'\f2cc'}.fa-shuttle-van:before{content:'\f5b6'}.fa-sign:before{content:'\f4d9'}.fa-sign-in-alt:before{content:'\f2f6'}.fa-sign-language:before{content:'\f2a7'}.fa-sign-out-alt:before{content:'\f2f5'}.fa-signal:before{content:'\f012'}.fa-signature:before{content:'\f5b7'}.fa-simplybuilt:before{content:'\f215'}.fa-sistrix:before{content:'\f3ee'}.fa-sitemap:before{content:'\f0e8'}.fa-sith:before{content:'\f512'}.fa-skull:before{content:'\f54c'}.fa-skyatlas:before{content:'\f216'}.fa-skype:before{content:'\f17e'}.fa-slack:before{content:'\f198'}.fa-slack-hash:before{content:'\f3ef'}.fa-sliders-h:before{content:'\f1de'}.fa-slideshare:before{content:'\f1e7'}.fa-smile:before{content:'\f118'}.fa-smile-beam:before{content:'\f5b8'}.fa-smile-wink:before{content:'\f4da'}.fa-smoking:before{content:'\f48d'}.fa-smoking-ban:before{content:'\f54d'}.fa-snapchat:before{content:'\f2ab'}.fa-snapchat-ghost:before{content:'\f2ac'}.fa-snapchat-square:before{content:'\f2ad'}.fa-snowflake:before{content:'\f2dc'}.fa-solar-panel:before{content:'\f5ba'}.fa-sort:before{content:'\f0dc'}.fa-sort-alpha-down:before{content:'\f15d'}.fa-sort-alpha-up:before{content:'\f15e'}.fa-sort-amount-down:before{content:'\f160'}.fa-sort-amount-up:before{content:'\f161'}.fa-sort-down:before{content:'\f0dd'}.fa-sort-numeric-down:before{content:'\f162'}.fa-sort-numeric-up:before{content:'\f163'}.fa-sort-up:before{content:'\f0de'}.fa-soundcloud:before{content:'\f1be'}.fa-spa:before{content:'\f5bb'}.fa-space-shuttle:before{content:'\f197'}.fa-speakap:before{content:'\f3f3'}.fa-spinner:before{content:'\f110'}.fa-splotch:before{content:'\f5bc'}.fa-spotify:before{content:'\f1bc'}.fa-spray-can:before{content:'\f5bd'}.fa-square:before{content:'\f0c8'}.fa-square-full:before{content:'\f45c'}.fa-squarespace:before{content:'\f5be'}.fa-stack-exchange:before{content:'\f18d'}.fa-stack-overflow:before{content:'\f16c'}.fa-stamp:before{content:'\f5bf'}.fa-star:before{content:'\f005'}.fa-star-half:before{content:'\f089'}.fa-star-half-alt:before{content:'\f5c0'}.fa-staylinked:before{content:'\f3f5'}.fa-steam:before{content:'\f1b6'}.fa-steam-square:before{content:'\f1b7'}.fa-steam-symbol:before{content:'\f3f6'}.fa-step-backward:before{content:'\f048'}.fa-step-forward:before{content:'\f051'}.fa-stethoscope:before{content:'\f0f1'}.fa-sticker-mule:before{content:'\f3f7'}.fa-sticky-note:before{content:'\f249'}.fa-stop:before{content:'\f04d'}.fa-stop-circle:before{content:'\f28d'}.fa-stopwatch:before{content:'\f2f2'}.fa-store:before{content:'\f54e'}.fa-store-alt:before{content:'\f54f'}.fa-strava:before{content:'\f428'}.fa-stream:before{content:'\f550'}.fa-street-view:before{content:'\f21d'}.fa-strikethrough:before{content:'\f0cc'}.fa-stripe:before{content:'\f429'}.fa-stripe-s:before{content:'\f42a'}.fa-stroopwafel:before{content:'\f551'}.fa-studiovinari:before{content:'\f3f8'}.fa-stumbleupon:before{content:'\f1a4'}.fa-stumbleupon-circle:before{content:'\f1a3'}.fa-subscript:before{content:'\f12c'}.fa-subway:before{content:'\f239'}.fa-suitcase:before{content:'\f0f2'}.fa-suitcase-rolling:before{content:'\f5c1'}.fa-sun:before{content:'\f185'}.fa-superpowers:before{content:'\f2dd'}.fa-superscript:before{content:'\f12b'}.fa-supple:before{content:'\f3f9'}.fa-surprise:before{content:'\f5c2'}.fa-swatchbook:before{content:'\f5c3'}.fa-swimmer:before{content:'\f5c4'}.fa-swimming-pool:before{content:'\f5c5'}.fa-sync:before{content:'\f021'}.fa-sync-alt:before{content:'\f2f1'}.fa-syringe:before{content:'\f48e'}.fa-table:before{content:'\f0ce'}.fa-table-tennis:before{content:'\f45d'}.fa-tablet:before{content:'\f10a'}.fa-tablet-alt:before{content:'\f3fa'}.fa-tablets:before{content:'\f490'}.fa-tachometer-alt:before{content:'\f3fd'}.fa-tag:before{content:'\f02b'}.fa-tags:before{content:'\f02c'}.fa-tape:before{content:'\f4db'}.fa-tasks:before{content:'\f0ae'}.fa-taxi:before{content:'\f1ba'}.fa-teamspeak:before{content:'\f4f9'}.fa-telegram:before{content:'\f2c6'}.fa-telegram-plane:before{content:'\f3fe'}.fa-tencent-weibo:before{content:'\f1d5'}.fa-terminal:before{content:'\f120'}.fa-text-height:before{content:'\f034'}.fa-text-width:before{content:'\f035'}.fa-th:before{content:'\f00a'}.fa-th-large:before{content:'\f009'}.fa-th-list:before{content:'\f00b'}.fa-themeco:before{content:'\f5c6'}.fa-themeisle:before{content:'\f2b2'}.fa-thermometer:before{content:'\f491'}.fa-thermometer-empty:before{content:'\f2cb'}.fa-thermometer-full:before{content:'\f2c7'}.fa-thermometer-half:before{content:'\f2c9'}.fa-thermometer-quarter:before{content:'\f2ca'}.fa-thermometer-three-quarters:before{content:'\f2c8'}.fa-thumbs-down:before{content:'\f165'}.fa-thumbs-up:before{content:'\f164'}.fa-thumbtack:before{content:'\f08d'}.fa-ticket-alt:before{content:'\f3ff'}.fa-times:before{content:'\f00d'}.fa-times-circle:before{content:'\f057'}.fa-tint:before{content:'\f043'}.fa-tint-slash:before{content:'\f5c7'}.fa-tired:before{content:'\f5c8'}.fa-toggle-off:before{content:'\f204'}.fa-toggle-on:before{content:'\f205'}.fa-toolbox:before{content:'\f552'}.fa-tooth:before{content:'\f5c9'}.fa-trade-federation:before{content:'\f513'}.fa-trademark:before{content:'\f25c'}.fa-train:before{content:'\f238'}.fa-transgender:before{content:'\f224'}.fa-transgender-alt:before{content:'\f225'}.fa-trash:before{content:'\f1f8'}.fa-trash-alt:before{content:'\f2ed'}.fa-tree:before{content:'\f1bb'}.fa-trello:before{content:'\f181'}.fa-tripadvisor:before{content:'\f262'}.fa-trophy:before{content:'\f091'}.fa-truck:before{content:'\f0d1'}.fa-truck-loading:before{content:'\f4de'}.fa-truck-moving:before{content:'\f4df'}.fa-tshirt:before{content:'\f553'}.fa-tty:before{content:'\f1e4'}.fa-tumblr:before{content:'\f173'}.fa-tumblr-square:before{content:'\f174'}.fa-tv:before{content:'\f26c'}.fa-twitch:before{content:'\f1e8'}.fa-twitter:before{content:'\f099'}.fa-twitter-square:before{content:'\f081'}.fa-typo3:before{content:'\f42b'}.fa-uber:before{content:'\f402'}.fa-uikit:before{content:'\f403'}.fa-umbrella:before{content:'\f0e9'}.fa-umbrella-beach:before{content:'\f5ca'}.fa-underline:before{content:'\f0cd'}.fa-undo:before{content:'\f0e2'}.fa-undo-alt:before{content:'\f2ea'}.fa-uniregistry:before{content:'\f404'}.fa-universal-access:before{content:'\f29a'}.fa-university:before{content:'\f19c'}.fa-unlink:before{content:'\f127'}.fa-unlock:before{content:'\f09c'}.fa-unlock-alt:before{content:'\f13e'}.fa-untappd:before{content:'\f405'}.fa-upload:before{content:'\f093'}.fa-usb:before{content:'\f287'}.fa-user:before{content:'\f007'}.fa-user-alt:before{content:'\f406'}.fa-user-alt-slash:before{content:'\f4fa'}.fa-user-astronaut:before{content:'\f4fb'}.fa-user-check:before{content:'\f4fc'}.fa-user-circle:before{content:'\f2bd'}.fa-user-clock:before{content:'\f4fd'}.fa-user-cog:before{content:'\f4fe'}.fa-user-edit:before{content:'\f4ff'}.fa-user-friends:before{content:'\f500'}.fa-user-graduate:before{content:'\f501'}.fa-user-lock:before{content:'\f502'}.fa-user-md:before{content:'\f0f0'}.fa-user-minus:before{content:'\f503'}.fa-user-ninja:before{content:'\f504'}.fa-user-plus:before{content:'\f234'}.fa-user-secret:before{content:'\f21b'}.fa-user-shield:before{content:'\f505'}.fa-user-slash:before{content:'\f506'}.fa-user-tag:before{content:'\f507'}.fa-user-tie:before{content:'\f508'}.fa-user-times:before{content:'\f235'}.fa-users:before{content:'\f0c0'}.fa-users-cog:before{content:'\f509'}.fa-ussunnah:before{content:'\f407'}.fa-utensil-spoon:before{content:'\f2e5'}.fa-utensils:before{content:'\f2e7'}.fa-vaadin:before{content:'\f408'}.fa-vector-square:before{content:'\f5cb'}.fa-venus:before{content:'\f221'}.fa-venus-double:before{content:'\f226'}.fa-venus-mars:before{content:'\f228'}.fa-viacoin:before{content:'\f237'}.fa-viadeo:before{content:'\f2a9'}.fa-viadeo-square:before{content:'\f2aa'}.fa-vial:before{content:'\f492'}.fa-vials:before{content:'\f493'}.fa-viber:before{content:'\f409'}.fa-video:before{content:'\f03d'}.fa-video-slash:before{content:'\f4e2'}.fa-vimeo:before{content:'\f40a'}.fa-vimeo-square:before{content:'\f194'}.fa-vimeo-v:before{content:'\f27d'}.fa-vine:before{content:'\f1ca'}.fa-vk:before{content:'\f189'}.fa-vnv:before{content:'\f40b'}.fa-volleyball-ball:before{content:'\f45f'}.fa-volume-down:before{content:'\f027'}.fa-volume-off:before{content:'\f026'}.fa-volume-up:before{content:'\f028'}.fa-vuejs:before{content:'\f41f'}.fa-walking:before{content:'\f554'}.fa-wallet:before{content:'\f555'}.fa-warehouse:before{content:'\f494'}.fa-weebly:before{content:'\f5cc'}.fa-weibo:before{content:'\f18a'}.fa-weight:before{content:'\f496'}.fa-weight-hanging:before{content:'\f5cd'}.fa-weixin:before{content:'\f1d7'}.fa-whatsapp:before{content:'\f232'}.fa-whatsapp-square:before{content:'\f40c'}.fa-wheelchair:before{content:'\f193'}.fa-whmcs:before{content:'\f40d'}.fa-wifi:before{content:'\f1eb'}.fa-wikipedia-w:before{content:'\f266'}.fa-window-close:before{content:'\f410'}.fa-window-maximize:before{content:'\f2d0'}.fa-window-minimize:before{content:'\f2d1'}.fa-window-restore:before{content:'\f2d2'}.fa-windows:before{content:'\f17a'}.fa-wine-glass:before{content:'\f4e3'}.fa-wine-glass-alt:before{content:'\f5ce'}.fa-wix:before{content:'\f5cf'}.fa-wolf-pack-battalion:before{content:'\f514'}.fa-won-sign:before{content:'\f159'}.fa-wordpress:before{content:'\f19a'}.fa-wordpress-simple:before{content:'\f411'}.fa-wpbeginner:before{content:'\f297'}.fa-wpexplorer:before{content:'\f2de'}.fa-wpforms:before{content:'\f298'}.fa-wrench:before{content:'\f0ad'}.fa-x-ray:before{content:'\f497'}.fa-xbox:before{content:'\f412'}.fa-xing:before{content:'\f168'}.fa-xing-square:before{content:'\f169'}.fa-y-combinator:before{content:'\f23b'}.fa-yahoo:before{content:'\f19e'}.fa-yandex:before{content:'\f413'}.fa-yandex-international:before{content:'\f414'}.fa-yelp:before{content:'\f1e9'}.fa-yen-sign:before{content:'\f157'}.fa-yoast:before{content:'\f2b1'}.fa-youtube:before{content:'\f167'}.fa-youtube-square:before{content:'\f431'}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} +.fa,.fab,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} \ No newline at end of file diff --git a/ghci.sh b/ghci.sh index 77391583f..ab5cf41bd 100755 --- a/ghci.sh +++ b/ghci.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +set -e + +[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || : + unset HOST export DETAILED_LOGGING=true export LOG_ALL=true diff --git a/haddock.sh b/haddock.sh index 7414e60e8..13bb626e0 100755 --- a/haddock.sh +++ b/haddock.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +set -e + +[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || : + move-back() { mv -v .stack-work .stack-work-doc [[ -d .stack-work-build ]] && mv -v .stack-work-build .stack-work diff --git a/hlint.sh b/hlint.sh index 74a2a9fb7..c6772fda7 100755 --- a/hlint.sh +++ b/hlint.sh @@ -1,3 +1,7 @@ #!/usr/bin/env bash +set -e + +[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || : + exec -- stack build --test --fast --flag uniworx:dev --flag uniworx:library-only uniworx:test:hlint diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg index ab4cb18fc..ef0003579 100644 --- a/messages/uniworx/de.msg +++ b/messages/uniworx/de.msg @@ -705,7 +705,7 @@ NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr abgeben NotificationTriggerSheetInactive: Abgabezeitraum eines meiner Übungsblätter ist abgelaufen NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt -NotificationTriggerCorrectionsNotDistributed: Abgaben eines meiner Übungsblätter konnten keinem Korrektur zugeteilt werden +NotificationTriggerCorrectionsNotDistributed: Nicht alle Abgaben eines meiner Übungsblätter konnten einem Korrektor zugeteilt werden NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert CorrCreate: Abgaben erstellen @@ -1171,3 +1171,10 @@ VersionHistory: Versionsgeschichte KnownBugs: Bekannte Bugs ExamUsersHeading: Klausurteilnehmer + +CsvFile: CSV-Datei +CsvModifyExisting: Existierende Einträge angleichen +CsvAddNew: Neue Einträge einfügen +CsvDeleteMissing: Fehlende Einträge entfernen +BtnCsvExport: CSV-Datei exportieren +BtnCsvImport: CSV-Datei importieren \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d6f8ba499..0478626f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "1.4.1", + "version": "2.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b1be3bc94..4d88cf8bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "1.4.1", + "version": "2.1.1", "description": "", "keywords": [], "author": "", @@ -9,10 +9,12 @@ "start": "run-p frontend:build:watch yesod:start", "test": "run-s frontend:test yesod:test", "lint": "run-s frontend:lint yesod:lint", + "build": "run-s frontend:build yesod:build", "yesod:db": "./db.sh", "yesod:start": "./start.sh", "yesod:lint": "./hlint.sh", "yesod:test": "./test.sh", + "yesod:build": "./build.sh", "frontend:lint": "eslint frontend/src", "frontend:test": "karma start --conf karma.conf.js", "frontend:test:watch": "karma start --conf karma.conf.js --single-run false", diff --git a/package.yaml b/package.yaml index ef2ddeefb..edeaae4b1 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 1.4.1 +version: 2.1.1 dependencies: # Due to a bug in GHC 8.0.1, we block its usage @@ -32,6 +32,7 @@ dependencies: - data-default - aeson >=0.6 && <1.3 - conduit >=1.0 && <2.0 + - conduit-combinators - monad-logger >=0.3 && <0.4 - fast-logger >=2.2 && <2.5 - wai-logger >=2.2 && <2.4 @@ -129,6 +130,9 @@ dependencies: - hourglass - unix - stm-delay + - cassava + - cassava-conduit + - constraints other-extensions: - GeneralizedNewtypeDeriving diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs index bd8120ba7..cc8ffbb24 100644 --- a/src/Database/Esqueleto/Utils.hs +++ b/src/Database/Esqueleto/Utils.hs @@ -7,7 +7,7 @@ module Database.Esqueleto.Utils , any, all , SqlIn(..) , mkExactFilter, mkExactFilterWith - , mkContainsFilter + , mkContainsFilter, mkContainsFilterWith , mkExistsFilter , anyFilter, allFilter ) where @@ -40,12 +40,18 @@ isJust :: (E.Esqueleto query expr backend, PersistField typ) => expr (E.Value (M isJust = E.not_ . E.isNothing -- | Check if the first string is contained in the text derived from the second argument -isInfixOf :: (E.Esqueleto query expr backend, E.SqlString s2) => - Text -> expr (E.Value s2) -> expr (E.Value Bool) -isInfixOf needle strExpr = E.castString strExpr `E.ilike` (E.%) E.++. E.val needle E.++. (E.%) +isInfixOf :: ( E.Esqueleto query expr backend + , E.SqlString s1 + , E.SqlString s2 + ) + => expr (E.Value s1) -> expr (E.Value s2) -> expr (E.Value Bool) +isInfixOf needle strExpr = E.castString strExpr `E.ilike` (E.%) E.++. needle E.++. (E.%) -hasInfix :: (E.Esqueleto query expr backend, E.SqlString s2) => - expr (E.Value s2) -> Text -> expr (E.Value Bool) +hasInfix :: ( E.Esqueleto query expr backend + , E.SqlString s1 + , E.SqlString s2 + ) + => expr (E.Value s2) -> expr (E.Value s1) -> expr (E.Value Bool) hasInfix = flip isInfixOf -- | Given a test and a set of values, check whether anyone succeeds the test @@ -101,14 +107,23 @@ mkExactFilterWith cast lenslike row criterias -- | generic filter creation for dbTable -- Given a lens-like function, make filter searching for needles in String-like elements -- (Keep Set here to ensure that there are no duplicates) -mkContainsFilter :: (E.SqlString a) - => (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element - -> t -- ^ query row - -> Set.Set Text -- ^ needle collection - -> E.SqlExpr (E.Value Bool) -mkContainsFilter lenslike row criterias +mkContainsFilter :: E.SqlString a + => (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element + -> t -- ^ query row + -> Set.Set a -- ^ needle collection + -> E.SqlExpr (E.Value Bool) +mkContainsFilter = mkContainsFilterWith id + +-- | like `mkContainsFiler` but allows for conversion; convenient in conjunction with `anyFilter` and `allFilter` +mkContainsFilterWith :: E.SqlString b + => (a -> b) + -> (t -> E.SqlExpr (E.Value b)) -- ^ getter from query to searched element + -> t -- ^ query row + -> Set.Set a -- ^ needle collection + -> E.SqlExpr (E.Value Bool) +mkContainsFilterWith cast lenslike row criterias | Set.null criterias = true - | otherwise = any (hasInfix $ lenslike row) criterias + | otherwise = any (hasInfix $ lenslike row) (E.val . cast <$> Set.toList criterias) mkExistsFilter :: PathPiece a => (t -> a -> E.SqlQuery ()) diff --git a/src/Foundation.hs b/src/Foundation.hs index d49734af3..f16c9dc79 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -689,7 +689,7 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of _ -> return () return Authorized - + CTutorialR tid ssh csh tutn TRegisterR -> maybeT (unauthorizedI MsgUnauthorizedTutorialTime) $ do now <- liftIO getCurrentTime course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh @@ -2374,6 +2374,14 @@ pageActions (CSheetR tid ssh csh shn SCorrR) = , menuItemModal = False , menuItemAccessCallback' = return True } + , MenuItem + { menuItemType = PageActionPrime + , menuItemLabel = MsgMenuCorrectionsAssign + , menuItemIcon = Nothing + , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SAssignR + , menuItemModal = False + , menuItemAccessCallback' = return True + } , MenuItem { menuItemType = PageActionSecondary , menuItemLabel = MsgMenuSheetEdit diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index 1b6242611..a2f4eafa3 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -388,6 +388,8 @@ postAdminFeaturesR = do } psValidator = def -- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"] & defaultSorting [SortAscBy "key"] + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing in dbTable psValidator DBTable{..} mkStudytermsTable :: Set (Key StudyTerms) -> Set (Key StudyTerms) -> DB (FormResult (DBFormResult (Key StudyTerms) (Maybe Text, Maybe Text) (DBRow (Entity StudyTerms))), Widget) @@ -421,6 +423,8 @@ postAdminFeaturesR = do psValidator = def -- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"] & defaultSorting [SortDescBy "isnew", SortDescBy "isbad", SortAscBy "key"] + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing in dbTable psValidator DBTable{..} mkCandidateTable = @@ -454,5 +458,7 @@ postAdminFeaturesR = do ] dbtParams = def psValidator = def & defaultSorting [SortAscBy "incidence", SortAscBy "key", SortAscBy "name"] + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing in dbTable psValidator DBTable{..} diff --git a/src/Handler/Corrections.hs b/src/Handler/Corrections.hs index 5a9ef3796..f1d5085a5 100644 --- a/src/Handler/Corrections.hs +++ b/src/Handler/Corrections.hs @@ -348,9 +348,9 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d ) , ( "corrector-name-email" -- corrector filter does not work for text-filtering , FilterColumn $ E.anyFilter - [ E.mkContainsFilter $ queryCorrector >>> (E.?. UserSurname) - , E.mkContainsFilter $ queryCorrector >>> (E.?. UserDisplayName) - , E.mkContainsFilter $ queryCorrector >>> (E.?. UserEmail) + [ E.mkContainsFilterWith Just $ queryCorrector >>> (E.?. UserSurname) + , E.mkContainsFilterWith Just $ queryCorrector >>> (E.?. UserDisplayName) + , E.mkContainsFilterWith (Just . CI.mk) $ queryCorrector >>> (E.?. UserEmail) ] ) , ( "user-name-email" @@ -360,7 +360,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d E.where_ $ (\f -> f user $ Set.singleton needle) $ E.anyFilter [ E.mkContainsFilter (E.^. UserSurname) , E.mkContainsFilter (E.^. UserDisplayName) - , E.mkContainsFilter (E.^. UserEmail) + , E.mkContainsFilterWith CI.mk (E.^. UserEmail) ] ) , ( "user-matriclenumber" @@ -380,6 +380,8 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d , dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (\_ -> defaultDBSFilterLayout) dbtFilterUI } , dbtParams , dbtIdent = "corrections" :: Text + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } data ActionCorrections = CorrDownload @@ -564,6 +566,8 @@ assignAction selId = ( CorrSetCorrector E.where_ $ either (\cId -> course E.^. CourseId E.==. E.val cId) (\shId -> sheet E.^. SheetId E.==. E.val shId) selId + E.orderBy $ [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName] + E.distinct $ return user correctors' <- forM correctors $ \Entity{ entityKey, entityVal = User{..} } -> (SomeMessage userDisplayName, ) <$> encrypt entityKey @@ -1136,17 +1140,24 @@ assignHandler tid ssh csh cid assignSids = do E.on $ corrector E.^. SheetCorrectorUser E.==. user E.^. UserId E.where_ $ corrector E.^. SheetCorrectorSheet `E.in_` E.valList sheetIds return (corrector, user) - let correctorMap :: Map UserId (User, Map SheetName SheetCorrector) - correctorMap = (\f -> foldl f Map.empty correctors) (\acc (Entity _ sheetcorr@SheetCorrector{sheetCorrectorSheet}, Entity uid user) -> - let shn = sheetName $ sheets ! sheetCorrectorSheet - in Map.insertWith (\(usr, ma) (_, mb) -> (usr, Map.union ma mb)) uid (user, Map.singleton shn sheetcorr) acc + let correctorMap' :: Map UserId (User, Map SheetName SheetCorrector) + correctorMap' = (\f -> foldl f Map.empty correctors) + (\acc (Entity _ sheetcorr@SheetCorrector{sheetCorrectorSheet}, Entity uid user) -> + let shn = sheetName $ sheets ! sheetCorrectorSheet + in Map.insertWith (\(usr, ma) (_, mb) -> (usr, Map.union ma mb)) uid (user, Map.singleton shn sheetcorr) acc + ) + -- Lecturers may correct without being enlisted SheetCorrectors, so fetch all names + act_correctors <- E.select . E.distinct . E.from $ \(submission `E.InnerJoin` user) -> do + E.on $ submission E.^. SubmissionRatingBy E.==. (E.just $ user E.^. UserId) + E.where_ $ submission E.^. SubmissionSheet `E.in_` E.valList sheetIds + return (submission E.^. SubmissionSheet, user) + let correctorMap :: Map UserId (User, Map SheetName SheetCorrector) + correctorMap = (\f -> foldl f correctorMap' act_correctors) + (\acc (E.Value sheetCorrectorSheet, Entity uid user) -> + let shn = sheetName $ sheets ! sheetCorrectorSheet + scr = SheetCorrector uid sheetCorrectorSheet mempty CorrectorExcused + in Map.insertWith (\_new old -> old) uid (user, Map.singleton shn scr) acc -- keep already known correctors unchanged ) - - -- -- lecturerNames :: Map UserId User - -- lecturerNames <- fmap entities2map $ E.select $ E.from $ \(user `E.InnerJoin` lecturer) -> do - -- E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser - -- E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid - -- return user submissions <- E.select . E.from $ \submission -> do E.where_ $ submission E.^. SubmissionSheet `E.in_` E.valList sheetIds diff --git a/src/Handler/Course.hs b/src/Handler/Course.hs index 3340c6894..404338e73 100644 --- a/src/Handler/Course.hs +++ b/src/Handler/Course.hs @@ -205,6 +205,8 @@ makeCourseTable whereClause colChoices psValidator = do , dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } , dbtParams = def , dbtIdent = "courses" :: Text + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } getCourseListR :: Handler Html @@ -402,6 +404,8 @@ getCShowR tid ssh csh = do dbtParams = def dbtIdent :: Text dbtIdent = "tutorials" + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing tutorialDBTableValidator = def & defaultSorting [SortAscBy "type", SortAscBy "name"] @@ -459,6 +463,8 @@ getCShowR tid ssh csh = do dbtParams = def dbtIdent :: Text dbtIdent = "exams" + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing examDBTableValidator = def & defaultSorting [SortAscBy "time"] @@ -1140,13 +1146,13 @@ makeCourseUserTable cid restrict colChoices psValidator = do , ("field-short" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand)) , ("field-key" , FilterColumn $ E.mkExactFilter $ queryFeaturesField >>> (E.?. StudyTermsKey)) , ("field" , FilterColumn $ E.anyFilter - [ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsName) - , E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand) + [ E.mkContainsFilterWith Just $ queryFeaturesField >>> E.joinV . (E.?. StudyTermsName) + , E.mkContainsFilterWith Just $ queryFeaturesField >>> E.joinV . (E.?. StudyTermsShorthand) , E.mkExactFilterWith readMay $ queryFeaturesField >>> (E.?. StudyTermsKey) ] ) , ("degree" , FilterColumn $ E.anyFilter - [ E.mkContainsFilter $ queryFeaturesDegree >>> (E.?. StudyDegreeName) - , E.mkContainsFilter $ queryFeaturesDegree >>> (E.?. StudyDegreeShorthand) + [ E.mkContainsFilterWith Just $ queryFeaturesDegree >>> E.joinV . (E.?. StudyDegreeName) + , E.mkContainsFilterWith Just $ queryFeaturesDegree >>> E.joinV . (E.?. StudyDegreeShorthand) , E.mkExactFilterWith readMay $ queryFeaturesDegree >>> (E.?. StudyDegreeKey) ] ) , ("semesternr" , FilterColumn $ E.mkExactFilter $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester)) @@ -1154,7 +1160,7 @@ makeCourseUserTable cid restrict colChoices psValidator = do E.from $ \(tutorial `E.InnerJoin` tutorialParticipant) -> do E.on $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid - E.&&. E.hasInfix (tutorial E.^. TutorialName) criterion + E.&&. E.hasInfix (tutorial E.^. TutorialName) (E.val criterion :: E.SqlExpr (E.Value (CI Text))) E.&&. tutorialParticipant E.^. TutorialParticipantUser E.==. queryUser row E.^. UserId ) -- , ("course-registration", error "TODO") -- TODO @@ -1181,6 +1187,8 @@ makeCourseUserTable cid restrict colChoices psValidator = do , dbParamsFormResult = id , dbParamsFormIdent = def } + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing over _1 postprocess <$> dbTable psValidator DBTable{..} where postprocess :: FormResult (First act, DBFormResult UserId Bool UserTableData) -> FormResult (act, Set UserId) diff --git a/src/Handler/Exam.hs b/src/Handler/Exam.hs index c93195d9b..1758e3ffa 100644 --- a/src/Handler/Exam.hs +++ b/src/Handler/Exam.hs @@ -9,6 +9,7 @@ import Handler.Utils.Exam import Handler.Utils.Invitations import Handler.Utils.Table.Columns import Handler.Utils.Table.Cells +import Handler.Utils.Csv import Jobs.Queue import Utils.Lens hiding (parts) @@ -29,6 +30,10 @@ import qualified Data.CaseInsensitive as CI import qualified Control.Monad.State.Class as State +import qualified Data.Csv as Csv + +import qualified Data.Conduit.List as C + getCExamListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCExamListR tid ssh csh = do @@ -74,6 +79,8 @@ getCExamListR tid ssh csh = do dbtParams = def dbtIdent :: Text dbtIdent = "exams" + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing examDBTableValidator = def & defaultSorting [SortAscBy "time"] @@ -733,8 +740,8 @@ getEShowR tid ssh csh examn = do examBonusW bonusRule = $(widgetFile "widgets/bonusRule") $(widgetFile "exam-show") -type ExamUserTableExpr = (E.SqlExpr (Entity ExamRegistration) `E.InnerJoin` E.SqlExpr (Entity User)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamOccurrence)) -type ExamUserTableData = DBRow (Entity ExamRegistration, Entity User, Maybe (Entity ExamOccurrence)) +type ExamUserTableExpr = (E.SqlExpr (Entity ExamRegistration) `E.InnerJoin` E.SqlExpr (Entity User)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamOccurrence)) `E.LeftOuterJoin` (E.SqlExpr (Maybe (Entity CourseParticipant)) `E.LeftOuterJoin` (E.SqlExpr (Maybe (Entity StudyFeatures)) `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree)) `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms)))) +type ExamUserTableData = DBRow (Entity ExamRegistration, Entity User, Maybe (Entity ExamOccurrence), Maybe (Entity StudyFeatures), Maybe (Entity StudyDegree), Maybe (Entity StudyTerms)) instance HasEntity ExamUserTableData User where hasEntity = _dbrOutput . _2 @@ -746,47 +753,124 @@ _userTableOccurrence :: Lens' ExamUserTableData (Maybe (Entity ExamOccurrence)) _userTableOccurrence = _dbrOutput . _3 queryUser :: ExamUserTableExpr -> E.SqlExpr (Entity User) -queryUser = $(sqlIJproj 2 2) . $(sqlLOJproj 2 1) +queryUser = $(sqlIJproj 2 2) . $(sqlLOJproj 3 1) + +queryStudyFeatures :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures)) +queryStudyFeatures = $(sqlIJproj 3 1) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3) queryExamRegistration :: ExamUserTableExpr -> E.SqlExpr (Entity ExamRegistration) -queryExamRegistration = $(sqlIJproj 2 1) . $(sqlLOJproj 2 1) +queryExamRegistration = $(sqlIJproj 2 1) . $(sqlLOJproj 3 1) + +queryStudyDegree :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree)) +queryStudyDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3) + +queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms)) +queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3) + +resultUser :: Lens' ExamUserTableData (Entity User) +resultUser = _dbrOutput . _2 + +resultStudyFeatures :: Traversal' ExamUserTableData (Entity StudyFeatures) +resultStudyFeatures = _dbrOutput . _4 . _Just + +resultStudyDegree :: Traversal' ExamUserTableData (Entity StudyDegree) +resultStudyDegree = _dbrOutput . _5 . _Just + +resultStudyField :: Traversal' ExamUserTableData (Entity StudyTerms) +resultStudyField = _dbrOutput . _6 . _Just + +resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence) +resultExamOccurrence = _dbrOutput . _3 . _Just + +data ExamUserTableCsv = ExamUserTableCsv + { csvUserSurname :: Text + , csvUserName :: Text + , csvUserMatriculation :: Maybe Text + , csvUserField :: Maybe Text + , csvUserDegree :: Maybe Text + , csvUserSemester :: Maybe Int + , csvUserRoom :: Maybe Text + } + deriving (Generic) + +examUserTableCsvOptions :: Csv.Options +examUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 1 } + +instance ToNamedRecord ExamUserTableCsv where + toNamedRecord = Csv.genericToNamedRecord examUserTableCsvOptions + +instance FromNamedRecord ExamUserTableCsv where + parseNamedRecord = Csv.genericParseNamedRecord examUserTableCsvOptions + +instance DefaultOrdered ExamUserTableCsv where + headerOrder = Csv.genericHeaderOrder examUserTableCsvOptions getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEUsersR = postEUsersR postEUsersR tid ssh csh examn = do - eid <- runDB $ fetchExamId tid ssh csh examn + Entity eid Exam{..} <- runDB $ fetchExam tid ssh csh examn let examUsersDBTable = DBTable{..} where - dbtSQLQuery ((examRegistration `E.InnerJoin` user) `E.LeftOuterJoin` occurrence) = do + dbtSQLQuery ((examRegistration `E.InnerJoin` user) `E.LeftOuterJoin` occurrence `E.LeftOuterJoin` (courseParticipant `E.LeftOuterJoin` (studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyField))) = do + E.on $ studyField E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField + E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree + E.on $ studyFeatures E.?. StudyFeaturesId E.==. E.joinV (courseParticipant E.?. CourseParticipantField) + E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse) + E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId) E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid) E.&&. occurrence E.?. ExamOccurrenceId E.==. examRegistration E.^. ExamRegistrationOccurrence E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eid - return (examRegistration, user, occurrence) + return (examRegistration, user, occurrence, studyFeatures, studyDegree, studyField) dbtRowKey = queryExamRegistration >>> (E.^. ExamRegistrationId) dbtProj = return dbtColonnade = dbColonnade $ mconcat [ colUserNameLink (CourseR tid ssh csh . CUserR) , colUserMatriclenr - -- , colUserDegreeShort - -- , colUserField - -- , colUserSemester + , colField resultStudyField + , colDegreeShort resultStudyDegree + , colFeaturesSemester resultStudyFeatures , sortable (Just "room") (i18nCell MsgExamRoom) (maybe mempty (cell . toWgt . examOccurrenceRoom . entityVal) . view _userTableOccurrence) ] dbtSorting = Map.fromList - [ sortUserNameLink queryUser - , sortUserSurname queryUser - , sortUserDisplayName queryUser - , sortUserMatriclenr queryUser + [ sortUserNameLink queryUser + , sortUserSurname queryUser + , sortUserDisplayName queryUser + , sortUserMatriclenr queryUser + , sortField queryStudyField + , sortDegreeShort queryStudyDegree + , sortFeaturesSemester queryStudyFeatures ] - dbtFilter = Map.empty - dbtFilterUI = const mempty - dbtStyle = def + dbtFilter = Map.fromList + [ fltrUserNameEmail queryUser + , fltrUserMatriclenr queryUser + , fltrField queryStudyField + , fltrDegree queryStudyDegree + , fltrFeaturesSemester queryStudyFeatures + ] + dbtFilterUI mPrev = mconcat + [ fltrUserNameEmailUI mPrev + , fltrUserMatriclenrUI mPrev + , fltrFieldUI mPrev + , fltrDegreeUI mPrev + , fltrFeaturesSemesterUI mPrev + ] + dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } dbtParams = def dbtIdent :: Text dbtIdent = "exam-users" + dbtCsvEncode :: DBTCsvEncode ExamUserTableData ExamUserTableCsv + dbtCsvEncode = DictJust . C.map $ ExamUserTableCsv + <$> view (resultUser . _entityVal . _userSurname) + <*> view (resultUser . _entityVal . _userDisplayName) + <*> view (resultUser . _entityVal . _userMatrikelnummer) + <*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just) + <*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just) + <*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester) + <*> preview (resultExamOccurrence . _entityVal . _examOccurrenceRoom) + dbtCsvDecode = Nothing examUsersDBTableValidator = def ((), examUsersTable) <- runDB $ dbTable examUsersDBTableValidator examUsersDBTable diff --git a/src/Handler/Home.hs b/src/Handler/Home.hs index 53cde3d91..7103afe14 100644 --- a/src/Handler/Home.hs +++ b/src/Handler/Home.hs @@ -80,6 +80,8 @@ homeOpenCourses = do , dbtStyle = def , dbtParams = def , dbtIdent = "open-courses" :: Text + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } $(widgetFile "home/openCourses") @@ -179,6 +181,8 @@ homeUpcomingSheets uid = do , dbtStyle = def { dbsEmptyStyle = DBESNoHeading, dbsEmptyMessage = MsgNoUpcomingSheetDeadlines } , dbtParams = def , dbtIdent = "upcoming-sheets" :: Text + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } $(widgetFile "home/upcomingSheets") @@ -286,6 +290,8 @@ homeUpcomingExams uid = do dbtParams = def dbtIdent :: Text dbtIdent = "exams" + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing examDBTableValidator = def & defaultSorting [SortAscBy "time"] diff --git a/src/Handler/Material.hs b/src/Handler/Material.hs index dbf6c8bad..3ff0c1349 100644 --- a/src/Handler/Material.hs +++ b/src/Handler/Material.hs @@ -149,6 +149,8 @@ getMaterialListR tid ssh csh = do ] , dbtFilter = mempty , dbtFilterUI = mempty + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } let headingLong = prependCourseTitle tid ssh csh MsgMaterialListHeading @@ -219,6 +221,8 @@ getMShowR tid ssh csh mnm = do [ sortFilePath $(sqlIJproj 2 2) , sortFileModification $(sqlIJproj 2 2) ] + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } return (matEnt,fileTable') diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs index 783752808..8afac65ce 100644 --- a/src/Handler/Profile.hs +++ b/src/Handler/Profile.hs @@ -258,6 +258,8 @@ mkOwnedCoursesTable = ] dbtFilterUI = mempty dbtParams = def + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..} @@ -308,6 +310,8 @@ mkEnrolledCoursesTable = , dbtFilterUI = mempty , dbtStyle = def , dbtParams = def + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } @@ -387,6 +391,8 @@ mkSubmissionTable = ] dbtFilterUI = mempty dbtParams = def + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing in \uid -> let dbtSQLQuery = dbtSQLQuery' uid dbtSorting = dbtSorting' uid in dbTableWidget' validator DBTable{..} @@ -459,6 +465,8 @@ mkSubmissionGroupTable = ] dbtFilterUI = mempty dbtParams = def + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in dbTableWidget' validator DBTable{..} @@ -535,6 +543,8 @@ mkCorrectionsTable = ] dbtFilterUI = mempty dbtParams = def + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in dbTableWidget' validator DBTable{..} diff --git a/src/Handler/Sheet.hs b/src/Handler/Sheet.hs index 946e0395f..df31ec398 100644 --- a/src/Handler/Sheet.hs +++ b/src/Handler/Sheet.hs @@ -310,6 +310,8 @@ getSheetListR tid ssh csh = do , dbtStyle = def , dbtParams = def , dbtIdent = "sheets" :: Text + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } -- ) ( -- !!!DEPRECTAED!!! Summary only over shown rows !!! -- -- Collect summary over all Sheets, not just the ones shown due to pagination: @@ -404,6 +406,8 @@ getSShowR tid ssh csh shn = do ) ] , dbtParams = def + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } (hasHints, hasSolution) <- runDB $ do hasHints <- (/= 0) <$> count [ SheetFileSheet ==. sid, SheetFileType ==. SheetHint ] @@ -731,6 +735,7 @@ correctorForm shid = wFormToAForm $ do E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet E.on $ sheetCorrector E.^. SheetCorrectorUser E.==. user E.^. UserId E.where_ $ lecturer E.^. LecturerUser E.==. E.val userId + E.orderBy $ [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName] return user miAdd :: ListPosition diff --git a/src/Handler/Submission.hs b/src/Handler/Submission.hs index 0fe085dc1..6dd006d40 100644 --- a/src/Handler/Submission.hs +++ b/src/Handler/Submission.hs @@ -520,6 +520,8 @@ submissionHelper tid ssh csh shn mcid = do , dbtFilter = mempty , dbtFilterUI = mempty , dbtParams = def + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } mFileTable <- traverse (runDB . dbTableWidget' def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid diff --git a/src/Handler/SystemMessage.hs b/src/Handler/SystemMessage.hs index 48a0a9337..273e33d6d 100644 --- a/src/Handler/SystemMessage.hs +++ b/src/Handler/SystemMessage.hs @@ -224,6 +224,8 @@ postMessageListR = do , dbParamsFormIdent = def } , dbtIdent = "messages" :: Text + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } let tableRes = tableRes' & mapped._2 %~ Map.keysSet . Map.filter id . getDBFormResult (const False) diff --git a/src/Handler/Term.hs b/src/Handler/Term.hs index f2e27c298..64a85bfef 100644 --- a/src/Handler/Term.hs +++ b/src/Handler/Term.hs @@ -145,6 +145,8 @@ getTermShowR = do , dbtStyle = def , dbtParams = def , dbtIdent = "terms" :: Text + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } defaultLayout $ do setTitleI MsgTermsHeading diff --git a/src/Handler/Tutorial.hs b/src/Handler/Tutorial.hs index 07c6bd181..1f56bcc8d 100644 --- a/src/Handler/Tutorial.hs +++ b/src/Handler/Tutorial.hs @@ -93,6 +93,8 @@ getCTutorialListR tid ssh csh = do dbtParams = def dbtIdent :: Text dbtIdent = "tutorials" + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing tutorialDBTableValidator = def & defaultSorting [SortAscBy "type", SortAscBy "name"] diff --git a/src/Handler/Users.hs b/src/Handler/Users.hs index 01b0055d9..30470cf3a 100644 --- a/src/Handler/Users.hs +++ b/src/Handler/Users.hs @@ -108,14 +108,14 @@ getUsersR = do ) ] , dbtFilter = Map.fromList -- OverloadedLists does not work with the templates - [ ( "user-search", FilterColumn $ \user criterion -> - if Set.null criterion then E.true else -- TODO: why is this condition not needed? + [ ( "user-search", FilterColumn $ \user (criteria :: Set.Set Text) -> + if Set.null criteria then E.true else -- TODO: why is this condition not needed? -- Set.foldr (\needle acc -> acc E.||. (user E.^. UserDisplayName) `E.hasInfix` needle) eFalse (criterion :: Set.Set Text) - E.any (user E.^. UserDisplayName `E.hasInfix`) criterion + E.any (\c -> user E.^. UserDisplayName `E.hasInfix` E.val c) criteria ) - , ( "matriculation", FilterColumn $ \user (criterion :: Set.Set Text) -> if - | Set.null criterion -> E.true -- TODO: why can this be eFalse and work still? - | otherwise -> E.any (user E.^. UserMatrikelnummer `E.hasInfix`) criterion + , ( "matriculation", FilterColumn $ \user (criteria :: Set.Set Text) -> if + | Set.null criteria -> E.true -- TODO: why can this be eFalse and work still? + | otherwise -> E.any (\c -> user E.^. UserMatrikelnummer `E.hasInfix` E.val c) criteria ) , ( "school", FilterColumn $ \user criterion -> if | Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool) @@ -140,6 +140,8 @@ getUsersR = do , dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } , dbtParams = def , dbtIdent = "users" :: Text + , dbtCsvEncode = noCsvEncode + , dbtCsvDecode = Nothing } defaultLayout $ do diff --git a/src/Handler/Utils.hs b/src/Handler/Utils.hs index ed7682772..155774b6f 100644 --- a/src/Handler/Utils.hs +++ b/src/Handler/Utils.hs @@ -31,6 +31,7 @@ import Handler.Utils.Rating as Handler.Utils hiding (extractRatings) -- import Handler.Utils.Submission as Handler.Utils import Handler.Utils.Sheet as Handler.Utils import Handler.Utils.Mail as Handler.Utils +import Handler.Utils.ContentDisposition as Handler.Utils import System.Directory (listDirectory) import System.FilePath.Posix (takeBaseName, takeFileName) @@ -41,21 +42,6 @@ import qualified Data.List.NonEmpty as NonEmpty import Control.Monad.Logger --- | Check whether the user's preference for files is inline-viewing or downloading -downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool -downloadFiles = do - mauth <- liftHandlerT maybeAuth - case mauth of - Just (Entity _ User{..}) -> return userDownloadFiles - Nothing -> do - UserDefaultConf{..} <- getsYesod $ view _appUserDefaults - return userDefaultDownloadFiles - -setContentDisposition' :: (MonadHandler m, HandlerSite m ~ UniWorX) => Maybe FilePath -> m () -setContentDisposition' mFileName = do - wantsDownload <- downloadFiles - setContentDisposition (bool ContentInline ContentAttachment wantsDownload) mFileName - -- | Simply send a `File`-Value sendThisFile :: File -> Handler TypedContent sendThisFile File{..} diff --git a/src/Handler/Utils/ContentDisposition.hs b/src/Handler/Utils/ContentDisposition.hs new file mode 100644 index 000000000..7be2bd81b --- /dev/null +++ b/src/Handler/Utils/ContentDisposition.hs @@ -0,0 +1,24 @@ +module Handler.Utils.ContentDisposition + ( downloadFiles + , setContentDisposition' + ) where + +import Import + +import Utils.Lens + +-- | Check whether the user's preference for files is inline-viewing or downloading +downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool +downloadFiles = do + mauth <- liftHandlerT maybeAuth + case mauth of + Just (Entity _ User{..}) -> return userDownloadFiles + Nothing -> do + UserDefaultConf{..} <- getsYesod $ view _appUserDefaults + return userDefaultDownloadFiles + +setContentDisposition' :: (MonadHandler m, HandlerSite m ~ UniWorX) => Maybe FilePath -> m () +setContentDisposition' mFileName = do + wantsDownload <- downloadFiles + setContentDisposition (bool ContentInline ContentAttachment wantsDownload) mFileName + diff --git a/src/Handler/Utils/Csv.hs b/src/Handler/Utils/Csv.hs new file mode 100644 index 000000000..27299f655 --- /dev/null +++ b/src/Handler/Utils/Csv.hs @@ -0,0 +1,71 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Handler.Utils.Csv + ( typeCsv, extensionCsv + , decodeCsv + , encodeCsv + , respondCsv, respondCsvDB + , fileSourceCsv + , CsvParseError(..) + , ToNamedRecord(..), FromNamedRecord(..) + , DefaultOrdered(..) + , ToField(..), FromField(..) + ) where + +import Import + +import Data.Csv +import Data.Csv.Conduit + +import qualified Data.Conduit.List as C +import qualified Data.Conduit.Combinators as C (sourceLazy) + +import qualified Data.Map as Map + + +deriving instance Typeable CsvParseError +instance Exception CsvParseError + + +typeCsv :: ContentType +typeCsv = "text/csv" + +extensionCsv :: Extension +extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ] + + +decodeCsv :: (MonadThrow m, FromNamedRecord csv) => Conduit ByteString m csv +decodeCsv = transPipe throwExceptT $ fromNamedCsv defaultDecodeOptions + +encodeCsv :: ( ToNamedRecord csv + , DefaultOrdered csv + , Monad m + ) + => Conduit csv m ByteString +-- ^ Encode a stream of records +-- +-- Currently not streaming +encodeCsv = fmap encodeDefaultOrderedByName (C.foldMap pure) >>= C.sourceLazy + + +respondCsv :: ( ToNamedRecord csv + , DefaultOrdered csv + ) + => Source (HandlerT site IO) csv + -> HandlerT site IO TypedContent +respondCsv src = respondSource typeCsv $ src .| encodeCsv .| awaitForever sendChunk + +respondCsvDB :: ( ToNamedRecord csv + , DefaultOrdered csv + , YesodPersistRunner site + ) + => Source (YesodDB site) csv + -> HandlerT site IO TypedContent +respondCsvDB src = respondSourceDB typeCsv $ src .| encodeCsv .| awaitForever sendChunk + +fileSourceCsv :: ( FromNamedRecord csv + , MonadResource m + ) + => FileInfo + -> Source m csv +fileSourceCsv = (.| decodeCsv) . fileSource diff --git a/src/Handler/Utils/Table/Cells.hs b/src/Handler/Utils/Table/Cells.hs index a4a9a2fb2..df62bbdbb 100644 --- a/src/Handler/Utils/Table/Cells.hs +++ b/src/Handler/Utils/Table/Cells.hs @@ -194,6 +194,18 @@ cellHasEMail :: (IsDBTable m a, HasUser u) => u -> DBCell m a cellHasEMail = emailCell . view _userEmail +cellHasSemester :: (IsDBTable m c, HasStudyFeatures a) => a -> DBCell m c +cellHasSemester = numCell . view _studyFeaturesSemester + + +cellHasField :: (IsDBTable m c, HasStudyTerms a) => a -> DBCell m c +cellHasField x = maybe (numCell $ x ^. _studyTermsKey) textCell $ x ^. _studyTermsName <|> x ^. _studyTermsShorthand + + +cellHasDegreeShort :: (IsDBTable m c, HasStudyDegree a) => a -> DBCell m c +cellHasDegreeShort x = maybe (numCell $ x ^. _studyDegreeKey) textCell $ x ^. _studyDegreeShorthand <|> x ^. _studyDegreeName + + -- Just for documentation purposes; inline this code instead: maybeDateTimeCell :: IsDBTable m a => Maybe UTCTime -> DBCell m a diff --git a/src/Handler/Utils/Table/Columns.hs b/src/Handler/Utils/Table/Columns.hs index 09db6649d..2f06cd252 100644 --- a/src/Handler/Utils/Table/Columns.hs +++ b/src/Handler/Utils/Table/Columns.hs @@ -11,7 +11,6 @@ import Import -- import Text.Blaze (ToMarkup(..)) -import Data.Monoid (Any(..)) import qualified Database.Esqueleto as E import Database.Esqueleto.Utils as E @@ -19,6 +18,8 @@ import Utils.Lens import Handler.Utils import Handler.Utils.Table.Cells +import qualified Data.CaseInsensitive as CI + -------------------------------- -- Generic Columns @@ -121,7 +122,7 @@ sortUserDisplayName queryUser = ("user-display-name", SortColumn $ queryUser >>> defaultSortingByName :: PSValidator m x -> PSValidator m x defaultSortingByName = -- defaultSorting [SortAscBy "user-surname", SortAscBy "user-display-name"] -- old way, requiring two exta sorters - defaultSorting [SortAscBy "user-name"] -- new way, untested, working with single sorter + defaultSorting [SortAscBy "user-name"] -- new way, working with single sorter -- | Alias for sortUserName for consistency fltrUserNameLink :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d) => (a -> E.SqlExpr (Entity User)) -> (d, FilterColumn t) @@ -156,9 +157,9 @@ fltrUserNameEmail :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool => (a -> E.SqlExpr (Entity User)) -> (d, FilterColumn t) fltrUserNameEmail queryUser = ( "user-name-email", FilterColumn $ anyFilter - [ mkContainsFilter $ queryUser >>> (E.^. UserDisplayName) - , mkContainsFilter $ queryUser >>> (E.^. UserSurname) - , mkContainsFilter $ queryUser >>> (E.^. UserEmail) + [ mkContainsFilter $ queryUser >>> (E.^. UserDisplayName) + , mkContainsFilter $ queryUser >>> (E.^. UserSurname) + , mkContainsFilterWith CI.mk $ queryUser >>> (E.^. UserEmail) ] ) @@ -179,12 +180,14 @@ colUserMatriclenr :: (IsDBTable m c, HasUser a) => Colonnade Sortable a (DBCell colUserMatriclenr = sortable (Just "user-matriclenumber") (i18nCell MsgMatrikelNr) cellHasMatrikelnummer sortUserMatriclenr :: IsString d => (t -> E.SqlExpr (Entity User)) -> (d, SortColumn t) -sortUserMatriclenr queryUser = ( "user-matriclenumber", SortColumn $ queryUser >>> (E.^. UserMatrikelnummer)) +sortUserMatriclenr queryUser = ("user-matriclenumber", SortColumn $ queryUser >>> (E.^. UserMatrikelnummer)) -fltrUserMatriclenr :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d) - => (a -> E.SqlExpr (Entity User)) - -> (d, FilterColumn t) -fltrUserMatriclenr queryUser = ( "user-matriclenumber", FilterColumn $ mkContainsFilter $ queryUser >>> (E.^. UserMatrikelnummer)) +fltrUserMatriclenr :: ( IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)) + , IsString d + ) + => (a -> E.SqlExpr (Entity User)) + -> (d, FilterColumn t) +fltrUserMatriclenr queryUser = ("user-matriclenumber", FilterColumn . mkContainsFilterWith Just $ queryUser >>> (E.^. UserMatrikelnummer)) fltrUserMatriclenrUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text]) fltrUserMatriclenrUI mPrev = @@ -199,13 +202,83 @@ colUserEmail = sortable (Just "user-email") (i18nCell MsgEMail) cellHasEMail sortUserEmail :: IsString d => (t -> E.SqlExpr (Entity User)) -> (d, SortColumn t) sortUserEmail queryUser = ( "user-email", SortColumn $ queryUser >>> (E.^. UserEmail)) -fltrUserEmail :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d) - => (a -> E.SqlExpr (Entity User)) - -> (d, FilterColumn t) -fltrUserEmail queryUser = ( "user-email", FilterColumn $ mkContainsFilter $ queryUser >>> (E.^. UserEmail)) +fltrUserEmail :: ( IsFilterColumn t (a -> Set (CI Text) -> E.SqlExpr (E.Value Bool)) + , IsString d + ) + => (a -> E.SqlExpr (Entity User)) + -> (d, FilterColumn t) +fltrUserEmail queryUser = ("user-email", FilterColumn . mkContainsFilter $ queryUser >>> (E.^. UserEmail)) fltrUserEmailUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text]) fltrUserEmailUI mPrev = prismAForm (singletonFilter "user-email") mPrev $ aopt textField (fslI MsgEMail) +-------------------- +-- Study features -- +-------------------- + +colFeaturesSemester :: (IsDBTable m c, HasStudyFeatures x) => Getting (Leftmost x) a x -> Colonnade Sortable a (DBCell m c) +colFeaturesSemester feature = sortable (Just "features-semester") (i18nCell MsgStudyFeatureAge) $ maybe mempty cellHasSemester . firstOf feature + +sortFeaturesSemester :: IsString d => (t -> E.SqlExpr (Maybe (Entity StudyFeatures))) -> (d, SortColumn t) +sortFeaturesSemester queryFeatures = ("features-semester", SortColumn $ queryFeatures >>> (E.?. StudyFeaturesSemester)) + +fltrFeaturesSemester :: ( IsFilterColumn t (a -> Set Int -> E.SqlExpr (E.Value Bool)) + , IsString d + ) + => (a -> E.SqlExpr (Maybe (Entity StudyFeatures))) + -> (d, FilterColumn t) +fltrFeaturesSemester queryFeatures = ("features-semester", FilterColumn . mkExactFilterWith Just $ queryFeatures >>> (E.?. StudyFeaturesSemester)) + +fltrFeaturesSemesterUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text]) +fltrFeaturesSemesterUI mPrev = + prismAForm (singletonFilter "features-semester" . maybePrism _PathPiece) mPrev $ aopt (intField :: Field (YesodDB UniWorX) Int) (fslI MsgStudyFeatureAge) + + +colField :: (IsDBTable m c, HasStudyTerms x) => Getting (Leftmost x) a x -> Colonnade Sortable a (DBCell m c) +colField terms = sortable (Just "terms") (i18nCell MsgStudyTerm) $ maybe mempty cellHasField . firstOf terms + +sortField :: IsString d => (t -> E.SqlExpr (Maybe (Entity StudyTerms))) -> (d, SortColumn t) +sortField queryTerms = ("terms", SortColumn $ queryTerms >>> (E.?. StudyTermsName)) + +fltrField :: ( IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)) + , IsString d + ) + => (a -> E.SqlExpr (Maybe (Entity StudyTerms))) + -> (d, FilterColumn t) +fltrField queryFeatures = ( "terms" + , FilterColumn $ anyFilter + [ mkContainsFilterWith Just $ queryFeatures >>> E.joinV . (E.?. StudyTermsName) + , mkContainsFilterWith Just $ queryFeatures >>> E.joinV . (E.?. StudyTermsShorthand) + , mkExactFilterWith readMay $ queryFeatures >>> (E.?. StudyTermsKey) + ] + ) + +fltrFieldUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text]) +fltrFieldUI mPrev = + prismAForm (singletonFilter "terms") mPrev $ aopt textField (fslI MsgStudyTerm) + + +colDegreeShort :: (IsDBTable m c, HasStudyDegree x) => Getting (Leftmost x) a x -> Colonnade Sortable a (DBCell m c) +colDegreeShort terms = sortable (Just "degree-short") (i18nCell MsgDegreeShort) $ maybe mempty cellHasDegreeShort . firstOf terms + +sortDegreeShort :: IsString d => (t -> E.SqlExpr (Maybe (Entity StudyDegree))) -> (d, SortColumn t) +sortDegreeShort queryTerms = ("degree-short", SortColumn $ queryTerms >>> (E.?. StudyDegreeShorthand)) + +fltrDegree :: ( IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)) + , IsString d + ) + => (a -> E.SqlExpr (Maybe (Entity StudyDegree))) + -> (d, FilterColumn t) +fltrDegree queryFeatures = ( "degree" + , FilterColumn $ anyFilter + [ mkContainsFilterWith Just $ queryFeatures >>> E.joinV . (E.?. StudyDegreeName) + , mkContainsFilterWith Just $ queryFeatures >>> E.joinV . (E.?. StudyDegreeShorthand) + , mkExactFilterWith readMay $ queryFeatures >>> (E.?. StudyDegreeKey) + ] + ) + +fltrDegreeUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text]) +fltrDegreeUI mPrev = + prismAForm (singletonFilter "degree") mPrev $ aopt textField (fslI MsgDegreeName) diff --git a/src/Handler/Utils/Table/Pagination.hs b/src/Handler/Utils/Table/Pagination.hs index a45c5fb86..70d0c27c8 100644 --- a/src/Handler/Utils/Table/Pagination.hs +++ b/src/Handler/Utils/Table/Pagination.hs @@ -6,7 +6,8 @@ module Handler.Utils.Table.Pagination , FilterColumn(..), IsFilterColumn , DBRow(..), _dbrOutput, _dbrIndex, _dbrCount , DBStyle(..), defaultDBSFilterLayout, DBEmptyStyle(..) - , DBTable(..), IsDBTable(..), DBCell(..) + , DBTCsvEncode, DBTCsvDecode + , DBTable(..), noCsvEncode, IsDBTable(..), DBCell(..) , singletonFilter , DBParams(..) , cellAttrs, cellContents @@ -35,6 +36,8 @@ module Handler.Utils.Table.Pagination import Handler.Utils.Table.Pagination.Types import Handler.Utils.Table.Pagination.Utils (getTableWidget) import Handler.Utils.Form +import Handler.Utils.Csv +import Handler.Utils.ContentDisposition import Utils import Utils.Lens.TH @@ -68,7 +71,8 @@ import Text.Hamlet (hamletFile) import Data.Ratio ((%)) -import Control.Lens +import Control.Lens hiding ((<.>)) +import Control.Lens.Extras (is) import Data.List (elemIndex) @@ -90,6 +94,8 @@ import qualified Data.ByteString.Lazy as LBS import Data.Semigroup as Sem (Semigroup(..)) +import qualified Data.Conduit.List as C + #if MIN_VERSION_base(4,11,0) type Monoid' = Monoid @@ -155,12 +161,12 @@ instance IsFilterColumn t (E.SqlExpr (E.Value Bool)) where filterColumn' fin _ _ = fin instance IsFilterColumn t cont => IsFilterColumn t (t -> cont) where - filterColumn' cont is t = filterColumn' (cont t) is t + filterColumn' cont is' t = filterColumn' (cont t) is' t instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, MonoPointed l, Monoid l) => IsFilterColumn t (l -> cont) where - filterColumn' cont is = filterColumn' (cont input) is' + filterColumn' cont is0 = filterColumn' (cont input) is' where - (input, ($ []) -> is') = go (mempty, id) is + (input, ($ []) -> is') = go (mempty, id) is0 go acc [] = acc go (acc, is3) (i:is2) | Just i' <- fromPathPiece i = go (acc `mappend` singleton i', is3) is2 @@ -264,6 +270,37 @@ piIsUnset PaginationInput{..} = and , isNothing piPage ] + +data ButtonCsvMode = BtnCsvExport | BtnCsvImport + deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) +instance Universe ButtonCsvMode +instance Finite ButtonCsvMode + +embedRenderMessage ''UniWorX ''ButtonCsvMode id + +nullaryPathPiece ''ButtonCsvMode $ camelToPathPiece' 1 + +instance Button UniWorX ButtonCsvMode where + btnLabel BtnCsvExport + = [whamlet| + $newline never + #{iconCSV} + \ _{BtnCsvExport} + |] + btnLabel BtnCsvImport + = [whamlet| + $newline never + _{BtnCsvImport} + |] + +data DBCsvMode = DBCsvNormal + | DBCsvExport + | DBCsvImport + { _dbCsvFiles :: [FileInfo] + , _dbCsvModifyExisting, _dbCsvAddNew, _dbCsvDeleteMissing :: Bool + } + + type DBTableKey k' = (ToJSON k', FromJSON k', Ord k', Binary k') data DBRow r = forall k'. DBTableKey k' => DBRow { dbrKey :: k' @@ -405,7 +442,10 @@ instance PathPiece x => PathPiece (WithIdent x) where WithIdent <$> pure ident <*> fromPathPiece rest -data DBTable m x = forall a r r' h i t k k'. +type DBTCsvEncode r' csv = DictMaybe (ToNamedRecord csv, DefaultOrdered csv) (Conduit r' (YesodDB UniWorX) csv) +type DBTCsvDecode csv = DictMaybe (FromNamedRecord csv) (Sink csv (YesodDB UniWorX) ()) + +data DBTable m x = forall a r r' h i t k k' csv. ( ToSortable h, Functor h , E.SqlSelect a r, E.SqlIn k k', DBTableKey k' , PathPiece i, Eq i @@ -413,16 +453,21 @@ data DBTable m x = forall a r r' h i t k k'. ) => DBTable { dbtSQLQuery :: t -> E.SqlQuery a , dbtRowKey :: t -> k -- ^ required for table forms; always same key for repeated requests. For joins: return unique tuples. - , dbtProj :: DBRow r -> MaybeT (ReaderT SqlBackend (HandlerT UniWorX IO)) r' + , dbtProj :: DBRow r -> MaybeT (YesodDB UniWorX) r' , dbtColonnade :: Colonnade h r' (DBCell m x) , dbtSorting :: Map SortingKey (SortColumn t) , dbtFilter :: Map FilterKey (FilterColumn t) - , dbtFilterUI :: Maybe (Map FilterKey [Text]) -> AForm (ReaderT SqlBackend (HandlerT UniWorX IO)) (Map FilterKey [Text]) + , dbtFilterUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text]) , dbtStyle :: DBStyle , dbtParams :: DBParams m x + , dbtCsvEncode :: DBTCsvEncode r' csv + , dbtCsvDecode :: DBTCsvDecode csv , dbtIdent :: i } +noCsvEncode :: DictMaybe (ToNamedRecord Void, DefaultOrdered Void) (Conduit r' (YesodDB UniWorX) Void) +noCsvEncode = Nothing + class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: * -> *) (x :: *) where data DBParams m x :: * type DBResult m x :: * @@ -694,18 +739,68 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db mapM_ (addMessageI Warning) errs + Just currentRoute <- getCurrentRoute -- `dbTable` should never be called from a 404-handler + getParams <- liftHandlerT $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest + let + tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX + tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing) getParams + substPi = foldr (.) id + [ setParams (wIdent "sorting") . map toPathPiece $ fromMaybe [] piSorting + , foldr (.) id . map (\k -> setParams (wIdent $ toPathPiece k) . fromMaybe [] . join $ traverse (Map.lookup k) piFilter) $ Map.keys dbtFilter + , setParam (wIdent "pagesize") $ fmap toPathPiece piLimit + , setParam (wIdent "page") $ fmap toPathPiece piPage + , setParam (wIdent "pagination") Nothing + ] + tblLink' :: (QueryText -> QueryText) -> Widget + tblLink' = toWidget <=< toTextUrl . tblLink + + ((csvExportRes, csvExportWdgt), csvExportEnctype) <- lift . runFormGet . identifyForm FIDDBTableCsvExport . set (mapped . mapped . _1 . mapped) DBCsvExport $ buttonForm' [BtnCsvExport] + ((csvImportRes, csvImportWdgt), csvImportEnctype) <- lift . runFormPost . identifyForm FIDDBTableCsvImport . renderAForm FormDBTableCsvImport $ DBCsvImport + <$> areq fileFieldMultiple (fslI MsgCsvFile) Nothing + <*> apopt checkBoxField (fslI MsgCsvModifyExisting) (Just True) + <*> apopt checkBoxField (fslI MsgCsvAddNew) (Just True) + <*> apopt checkBoxField (fslI MsgCsvDeleteMissing) (Just False) + + let + csvMode = asum + [ csvExportRes <* guard (is _Just dbtCsvEncode) + , csvImportRes <* guard (is _Just dbtCsvDecode) + , FormSuccess DBCsvNormal + ] + csvExportWdgt' = wrapForm csvExportWdgt FormSettings + { formMethod = GET + , formAction = Just $ tblLink id + , formEncoding = csvExportEnctype + , formAttrs = [("target", "_blank")] + , formSubmit = FormNoSubmit + , formAnchor = Nothing :: Maybe Text + } + csvImportWdgt' = wrapForm' BtnCsvImport csvImportWdgt FormSettings + { formMethod = POST + , formAction = Just $ tblLink id + , formEncoding = csvImportEnctype + , formAttrs = [] + , formSubmit = FormSubmit + , formAnchor = Nothing :: Maybe Text + } + + rows' <- E.select . E.from $ \t -> do res <- dbtSQLQuery t E.orderBy (map (sqlSortDirection t) psSorting') - case previousKeys of - Nothing - | PagesizeLimit l <- psLimit - -> do - E.limit l - E.offset (psPage * l) - Just ps -> E.where_ $ dbtRowKey t `E.sqlIn` ps - _other -> return () - Map.foldrWithKey (\key args expr -> E.where_ (filterColumn (Map.findWithDefault (error $ "Invalid filter key: " <> show key) key dbtFilter) args t) >> expr) (return ()) psFilter + case csvMode of + FormSuccess DBCsvExport -> return () + FormSuccess DBCsvImport{} -> return () + _other -> do + case previousKeys of + Nothing + | PagesizeLimit l <- psLimit + -> do + E.limit l + E.offset (psPage * l) + Just ps -> E.where_ $ dbtRowKey t `E.sqlIn` ps + _other -> return () + Map.foldrWithKey (\key args expr -> E.where_ (filterColumn (Map.findWithDefault (error $ "Invalid filter key: " <> show key) key dbtFilter) args t) >> expr) (return ()) psFilter return (E.unsafeSqlValue "count(*) OVER ()" :: E.SqlExpr (E.Value Int64), dbtRowKey t, res) let mapMaybeM f = fmap catMaybes . mapM (\(k, v) -> runMaybeT $ (,) <$> pure k <*> f v) @@ -723,20 +818,17 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db (currentKeys, rows) <- fmap unzip . mapMaybeM dbtProj . map (\(dbrIndex, (E.Value dbrCount, dbrKey, dbrOutput)) -> (dbrKey, DBRow{..})) . zip [firstRow..] $ reproduceSorting rows' - Just currentRoute <- getCurrentRoute -- `dbTable` should never be called from a 404-handler - getParams <- liftHandlerT $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest - let - tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX - tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing) getParams - substPi = foldr (.) id - [ setParams (wIdent "sorting") . map toPathPiece $ fromMaybe [] piSorting - , foldr (.) id . map (\k -> setParams (wIdent $ toPathPiece k) . fromMaybe [] . join $ traverse (Map.lookup k) piFilter) $ Map.keys dbtFilter - , setParam (wIdent "pagesize") $ fmap toPathPiece piLimit - , setParam (wIdent "page") $ fmap toPathPiece piPage - , setParam (wIdent "pagination") Nothing - ] - tblLink' :: (QueryText -> QueryText) -> Widget - tblLink' = toWidget <=< toTextUrl . tblLink + + formResult csvMode $ \case + DBCsvExport + | Just (Dict, dbtCsvEncode') <- dbtCsvEncode + -> do + setContentDisposition' . Just $ unpack dbtIdent <.> unpack extensionCsv + sendResponse <=< liftHandlerT . respondCsvDB $ C.sourceList rows .| dbtCsvEncode' + DBCsvImport{} + | Just (Dict, _dbtCsvDecode) <- dbtCsvDecode + -> error "dbCsvImport" + _other -> return () let rowCount @@ -791,6 +883,9 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db , formSubmit = FormAutoSubmit , formAnchor = Just $ wIdent "pagesize-form" } + + csvWdgt = $(widgetFile "table/csv-transcode") + uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout") dbInvalidateResult' = foldr (<=<) return . catMaybes $ diff --git a/src/Import/NoModel.hs b/src/Import/NoModel.hs index 744c848b6..bb7c5dd78 100644 --- a/src/Import/NoModel.hs +++ b/src/Import/NoModel.hs @@ -74,6 +74,9 @@ import Network.Mime as Import import Data.Aeson.TH as Import import Data.Aeson.Types as Import (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), toJSONKeyText, FromJSONKeyFunction(..), ToJSONKeyFunction(..), Value) +import Data.Constraint as Import (Dict(..)) +import Data.Void as Import (Void) + import Language.Haskell.TH.Instances as Import () import Data.List.NonEmpty.Instances as Import () import Data.NonNull.Instances as Import () diff --git a/src/Utils.hs b/src/Utils.hs index f2beb56d2..cb2fec606 100644 --- a/src/Utils.hs +++ b/src/Utils.hs @@ -77,6 +77,8 @@ import Data.List.NonEmpty (NonEmpty, nonEmpty) import Algebra.Lattice (top, bottom, (/\), (\/), BoundedJoinSemiLattice, BoundedMeetSemiLattice) +import Data.Constraint (Dict(..)) + {-# ANN choice ("HLint: ignore Use asum" :: String) #-} @@ -164,6 +166,10 @@ fileDownload = fontAwesomeIcon "file-download" zipDownload :: Markup zipDownload = fontAwesomeIcon "file-archive" +iconCSV :: Markup +iconCSV = fontAwesomeIcon "file-csv" + + -- Conditional icons isVisible :: Bool -> Markup @@ -983,3 +989,12 @@ foldJoin = foldr (\/) bottom foldMeet :: (MonoFoldable mono, BoundedMeetSemiLattice (Element mono)) => mono -> Element mono foldMeet = foldr (/\) top + +----------------- +-- Constraints -- +----------------- + +type DictMaybe constr a = Maybe (Dict constr, a) + +pattern DictJust :: constr => a -> DictMaybe constr a +pattern DictJust a = Just (Dict, a) diff --git a/src/Utils/Form.hs b/src/Utils/Form.hs index 0129b8750..1749dd51a 100644 --- a/src/Utils/Form.hs +++ b/src/Utils/Form.hs @@ -8,6 +8,7 @@ import Yesod.Core.Instances () import Settings import Utils.Parameters +import Utils.Lens import Text.Blaze (Markup) import qualified Text.Blaze.Internal as Blaze (null) @@ -32,8 +33,6 @@ import Control.Monad.Morph (MFunctor(..)) import Data.List ((!!)) -import Control.Lens - import Web.PathPieces import Data.UUID @@ -197,6 +196,8 @@ data FormIdentifier | FIDDBTableFilter | FIDDBTablePagesize | FIDDBTable + | FIDDBTableCsvExport + | FIDDBTableCsvImport | FIDDelete | FIDCourseRegister | FIDuserRights @@ -591,6 +592,19 @@ htmlFieldSmall = checkMMap sanitize (pack . renderHtml) textField sanitize :: Text -> m (Either FormMessage Html) sanitize = return . Right . preEscapedText . sanitizeBalance +fileFieldMultiple :: Monad m => Field m [FileInfo] +fileFieldMultiple = Field + { fieldParse = \_ files -> return $ case files of + [] -> Right Nothing + fs -> Right $ Just fs + , fieldView = \id' name attrs _ isReq -> + [whamlet| + $newline never + + |] + , fieldEnctype = Multipart + } + ----------- -- Forms -- ----------- @@ -635,7 +649,7 @@ wrapForm' btn formWidget FormSettings{..} = do ------------------- -- | Use this type to pass information to the form template -data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize +data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize | FormDBTableCsvImport renderAForm :: Monad m => FormLayout -> FormRender m a renderAForm formLayout aform fragment = do @@ -932,3 +946,20 @@ apreq f fs mx = formToAForm $ over _2 pure <$> mpreq f fs mx wpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) => Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a) wpreq f fs mx = mFormToWForm $ mpreq f fs mx + + +mpopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) + => Field m a -> FieldSettings site -> Maybe a -> MForm m (FormResult a, FieldView site) +-- ^ Pseudo optional +-- +-- `FieldView` has `fvRequired` set to `False` +-- Otherwise acts exactly like `mreq`. +mpopt f fs mx = set (_2 . _fvRequired) False <$> mreq f fs mx + +apopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) + => Field m a -> FieldSettings site -> Maybe a -> AForm m a +apopt f fs mx = formToAForm $ over _2 pure <$> mpopt f fs mx + +wpopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) + => Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a) +wpopt f fs mx = mFormToWForm $ mpopt f fs mx diff --git a/src/Utils/Lens.hs b/src/Utils/Lens.hs index 0f3deec79..9388bea0b 100644 --- a/src/Utils/Lens.hs +++ b/src/Utils/Lens.hs @@ -41,13 +41,13 @@ _nullable = prism' toNullable fromNullable -- makeLenses_ ''Course -makeClassyFor_ "HasCourse" "hasCourse" ''Course +makeClassyFor_ ''Course -- class HasCourse c where -- hasCourse :: Lens' c Course -- makeLenses_ ''User -makeClassyFor_ "HasUser" "hasUser" ''User +makeClassyFor_ ''User -- > :info HasUser -- class HasUser c where -- hasUser :: Lens' c User -- MINIMAL @@ -56,8 +56,24 @@ makeClassyFor_ "HasUser" "hasUser" ''User -- _user... -- +makeClassyFor_ ''StudyFeatures + +makeClassyFor_ ''StudyDegree + +makeClassyFor_ ''StudyTerms + makeLenses_ ''Entity + +instance HasStudyFeatures a => HasStudyFeatures (Entity a) where + hasStudyFeatures = _entityVal . hasStudyFeatures + +instance HasStudyTerms a => HasStudyTerms (Entity a) where + hasStudyTerms = _entityVal . hasStudyTerms + +instance HasStudyDegree a => HasStudyDegree (Entity a) where + hasStudyDegree = _entityVal . hasStudyDegree + -- BUILD SERVER FAILS TO MAKE HADDOCK FOR THE ONE BELOW: -- makeClassyFor_ "HasEntity" "hasEntity" ''Entity -- class HasEntity c record | c -> record where @@ -96,12 +112,6 @@ makePrisms ''AuthResult makePrisms ''FormResult -makeLenses_ ''StudyFeatures - -makeLenses_ ''StudyDegree - -makeLenses_ ''StudyTerms - makeLenses_ ''StudyTermCandidate makeLenses_ ''FieldView @@ -133,6 +143,8 @@ makeLenses_ ''ExamGradingRule makeLenses_ ''UTCTime +makeLenses_ ''ExamOccurrence + -- makeClassy_ ''Load diff --git a/src/Utils/Lens/TH.hs b/src/Utils/Lens/TH.hs index b8d8857a7..701c87b76 100644 --- a/src/Utils/Lens/TH.hs +++ b/src/Utils/Lens/TH.hs @@ -1,6 +1,6 @@ module Utils.Lens.TH where -import ClassyPrelude (String, Maybe(..)) +import ClassyPrelude (Maybe(..), (<>)) import Control.Lens import Control.Lens.Internal.FieldTH import Language.Haskell.TH @@ -56,9 +56,12 @@ makeLenses_ = makeFieldOptics lensRules_ -- | like makeClassyFor but only specifies names for class and its function, -- otherwise lenses are created with underscore like `makeLenses_` -makeClassyFor_ :: String -> String -> Name -> DecsQ -makeClassyFor_ clsName funName = makeFieldOptics (classyRulesFor_ clNamer) +makeClassyFor_ :: Name -> DecsQ +makeClassyFor_ recName = makeFieldOptics (classyRulesFor_ clNamer) recName where - clNamer :: ClassyNamer - -- clNamer _ = Just (clsName, funName) -- for newer versions >= 4.17 - clNamer _ = Just (mkName clsName, mkName funName) \ No newline at end of file + clsName = "Has" <> nameBase recName + funName = "has" <> nameBase recName + + clNamer :: ClassyNamer + -- clNamer _ = Just (clsName, funName) -- for newer versions >= 4.17 + clNamer _ = Just (mkName clsName, mkName funName) diff --git a/start.sh b/start.sh index cdad4b731..a9ef7cb8d 100755 --- a/start.sh +++ b/start.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +set -e + +[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || : + unset HOST export DETAILED_LOGGING=${DETAILED_LOGGIN:-true} export LOG_ALL=${LOG_ALL:-false} diff --git a/static/fonts/fontawesome/fa-solid-900.eot b/static/fonts/fontawesome/fa-solid-900.eot index cc691d633..c77baa8d4 100644 Binary files a/static/fonts/fontawesome/fa-solid-900.eot and b/static/fonts/fontawesome/fa-solid-900.eot differ diff --git a/static/fonts/fontawesome/fa-solid-900.svg b/static/fonts/fontawesome/fa-solid-900.svg index 1534b64be..627128b82 100644 --- a/static/fonts/fontawesome/fa-solid-900.svg +++ b/static/fonts/fontawesome/fa-solid-900.svg @@ -1,2231 +1,4649 @@ - + + - diff --git a/static/fonts/fontawesome/fa-solid-900.ttf b/static/fonts/fontawesome/fa-solid-900.ttf index 618136ab1..c6c3dd4d4 100644 Binary files a/static/fonts/fontawesome/fa-solid-900.ttf and b/static/fonts/fontawesome/fa-solid-900.ttf differ diff --git a/static/fonts/fontawesome/fa-solid-900.woff b/static/fonts/fontawesome/fa-solid-900.woff index af4765781..77c178622 100644 Binary files a/static/fonts/fontawesome/fa-solid-900.woff and b/static/fonts/fontawesome/fa-solid-900.woff differ diff --git a/static/fonts/fontawesome/fa-solid-900.woff2 b/static/fonts/fontawesome/fa-solid-900.woff2 index 9ef566a9e..e30fb6711 100644 Binary files a/static/fonts/fontawesome/fa-solid-900.woff2 and b/static/fonts/fontawesome/fa-solid-900.woff2 differ diff --git a/static/fonts/fonts.css b/static/fonts/fonts.css index 785fe33d3..8133ec104 100644 --- a/static/fonts/fonts.css +++ b/static/fonts/fonts.css @@ -1,8 +1,8 @@ /*! - * Font Awesome Free 5.1.0 by @fontawesome - https://fontawesome.com + * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */ - @font-face{ +@font-face{ font-family:"Font Awesome 5 Free"; font-style:normal; font-weight:900; diff --git a/templates/corrections-overview.hamlet b/templates/corrections-overview.hamlet index 152ba73e5..64a647387 100644 --- a/templates/corrections-overview.hamlet +++ b/templates/corrections-overview.hamlet @@ -130,6 +130,16 @@
_{MsgAssignSubmissionsRandomWarning} \ No newline at end of file diff --git a/templates/i18n/knownBugs/de.hamlet b/templates/i18n/knownBugs/de.hamlet index f609cd287..82201f80e 100644 --- a/templates/i18n/knownBugs/de.hamlet +++ b/templates/i18n/knownBugs/de.hamlet @@ -1,6 +1,8 @@ $newline never
- Stand: Mai 2019 + Stand: July 2019