From 37db6256c11c9ece20fd667ce4b29fc700df35d3 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 3 Jul 2019 11:59:02 +0200 Subject: [PATCH] Merge branch 'master' into course-teaser --- .babelrc | 9 + .directory | 2 +- .eslintrc.json | 28 + .gitignore | 5 +- .vscode/tasks.json | 26 + CHANGELOG.md | 53 + ChangeLog.md | 81 - README.md | 130 + assets/lmu/logo.svg | 6 + assets/lmu/sigillum.svg | 19 + assets/logo-o2.svg | 4 - assets/logo.png | Bin 6698 -> 0 bytes build.sh | 2 +- commitlint.config.js | 1 + config/archive-types | 40 + config/mimetypes | 788 + config/settings.yml | 14 +- db.sh | 2 + .../polyfills/fetch.js | 0 frontend/polyfills/main.js | 2 + .../polyfills/url-search-params.js | 0 frontend/src/app.js | 28 + frontend/src/app.spec.js | 65 + frontend/src/core/utility.js | 22 + frontend/src/main.js | 24 + .../src/services/html-helpers/html-helpers.js | 42 + .../html-helpers/html-helpers.spec.js | 56 + .../src/services/http-client/http-client.js | 41 + .../services/http-client/http-client.spec.js | 116 + frontend/src/services/i18n/i18n.js | 32 + frontend/src/services/i18n/i18n.spec.js | 51 + .../services/util-registry/util-registry.js | 123 + .../util-registry/util-registry.spec.js | 146 + frontend/src/utils/alerts/alerts.js | 165 + frontend/src/utils/alerts/alerts.md | 35 + .../src/utils/alerts}/alerts.scss | 30 +- frontend/src/utils/alerts/alerts.spec.js | 27 + frontend/src/utils/asidenav/asidenav.js | 82 + frontend/src/utils/asidenav/asidenav.md | 21 + .../src/utils/asidenav}/asidenav.scss | 75 +- frontend/src/utils/asidenav/asidenav.spec.js | 21 + frontend/src/utils/async-form/async-form.js | 95 + frontend/src/utils/async-form/async-form.md | 17 + .../src/utils/async-form/async-form.scss | 22 +- .../src/utils/async-form/async-form.spec.js | 21 + .../utils/async-table/async-table-filter.scss | 0 frontend/src/utils/async-table/async-table.js | 376 + frontend/src/utils/async-table/async-table.md | 7 + .../src/utils/async-table/async-table.scss | 0 .../src/utils/async-table/async-table.spec.js | 52 + frontend/src/utils/check-all/check-all.js | 127 + frontend/src/utils/check-all/check-all.md | 9 + .../src/utils/check-all/check-all.spec.js | 27 + frontend/src/utils/form/auto-submit-button.js | 29 + frontend/src/utils/form/auto-submit-button.md | 11 + frontend/src/utils/form/auto-submit-input.js | 47 + frontend/src/utils/form/auto-submit-input.md | 9 + frontend/src/utils/form/datepicker.js | 62 + frontend/src/utils/form/datepicker.md | 8 + frontend/src/utils/form/form-error-remover.js | 46 + frontend/src/utils/form/form-error-remover.md | 8 + frontend/src/utils/form/form.js | 17 + .../src/utils/form}/form.scss | 9 +- .../src/utils/form/interactive-fieldset.js | 97 + .../src/utils/form/interactive-fieldset.md | 35 + .../src/utils/form/navigate-away-prompt.js | 71 + .../src/utils/form/navigate-away-prompt.md | 12 + .../src/utils/form/reactive-submit-button.js | 85 + .../src/utils/form/reactive-submit-button.md | 17 + frontend/src/utils/inputs/checkbox.js | 43 + frontend/src/utils/inputs/checkbox.md | 10 + .../src/utils/inputs}/checkbox.scss | 22 +- frontend/src/utils/inputs/file-input.js | 99 + frontend/src/utils/inputs/file-input.md | 23 + frontend/src/utils/inputs/inputs.js | 10 + .../src/utils/inputs}/inputs.scss | 59 +- .../src/utils/inputs}/radio.scss | 6 +- frontend/src/utils/mass-input/mass-input.js | 197 + frontend/src/utils/mass-input/mass-input.md | 17 + frontend/src/utils/modal/modal.js | 182 + frontend/src/utils/modal/modal.md | 16 + .../src/utils/modal}/modal.scss | 34 +- frontend/src/utils/show-hide/show-hide.js | 87 + frontend/src/utils/show-hide/show-hide.md | 21 + .../src/utils/show-hide/show-hide.scss | 30 +- .../src/utils/tabber}/tabber.js | 4 +- .../src/utils/tabber}/tabber.scss | 0 frontend/src/utils/tooltips/tooltips.js | 10 + .../src/utils/tooltips/tooltips.scss | 10 +- frontend/src/utils/utils.js | 26 + {static/css => frontend}/vendor/flatpickr.css | 19 +- frontend/vendor/fontawesome.css | 5 + frontend/vendor/main.js | 2 + haddock.sh | 13 +- hlint.sh | 2 +- karma.conf.js | 80 + messages/frontend/de.msg | 4 + messages/uniworx/de.msg | 613 +- models/courses | 2 +- models/exams | 75 +- models/invitations | 5 + models/materials | 12 + models/rooms | 32 - models/sheets | 5 +- models/tutorials | 29 +- models/users | 5 +- package-lock.json | 12609 ++++++++++++++++ package.json | 90 + package.yaml | 18 +- postcss.config.js | 6 + routes | 172 +- shell.nix | 6 +- src/Application.hs | 233 +- src/Auth/Dummy.hs | 2 +- src/Auth/LDAP.hs | 4 +- src/Auth/PWHash.hs | 2 +- src/CryptoID.hs | 26 +- src/CryptoID/TH.hs | 10 + src/Data/Aeson/Types/Instances.hs | 26 + src/Data/CaseInsensitive/Instances.hs | 9 +- src/Data/CryptoID/Instances.hs | 14 + src/Data/Fixed/Instances.hs | 13 + src/Data/HashMap/Strict/Instances.hs | 16 + src/Data/HashSet/Instances.hs | 17 + src/Data/Maybe/Instances.hs | 13 + src/Data/NonNull/Instances.hs | 28 + src/Data/Set/Instances.hs | 14 + src/Data/Sum/Instances.hs | 13 + src/Data/Time/Calendar/Instances.hs | 18 + src/Data/Time/Clock/Instances.hs | 29 + src/Data/Time/Format/Instances.hs | 14 + src/Data/Time/LocalTime/Instances.hs | 23 + src/Data/UUID/Instances.hs | 27 + .../Instances/Reverse/MonoTraversable.hs | 17 + src/Data/Universe/TH.hs | 69 + src/Data/Vector/Instances.hs | 18 + src/Database/Esqueleto/Utils.hs | 29 +- src/Database/Esqueleto/Utils/TH.hs | 11 +- src/Database/Persist/Class/Instances.hs | 23 + src/Database/Persist/Sql/Instances.hs | 33 - src/Database/Persist/TH/Directory.hs | 27 +- src/Database/Persist/Types/Instances.hs | 22 + src/Foundation.hs | 1239 +- src/Handler/Admin.hs | 33 +- src/Handler/Common.hs | 19 +- src/Handler/Corrections.hs | 580 +- src/Handler/Course.hs | 782 +- src/Handler/Exam.hs | 764 + src/Handler/Health.hs | 97 + src/Handler/Help.hs | 19 +- src/Handler/Home.hs | 19 +- src/Handler/Info.hs | 13 +- src/Handler/Material.hs | 369 + src/Handler/Profile.hs | 137 +- src/Handler/Sheet.hs | 654 +- src/Handler/Submission.hs | 485 +- src/Handler/SystemMessage.hs | 4 +- src/Handler/Term.hs | 4 +- src/Handler/Tutorial.hs | 472 + src/Handler/Users.hs | 8 +- src/Handler/Utils.hs | 135 +- src/Handler/Utils/Communication.hs | 187 + src/Handler/Utils/Corrections.hs | 47 + src/Handler/Utils/DateTime.hs | 59 +- src/Handler/Utils/Delete.hs | 10 +- src/Handler/Utils/Exam.hs | 47 + src/Handler/Utils/Form.hs | 597 +- src/Handler/Utils/Form/MassInput.hs | 411 +- .../Utils/Form/MassInput/Liveliness.hs | 45 + src/Handler/Utils/Form/MassInput/TH.hs | 40 + src/Handler/Utils/Form/Occurrences.hs | 123 + src/Handler/Utils/Invitations.hs | 358 + src/Handler/Utils/Mail.hs | 20 +- src/Handler/Utils/Rating.hs | 9 +- src/Handler/Utils/Sheet.hs | 24 +- src/Handler/Utils/Submission.hs | 396 +- src/Handler/Utils/Table.hs | 1 + src/Handler/Utils/Table/Cells.hs | 91 +- src/Handler/Utils/Table/Columns.hs | 64 +- src/Handler/Utils/Table/Pagination.hs | 107 +- src/Handler/Utils/Tokens.hs | 34 + src/Handler/Utils/Tutorial.hs | 47 + src/Handler/Utils/Zip.hs | 16 +- src/Import/NoFoundation.hs | 66 +- src/Import/NoModel.hs | 109 + src/Jobs.hs | 182 +- src/Jobs/Crontab.hs | 67 +- src/Jobs/Handler/HelpRequest.hs | 6 +- src/Jobs/Handler/Invitation.hs | 27 + src/Jobs/Handler/SendCourseCommunication.hs | 37 + .../SendNotification/CorrectionsAssigned.hs | 6 +- .../Handler/SendNotification/SheetActive.hs | 6 +- .../Handler/SendNotification/SheetInactive.hs | 11 +- .../SendNotification/SubmissionRated.hs | 4 +- .../SendNotification/UserRightsUpdate.hs | 5 +- src/Jobs/Handler/SendNotification/Utils.hs | 20 + src/Jobs/HealthReport.hs | 137 + src/Jobs/Queue.hs | 32 +- src/Jobs/Types.hs | 27 +- src/Jose/Jwt/Instances.hs | 27 + src/Language/Haskell/TH/Instances.hs | 14 + src/Ldap/Client/Pool.hs | 2 +- src/Mail.hs | 74 +- src/Model.hs | 41 +- src/Model/Migration.hs | 95 +- src/Model/Migration/Types.hs | 43 +- src/Model/Rating.hs | 1 + src/Model/Submission.hs | 1 + src/Model/Tokens.hs | 149 + src/Model/Types.hs | 810 +- src/Model/Types/Common.hs | 36 + src/Model/Types/Course.hs | 26 + src/Model/Types/DateTime.hs | 192 + src/Model/Types/Exam.hs | 122 + src/Model/Types/Health.hs | 87 + src/Model/Types/Mail.hs | 75 + src/Model/Types/Misc.hs | 44 + src/Model/Types/Security.hs | 144 + src/Model/Types/Sheet.hs | 312 + src/Model/Types/Submission.hs | 151 + src/Model/Types/{ => TH}/JSON.hs | 25 +- src/Model/Types/{ => TH}/Wordlist.hs | 4 +- src/Network/Mime/TH.hs | 56 + src/Settings.hs | 124 +- src/Settings/Cluster.hs | 24 + src/System/FilePath/Instances.hs | 16 + src/Text/Blaze/Instances.hs | 37 + src/Time/Types/Instances.hs | 25 + src/Utils.hs | 415 +- src/Utils/DB.hs | 58 +- src/Utils/DateTime.hs | 13 +- src/Utils/Form.hs | 362 +- src/Utils/Frontend/I18n.hs | 41 + src/Utils/Frontend/Modal.hs | 74 + src/Utils/Lens.hs | 54 +- src/Utils/Message.hs | 5 +- src/Utils/Modal.hs | 42 - src/Utils/Occurrences.hs | 84 + src/Utils/Parameters.hs | 16 +- src/Utils/PathPiece.hs | 71 +- src/Utils/Sheet.hs | 64 + src/Utils/TH.hs | 3 - src/Utils/Tokens.hs | 174 + src/Web/PathPieces/Instances.hs | 12 + src/Yesod/Core/Instances.hs | 68 +- src/Yesod/Core/Types/Instances.hs | 51 +- src/index.md | 3 +- stack.nix | 13 +- stack.yaml | 25 +- stackage.nix | 30 + start.sh | 11 +- static/css/vendor/fontawesome.css | 5 - static/{css => fonts}/fonts.css | 0 static/img/lmu/sigillum.svg | 1 + static/js/utils/alerts.js | 96 - static/js/utils/asidenav.js | 64 - static/js/utils/asyncForm.js | 86 - static/js/utils/asyncTable.js | 237 - static/js/utils/asyncTableFilter.js | 171 - static/js/utils/checkAll.js | 131 - static/js/utils/form.js | 202 - static/js/utils/httpClient.js | 32 - static/js/utils/inputs.js | 253 - static/js/utils/modal.js | 155 - static/js/utils/setup.js | 114 - static/js/utils/showHide.js | 77 - static/js/vendor/flatpickr.js | 2 - static/js/vendor/zepto.js | 1650 -- sync-versions.hs | 81 + templates/adminTest.hamlet | 12 +- templates/correction-user.hamlet | 4 +- templates/corrections-assign.hamlet | 52 + templates/corrections-overview.hamlet | 130 + templates/corrections-upload.hamlet | 5 +- templates/course-user.hamlet | 30 +- templates/course.hamlet | 123 +- templates/course/lecturerMassInput/add.hamlet | 6 + .../lecturerMassInput/cellInvitation.hamlet | 12 + .../course/lecturerMassInput/cellKnown.hamlet | 6 + .../course/lecturerMassInput/layout.hamlet | 11 + templates/default-layout-wrapper.hamlet | 30 +- templates/default-layout.hamlet | 20 +- templates/default-layout.julius | 53 +- templates/default-layout.lucius | 118 +- templates/deletedUser.hamlet | 6 +- templates/exam-edit.hamlet | 2 + templates/exam-list.hamlet | 2 + templates/exam-new.hamlet | 2 + templates/exam-show.cassius | 6 + templates/exam-show.hamlet | 153 + templates/help.hamlet | 5 +- templates/home/openCourses.hamlet | 1 + templates/home/upcomingSheets.hamlet | 1 + templates/i18n/README_i18n.txt | 6 + templates/i18n/changelog/de.hamlet | 124 + .../corrections-upload-instructions/de.hamlet | 26 + .../{ => i18n}/data-protection/de.hamlet | 6 +- .../featureList/de.hamlet} | 2 - templates/i18n/html-input/de.hamlet | 8 + templates/{ => i18n}/imprint/de.hamlet | 0 templates/i18n/info-lecturer/de.hamlet | 331 + templates/i18n/knownBugs/de.hamlet | 6 + .../i18n/profile/tokenExplanation/de.hamlet | 13 + templates/i18n/sheet-edit/de.hamlet | 22 + templates/info-lecturer/de.hamlet | 199 - templates/mail/correctorInvitation.hamlet | 11 + templates/mail/editNotifications.hamlet | 5 +- templates/mail/invitation.hamlet | 11 + templates/mail/lecturerInvitation.hamlet | 11 + templates/mail/submissionRated.hamlet | 4 +- templates/material-list.hamlet | 2 + templates/material-show.hamlet | 22 + templates/messages/correctionsUploaded.hamlet | 4 +- .../courseInvitationAlreadyRegistered.hamlet | 5 + ...rseInvitationRegisteredWithoutField.hamlet | 5 + .../messages/submissionFilesIgnored.hamlet | 2 +- .../messages/submissionsAssignNotFound.hamlet | 4 + .../submissionsAssignUnauthorized.hamlet | 2 +- templates/multiFileField.hamlet | 20 - templates/multiFileField.lucius | 55 - templates/profile.hamlet | 2 - templates/profile/profile.hamlet | 13 + templates/profile/profile.julius | 24 + templates/profileData.hamlet | 10 +- templates/sheetCorrectors/add.hamlet | 6 + templates/sheetCorrectors/cell.hamlet | 20 + templates/sheetCorrectors/layout.hamlet | 18 + templates/sheetShow.hamlet | 15 +- templates/submission-assign.hamlet | 2 + templates/submission.hamlet | 22 +- templates/system-message.hamlet | 6 +- templates/table/cell/body.hamlet | 2 +- templates/table/cell/header.hamlet | 5 +- templates/table/cell/link.hamlet | 2 +- templates/table/layout-filter-default.hamlet | 2 +- templates/table/layout-filter-default.lucius | 4 + templates/table/layout-standalone.hamlet | 1 + templates/table/layout-wrapper.hamlet | 2 +- templates/table/layout.hamlet | 4 + templates/table/layout.julius | 10 - templates/table/layout.lucius | 7 + templates/table/sortable-header.hamlet | 1 + templates/terms.hamlet | 4 +- templates/tutorial-edit.hamlet | 2 + templates/tutorial-list.hamlet | 2 + templates/tutorial-new.hamlet | 2 + templates/tutorial-participants.hamlet | 2 + templates/tutorial/tutorMassInput/add.hamlet | 6 + .../tutorMassInput/cellInvitation.hamlet | 9 + .../tutorial/tutorMassInput/cellKnown.hamlet | 3 + .../tutorial/tutorMassInput/layout.hamlet | 11 + templates/versionHistory.hamlet | 37 +- templates/widgets/aform/aform.hamlet | 2 +- templates/widgets/alerts/alerts.hamlet | 3 +- templates/widgets/alerts/alerts.julius | 4 - templates/widgets/asidenav/asidenav.hamlet | 17 +- templates/widgets/asidenav/asidenav.julius | 4 - templates/widgets/bonusRule.hamlet | 8 + .../widgets/communication/recipientAdd.hamlet | 5 + .../communication/recipientEmail.hamlet | 6 + .../communication/recipientLayout.hamlet | 22 + .../communication/recipientLayout.julius | 87 + .../communication/recipientLayout.lucius | 65 + .../communication/recipientName.hamlet | 6 + .../course-teaser/course-teaser.julius | 25 - .../widgets/date-time/yet-invisible.hamlet | 1 + templates/widgets/email.hamlet | 2 + templates/widgets/footer/footer.hamlet | 1 + templates/widgets/footer/footer.lucius | 8 + templates/widgets/form/form.hamlet | 33 +- templates/widgets/form/form.julius | 3 - .../grading-summary-row.hamlet | 18 +- .../grading-summary/grading-summary.hamlet | 4 +- templates/widgets/gradingKey.cassius | 3 + templates/widgets/gradingKey.hamlet | 15 + templates/widgets/invitation-site.hamlet | 4 + templates/widgets/link-email.hamlet | 3 +- .../massinput/examCorrectors/add.hamlet | 6 + .../examCorrectors/cellInvitation.hamlet | 9 + .../massinput/examCorrectors/cellKnown.hamlet | 3 + .../massinput/examCorrectors/layout.hamlet | 11 + .../widgets/massinput/examParts/add.hamlet | 4 + .../widgets/massinput/examParts/form.hamlet | 4 + .../widgets/massinput/examParts/layout.hamlet | 16 + .../widgets/massinput/examRooms/add.hamlet | 4 + .../widgets/massinput/examRooms/form.hamlet | 6 + .../widgets/massinput/examRooms/layout.hamlet | 18 + .../widgets/massinput/list/layout.hamlet | 14 + .../massinput-standalone-wrapper.hamlet | 8 + .../massinput/massinput-standalone.hamlet | 6 + templates/widgets/massinput/massinput.hamlet | 2 +- templates/widgets/massinput/massinput.julius | 21 - templates/widgets/massinput/massinput.lucius | 8 + templates/widgets/massinput/row.hamlet | 6 +- .../massinput/submissionUsers/add.hamlet | 6 + .../submissionUsers/cellInvitation.hamlet | 10 + .../submissionUsers/cellKnown.hamlet | 4 + .../massinput/submissionUsers/layout.hamlet | 13 + .../massinput/uploadSpecificFiles/add.hamlet | 4 + .../massinput/uploadSpecificFiles/form.hamlet | 4 + .../uploadSpecificFiles/layout.hamlet | 16 + templates/widgets/modal/modal.hamlet | 2 +- templates/widgets/modal/modal.julius | 6 - .../multi-action/multi-action-collect.hamlet | 4 - .../widgets/multi-action/multi-action.hamlet | 2 +- templates/widgets/multiFileField.hamlet | 27 + templates/widgets/multiFileField.lucius | 11 + templates/widgets/navbar/item.hamlet | 5 +- templates/widgets/navbar/navbar.julius | 31 - templates/widgets/navbar/navbar.lucius | 39 +- templates/widgets/occurrence/cell.hamlet | 12 + .../occurrence/cell/except-no-occur.hamlet | 2 + .../occurrence/cell/except-no-occurr.hamlet | 2 + .../occurrence/cell/except-occur.hamlet | 2 + .../occurrence/cell/except-occurr.hamlet | 2 + .../widgets/occurrence/cell/weekly.hamlet | 2 + .../widgets/occurrence/form/except-add.hamlet | 5 + .../occurrence/form/except-layout.hamlet | 11 + .../occurrence/form/except-no-occur.hamlet | 5 + .../occurrence/form/except-occur.hamlet | 5 + .../occurrence/form/scheduled-add.hamlet | 5 + .../occurrence/form/scheduled-layout.hamlet | 11 + .../widgets/occurrence/form/weekly.hamlet | 5 + .../widgets/pageaction/pageaction.hamlet | 4 +- .../widgets/pageaction/pageaction.julius | 0 .../widgets/pageaction/pageaction.lucius | 10 +- .../register-form/register-form.hamlet | 1 + templates/widgets/specificFileField.hamlet | 8 + templates/widgets/zipFileField.hamlet | 8 + test.sh | 11 + test/Database.hs | 163 +- test/FoundationSpec.hs | 56 + test/Handler/Utils/SubmissionSpec.hs | 192 + test/MailSpec.hs | 2 +- test/Model/TypesSpec.hs | 92 +- test/ModelSpec.hs | 37 + test/Test/QuickCheck/Classes/Binary.hs | 17 + test/TestImport.hs | 8 +- test/Utils/DateTimeSpec.hs | 3 + webpack.config.js | 108 + 440 files changed, 32390 insertions(+), 7381 deletions(-) create mode 100644 .babelrc create mode 100644 .eslintrc.json create mode 100644 CHANGELOG.md delete mode 100644 ChangeLog.md create mode 100644 README.md create mode 100644 assets/lmu/logo.svg create mode 100644 assets/lmu/sigillum.svg delete mode 100644 assets/logo-o2.svg delete mode 100644 assets/logo.png create mode 100644 commitlint.config.js create mode 100644 config/archive-types create mode 100644 config/mimetypes rename static/js/polyfills/fetchPolyfill.js => frontend/polyfills/fetch.js (100%) create mode 100644 frontend/polyfills/main.js rename static/js/polyfills/urlPolyfill.js => frontend/polyfills/url-search-params.js (100%) create mode 100644 frontend/src/app.js create mode 100644 frontend/src/app.spec.js create mode 100644 frontend/src/core/utility.js create mode 100644 frontend/src/main.js create mode 100644 frontend/src/services/html-helpers/html-helpers.js create mode 100644 frontend/src/services/html-helpers/html-helpers.spec.js create mode 100644 frontend/src/services/http-client/http-client.js create mode 100644 frontend/src/services/http-client/http-client.spec.js create mode 100644 frontend/src/services/i18n/i18n.js create mode 100644 frontend/src/services/i18n/i18n.spec.js create mode 100644 frontend/src/services/util-registry/util-registry.js create mode 100644 frontend/src/services/util-registry/util-registry.spec.js create mode 100644 frontend/src/utils/alerts/alerts.js create mode 100644 frontend/src/utils/alerts/alerts.md rename {static/css/utils => frontend/src/utils/alerts}/alerts.scss (85%) create mode 100644 frontend/src/utils/alerts/alerts.spec.js create mode 100644 frontend/src/utils/asidenav/asidenav.js create mode 100644 frontend/src/utils/asidenav/asidenav.md rename {static/css/utils => frontend/src/utils/asidenav}/asidenav.scss (89%) create mode 100644 frontend/src/utils/asidenav/asidenav.spec.js create mode 100644 frontend/src/utils/async-form/async-form.js create mode 100644 frontend/src/utils/async-form/async-form.md rename static/css/utils/asyncForm.scss => frontend/src/utils/async-form/async-form.scss (74%) create mode 100644 frontend/src/utils/async-form/async-form.spec.js rename static/css/utils/asyncTableFilter.scss => frontend/src/utils/async-table/async-table-filter.scss (100%) create mode 100644 frontend/src/utils/async-table/async-table.js create mode 100644 frontend/src/utils/async-table/async-table.md rename static/css/utils/asyncTable.scss => frontend/src/utils/async-table/async-table.scss (100%) create mode 100644 frontend/src/utils/async-table/async-table.spec.js create mode 100644 frontend/src/utils/check-all/check-all.js create mode 100644 frontend/src/utils/check-all/check-all.md create mode 100644 frontend/src/utils/check-all/check-all.spec.js create mode 100644 frontend/src/utils/form/auto-submit-button.js create mode 100644 frontend/src/utils/form/auto-submit-button.md create mode 100644 frontend/src/utils/form/auto-submit-input.js create mode 100644 frontend/src/utils/form/auto-submit-input.md create mode 100644 frontend/src/utils/form/datepicker.js create mode 100644 frontend/src/utils/form/datepicker.md create mode 100644 frontend/src/utils/form/form-error-remover.js create mode 100644 frontend/src/utils/form/form-error-remover.md create mode 100644 frontend/src/utils/form/form.js rename {static/css/utils => frontend/src/utils/form}/form.scss (83%) create mode 100644 frontend/src/utils/form/interactive-fieldset.js create mode 100644 frontend/src/utils/form/interactive-fieldset.md create mode 100644 frontend/src/utils/form/navigate-away-prompt.js create mode 100644 frontend/src/utils/form/navigate-away-prompt.md create mode 100644 frontend/src/utils/form/reactive-submit-button.js create mode 100644 frontend/src/utils/form/reactive-submit-button.md create mode 100644 frontend/src/utils/inputs/checkbox.js create mode 100644 frontend/src/utils/inputs/checkbox.md rename {static/css/utils => frontend/src/utils/inputs}/checkbox.scss (81%) create mode 100644 frontend/src/utils/inputs/file-input.js create mode 100644 frontend/src/utils/inputs/file-input.md create mode 100644 frontend/src/utils/inputs/inputs.js rename {static/css/utils => frontend/src/utils/inputs}/inputs.scss (82%) rename {static/css/utils => frontend/src/utils/inputs}/radio.scss (95%) create mode 100644 frontend/src/utils/mass-input/mass-input.js create mode 100644 frontend/src/utils/mass-input/mass-input.md create mode 100644 frontend/src/utils/modal/modal.js create mode 100644 frontend/src/utils/modal/modal.md rename {static/css/utils => frontend/src/utils/modal}/modal.scss (78%) create mode 100644 frontend/src/utils/show-hide/show-hide.js create mode 100644 frontend/src/utils/show-hide/show-hide.md rename static/css/utils/showHide.scss => frontend/src/utils/show-hide/show-hide.scss (55%) rename {static/js/utils => frontend/src/utils/tabber}/tabber.js (97%) rename {static/css/utils => frontend/src/utils/tabber}/tabber.scss (100%) create mode 100644 frontend/src/utils/tooltips/tooltips.js rename static/css/utils/tooltip.scss => frontend/src/utils/tooltips/tooltips.scss (87%) create mode 100644 frontend/src/utils/utils.js rename {static/css => frontend}/vendor/flatpickr.css (97%) create mode 100644 frontend/vendor/fontawesome.css create mode 100644 frontend/vendor/main.js create mode 100644 karma.conf.js create mode 100644 messages/frontend/de.msg create mode 100644 models/invitations create mode 100644 models/materials delete mode 100644 models/rooms create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 src/Data/Aeson/Types/Instances.hs create mode 100644 src/Data/CryptoID/Instances.hs create mode 100644 src/Data/Fixed/Instances.hs create mode 100644 src/Data/HashMap/Strict/Instances.hs create mode 100644 src/Data/HashSet/Instances.hs create mode 100644 src/Data/Maybe/Instances.hs create mode 100644 src/Data/NonNull/Instances.hs create mode 100644 src/Data/Set/Instances.hs create mode 100644 src/Data/Sum/Instances.hs create mode 100644 src/Data/Time/Calendar/Instances.hs create mode 100644 src/Data/Time/Clock/Instances.hs create mode 100644 src/Data/Time/Format/Instances.hs create mode 100644 src/Data/Time/LocalTime/Instances.hs create mode 100644 src/Data/UUID/Instances.hs create mode 100644 src/Data/Universe/Instances/Reverse/MonoTraversable.hs create mode 100644 src/Data/Universe/TH.hs create mode 100644 src/Data/Vector/Instances.hs create mode 100644 src/Database/Persist/Class/Instances.hs delete mode 100644 src/Database/Persist/Sql/Instances.hs create mode 100644 src/Database/Persist/Types/Instances.hs create mode 100644 src/Handler/Exam.hs create mode 100644 src/Handler/Health.hs create mode 100644 src/Handler/Material.hs create mode 100644 src/Handler/Tutorial.hs create mode 100644 src/Handler/Utils/Communication.hs create mode 100644 src/Handler/Utils/Corrections.hs create mode 100644 src/Handler/Utils/Exam.hs create mode 100644 src/Handler/Utils/Form/MassInput/Liveliness.hs create mode 100644 src/Handler/Utils/Form/MassInput/TH.hs create mode 100644 src/Handler/Utils/Form/Occurrences.hs create mode 100644 src/Handler/Utils/Invitations.hs create mode 100644 src/Handler/Utils/Tokens.hs create mode 100644 src/Handler/Utils/Tutorial.hs create mode 100644 src/Import/NoModel.hs create mode 100644 src/Jobs/Handler/Invitation.hs create mode 100644 src/Jobs/Handler/SendCourseCommunication.hs create mode 100644 src/Jobs/Handler/SendNotification/Utils.hs create mode 100644 src/Jobs/HealthReport.hs create mode 100644 src/Jose/Jwt/Instances.hs create mode 100644 src/Language/Haskell/TH/Instances.hs create mode 100644 src/Model/Tokens.hs create mode 100644 src/Model/Types/Common.hs create mode 100644 src/Model/Types/Course.hs create mode 100644 src/Model/Types/DateTime.hs create mode 100644 src/Model/Types/Exam.hs create mode 100644 src/Model/Types/Health.hs create mode 100644 src/Model/Types/Mail.hs create mode 100644 src/Model/Types/Misc.hs create mode 100644 src/Model/Types/Security.hs create mode 100644 src/Model/Types/Sheet.hs create mode 100644 src/Model/Types/Submission.hs rename src/Model/Types/{ => TH}/JSON.hs (77%) rename src/Model/Types/{ => TH}/Wordlist.hs (95%) create mode 100644 src/Network/Mime/TH.hs create mode 100644 src/System/FilePath/Instances.hs create mode 100644 src/Text/Blaze/Instances.hs create mode 100644 src/Time/Types/Instances.hs create mode 100644 src/Utils/Frontend/I18n.hs create mode 100644 src/Utils/Frontend/Modal.hs delete mode 100644 src/Utils/Modal.hs create mode 100644 src/Utils/Occurrences.hs create mode 100644 src/Utils/Tokens.hs create mode 100644 src/Web/PathPieces/Instances.hs create mode 100644 stackage.nix delete mode 100644 static/css/vendor/fontawesome.css rename static/{css => fonts}/fonts.css (100%) create mode 120000 static/img/lmu/sigillum.svg delete mode 100644 static/js/utils/alerts.js delete mode 100644 static/js/utils/asidenav.js delete mode 100644 static/js/utils/asyncForm.js delete mode 100644 static/js/utils/asyncTable.js delete mode 100644 static/js/utils/asyncTableFilter.js delete mode 100644 static/js/utils/checkAll.js delete mode 100644 static/js/utils/form.js delete mode 100644 static/js/utils/httpClient.js delete mode 100644 static/js/utils/inputs.js delete mode 100644 static/js/utils/modal.js delete mode 100644 static/js/utils/setup.js delete mode 100644 static/js/utils/showHide.js delete mode 100644 static/js/vendor/flatpickr.js delete mode 100644 static/js/vendor/zepto.js create mode 100755 sync-versions.hs create mode 100644 templates/corrections-assign.hamlet create mode 100644 templates/corrections-overview.hamlet create mode 100644 templates/course/lecturerMassInput/add.hamlet create mode 100644 templates/course/lecturerMassInput/cellInvitation.hamlet create mode 100644 templates/course/lecturerMassInput/cellKnown.hamlet create mode 100644 templates/course/lecturerMassInput/layout.hamlet create mode 100644 templates/exam-edit.hamlet create mode 100644 templates/exam-list.hamlet create mode 100644 templates/exam-new.hamlet create mode 100644 templates/exam-show.cassius create mode 100644 templates/exam-show.hamlet create mode 100644 templates/i18n/README_i18n.txt create mode 100644 templates/i18n/changelog/de.hamlet create mode 100644 templates/i18n/corrections-upload-instructions/de.hamlet rename templates/{ => i18n}/data-protection/de.hamlet (97%) rename templates/{featureList.hamlet => i18n/featureList/de.hamlet} (89%) create mode 100644 templates/i18n/html-input/de.hamlet rename templates/{ => i18n}/imprint/de.hamlet (100%) create mode 100644 templates/i18n/info-lecturer/de.hamlet create mode 100644 templates/i18n/knownBugs/de.hamlet create mode 100644 templates/i18n/profile/tokenExplanation/de.hamlet create mode 100644 templates/i18n/sheet-edit/de.hamlet delete mode 100644 templates/info-lecturer/de.hamlet create mode 100644 templates/mail/correctorInvitation.hamlet create mode 100644 templates/mail/invitation.hamlet create mode 100644 templates/mail/lecturerInvitation.hamlet create mode 100644 templates/material-list.hamlet create mode 100644 templates/material-show.hamlet create mode 100644 templates/messages/courseInvitationAlreadyRegistered.hamlet create mode 100644 templates/messages/courseInvitationRegisteredWithoutField.hamlet create mode 100644 templates/messages/submissionsAssignNotFound.hamlet delete mode 100644 templates/multiFileField.hamlet delete mode 100644 templates/multiFileField.lucius delete mode 100644 templates/profile.hamlet create mode 100644 templates/profile/profile.hamlet create mode 100644 templates/profile/profile.julius create mode 100644 templates/sheetCorrectors/add.hamlet create mode 100644 templates/sheetCorrectors/cell.hamlet create mode 100644 templates/sheetCorrectors/layout.hamlet delete mode 100644 templates/table/layout.julius create mode 100644 templates/tutorial-edit.hamlet create mode 100644 templates/tutorial-list.hamlet create mode 100644 templates/tutorial-new.hamlet create mode 100644 templates/tutorial-participants.hamlet create mode 100644 templates/tutorial/tutorMassInput/add.hamlet create mode 100644 templates/tutorial/tutorMassInput/cellInvitation.hamlet create mode 100644 templates/tutorial/tutorMassInput/cellKnown.hamlet create mode 100644 templates/tutorial/tutorMassInput/layout.hamlet delete mode 100644 templates/widgets/alerts/alerts.julius delete mode 100644 templates/widgets/asidenav/asidenav.julius create mode 100644 templates/widgets/bonusRule.hamlet create mode 100644 templates/widgets/communication/recipientAdd.hamlet create mode 100644 templates/widgets/communication/recipientEmail.hamlet create mode 100644 templates/widgets/communication/recipientLayout.hamlet create mode 100644 templates/widgets/communication/recipientLayout.julius create mode 100644 templates/widgets/communication/recipientLayout.lucius create mode 100644 templates/widgets/communication/recipientName.hamlet delete mode 100644 templates/widgets/course-teaser/course-teaser.julius create mode 100644 templates/widgets/date-time/yet-invisible.hamlet create mode 100644 templates/widgets/email.hamlet delete mode 100644 templates/widgets/form/form.julius create mode 100644 templates/widgets/gradingKey.cassius create mode 100644 templates/widgets/gradingKey.hamlet create mode 100644 templates/widgets/invitation-site.hamlet create mode 100644 templates/widgets/massinput/examCorrectors/add.hamlet create mode 100644 templates/widgets/massinput/examCorrectors/cellInvitation.hamlet create mode 100644 templates/widgets/massinput/examCorrectors/cellKnown.hamlet create mode 100644 templates/widgets/massinput/examCorrectors/layout.hamlet create mode 100644 templates/widgets/massinput/examParts/add.hamlet create mode 100644 templates/widgets/massinput/examParts/form.hamlet create mode 100644 templates/widgets/massinput/examParts/layout.hamlet create mode 100644 templates/widgets/massinput/examRooms/add.hamlet create mode 100644 templates/widgets/massinput/examRooms/form.hamlet create mode 100644 templates/widgets/massinput/examRooms/layout.hamlet create mode 100644 templates/widgets/massinput/list/layout.hamlet create mode 100644 templates/widgets/massinput/massinput-standalone-wrapper.hamlet create mode 100644 templates/widgets/massinput/massinput-standalone.hamlet delete mode 100644 templates/widgets/massinput/massinput.julius create mode 100644 templates/widgets/massinput/massinput.lucius create mode 100644 templates/widgets/massinput/submissionUsers/add.hamlet create mode 100644 templates/widgets/massinput/submissionUsers/cellInvitation.hamlet create mode 100644 templates/widgets/massinput/submissionUsers/cellKnown.hamlet create mode 100644 templates/widgets/massinput/submissionUsers/layout.hamlet create mode 100644 templates/widgets/massinput/uploadSpecificFiles/add.hamlet create mode 100644 templates/widgets/massinput/uploadSpecificFiles/form.hamlet create mode 100644 templates/widgets/massinput/uploadSpecificFiles/layout.hamlet delete mode 100644 templates/widgets/modal/modal.julius delete mode 100644 templates/widgets/multi-action/multi-action-collect.hamlet create mode 100644 templates/widgets/multiFileField.hamlet create mode 100644 templates/widgets/multiFileField.lucius delete mode 100644 templates/widgets/navbar/navbar.julius create mode 100644 templates/widgets/occurrence/cell.hamlet create mode 100644 templates/widgets/occurrence/cell/except-no-occur.hamlet create mode 100644 templates/widgets/occurrence/cell/except-no-occurr.hamlet create mode 100644 templates/widgets/occurrence/cell/except-occur.hamlet create mode 100644 templates/widgets/occurrence/cell/except-occurr.hamlet create mode 100644 templates/widgets/occurrence/cell/weekly.hamlet create mode 100644 templates/widgets/occurrence/form/except-add.hamlet create mode 100644 templates/widgets/occurrence/form/except-layout.hamlet create mode 100644 templates/widgets/occurrence/form/except-no-occur.hamlet create mode 100644 templates/widgets/occurrence/form/except-occur.hamlet create mode 100644 templates/widgets/occurrence/form/scheduled-add.hamlet create mode 100644 templates/widgets/occurrence/form/scheduled-layout.hamlet create mode 100644 templates/widgets/occurrence/form/weekly.hamlet delete mode 100644 templates/widgets/pageaction/pageaction.julius create mode 100644 templates/widgets/specificFileField.hamlet create mode 100644 templates/widgets/zipFileField.hamlet create mode 100644 test/FoundationSpec.hs create mode 100644 test/Handler/Utils/SubmissionSpec.hs create mode 100644 test/Test/QuickCheck/Classes/Binary.hs create mode 100644 webpack.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..857e7ba32 --- /dev/null +++ b/.babelrc @@ -0,0 +1,9 @@ +{ + "presets": [ + ["@babel/preset-env", { "useBuiltIns": "usage" }] + ], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/plugin-proposal-class-properties", { "loose": true }] + ] +} diff --git a/.directory b/.directory index 59c2c250d..9e958424d 100644 --- a/.directory +++ b/.directory @@ -1,5 +1,5 @@ [Dolphin] -Timestamp=2018,3,14,10,57,55 +Timestamp=2019,6,26,19,32,25 Version=4 [Settings] diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..6fcef7c27 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "env": { + "browser": true, + "es6": true, + "jasmine": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly", + "flatpickr": "readonly", + "$": "readonly" + }, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 2018, + "ecmaFeatures": { + "legacyDecorators": true + } + }, + "rules": { + "no-console": "off", + "no-extra-semi": "off", + "semi": ["error", "always"], + "comma-dangle": ["error", "always-multiline"], + "quotes": ["error", "single"] + } +} diff --git a/.gitignore b/.gitignore index b85a1c848..0fb3c32c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ dist* +static/bundles/ static/tmp/ static/combined/ +node_modules/ *.hi *.o *.sqlite3 @@ -31,4 +33,5 @@ src/Handler/Course.SnapCustom.hs .stack-work-* .directory tags -test.log \ No newline at end of file +test.log +*.dump-splices diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8b60430d0..4c18542ba 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -14,6 +14,7 @@ "reveal": "always", "focus": false, "panel": "dedicated", + "clear": true, "showReuseMessage": false } }, @@ -43,6 +44,31 @@ "panel": "dedicated", "showReuseMessage": false } + }, + { + "type": "npm", + "script": "yesod:lint", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "yesod:start", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "start", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "frontend:lint", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "lint", + "problemMatcher": [] } ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..6828319d7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog + +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. + +## 1.0.0 (2019-07-03) + + +### Bug Fixes + +* **sheet corrector assigment:** minor bugfix ([749cd2f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/749cd2f)) +* async table js util now knows current random css prefix ([cc90faf](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/cc90faf)) +* **correction assignment:** correcting lecturer's names are shown now ([16c556b](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/16c556b)) +* **corrector assignment:** sheet tabel mixed up columns sorted ([d07f53e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d07f53e)) +* **datepicker:** hide number input spinners in datepicker ([2073130](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2073130)) +* **exam grading keys:** Fix spacing ([24aacef](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/24aacef)) +* **exams:** Fix registration ([1684da0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/1684da0)) +* **fe:** style notifications acceptably for now ([fc80f08](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fc80f08)) +* **fe-async-table:** Emulate no-js behaviour when handling pagesize ([28dcc8d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/28dcc8d)) +* **fe-check-all:** use arrow fn to keep scope in event listeners ([09e681e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/09e681e)) +* **fe-deflist:** avoid horizontal scroll on pages with deflist ([16d422d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/16d422d)) +* **Help Widget, Corrector Assignment:** Modal Form closes in place; assign alerts ([89d5364](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/89d5364)), closes [#195](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/195) +* **info-lecturer:** Touch ups ([e1e26ab](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e1e26ab)) +* **many occurrences throughout the project:** Fix typo: occurence -> occurrence everywhere ([96387cb](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/96387cb)) +* filter submission by not having corrector ([3bded50](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/3bded50)) +* minor heat correction for correction overview ([5546849](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5546849)) +* **ratings:** disallow ratings for graded sheets without point value ([463b2b7](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/463b2b7)) +* **standard-version:** properly reset staging area before release ([5aa906e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5aa906e)) + + +### Features + +* **corrector-assignment:** show load/submission percentages ([228cd50](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/228cd50)) +* make pagesize changes load async ([6486120](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/6486120)) +* **development:** add commitlint to ensure proper commit msgs ([dd528c1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/dd528c1)) +* **development:** add standard-version for automatic changelog generation ([c495ef5](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c495ef5)) +* **exams:** CRU (no D) for exams ([67a50c9](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/67a50c9)) +* **exams:** exam registration ([99184ff](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/99184ff)) +* **exams:** Form validation ([6fb1399](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/6fb1399)) +* **fe-heatmap:** add css class heated for heatmap elements ([b09b876](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/b09b876)), closes [#405](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/405) +* **forms:** Introduce more convenient form validation ([f8d0b02](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/f8d0b02)) +* **standard-version:** allow adding additional changes to release ([7ed6fe4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7ed6fe4)) +* **standard-version:** complete release workflow ([605e62f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/605e62f)) + + +### Tests + +* Does ist build with everything except for `makeClassy ''Entity`? Probably the functional dependency is to blame?! ([bb552c4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/bb552c4)) +* removing makeCLassyFor maybe build works then? ([2550f74](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2550f74)) + + +### BREAKING CHANGES + +* **standard-version:** Start of new versioning schema diff --git a/ChangeLog.md b/ChangeLog.md deleted file mode 100644 index f35e0e155..000000000 --- a/ChangeLog.md +++ /dev/null @@ -1,81 +0,0 @@ - * Version 27.03.2019 - - Kurse Veranstalter können nun mehrere Dozenten und Assistenten selbst eintragen - - Erfassung Studiengangsdaten - - * Version 20.03.2019 - - Kursanmeldung benötigen assoziertes Hauptfach (für Studierende mit mehreren Hauptfächern) - - * Version 30.01.2019 - - Designänderungen - - * Version 16.01.2019 - - Links für Bequemlichkeiten hinzugefügt (z.B. aktuelles Übungsblatt) - - Liste zugewiesener Abgaben lassen sich nun filtern - - Bugfix: Wenn zwischen Anzeige und Empfang eines Tabellen-Formulars Zeilen verschwinden wird nun eine sinnvolle Fehlermeldung angezeigt - - * Version 30.11.2018 - - Bugfix: Übungsblätter im "bestehen nach Punkten"-Modus werden wieder korrekt gespeichert - - * Version 29.11.2018 - - Bugfix: Formulare innerhalb von Tabellen funktionieren nun auch nach Javascript-Seitenwechsel oder Ändern der Sortierung - - * Version 09.11.2018 - - Bugfix: Zahlreiche Knöpfe/Formulare funktionieren wieder bei eingeschaltetem Javascript - - Verschiedene Verbesserungen für Korrektoren - - * Version 19.10.2018 - - Benutzer können sich in der Testphase komplett selbst löschen - - Hilfe Widget - - Benachrichtigungen per eMail für einige Ereignisse - - * Version 18.09.2018 - - Tooltips funktionieren auch ohne JavaScript - - Kurskürzel müssen nur innerhalb eines Instituts eindeutig sein - - User Data zeigt nun alle momentan gespeicherten Datensätze an - - Unterstützung von Tabellenzusammenfassungen, z.B. Punktsummen - - Intelligente Verteilung von Abgaben auf Korrektoren (z.B. bei Krankheit) - - Übungsblätter können Abgabe von Dateien verbieten und angeben ob ZIP-Archive entpackt werden sollen - - * Version 06.08.2018 - - Einführung einer Option, ob Dateien automatisch heruntergeladen werden sollen - - * Version 01.08.2018 - - Verbesserter Campus-Login - (Ersatz einer C-Bibliothek mit undokumentierter Abhängigkeit durch selbst entwickelten Haskell-Code erlaubt nun auch Umlaute.) - - * Version 31.07.2018 - - Viele Verbesserung zur Anzeige von Korrekturen - - Kursliste über alle Semester hinweg (Top-Level-Navigation "Kurse"), wird in Zukunft Filter/Suchfunktion erhalten - - * Version 10.07.2018 - - Bugfixes, wählbares Format für Datum - - * Version 03.07.2018 - - Willkommen bei Uni2work aka "You-need-to-work!" - diff --git a/README.md b/README.md new file mode 100644 index 000000000..f61775faa --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# "Quick Start" Guide + +The following description applies to Ubuntu and similar debian based Linux distributions. + +## Prerequisites +These are the things you need to do/install before you can get started working on Uni2work. + +### Clone repository +Clone this repository and navigate into it +```sh +$ git clone https://gitlab.cip.ifi.lmu.de/jost/UniWorX.git && cd UniWorX +``` + +### `LDAP` +LDAP is needed to handle logins. + +Install: +```sh +sudo apt-get install slapd ldap-utils +``` + +### `PostgreSQL` +PostgreSQL will serve as database for Uni2work. + +Install: +```sh +$ sudo apt-get install postgresql +``` + +Switch to user *postgres* (got created during installation): +```sh +$ sudo -i -u postgres +``` + +Add new database user *uniworx*: +```sh +$ createuser --interactive +``` + +You'll get a prompt: + +```sh +Enter name of role to add: uniworx +Shall the new role be a superuser? (y/n) [not exactly sure. Guess not?] +Password: uniworx +... +``` + +Create database *uniworx*: +```sh +$ psql -c 'create database uniworx owner uniworx' +$ psql -c 'create database uniworx_test owner uniworx' +``` + +After you added the database switch back to your own user with `Ctrl + D`. + +To properly access the database you now need to add a new linux user called *uniworx*. Enter "uniworx" as the password. +```sh +$ sudo adduser uniworx +``` + +### `Stack` +Stack is a toolbox for "Haskellers" to aid in developing Haskell projects. + +Install: +```sh +$ curl -sSL https://get.haskellstack.org/ | sh +``` + +Setup stack and install dependencies. This needs to be run from inside the directory you cloned this repository to: +```sh +$ stack setup +``` + +During this step or the next you might get an error that says something about missing C libraries for `ldap` and `lber`. You can install these using +```sh +$ sudo apt-get install libsasl2-dev libldap2-dev +``` + +If you get an error that says *You need to install postgresql-server-dev-X.Y for building a server-side extension or libpq-dev for building a client-side application.* +Go ahead and install `libpq-dev` with +```sh +$ sudo apt-get install libpq-dev +``` + +Other packages you might need to install during this process: +```sh +$ sudo apt-get install pkg-config +$ sudo apt-get install libsodium-dev +``` + +Build the app: +```sh +$ stack build +``` + +This might take a few minutes... if not hours... be prepared. + +install yesod: +```sh +$ stack install yesod-bin --install-ghc +``` + +### `Node` & `npm` +Node and Npm are needed to compile the frontend. + +Install: +```sh +$ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - +$ sudo apt-get install -y nodejs +``` + +### Add dummy data to the database +After building the app you can prepare the database and add some dummy data: +```sh +$ ./db.sh -f +``` + +## Run Uni2work +```sh +$ npm start +``` + +This will compile both frontend and backend and will start Uni2work in development mode (might take a few minutes the first time). It will keep running and will watch any file changes to automatically re-compile the application if necessary. + +If you followed the steps above you should now be able to visit http://localhost:3000 and login as one of the accounts from the Development-Logins dropdown. + +## Troubleshooting + +Please see the [wiki](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/wikis/home) for more infos. diff --git a/assets/lmu/logo.svg b/assets/lmu/logo.svg new file mode 100644 index 000000000..6b72bb7b9 --- /dev/null +++ b/assets/lmu/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/lmu/sigillum.svg b/assets/lmu/sigillum.svg new file mode 100644 index 000000000..78538233a --- /dev/null +++ b/assets/lmu/sigillum.svg @@ -0,0 +1,19 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/assets/logo-o2.svg b/assets/logo-o2.svg deleted file mode 100644 index 80620673b..000000000 --- a/assets/logo-o2.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/logo.png b/assets/logo.png deleted file mode 100644 index 4ef03212ece5a17e763ac4dc0a1bae6fb6bf561f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6698 zcmV+_8r9{AP)BqOA=|uf46cNFkZ<@M2Nh z-t^Y0NK0TQfkvxns};ejw~FDFhXVCgtJSvJ2L&G}YQig-naoViUiXhgjA9_k*=Od= zyZDKsgokjQrIl!(}`%ifm#SB!)6cH%mB$AQvP{ z7B~n1%(S0~!^GM<#Ie0?727__X)i~-W24e*a~0wOfW{Nyd_bH7@D~8&_fujFz(1Mz z&w|BkAiNTeYJb4u&EcG~vz&PO!O6pTnI+S5C~g02pHN(S|{&io~P0W6^Wb9P!c=o*Mr&NDeY^ zYMKrGUqm!7TwcE=N4zw`;M(GWvAC=Pa9x^Dr8?p`xCnDzPUMJZc`N5Vm!)_Vz$qCr zr5%7aGZNVHUXFNHC0(fKn+&=afHT9^XpaV2+!|nlk|}^gYY~EuR`)JBJ7bs8%eaoCIm4CLGlSK_*4c>B*p{) zu4J64gf6Q9%rHDf<_h=T=F=Qi-dV{^R!G`b!M}#&x63d{+Bi0e2>(=%HRNi@uq(kk}r(RZ+yAm><#(4&+V_wa$2;a z>D`PWUVcsS8B*$-02gFHpXf2s z5CUkliK=}f5@`6EBH{tSdjv5p9N7AL+WUB}pl`g?<+Gn9U7xQ`KHPV$S)>DK6EvI= z4%FYJf^+~7sc75~X}6zEz*-a4J0ox9RFz2($1~(Ju29jWv?iW2SadNF8bBCoNKoxK zJG1;Bb2!JhM_MLd8lBs)N4f09o5SHi!z^Z)b?rl%(En4h%)tcO=&C8cC@qQC>5tKq z06a!zd18ZRnzCgv-5M!xycLy|(o7{sD;ky%QP3n`X_g6M^GV8$&T8M3v?89X+CLE* zHmZ8Zz=Q=R3^Np|sJ}f~hpX`L!bJc!t3P4jG*77bY!im;5sW8Y)kR-U3*vc#MduK+ zGSa!Ledhywf!fvL?U5F_D4ADDn7LjDwCeyU4|g%KS(d4Qm^S9G^qysD;<;A)PGG{5 z01DK(V7ONCoZEFFu19542a=3PV9R?T`jL7!mCf|W21y|{xoS#AS(12#!RZCmfel7a zubly3&O(N$eEGo{4q%!uW?O`BzB&|MYwL<0>k!6INrlVXv_CCgqD)4loC zMQ5glc*`fcbco-XRdd9(+IIqgVG7l?sZRla^G(OgN{K9tU)1*j`I2cZ)_9#sC!WXQ zz7xQg%$k|jfiEf*DEWC<2tvqzs&yMF+4+fQd0OdbNhO{qSX|7^0W)VMS?$|c2L5a* z;<5IQT7lA&>^U&eg8Y!rmlWa+s`3tEl51dgN);)+Mu9U={M|~#Yi-sKDrP|{Dh>z< zzJG9S@j%mw7q^K!0GyZvCuo>LVd4FAwjQ(+@wy@>%E&^56czfRQL(tZ%~ayKYy1}i zxFI>dHxE`Q?gLm2wd3qi`1F8*RNha{a|OK<%pu;oQVo)8k)#Jg4CE;kXSuRpp6H-L zrS1x2PYS0aNQmmbJR0it7w6jBTn3`^6b?3iREx)Wn}wPy2L@ZN?g-hbkfplY9i3=r zKi9yai1_2&AMX2p1gPoVj@AowmHyfZg~bgSv>+O&Ux_S*ySiuwn4VB1?qH<6VQ^pX zDP5tWg#=oz(CRijk$1|zin>Eew%S-izX@t9YCaV$uMZ--q3{(Nzto@0@o0JD3ILBO zbj*jgi`Zf%KAyX3@<=evSE%r}Z3+9gEfqyb0a%i<%w7ilvA-ucM~A!~;9Ux>-i@xH z_e8_QgJ%B`pe!5{2jo?U7H!&>)wXU~iHAsG9Gbb`G3Wyen+`yTtB>V#_hkns(rz|H zJkJXM2oS!bP~~?-0!@EJmcx^$e^uef-0p&Ns}TW_vig?*l?9OadUw_2k^Lr~wBbf5 zD#<{=KO*_^PUHY4R{13H>E1Z=$0Q#+eg;5Q)0aHh9D#ljZ*Xn#Kw`c@nXZAkxUQ)^ z+ezoF_MHS^s=8ekk17#VQ5TcUa{*PpHaq{(fAAq4=55>kkDv$--uUPDHx>6+8xv!@}?Z+09Q@PC@?AWnF)AY zRmqJ{&XRI5fI})|G0_}UR*GH|&pqVXOTai;(Ojyssa4WZi6ZqMLSZP7A(yKjW7k?$ zDH3;zr*ONkFI%lDYuDoBys8}KK2EUU>(w1l&}h~Tfs^x=Ls8NJ3puA(#Opc+U#?~= zpu1F@xY8b#URzg!g|f<<1UBuN=Dv!$Lqu4n$SlwBtniOGCh^$jn5oG5-5Yket8=yv z@C?xw0Z^G>@z+RMW1X2}4o7X(P?Jt(XB)n8OyUvoOhrtu!JH=}Sx>rE{;>@HiAtFa z{KWK-5Uo;To%u*ki6c*4n5f{l5+LoqtT%OQyiV5nL8$FGv?E&H@MtoIPo&=s z1zFMzT;vW-J-$cc30OLZ&`_lKwnYt8*j9g zZ+#}4NjD(mJ)Jk=x zNJ9J`3Tp&*IZFTCRdXe|=v>Q36i9Mi5RW7&30xD?<5_F!Dl0|Yrah>>9X)M| z1R8#os%ep?W=$o1k{ltrojiOBWbaHyj|9v5bwi&08Nin*wsg%T^zWo{7YWa+e+|ID zjT=uY{xtDwy!jxUXK0s#Fw8faK=iAj=Bv9_AaEzdhlJdK2khbNI}3%`(cq+97dOF}*Y zRqs=~xh;7Ps<6sDqqhAo05BejVjkbk$1`5epMaNAQAs_Oa92$pDTR0n>JnxTuQOPb zTB_d5oR<>>7|wb5yOajkk6X^!g(iq+;U+4veW za8@VrD!n#h7%tpQ;Orq@WvQc`Vvj;?#>vM{{4b*A^=qv-Z4ye>_*ASb)kNN4`)C#Z z3F#s`{JV9fntN!=Ls0l&ryJ=Sqs?-ORi`dAy9(mvIrn@tN@!Y#g3C^NKfIv+gN!E~ zgWMl9*Mg7CFH<1=9Zj=cV`V?NZSLj|0eau?(bh&^6fE%t1xoujvH-5?qB{Y)UOD9m ztt9x$&V{x7*JpUGCDW+_pyQ37kdg7ka|M0n1Ty2bTtY~EBV5+>YI+!b7_PHIf=3Dx z7-3itWj66VA^){RFr%az6XS+(MZ@NF(FL)B-HkCqfK?EWc4aE*+|~Ze8N3E&R6-@H z3|BPVpHBK9s90+nAygE(CZ@MCb9?np0P|WXtX15NboWKd>;E_1bfRdjhYNrU4Lge@ zGwunaXO({}k*EVq=$OO2F=9XP{dClegp#!$A^KWfJL}tBS|4j*S2YnIQg! zh{mOu^Kv2`b)yLmD-e?>05)7wL1;<_V+rI3Cl4bjp9XlO3F2S3CG3}?tSN0j)DGb= zzSaz)6+l!#%*eE2;{)~m%7_;;Dy3a-^Q0x|LcV}W@mHOhuWbu#Z9;}2P7W0f znd1U5eicHtyrJo)Ku^ejEy1MtD=<4+(fAlL5F)`U*1AoA$#9uiQrm9Ibhv}QE4t#Z zWCH6Jg#!(1kf{(*Ks@?L0BpGAs16>HE_!nXy%Ruq7$(GDiTIXidBX~1Fr=j63O>($ zg2}kW6DB-(F=u-%KAT894HM$8L=+5{H{O8^hY)yoFZ0G z#giwX(rZgO=|a8$F)ubj^hZL7lJFKWn14smLh-fzDWS`4IjxsFCHddIBI`h#Q}qerlm?TCdOZ*l5j<``%75v zTtR$^0x^j;hwBsKb)C+PZ?{aBr$Q;NYX3x&;;#uXm$sKTJc}#`5sViVXT17xf4lCf z?>iLudFSS>oO)_972*jMpH0jgpg#U8ndrYF6^$FRopb|2-qWEVNI-<`pZI#8kYYG$a1Xz;7e_rj+NX*6rZ7!ao8a6+XL!oqpFN@uKs$z761c1wZ7L z9r^;r%;<{0;_D{pcyqL6$aSc!lsUR}V~Zvi0We(DiGc0Vxea?h^Jkyg)2n?8h*70r z+=uL1{A5+ZbXT?iE|WrGk{J&XQ8d$tW7lF!`m6tf<(CvD9Esh;7+`$Art(O6eNFd2 zI^x1rkX*|wmcjUCY(YY|mjn2zVUr*7-^AvSu5-$SZy^H{h~pi$p91I?@jC2@3MHfi z;zHAo=*MDO1rVEBHon&b6?oiQ{48AkB@g)z1!RZA6)bvp!{QmQ**^3^I zrT3_sL)4Qu&zO~_Gho8|*Bp9AJH-M}bSY2FpWER1bBPxjK6Dd+x0N_hz^$&;z7ulG znI3S}l#Bv!vm$G^JKS>dmR=JNGuP|HT&;%j7$7?M7r7-(50LCw0Y!0dW)AjrQ_y35 zygWE-0Vru($)KyZ`evRWISpsxh#uB#3?9ol_8p6Gh|ANwlm4iWRc%DyCECDwSI zx#hJeg46Rr2&odsjMYc$w=0hBpK%q`Bo2UqQ{7?hrrZi!6K=HMqA>OyCDMXN`wK@U zUbLd=T>$q$-DmJ4SEy)GZka81pa^;=faqpb@(B24B(UYZeiIJ>(2=+uDtgg(!bYIA z18TiPbIWZF6fD1_kVvcrU{@qE44sx9>n|OXc#(zM-e+Ky!nvNP$Hf|O?rtvSU{HrW z@gNjd`(hx}o8HB}s`~EYyq^M4*$DY^PsqP0xAG}I#n$+40-*A?;X|iG^eW2KE8^{| zs5=CpQt8J=e1|hwba8I^Q-l1K{vu}Frc@~5)}x(d`%F9lh>RF=4_HOTfy1y1qW|-( z@{i5YAla7Ad#)7xIMlw%dnM95<-Xqj<=!_~nYmtP!8ZU5QfmucAy=c?i{ zEJYoFfeOV+mMHH_K4YI9e0Q*@8boD^^|6z5n$#PoGzSCQYjE-~UD&s&?$t)X%5b2e zPl>;MZYkxC#%~7r9mP61g*7~7G=(aMo>3T_ULb|N&V&YmZ-s^YVPF4IpNZG}gB`5| z%!cyJNxmTBb~c#uRwLUj-RhFz?IP9y#yG{IB@xZ;PaD*JHPH(P8aEM}Q5=UakR2V* z<<)qL_K}qVhs}y6?HMe^i2R}beleorD)3C^BEzXzeozP9f0yATqd+? zkfpe;slC54G$R9u*X{bqcG>7R3784>>FG`)y4HT_Xf5&OZ$FEsCeG2RH>?1;hWh7& z_}2j3ru+i%t#F{xPz5a|WN}sdeol-VP1NRA(#5s8>z1QqK&^ME9v2S)xYV?jU@VPR zG%PVHM@fh7XtP`bV6%BDrDE3POSzP)qihdB|71NbUNMt&0P3R04N+3!L)rFN7@S_v zPO?Z@~S)1KBf5wJ6bJ8JOFTq zrXCOIF90~vWOeTZ5QqdC>azL(5%T$TphihGr=#ON7Pbk|vdzi@2g-BeJgfX;S?5Lo zg{Ets@F+{Uuq#tKYv8IW8P)X~?CVMRxfUU%w{3pYU(_z08Hx$|#zSDMq7n$lLK_j^ z?h41NRc!-S#n7db21ML>73h5T>Pf;+9s;C45@`CPdYfUEvs>+(A{cch9qB(0`v3&( zT0EF(U3UtWUsBj%PgJ0@ti<3X{cJ=sO>3{H-)7EcW%5Z&5I+T=Ac^bLMnv~9i>1eU zaAJ8ME;xCZBzqZv`2YqbQ-=bW(H3ZEFwaXe+aC$~#uK3~DL&t)5e4B9X8u()(6}R= z2R#+jO{h4#Orq8r~`0(iq`TCfFIU$tZQA+_;)LkZ9vF- zx=z|mW}cOVQ)*9mS4cTs^+6(K#Ornv+W>R#`Hl_7YfN~WnK!oQ9r^?2yqrkMIBLh) z^W%dj=`5#pH7`kVvuS#jZG8D>D|eTFb=c$Na0{j(l)w-cawG^RG1F*fjOl8p zZp8#wX7IA;h@rP&=6XG;_BB;Ac2^h80Mi3d`{*QxKAc<`_--`Nus$Uldup!>{9lsb zhe60aaZIv5r$L|98fbbmRU;57{p2d@{}w^scxEhx+GmqF^yJnO1taX3kV?{}+~R%8 z`40L@nD|!!jmq)E6ud=5v-|r-BBLx`cZ4e%Hb)%!r*~ODJ;xJekO1B7j>OKk3h69m z@w&rR?Vm`@s{x#o-BK z$c0uUonh=UBSCD>M`TMAg(mV+$} zPA`ZdKAVXHP*_SeO;&9;gF>Eg`#tHJb)Eioh$}0Fr*OLuByV6~1^`Ef(oF}rkwJGy zTjb-YZ0bnwZzqG`S>Ycc1(y=|Y7l+Js;BloKoYQni5?M}w%)AFnH=#{`hr34i2!|_ zh%Q5CEBSQkDij6iSq5)_4UZ)&KQ2c+Gf;bl+ns3hlBB5&OakFNnELp+Pv`*fCjxdb zuqBd+Heu0o(M(^G?CdqMti+S=(8sgJ<0WG(G0p Ap8x;= diff --git a/build.sh b/build.sh index 962ccc1ee..9b4f5a2e2 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -exec -- stack build --fast --flag uniworx:-library-only --flag uniworx:dev +exec -- stack build --fast --flag uniworx:-library-only --flag uniworx:dev $@ echo Build task completed. diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 000000000..28fe5c5bf --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +module.exports = {extends: ['@commitlint/config-conventional']} diff --git a/config/archive-types b/config/archive-types new file mode 100644 index 000000000..0599971bb --- /dev/null +++ b/config/archive-types @@ -0,0 +1,40 @@ +# Simple list of mime-types corresponding to archive-formats +# +# Comments are empty lines and any line for which the first non-whitespace symbol is ‘#’ +# +# Format is a single mime-type per line (may not contain whitespace) +# +# Largely copied from https://en.wikipedia.org/wiki/List_of_archive_formats + +application/x-archive +application/x-cpio +application/x-bcpio +application/x-shar +application/x-iso9660-image +application/x-sbx +application/x-tar +application/x-7z-compressed +application/x-ace-compressed +application/x-astrotite-afa +application/x-alz-compressed +application/vnd.android.package-archive +application/x-arj +application/x-b1 +application/vnd.ms-cab-compressed +application/x-cfs-compressed +application/x-dar +application/x-dgc-compressed +application/x-apple-diskimage +application/x-gca-compressed +application/java-archive +application/x-lzh +application/x-lzx +application/x-rar-compressed +application/x-stuffit +application/x-stuffitx +application/x-gtar +application/x-ms-wim +application/x-xar +application/zip +application/x-zoo +application/x-par2 \ No newline at end of file diff --git a/config/mimetypes b/config/mimetypes new file mode 100644 index 000000000..dd3fe4224 --- /dev/null +++ b/config/mimetypes @@ -0,0 +1,788 @@ +# Mapping of mime-types to file extensions +# +# Comments are empty lines and any line for which the first non-whitespace symbol is ‘#’ +# +# Format is a single mime-type per line (may not contain whitespace) followed by a whitespace separated list of zero or more file extension (without leading ‘.’) +# Any file extension may occur at most once within this file +# +# Extensions are compared case-insensitive (see `Data.Text.toLower`) + +application/andrew-inset ez +application/applixware aw +application/atom+xml atom +application/atomcat+xml atomcat +application/atomsvc+xml atomsvc +application/ccxml+xml ccxml +application/cdmi-capability cdmia +application/cdmi-container cdmic +application/cdmi-domain cdmid +application/cdmi-object cdmio +application/cdmi-queue cdmiq +application/cu-seeme cu +application/davmount+xml davmount +application/docbook+xml dbk +application/dssc+der dssc +application/dssc+xml xdssc +application/ecmascript ecma +application/emma+xml emma +application/epub+zip epub +application/exi exi +application/font-tdpfr pfr +application/font-woff woff +application/font-woff2 woff2 +application/futuresplash spl +application/gml+xml gml +application/gpx+xml gpx +application/gxf gxf +application/hyperstudio stk +application/inkml+xml inkml ink +application/ipfix ipfix +application/java-archive war jar ear +application/java-serialized-object ser +application/java-vm class +application/javascript js +application/json json +application/jsonml+json jsonml +application/lost+xml lostxml +application/mac-binhex40 hqx +application/mac-compactpro cpt +application/mads+xml mads +application/marc mrc +application/marcxml+xml mrcx +application/mathematica nb mb ma +application/mathml+xml mathml +application/mbox mbox +application/mediaservercontrol+xml mscml +application/metalink+xml metalink +application/metalink4+xml meta4 +application/mets+xml mets +application/mods+xml mods +application/mp21 mp21 m21 +application/mp4 mp4s +application/msword dot doc +application/mxf mxf +application/octet-stream so pkg msp msm mar lrf img elc dump dms distz dist deploy bpk bin +application/oda oda +application/oebps-package+xml opf +application/ogg ogx +application/omdoc+xml omdoc +application/onenote onetoc2 onetoc onetmp onepkg +application/oxps oxps +application/patch-ops-error+xml xer +application/pdf pdf +application/pgp-encrypted pgp +application/pgp-signature sig +application/pics-rules prf +application/pkcs10 p10 +application/pkcs7-mime p7m p7c +application/pkcs7-signature p7s +application/pkcs8 p8 +application/pkix-attr-cert ac +application/pkix-cert cer +application/pkix-crl crl +application/pkix-pkipath pkipath +application/pkixcmp pki +application/pls+xml pls +application/postscript ps eps ai +application/prs.cww cww +application/pskc+xml pskcxml +application/rdf+xml rdf +application/reginfo+xml rif +application/relax-ng-compact-syntax rnc +application/resource-lists+xml rl +application/resource-lists-diff+xml rld +application/rls-services+xml rs +application/rpki-ghostbusters gbr +application/rpki-manifest mft +application/rpki-roa roa +application/rsd+xml rsd +application/rss+xml rss +application/rtf rtf +application/sbml+xml sbml +application/scvp-cv-request scq +application/scvp-cv-response scs +application/scvp-vp-request spq +application/scvp-vp-response spp +application/sdp sdp +application/set-payment-initiation setpay +application/set-registration-initiation setreg +application/shf+xml shf +application/smil+xml smil smi +application/sparql-query rq +application/sparql-results+xml srx +application/srgs gram +application/srgs+xml grxml +application/sru+xml sru +application/ssdl+xml ssdl +application/ssml+xml ssml +application/tei+xml teicorpus tei +application/thraud+xml tfi +application/timestamped-data tsd +application/vnd.3gpp.pic-bw-large plb +application/vnd.3gpp.pic-bw-small psb +application/vnd.3gpp.pic-bw-var pvb +application/vnd.3gpp2.tcap tcap +application/vnd.3m.post-it-notes pwn +application/vnd.accpac.simply.aso aso +application/vnd.accpac.simply.imp imp +application/vnd.acucobol acu +application/vnd.acucorp atc acutc +application/vnd.adobe.air-application-installer-package+zip air +application/vnd.adobe.formscentral.fcdt fcdt +application/vnd.adobe.fxp fxpl fxp +application/vnd.adobe.xdp+xml xdp +application/vnd.adobe.xfdf xfdf +application/vnd.ahead.space ahead +application/vnd.airzip.filesecure.azf azf +application/vnd.airzip.filesecure.azs azs +application/vnd.amazon.ebook azw +application/vnd.americandynamics.acc acc +application/vnd.amiga.ami ami +application/vnd.android.package-archive apk +application/vnd.anser-web-certificate-issue-initiation cii +application/vnd.anser-web-funds-transfer-initiation fti +application/vnd.antix.game-component atx +application/vnd.apple.installer+xml mpkg +application/vnd.apple.mpegurl m3u8 +application/vnd.aristanetworks.swi swi +application/vnd.astraea-software.iota iota +application/vnd.audiograph aep +application/vnd.blueice.multipass mpm +application/vnd.bmi bmi +application/vnd.businessobjects rep +application/vnd.chemdraw+xml cdxml +application/vnd.chipnuts.karaoke-mmd mmd +application/vnd.cinderella cdy +application/vnd.claymore cla +application/vnd.cloanto.rp9 rp9 +application/vnd.clonk.c4group c4u c4p c4g c4f c4d +application/vnd.cluetrust.cartomobile-config c11amc +application/vnd.cluetrust.cartomobile-config-pkg c11amz +application/vnd.commonspace csp +application/vnd.contact.cmsg cdbcmsg +application/vnd.cosmocaller cmc +application/vnd.crick.clicker clkx +application/vnd.crick.clicker.keyboard clkk +application/vnd.crick.clicker.palette clkp +application/vnd.crick.clicker.template clkt +application/vnd.crick.clicker.wordbank clkw +application/vnd.criticaltools.wbs+xml wbs +application/vnd.ctc-posml pml +application/vnd.cups-ppd ppd +application/vnd.curl.car car +application/vnd.curl.pcurl pcurl +application/vnd.dart dart +application/vnd.data-vision.rdz rdz +application/vnd.dece.data uvvf uvvd uvf uvd +application/vnd.dece.ttml+xml uvvt uvt +application/vnd.dece.unspecified uvx uvvx +application/vnd.dece.zip uvz uvvz +application/vnd.denovo.fcselayout-link fe_launch +application/vnd.dna dna +application/vnd.dolby.mlp mlp +application/vnd.dpgraph dpg +application/vnd.dreamfactory dfac +application/vnd.ds-keypoint kpxx +application/vnd.dvb.ait ait +application/vnd.dvb.service svc +application/vnd.dynageo geo +application/vnd.ecowin.chart mag +application/vnd.enliven nml +application/vnd.epson.esf esf +application/vnd.epson.msf msf +application/vnd.epson.quickanime qam +application/vnd.epson.salt slt +application/vnd.epson.ssf ssf +application/vnd.eszigno3+xml et3 es3 +application/vnd.ezpix-album ez2 +application/vnd.ezpix-package ez3 +application/vnd.fdf fdf +application/vnd.fdsn.mseed mseed +application/vnd.fdsn.seed seed dataless +application/vnd.flographit gph +application/vnd.fluxtime.clip ftc +application/vnd.framemaker maker frame fm book +application/vnd.frogans.fnc fnc +application/vnd.frogans.ltf ltf +application/vnd.fsc.weblaunch fsc +application/vnd.fujitsu.oasys oas +application/vnd.fujitsu.oasys2 oa2 +application/vnd.fujitsu.oasys3 oa3 +application/vnd.fujitsu.oasysgp fg5 +application/vnd.fujitsu.oasysprs bh2 +application/vnd.fujixerox.ddd ddd +application/vnd.fujixerox.docuworks xdw +application/vnd.fujixerox.docuworks.binder xbd +application/vnd.fuzzysheet fzs +application/vnd.genomatix.tuxedo txd +application/vnd.geogebra.file ggb +application/vnd.geogebra.tool ggt +application/vnd.geometry-explorer gre gex +application/vnd.geonext gxt +application/vnd.geoplan g2w +application/vnd.geospace g3w +application/vnd.gmx gmx +application/vnd.google-earth.kml+xml kml +application/vnd.google-earth.kmz kmz +application/vnd.grafeq gqs gqf +application/vnd.groove-account gac +application/vnd.groove-help ghf +application/vnd.groove-identity-message gim +application/vnd.groove-injector grv +application/vnd.groove-tool-message gtm +application/vnd.groove-tool-template tpl +application/vnd.groove-vcard vcg +application/vnd.hal+xml hal +application/vnd.handheld-entertainment+xml zmm +application/vnd.hbci hbci +application/vnd.hhe.lesson-player les +application/vnd.hp-hpgl hpgl +application/vnd.hp-hpid hpid +application/vnd.hp-hps hps +application/vnd.hp-jlyt jlt +application/vnd.hp-pcl pcl +application/vnd.hp-pclxl pclxl +application/vnd.hydrostatix.sof-data sfd-hdstx +application/vnd.ibm.minipay mpy +application/vnd.ibm.modcap listafp list3820 afp +application/vnd.ibm.rights-management irm +application/vnd.ibm.secure-container sc +application/vnd.iccprofile icm icc +application/vnd.igloader igl +application/vnd.immervision-ivp ivp +application/vnd.immervision-ivu ivu +application/vnd.insors.igm igm +application/vnd.intercon.formnet xpx xpw +application/vnd.intergeo i2g +application/vnd.intu.qbo qbo +application/vnd.intu.qfx qfx +application/vnd.ipunplugged.rcprofile rcprofile +application/vnd.irepository.package+xml irp +application/vnd.is-xpr xpr +application/vnd.isac.fcs fcs +application/vnd.jam jam +application/vnd.jcp.javame.midlet-rms rms +application/vnd.jisp jisp +application/vnd.joost.joda-archive joda +application/vnd.kahootz ktz ktr +application/vnd.kde.karbon karbon +application/vnd.kde.kchart chrt +application/vnd.kde.kformula kfo +application/vnd.kde.kivio flw +application/vnd.kde.kontour kon +application/vnd.kde.kpresenter kpt kpr +application/vnd.kde.kspread ksp +application/vnd.kde.kword kwt kwd +application/vnd.kenameaapp htke +application/vnd.kidspiration kia +application/vnd.kinar knp kne +application/vnd.koan skt skp skm skd +application/vnd.kodak-descriptor sse +application/vnd.las.las+xml lasxml +application/vnd.llamagraphics.life-balance.desktop lbd +application/vnd.llamagraphics.life-balance.exchange+xml lbe +application/vnd.lotus-1-2-3 123 +application/vnd.lotus-approach apr +application/vnd.lotus-freelance pre +application/vnd.lotus-notes nsf +application/vnd.lotus-organizer org +application/vnd.lotus-screencam scm +application/vnd.lotus-wordpro lwp +application/vnd.macports.portpkg portpkg +application/vnd.mcd mcd +application/vnd.medcalcdata mc1 +application/vnd.mediastation.cdkey cdkey +application/vnd.mfer mwf +application/vnd.mfmp mfm +application/vnd.micrografx.flo flo +application/vnd.micrografx.igx igx +application/vnd.mif mif +application/vnd.mobius.daf daf +application/vnd.mobius.dis dis +application/vnd.mobius.mbk mbk +application/vnd.mobius.mqy mqy +application/vnd.mobius.msl msl +application/vnd.mobius.plc plc +application/vnd.mobius.txf txf +application/vnd.mophun.application mpn +application/vnd.mophun.certificate mpc +application/vnd.mozilla.xul+xml xul +application/vnd.ms-artgalry cil +application/vnd.ms-cab-compressed cab +application/vnd.ms-excel xlw xlt xls xlm xlc xla +application/vnd.ms-excel.addin.macroenabled.12 xlam +application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb +application/vnd.ms-excel.sheet.macroenabled.12 xlsm +application/vnd.ms-excel.template.macroenabled.12 xltm +application/vnd.ms-fontobject eot +application/vnd.ms-htmlhelp chm +application/vnd.ms-ims ims +application/vnd.ms-lrm lrm +application/vnd.ms-officetheme thmx +application/vnd.ms-pki.seccat cat +application/vnd.ms-pki.stl stl +application/vnd.ms-powerpoint ppt pps pot +application/vnd.ms-powerpoint.addin.macroenabled.12 ppam +application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm +application/vnd.ms-powerpoint.slide.macroenabled.12 sldm +application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm +application/vnd.ms-powerpoint.template.macroenabled.12 potm +application/vnd.ms-project mpt mpp +application/vnd.ms-word.document.macroenabled.12 docm +application/vnd.ms-word.template.macroenabled.12 dotm +application/vnd.ms-works wps wks wdb wcm +application/vnd.ms-wpl wpl +application/vnd.ms-xpsdocument xps +application/vnd.mseq mseq +application/vnd.musician mus +application/vnd.muvee.style msty +application/vnd.mynfc taglet +application/vnd.neurolanguage.nlu nlu +application/vnd.nitf ntf nitf +application/vnd.noblenet-directory nnd +application/vnd.noblenet-sealer nns +application/vnd.noblenet-web nnw +application/vnd.nokia.n-gage.data ngdat +application/vnd.nokia.n-gage.symbian.install n-gage +application/vnd.nokia.radio-preset rpst +application/vnd.nokia.radio-presets rpss +application/vnd.novadigm.edm edm +application/vnd.novadigm.edx edx +application/vnd.novadigm.ext ext +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.database odb +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template odft +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-master odm +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-web oth +application/vnd.olpc-sugar xo +application/vnd.oma.dd2+xml dd2 +application/vnd.openofficeorg.extension oxt +application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +application/vnd.openxmlformats-officedocument.presentationml.slide sldx +application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx +application/vnd.openxmlformats-officedocument.presentationml.template potx +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx +application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx +application/vnd.osgeo.mapguide.package mgp +application/vnd.osgi.dp dp +application/vnd.osgi.subsystem esa +application/vnd.palm pqa pdb oprc +application/vnd.pawaafile paw +application/vnd.pg.format str +application/vnd.pg.osasli ei6 +application/vnd.picsel efif +application/vnd.pmi.widget wg +application/vnd.pocketlearn plf +application/vnd.powerbuilder6 pbd +application/vnd.previewsystems.box box +application/vnd.proteus.magazine mgz +application/vnd.publishare-delta-tree qps +application/vnd.pvi.ptid1 ptid +application/vnd.quark.quarkxpress qxt qxl qxd qxb qwt qwd +application/vnd.realvnc.bed bed +application/vnd.recordare.musicxml mxl +application/vnd.recordare.musicxml+xml musicxml +application/vnd.rig.cryptonote cryptonote +application/vnd.rim.cod cod +application/vnd.rn-realmedia rm +application/vnd.rn-realmedia-vbr rmvb +application/vnd.route66.link66+xml link66 +application/vnd.sailingtracker.track st +application/vnd.seemail see +application/vnd.sema sema +application/vnd.semd semd +application/vnd.semf semf +application/vnd.shana.informed.formdata ifm +application/vnd.shana.informed.formtemplate itp +application/vnd.shana.informed.interchange iif +application/vnd.shana.informed.package ipk +application/vnd.simtech-mindmapper twds twd +application/vnd.smaf mmf +application/vnd.smart.teacher teacher +application/vnd.solent.sdkm+xml sdkm sdkd +application/vnd.spotfire.dxp dxp +application/vnd.spotfire.sfs sfs +application/vnd.stardivision.calc sdc +application/vnd.stardivision.draw sda +application/vnd.stardivision.impress sdd +application/vnd.stardivision.math smf +application/vnd.stardivision.writer vor sdw +application/vnd.stardivision.writer-global sgl +application/vnd.stepmania.package smzip +application/vnd.stepmania.stepchart sm +application/vnd.sun.xml.calc sxc +application/vnd.sun.xml.calc.template stc +application/vnd.sun.xml.draw sxd +application/vnd.sun.xml.draw.template std +application/vnd.sun.xml.impress sxi +application/vnd.sun.xml.impress.template sti +application/vnd.sun.xml.math sxm +application/vnd.sun.xml.writer sxw +application/vnd.sun.xml.writer.global sxg +application/vnd.sun.xml.writer.template stw +application/vnd.sus-calendar susp sus +application/vnd.svd svd +application/vnd.symbian.install sisx sis +application/vnd.syncml+xml xsm +application/vnd.syncml.dm+wbxml bdm +application/vnd.syncml.dm+xml xdm +application/vnd.tao.intent-module-archive tao +application/vnd.tcpdump.pcap pcap dmp cap +application/vnd.tmobile-livetv tmo +application/vnd.trid.tpt tpt +application/vnd.triscape.mxs mxs +application/vnd.trueapp tra +application/vnd.ufdl ufdl ufd +application/vnd.uiq.theme utz +application/vnd.umajin umj +application/vnd.unity unityweb +application/vnd.uoml+xml uoml +application/vnd.vcx vcx +application/vnd.visio vsw vst vss vsd +application/vnd.visionary vis +application/vnd.vsf vsf +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/vnd.webturbo wtb +application/vnd.wolfram.player nbp +application/vnd.wordperfect wpd +application/vnd.wqd wqd +application/vnd.wt.stf stf +application/vnd.xara xar +application/vnd.xfdl xfdl +application/vnd.yamaha.hv-dic hvd +application/vnd.yamaha.hv-script hvs +application/vnd.yamaha.hv-voice hvp +application/vnd.yamaha.openscoreformat osf +application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg +application/vnd.yamaha.smaf-audio saf +application/vnd.yamaha.smaf-phrase spf +application/vnd.yellowriver-custom-menu cmp +application/vnd.zul zirz zir +application/vnd.zzazz.deck+xml zaz +application/voicexml+xml vxml +application/widget wgt +application/winhlp hlp +application/wsdl+xml wsdl +application/wspolicy+xml wspolicy +application/x-7z-compressed 7z +application/x-abiword abw +application/x-ace-compressed ace +application/x-apple-diskimage dmg +application/x-authorware-bin x32 vox u32 aab +application/x-authorware-map aam +application/x-authorware-seg aas +application/x-bcpio bcpio +application/x-bittorrent torrent +application/x-blorb blorb blb +application/x-bzip bz2 bz +application/x-bzip-compressed-tar tbz tar.bz2 +application/x-bzip2 boz +application/x-cbr cbz cbt cbr cba cb7 +application/x-cdlink vcd +application/x-cfs-compressed cfs +application/x-chat chat +application/x-chess-pgn pgn +application/x-cocoa cco +application/x-conference nsc +application/x-cpio cpio +application/x-csh csh +application/x-debian-package udeb deb +application/x-dgc-compressed dgc +application/x-director w3d swa fgd dxr dir dcr cxt cst cct +application/x-doom wad +application/x-dtbncx+xml ncx +application/x-dtbook+xml dtb +application/x-dtbresource+xml res +application/x-dvi dvi +application/x-envoy evy +application/x-eva eva +application/x-font-bdf bdf +application/x-font-ghostscript gsf +application/x-font-linux-psf psf +application/x-font-otf otf +application/x-font-pcf pcf +application/x-font-snf snf +application/x-font-ttf ttf ttc +application/x-font-type1 pfm pfb pfa afm +application/x-freearc arc +application/x-gca-compressed gca +application/x-glulx ulx +application/x-gnumeric gnumeric +application/x-gramps-xml gramps +application/x-gtar gtar +application/x-gzip gz +application/x-hdf hdf +application/x-install-instructions install +application/x-iso9660-image iso +application/x-java-archive-diff jardiff +application/x-java-jnlp-file jnlp +application/x-latex latex +application/x-lzh-compressed lzh lha +application/x-makeself run +application/x-mie mie +application/x-mobipocket-ebook prc mobi +application/x-ms-application application +application/x-ms-shortcut lnk +application/x-ms-wmd wmd +application/x-ms-xbap xbap +application/x-msaccess mdb +application/x-msbinder obd +application/x-mscardfile crd +application/x-msclip clp +application/x-msdownload msi exe dll com bat +application/x-msmediaview mvb m14 m13 +application/x-msmetafile wmz wmf emz emf +application/x-msmoney mny +application/x-mspublisher pub +application/x-msschedule scd +application/x-msterminal trm +application/x-mswrite wri +application/x-netcdf nc cdf +application/x-ns-proxy-autoconfig pac +application/x-nzb nzb +application/x-perl pm pl +application/x-pkcs12 pfx p12 +application/x-pkcs7-certificates spc p7b +application/x-pkcs7-certreqresp p7r +application/x-rar-compressed rar +application/x-redhat-package-manager rpm +application/x-research-info-systems ris +application/x-sea sea +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-silverlight-app xap +application/x-sql sql +application/x-stuffit sit +application/x-stuffitx sitx +application/x-subrip srt +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-t3vm-image t3 +application/x-tads gam +application/x-tar tar +application/x-tcl tk tcl +application/x-tex tex +application/x-tex-tfm tfm +application/x-texinfo texinfo texi +application/x-tgif obj +application/x-tgz tgz tar.gz +application/x-ustar ustar +application/x-wais-source src +application/x-x509-ca-cert pem der crt +application/x-xfig fig +application/x-xliff+xml xlf +application/x-xpinstall xpi +application/x-xz xz +application/x-zmachine z8 z7 z6 z5 z4 z3 z2 z1 +application/xaml+xml xaml +application/xcap-diff+xml xdf +application/xenc+xml xenc +application/xhtml+xml xhtml xht +application/xml xsl +application/xml-dtd dtd +application/xop+xml xop +application/xproc+xml xpl +application/xslt+xml xslt +application/xspf+xml xspf +application/xv+xml xvml xvm xhvml mxml +application/yang yang +application/yin+xml yin +application/zip zip +audio/adpcm adp +audio/basic snd au +audio/midi rmi midi mid kar +audio/mp4 mp4a +audio/mpeg mpga mp3 mp2a mp2 m3a m2a +audio/ogg spx ogg oga +audio/s3m s3m +audio/silk sil +audio/vnd.dece.audio uvva uva +audio/vnd.digital-winds eol +audio/vnd.dra dra +audio/vnd.dts dts +audio/vnd.dts.hd dtshd +audio/vnd.lucent.voice lvp +audio/vnd.ms-playready.media.pya pya +audio/vnd.nuera.ecelp4800 ecelp4800 +audio/vnd.nuera.ecelp7470 ecelp7470 +audio/vnd.nuera.ecelp9600 ecelp9600 +audio/vnd.rip rip +audio/webm weba +audio/x-aac aac +audio/x-aiff aiff aifc aif +audio/x-caf caf +audio/x-flac flac +audio/x-m4a m4a +audio/x-matroska mka +audio/x-mpegurl m3u +audio/x-ms-wax wax +audio/x-ms-wma wma +audio/x-pn-realaudio ram ra +audio/x-pn-realaudio-plugin rmp +audio/x-wav wav +audio/xm xm +chemical/x-cdx cdx +chemical/x-cif cif +chemical/x-cmdf cmdf +chemical/x-cml cml +chemical/x-csml csml +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +image/g3fax g3 +image/gif gif +image/ief ief +image/jpeg jpg jpeg jpe +image/ktx ktx +image/png png +image/prs.btif btif +image/sgi sgi +image/svg+xml svgz svg +image/tiff tiff tif +image/vnd.adobe.photoshop psd +image/vnd.dece.graphic uvvi uvvg uvi uvg +image/vnd.djvu djvu djv +image/vnd.dwg dwg +image/vnd.dxf dxf +image/vnd.fastbidsheet fbs +image/vnd.fpx fpx +image/vnd.fst fst +image/vnd.fujixerox.edmics-mmr mmr +image/vnd.fujixerox.edmics-rlc rlc +image/vnd.microsoft.icon ico +image/vnd.ms-modi mdi +image/vnd.ms-photo wdp +image/vnd.net-fpx npx +image/vnd.wap.wbmp wbmp +image/vnd.xiff xif +image/webp webp +image/x-3ds 3ds +image/x-cmu-raster ras +image/x-cmx cmx +image/x-freehand fhc fh7 fh5 fh4 fh +image/x-jng jng +image/x-mrsid-image sid +image/x-pcx pcx +image/x-pict pic pct +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-tga tga +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +message/rfc822 mime eml +model/iges igs iges +model/mesh silo msh mesh +model/vnd.collada+xml dae +model/vnd.dwf dwf +model/vnd.gdl gdl +model/vnd.gtw gtw +model/vnd.mts mts +model/vnd.vtu vtu +model/vrml wrl vrml +model/x3d+binary x3dbz x3db +model/x3d+vrml x3dvz x3dv +model/x3d+xml x3dz x3d +text/cache-manifest manifest appcache +text/calendar ifb ics +text/css less css +text/csv csv +text/html shtml html htm +text/mathml mml +text/n3 n3 +text/plain txt text log list in hs def cxx cpp conf c asc +text/prs.lines.tag dsc +text/richtext rtx +text/sgml sgml sgm +text/tab-separated-values tsv +text/troff tr t roff ms me man +text/turtle ttl +text/uri-list urls uris uri +text/vcard vcard +text/vnd.curl curl +text/vnd.curl.dcurl dcurl +text/vnd.curl.mcurl mcurl +text/vnd.curl.scurl scurl +text/vnd.dvb.subtitle sub +text/vnd.fly fly +text/vnd.fmi.flexstor flx +text/vnd.graphviz gv +text/vnd.in3d.3dml 3dml +text/vnd.in3d.spot spot +text/vnd.sun.j2me.app-descriptor jad +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-asm s asm +text/x-c hh h dic cc +text/x-component htc +text/x-fortran for f90 f77 f +text/x-java-source java +text/x-nfo nfo +text/x-opml opml +text/x-pascal pas p +text/x-setext etx +text/x-sfv sfv +text/x-uuencode uu +text/x-vcalendar vcs +text/x-vcard vcf +text/xml xml +video/3gpp 3gpp 3gp +video/3gpp2 3g2 +video/h261 h261 +video/h263 h263 +video/h264 h264 +video/jpeg jpgv +video/jpm jpm jpgm +video/mj2 mjp2 mj2 +video/mp4 mpg4 mp4v mp4 +video/mpeg mpg mpeg mpe m2v m1v +video/ogg ogv +video/quicktime qt mov +video/vnd.dece.hd uvvh uvh +video/vnd.dece.mobile uvvm uvm +video/vnd.dece.pd uvvp uvp +video/vnd.dece.sd uvvs uvs +video/vnd.dece.video uvvv uvv +video/vnd.dvb.file dvb +video/vnd.fvt fvt +video/vnd.mpegurl mxu m4u +video/vnd.ms-playready.media.pyv pyv +video/vnd.uvvu.mp4 uvvu uvu +video/vnd.vivo viv +video/webm webm +video/x-f4v f4v +video/x-fli fli +video/x-flv flv +video/x-m4v m4v +video/x-matroska mkv mks mk3d +video/x-mng mng +video/x-ms-asf asx asf +video/x-ms-vob vob +video/x-ms-wm wm +video/x-ms-wmv wmv +video/x-ms-wmx wmx +video/x-ms-wvx wvx +video/x-msvideo avi +video/x-sgi-movie movie +video/x-smv smv +x-conference/x-cooltalk ice diff --git a/config/settings.yml b/config/settings.yml index 3211d42db..edd971e64 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -27,7 +27,17 @@ notification-rate-limit: 3600 notification-collate-delay: 300 notification-expiration: 259201 session-timeout: 7200 -maximum-content-length: 52428800 +jwt-expiration: 604800 +jwt-encoding: HS256 +maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728" +health-check-interval: + matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600" + http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600" + ldap-admins: "_env:HEALTHCHECK_INTERVAL_LDAP_ADMINS:600" + smtp-connect: "_env:HEALTHCHECK_INTERVAL_SMTP_CONNECT:600" + widget-memcached: "_env:HEALTHCHECK_INTERVAL_WIDGET_MEMCACHED:600" +health-check-delay-notify: "_env:HEALTHCHECK_DELAY_NOTIFY:true" +health-check-http: "_env:HEALTHCHECK_HTTP:true" # Can we assume, that we can reach ourselves under APPROOT via HTTP (reverse proxies or firewalls might prevent this)? log-settings: detailed: "_env:DETAILED_LOGGING:false" @@ -59,6 +69,8 @@ database: database: "_env:PGDATABASE:uniworx" poolsize: "_env:PGPOOLSIZE:10" +auto-db-migrate: '_env:AUTO_DB_MIGRATE:true' + ldap: host: "_env:LDAPHOST:" tls: "_env:LDAPTLS:" diff --git a/db.sh b/db.sh index b05463c3a..3d80bf68f 100755 --- a/db.sh +++ b/db.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash # Options: see /test/Database.hs (Main) +set -e + stack build --fast --flag uniworx:-library-only --flag uniworx:dev stack exec uniworxdb -- $@ diff --git a/static/js/polyfills/fetchPolyfill.js b/frontend/polyfills/fetch.js similarity index 100% rename from static/js/polyfills/fetchPolyfill.js rename to frontend/polyfills/fetch.js diff --git a/frontend/polyfills/main.js b/frontend/polyfills/main.js new file mode 100644 index 000000000..7e4c554ea --- /dev/null +++ b/frontend/polyfills/main.js @@ -0,0 +1,2 @@ +import './fetch'; +import './url-search-params'; diff --git a/static/js/polyfills/urlPolyfill.js b/frontend/polyfills/url-search-params.js similarity index 100% rename from static/js/polyfills/urlPolyfill.js rename to frontend/polyfills/url-search-params.js diff --git a/frontend/src/app.js b/frontend/src/app.js new file mode 100644 index 000000000..b2db86c31 --- /dev/null +++ b/frontend/src/app.js @@ -0,0 +1,28 @@ +import { HttpClient } from './services/http-client/http-client'; +import { HtmlHelpers } from './services/html-helpers/html-helpers'; +import { I18n } from './services/i18n/i18n'; +import { UtilRegistry } from './services/util-registry/util-registry'; +import { isValidUtility } from './core/utility'; + +export class App { + httpClient = new HttpClient(); + htmlHelpers = new HtmlHelpers(); + i18n = new I18n(); + utilRegistry = new UtilRegistry(); + + constructor() { + this.utilRegistry.setApp(this); + + document.addEventListener('DOMContentLoaded', () => this.utilRegistry.setupAll()); + } + + registerUtilities(utils) { + if (!Array.isArray(utils)) { + throw new Error('Utils are expected to be passed as array!'); + } + + utils.filter(isValidUtility).forEach((util) => { + this.utilRegistry.register(util); + }); + } +} diff --git a/frontend/src/app.spec.js b/frontend/src/app.spec.js new file mode 100644 index 000000000..247be9f00 --- /dev/null +++ b/frontend/src/app.spec.js @@ -0,0 +1,65 @@ +import { App } from './app'; +import { Utility } from './core/utility'; + +@Utility({ selector: 'util1' }) +class TestUtil1 { } + +@Utility({ selector: 'util2' }) +class TestUtil2 { } + +const TEST_UTILS = [ + TestUtil1, + TestUtil2, +]; + +describe('App', () => { + let app; + + beforeEach(() => { + app = new App(); + }); + + it('should create', () => { + expect(app).toBeTruthy(); + }); + + it('should setup all utlites when page is done loading', () => { + spyOn(app.utilRegistry, 'setupAll'); + document.dispatchEvent(new Event('DOMContentLoaded')); + expect(app.utilRegistry.setupAll).toHaveBeenCalled(); + }); + + describe('provides services', () => { + it('HttpClient as httpClient', () => { + expect(app.httpClient).toBeTruthy(); + }); + + it('HtmlHelpers as htmlHelpers', () => { + expect(app.htmlHelpers).toBeTruthy(); + }); + + it('I18n as i18n', () => { + expect(app.i18n).toBeTruthy(); + }); + + it('UtilRegistry as utilRegistry', () => { + expect(app.utilRegistry).toBeTruthy(); + }); + }); + + describe('registerUtilities()', () => { + it('should register the given utilities', () => { + spyOn(app.utilRegistry, 'register'); + app.registerUtilities(TEST_UTILS); + expect(app.utilRegistry.register.calls.count()).toBe(TEST_UTILS.length); + expect(app.utilRegistry.register.calls.argsFor(0)).toEqual([TEST_UTILS[0]]); + expect(app.utilRegistry.register.calls.argsFor(1)).toEqual([TEST_UTILS[1]]); + }); + + it('should throw an error if not passed an array of utilities', () => { + expect(() => { + app.registerUtilities({}); + }).toThrow(); + }); + }); +}); diff --git a/frontend/src/core/utility.js b/frontend/src/core/utility.js new file mode 100644 index 000000000..dba52923a --- /dev/null +++ b/frontend/src/core/utility.js @@ -0,0 +1,22 @@ +export function isValidUtility(utility) { + if (!utility) { + return false; + } + + if (!utility.selector) { + return false; + } + + return true; +}; + +export function Utility(metadata) { + if (!metadata.selector) { + throw new Error('Utility needs to have a selector!'); + } + + return function (target) { + target.selector = metadata.selector; + target.isUtility = true; + }; +}; diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 000000000..aa509bdc3 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,24 @@ +import { App } from './app'; +import { Utils } from './utils/utils'; + +export const app = new App(); +app.registerUtilities(Utils); + +// attach the app to window to be able to get a hold of the +// app instance from the shakespearean templates +window.App = app; + +// dont know where to put this currently... +// interceptor to throw an error if an http response does not match the expected content-type +// function contentTypeInterceptor(response, options) { +// if (!options || !options.headers.get('Accept')) { +// return; +// } + +// const contentType = response.headers.get('content-type'); +// if (!contentType.match(options.accept)) { +// throw new Error('Server returned with '' + contentType + '' when '' + options.accept + '' was expected'); +// } +// } + +// window.HttpClient.addResponseInterceptor(contentTypeInterceptor); diff --git a/frontend/src/services/html-helpers/html-helpers.js b/frontend/src/services/html-helpers/html-helpers.js new file mode 100644 index 000000000..b8bf7771b --- /dev/null +++ b/frontend/src/services/html-helpers/html-helpers.js @@ -0,0 +1,42 @@ +export class HtmlHelpers { + + // `parseResponse` takes a raw HttpClient response and an options object. + // Returns an object with `element` being an contextual fragment of the + // HTML in the response and `ifPrefix` being the prefix that was used to + // 'unique-ify' the ids of the received HTML. + // Original Response IDs can optionally be kept by adding `keepIds: true` + // to the `options` object. + parseResponse(response, options = {}) { + return response.text() + .then( + (responseText) => { + const element = document.createElement('div'); + element.innerHTML = responseText; + let idPrefix = ''; + if (!options.keepIds) { + idPrefix = this._getIdPrefix(); + this._prefixIds(element, idPrefix); + } + return Promise.resolve({ idPrefix, element }); + }, + Promise.reject, + ).catch(console.error); + } + + _prefixIds(element, idPrefix) { + const idAttrs = ['id', 'for', 'data-conditional-input', 'data-modal-trigger']; + + idAttrs.forEach((attr) => { + Array.from(element.querySelectorAll('[' + attr + ']')).forEach((input) => { + const value = idPrefix + input.getAttribute(attr); + input.setAttribute(attr, value); + }); + }); + } + + _getIdPrefix() { + // leading 'r'(andom) to overcome the fact that IDs + // starting with a numeric value are not valid in CSS + return 'r' + Math.floor(Math.random() * 100000) + '__'; + } +} diff --git a/frontend/src/services/html-helpers/html-helpers.spec.js b/frontend/src/services/html-helpers/html-helpers.spec.js new file mode 100644 index 000000000..f092e17fa --- /dev/null +++ b/frontend/src/services/html-helpers/html-helpers.spec.js @@ -0,0 +1,56 @@ +import { HtmlHelpers } from './html-helpers'; + +describe('HtmlHelpers', () => { + let htmlHelpers; + + beforeEach(() => { + htmlHelpers = new HtmlHelpers(); + }); + + it('should create', () => { + expect(htmlHelpers).toBeTruthy(); + }); + + describe('parseResponse()', () => { + let fakeHttpResponse; + + beforeEach(() => { + fakeHttpResponse = { + text: () => Promise.resolve('
Test
'), + }; + }); + + it('should return a promise with idPrefix and element', (done) => { + htmlHelpers.parseResponse(fakeHttpResponse).then(result => { + expect(result.idPrefix).toBeDefined(); + expect(result.element).toBeDefined(); + expect(result.element.textContent).toMatch('Test'); + done(); + }); + }); + + it('should nudge IDs', (done) => { + htmlHelpers.parseResponse(fakeHttpResponse).then(result => { + expect(result.idPrefix).toBeDefined(); + expect(result.element).toBeDefined(); + const elementWithOrigId = result.element.querySelector('#test-div'); + expect(elementWithOrigId).toBeFalsy(); + const elementWithNudgedId = result.element.querySelector('#' + result.idPrefix + 'test-div'); + expect(elementWithNudgedId).toBeTruthy(); + done(); + }); + }); + + it('should not nudge IDs with option "keepIds"', (done) => { + const options = { keepIds: true }; + + htmlHelpers.parseResponse(fakeHttpResponse, options).then(result => { + expect(result.idPrefix).toBe(''); + expect(result.element).toBeDefined(); + const elementWithOrigId = result.element.querySelector('#test-div'); + expect(elementWithOrigId).toBeTruthy(); + done(); + }); + }); + }); +}); diff --git a/frontend/src/services/http-client/http-client.js b/frontend/src/services/http-client/http-client.js new file mode 100644 index 000000000..6ba2341f0 --- /dev/null +++ b/frontend/src/services/http-client/http-client.js @@ -0,0 +1,41 @@ +export class HttpClient { + + static ACCEPT = { + TEXT_HTML: 'text/html', + JSON: 'application/json', + }; + + _responseInterceptors = []; + + addResponseInterceptor(interceptor) { + if (typeof interceptor === 'function') { + this._responseInterceptors.push(interceptor); + } + } + + get(args) { + args.method = 'GET'; + return this._fetch(args); + } + + post(args) { + args.method = 'POST'; + return this._fetch(args); + } + + _fetch(options) { + const requestOptions = { + credentials: 'same-origin', + ...options, + }; + + return fetch(options.url, requestOptions) + .then( + (response) => { + this._responseInterceptors.forEach((interceptor) => interceptor(response, options)); + return Promise.resolve(response); + }, + Promise.reject, + ).catch(console.error); + } +} diff --git a/frontend/src/services/http-client/http-client.spec.js b/frontend/src/services/http-client/http-client.spec.js new file mode 100644 index 000000000..a0f76584d --- /dev/null +++ b/frontend/src/services/http-client/http-client.spec.js @@ -0,0 +1,116 @@ +import { HttpClient } from './http-client'; + +const TEST_URL = 'http://example.com'; +const FAKE_RESPONSE = { + data: 'data', +}; + +describe('HttpClient', () => { + let httpClient; + + beforeEach(() => { + httpClient = new HttpClient(); + + // setup and spy on fake fetch API + spyOn(window, 'fetch').and.returnValue(Promise.resolve(FAKE_RESPONSE)); + }); + + it('should create', () => { + expect(httpClient).toBeTruthy(); + }); + + describe('get()', () => { + let params; + + beforeEach(() => { + params = { + url: TEST_URL, + }; + }); + + it('should GET the given url', () => { + httpClient.get(params); + expect(window.fetch).toHaveBeenCalledWith(params.url, jasmine.objectContaining({ method: 'GET' })); + }); + + it('should return a promise', (done) => { + const result = httpClient.get(params); + result.then((response) => { + expect(response).toEqual(FAKE_RESPONSE); + done(); + }); + }); + }); + + describe('post()', () => { + let params; + + beforeEach(() => { + params = { + url: TEST_URL, + }; + }); + + it('should POST the given url', () => { + httpClient.post(params); + expect(window.fetch).toHaveBeenCalledWith(params.url, jasmine.objectContaining({ method: 'POST' })); + }); + + it('should return a promise', (done) => { + const result = httpClient.post(params); + result.then((response) => { + expect(response).toEqual(FAKE_RESPONSE); + done(); + }); + }); + }); + + describe('Response Interceptors', () => { + it('can be added', () => { + const interceptor = () => {}; + expect(httpClient._responseInterceptors.length).toBe(0); + httpClient.addResponseInterceptor(interceptor); + expect(httpClient._responseInterceptors.length).toBe(1); + httpClient.addResponseInterceptor(interceptor); + expect(httpClient._responseInterceptors.length).toBe(2); + }); + + describe('get called', () => { + let intercepted1; + let intercepted2; + const interceptors = { + interceptor1: () => intercepted1 = true, + interceptor2: () => intercepted2 = true, + }; + + beforeEach(() => { + intercepted1 = false; + intercepted2 = false; + spyOn(interceptors, 'interceptor1').and.callThrough(); + spyOn(interceptors, 'interceptor2').and.callThrough(); + httpClient.addResponseInterceptor(interceptors.interceptor1); + httpClient.addResponseInterceptor(interceptors.interceptor2); + }); + + it('for GET requests', (done) => { + httpClient.get({ url: TEST_URL }).then(() => { + expect(intercepted1).toBeTruthy(); + expect(intercepted2).toBeTruthy(); + expect(interceptors.interceptor1).toHaveBeenCalledWith(FAKE_RESPONSE, jasmine.any(Object)); + expect(interceptors.interceptor2).toHaveBeenCalledWith(FAKE_RESPONSE, jasmine.any(Object)); + done(); + }); + }); + + it('for POST requests', (done) => { + httpClient.post({ url: TEST_URL }).then(() => { + expect(intercepted1).toBeTruthy(); + expect(intercepted2).toBeTruthy(); + expect(interceptors.interceptor1).toHaveBeenCalledWith(FAKE_RESPONSE, jasmine.any(Object)); + expect(interceptors.interceptor2).toHaveBeenCalledWith(FAKE_RESPONSE, jasmine.any(Object)); + done(); + }); + }); + }); + }); +}); diff --git a/frontend/src/services/i18n/i18n.js b/frontend/src/services/i18n/i18n.js new file mode 100644 index 000000000..7061f6ba6 --- /dev/null +++ b/frontend/src/services/i18n/i18n.js @@ -0,0 +1,32 @@ + /** + * I18n + * + * This module stores and serves translated strings, according to the users language settings. + * + * Translations are stored in /messages/frontend/*.msg. + * + * To make additions to any of these files accessible to JavaScrip Utilities + * you need to add them to the respective *.msg file and to the list of FrontendMessages + * in /src/Utils/Frontend/I18n.hs. + * + */ + +export class I18n { + + translations = {}; + + add(id, translation) { + this.translations[id] = translation; + } + + addMany(manyTranslations) { + Object.keys(manyTranslations).forEach((key) => this.add(key, manyTranslations[key])); + } + + get(id) { + if (!this.translations[id]) { + throw new Error('I18N Error: Translation missing for »' + id + '«!'); + } + return this.translations[id]; + } +} diff --git a/frontend/src/services/i18n/i18n.spec.js b/frontend/src/services/i18n/i18n.spec.js new file mode 100644 index 000000000..1b4edf3c4 --- /dev/null +++ b/frontend/src/services/i18n/i18n.spec.js @@ -0,0 +1,51 @@ +import { I18n } from './i18n'; + +describe('I18n', () => { + let i18n; + + beforeEach(() => { + i18n = new I18n(); + }); + + // helper function + function expectTranslation(id, value) { + expect(i18n.translations[id]).toMatch(value); + } + + it('should create', () => { + expect(i18n).toBeTruthy(); + }); + + describe('add()', () => { + it('should add the translation', () => { + i18n.add('id1', 'translated-id1'); + expectTranslation('id1', 'translated-id1'); + }); + }); + + describe('addMany()', () => { + it('should add many translations', () => { + i18n.addMany({ + id1: 'translated-id1', + id2: 'translated-id2', + id3: 'translated-id3', + }); + expectTranslation('id1', 'translated-id1'); + expectTranslation('id2', 'translated-id2'); + expectTranslation('id3', 'translated-id3'); + }); + }); + + describe('get()', () => { + it('should return stored translations', () => { + i18n.translations.id1 = 'something'; + expect(i18n.get('id1')).toMatch('something'); + }); + + it('should throw error if translation is missing', () => { + expect(() => { + i18n.get('id1'); + }).toThrow(); + }); + }); +}); diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js new file mode 100644 index 000000000..d96d7a4b3 --- /dev/null +++ b/frontend/src/services/util-registry/util-registry.js @@ -0,0 +1,123 @@ +const DEBUG_MODE = /localhost/.test(window.location.href) && 0; + +export class UtilRegistry { + + _registeredUtils = []; + _activeUtilInstances = []; + _appInstance; + + /** + * function registerUtil + * + * utils need to have at least these properties: + * name: string | utils name, e.g. 'example' + * selector: string | utils selector, e.g. '[uw-example]' + * setup: Function | utils setup function, see below + * + * setup function must return instance object with at least these properties: + * name: string | utils name + * element: HTMLElement | element the util is applied to + * destroy: Function | function to destroy the util and remove any listeners + * + * @param util Object Utility that should be added to the registry + */ + register(util) { + if (DEBUG_MODE > 2) { + console.log('registering util "' + util.name + '"'); + console.log({ util }); + } + this._registeredUtils.push(util); + } + + deregister(name, destroy) { + const utilIndex = this._findUtilIndex(name); + + if (utilIndex >= 0) { + if (destroy === true) { + this._destroyUtilInstances(name); + } + + this._registeredUtils.splice(utilIndex, 1); + } + } + + setApp(appInstance) { + this._appInstance = appInstance; + } + + setupAll(scope) { + if (DEBUG_MODE > 1) { + console.info('registered js utilities:'); + console.table(this._registeredUtils); + } + + this._registeredUtils.forEach((util) => this.setup(util, scope)); + } + + setup(util, scope = document.body) { + if (DEBUG_MODE > 2) { + console.log('setting up util', { util }); + } + + let instances = []; + + if (util) { + const elements = this._findUtilElements(util, scope); + + elements.forEach((element) => { + let utilInstance = null; + + try { + utilInstance = new util(element, this._appInstance); + } catch(err) { + if (DEBUG_MODE > 0) { + console.warn('Error while trying to initialize a utility!', { util , element, err }); + } + } + + if (utilInstance) { + if (DEBUG_MODE > 2) { + console.info('Got utility instance for utility "' + util.name + '"', { utilInstance }); + } + + instances.push(utilInstance); + } + }); + } + + this._activeUtilInstances.push(...instances); + return instances; + } + + find(name) { + return this._registeredUtils.find((util) => util.name === name); + } + + _findUtilElements(util, scope) { + if (scope && scope.matches(util.selector)) { + return [scope]; + } + return Array.from(scope.querySelectorAll(util.selector)); + } + + _findUtilIndex(name) { + return this._registeredUtils.findIndex((util) => util.name === name); + } + + _destroyUtilInstances(name) { + this._activeUtilInstances + .map((util, index) => ({ + util: util, + index: index, + })) + .filter((activeUtil) => activeUtil.util.name === name) + .forEach((activeUtil) => { + // destroy util instance + activeUtil.util.destroy(); + delete this._activeUtilInstances[activeUtil.index]; + }); + + // get rid of now empty array slots + this._activeUtilInstances = this._activeUtilInstances.filter((util) => !!util); + } +} diff --git a/frontend/src/services/util-registry/util-registry.spec.js b/frontend/src/services/util-registry/util-registry.spec.js new file mode 100644 index 000000000..5f29f6a3c --- /dev/null +++ b/frontend/src/services/util-registry/util-registry.spec.js @@ -0,0 +1,146 @@ +import { UtilRegistry } from './util-registry'; +import { Utility } from '../../core/utility'; + +describe('UtilRegistry', () => { + let utilRegistry; + + beforeEach(() => { + utilRegistry = new UtilRegistry(); + }); + + it('should create', () => { + expect(utilRegistry).toBeTruthy(); + }); + + describe('register()', () => { + it('should allow to add utilities', () => { + let foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeFalsy(); + + utilRegistry.register(TestUtil1); + + foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toEqual(TestUtil1); + }); + }); + + describe('deregister()', () => { + it('should remove util', () => { + // register util + utilRegistry.register(TestUtil1); + let foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeTruthy(); + + // deregister util + utilRegistry.deregister(TestUtil1.name); + foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeFalsy(); + }); + + it('should destroy util instances if requested', () => { + pending('TBD'); + }); + }); + + describe('setup()', () => { + + it('should catch errors thrown by the utility', () => { + expect(() => { + utilRegistry.setup(ThrowingUtil); + }).not.toThrow(); + }); + + describe('scope has no matching elements', () => { + it('should not construct an instance', () => { + const scope = document.createElement('div'); + const instances = utilRegistry.setup(TestUtil1, scope); + expect(instances.length).toBe(0); + }); + + it('should use fallback scope', () => { + const instances = utilRegistry.setup(TestUtil1); + expect(instances.length).toBe(0); + }); + }); + + describe('scope has matching elements', () => { + let testScope; + let testElement1; + let testElement2; + + beforeEach(() => { + testScope = document.createElement('div'); + testElement1 = document.createElement('div'); + testElement2 = document.createElement('div'); + testElement1.classList.add('util1'); + testElement2.classList.add('util1'); + testScope.appendChild(testElement1); + testScope.appendChild(testElement2); + }); + + it('should construct a utility instance', () => { + const setupUtilities = utilRegistry.setup(TestUtil1, testScope); + expect(setupUtilities).toBeTruthy(); + expect(setupUtilities[0]).toBeTruthy(); + }); + + it('should construct an instance for each matching element', () => { + const setupUtilities = utilRegistry.setup(TestUtil1, testScope); + expect(setupUtilities).toBeTruthy(); + expect(setupUtilities[0].element).toBe(testElement1); + expect(setupUtilities[1].element).toBe(testElement2); + }); + + it('should pass the app instance', () => { + const fakeApp = { }; + utilRegistry.setApp(fakeApp); + + const setupUtilities = utilRegistry.setup(TestUtil1, testScope); + expect(setupUtilities).toBeTruthy(); + expect(setupUtilities[0].app).toBe(fakeApp); + expect(setupUtilities[1].app).toBe(fakeApp); + }); + }); + }); + + describe('setupAll()', () => { + it('should setup all the utilities', () => { + spyOn(utilRegistry, 'setup'); + utilRegistry.register(TestUtil1); + utilRegistry.register(TestUtil2); + utilRegistry.setupAll(); + + expect(utilRegistry.setup.calls.count()).toBe(2); + expect(utilRegistry.setup.calls.argsFor(0)).toEqual([TestUtil1, undefined]); + expect(utilRegistry.setup.calls.argsFor(1)).toEqual([TestUtil2, undefined]); + }); + + it('should pass the given scope', () => { + spyOn(utilRegistry, 'setup'); + utilRegistry.register(TestUtil1); + const scope = document.createElement('div'); + utilRegistry.setupAll(scope); + + expect(utilRegistry.setup).toHaveBeenCalledWith(TestUtil1, scope); + }); + }); +}); + +// test utilities +@Utility({ selector: '.util1' }) +class TestUtil1 { + constructor(element, app) { + this.element = element; + this.app = app; + } +} + +@Utility({ selector: '#util2' }) +class TestUtil2 { } + +@Utility({ selector: '#throws' }) +class ThrowingUtil { + constructor() { + throw new Error(); + } + } diff --git a/frontend/src/utils/alerts/alerts.js b/frontend/src/utils/alerts/alerts.js new file mode 100644 index 000000000..e7e04ddbb --- /dev/null +++ b/frontend/src/utils/alerts/alerts.js @@ -0,0 +1,165 @@ +import { Utility } from '../../core/utility'; +import './alerts.scss'; + +const ALERTS_INITIALIZED_CLASS = 'alerts--initialized'; +const ALERTS_ELEVATED_CLASS = 'alerts--elevated'; +const ALERTS_TOGGLER_CLASS = 'alerts__toggler'; +const ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible'; +const ALERTS_TOGGLER_APPEAR_DELAY = 120; + +const ALERT_CLASS = 'alert'; +const ALERT_INITIALIZED_CLASS = 'alert--initialized'; +const ALERT_CLOSER_CLASS = 'alert__closer'; +const ALERT_ICON_CLASS = 'alert__icon'; +const ALERT_CONTENT_CLASS = 'alert__content'; +const ALERT_INVISIBLE_CLASS = 'alert--invisible'; +const ALERT_AUTO_HIDE_DELAY = 10; +const ALERT_AUTOCLOSING_MATCHER = '.alert-info, .alert-success'; + +@Utility({ + selector: '[uw-alerts]', +}) +export class Alerts { + _togglerCheckRequested = false; + _togglerElement; + _alertElements; + + _element; + _app; + + constructor(element, app) { + if (!element) { + throw new Error('Alerts util has to be called with an element!'); + } + + this._element = element; + this._app = app; + + if (this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) { + return false; + } + + this._togglerElement = this._element.querySelector('.' + ALERTS_TOGGLER_CLASS); + this._alertElements = this._gatherAlertElements(); + + if (this._togglerElement) { + this._initToggler(); + } + + this._initAlerts(); + + // register http client interceptor to filter out Alerts Header + this._setupHttpInterceptor(); + + // mark initialized + this._element.classList.add(ALERTS_INITIALIZED_CLASS); + } + + destroy() { + console.log('TBD: Destroy Alert'); + } + + _gatherAlertElements() { + return Array.from(this._element.querySelectorAll('.' + ALERT_CLASS)).filter(function(alert) { + return !alert.classList.contains(ALERT_INITIALIZED_CLASS); + }); + } + + _initToggler() { + this._togglerElement.addEventListener('click', () => { + this._alertElements.forEach((alertEl) => this._toggleAlert(alertEl, true)); + this._togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS); + }); + } + + _initAlerts() { + this._alertElements.forEach((alert) => this._initAlert(alert)); + } + + _initAlert(alertElement) { + let autoHideDelay = ALERT_AUTO_HIDE_DELAY; + if (alertElement.dataset.decay) { + autoHideDelay = parseInt(alertElement.dataset.decay, 10); + } + + const closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS); + closeEl.addEventListener('click', () => { + this._toggleAlert(alertElement); + }); + + if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) { + window.setTimeout(() => this._toggleAlert(alertElement), autoHideDelay * 1000); + } + } + + _toggleAlert(alertEl, visible) { + alertEl.classList.toggle(ALERT_INVISIBLE_CLASS, !visible); + this._checkToggler(); + } + + _checkToggler() { + if (this._togglerCheckRequested) { + return; + } + + const alertsHidden = this._alertElements.reduce(function(acc, alert) { + return acc && alert.classList.contains(ALERT_INVISIBLE_CLASS); + }, true); + + window.setTimeout(() => { + this._togglerElement.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden); + this._togglerCheckRequested = false; + }, ALERTS_TOGGLER_APPEAR_DELAY); + } + + _setupHttpInterceptor() { + this._app.httpClient.addResponseInterceptor(this._responseInterceptor.bind(this)); + } + + _elevateAlerts() { + this._element.classList.add(ALERTS_ELEVATED_CLASS); + } + + _responseInterceptor = (response) => { + let alerts; + for (const header of response.headers) { + if (header[0] === 'alerts') { + const decodedHeader = decodeURIComponent(header[1]); + alerts = JSON.parse(decodedHeader); + break; + } + } + + if (alerts) { + alerts.forEach((alert) => { + const alertElement = this._createAlertElement(alert.status, alert.content); + this._element.appendChild(alertElement); + this._alertElements.push(alertElement); + this._initAlert(alertElement); + }); + + this._elevateAlerts(); + } + } + + _createAlertElement(type, content) { + const alertElement = document.createElement('div'); + alertElement.classList.add(ALERT_CLASS, 'alert-' + type); + + const alertCloser = document.createElement('div'); + alertCloser.classList.add(ALERT_CLOSER_CLASS); + + const alertIcon = document.createElement('div'); + alertIcon.classList.add(ALERT_ICON_CLASS); + + const alertContent = document.createElement('div'); + alertContent.classList.add(ALERT_CONTENT_CLASS); + alertContent.innerHTML = content; + + alertElement.appendChild(alertCloser); + alertElement.appendChild(alertIcon); + alertElement.appendChild(alertContent); + + return alertElement; + } +} diff --git a/frontend/src/utils/alerts/alerts.md b/frontend/src/utils/alerts/alerts.md new file mode 100644 index 000000000..e3b36feed --- /dev/null +++ b/frontend/src/utils/alerts/alerts.md @@ -0,0 +1,35 @@ +# Alerts + +Makes alerts interactive. + +## Attribute: `uw-alerts` + +## Types of alerts: +- `default`\ + Regular Info Alert + Disappears automatically after 30 seconds + Disappears after x seconds if explicitly specified via data-decay='x' + Can be told not to disappear with data-decay='0' + +- `success`\ + Currently no special visual appearance + Disappears automatically after 30 seconds + +- `warning`\ + Will be coloured warning-orange regardless of user's selected theme + Does not disappear + +- `error`\ + Will be coloured error-red regardless of user's selected theme + Does not disappear + +## Example usage: +```html +
+
+
+
+
+
+ This is some information +``` diff --git a/static/css/utils/alerts.scss b/frontend/src/utils/alerts/alerts.scss similarity index 85% rename from static/css/utils/alerts.scss rename to frontend/src/utils/alerts/alerts.scss index 3256eef96..d2faf1b22 100644 --- a/static/css/utils/alerts.scss +++ b/frontend/src/utils/alerts/alerts.scss @@ -1,23 +1,3 @@ -/* ALERTS */ -/** - .alert - Regular Info Alert - Disappears automatically after 30 seconds - Disappears after x seconds if explicitly specified via data-decay='x' - Can be told not to disappear with data-decay='0' - - .alert-success - Disappears automatically after 30 seconds - - .alert-warning - Does not disappear - Orange regardless of user's selected theme - - .alert-error - Does not disappear - Red regardless of user's selected theme - - */ .alerts { position: fixed; bottom: 0; @@ -40,7 +20,7 @@ &::before { content: '\f077'; position: absolute; - font-family: "Font Awesome 5 Free"; + font-family: 'Font Awesome 5 Free'; left: 50%; top: 0; height: 30px; @@ -54,6 +34,10 @@ } } +.alerts--elevated { + z-index: 1000; +} + .alerts__toggler--visible { top: -40px; opacity: 1; @@ -142,7 +126,7 @@ &::before { content: '\f05a'; position: absolute; - font-family: "Font Awesome 5 Free"; + font-family: 'Font Awesome 5 Free'; font-size: 24px; top: 50%; left: 50%; @@ -180,7 +164,7 @@ &::before { content: '\f00d'; position: absolute; - font-family: "Font Awesome 5 Free"; + font-family: 'Font Awesome 5 Free'; top: 50%; left: 50%; display: flex; diff --git a/frontend/src/utils/alerts/alerts.spec.js b/frontend/src/utils/alerts/alerts.spec.js new file mode 100644 index 000000000..0b4749e97 --- /dev/null +++ b/frontend/src/utils/alerts/alerts.spec.js @@ -0,0 +1,27 @@ +import { Alerts } from './alerts'; + +const MOCK_APP = { + httpClient: { + addResponseInterceptor: () => {}, + }, +}; + +describe('Alerts', () => { + + let alerts; + + beforeEach(() => { + const element = document.createElement('div'); + alerts = new Alerts(element, MOCK_APP); + }); + + it('should create', () => { + expect(alerts).toBeTruthy(); + }); + + it('should throw if called without an element', () => { + expect(() => { + new Alerts(); + }).toThrow(); + }); +}); diff --git a/frontend/src/utils/asidenav/asidenav.js b/frontend/src/utils/asidenav/asidenav.js new file mode 100644 index 000000000..2964e1617 --- /dev/null +++ b/frontend/src/utils/asidenav/asidenav.js @@ -0,0 +1,82 @@ +import { Utility } from '../../core/utility'; +import './asidenav.scss'; + +const FAVORITES_BTN_CLASS = 'navbar__list-item--favorite'; +const FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active'; +const ASIDENAV_INITIALIZED_CLASS = 'asidenav--initialized'; +const ASIDENAV_EXPANDED_CLASS = 'main__aside--expanded'; +const ASIDENAV_LIST_ITEM_CLASS = 'asidenav__list-item'; +const ASIDENAV_SUBMENU_CLASS = 'asidenav__nested-list-wrapper'; + +@Utility({ + selector: '[uw-asidenav]', +}) +export class Asidenav { + + _element; + _asidenavSubmenus; + + constructor(element) { + if (!element) { + throw new Error('Asidenav utility cannot be setup without an element!'); + } + + this._element = element; + + if (this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) { + return false; + } + + this._initFavoritesButton(); + this._initAsidenavSubmenus(); + + // mark initialized + this._element.classList.add(ASIDENAV_INITIALIZED_CLASS); + } + + destroy() { + this._asidenavSubmenus.forEach((union) => { + union.listItem.removeEventListener(union.hoverHandler); + }); + } + + _initFavoritesButton() { + const favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS); + if (favoritesBtn) { + favoritesBtn.addEventListener('click', (event) => { + favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS); + this._element.classList.toggle(ASIDENAV_EXPANDED_CLASS); + event.preventDefault(); + }, true); + } + } + + _initAsidenavSubmenus() { + this._asidenavSubmenus = Array.from(this._element.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS)) + .map(function(listItem) { + const submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS); + return { listItem, submenu }; + }).filter(function(union) { + return union.submenu !== null; + }); + + this._asidenavSubmenus.forEach((union) => { + union.hoverHandler = this._createMouseoverHandler(union); + union.listItem.addEventListener('mouseover', union.hoverHandler); + }); + } + + _createMouseoverHandler(union) { + return () => { + const rectListItem = union.listItem.getBoundingClientRect(); + const rectSubMenu = union.submenu.getBoundingClientRect(); + + union.submenu.style.left = (rectListItem.left + rectListItem.width) + 'px'; + if (window.innerHeight - rectListItem.top < rectSubMenu.height) { + union.submenu.style.top = (rectListItem.top + rectListItem.height - rectSubMenu.height) + 'px'; + } else { + union.submenu.style.top = rectListItem.top + 'px'; + } + }; + } +} diff --git a/frontend/src/utils/asidenav/asidenav.md b/frontend/src/utils/asidenav/asidenav.md new file mode 100644 index 000000000..0aa73b0f7 --- /dev/null +++ b/frontend/src/utils/asidenav/asidenav.md @@ -0,0 +1,21 @@ +# Asidenav + +Correctly positions hovered asidenav submenus and handles the favorites button on mobile + +## Attribute: `uw-asidenav` + +## Example usage: +```html +
+
+
+