diff --git a/frontend/src/app.sass b/frontend/src/app.sass index 2568a5dbb..2b74c3920 100644 --- a/frontend/src/app.sass +++ b/frontend/src/app.sass @@ -160,7 +160,7 @@ h4 --current-header-height: var(--header-height-collapsed) position: relative background-color: white - transition: padding-left .2s ease-out + transition: padding-left .2s ease-out, margin-top 0.2s ease margin-top: var(--current-header-height) margin-left: 0 @@ -173,6 +173,12 @@ h4 > .container margin: 20px 0 + .navbar__container-radio:checked ~ * & + margin-top: calc(var(--current-header-height) + 30px) + + @media (min-width: 769px) and (min-height: 501px) + margin-top: calc(var(--current-header-height) + 44px) + .main__content, .modal__content a text-decoration: underline @@ -180,19 +186,25 @@ h4 p, form, .div-p margin: 0.5rem 0 + &:first-child + margin: 0 0 0.5rem 0 + &:last-child - margin: 0.5rem 0 0 + margin: 0.5rem 0 0 0 &:first-child margin: 0 +@media (min-width: 769px) and (min-height: 501px) + .main__content + --current-header-height: var(--header-height) + @media (min-width: 426px) .main__content margin-left: var(--asidenav-width-md, 50px) @media (min-width: 769px) .main__content - --current-header-height: var(--header-height) margin-left: var(--asidenav-width-lg, 20%) @media (min-width: 1200px) @@ -228,6 +240,7 @@ input[type="submit"], input[type="button"], button, .btn + font-family: var(--font-base) outline: 0 border: 0 box-shadow: 0 @@ -524,6 +537,9 @@ section display: grid grid-column: 2 + h1 + & + margin: 0 auto 0.5rem + &::before @extend .fas @@ -582,7 +598,7 @@ section &::before height: auto width: 45px - font-size: 40px + font-size: 20px top: 15px .notification-error @@ -598,15 +614,15 @@ section color: var(--color-warning) // "Heated" element. - Set custom property "--hotness" to a value from 0 to 1 to turn - the element's background to a color on a gradient from green to red. +// Set custom property "--hotness" to a value from 0 to 1 to turn +// the element's background to a color on a gradient from green to red. - TBD: - - move to a proper place - - think about font-weight... +// TBD: +// - move to a proper place +// - think about font-weight... - Example: -
Lorem ipsum +// Example: +//
Lorem ipsum .heated --hotness: 0 @@ -625,7 +641,7 @@ section .ribbon position: fixed top: calc(40px + var(--header-height)) - transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1) + transition: all 0.2s ease right: -63px transform: rotate(45deg) width: 250px @@ -636,15 +652,21 @@ section font-size: 1.25rem line-height: 2em box-shadow: 0 0 3px rgba(0, 0, 0, 0.4) - z-index: 19 + z-index: 21 pointer-events: none -@media (max-width: 768px) + .navbar__container-radio:checked ~ & + top: calc(84px + var(--header-height)) + +@media (max-width: 768px), (max-height: 500px) .ribbon top: calc(20px + var(--header-height-collapsed)) right: -83px transform: rotate(45deg) scale(0.6) + .navbar__container-radio:checked ~ & + top: calc(50px + var(--header-height-collapsed)) + #admin-studyterms select, option, input min-width: 50px @@ -981,54 +1003,63 @@ th, td .breadcrumbs__container position: relative color: var(--color-lightwhite) - padding: 4px 13px + padding: 4px 20px 4px 40px background-color: var(--color-dark) - line-height: 30px + + a + color: var(--color-lightwhite) @media (min-width: 426px) .breadcrumbs__container - padding: 7px 20px + padding: 7px 20px 7px 40px @media (min-width: 769px) .breadcrumbs__container padding: 7px 40px -.breadcrumbs__link - color: var(--color-lightwhite) +ul.breadcrumbs__list + display: flex + align-items: center + height: 30px + margin: 0 -5px - &:hover - color: var(--color-white) + & > li + display: block .breadcrumbs__item - padding-right: 14px - position: relative - line-height: 28px opacity: 0.8 - z-index: 1 - margin-right: 10px + margin: 0 5px &:hover opacity: 1 - &::after - content: '' - position: absolute - top: 11px - right: 0 - width: 7px - height: 7px - border-style: solid - border-width: 0 - border-bottom-width: 1px - border-right-width: 1px - border-color: var(--color-white) - transform: rotate(-45deg) - z-index: 10 +.breadcrumbs__item-separator + line-height: 0 + opacity: 0.5 + margin: 0 5px + margin-top: 1px + +a.breadcrumbs__home + position: absolute + left: 10px + top: 5px + width: 20px + height: 30px + opacity: 0.5 + text-decoration: none + color: var(--color-lightwhite) + text-align: center + line-height: 30px + + @media (min-width: 426px) + top: 8px + + &:hover + opacity: 1 .breadcrumbs__last-item - line-height: 28px - vertical-align: bottom font-weight: 600 + opacity: 1 .recipient-category max-width: 400px @@ -1096,10 +1127,15 @@ th, td overflow: auto .footer + display: flex + flex-flow: row wrap + justify-content: center + align-items: baseline + align-content: flex-start text-align: center padding: 20px position: relative - margin: 40px 0 + margin: 40px 0 0 0 &::before content: '' @@ -1110,9 +1146,9 @@ th, td height: 2px background-color: var(--color-grey-light) -.footer-links * +.footer-links > * margin-right: 0.5em - display: inline-block + display: block &:last margin-right: 0 @@ -1138,70 +1174,3 @@ th, td .checkbox display: inline-block margin-left: 7px - -.pagenav - display: flex - align-items: flex-start - padding-bottom: 15px - margin-bottom: 20px - border-bottom: 1px solid #eee - -.pagenav__list-item - flex: 1 - position: relative - display: inline-flex - box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.6) - margin: 10px 10px 0 0 - -.pagenav__link-wrapper - flex: 1 - padding: 10px 10px 12px - text-decoration: none !important - - &:hover - background-color: var(--color-grey-light) - -@media (max-width: 1024px) - .pagenav - flex-direction: column - -@media (min-width: 1025px) - .pagenav-secondary - position: relative - overflow: visible - padding-top: 10px - - &::after - content: '\2026' - display: inline-block - padding: 10px 10px 12px - width: 40px - box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.6) - box-sizing: border-box - text-align: center - transition: box-shadow 0.2s ease - - &:hover - &::after - box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.8) - - .pagenav-secondary__list - display: block - - .pagenav-secondary__list - position: absolute - display: none - right: 0 - top: 50px - width: 250px - background-color: white - box-shadow: 0 0 6px 3px var(--color-grey-light) - z-index: 18 - - .pagenav__list-item--secondary - display: flex - box-shadow: none - margin: 0 - - &:hover - background-color: var(--color-grey-light) diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index e035903fa..3fcfb99d5 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -58,7 +58,7 @@ export class UtilRegistry { if (utilInstance) { const instance = utilInstance.instance; if (instance && typeof instance.start === 'function') { - instance.start(); + instance.start.bind(instance)(); startedInstances.push(instance); } } @@ -91,6 +91,7 @@ export class UtilRegistry { if (DEBUG_MODE > 0) { console.error('Error while trying to initialize a utility!', { util , element, err }); } + utilInstance = null; } if (utilInstance) { diff --git a/frontend/src/utils/asidenav/asidenav.sass b/frontend/src/utils/asidenav/asidenav.sass index b9beb15b4..4ed526a21 100644 --- a/frontend/src/utils/asidenav/asidenav.sass +++ b/frontend/src/utils/asidenav/asidenav.sass @@ -1,7 +1,6 @@ .main__aside position: fixed box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) - z-index: 1 top: 0 left: 0 width: var(--asidenav-width-lg, 20%) @@ -9,16 +8,7 @@ flex: 0 0 0 flex-basis: var(--asidenav-width-lg, 20%) transition: all .2s ease-out - - &::before - position: absolute - z-index: -1 - left: 0 - top: 0 - width: 100% - height: 100% - background-color: var(--color-dark) - opacity: 0.05 + z-index: 20 &::after content: '' @@ -85,6 +75,7 @@ padding: 10px 13px margin: 0 border-bottom: 1px solid var(--color-grey) + height: 44px .asidenav-term-identifier--long display: inherit @@ -232,7 +223,7 @@ // hover sub-menus .asidenav__nested-list-wrapper position: absolute - z-index: 10 + z-index: 22 display: none color: var(--color-font) background-color: var(--color-grey-light) @@ -269,6 +260,10 @@ min-height: calc(100% - var(--header-height-collapsed)) top: var(--header-height-collapsed) + .navbar__container-radio:checked ~ & + min-height: calc(100% - var(--header-height-collapsed) - 30px) + top: calc(var(--header-height-collapsed) + 30px) + .asidenav__box-title width: var(--asidenav-width-md, 50px) font-size: 18px @@ -277,6 +272,7 @@ word-break: break-all background-color: var(--color-dark) color: var(--color-lightwhite) + border: none &:hover background-color: var(--color-darker) diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index d196aae51..7922cd3ee 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -5,7 +5,7 @@ const CHECKBOX_SELECTOR = '[type="checkbox"]'; const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized'; @Utility({ - selector: 'table', + selector: 'table:not([uw-no-check-all])', }) export class CheckAll { diff --git a/frontend/src/utils/exam-correct/exam-correct.js b/frontend/src/utils/exam-correct/exam-correct.js index 7e99ac5e4..2731d66c9 100644 --- a/frontend/src/utils/exam-correct/exam-correct.js +++ b/frontend/src/utils/exam-correct/exam-correct.js @@ -23,6 +23,7 @@ const EXAM_CORRECT_USER_INPUT_CANDIDATES_ID = 'exam-correct__user-candidates'; const EXAM_CORRECT_INPUT_BODY_ID = 'exam-correct__new'; const EXAM_CORRECT_USER_ATTR = 'exam-correct--user-id'; const EXAM_CORRECT_USER_DNAME_ATTR = 'exam-correct--user-dname'; +const EXAM_CORRECT_STATUS_CELL_CLASS = 'exam-correct--status-cell'; const STATUS = { NONE: null, @@ -162,7 +163,7 @@ export class ExamCorrect { }).then( (response) => response.json() ).then( - (response) => this._processResponse(response, body.user) + (response) => this._processResponse(body, response, body.user) ).catch((error) => { console.error('Error while validating user input', error); }); @@ -203,14 +204,18 @@ export class ExamCorrect { } } + const result = this._resultSelect.value !== 'none' && this._resultSelect.value; + // abort send if there are no results (after validation) if (Object.keys(results).length <= 0) return; const rowInfo = { - users: [userId || user], + users: [user], results: results, status: STATUS.LOADING, }; + if (results) rowInfo.results = results; + if (result) rowInfo.result = result === 'delete' ? null : result; this._addRow(rowInfo); // clear inputs on validation success @@ -219,8 +224,11 @@ export class ExamCorrect { const body = { user: userId || user, - results: results, }; + if (results) body.results = results; + if (result) body.grade = result === 'result' ? this._resultGradeSelect.value : (result === 'delete' ? null : result); + + console.log('request body', body); this._app.httpClient.post({ url: EXAM_CORRECT_URL_POST, @@ -229,13 +237,13 @@ export class ExamCorrect { }).then( (response) => response.json() ).then( - (response) => this._processResponse(response, body.user, results) + (response) => this._processResponse(body, response, user, undefined, { results: results, result: result }) ).catch((error) => { console.error('Error while processing response', error); }); } - _processResponse(response, user, results) { + _processResponse(request, response, user, targetRow, ...results) { if (response) { if (response.status === 'no-op') { if (response.users) { @@ -292,19 +300,22 @@ export class ExamCorrect { let newEntry = { users: null, results: null, + result: null, status: STATUS.FAILURE, + message: null, date: null, }; console.log('response', response); - for (let row of [...this._element.rows]) { + const candidateRows = (targetRow && [targetRow]) || [...this._element.rows]; + for (let row of candidateRows) { let userElem = row.cells.item(this._cIndices.get('user')); const userIdent = userElem && userElem.getAttribute(EXAM_CORRECT_USER_ATTR); // TODO use other attribute identifier if (userIdent === user) { + console.log('response-update', row); let status = STATUS.FAILURE; switch (response.status) { - // TODO fetch update time from response and replace case 'success': status = STATUS.SUCCESS; if (response.user) { @@ -315,24 +326,27 @@ export class ExamCorrect { timeElem.classList.remove('exam-correct--local-time'); newEntry.users = [response.user]; newEntry.results = response.results; + newEntry.result = response.grade; } - // TODO replace results with results from response // TODO set edit button visibility break; case 'ambiguous': - // TODO show tooltip with error message // TODO set edit button visibility status = STATUS.AMBIGUOUS; if (response.users) { userElem = this._showUserList(row, response.users, results); newEntry.users = response.users; - newEntry.results = results; + newEntry.results = results.partResults; + newEntry.result = results.result; } + newEntry.message = response.message || null; break; case 'failure': status = STATUS.FAILURE; - newEntry.users = [user]; + newEntry.users = (response.user && [response.user]) || null; newEntry.results = results; + newEntry.message = response.message || null; + newEntry.result = results.result; break; default: // TODO show tooltip with 'invalid response' @@ -344,6 +358,36 @@ export class ExamCorrect { }); newEntry.status = status || STATUS.FAILURE; newEntry.date = response.time || moment().utc().format(); + + const statusCell = row.querySelector(`.${EXAM_CORRECT_STATUS_CELL_CLASS}`); + const messageElem = statusCell.querySelector('.uw-exam-correct--message'); + if (messageElem) { + statusCell.removeChild(messageElem); + } + + if (newEntry.message) { + const messageElem = document.createElement('SPAN'); + messageElem.classList.add('uw-exam-correct--message'); + const messageText = document.createTextNode(newEntry.message); + messageElem.appendChild(messageText); + statusCell.appendChild(messageElem); + } + + const userCell = row.querySelector('.uw-exam-correct--user-cell'); + if (userCell && newEntry.users && newEntry.users.length === 1) { + const user = newEntry.users[0]; + userCell.innerHTML = userToHTML(user); + userCell.setAttribute(EXAM_CORRECT_USER_ATTR, user); + } else if (userCell && newEntry.users) { + row.replaceChild(userCell, this._showUserList(row, newEntry.users, request.results)); + } + + for (let [k, v] of Object.entries(newEntry.results)) { + const resultCell = row.cells.item(this._cIndices.get(k)); + if (v.result) + resultCell.innerHTML = v.result; + } + savedEntries.push(newEntry); this._storageManager.save('entries', savedEntries); return; @@ -392,14 +436,15 @@ export class ExamCorrect { const timeElem = row.cells.item(0); timeElem.innerHTML = now.format(this._dateFormat); timeElem.classList.add('exam-correct--local-time'); - const userElem = row.cells.item(1); + const userElem = row.cells.item(this._cIndices.get('user')); const statusElem = row.querySelector('.exam-correct--ambiguous'); setStatus(statusElem, STATUS.LOADING); const body = { user: listItem.getAttribute(EXAM_CORRECT_USER_ATTR), - results: results, + results: results.partResults, + grade: results.result, }; this._app.httpClient.post({ @@ -408,37 +453,8 @@ export class ExamCorrect { body: JSON.stringify(body), }).then( (response) => response.json() - ).then((response) => { - switch (response.status) { - case 'success': { - userElem.innerHTML = userToHTML(response.user); - // TODO replace part results with results from server - timeElem.innerHTML = moment(response.time).format(this._dateFormat); - timeElem.classList.remove('exam-correct--local-time'); - setStatus(statusElem, STATUS.SUCCESS); - const savedEntries = this._storageManager.load('entries'); - for (let i = 0; i < savedEntries.length; i++) { - for (let user of savedEntries[i].users) { - if (user.id === response.user.id) { - savedEntries[i] = { - users: [response.user], - results: response.results, - status: STATUS.SUCCESS, - date: response.time, - }; - break; - } - } - } - this._storageManager.save('entries', savedEntries); - break; - } - default: - // non-success response on request with a uuid => panic and ignore for now - } - }).catch((error) => { - console.error(error); - }); + ).then((response) => this._processResponse(body, response, userElem.getAttribute(EXAM_CORRECT_USER_ATTR), row, { results: results.partResults, result: results.result }) + ).catch(console.error); } _addRow(rowInfo) { @@ -458,6 +474,7 @@ export class ExamCorrect { cells.set(this._cIndices.get('date'), dateCell); let userCell = document.createElement('TD'); + userCell.classList.add('uw-exam-correct--user-cell'); if (!rowInfo.users || rowInfo.users.length === 0) { console.error('Found rowInfo without users info!'); } else if (rowInfo.users.length === 1) { @@ -465,7 +482,7 @@ export class ExamCorrect { userCell.innerHTML = userToHTML(user); userCell.setAttribute(EXAM_CORRECT_USER_ATTR, user); } else { - userCell = this._showUserList(newRow, rowInfo.users, rowInfo.results); + userCell = this._showUserList(newRow, rowInfo.users, { partResults: rowInfo.results, result: rowInfo.result }); } cells.set(this._cIndices.get('user'), userCell); @@ -483,12 +500,15 @@ export class ExamCorrect { const resultCell = document.createElement('TD'); resultCell.colSpan = 2; + if (rowInfo.result) + resultCell.innerHTML = rowInfo.result; cells.set(this._cIndices.get('result'), resultCell); const statusCell = document.createElement('TD'); - const statusDiv = document.createElement('DIV'); - setStatus(statusDiv, rowInfo.status); - statusCell.appendChild(statusDiv); + statusCell.classList.add(EXAM_CORRECT_STATUS_CELL_CLASS); + const statusSymbol = document.createElement('I'); + setStatus(statusSymbol, rowInfo.status); + statusCell.appendChild(statusSymbol); cells.set(this._cIndices.get('status'), statusCell); for (let i = 0; i <= this._lastColumnIndex; i++) { diff --git a/frontend/src/utils/exam-correct/exam-correct.sass b/frontend/src/utils/exam-correct/exam-correct.sass index 95836f0f6..44655052d 100644 --- a/frontend/src/utils/exam-correct/exam-correct.sass +++ b/frontend/src/utils/exam-correct/exam-correct.sass @@ -8,13 +8,26 @@ table[uw-exam-correct] th.uw-exam-correct--user-cell, td.uw-exam-correct--user-cell min-width: 200px + th.uw-exam-correct--part-cell, td.uw-exam-correct--part-cell - width: 85px + width: min-content text-align: center + white-space: nowrap + input width: 70px padding: 4px 8px + .uw-exam-correct--delete-exam-part ~ .fa-trash + opacity: .5 + cursor: pointer + margin-left: 5px + .uw-exam-correct--delete-exam-part ~ .fa-trash:hover + opacity: 1 + .uw-exam-correct--delete-exam-part:checked ~ .fa-trash + opacity: 1 + color: var(--color-error) + td#uw-exam-correct__result width: min-content select @@ -34,6 +47,16 @@ table[uw-exam-correct] td#uw-exam-correct__result__grade select.grade-hidden visibility: hidden + td.exam-correct--status-cell + font-size: .9rem + font-weight: 600 + color: var(--color-fontsec) + font-style: italic + + .fas + font-size: 1rem + text-align: center + padding-right: .25rem [uw-exam-correct] input:invalid:not(.no-value) diff --git a/frontend/src/utils/navbar/navbar.js b/frontend/src/utils/navbar/navbar.js index 0bb6fb029..f31ba77bd 100644 --- a/frontend/src/utils/navbar/navbar.js +++ b/frontend/src/utils/navbar/navbar.js @@ -1,48 +1,91 @@ import { Utility } from '../../core/utility'; import './navbar.sass'; +import * as throttle from 'lodash.throttle'; - -export const LANGUAGE_SELECT_UTIL_SELECTOR = '[uw-language-select]'; -const LANGUAGE_SELECT_INITIALIZED_CLASS = 'language-select--initialized'; - +export const HEADER_CONTAINER_UTIL_SELECTOR = '.navbar__list-item--container-selector .navbar__link-wrapper'; +const HEADER_CONTAINER_INITIALIZED_CLASS = 'navbar-header-container--initialized'; @Utility({ - selector: LANGUAGE_SELECT_UTIL_SELECTOR, + selector: HEADER_CONTAINER_UTIL_SELECTOR, }) -export class LanguageSelectUtil { +export class NavHeaderContainerUtil { _element; - checkbox; - + radioButton; + closeButton; + container; + + wasOpen; + + _throttleUpdateWasOpen; + constructor(element) { if (!element) { - throw new Error('Language Select utility needs to be passed an element!'); + throw new Error('Navbar Header Container utility needs to be passed an element!'); } - if (element.classList.contains(LANGUAGE_SELECT_INITIALIZED_CLASS)) { - return false; + if (element.classList.contains(HEADER_CONTAINER_INITIALIZED_CLASS)) { + return; } this._element = element; - this.checkbox = element.querySelector('#lang-checkbox'); + this.radioButton = document.getElementById(`${this._element.id}-radio`); + if (!this.radioButton) { + throw new Error('Navbar Header Container utility could not find associated radio button!'); + } - window.addEventListener('click', event => this.close(event)); + this.closeButton = document.getElementById('container-radio-none'); + if (!this.closeButton) { + throw new Error('Navbar Header Container utility could not find radio button for closing!'); + } + + this.container = document.getElementById(`${this._element.id}-container`); + if (!this.container) { + throw new Error('Navbar Header Container utility could not find associated container!'); + } - element.classList.add(LANGUAGE_SELECT_INITIALIZED_CLASS); + const closer = this.container.querySelector('.navbar__container-list-closer'); + if (closer) { + closer.classList.add('navbar__container-list-closer--hidden'); + } + + this.updateWasOpen(); + this.throttleUpdateWasOpen = throttle(this.updateWasOpen.bind(this), 100, { leading: false, trailing: true }); + + this._element.classList.add(HEADER_CONTAINER_INITIALIZED_CLASS); } - close(event) { - if (!this._element.contains(event.target) && window.document.contains(event.target)) { - this.checkbox.checked = false; + start() { + if (!this.container) + return; + + window.addEventListener('click', this.clickHandler.bind(this)); + this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this)); + } + + clickHandler() { + if (!this.container.contains(event.target) && window.document.contains(event.target) && this.wasOpen) { + this.close(); } } - destroy() { - // TODO + close() { + this.radioButton.checked = false; + this.throttleUpdateWasOpen(); } + isOpen() { + return this.radioButton.checked; + } + + updateWasOpen() { + this.wasOpen = this.isOpen(); + } + + destroy() { /* TODO */ } } + export const NavbarUtils = [ - LanguageSelectUtil, + NavHeaderContainerUtil, ]; diff --git a/frontend/src/utils/navbar/navbar.sass b/frontend/src/utils/navbar/navbar.sass index 05e3e23ab..67094233e 100644 --- a/frontend/src/utils/navbar/navbar.sass +++ b/frontend/src/utils/navbar/navbar.sass @@ -1,49 +1,137 @@ .navbar-container position: relative -.navbar-shadow - position: fixed - right: 0 - top: 0 - height: var(--header-height-collapsed) - width: 20px - z-index: 50 - background-image: linear-gradient(to left, rgba(0, 0, 0, 0.4), transparent) - transition: height 0.2s cubic-bezier(0.03, 0.43, 0.58, 1) - -@media (min-width: 768px) - .navbar-shadow - height: var(--header-height) - -@media (min-width: 1025px) - .navbar-shadow - display: none - .navbar position: fixed - display: flex - flex-direction: row - align-items: center - justify-content: flex-start right: 0 top: 0 left: var(--asidenav-width-xl) - height: var(--header-height) + min-height: var(--header-height) background-color: var(--color-primary) color: white - z-index: 20 + z-index: 22 box-shadow: 0 0 4px rgba(0, 0, 0, 0.2) overflow: auto transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1) + margin: 0 + padding: 10px 0 -@media (max-width: 1199px) - .navbar + @media (max-width: 1199px) left: var(--asidenav-width-lg) -@media (max-width: 768px) - .navbar + @media (max-width: 768px), (max-height: 500px) + min-height: var(--header-height-collapsed) + padding: 0 + + @media (max-width: 768px) left: 0 + display: flex + & > * + flex-grow: 1 + +.navbar__stack + display: flex + flex-flow: column nowrap + + & > * + flex-grow: 1 + +.navbar__list-wrapper + display: flex + flex-flow: row nowrap + justify-content: space-between + align-items: center + margin: 0 + + @media (min-width: 769px) + margin: 0 40px + +.navbar__list + display: flex + flex-flow: row nowrap + justify-content: flex-end + align-items: center + list-style-type: none + margin: 0 + + &.navbar__list-left + justify-content: flex-start + margin-right: 40px + + & > * + display: block + +.navbar__container-list + /* margin: 10px 0 0 0 */ + position: relative + padding: 0 40px + overflow: hidden + display: flex + flex-grow: 1 + + & > ul + display: flex + flex-grow: 1 + flex-flow: row nowrap + align-items: center + overflow: overlay + list-style-type: none + justify-content: flex-end + margin: 0 + + & > * + display: block + + @media (min-width: 501px) + margin-right: 12px + + &:last-child + margin-right: 0 + + &.navbar__container-list--left > ul + justify-content: flex-start + + @media (max-width: 768px) + padding: 0 + + margin: 0 + height: 0 + transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1) + + .navbar__container-list-closer + position: absolute + top: 5px + right: 10px + width: 20px + height: 20px + text-align: center + + transform-origin: 10px 10px + transform: rotate(-0.25turn) + + opacity: 0.5 + transition: transform 0.2s, opacity 0.2s ease + + &:hover + opacity: 1 + + transform: scale(1.4) + + &.navbar__container-list-closer--hidden + visibility: hidden + + @media (max-width: 768px) + visibility: hidden + + &.navbar__container-list--left .navbar__container-list-closer + transform: rotate(0.25turn) + right: auto + left: 10px + + &:hover + transform: scale(1.4) + // links .navbar__link-wrapper display: flex @@ -57,6 +145,10 @@ overflow: hidden cursor: pointer + @media (max-width: 768px), (max-height: 500px) + height: var(--header-height-collapsed) + + .navbar__link-icon opacity: 0.7 transition: opacity 0.2s ease @@ -67,12 +159,13 @@ padding: 2px 4px text-transform: uppercase font-weight: 600 + font-size: 16px -@media (min-width: 769px) +@media (min-width: 769px) and (min-height: 501px) .navbar__link-wrapper border: 1px solid rgba(255, 255, 255, 0.7) -@media (max-width: 768px) +@media (max-width: 768px), (max-height: 500px) .navbar__link-wrapper box-shadow: none min-width: 0 @@ -86,72 +179,56 @@ transform: scale(0.65) margin-bottom: 0 -// navbar list -.navbar__list +.navbar__container-link + display: block + + @media (min-width: 769px) and (min-height: 501px) + border: 1px solid rgba(255, 255, 255, 0.7) + + height: 30px + color: var(--color-lightwhite) !important + background-color: rgba(0, 0, 0, 0) !important + padding: 5px 10px + text-transform: uppercase + font-weight: 600 + font-size: 16px + outline: 0 + min-width: 0 + transition: none + cursor: pointer white-space: nowrap - + .navbar__list - margin-left: 12px + &:not(.navbar__container-link--active):hover + background-color: var(--color-dark) !important + color: var(--color-lightwhite) !important -@media (min-width: 769px) - .navbar__list:last-of-type - padding-right: 40px + &.navbar__container-link--active + background-color: var(--color-lightwhite) !important + color: var(--color-dark) !important -@media (max-width: 768px) - .navbar__list - + .navbar__list - margin-left: 0 - padding-right: 40px // list item .navbar__list-item position: relative transition: background-color .1s ease - &:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper - margin-left: 12px - - &:not(.navbar__list-item--favorite) + .navbar__list-item + & + .navbar__list-item margin-left: 12px @media (max-width: 500px) .navbar__list-item min-width: 60px - &:not(.navbar__list-item--favorite) + .navbar__list-item + & + .navbar__list-item margin-left: 0 - &:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper - margin-left: 0 - -.navbar__list-left - flex: 5 - padding-left: 40px - -@media (max-width: 768px) - .navbar__list-left - padding-left: 0 - // "Favorites" list item, only visible on small screens and logged in -.navbar__list-item - &.navbar__list-item--favorite - display: none - -.navbar__list-item--favorite - display: none - background-color: var(--color-primary) - -.logged-in - .navbar__list - li.navbar__list-item--favorite, - .navbar__list-item--favorite - display: inline-block - @media (min-width: 426px) - .logged-in - .navbar__list - .navbar__list-item--favorite - display: none !important + .navbar__list-item--favorite + display: none !important + + & + .navbar__list-item + margin-left: 0 .navbar__list-item--active background-color: var(--color-lightwhite) @@ -163,7 +240,7 @@ .navbar__list-item--active .navbar__link-wrapper color: var(--color-dark) -.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper, #lang-checkbox:checked ~ * .navbar__link-wrapper +.navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-wrapper background-color: var(--color-dark) color: var(--color-lightwhite) @@ -186,43 +263,5 @@ display: block height: var(--header-height-collapsed) -@media (max-width: 768px) - .navbar, - .navbar__pushdown - height: var(--header-height-collapsed) - - .navbar__link-wrapper - height: var(--header-height-collapsed) - -@media (max-height: 500px) - .navbar, - .navbar__pushdown - height: var(--header-height-collapsed) - - .navbar__link-wrapper - height: var(--header-height-collapsed) - -#lang-dropdown +.navbar__container-radio--none, .navbar__container-radio display: none - position: fixed - top: var(--header-height) - right: 0 - min-width: 200px - z-index: 10 - background-color: white - border-radius: 2px - box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) - - select - display: block - - button - display: block - width: 100% - -#lang-checkbox:checked ~ #lang-dropdown - display: block - -@media (max-width: 768px) - #lang-dropdown - top: var(--header-height-collapsed) diff --git a/frontend/src/utils/pageactions/pageactions.js b/frontend/src/utils/pageactions/pageactions.js new file mode 100644 index 000000000..97a719ea4 --- /dev/null +++ b/frontend/src/utils/pageactions/pageactions.js @@ -0,0 +1,110 @@ +import { Utility } from '../../core/utility'; +import './pageactions.sass'; +import * as throttle from 'lodash.throttle'; + +export const PAGEACTION_SECONDARY_UTIL_SELECTOR = '.pagenav__list-item'; +const PAGEACTION_SECONDARY_INITIALIZED_CLASS = '.pagenav-list-item--initialized'; +const PAGEACTION_SECONDARY_CLASS = 'pagenav-secondary'; + +@Utility({ + selector: PAGEACTION_SECONDARY_UTIL_SELECTOR, +}) +export class PageActionSecondaryUtil { + _element; + navIdent; + radioButton; + closeButton; + container; + wasOpen; + + _throttleUpdateWasOpen; + + constructor(element) { + if (!element) { + throw new Error('Pageaction Secondary utility needs to be passed an element!'); + } + + if (element.classList.contains(PAGEACTION_SECONDARY_INITIALIZED_CLASS)) { + return false; + } + + this._element = element; + + const childContainer = this._element.querySelector('.pagenav-item__children'); + + if (!childContainer) { + return false; + } + + if (this._element.classList.contains(PAGEACTION_SECONDARY_CLASS)) { + this.navIdent = 'secondary'; + } else { + const links = Array.from(this._element.querySelectorAll('.pagenav-item__link')).filter(l => !childContainer.contains(l)); + + if (!links || Array.from(links).length !== 1) { + throw new Error('Pageaction Secondary utility could not find associated link!'); + } + this.navIdent = links[0].id; + } + + this.radioButton = document.getElementById(`pageaction-item__expand-${this.navIdent}`); + if (!this.radioButton) { + throw new Error('Pageaction Secondary utility could not find associated radio button!'); + } + + this.closeButton = document.getElementById('pageaction-item__expand-none'); + if (!this.closeButton) { + throw new Error('Pageaction Secondary utility could not find radio button for closing!'); + } + + this.container = document.querySelector('.pagenav-item__children-wrapper'); + if (!this.container) { + throw new Error('Pageaction Secondary utility could not find associated container!'); + } + + const closer = this._element.querySelector('.pagenav-item__close-label'); + if (closer) { + closer.classList.add('pagenav-item__close-label--hidden'); + } + + this.updateWasOpen(); + this.throttleUpdateWasOpen = throttle(this.updateWasOpen.bind(this), 100, { leading: false, trailing: true }); + + this._element.classList.add(PAGEACTION_SECONDARY_INITIALIZED_CLASS); + } + + start() { + if (!this.container) + return; + + window.addEventListener('click', this.clickHandler.bind(this)); + this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this)); + } + + clickHandler() { + if (!this.container.contains(event.target) && window.document.contains(event.target) && this.wasOpen) { + this.close(); + } + } + + close() { + this.radioButton.checked = false; + this.throttleUpdateWasOpen(); + } + + isOpen() { + return this.radioButton.checked; + } + + updateWasOpen() { + this.wasOpen = this.isOpen(); + } + + destroy() { /* TODO */ } +} + + + +export const PageActionsUtils = [ + PageActionSecondaryUtil, +]; diff --git a/frontend/src/utils/pageactions/pageactions.sass b/frontend/src/utils/pageactions/pageactions.sass new file mode 100644 index 000000000..9dbdad870 --- /dev/null +++ b/frontend/src/utils/pageactions/pageactions.sass @@ -0,0 +1,190 @@ + +.pagenav + display: flex + align-content: flex-start + align-items: flex-start + flex-flow: row wrap + padding: 0 0 10px 0 + margin: -5px -5px 20px -5px + border-bottom: 1px solid #eee + list-style: none + +.pagenav-item__expand-radio + display: none + +.pagenav-item__link + display: block + padding: 6px 10px + background-color: white + + &:hover + background-color: var(--color-grey-light) + +a.pagenav-item__link, .pagenav-item__link a + text-decoration: none + +.pagenav__list-item + position: relative + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.6) + margin: 5px + padding: 0 + display: grid + grid-template-areas: "label expand" + grid-template-columns: auto minmax(0, auto) + flex: 0 0 auto + + & > * + grid-area: label + place-self: stretch / stretch + line-height: 20px + + &.pagenav-item__children-wrapper + grid-column: label-start / expand-end + + & > .pagenav-item__expand-label + display: flex + justify-content: center + align-items: center + grid-area: expand + background-color: white + transition: background-color 0.2s ease + padding: 6px 10px + cursor: pointer + + .fas + line-height: 20px + + &:hover + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.9) + + & > .pagenav-item__expand-label + background-color: var(--color-grey-light) + + .pagenav-item__link:hover ~ .pagenav-item__expand-label + background-color: white + + .pagenav-item__expand-radio:checked ~ .pagenav-item__expand-label + background-color: var(--color-grey-light) + + .pagenav-secondary & + grid-template-areas: "expand" + & > .pagenav-item__children-wrapper + grid-column: exand-start / exand-end + + +.pagenav-item__children + flex: 1 0 auto + list-style: none + margin: 0 + padding: 0 + display: grid + grid-template-rows: auto + justify-items: stretch + grid-auto-columns: max-content + + .pagenav-item__link + max-width: 250px + + & > li + display: flex + + & > * + flex: 1 0 auto + +.pagenav-item__close-label + display: none + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.6) + padding: 6px 10px + transition: all 0.2s ease + position: absolute + top: 100% + right: 0 + background-color: white + z-index: -1 + cursor: pointer + + .fas + transition: all 0.2s ease + opacity: 0.5 + transform: rotate(0.5turn) + line-height: 20px + + &:hover + background-color: var(--color-grey-light) + .fas + transform: rotate(0) + opacity: 1 + +.pagenav-item__children-wrapper + display: none + position: absolute + right: 0 + top: 100% + background-color: white + z-index: 21 + margin: 0 + padding: 0 + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.6) + + .pagenav-item__expand-radio:checked ~ &, .pagenav-item__expand-label:hover ~ &, &:hover + display: flex + + .pagenav__list-item:hover & + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.9) + + .pagenav-item__expand-radio:checked ~ & + .pagenav-item__close-label:not(.pagenav-item__close-label--hidden) + display: block + +/* .pagenav__link-wrapper +/* flex: 1 +/* padding: 10px 10px 12px +/* text-decoration: none !important + +/* &:hover +/* background-color: var(--color-grey-light) + +/* @media (max-width: 1024px) +/* .pagenav +/* flex-direction: column + +/* @media (min-width: 1025px) +/* .pagenav-secondary +/* position: relative +/* overflow: visible +/* padding-top: 10px + +/* &::after +/* content: '\2026' +/* display: inline-block +/* padding: 10px 10px 12px +/* width: 40px +/* box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.6) +/* box-sizing: border-box +/* text-align: center +/* transition: box-shadow 0.2s ease + +/* &:hover +/* &::after +/* box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.8) + +/* .pagenav-secondary__list +/* display: block + +/* .pagenav-secondary__list +/* position: absolute +/* display: none +/* right: 0 +/* top: 50px +/* width: 250px +/* background-color: white +/* box-shadow: 0 0 6px 3px var(--color-grey-light) +/* z-index: 18 + +/* .pagenav__list-item--secondary +/* display: flex +/* box-shadow: none +/* margin: 0 + +/* &:hover +/* background-color: var(--color-grey-light) diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 215134503..8727b1844 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -11,6 +11,7 @@ import { Modal } from './modal/modal'; import { Tooltip } from './tooltips/tooltips'; import { CourseTeaser } from './course-teaser/course-teaser'; import { NavbarUtils } from './navbar/navbar'; +import { PageActionsUtils } from './pageactions/pageactions'; import { HideColumns } from './hide-columns/hide-columns'; import { ExamCorrect } from './exam-correct/exam-correct'; import { SortTable } from './sort-table/sort-table'; @@ -30,6 +31,7 @@ export const Utils = [ Tooltip, CourseTeaser, ...NavbarUtils, + ...PageActionsUtils, HideColumns, ExamCorrect, SortTable, diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index c92ffab3c..e79cff87f 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -494,7 +494,7 @@ CorrectorsPlaceholder: Korrektoren... CorrectorsDefaulted: Korrektoren-Liste wurde aus bisherigen Übungsblättern diesen Kurses generiert. Es sind keine Daten gespeichert. Users: Benutzer -HomeHeading: Aktuelle Termine +NewsHeading: Aktuelles LoginHeading: Authentifizierung LoginTitle: Authentifizierung ProfileHeading: Benutzereinstellungen @@ -509,9 +509,9 @@ NotificationSettingsHeading displayName@Text: Benachrichtigungs-Einstellungen f TokensLastReset: Tokens zuletzt invalidiert TokensResetSuccess: Authorisierungs-Tokens invalidiert -HomeOpenAllocations: Offene Zentralanmeldungen -HomeUpcomingSheets: Anstehende Übungsblätter -HomeUpcomingExams: Bevorstehende Prüfungen +NewsOpenAllocations: Offene Zentralanmeldungen +NewsUpcomingSheets: Anstehende Übungsblätter +NewsUpcomingExams: Bevorstehende Prüfungen NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"} CloseAlert: Schliessen @@ -886,9 +886,9 @@ MailSubjectSubmissionsUnassigned csh@CourseShorthand sheetName@SheetName: Abgabe MailSubmissionsUnassignedIntro n@Int courseName@Text termDesc@Text sheetName@SheetName: #{n} Abgaben zu #{sheetName} im Kurs #{courseName} (#{termDesc}) konnten nicht automatisiert verteilt werden. MailSubjectSheetSoonInactive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} kann nur noch kurze Zeit abgegeben werden -MailSheetSoonInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) endet in Kürze. +MailSheetSoonInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Abgabefrist für #{sheetName} im Kurs #{courseName} (#{termDesc}) endet in Kürze. MailSubjectSheetInactive csh@CourseShorthand sheetName@SheetName: Abgabezeitraum für #{sheetName} in #{csh} abgelaufen -MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName n@Int num@Int64: Die Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet. Es gab #{noneOneMoreDE n "Keine Abgaben" "Nur eine Abgabe von " (toMessage n <> " Abgaben von ")}#{noneOneMoreDE num "" "einem Teilnehmer" (toMessage num <> " Teilnehmern")}. +MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName n@Int num@Int64: Die Abgabefrist für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet. Es gab #{noneOneMoreDE n "Keine Abgaben" "Nur eine Abgabe von " (toMessage n <> " Abgaben von ")}#{noneOneMoreDE num "" "einem Teilnehmer" (toMessage num <> " Teilnehmern")}. MailSubjectCorrectionsAssigned csh@CourseShorthand sheetName@SheetName: Ihnen wurden Korrekturen zu #{sheetName} in #{csh} zugeteilt MailCorrectionsAssignedIntro courseName@Text termDesc@Text sheetName@SheetName n@Int: #{n} #{pluralDE n "Abgabe wurde" "Abgaben wurden"} Ihnen zur Korrektur für #{sheetName} im Kurs #{courseName} (#{termDesc}) zugeteilt. @@ -1115,7 +1115,7 @@ InvalidRoute: Konnte URL nicht interpretieren MenuOpenCourses: Kurse mit offener Registrierung MenuOpenAllocations: Aktive Zentralanmeldungen -MenuHome: Aktuell +MenuNews: Aktuell MenuInformation: Informationen MenuLegal: Rechtliche Informationen MenuDataProt: Datenschutzerklärung @@ -1186,7 +1186,7 @@ MenuTutorialEdit: Tutorium editieren MenuTutorialComm: Mitteilung an Teilnehmer MenuExamList: Prüfungen MenuExamNew: Neue Prüfung anlegen -MenuExamEdit: Bearbeiten +MenuExamEdit: Prüfung bearbeiten MenuExamUsers: Teilnehmer MenuExamGrades: Prüfungsleistungen MenuExamAddMembers: Prüfungsteilnehmer hinzufügen @@ -1272,8 +1272,8 @@ BreadcrumbExternalExamGrades: Prüfungsleistungen BreadcrumbExternalExamStaffInvite: Einladung zum Prüfer BreadcrumbParticipantsList: Kursteilnehmerlisten BreadcrumbParticipants: Kursteilnehmerliste -BreadcrumbStorageKey: Lokalen Schlüssel generieren BreadcrumbExamAutoOccurrence: Automatische Termin-/Raumverteilung +BreadcrumbStorageKey: Lokalen Schlüssel generieren ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn} ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn} @@ -1399,10 +1399,10 @@ ExamRegistrationInviteHeading examn@ExamName: Einladung zum Teilnehmer für #{ex ExamRegistrationInviteExplanation: Sie wurden eingeladen, Prüfungsteilnehmer zu sein. ExamCorrectHeading examname@Text: Prüfungsergebnisse für #{examname} eintragen +ExamCorrectExamResultDelete: Prüfungsergebnis löschen ExamCorrectHeadDate: Zeit ExamCorrectHeadParticipant: Teilnehmer -ExamCorrectHeadParticipantTooltip: Geben Sie hier einen beliebigen eindeutigen Identifikator des Teilnehmers an. Definitiv eindeutig ist die Matrikelnummer des Teilnehmers, aber auch der Name oder ein Teil der Matrikelnummer können unter Umständen (je nach Liste aller Prüfungsteilnehmer) bereits eindeutig sein. ExamCorrectHeadPart exampartnum@ExamPartNumber: #{exampartnum} ExamCorrectHeadPartName exampartname@ExamPartName: #{exampartname} ExamCorrectHeadStatus: Status @@ -1411,6 +1411,8 @@ ExamCorrectButtonSend: Senden ExamCorrectErrorMultipleMatchingParticipants: Dem Identifikator konnten mehrere Prüfungsteilnehmer zugeordnet werden. ExamCorrectErrorNoMatchingParticipants: Dem Identifikator konnte kein Prüfungsteilnehmer zugeordnet werden. +ExamCorrectErrorPartResultOutOfBounds examPartNumber@ExamPartNumber: Prüfungsergebnis für Teil #{examPartNumber} ist nicht größer Null. +ExamCorrectErrorPartResultOutOfBoundsMax examPartNumber@ExamPartNumber maxPoints@Points: Prüfungsergebnis für Teil #{examPartNumber} liegt nicht zwischen 0 und #{maxPoints}. SubmissionUserInvitationAccepted shn@SheetName: Sie wurden als Mitabgebende(r) für eine Abgabe zu #{shn} eingetragen SubmissionUserInvitationDeclined shn@SheetName: Sie haben die Einladung, Mitabgebende(r) für #{shn} zu werden, abgelehnt @@ -1553,9 +1555,9 @@ ExamDeregisterUntil: Abmeldung bis ExamPublishOccurrenceAssignments: Termin- bzw. Raumzuteilung den Teilnehmern mitteilen um ExamPublishOccurrenceAssignmentsTip: Ab diesem Zeitpunkt Teilnehmer einsehen zu welcher Teilprüfung bzw. welchen Raum sie angemeldet sind ExamPublishOccurrenceAssignmentsParticipant: Termin- bzw. Raumzuteilung einsehbar ab -ExamFinished: Bewertung abgeschlossen ab +ExamFinished: Ergebnisse sichtbar ab ExamFinishedOffice: Noten bekannt gegeben -ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen +ExamFinishedParticipant: Bewertung voraussichtlich abgeschlossen ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden ExamClosed: Noten gemeldet ExamClosedTip: Prüfungsbeauftraget, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert @@ -1580,7 +1582,6 @@ ExamBonusRule: Prüfungsbonus aus Übungsbetrieb ExamNoBonus': Kein automatischer Bonus ExamBonusPoints': Umrechnung von Übungspunkten ExamBonusManual': Manuelle Berechnung -ExamGradesExplanation: Diese Ansicht zeigt die selben Daten an, wie die Tabelle von Prüfungsteilnehmern. Anpassen der Teilnehmerdaten und Ergebnisse ist nur dort möglich. Hier können Sie vor Allem einsehen und markieren, welche Prüfungsleistungen von den zuständigen Prüfungsbeauftragten bereits vollständig bearbeitet wurden. ExamRegisterForOccurrence: Anmeldung zur Klausur erfolgt durch Anmeldung zu einem Termin/Raum @@ -1673,12 +1674,12 @@ ExamLoginToRegister: Um sich zum Kurs anzumelden müssen Sie zunächst in Uni2wo ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen -ExamStartMustBeAfterPublishOccurrenceAssignments: Start muss nach Veröffentlichung der Termin- bzw. Raumzuordnung liegen +ExamStartMustBeAfterPublishOccurrenceAssignments: Beginn muss nach Veröffentlichung der Termin- bzw. Raumzuordnung liegen ExamEndMustBeAfterStart: Beginn der Prüfung muss vor ihrem Ende liegen -ExamFinishedMustBeAfterEnd: "Bewertung abgeschlossen ab" muss nach Ende liegen -ExamFinishedMustBeAfterStart: "Bewertung abgeschlossen ab" muss nach Start liegen -ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Bewertung abgeschlossen ab" liegen -ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Start liegen +ExamFinishedMustBeAfterEnd: "Ergebnisse sichtbar ab" muss nach Ende liegen +ExamFinishedMustBeAfterStart: "Ergebnisse sichtbar ab" muss nach Beginn liegen +ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Ergebnisse sichtbar ab" liegen +ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Beginn liegen ExamClosedMustBeAfterEnd: "Noten stehen fest ab" muss nach Ende liegen ExamOccurrenceEndMustBeAfterStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss vor seinem Ende liegen diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index c9c87345a..8f829c850 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -492,7 +492,7 @@ CorrectorsPlaceholder: Correctors... CorrectorsDefaulted: List of correctors was automatically generated based on those of preceding sheets for this course. No data has been saved, yet. Users: Users -HomeHeading: Home +NewsHeading: News LoginHeading: Authentication LoginTitle: Authentication ProfileHeading: Settings @@ -507,9 +507,9 @@ NotificationSettingsHeading displayName: Notification settings for #{displayName TokensLastReset: Tokens last reset TokensResetSuccess: Successfully invalidated all authorisation tokens -HomeOpenAllocations: Active central allocations -HomeUpcomingSheets: Upcoming exercise sheets -HomeUpcomingExams: Upcoming exams +NewsOpenAllocations: Active central allocations +NewsUpcomingSheets: Upcoming exercise sheets +NewsUpcomingExams: Upcoming exams NumCourses num: #{num} #{pluralEN num "course" "courses"} CloseAlert: Close @@ -1114,7 +1114,7 @@ InvalidRoute: Could not interpret url MenuOpenCourses: Courses with open registration MenuOpenAllocations: Active central allocations -MenuHome: Home +MenuNews: News MenuInformation: Information MenuLegal: Legal MenuDataProt: Data protection @@ -1185,7 +1185,7 @@ MenuTutorialEdit: Edit tutorial MenuTutorialComm: Send course message MenuExamList: Exams MenuExamNew: Create new exam -MenuExamEdit: Edit +MenuExamEdit: Edit exam MenuExamUsers: Participants MenuExamGrades: Exam results MenuExamAddMembers: Add exam participants @@ -1271,8 +1271,8 @@ BreadcrumbExternalExamGrades: Exam results BreadcrumbExternalExamStaffInvite: Invitation BreadcrumbParticipantsList: Lists of course participants BreadcrumbParticipants: Course participants -BreadcrumbStorageKey: Generate storage key BreadcrumbExamAutoOccurrence: Automatic occurrence/room distribution +BreadcrumbStorageKey: Generate storage key ExternalExamEdit coursen examn: Edit: #{coursen}, #{examn} ExternalExamGrades coursen examn: Exam achievements: #{coursen}, #{examn} @@ -1400,7 +1400,6 @@ ExamCorrectHeading examname: Submit corrections for #{examname} ExamCorrectHeadDate: Time ExamCorrectHeadParticipant: Participant -ExamCorrectHeadParticipantTooltip: Enter any string that uniquely identifies the participant. Their matriculation number is definitely unique, but also their name or a part of their matriculation number may already be unique for this participant (depending on the list of all participants). ExamCorrectHeadPart exampartnum: #{exampartnum} ExamCorrectHeadPartName exampartname: #{exampartname} ExamCorrectHeadStatus: Status @@ -1409,6 +1408,10 @@ ExamCorrectButtonSend: Submit ExamCorrectErrorMultipleMatchingParticipants: This identifier matches on multiple exam participants. ExamCorrectErrorNoMatchingParticipants: This identifier does not match on any exam participant. +ExamCorrectErrorPartResultOutOfBounds examPartNumber: Exam part result for #{examPartNumber} ist not greater zero. +ExamCorrectErrorPartResultOutOfBoundsMax examPartNumber maxPoints: Exam part result for #{examPartNumber} is not between 0 and #{maxPoints}. + +ExamCorrectExamResultDelete: Delete exam result SubmissionUserInvitationAccepted shn: You now participate in a submission for #{shn} SubmissionUserInvitationDeclined shn: You have declined the invitation to participate in a submission for #{shn} @@ -1551,7 +1554,7 @@ ExamDeregisterUntil: Deregister until ExamPublishOccurrenceAssignments: Publish occurrence/room-assignments ExamPublishOccurrenceAssignmentsTip: At this time participants are informed to which occurrence/room they are assigned ExamPublishOccurrenceAssignmentsParticipant: Occurrence/room-assignments published -ExamFinished: Marking finished +ExamFinished: Results visible from ExamFinishedOffice: Exam achievements published ExamFinishedParticipant: Marking expected to be finished ExamFinishedTip: At this participants are informed of their exam achievements @@ -1578,7 +1581,6 @@ ExamBonusRule: Bonus points from exercises ExamNoBonus': No automatic exam bonus ExamBonusPoints': Compute from exercise achievements ExamBonusManual': Manual computation -ExamGradesExplanation: This view shows the same data as the table of exam participants. Changing participant's data and achievements is only possible via the table of exam participants. Primarily, this view allows you to check and adjust which exam achievements were properly handled by the relevant exam offices. ExamRegisterForOccurrence: Registration for this exam is done by registering for an occurrence/room @@ -1631,6 +1633,7 @@ ExamFormCorrection: Correction ExamFormParts: Exam parts ExamCorrectors: Correctors +ExamCorrectorsTip: Correctors configured here may, after the start of the exam and until "Results visible from", enter exam part results for all exam parts and participants. ExamCorrectorAlreadyAdded: A corrector with this email address already exists ExamParts: Exam parts/questions @@ -1672,9 +1675,9 @@ ExamRegisterToMustBeAfterRegisterFrom: "Register to" must be after "register fro ExamDeregisterUntilMustBeAfterRegisterFrom: "Deregister until" must be after "register from" ExamStartMustBeAfterPublishOccurrenceAssignments: "Start" must be after "publish occurrence/room-assignments" ExamEndMustBeAfterStart: "End" must be after "start" -ExamFinishedMustBeAfterEnd: "Marking finished" must be after "end" -ExamFinishedMustBeAfterStart: "Marking finished" must be after "start" -ExamClosedMustBeAfterFinished: "Exam achievements registered" must be after "marking finished" +ExamFinishedMustBeAfterEnd: "Results visible from" must be after "end" +ExamFinishedMustBeAfterStart: "Results visible from" must be after "start" +ExamClosedMustBeAfterFinished: "Exam achievements registered" must be after "results visible from" ExamClosedMustBeAfterStart: "Exam achievements registered" must be after "start" ExamClosedMustBeAfterEnd: "Exam achievements registered" must be after "end" diff --git a/records.json b/records.json index a2a5ae297..d84014b37 100644 --- a/records.json +++ b/records.json @@ -789,5 +789,18 @@ "usedIds": [] } } + ], + "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/pageactions/pageactions.sass": [ + { + "modules": { + "byIdentifier": {}, + "usedIds": {} + }, + "chunks": { + "byName": {}, + "bySource": {}, + "usedIds": [] + } + } ] } \ No newline at end of file diff --git a/routes b/routes index 9f7a5b92f..753ea445a 100644 --- a/routes +++ b/routes @@ -41,7 +41,7 @@ /metrics MetricsR GET -/ HomeR GET !free +/ NewsR GET !free /users UsersR GET POST -- no tags, i.e. admins only /users/#CryptoUUIDUser AdminUserR GET POST /users/#CryptoUUIDUser/delete AdminUserDeleteR POST @@ -189,8 +189,8 @@ /register ERegisterR POST !timeANDcourse-registeredAND¬exam-registered !timeANDexam-registeredAND¬exam-result /register/#ExamOccurrenceName ERegisterOccR POST !exam-occurrence-registrationANDtimeANDcapacityANDcourse-registeredAND¬exam-occurrence-registered !exam-occurrence-registrationANDtimeANDexam-occurrence-registeredAND¬exam-result /grades EGradesR GET POST !exam-office - /correct ECorrectR GET POST !exam-correctorANDtime /assign-occurrences EAutoOccurrenceR POST + /correct ECorrectR GET POST !exam-correctorANDtime /apps CApplicationsR GET POST !/apps/files CAppsFilesR GET /apps/#CryptoFileNameCourseApplication CourseApplicationR: diff --git a/src/Application.hs b/src/Application.hs index f61813796..c2274a508 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -101,7 +101,7 @@ import Data.List (cycle) -- Import all relevant handler modules here. -- (HPack takes care to add new modules to our cabal file nowadays.) -import Handler.Home +import Handler.News import Handler.Info import Handler.Help import Handler.Profile diff --git a/src/Foundation.hs b/src/Foundation.hs index 7c8878a40..ec9808928 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1,5 +1,7 @@ {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedLabels #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- MonadCrypto module Foundation @@ -63,6 +65,7 @@ import Control.Monad.Except (MonadError(..), ExceptT) import Control.Monad.Trans.Maybe (MaybeT(..)) import Control.Monad.Trans.Reader (runReader, mapReaderT) import Control.Monad.Trans.Writer (WriterT(..), runWriterT) +import Control.Monad.Trans.State (execStateT) import Control.Monad.Writer.Class (MonadWriter(..)) import Control.Monad.Memo.Class (MonadMemo(..), for4) import qualified Control.Monad.Catch as C @@ -79,6 +82,7 @@ import Utils.Sheet import Utils.SystemMessage import Text.Shakespeare.Text (st) +import Text.Cassius (cassiusFile) import Yesod.Form.I18n.German import Yesod.Form.I18n.English @@ -113,64 +117,99 @@ instance RenderMessage UniWorX (UnsupportedAuthPredicate AuthTag (Route UniWorX) mr = renderMessage f ls (pieces, _) = renderRoute route +data NavQuickView + = NavQuickViewFavourite + | NavQuickViewPageActionSecondary + deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) +instance Universe NavQuickView +instance Finite NavQuickView --- Menus and Favourites -data MenuType = NavbarAside | NavbarRight | NavbarSecondary | PageActionPrime | PageActionSecondary | Footer - deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic) +navQuick :: NavQuickView -> (NavQuickView -> Any) +navQuick x x' = Any $ x == x' -instance Universe MenuType -instance Finite MenuType +data NavType + = NavTypeLink + { navModal :: Bool + } + | NavTypeButton + { navMethod :: StdMethod + , navData :: [(Text, Text)] + } deriving (Eq, Ord, Read, Show, Generic, Typeable) -makePrisms ''MenuType +makeLenses_ ''NavType +makePrisms ''NavType -data MenuItem = MenuItem - { menuItemLabel :: UniWorXMessage - , menuItemIcon :: Maybe Text -- currently from: https://fontawesome.com/icons?d=gallery - , menuItemRoute :: SomeRoute UniWorX - , menuItemAccessCallback' :: Handler Bool -- Check whether action is shown in ADDITION to authorization (which is always checked) - , menuItemModal :: Bool - , menuItemType :: MenuType +data NavLevel = NavLevelTop | NavLevelInner + deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) + +data NavHeaderRole = NavHeaderPrimary | NavHeaderSecondary + deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) + +data NavLink = forall msg route. (RenderMessage UniWorX msg, HasRoute UniWorX route, RedirectUrl UniWorX route) => NavLink + { navLabel :: msg + , navRoute :: route + , navAccess' :: Handler Bool + , navType :: NavType + , navQuick' :: NavQuickView -> Any + , navForceActive :: Bool } -makeLenses_ ''MenuItem +makeLenses_ ''NavLink -instance RedirectUrl UniWorX MenuItem where - toTextUrl MenuItem{..} = toTextUrl menuItemRoute -instance HasRoute UniWorX MenuItem where - urlRoute MenuItem{..} = urlRoute menuItemRoute +instance HasRoute UniWorX NavLink where + urlRoute NavLink{..} = urlRoute navRoute +instance RedirectUrl UniWorX NavLink where + toTextUrl NavLink{..} = toTextUrl navRoute +instance RenderMessage UniWorX NavLink where + renderMessage app ls NavLink{..} = renderMessage app ls navLabel -menuItemAccessCallback :: MenuItem -> Handler Bool -menuItemAccessCallback MenuItem{..} = and2M ((==) Authorized <$> authCheck) menuItemAccessCallback' +data Nav + = NavHeader + { navHeaderRole :: NavHeaderRole + , navIcon :: Icon + , navLink :: NavLink + } + | NavHeaderContainer + { navHeaderRole :: NavHeaderRole + , navLabel :: SomeMessage UniWorX + , navIcon :: Icon + , navChildren :: [NavLink] + } + | NavPageActionPrimary + { navLink :: NavLink + , navChildren :: [NavLink] + } + | NavPageActionSecondary + { navLink :: NavLink + } + | NavFooter + { navLink :: NavLink + } deriving (Generic, Typeable) + +makeLenses_ ''Nav +makePrisms ''Nav + +data NavChildren +type instance Children NavChildren a = ChildrenNavChildren a +type family ChildrenNavChildren a where + ChildrenNavChildren (SomeMessage UniWorX) = '[] + + ChildrenNavChildren a = Children ChGeneric a + +navAccess :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadCatch m) => Nav -> MaybeT m Nav +navAccess = execStateT $ do + guardM $ preuse _navLink >>= maybe (return True) navLinkAccess + + _navChildren <~ (filterM navLinkAccess =<< use _navChildren) + whenM (hasn't _navLink <$> use id) $ + guardM $ not . null <$> use _navChildren + +navLinkAccess :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX, MonadCatch m) => NavLink -> m Bool +navLinkAccess NavLink{..} = liftHandler navAccess' `and2M` accessCheck navType navRoute where - authCheck = handleAny (\_ -> return . Unauthorized $ error "authCheck caught exception") $ isAuthorized (urlRoute menuItemRoute) False - -$(return []) - - -data instance ButtonClass UniWorX - = BCIsButton - | BCDefault - | BCPrimary - | BCSuccess - | BCInfo - | BCWarning - | BCDanger - | BCLink - | BCMassInputAdd | BCMassInputDelete - deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) -instance Universe (ButtonClass UniWorX) -instance Finite (ButtonClass UniWorX) - -instance PathPiece (ButtonClass UniWorX) where - toPathPiece BCIsButton = "btn" - toPathPiece bClass = ("btn-" <>) . camelToPathPiece' 1 $ tshow bClass - fromPathPiece = finiteFromPathPiece - - -embedRenderMessage ''UniWorX ''ButtonSubmit id -instance Button UniWorX ButtonSubmit where - btnClasses BtnSubmit = [BCIsButton, BCPrimary] - + accessCheck :: HasRoute UniWorX route => NavType -> route -> m Bool + accessCheck nt (urlRoute -> route) = handleAll (\_ -> return False) $ bool hasWriteAccessTo hasReadAccessTo (is _NavTypeLink nt) route + getTimeLocale' :: [Lang] -> TimeLocale getTimeLocale' = $(timeLocaleMap [("de-de", "de_DE.utf8"), ("en-GB", "en_GB.utf8")]) @@ -186,9 +225,9 @@ appLanguagesOpts :: ( MonadHandler m ) => m (OptionList Lang) -- ^ Authoritive list of supported Languages appLanguagesOpts = do - mr <- getsYesod renderMessage + MsgRenderer mr <- getMsgRenderer let mkOption l = Option - { optionDisplay = mr (l : filter (/= l) (optionInternalValue <$> langOptions)) (MsgLanguage l) + { optionDisplay = mr $ MsgLanguage l , optionInternalValue = l , optionExternalValue = l } @@ -573,8 +612,6 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of | otherwise -> guard $ visible && NTop (Just cTime) <= NTop examDeregisterUntil - ECorrectR -> guard $ NTop (Just cTime) >= NTop examStart - && NTop (Just cTime) <= NTop examFinished ERegisterOccR occn -> do occId <- (>>= hoistMaybe) . $cachedHereBinary (eId, occn) . lift . getKeyBy $ UniqueExamOccurrence eId occn if @@ -585,6 +622,8 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of -> guard $ visible && NTop examRegisterFrom <= NTop (Just cTime) && NTop (Just cTime) <= NTop examRegisterTo + ECorrectR -> guard $ NTop (Just cTime) >= NTop examStart + && NTop (Just cTime) <= NTop examFinished _ -> return () return Authorized @@ -1350,6 +1389,30 @@ evalAccessCorrector :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) evalAccessCorrector tid ssh csh = evalAccess (CourseR tid ssh csh CNotesR) False +data instance ButtonClass UniWorX + = BCIsButton + | BCDefault + | BCPrimary + | BCSuccess + | BCInfo + | BCWarning + | BCDanger + | BCLink + | BCMassInputAdd | BCMassInputDelete + deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) +instance Universe (ButtonClass UniWorX) +instance Finite (ButtonClass UniWorX) + +instance PathPiece (ButtonClass UniWorX) where + toPathPiece BCIsButton = "btn" + toPathPiece bClass = ("btn-" <>) . camelToPathPiece' 1 $ tshow bClass + fromPathPiece = finiteFromPathPiece + +embedRenderMessage ''UniWorX ''ButtonSubmit id +instance Button UniWorX ButtonSubmit where + btnClasses BtnSubmit = [BCIsButton, BCPrimary] + + updateFavourites :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX) => Maybe (TermId, SchoolId, CourseShorthand) -- ^ Insert course into favourites, as appropriate -> ReaderT SqlBackend m () @@ -1523,16 +1586,16 @@ instance Yesod UniWorX where makeLogger = readTVarIO . snd . appLogger -langForm :: Form (Lang, Route UniWorX) -langForm csrf = do - lang <- selectLanguage appLanguages - route <- getCurrentRoute - (urlRes, urlView) <- mreq hiddenField ("" & addName ("referer" :: Text)) route - (langBoxRes, langBoxView) <- mreq - (selectField appLanguagesOpts) - ("" & addAttr "multiple" "multiple" & addAttr "size" (tshow . min 10 $ length appLanguages) & addAutosubmit & addName ("lang" :: Text)) - (Just lang) - return ((,) <$> langBoxRes <*> urlRes, toWidget csrf <> fvInput urlView <> fvInput langBoxView) +-- langForm :: Form (Lang, Route UniWorX) +-- langForm csrf = do +-- lang <- selectLanguage appLanguages +-- route <- getCurrentRoute +-- (urlRes, urlView) <- mreq hiddenField ("" & addName ("referer" :: Text)) route +-- (langBoxRes, langBoxView) <- mreq +-- (selectField appLanguagesOpts) +-- ("" & addAttr "multiple" "multiple" & addAttr "size" (tshow . min 10 $ length appLanguages) & addAutosubmit & addName ("lang" :: Text)) +-- (Just lang) +-- return ((,) <$> langBoxRes <*> urlRes, toWidget csrf <> fvInput urlView <> fvInput langBoxView) siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX) => msg -> Widget -> Handler Html siteLayoutMsg msg widget = do @@ -1579,12 +1642,6 @@ siteLayout' headingOverride widget = do -- let isParent :: Route UniWorX -> Bool -- isParent r = r == (fst parents) - defaultLinks' <- defaultLinks - let menu :: [MenuItem] - menu = defaultLinks' ++ maybe [] pageActions mcurrentRoute - - menuTypes <- mapM (\x -> (,,) <$> pure x <*> newIdent <*> toTextUrl x) =<< filterM menuItemAccessCallback menu - isAuth <- isJust <$> maybeAuthId -- Lookup Favourites & Theme if possible @@ -1639,9 +1696,16 @@ siteLayout' headingOverride widget = do -> let courseRoute = CourseR courseTerm courseSchool courseShorthand CShowR favouriteReason = fromMaybe FavouriteCurrent mFavourite in do - items <- filterM menuItemAccessCallback (pageActions courseRoute) - items' <- forM items $ \i -> (i, ) <$> toTextUrl i - return (c, courseRoute, items', favouriteReason) + items' <- pageQuickActions NavQuickViewFavourite courseRoute + items <- forM items' $ \n -> (n,) <$> toTextUrl n + return (c, courseRoute, items, favouriteReason) + + nav'' <- mconcat <$> sequence + [ defaultLinks + , maybe (return []) pageActions mcurrentRoute + ] + nav' <- catMaybes <$> mapM (runMaybeT . navAccess) nav'' + nav <- forM nav' $ \n -> (n,,,) <$> newIdent <*> traverse toTextUrl (n ^? _navLink) <*> traverse (\nc -> (nc,, ) <$> newIdent <*> toTextUrl nc) (n ^. _navChildren) mmsgs <- if | isModal -> getMessages @@ -1652,21 +1716,23 @@ siteLayout' headingOverride widget = do \authTag -> addMessageWidget Info $ msgModal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute])) getMessages - (langFormView, langFormEnctype) <- generateFormPost $ identifyForm FIDLanguage langForm - let langFormView' = wrapForm langFormView def - { formAction = Just $ SomeRoute LangR - , formSubmit = FormAutoSubmit - , formEncoding = langFormEnctype - } + -- (langFormView, langFormEnctype) <- generateFormPost $ identifyForm FIDLanguage langForm + -- let langFormView' = wrapForm langFormView def + -- { formAction = Just $ SomeRoute LangR + -- , formSubmit = FormAutoSubmit + -- , formEncoding = langFormEnctype + -- } - let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority - highlight = let crumbs = mcons mcurrentRoute $ view _1 <$> reverse parents - navItems = map (view _2) favourites ++ map (urlRoute . menuItemRoute . view _1) menuTypes - highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map (view _2) favourites) crumbs - in \r -> Just r == highR + let highlight :: HasRoute UniWorX url => url -> Bool + -- ^ highlight last route in breadcrumbs, favorites taking priority + highlight = (highR ==) . Just . urlRoute + where crumbs = mcons mcurrentRoute $ view _1 <$> reverse parents + navItems = map (view _2) favourites ++ toListOf (folded . typesUsing @NavChildren @NavLink . to urlRoute) nav + highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map (view _2) favourites) crumbs + highlightNav = (||) <$> navForceActive <*> highlight favouriteTerms :: [TermIdentifier] favouriteTerms = take maxFavouriteTerms . Set.toDescList $ foldMap (\(Course{..}, _, _, _) -> Set.singleton $ unTermKey courseTerm) favourites - favouriteTermReason :: TermIdentifier -> FavouriteReason -> [(Course, Route UniWorX, [(MenuItem, Text)], FavouriteReason)] + favouriteTermReason :: TermIdentifier -> FavouriteReason -> [(Course, Route UniWorX, [(NavLink, Text)], FavouriteReason)] favouriteTermReason tid favReason' = favourites & filter (\(Course{..}, _, _, favReason) -> unTermKey courseTerm == tid && favReason == favReason') & sortOn (\(Course{..}, _, _, _) -> courseName) @@ -1677,24 +1743,101 @@ siteLayout' headingOverride widget = do -- value passed to hamletToRepHtml cannot be a widget, this allows -- you to use normal widget features in default-layout. - navbarModal (MenuItem{..}, menuIdent') = customModal Modal - { modalTriggerId = Just menuIdent' - , modalId = Nothing - , modalTrigger = \(Just route) menuIdent -> $(widgetFile "widgets/navbar/item") - , modalContent = Left menuItemRoute - } + navWidget :: (Nav, Text, Maybe Text, [(NavLink, Text, Text)]) -> Widget + navWidget (n, navIdent, navRoute', navChildren') = case n of + NavHeader{ navLink = navLink@NavLink{..}, .. } + | NavTypeLink{..} <- navType + , navModal + -> customModal Modal + { modalTriggerId = Just navIdent + , modalId = Nothing + , modalTrigger = \(Just route) ident -> $(widgetFile "widgets/navbar/item") + , modalContent = Left $ SomeRoute navLink + } + | NavTypeLink{} <- navType + -> let route = navRoute' + ident = navIdent + in $(widgetFile "widgets/navbar/item") + NavPageActionPrimary{ navLink = navLink@NavLink{..}, .. } + -> let pWidget + | NavTypeLink{..} <- navType + , navModal + = customModal Modal + { modalTriggerId = Just navIdent + , modalId = Nothing + , modalTrigger = \(Just route) ident -> $(widgetFile "widgets/pageaction/primary") + , modalContent = Left $ SomeRoute navLink + } + | NavTypeLink{} <- navType + = let route = navRoute' + ident = navIdent + in $(widgetFile "widgets/pageaction/primary") + | otherwise + = error "not implemented" + sWidgets = navChildren' + & map (\(l, i, r) -> navWidget (NavPageActionSecondary l, i, Just r, [])) + in $(widgetFile "widgets/pageaction/primary-wrapper") + NavPageActionSecondary{ navLink = navLink@NavLink{..}, .. } + | NavTypeLink{..} <- navType + , navModal + -> customModal Modal + { modalTriggerId = Just navIdent + , modalId = Nothing + , modalTrigger = \(Just route) ident -> $(widgetFile "widgets/pageaction/secondary") + , modalContent = Left $ SomeRoute navLink + } + | NavTypeLink{} <- navType + -> let route = navRoute' + ident = navIdent + in $(widgetFile "widgets/pageaction/secondary") + NavHeaderContainer{..} -> $(widgetFile "widgets/navbar/container") + NavFooter{ navLink = navLink@NavLink{..} } + | NavTypeLink{..} <- navType + , not navModal + -> let route = navRoute' + ident = navIdent + in $(widgetFile "widgets/footer/link") + _other -> error "not implemented" - navbarItem (MenuItem{..}, menuIdent) = do - route <- toTextUrl menuItemRoute - $(widgetFile "widgets/navbar/item") + navContainerItemWidget :: (Nav, Text, Maybe Text, [(NavLink, Text, Text)]) + -> (NavLink, Text, Text) + -> Widget + navContainerItemWidget (n, _navIdent, _navRoute', _navChildren') (iN@NavLink{..}, iNavIdent, iNavRoute) = case n of + NavHeaderContainer{} + | NavTypeLink{..} <- navType + , navModal + -> customModal Modal + { modalTriggerId = Just iNavIdent + , modalId = Nothing + , modalTrigger = \(Just route) ident -> $(widgetFile "widgets/navbar/navbar-container-item--link") + , modalContent = Left $ SomeRoute iN + } + | NavTypeLink{} <- navType + -> let route = iNavRoute + ident = iNavIdent + in $(widgetFile "widgets/navbar/navbar-container-item--link") + | NavTypeButton{..} <- navType -> do + csrfToken <- reqToken <$> getRequest + wrapForm $(widgetFile "widgets/navbar/navbar-container-item--button") def + { formMethod = navMethod + , formSubmit = FormNoSubmit + , formAction = Just $ SomeRoute iN + } + _other -> error "not implemented" navbar :: Widget - navbar = $(widgetFile "widgets/navbar/navbar") + navbar = do + $(widgetFile "widgets/navbar/navbar") + forM_ (filter isNavHeaderContainer nav) $ \(_, containerIdent, _, _) -> + toWidget $(cassiusFile "templates/widgets/navbar/container-radio.cassius") + where isNavHeaderPrimary = has $ _1 . _navHeaderRole . only NavHeaderPrimary + isNavHeaderSecondary = has $ _1 . _navHeaderRole . only NavHeaderSecondary asidenav :: Widget asidenav = $(widgetFile "widgets/asidenav/asidenav") where logo = preEscapedToMarkup $ decodeUtf8 $(embedFile "assets/lmu/logo.svg") footer :: Widget footer = $(widgetFile "widgets/footer/footer") + where isNavFooter = has $ _1 . _NavFooter alerts :: Widget alerts = $(widgetFile "widgets/alerts/alerts") contentHeadline :: Maybe Widget @@ -1706,11 +1849,16 @@ siteLayout' headingOverride widget = do -- functions to determine if there are page-actions (primary or secondary) hasPageActions, hasSecondaryPageActions, hasPrimaryPageActions :: Bool hasPageActions = hasPrimaryPageActions || hasSecondaryPageActions - hasSecondaryPageActions = any (is _PageActionSecondary) $ toListOf (traverse . _1 . _menuItemType) menuTypes - hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes + hasSecondaryPageActions = has (folded . _1 . _NavPageActionSecondary) nav + hasPrimaryPageActions = has (folded . _1 . _NavPageActionPrimary ) nav + hasPrimarySubActions = has (folded . _1 . filtered (is _NavPageActionPrimary) . _navChildren . folded) nav contentRibbon :: Maybe Widget contentRibbon = fmap toWidget appRibbon + isNavHeaderContainer = has $ _1 . _NavHeaderContainer + isPageActionPrimary = has $ _1 . _NavPageActionPrimary + isPageActionSecondary = has $ _1 . _NavPageActionSecondary + MsgRenderer mr <- getMsgRenderer let -- See Utils.Frontend.I18n and files in messages/frontend for message definitions @@ -1769,12 +1917,12 @@ i18nCrumb msg mbR = do -- i.e. information might be leaked by not performing permission checks if the -- breadcrumb value depends on sensitive content (like an user's name). instance YesodBreadcrumbs UniWorX where - breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just HomeR + breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just NewsR breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing breadcrumb (WellKnownR _) = i18nCrumb MsgBreadcrumbWellKnown Nothing breadcrumb MetricsR = i18nCrumb MsgBreadcrumbMetrics Nothing - breadcrumb HomeR = i18nCrumb MsgMenuHome Nothing + breadcrumb NewsR = i18nCrumb MsgMenuNews Nothing breadcrumb UsersR = i18nCrumb MsgMenuUsers $ Just AdminR breadcrumb AdminUserAddR = i18nCrumb MsgMenuUserAdd $ Just UsersR breadcrumb (AdminUserR cID) = maybeT (i18nCrumb MsgBreadcrumbUser $ Just UsersR) $ do @@ -1839,7 +1987,7 @@ instance YesodBreadcrumbs UniWorX where breadcrumb StorageKeyR = i18nCrumb MsgBreadcrumbStorageKey Nothing - breadcrumb TermShowR = i18nCrumb MsgMenuTermShow $ Just HomeR + breadcrumb TermShowR = i18nCrumb MsgMenuTermShow $ Just NewsR breadcrumb TermCurrentR = i18nCrumb MsgMenuTermCurrent $ Just TermShowR breadcrumb TermEditR = i18nCrumb MsgMenuTermCreate $ Just TermShowR breadcrumb (TermEditExistR tid) = i18nCrumb MsgMenuTermEdit . Just $ TermCourseListR tid @@ -1853,7 +2001,7 @@ instance YesodBreadcrumbs UniWorX where <*> fmap isJust (get tid) return (CI.original $ unSchoolKey ssh, Just $ TermCourseListR tid) - breadcrumb AllocationListR = i18nCrumb MsgAllocationListTitle $ Just HomeR + breadcrumb AllocationListR = i18nCrumb MsgAllocationListTitle $ Just NewsR breadcrumb (AllocationR tid ssh ash AShowR) = maybeT (i18nCrumb MsgBreadcrumbAllocation $ Just AllocationListR) $ do mr <- getMessageRender Entity _ Allocation{allocationName} <- MaybeT . runDB . getBy $ TermSchoolAllocationShort tid ssh ash @@ -1940,9 +2088,9 @@ instance YesodBreadcrumbs UniWorX where ECInviteR -> i18nCrumb MsgBreadcrumbExamCorrectorInvite . Just $ CExamR tid ssh csh examn EShowR EInviteR -> i18nCrumb MsgBreadcrumbExamParticipantInvite . Just $ CExamR tid ssh csh examn EShowR ERegisterR -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR - ECorrectR -> i18nCrumb MsgBreadcrumbExamCorrect . Just $ CExamR tid ssh csh examn EShowR ERegisterOccR _occn -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR EAutoOccurrenceR -> i18nCrumb MsgBreadcrumbExamAutoOccurrence . Just $ CExamR tid ssh csh examn EUsersR + ECorrectR -> i18nCrumb MsgMenuExamCorrect . Just $ CExamR tid ssh csh examn EShowR breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do @@ -2008,7 +2156,7 @@ instance YesodBreadcrumbs UniWorX where mayList <- (== Authorized) <$> evalAccess MessageListR False if | mayList -> i18nCrumb MsgBreadcrumbSystemMessage $ Just MessageListR - | otherwise -> i18nCrumb MsgBreadcrumbSystemMessage $ Just HomeR + | otherwise -> i18nCrumb MsgBreadcrumbSystemMessage $ Just NewsR breadcrumb MessageListR = i18nCrumb MsgMenuMessageList $ Just AdminR breadcrumb GlossaryR = i18nCrumb MsgMenuGlossary $ Just InfoR @@ -2049,701 +2197,856 @@ submissionList tid csh shn uid = E.select . E.from $ \(course `E.InnerJoin` shee -defaultLinks :: (MonadHandler m, HandlerSite m ~ UniWorX) => m [MenuItem] +defaultLinks :: (MonadHandler m, HandlerSite m ~ UniWorX) => m [Nav] defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header. - [ return MenuItem - { menuItemType = NavbarAside - , menuItemLabel = MsgMenuHome - , menuItemIcon = Just "home" - , menuItemRoute = SomeRoute HomeR - , menuItemModal = False - , menuItemAccessCallback' = return True + [ return NavHeader + { navHeaderRole = NavHeaderSecondary + , navIcon = IconMenuLogout + , navLink = NavLink + { navLabel = MsgMenuLogout + , navRoute = AuthR LogoutR + , navAccess' = is _Just <$> maybeAuthId + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } } - , return MenuItem - { menuItemType = Footer - , menuItemLabel = MsgMenuDataProt - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ LegalR :#: ("data-protection" :: Text) - , menuItemModal = False - , menuItemAccessCallback' = return True + , return NavHeader + { navHeaderRole = NavHeaderSecondary + , navIcon = IconMenuLogin + , navLink = NavLink + { navLabel = MsgMenuLogin + , navRoute = AuthR LoginR + , navAccess' = is _Nothing <$> maybeAuthId + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False + } } - , return MenuItem - { menuItemType = Footer - , menuItemLabel = MsgMenuTermsUse - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ LegalR :#: ("terms-of-use" :: Text) - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , return MenuItem - { menuItemType = Footer - , menuItemLabel = MsgMenuCopyright - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ LegalR :#: ("copyright" :: Text) - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , return MenuItem - { menuItemType = Footer - , menuItemLabel = MsgMenuImprint - , menuItemIcon = Just "file-signature" - , menuItemRoute = SomeRoute $ LegalR :#: ("imprint" :: Text) - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , return MenuItem - { menuItemType = Footer - , menuItemLabel = MsgMenuInformation - , menuItemIcon = Just "info" - , menuItemRoute = SomeRoute InfoR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , return MenuItem - { menuItemType = Footer - , menuItemLabel = MsgMenuGlossary - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute GlossaryR - , menuItemModal = False - , menuItemAccessCallback' = return True + , return NavHeader + { navHeaderRole = NavHeaderSecondary + , navIcon = IconMenuProfile + , navLink = NavLink + { navLabel = MsgMenuProfile + , navRoute = ProfileR + , navAccess' = is _Just <$> maybeAuthId + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } } , do mCurrentRoute <- getCurrentRoute - return MenuItem - { menuItemType = NavbarRight - , menuItemLabel = MsgMenuHelp - , menuItemIcon = Just "question" - , menuItemRoute = SomeRoute (HelpR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mCurrentRoute]) - , menuItemModal = True - , menuItemAccessCallback' = return True + activeLang <- selectLanguage appLanguages + + let navChildren = flip map (toList appLanguages) $ \lang -> NavLink + { navLabel = MsgLanguage lang + , navRoute = (LangR, [(toPathPiece GetReferer, toPathPiece currentRoute) | let Just currentRoute = mCurrentRoute ]) + , navAccess' = return True + , navType = NavTypeButton + { navMethod = POST + , navData = [(toPathPiece PostLanguage, lang)] + } + , navQuick' = mempty + , navForceActive = lang == activeLang + } + + guard $ length navChildren > 1 + + return NavHeaderContainer + { navHeaderRole = NavHeaderSecondary + , navLabel = SomeMessage MsgMenuLanguage + , navIcon = IconLanguage + , navChildren } - , return MenuItem - { menuItemType = NavbarRight - , menuItemLabel = MsgMenuProfile - , menuItemIcon = Just "cogs" - , menuItemRoute = SomeRoute ProfileR - , menuItemModal = False - , menuItemAccessCallback' = isJust <$> maybeAuthPair + , do + mCurrentRoute <- getCurrentRoute + + return NavHeader + { navHeaderRole = NavHeaderSecondary + , navIcon = IconMenuHelp + , navLink = NavLink + { navLabel = MsgMenuHelp + , navRoute = (HelpR, [(toPathPiece GetReferer, toPathPiece currentRoute) | let Just currentRoute = mCurrentRoute ]) + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False + } + } + , return $ NavFooter NavLink + { navLabel = MsgMenuDataProt + , navRoute = LegalR :#: ("data-protection" :: Text) + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , return MenuItem - { menuItemType = NavbarSecondary - , menuItemLabel = MsgMenuLogin - , menuItemIcon = Just "sign-in-alt" - , menuItemRoute = SomeRoute $ AuthR LoginR - , menuItemModal = True - , menuItemAccessCallback' = isNothing <$> maybeAuthPair + , return $ NavFooter NavLink + { navLabel = MsgMenuTermsUse + , navRoute = LegalR :#: ("terms-of-use" :: Text) + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , return MenuItem - { menuItemType = NavbarSecondary - , menuItemLabel = MsgMenuLogout - , menuItemIcon = Just "sign-out-alt" - , menuItemRoute = SomeRoute $ AuthR LogoutR - , menuItemModal = False - , menuItemAccessCallback' = isJust <$> maybeAuthPair + , return $ NavFooter NavLink + { navLabel = MsgMenuCopyright + , navRoute = LegalR :#: ("copyright" :: Text) + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , return MenuItem - { menuItemType = NavbarAside - , menuItemLabel = MsgMenuTermShow - , menuItemIcon = Just "calendar-alt" -- SJ wrote: calendar icon, since Term will be repleaced with TimeTable in the future; arguably Term is more calendar-like than courses anyway!!! - , menuItemRoute = SomeRoute TermShowR - , menuItemModal = False - , menuItemAccessCallback' = return True + , return $ NavFooter NavLink + { navLabel = MsgMenuImprint + , navRoute = LegalR :#: ("imprint" :: Text) + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , return MenuItem - { menuItemType = NavbarAside - , menuItemLabel = MsgMenuCourseList - , menuItemIcon = Just "graduation-cap" - , menuItemRoute = SomeRoute CourseListR - , menuItemModal = False - , menuItemAccessCallback' = return True + , return $ NavFooter NavLink + { navLabel = MsgMenuInformation + , navRoute = InfoR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , return MenuItem - { menuItemType = NavbarAside - , menuItemLabel = MsgMenuCorrections - , menuItemIcon = Just "check" - , menuItemRoute = SomeRoute CorrectionsR - , menuItemModal = False - , menuItemAccessCallback' = return True + , return $ NavFooter NavLink + { navLabel = MsgMenuGlossary + , navRoute = GlossaryR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , return MenuItem - { menuItemType = NavbarAside - , menuItemLabel = MsgMenuExamOfficeExams - , menuItemIcon = Just "poll-h" - , menuItemRoute = SomeRoute $ ExamOfficeR EOExamsR - , menuItemModal = False - , menuItemAccessCallback' = return True + , return NavHeader + { navHeaderRole = NavHeaderPrimary + , navIcon = IconMenuNews + , navLink = NavLink + { navLabel = MsgMenuNews + , navRoute = NewsR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } } - , return MenuItem - { menuItemType = NavbarAside - , menuItemLabel = MsgMenuUsers - , menuItemIcon = Just "users" - , menuItemRoute = SomeRoute UsersR - , menuItemModal = False - , menuItemAccessCallback' = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False + , return NavHeader + { navHeaderRole = NavHeaderPrimary + , navIcon = IconMenuCourseList + , navLink = NavLink + { navLabel = MsgMenuCourseList + , navRoute = CourseListR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } } - , return MenuItem - { menuItemType = NavbarAside - , menuItemLabel = MsgAdminHeading - , menuItemIcon = Just "screwdriver" - , menuItemRoute = SomeRoute AdminR - , menuItemModal = False - , menuItemAccessCallback' = return True + , return NavHeader + { navHeaderRole = NavHeaderPrimary + , navIcon = IconMenuCorrections + , navLink = NavLink + { navLabel = MsgMenuCorrections + , navRoute = CorrectionsR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + } + , return NavHeader + { navHeaderRole = NavHeaderPrimary + , navIcon = IconMenuExams + , navLink = NavLink + { navLabel = MsgMenuExamOfficeExams + , navRoute = ExamOfficeR EOExamsR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + } + , return NavHeaderContainer + { navHeaderRole = NavHeaderPrimary + , navLabel = SomeMessage MsgAdminHeading + , navIcon = IconMenuAdmin + , navChildren = + [ NavLink + { navLabel = MsgMenuUsers + , navRoute = UsersR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , NavLink + { navLabel = MsgMenuSchoolList + , navRoute = SchoolListR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , NavLink + { navLabel = MsgAdminFeaturesHeading + , navRoute = AdminFeaturesR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , NavLink + { navLabel = MsgMenuMessageList + , navRoute = MessageListR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , NavLink + { navLabel = MsgMenuAdminErrMsg + , navRoute = AdminErrMsgR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , NavLink + { navLabel = MsgMenuAdminTest + , navRoute = AdminTestR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + ] + } + , return NavHeaderContainer + { navHeaderRole = NavHeaderPrimary + , navLabel = SomeMessage (mempty :: Text) + , navIcon = IconMenuExtra + , navChildren = + [ NavLink + { navLabel = MsgMenuCourseNew + , navRoute = CourseNewR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , NavLink + { navLabel = MsgMenuExternalExamList + , navRoute = EExamListR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , NavLink + { navLabel = MsgMenuTermShow + , navRoute = TermShowR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , NavLink + { navLabel = MsgMenuAllocationList + , navRoute = AllocationListR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , NavLink + { navLabel = MsgInfoLecturerTitle + , navRoute = InfoLecturerR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + ] } ] -pageActions :: Route UniWorX -> [MenuItem] -{- - Icons: https://fontawesome.com/icons?d=gallery - Guideline: use icons without boxes/frames, only non-pro +pageActions :: ( MonadHandler m + , HandlerSite m ~ UniWorX + , MonadCatch m + ) + => Route UniWorX -> m [Nav] +pageActions NewsR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuOpenCourses + , navRoute = (CourseListR, [("courses-openregistration", toPathPiece True)]) + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuOpenAllocations + , navRoute = (AllocationListR, [("allocations-active", toPathPiece True)]) + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + ] +pageActions (CourseR tid ssh csh CShowR) = do + materialListSecondary <- pageQuickActions NavQuickViewPageActionSecondary $ CourseR tid ssh csh MaterialListR + tutorialListSecondary <- pageQuickActions NavQuickViewPageActionSecondary $ CourseR tid ssh csh CTutorialListR + sheetListSecondary <- pageQuickActions NavQuickViewPageActionSecondary $ CourseR tid ssh csh SheetListR + examListSecondary <- pageQuickActions NavQuickViewPageActionSecondary $ CourseR tid ssh csh CExamListR + membersSecondary <- pageQuickActions NavQuickViewPageActionSecondary $ CourseR tid ssh csh CUsersR - Please keep sorted according to routes --} -pageActions (HomeR) = - [ - MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgInfoLecturerTitle - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute InfoLecturerR - , menuItemModal = False - , menuItemAccessCallback' = return True + let examListBound :: Num a => a + examListBound = 4 -- guaranteed random; chosen by fair dice roll + examListExams <- liftHandler . runDB $ do + examNames <- E.select . E.from $ \(course `E.InnerJoin` exam) -> do + E.on $ exam E.^. ExamCourse E.==. course E.^. CourseId + E.where_ $ course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + E.limit $ succ examListBound + return $ exam E.^. ExamName + return $ do + E.Value examn <- examNames + return NavLink + { navLabel = examn + , navRoute = CExamR tid ssh csh examn EShowR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewFavourite + , navForceActive = False + } + let showExamList = length examListExams <= examListBound + + let + navMembers = NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCourseMembers + , navRoute = CourseR tid ssh csh CUsersR + , navAccess' = + let courseWhere course = course <$ do + E.where_ $ course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + hasParticipants = E.selectExists . E.from $ \(course `E.InnerJoin` courseParticipant) -> do + E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse + void $ courseWhere course + mayRegister = hasWriteAccessTo $ CourseR tid ssh csh CAddUserR + in runDB $ mayRegister `or2M` hasParticipants + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewFavourite + , navForceActive = False + } + , navChildren = membersSecondary } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCourseNew - , menuItemIcon = Just "book" - , menuItemRoute = SomeRoute CourseNewR - , menuItemModal = False - , menuItemAccessCallback' = return True + showMembers <- maybeT (return False) $ True <$ navAccess navMembers + + return $ + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuMaterialList + , navRoute = CourseR tid ssh csh MaterialListR + , navAccess' = + let lecturerAccess = hasWriteAccessTo $ CourseR tid ssh csh MaterialNewR -- Always show for lecturers to create new material + materialAccess mnm = hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR -- otherwise show only if the user can see at least one of the contents + existsVisible = do + matNames <- E.select . E.from $ \(course `E.InnerJoin` material) -> do + E.on $ course E.^. CourseId E.==. material E.^. MaterialCourse + E.where_ $ course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + return $ material E.^. MaterialName + anyM matNames (materialAccess . E.unValue) + in runDB $ lecturerAccess `or2M` existsVisible + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewFavourite + , navForceActive = False + } + , navChildren = materialListSecondary } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuExternalExamList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute EExamListR - , menuItemModal = False - , menuItemAccessCallback' = return True + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSheetList + , navRoute = CourseR tid ssh csh SheetListR + , navAccess' = + let lecturerAccess = hasWriteAccessTo $ CourseR tid ssh csh SheetNewR -- Always show for lecturers to create new sheets + sheetAccess shn = hasReadAccessTo $ CSheetR tid ssh csh shn SShowR -- othwerwise show only if the user can see at least one of the contents + existsVisible = do + sheetNames <- E.select . E.from $ \(course `E.InnerJoin` sheet) -> do + E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse + E.where_ $ course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + return $ sheet E.^. SheetName + anyM sheetNames $ sheetAccess . E.unValue + in runDB $ lecturerAccess `or2M` existsVisible + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewFavourite + , navForceActive = False + } + , navChildren = sheetListSecondary } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuOpenCourses - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute (CourseListR, [("courses-openregistration", "True")]) - , menuItemModal = False - , menuItemAccessCallback' = return True + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuTutorialList + , navRoute = CourseR tid ssh csh CTutorialListR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewFavourite + , navForceActive = False + } + , navChildren = tutorialListSecondary } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuOpenAllocations - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute (AllocationListR, [("allocations-active", "True")]) - , menuItemModal = False - , menuItemAccessCallback' = return True + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamList + , navRoute = CourseR tid ssh csh CExamListR + , navAccess' = + let lecturerAccess = hasWriteAccessTo $ CourseR tid ssh csh CExamNewR + examAccess examn = hasReadAccessTo $ CExamR tid ssh csh examn EShowR + existsVisible = do + examNames <- E.select . E.from $ \(course `E.InnerJoin` exam) -> do + E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse + E.where_ $ course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + return $ exam E.^. ExamName + anyM examNames $ examAccess . E.unValue + in runDB $ lecturerAccess `or2M` existsVisible + , navType = NavTypeLink { navModal = False } + , navQuick' = bool (navQuick NavQuickViewFavourite) mempty showExamList + , navForceActive = False + } + , navChildren = examListSecondary ++ guardOnM showExamList examListExams } + , navMembers + ] ++ guardOnM (not showMembers) [ NavPageActionPrimary{ navLink, navChildren = [] } | navLink <- membersSecondary ] ++ + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCourseCommunication + , navRoute = CourseR tid ssh csh CCommR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewFavourite + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuCourseExamOffice + , navRoute = CourseR tid ssh csh CExamOfficeR + , navAccess' = do + uid <- requireAuthId + runDB $ do + cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh + E.selectExists $ do + (_school, isForced) <- courseExamOfficeSchools (E.val uid) (E.val cid) + E.where_ $ E.not_ isForced + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False + } + } + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuCourseEdit + , navRoute = CourseR tid ssh csh CEditR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + } + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuCourseClone + , navRoute = ( CourseNewR + , [("tid", toPathPiece tid), ("ssh", toPathPiece ssh), ("csh", toPathPiece csh)] + ) + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + } + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuCourseDelete + , navRoute = CourseR tid ssh csh CDeleteR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + } + ] +pageActions (ExamOfficeR EOExamsR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamOfficeFields + , navRoute = ExamOfficeR EOFieldsR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamOfficeUsers + , navRoute = ExamOfficeR EOUsersR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } ] -pageActions (AdminR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSchoolList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute SchoolListR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgAdminFeaturesHeading - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute AdminFeaturesR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuMessageList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute MessageListR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuAdminErrMsg - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute AdminErrMsgR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuAdminTest - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute AdminTestR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions SchoolListR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSchoolNew + , navRoute = SchoolNewR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } ] -pageActions (ExamOfficeR EOExamsR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamOfficeFields - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ ExamOfficeR EOFieldsR - , menuItemModal = True - , menuItemAccessCallback' = return True +pageActions UsersR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuLecturerInvite + , navRoute = AdminNewFunctionaryInviteR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamOfficeUsers - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ ExamOfficeR EOUsersR - , menuItemModal = True - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuUserAdd + , navRoute = AdminUserAddR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } ] -pageActions (SchoolListR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSchoolNew - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute SchoolNewR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (AdminUserR cID) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuUserNotifications + , navRoute = UserNotificationR cID + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False } - ] -pageActions (UsersR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuLecturerInvite - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute AdminNewFunctionaryInviteR - , menuItemModal = True - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuUserAdd - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute AdminUserAddR - , menuItemModal = True - , menuItemAccessCallback' = return True - } - ] -pageActions (AdminUserR cID) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuUserNotifications - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ UserNotificationR cID - , menuItemModal = True - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuUserPassword - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ UserPasswordR cID - , menuItemModal = True - , menuItemAccessCallback' = do + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuUserPassword + , navRoute = UserPasswordR cID + , navAccess' = do uid <- decrypt cID User{userAuthentication} <- runDB $ get404 uid return $ is _AuthPWHash userAuthentication + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } ] -pageActions (InfoR) = [ - MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgInfoLecturerTitle - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute InfoLecturerR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions InfoR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgInfoLecturerTitle + , navRoute = InfoLecturerR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuLegal - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute LegalR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuLegal + , navRoute = LegalR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuGlossary - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute GlossaryR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuGlossary + , navRoute = GlossaryR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } ] -pageActions (VersionR) = [ - MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgInfoLecturerTitle - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute InfoLecturerR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions VersionR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgInfoLecturerTitle + , navRoute = InfoLecturerR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuLegal + , navRoute = LegalR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuGlossary + , navRoute = GlossaryR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } ] -pageActions HealthR = [ - MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuInstance - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute InstanceR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions HealthR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuInstance + , navRoute = InstanceR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } ] -pageActions InstanceR = [ - MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuHealth - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute HealthR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions InstanceR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuHealth + , navRoute = HealthR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } ] -pageActions (HelpR) = [ - -- MenuItem - -- { menuItemType = PageActionPrime - -- , menuItemLabel = MsgInfoLecturerTitle - -- , menuItemIcon = Nothing - -- , menuItemRoute = SomeRoute InfoLecturerR - -- , menuItemModal = False - -- , menuItemAccessCallback' = return True - -- } +pageActions HelpR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgInfoLecturerTitle + , navRoute = InfoLecturerR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuGlossary + , navRoute = GlossaryR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } ] -pageActions (ProfileR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuProfileData - , menuItemIcon = Just "book" - , menuItemRoute = SomeRoute ProfileDataR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions ProfileR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuProfileData + , navRoute = ProfileDataR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuAuthPreds - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute AuthPredsR - , menuItemModal = True - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuAuthPreds + , navRoute = AuthPredsR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgCsvOptions + , navRoute = CsvOptionsR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } ] -pageActions TermShowR = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuTermCreate - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute TermEditR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions TermShowR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuTermCreate + , navRoute = TermEditR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuParticipantsList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute ParticipantsListR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuParticipantsList + , navRoute = ParticipantsListR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } ] -pageActions (TermCourseListR tid) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCourseNew - , menuItemIcon = Just "book" - , menuItemRoute = SomeRoute CourseNewR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (AllocationR _tid _ssh _ash AShowR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuAllocationInfo + , navRoute = InfoAllocationR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuTermEdit - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ TermEditExistR tid - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + ] +pageActions CourseListR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCourseNew + , navRoute = CourseNewR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuAllocationList + , navRoute = AllocationListR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuParticipantsList + , navRoute = ParticipantsListR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + ] +pageActions CourseNewR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgInfoLecturerTitle + , navRoute = InfoLecturerR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } ] -pageActions (TermSchoolCourseListR _tid _ssh) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCourseNew - , menuItemIcon = Just "book" - , menuItemRoute = SomeRoute CourseNewR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CourseR tid ssh csh CCorrectionsR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrectionsAssign + , navRoute = CourseR tid ssh csh CAssignR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary <> navQuick NavQuickViewFavourite + , navForceActive = False } - ] -pageActions (AllocationR _tid _ssh _ash AShowR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuAllocationInfo - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute InfoAllocationR - , menuItemModal = True - , menuItemAccessCallback' = return True - } - ] -pageActions (CourseListR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCourseNew - , menuItemIcon = Just "book" - , menuItemRoute = SomeRoute CourseNewR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuAllocationList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute AllocationListR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuParticipantsList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute ParticipantsListR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - ] -pageActions (CourseNewR) = [ - MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgInfoLecturerTitle - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute InfoLecturerR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - ] -pageActions (CourseR tid ssh csh CShowR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuMaterialList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh MaterialListR - , menuItemModal = False - , menuItemAccessCallback' = - let lecturerAccess = hasWriteAccessTo $ CourseR tid ssh csh MaterialNewR -- Always show for lecturers to create new material - materialAccess mnm = hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR -- otherwise show only if the user can see at least one of the contents - existsVisible = do - matNames <- E.select . E.from $ \(course `E.InnerJoin` material) -> do - E.on $ course E.^. CourseId E.==. material E.^. MaterialCourse - E.where_ $ course E.^. CourseTerm E.==. E.val tid - E.&&. course E.^. CourseSchool E.==. E.val ssh - E.&&. course E.^. CourseShorthand E.==. E.val csh - return $ material E.^. MaterialName - anyM matNames (materialAccess . E.unValue) - in runDB $ lecturerAccess `or2M` existsVisible - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSheetList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh SheetListR - , menuItemModal = False - , menuItemAccessCallback' = - let lecturerAccess = hasWriteAccessTo $ CourseR tid ssh csh SheetNewR -- Always show for lecturers to create new sheets - sheetAccess shn = hasReadAccessTo $ CSheetR tid ssh csh shn SShowR -- othwerwise show only if the user can see at least one of the contents - existsVisible = do - sheetNames <- E.select . E.from $ \(course `E.InnerJoin` sheet) -> do - E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse - E.where_ $ course E.^. CourseTerm E.==. E.val tid - E.&&. course E.^. CourseSchool E.==. E.val ssh - E.&&. course E.^. CourseShorthand E.==. E.val csh - return $ sheet E.^. SheetName - anyM sheetNames $ sheetAccess . E.unValue - in runDB $ lecturerAccess `or2M` existsVisible - } - ] ++ pageActions (CourseR tid ssh csh SheetListR) ++ - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuTutorialList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CTutorialListR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamList - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CExamListR - , menuItemModal = False - , menuItemAccessCallback' = - let lecturerAccess = hasWriteAccessTo $ CourseR tid ssh csh CExamNewR - examAccess examn = hasReadAccessTo $ CExamR tid ssh csh examn EShowR - existsVisible = do - examNames <- E.select . E.from $ \(course `E.InnerJoin` exam) -> do - E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse - E.where_ $ course E.^. CourseTerm E.==. E.val tid - E.&&. course E.^. CourseSchool E.==. E.val ssh - E.&&. course E.^. CourseShorthand E.==. E.val csh - return $ exam E.^. ExamName - anyM examNames $ examAccess . E.unValue - in runDB $ lecturerAccess `or2M` existsVisible - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuCourseApplications - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CApplicationsR - , menuItemModal = False - , menuItemAccessCallback' = - let courseWhere course = course <$ do - E.where_ $ course E.^. CourseTerm E.==. E.val tid - E.&&. course E.^. CourseSchool E.==. E.val ssh - E.&&. course E.^. CourseShorthand E.==. E.val csh - existsApplications = E.selectExists . E.from $ \(course `E.InnerJoin` courseApplication) -> do - E.on $ course E.^. CourseId E.==. courseApplication E.^. CourseApplicationCourse - void $ courseWhere course - courseApplications = fmap (any E.unValue) . E.select . E.from $ \course -> do - void $ courseWhere course - return $ course E.^. CourseApplicationsRequired - courseAllocation = E.selectExists . E.from $ \(course `E.InnerJoin` allocationCourse) -> do - E.on $ course E.^. CourseId E.==. allocationCourse E.^. AllocationCourseCourse - void $ courseWhere course - in runDB $ courseAllocation `or2M` courseApplications `or2M` existsApplications - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuCourseMembers - , menuItemIcon = Just "user-graduate" - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CUsersR - , menuItemModal = False - , menuItemAccessCallback' = do - now <- liftIO getCurrentTime - let courseWhere course = course <$ do - E.where_ $ course E.^. CourseTerm E.==. E.val tid - E.&&. course E.^. CourseSchool E.==. E.val ssh - E.&&. course E.^. CourseShorthand E.==. E.val csh - hasActiveAllocation = E.selectExists . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` allocation) -> do - E.on $ allocation E.^. AllocationId E.==. allocationCourse E.^. AllocationCourseAllocation - E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId - void $ courseWhere course - E.where_ $ E.maybe E.false (E.<=. E.val now) (allocation E.^. AllocationRegisterByStaffFrom) - E.||. E.maybe E.false (E.<=. E.val now) (allocation E.^. AllocationRegisterByCourse) - hasParticipants = E.selectExists . E.from $ \(course `E.InnerJoin` courseParticipant) -> do - E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse - void $ courseWhere course - runDB $ (not <$> hasActiveAllocation) `or2M` hasParticipants - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuCourseCommunication - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CCommR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuCourseEdit - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CEditR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuCourseClone - , menuItemIcon = Just "copy" - , menuItemRoute = SomeRoute (CourseNewR, [("tid", toPathPiece tid), ("ssh", toPathPiece ssh), ("csh", toPathPiece csh)]) - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuCourseDelete - , menuItemIcon = Just "trash" - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CDeleteR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuCourseExamOffice - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CExamOfficeR - , menuItemModal = True - , menuItemAccessCallback' = do - uid <- requireAuthId - runDB $ do - cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh - E.selectExists $ do - (_school, isForced) <- courseExamOfficeSchools (E.val uid) (E.val cid) - E.where_ $ E.not_ isForced - } - ] -pageActions (CourseR tid ssh csh CCorrectionsR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsAssign - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CAssignR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - ] -pageActions (CourseR tid ssh csh SheetListR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSheetCurrent - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh SheetCurrentR - , menuItemModal = False - , menuItemAccessCallback' = runDB . maybeT (return False) $ do - void . MaybeT $ sheetCurrent tid ssh csh - return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSheetOldUnassigned - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh SheetOldUnassignedR - , menuItemModal = False - , menuItemAccessCallback' = runDB . maybeT (return False) $ do - void . MaybeT $ sheetOldUnassigned tid ssh csh - return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSubmissions - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CCorrectionsR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsAssign - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CAssignR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsOwn - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid) - , ("corrections-school", CI.original $ unSchoolKey ssh) - , ("corrections-course", CI.original csh) - ]) - , menuItemModal = False - , menuItemAccessCallback' = do + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrectionsOwn + , navRoute = ( CorrectionsR + , [ ("corrections-term", toPathPiece tid) + , ("corrections-school", toPathPiece ssh) + , ("corrections-course", toPathPiece csh) + ] + ) + , navAccess' = do muid <- maybeAuthId case muid of Nothing -> return False @@ -2756,32 +3059,90 @@ pageActions (CourseR tid ssh csh SheetListR) = E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseShorthand E.==. E.val csh return ok + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSheetNew - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh SheetNewR - , menuItemModal = False - , menuItemAccessCallback' = return True - } + , navChildren = [] + } ] -pageActions (CourseR tid ssh csh CUsersR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCourseAddMembers - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CAddUserR - , menuItemModal = True - , menuItemAccessCallback' = return True +pageActions (CourseR tid ssh csh SheetListR) = do + correctionsSecondary <- pageQuickActions NavQuickViewPageActionSecondary $ CourseR tid ssh csh CCorrectionsR + + let + navCorrections = NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSubmissions + , navRoute = CourseR tid ssh csh CCorrectionsR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary <> navQuick NavQuickViewFavourite + , navForceActive = False + } + , navChildren = correctionsSecondary } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCourseApplications - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CApplicationsR - , menuItemModal = False - , menuItemAccessCallback' = + showCorrections <- maybeT (return False) $ True <$ navAccess navCorrections + + return $ + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSheetCurrent + , navRoute = CourseR tid ssh csh SheetCurrentR + , navAccess' = + runDB . maybeT (return False) $ do + void . MaybeT $ sheetCurrent tid ssh csh + return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary <> navQuick NavQuickViewFavourite + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSheetOldUnassigned + , navRoute = CourseR tid ssh csh SheetOldUnassignedR + , navAccess' = + runDB . maybeT (return False) $ do + void . MaybeT $ sheetOldUnassigned tid ssh csh + return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary <> navQuick NavQuickViewFavourite + , navForceActive = False + } + , navChildren = [] + } + , navCorrections + ] ++ guardOnM (not showCorrections) [ NavPageActionPrimary{ navLink, navChildren = [] } | navLink <- correctionsSecondary ] ++ + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSheetNew + , navRoute = CourseR tid ssh csh SheetNewR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary <> navQuick NavQuickViewFavourite + , navForceActive = False + } + , navChildren = [] + } + ] +pageActions (CourseR tid ssh csh CUsersR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCourseAddMembers + , navRoute = CourseR tid ssh csh CAddUserR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCourseApplications + , navRoute = CourseR tid ssh csh CApplicationsR + , navAccess' = let courseWhere course = course <$ do E.where_ $ course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseSchool E.==. E.val ssh @@ -2796,295 +3157,442 @@ pageActions (CourseR tid ssh csh CUsersR) = E.on $ course E.^. CourseId E.==. allocationCourse E.^. AllocationCourseCourse void $ courseWhere course in runDB $ courseAllocation `or2M` courseApplications `or2M` existsApplications + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary <> navQuick NavQuickViewFavourite + , navForceActive = False } + , navChildren = [] + } ] -pageActions (CourseR tid ssh csh MaterialListR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuMaterialNew - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh MaterialNewR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CourseR tid ssh csh MaterialListR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuMaterialNew + , navRoute = CourseR tid ssh csh MaterialNewR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } + , navChildren = [] + } ] -pageActions (CMaterialR tid ssh csh mnm MShowR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuMaterialEdit - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CMaterialR tid ssh csh mnm MEditR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CMaterialR tid ssh csh mnm MShowR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuMaterialEdit + , navRoute = CMaterialR tid ssh csh mnm MEditR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuMaterialDelete - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CMaterialR tid ssh csh mnm MDelR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuMaterialDelete + , navRoute = CMaterialR tid ssh csh mnm MDelR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False } + } ] -pageActions (CourseR tid ssh csh CTutorialListR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuTutorialNew - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CTutorialNewR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CourseR tid ssh csh CTutorialListR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuTutorialNew + , navRoute = CourseR tid ssh csh CTutorialNewR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } + , navChildren = [] + } ] -pageActions (CTutorialR tid ssh csh tutn TEditR) = - [ MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuTutorialDelete - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CTutorialR tid ssh csh tutn TDeleteR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CTutorialR tid ssh csh tutn TEditR) = return + [ NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuTutorialDelete + , navRoute = CTutorialR tid ssh csh tutn TDeleteR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + } ] -pageActions (CTutorialR tid ssh csh tutn TUsersR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuTutorialComm - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CTutorialR tid ssh csh tutn TCommR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CTutorialR tid ssh csh tutn TUsersR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuTutorialComm + , navRoute = CTutorialR tid ssh csh tutn TCommR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuTutorialEdit - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CTutorialR tid ssh csh tutn TEditR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuTutorialEdit + , navRoute = CTutorialR tid ssh csh tutn TEditR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuTutorialDelete - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CTutorialR tid ssh csh tutn TDeleteR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuTutorialDelete + , navRoute = CTutorialR tid ssh csh tutn TDeleteR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + } ] -pageActions (CourseR tid ssh csh CExamListR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamNew - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CExamNewR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CourseR tid ssh csh CExamListR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamNew + , navRoute = CourseR tid ssh csh CExamNewR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } + , navChildren = [] + } ] -pageActions (CExamR tid ssh csh examn EShowR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamEdit - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CExamR tid ssh csh examn EEditR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CExamR tid ssh csh examn EShowR) = do + usersSecondary <- pageQuickActions NavQuickViewPageActionSecondary $ CExamR tid ssh csh examn EUsersR + + return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamEdit + , navRoute = CExamR tid ssh csh examn EEditR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamUsers - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CExamR tid ssh csh examn EUsersR - , menuItemModal = False - , menuItemAccessCallback' = return True + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamUsers + , navRoute = CExamR tid ssh csh examn EUsersR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = usersSecondary } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamGrades - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CExamR tid ssh csh examn EGradesR - , menuItemModal = False - , menuItemAccessCallback' = return True + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamGrades + , navRoute = CExamR tid ssh csh examn EGradesR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamCorrect - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CExamR tid ssh csh examn ECorrectR - , menuItemModal = False - , menuItemAccessCallback' = return True + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamCorrect + , navRoute = CExamR tid ssh csh examn ECorrectR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } + ] +pageActions (CExamR tid ssh csh examn ECorrectR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamUsers + , navRoute = CExamR tid ssh csh examn EUsersR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamGrades + , navRoute = CExamR tid ssh csh examn EGradesR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuExamEdit + , navRoute = CExamR tid ssh csh examn EEditR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + } ] -pageActions (CExamR tid ssh csh examn EUsersR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamAddMembers - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CExamR tid ssh csh examn EAddUserR - , menuItemModal = True - , menuItemAccessCallback' = return True +pageActions (CExamR tid ssh csh examn EUsersR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamAddMembers + , navRoute = CExamR tid ssh csh examn EAddUserR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamGrades - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CExamR tid ssh csh examn EGradesR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamGrades + , navRoute = CExamR tid ssh csh examn EGradesR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamCorrect + , navRoute = CExamR tid ssh csh examn ECorrectR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } ] -pageActions (CExamR tid ssh csh examn EGradesR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExamUsers - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CExamR tid ssh csh examn EUsersR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CExamR tid ssh csh examn EGradesR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamUsers + , navRoute = CExamR tid ssh csh examn EUsersR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExamCorrect + , navRoute = CExamR tid ssh csh examn ECorrectR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } ] -pageActions (CSheetR tid ssh csh shn SShowR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSubmissionNew - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SubmissionNewR - , menuItemModal = True - , menuItemAccessCallback' = runDB . maybeT (return False) $ do - uid <- MaybeT $ liftHandler maybeAuthId - submissions <- lift $ submissionList tid csh shn uid - guard $ null submissions - return True +pageActions (CSheetR tid ssh csh shn SShowR) = do + subsSecondary <- pageQuickActions NavQuickViewPageActionSecondary $ CSheetR tid ssh csh shn SSubsR + let + navSubmissions = NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSubmissions + , navRoute = CSheetR tid ssh csh shn SSubsR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = subsSecondary } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSubmissionOwn - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SubmissionOwnR - , menuItemModal = False - , menuItemAccessCallback' = runDB . maybeT (return False) $ do - uid <- MaybeT $ liftHandler maybeAuthId - submissions <- lift $ submissionList tid csh shn uid - guard . not $ null submissions - return True + showSubmissions <- maybeT (return False) $ True <$ navAccess navSubmissions + + return $ + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSubmissionOwn + , navRoute = CSheetR tid ssh csh shn SubmissionOwnR + , navAccess' = + runDB . maybeT (return False) $ do + uid <- MaybeT $ liftHandler maybeAuthId + submissions <- lift $ submissionList tid csh shn uid + guard . not $ null submissions + return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsOwn - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid) - , ("corrections-school", CI.original $ unSchoolKey ssh) - , ("corrections-course", CI.original csh) - , ("corrections-sheet" , CI.original shn) - ]) - , menuItemModal = False - , menuItemAccessCallback' = (== Authorized) <$> evalAccessCorrector tid ssh csh + , navSubmissions + ] ++ guardOnM (not showSubmissions) [ NavPageActionPrimary{ navLink, navChildren = [] } | navLink <- subsSecondary ] ++ + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSheetEdit + , navRoute = CSheetR tid ssh csh shn SEditR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSubmissions - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SSubsR - , menuItemModal = False - , menuItemAccessCallback' = return True + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuSheetClone + , navRoute = (CourseR tid ssh csh SheetNewR, [("shn", toPathPiece shn)]) + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsAssign - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SAssignR - , menuItemModal = False - , menuItemAccessCallback' = return True + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuSheetDelete + , navRoute = CSheetR tid ssh csh shn SDelR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSheetEdit - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SEditR - , menuItemModal = False - , menuItemAccessCallback' = return True + ] +pageActions (CSheetR tid ssh csh shn SSubsR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuSubmissionNew + , navRoute = CSheetR tid ssh csh shn SubmissionNewR + , navAccess' = + let submissionAccess = hasWriteAccessTo $ CSheetR tid ssh csh shn SSubsR + hasNoSubmission = maybeT (return False) $ do + uid <- MaybeT $ liftHandler maybeAuthId + submissions <- lift $ submissionList tid csh shn uid + guard $ null submissions + return True + in runDB $ hasNoSubmission `or2M` submissionAccess + , navType = NavTypeLink { navModal = True } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuSheetClone - , menuItemIcon = Just "copy" - , menuItemRoute = SomeRoute (CourseR tid ssh csh SheetNewR, [("shn", toPathPiece shn)]) - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrectionsOwn + , navRoute = ( CorrectionsR + , [ ("corrections-term", toPathPiece tid) + , ("corrections-school", toPathPiece ssh) + , ("corrections-course", toPathPiece csh) + , ("corrections-sheet", toPathPiece shn) + ] + ) + , navAccess' = (== Authorized) <$> evalAccessCorrector tid ssh csh + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuSheetDelete - , menuItemIcon = Just "trash" - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SDelR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrectionsAssign + , navRoute = CSheetR tid ssh csh shn SAssignR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } + , navChildren = [] + } ] -pageActions (CSheetR tid ssh csh shn SSubsR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSubmissionNew - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SubmissionNewR - , menuItemModal = True - , menuItemAccessCallback' = return True +pageActions (CSubmissionR tid ssh csh shn cid SubShowR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrection + , navRoute = CSubmissionR tid ssh csh shn cid CorrectionR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsAssign - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SAssignR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgCorrectorAssignTitle + , navRoute = CSubmissionR tid ssh csh shn cid SubAssignR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuSubmissionDelete + , navRoute = CSubmissionR tid ssh csh shn cid SubDelR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + } ] -pageActions (CSubmissionR tid ssh csh shn cid SubShowR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrection - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSubmissionR tid ssh csh shn cid CorrectionR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (CSubmissionR tid ssh csh shn cid CorrectionR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgCorrectorAssignTitle + , navRoute = CSubmissionR tid ssh csh shn cid SubAssignR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgCorrectorAssignTitle - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSubmissionR tid ssh csh shn cid SubAssignR - , menuItemModal = True - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuSubmissionDelete - , menuItemIcon = Just "trash" - , menuItemRoute = SomeRoute $ CSubmissionR tid ssh csh shn cid SubDelR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionSecondary + { navLink = NavLink + { navLabel = MsgMenuSubmissionDelete + , navRoute = CSubmissionR tid ssh csh shn cid SubDelR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + } ] -pageActions (CSubmissionR tid ssh csh shn cid CorrectionR) = - [ MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuSubmissionDelete - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSubmissionR tid ssh csh shn cid SubDelR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - ] -pageActions (CourseR tid ssh csh CApplicationsR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCourseApplicationsFiles - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CAppsFilesR - , menuItemModal = False - , menuItemAccessCallback' - = let appAccess (E.Value appId) = do +pageActions (CourseR tid ssh csh CApplicationsR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCourseApplicationsFiles + , navRoute = CourseR tid ssh csh CAppsFilesR + , navAccess' = + let appAccess (E.Value appId) = do cID <- encrypt appId hasReadAccessTo $ CApplicationR tid ssh csh cID CAFilesR appSource = E.selectSource . E.from $ \(course `E.InnerJoin` courseApplication) -> do @@ -3096,42 +3604,55 @@ pageActions (CourseR tid ssh csh CApplicationsR) = E.where_ $ courseApplicationFile E.^. CourseApplicationFileApplication E.==. courseApplication E.^. CourseApplicationId return $ courseApplication E.^. CourseApplicationId in runDB . runConduit $ appSource .| anyMC appAccess + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCourseMembers - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CourseR tid ssh csh CUsersR - , menuItemModal = False - , menuItemAccessCallback' = runDB $ do - cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh - exists [ CourseParticipantCourse ==. cid ] + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCourseMembers + , navRoute = CourseR tid ssh csh CUsersR + , navAccess' = + runDB $ do + cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh + exists [ CourseParticipantCourse ==. cid ] + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False } + , navChildren = [] + } ] -pageActions (CorrectionsR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsDownload - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute CorrectionsDownloadR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions CorrectionsR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrectionsDownload + , navRoute = CorrectionsDownloadR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsUpload - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute CorrectionsUploadR - , menuItemModal = True - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrectionsUpload + , navRoute = CorrectionsUploadR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsCreate - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute CorrectionsCreateR - , menuItemModal = False - , menuItemAccessCallback' = runDB . maybeT (return False) $ do + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrectionsCreate + , navRoute = CorrectionsCreateR + , navAccess' = runDB . maybeT (return False) $ do uid <- MaybeT $ liftHandler maybeAuthId sheets <- lift . E.select . E.from $ \(course `E.InnerJoin` sheet) -> do E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse @@ -3145,114 +3666,137 @@ pageActions (CorrectionsR) = E.where_ $ isCorrector' E.||. isLecturer return $ sheet E.^. SheetSubmissionMode return $ orOf (traverse . _Value . _submissionModeCorrector) sheets + , navType = NavTypeLink { navModal = False } + , navQuick' = navQuick NavQuickViewPageActionSecondary + , navForceActive = False } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsGrade - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute CorrectionsGradeR - , menuItemModal = False - , menuItemAccessCallback' = return True + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrectionsGrade + , navRoute = CorrectionsGradeR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + ] +pageActions CorrectionsGradeR = do + correctionsSecondary <- pageQuickActions NavQuickViewPageActionSecondary CorrectionsR + return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuCorrections + , navRoute = CorrectionsR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = correctionsSecondary + } + ] +pageActions EExamListR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExternalExamNew + , navRoute = EExamNewR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } ] -pageActions (CorrectionsGradeR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsUpload - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute CorrectionsUploadR - , menuItemModal = True - , menuItemAccessCallback' = return True +pageActions (EExamR tid ssh coursen examn EEShowR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExternalExamEdit + , navRoute = EExamR tid ssh coursen examn EEEditR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsCreate - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute CorrectionsCreateR - , menuItemModal = False - , menuItemAccessCallback' = runDB . maybeT (return False) $ do - uid <- MaybeT $ liftHandler maybeAuthId - sheets <- lift . E.select . E.from $ \(course `E.InnerJoin` sheet) -> do - E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse - let - isCorrector' = E.exists . E.from $ \sheetCorrector -> E.where_ - $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val uid - E.&&. sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId - isLecturer = E.exists . E.from $ \lecturer -> E.where_ - $ lecturer E.^. LecturerUser E.==. E.val uid - E.&&. lecturer E.^. LecturerCourse E.==. course E.^. CourseId - E.where_ $ isCorrector' E.||. isLecturer - return $ sheet E.^. SheetSubmissionMode - return $ orOf (traverse . _Value . _submissionModeCorrector) sheets + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExternalExamUsers + , navRoute = EExamR tid ssh coursen examn EEUsersR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExternalExamGrades + , navRoute = EExamR tid ssh coursen examn EEGradesR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } ] -pageActions EExamListR = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExternalExamNew - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute EExamNewR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (EExamR tid ssh coursen examn EEGradesR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExternalExamUsers + , navRoute = EExamR tid ssh coursen examn EEUsersR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } ] -pageActions (EExamR tid ssh coursen examn EEShowR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExternalExamEdit - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEEditR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExternalExamUsers - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEUsersR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExternalExamGrades - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEGradesR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions (EExamR tid ssh coursen examn EEUsersR) = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExternalExamGrades + , navRoute = EExamR tid ssh coursen examn EEGradesR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } ] -pageActions (EExamR tid ssh coursen examn EEGradesR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExternalExamUsers - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEUsersR - , menuItemModal = False - , menuItemAccessCallback' = return True +pageActions ParticipantsListR = return + [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgCsvOptions + , navRoute = CsvOptionsR + , navAccess' = return True + , navType = NavTypeLink { navModal = True } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] } ] -pageActions (EExamR tid ssh coursen examn EEUsersR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuExternalExamGrades - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEGradesR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - ] -pageActions ParticipantsListR = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgCsvOptions - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute CsvOptionsR - , menuItemModal = True - , menuItemAccessCallback' = return True - } - ] -pageActions _ = [] +pageActions _ = return [] + +pageQuickActions :: ( MonadCatch m + , MonadHandler m + , HandlerSite m ~ UniWorX + ) + => NavQuickView -> Route UniWorX -> m [NavLink] +pageQuickActions qView route = do + items'' <- pageActions route + items' <- catMaybes <$> mapM (runMaybeT . navAccess) items'' + filterM navLinkAccess $ items' ^.. typesUsing @NavChildren @NavLink . filtered (getAny . ($ qView) . navQuick') i18nHeading :: (MonadWidget m, RenderMessage site msg, HandlerSite m ~ site) => msg -> m () @@ -3262,8 +3806,8 @@ i18nHeading msg = liftWidget $ toWidget =<< getMessageRender <*> pure msg pageHeading :: Route UniWorX -> Maybe Widget pageHeading (AuthR _) = Just $ i18nHeading MsgLoginHeading -pageHeading HomeR - = Just $ i18nHeading MsgHomeHeading +pageHeading NewsR + = Just $ i18nHeading MsgNewsHeading pageHeading UsersR = Just $ i18nHeading MsgUsers pageHeading (AdminUserR _) @@ -3889,9 +4433,9 @@ instance YesodAuth UniWorX where type AuthId UniWorX = UserId -- Where to send a user after successful login - loginDest _ = HomeR + loginDest _ = NewsR -- Where to send a user after logout - logoutDest _ = HomeR + logoutDest _ = NewsR -- Override the above two destinations when a Referer: header is present redirectToReferer _ = True diff --git a/src/Foundation/I18n.hs b/src/Foundation/I18n.hs index 99406decd..028be1e3a 100644 --- a/src/Foundation/I18n.hs +++ b/src/Foundation/I18n.hs @@ -168,7 +168,7 @@ instance RenderMessage UniWorX MsgLanguage where | ("en" : _) <- lang' = mr MsgEnglish | otherwise = lang where - mr = renderMessage foundation ls + mr = renderMessage foundation $ lang : filter (/= lang) ls embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>) embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel diff --git a/src/Handler/Exam.hs b/src/Handler/Exam.hs index ef54cc1ef..0e3c209dd 100644 --- a/src/Handler/Exam.hs +++ b/src/Handler/Exam.hs @@ -11,5 +11,5 @@ import Handler.Exam.Edit as Handler.Exam import Handler.Exam.Show as Handler.Exam import Handler.Exam.Users as Handler.Exam import Handler.Exam.AddUser as Handler.Exam -import Handler.Exam.Correct as Handler.Exam import Handler.Exam.AutoOccurrence as Handler.Exam +import Handler.Exam.Correct as Handler.Exam diff --git a/src/Handler/Exam/Correct.hs b/src/Handler/Exam/Correct.hs index 70996ab93..447a4c301 100644 --- a/src/Handler/Exam/Correct.hs +++ b/src/Handler/Exam/Correct.hs @@ -38,7 +38,8 @@ data CorrectInterfaceResponse , ciraMessage :: Text } | CorrectInterfaceResponseFailure - { cirfMessage :: Text + { cirfUser :: Maybe CorrectInterfaceUser + , cirfMessage :: Text } | CorrectInterfaceResponseNoOp { cirnUsers :: Set CorrectInterfaceUser @@ -84,8 +85,6 @@ getECorrectR tid ssh csh examn = do name <- newIdent fieldView (pointsField :: Field Handler Points) ("exam-correct__" <> toPathPiece n) name [("uw-exam-correct--part-input", toPathPiece n)] (Left "") False - participantHeadTooltip = [whamlet| _{MsgExamCorrectHeadParticipantTooltip} |] - examGrades :: [ExamGrade] examGrades = universeF @@ -93,6 +92,7 @@ getECorrectR tid ssh csh examn = do siteLayoutMsg heading $ do setTitleI heading + let examCorrectExplanation = $(i18nWidgetFile "exam-correct-explanation") $(widgetFile "exam-correct") @@ -102,11 +102,11 @@ postECorrectR tid ssh csh examn = do CorrectInterfaceRequest{..} <- requireCheckJsonBody - response <- runDB $ do - Entity eId Exam{..} <- fetchExam tid ssh csh examn + response <- exceptT return return . hoist runDB $ do + Entity eId Exam{..} <- lift $ fetchExam tid ssh csh examn euid <- traverse decrypt ciqUser - participantMatches <- E.select . E.from $ \(examRegistration `E.InnerJoin` user) -> do + participantMatches <- lift . E.select . E.from $ \(examRegistration `E.InnerJoin` user) -> do E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId @@ -149,10 +149,10 @@ postECorrectR tid ssh csh examn = do now <- liftIO getCurrentTime newExamPartResults <- if | Just results <- ciqResults -> iforM (toNullable results) $ \partNumber mPartResult -> do - (Entity examPartId ExamPart{..}) <- getBy404 $ UniqueExamPartNumber eId partNumber - mOldResult <- getBy $ UniqueExamPartResult examPartId uid + (Entity examPartId ExamPart{..}) <- lift . getBy404 $ UniqueExamPartNumber eId partNumber + mOldResult <- lift . getBy $ UniqueExamPartResult examPartId uid if - | Just (Entity oldId _) <- mOldResult, is _Nothing mPartResult -> do + | Just (Entity oldId _) <- mOldResult, is _Nothing mPartResult -> lift $ do delete oldId audit $ TransactionExamPartResultDeleted examPartId uid return Nothing @@ -161,29 +161,36 @@ postECorrectR tid ssh csh examn = do mNew = ExamAttended <$> mPartResult resultVal = _entityVal . _examPartResultResult in if - | mOld /= mNew -> let - -- cut off part results that exceed the maximum number of points for this exam part for now - -- TODO answer with new failure response type instead - partResult' = if - | Just maxPts <- examPartMaxPoints, maxPts < partResult -> maxPts - | otherwise -> partResult - in do - newExamPartResult <- upsert ExamPartResult + | mOld /= mNew -> do + let + partResultAcceptable = 0 <= partResult + && maybe True (partResult <=) examPartMaxPoints + guardMExceptT partResultAcceptable $ + let + msg | Just maxPoints <- examPartMaxPoints + = MsgExamCorrectErrorPartResultOutOfBoundsMax partNumber maxPoints + | otherwise + = MsgExamCorrectErrorPartResultOutOfBounds partNumber + in CorrectInterfaceResponseFailure + <$> (Just <$> userToResponse match) + <*> (getMessageRender <*> pure msg) + + newExamPartResult <- lift $ upsert ExamPartResult { examPartResultExamPart = examPartId , examPartResultUser = uid - , examPartResultResult = ExamAttended partResult' + , examPartResultResult = ExamAttended partResult , examPartResultLastChanged = now } - [ ExamPartResultResult =. ExamAttended partResult' + [ ExamPartResultResult =. ExamAttended partResult , ExamPartResultLastChanged =. now ] - audit $ TransactionExamPartResultEdit examPartId uid + lift . audit $ TransactionExamPartResultEdit examPartId uid return $ newExamPartResult ^? resultVal | otherwise -> return $ mOldResult ^? _Just . resultVal | otherwise -> return Nothing | otherwise -> return mempty - newExamResult <- do + newExamResult <- lift $ do mOldResult <- getBy $ UniqueExamResult eId uid if | Just (Entity oldId _) <- mOldResult, is _Nothing ciqGrade -> do @@ -221,7 +228,8 @@ postECorrectR tid ssh csh examn = do -- on match with no exam participant, answer with 400 | [] <- participantMatches -> return CorrectInterfaceResponseFailure - { cirfMessage = mr MsgExamCorrectErrorNoMatchingParticipants + { cirfUser = Nothing + , cirfMessage = mr MsgExamCorrectErrorNoMatchingParticipants } -- on match with multiple exam participants, answer with 400 and a set of all matches diff --git a/src/Handler/Exam/Show.hs b/src/Handler/Exam/Show.hs index e1bec059e..8a01b551b 100644 --- a/src/Handler/Exam/Show.hs +++ b/src/Handler/Exam/Show.hs @@ -82,6 +82,7 @@ getEShowR tid ssh csh examn = do examClosedShown = lecturerInfoShown showCloseWidget = lecturerInfoShown showAutoOccurrenceCalculateWidget = lecturerInfoShown + examFinishedMsg = if lecturerInfoShown then MsgExamFinished else MsgExamFinishedParticipant sumMaxPoints = sum [ fromRational examPartWeight * mPoints | Entity _ ExamPart{..} <- examParts, mPoints <- examPartMaxPoints ^.. _Just ] diff --git a/src/Handler/Exam/Users.hs b/src/Handler/Exam/Users.hs index eee9a53b0..2d82115c7 100644 --- a/src/Handler/Exam/Users.hs +++ b/src/Handler/Exam/Users.hs @@ -1089,5 +1089,6 @@ postEUsersR tid ssh csh examn = do siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading - let computedValuesTip = $(i18nWidgetFile "exam-users/computed-values-tip") + let computedValuesTip = notificationWidget NotificationBroad Warning + $(i18nWidgetFile "exam-users/computed-values-tip") $(widgetFile "exam-users") diff --git a/src/Handler/ExamOffice/Exam.hs b/src/Handler/ExamOffice/Exam.hs index fbb18a591..c92ec1e62 100644 --- a/src/Handler/ExamOffice/Exam.hs +++ b/src/Handler/ExamOffice/Exam.hs @@ -441,4 +441,5 @@ postEGradesR tid ssh csh examn = do siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading) $ do setTitleI $ prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading + let examGradesExplanation = notificationWidget NotificationBroad Info $(i18nWidgetFile "exam-office/exam-grades-explanation") $(widgetFile "exam-office/exam-results") diff --git a/src/Handler/ExamOffice/ExternalExam.hs b/src/Handler/ExamOffice/ExternalExam.hs index 2d7978fbc..db65d9c8c 100644 --- a/src/Handler/ExamOffice/ExternalExam.hs +++ b/src/Handler/ExamOffice/ExternalExam.hs @@ -30,4 +30,5 @@ postEEGradesR tid ssh coursen examn = do siteLayoutMsg (MsgExternalExamGrades coursen examn) $ do setTitleI MsgBreadcrumbExternalExamGrades + let examGradesExplanation = notificationWidget NotificationBroad Info $(i18nWidgetFile "exam-office/exam-grades-explanation") $(widgetFile "exam-office/externalExamGrades") diff --git a/src/Handler/ExamOffice/Users.hs b/src/Handler/ExamOffice/Users.hs index fd03b912b..ab5588677 100644 --- a/src/Handler/ExamOffice/Users.hs +++ b/src/Handler/ExamOffice/Users.hs @@ -80,7 +80,7 @@ examOfficeUserInvitationConfig = InvitationConfig{..} return res invitationSuccessMsg _ _ = return $ SomeMessage MsgExamOfficeUserInvitationAccepted - invitationUltDest _ _ = return $ SomeRoute HomeR + invitationUltDest _ _ = return $ SomeRoute NewsR makeExamOfficeUsersForm :: Maybe (Set (Either UserEmail UserId)) -> Form (Set (Either UserEmail UserId)) diff --git a/src/Handler/Home.hs b/src/Handler/News.hs similarity index 96% rename from src/Handler/Home.hs rename to src/Handler/News.hs index 532326ec3..e5eab1715 100644 --- a/src/Handler/Home.hs +++ b/src/Handler/News.hs @@ -1,4 +1,4 @@ -module Handler.Home where +module Handler.News where import Import @@ -9,21 +9,25 @@ import Database.Esqueleto.Utils.TH import qualified Database.Esqueleto as E import qualified Database.Esqueleto.Utils as E -getHomeR :: Handler Html -getHomeR = do +getNewsR :: Handler Html +getNewsR = do muid <- maybeAuthId defaultLayout $ do - setTitleI MsgHomeHeading + setTitleI MsgNewsHeading + + when (is _Nothing muid) $ + notificationWidget NotificationBroad Info $(i18nWidgetFile "pitch") + case muid of Just uid -> do - homeUpcomingExams uid - homeUpcomingSheets uid + newsUpcomingExams uid + newsUpcomingSheets uid Nothing -> - $(i18nWidgetFile "unauth-home") + $(i18nWidgetFile "unauth-news") -homeUpcomingSheets :: UserId -> Widget -homeUpcomingSheets uid = do +newsUpcomingSheets :: UserId -> Widget +newsUpcomingSheets uid = do cTime <- liftIO getCurrentTime let tableData :: E.LeftOuterJoin (E.InnerJoin (E.InnerJoin (E.SqlExpr (Entity CourseParticipant)) (E.SqlExpr (Entity Course))) (E.SqlExpr (Entity Sheet))) @@ -121,11 +125,11 @@ homeUpcomingSheets uid = do , dbtCsvEncode = noCsvEncode , dbtCsvDecode = Nothing } - $(widgetFile "home/upcomingSheets") + $(widgetFile "news/upcomingSheets") -homeUpcomingExams :: UserId -> Widget -homeUpcomingExams uid = do +newsUpcomingExams :: UserId -> Widget +newsUpcomingExams uid = do now <- liftIO getCurrentTime ((Any hasExams, examTable), warningDays) <- liftHandler . runDB $ do User {userWarningDays} <- get404 uid @@ -255,6 +259,6 @@ homeUpcomingExams uid = do (, userWarningDays) <$> dbTable examDBTableValidator examDBTable - $(widgetFile "home/upcomingExams") + $(widgetFile "news/upcomingExams") diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs index c13c3f13f..b34ce9cb2 100644 --- a/src/Handler/Profile.hs +++ b/src/Handler/Profile.hs @@ -850,17 +850,14 @@ postCsvOptionsR = do , formAttrs = [ asyncSubmitAttr | isModal ] } -postLangR :: Handler () +postLangR :: Handler Void postLangR = do - ((langRes, _), _) <- runFormPost $ identifyForm FIDLanguage langForm + requestedLang <- selectLanguage' appLanguages . hoistMaybe <$> lookupGlobalPostParam PostLanguage + lang' <- runDB . updateUserLanguage $ Just requestedLang - formResult langRes $ \(lang, route) -> do - lang' <- runDB . updateUserLanguage $ Just lang - - app <- getYesod - let mr | Just lang'' <- lang' = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang'' - | otherwise = renderMessage app [] - addMessage Success . toHtml $ mr MsgLanguageChanged - redirect route + app <- getYesod + let mr | Just lang'' <- lang' = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang'' + | otherwise = renderMessage app [] + addMessage Success . toHtml $ mr MsgLanguageChanged - invalidArgs ["Language form required"] + redirect . fromMaybe NewsR =<< lookupGlobalGetParam GetReferer diff --git a/src/Handler/Utils/Invitations.hs b/src/Handler/Utils/Invitations.hs index 5e3489ac0..983ea7b3c 100644 --- a/src/Handler/Utils/Invitations.hs +++ b/src/Handler/Utils/Invitations.hs @@ -399,7 +399,7 @@ invitationR' InvitationConfig{..} = liftHandler $ do Nothing -> do addMessageI Info MsgInvitationDeclined deleteBy . UniqueInvitation itEmail $ invRef @junction fid - return . Just $ SomeRoute HomeR + return . Just $ SomeRoute NewsR Just (jData, formCtx) -> do let junction = review _InvitableJunction (invitee, fid, jData) mResult <- invitationInsertHook itEmail fEnt iData junction formCtx $ insertUniqueEntity junction diff --git a/src/Import/NoFoundation.hs b/src/Import/NoFoundation.hs index b722fb338..c2f083f37 100644 --- a/src/Import/NoFoundation.hs +++ b/src/Import/NoFoundation.hs @@ -10,6 +10,7 @@ import Model.Submission as Import import Model.Tokens as Import import Utils.Tokens as Import import Utils.Frontend.Modal as Import +import Utils.Frontend.Notification as Import import Utils.Lens as Import import Settings as Import diff --git a/src/Utils.hs b/src/Utils.hs index f924d3141..c1a05222d 100644 --- a/src/Utils.hs +++ b/src/Utils.hs @@ -630,6 +630,15 @@ assertM' f x = x <$ guard (f x) guardOn :: Alternative m => Bool -> a -> m a guardOn b x = x <$ guard b +guardOnM :: Alternative m => Bool -> m a -> m a +guardOnM b x = guard b *> x + +guardMOn :: MonadPlus m => m Bool -> a -> m a +guardMOn b x = x <$ guardM b + +guardMOnM :: MonadPlus m => m Bool -> m a -> m a +guardMOnM b x = guardM b *> x + -- Some Utility Functions from Agda.Utils.Monad -- | Monadic if-then-else. ifM :: Monad m => m Bool -> m a -> m a -> m a diff --git a/src/Utils/Form.hs b/src/Utils/Form.hs index 78af6cfaf..3a3e2e8f6 100644 --- a/src/Utils/Form.hs +++ b/src/Utils/Form.hs @@ -46,6 +46,7 @@ import Data.Scientific import Data.Time.Clock (NominalDiffTime, nominalDay) import Utils +import Utils.Frontend.Notification -- import Utils.Message -- import Utils.PathPiece -- import Utils.Route @@ -869,26 +870,15 @@ wformMessage :: (MonadHandler m) => Message -> WForm m () wformMessage = void . aFormToWForm . aformMessage formMessage :: (MonadHandler m) => Message -> MForm m (FormResult (), FieldView site) -formMessage Message{..} = do +formMessage msg = do return (FormSuccess (), FieldView { fvLabel = mempty , fvTooltip = Nothing , fvId = idFormMessageNoinput , fvErrors = Nothing , fvRequired = False - , fvInput = [whamlet| - $newline never -
-
- #{messageContent} - |] + , fvInput = notification NotificationNarrow msg }) - where - defaultIcon = case messageStatus of - Success -> "check-circle" - Info -> "info-circle" - Warning -> "exclamation-circle" - Error -> "exclamation-triangle" --------------------- -- Form evaluation -- diff --git a/src/Utils/Frontend/Notification.hs b/src/Utils/Frontend/Notification.hs new file mode 100644 index 000000000..d4ec0758a --- /dev/null +++ b/src/Utils/Frontend/Notification.hs @@ -0,0 +1,43 @@ +module Utils.Frontend.Notification + ( NotificationType(..) + , notification + , notificationWidget + ) where + +import ClassyPrelude.Yesod +import Settings + +import Utils.Message +import Utils.Icon + +import Control.Lens +import Control.Lens.Extras (is) + + +data NotificationType + = NotificationNarrow + | NotificationBroad + deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) + +makePrisms ''NotificationType + + +notification :: NotificationType + -> Message + -> WidgetFor site () +notification nType Message{ messageIcon = messageIcon', .. } + = $(widgetFile "widgets/notification") + where + messageIcon = fromMaybe defaultIcon messageIcon' + defaultIcon = case messageStatus of + Success -> IconNotificationSuccess + Info -> IconNotificationInfo + Warning -> IconNotificationWarning + Error -> IconNotificationError + +notificationWidget :: Yesod site + => NotificationType + -> MessageStatus + -> WidgetFor site () + -> WidgetFor site () +notificationWidget nType ms = notification nType <=< messageWidget ms diff --git a/src/Utils/Icon.hs b/src/Utils/Icon.hs index e9fdcce66..37b29ec16 100644 --- a/src/Utils/Icon.hs +++ b/src/Utils/Icon.hs @@ -63,44 +63,85 @@ data Icon | IconApplicationVeto | IconApplicationFiles | IconTooltipDefault - deriving (Eq, Ord, Enum, Bounded, Show, Read) + | IconNotificationSuccess + | IconNotificationInfo + | IconNotificationWarning + | IconNotificationError + | IconFavourite + | IconLanguage + | IconNavContainerClose | IconPageActionChildrenClose + | IconMenuNews + | IconMenuHelp + | IconMenuProfile + | IconMenuLogin | IconMenuLogout + | IconBreadcrumbsHome + | IconMenuExtra + | IconMenuCourseList + | IconMenuCorrections + | IconMenuExams + | IconMenuAdmin + | IconPageActionPrimaryExpand | IconPageActionSecondary + | IconBreadcrumbSeparator + deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic, Typeable) iconText :: Icon -> Text iconText = \case - IconNew -> "seedling" - IconOK -> "check" - IconNotOK -> "times" - IconWarning -> "exclamation" - IconProblem -> "bolt" - IconVisible -> "eye" - IconInvisible -> "eye-slash" - IconCourse -> "graduation-cap" - IconEnrolTrue -> "user-plus" - IconEnrolFalse -> "user-slash" - IconPlanned -> "cog" - IconAnnounce -> "bullhorn" - IconExam -> "poll-h" - IconExamRegisterTrue -> "calendar-check" - IconExamRegisterFalse -> "calendar-times" - IconCommentTrue -> "comment-alt" - IconCommentFalse -> "comment-slash" -- comment-alt-slash is not available for free - IconLink -> "link" - IconFileDownload -> "file-download" - IconFileUpload -> "file-upload" - IconFileZip -> "file-archive" - IconFileCSV -> "file-csv" - IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar) - IconSFTHint -> "life-ring" -- for SheetFileType only - IconSFTSolution -> "exclamation-circle" -- for SheetFileType only - IconSFTMarking -> "check-circle" -- for SheetFileType only - IconEmail -> "envelope" - IconRegisterTemplate -> "file-alt" - IconApplyTrue -> "file-alt" - IconApplyFalse -> "trash" - IconNoCorrectors -> "user-slash" - IconApplicationVeto -> "times" - IconApplicationFiles -> "file-alt" - IconTooltipDefault -> "question-circle" + IconNew -> "seedling" + IconOK -> "check" + IconNotOK -> "times" + IconWarning -> "exclamation" + IconProblem -> "bolt" + IconVisible -> "eye" + IconInvisible -> "eye-slash" + IconCourse -> "graduation-cap" + IconEnrolTrue -> "user-plus" + IconEnrolFalse -> "user-slash" + IconPlanned -> "cog" + IconAnnounce -> "bullhorn" + IconExam -> "poll-h" + IconExamRegisterTrue -> "calendar-check" + IconExamRegisterFalse -> "calendar-times" + IconCommentTrue -> "comment-alt" + IconCommentFalse -> "comment-alt-slash" + IconLink -> "link" + IconFileDownload -> "file-download" + IconFileUpload -> "file-upload" + IconFileZip -> "file-archive" + IconFileCSV -> "file-csv" + IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar) + IconSFTHint -> "life-ring" -- for SheetFileType only + IconSFTSolution -> "exclamation-circle" -- for SheetFileType only + IconSFTMarking -> "check-circle" -- for SheetFileType only + IconEmail -> "envelope" + IconRegisterTemplate -> "file-alt" + IconApplyTrue -> "file-alt" + IconApplyFalse -> "trash" + IconNoCorrectors -> "user-slash" + IconApplicationVeto -> "times" + IconApplicationFiles -> "file-alt" + IconTooltipDefault -> "question-circle" + IconNotificationSuccess -> "check-circle" + IconNotificationInfo -> "info-circle" + IconNotificationWarning -> "exclamation-circle" + IconNotificationError -> "exclamation-triangle" + IconFavourite -> "star" + IconLanguage -> "flag-alt" + IconNavContainerClose -> "chevron-up" + IconPageActionChildrenClose -> "chevron-up" + IconMenuNews -> "megaphone" + IconMenuHelp -> "question" + IconMenuProfile -> "cogs" + IconMenuLogin -> "sign-in-alt" + IconMenuLogout -> "sign-out-alt" + IconBreadcrumbsHome -> "home" + IconMenuExtra -> "ellipsis-h" + IconMenuCourseList -> "graduation-cap" + IconMenuCorrections -> "check" + IconMenuExams -> "poll-h" + IconMenuAdmin -> "screwdriver" + IconPageActionPrimaryExpand -> "bars" + IconPageActionSecondary -> "ellipsis-h" + IconBreadcrumbSeparator -> "angle-right" instance Universe Icon instance Finite Icon diff --git a/src/Utils/Lens/TH.hs b/src/Utils/Lens/TH.hs index c4a2f1a82..e52157a18 100644 --- a/src/Utils/Lens/TH.hs +++ b/src/Utils/Lens/TH.hs @@ -1,5 +1,6 @@ module Utils.Lens.TH - ( makeLenses_, makeClassyFor_ + ( lensRules_ + , makeLenses_, makeClassyFor_ , multifocusG, multifocusL ) where diff --git a/src/Utils/Message.hs b/src/Utils/Message.hs index 59bfbb926..71b4d95a3 100644 --- a/src/Utils/Message.hs +++ b/src/Utils/Message.hs @@ -6,7 +6,7 @@ module Utils.Message , addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget , statusToUrgencyClass , Message(..) - , messageIconI + , messageIconI, messageIconIHamlet, messageIconWidget , messageI, messageIHamlet, messageFile, messageWidget, messageTooltip ) where @@ -163,6 +163,15 @@ messageIHamlet ms iHamlet = do let mi = Nothing Message ms <$> withUrlRenderer (iHamlet $ toHtml . mr) <*> pure mi +messageIconIHamlet :: ( MonadHandler m + , RenderMessage (HandlerSite m) msg + , HandlerSite m ~ site + ) => MessageStatus -> Icon -> HtmlUrlI18n msg (Route site) -> m Message +messageIconIHamlet messageStatus (Just -> messageIcon) iHamlet = do + mr <- getMessageRender + messageContent <- withUrlRenderer (iHamlet $ toHtml . mr) + return Message{..} + addMessageFile :: MessageStatus -> FilePath -> ExpQ addMessageFile mc tPath = [e|addMessageIHamlet mc $(ihamletFile tPath)|] @@ -189,6 +198,15 @@ messageWidget mc wgt = do PageContent{pageBody} <- liftHandler $ widgetToPageContent wgt messageIHamlet mc (const pageBody :: HtmlUrlI18n (SomeMessage site) (Route site)) +messageIconWidget :: forall m site. + ( MonadHandler m + , HandlerSite m ~ site + , Yesod site + ) => MessageStatus -> Icon -> WidgetFor site () -> m Message +messageIconWidget ms mi wgt = do + PageContent{pageBody} <- liftHandler $ widgetToPageContent wgt + messageIconIHamlet ms mi (const pageBody :: HtmlUrlI18n (SomeMessage site) (Route site)) + getMessages :: MonadHandler m => m [Message] getMessages = fmap decodeMessage <$> ClassyPrelude.Yesod.getMessages diff --git a/src/Utils/Parameters.hs b/src/Utils/Parameters.hs index f78926740..cdc4a80c1 100644 --- a/src/Utils/Parameters.hs +++ b/src/Utils/Parameters.hs @@ -58,6 +58,7 @@ data GlobalPostParam = PostFormIdentifier | PostDBCsvImportAction | PostLoginDummy | PostExamAutoOccurrencePrevious + | PostLanguage deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic) instance Universe GlobalPostParam diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet index 22fe5fa2d..2f8a66ebb 100644 --- a/templates/default-layout.hamlet +++ b/templates/default-layout.hamlet @@ -1,5 +1,11 @@ $newline never $if not isModal + $with containers <- filter isNavHeaderContainer nav + $if not (null containers) + + $forall (_, containerIdent, _, _) <- containers + + ^{asidenav} @@ -15,7 +21,7 @@ $if not isModal $if not isModal - $if not $ Just HomeR == mcurrentRoute + $if not $ Just NewsR == mcurrentRoute ^{breadcrumbsWgt}
@@ -26,7 +32,7 @@ $if not isModal #{snd back} --> ^{headline} - $if not isModal && hasPageActions + $if hasPageActions ^{pageaction} diff --git a/templates/exam-correct.hamlet b/templates/exam-correct.hamlet index 8700f3b8d..1a2bf13a2 100644 --- a/templates/exam-correct.hamlet +++ b/templates/exam-correct.hamlet @@ -1,24 +1,18 @@ $newline never +
+ ^{examCorrectExplanation}
- +
_{MsgExamCorrectHeadDate} _{MsgExamCorrectHeadParticipant} - ^{iconTooltip participantHeadTooltip Nothing True} - $forall ExamPart{examPartNumber,examPartName} <- examParts + $forall ExamPart{examPartNumber} <- examParts - $maybe name <- examPartName - - - _{MsgExamCorrectHeadPart examPartNumber} - - _{MsgExamCorrectHeadPartName name} - $nothing _{MsgExamCorrectHeadPart examPartNumber} $if mayEditResults @@ -35,6 +29,8 @@ $newline never $forall ExamPart{examPartNumber} <- examParts ^{ptsInput examPartNumber} + + - -
Automatisch berechnet - Normaler Wert - Inkonsistent + + +
Automatisch berechnet + Normaler Wert + Inkonsistent diff --git a/templates/i18n/exam-users/computed-values-tip/en-eu.hamlet b/templates/i18n/exam-users/computed-values-tip/en-eu.hamlet index 4d3f28283..438c706d0 100644 --- a/templates/i18n/exam-users/computed-values-tip/en-eu.hamlet +++ b/templates/i18n/exam-users/computed-values-tip/en-eu.hamlet @@ -1,23 +1,21 @@ $newline never -
-
-

- This table contains values that were computed automatically. -

- Values computed automatically (bonus and result) are shown to neither the # - participant nor relevant exam offices until they are manually accepted.
- To do this you may use the action “Accept computed result”. -

- You are also able to override the automatically computed values manually # - (via CSV import).
+

+ This table contains values that were computed automatically. +

+ Values computed automatically (bonus and result) are shown to neither the # + participant nor relevant exam offices until they are manually accepted.
+ To do this you may use the action “Accept computed result”. +

+ You are also able to override the automatically computed values manually # + (via CSV import).
- If values thus overriden do not match the automatically computed values # - they are considered inconsistent. -

- Automatically computed values are marked as follows: + If values thus overriden do not match the automatically computed values # + they are considered inconsistent. +

+ Automatically computed values are marked as follows: - - -
Automatically computed - Normal value - Inconsistent + + +
Automatically computed + Normal value + Inconsistent diff --git a/templates/i18n/glossary/exam-result.de-de-formal.hamlet b/templates/i18n/glossary/exam-result.de-de-formal.hamlet index 062047f3a..48dcc1c8f 100644 --- a/templates/i18n/glossary/exam-result.de-de-formal.hamlet +++ b/templates/i18n/glossary/exam-result.de-de-formal.hamlet @@ -3,6 +3,6 @@ $newline never Kursverwalter (und von ihnen beauftragte Korrektoren) können die Ergebnisse # einer Prüfung direkt in Uni2work hinterlegen.
Dies dient sowohl der Rückmeldung an den jeweiligen Studierenden als auch # - können Prüfungsämter, mit den notwendigen Berechtigungen, die # + können Prüfungsverwalter, mit den notwendigen Berechtigungen, die # Prüfungsleistungen direkt aus Uni2work mit ihren eigenen Verwaltungssystemen # synchronisieren. diff --git a/templates/i18n/glossary/school-exam-office.de-de-formal.hamlet b/templates/i18n/glossary/school-exam-office.de-de-formal.hamlet index 3f0461dab..ecb177252 100644 --- a/templates/i18n/glossary/school-exam-office.de-de-formal.hamlet +++ b/templates/i18n/glossary/school-exam-office.de-de-formal.hamlet @@ -5,7 +5,7 @@ $newline never benötigen um sicherzustellen, dass Studienmodalitäten erfüllt und Leistungen # korrekt anerkannt werden.

- Teil des Prüfungsamts ist eine Berechtigung die einzelnen Benutzern von # + Mit Prüfungsverwaltung beauftragt ist eine Berechtigung die einzelnen Benutzern von # einem Administrator für ein bestimmtes Institut eingeräumt wird.
Diese Benutzer haben dann u.A. Zugriff auf alle beim relevanten Institut # erbrachten und in Uni2work hinterlegten Prüfungsleistungen. diff --git a/templates/i18n/info-lecturer/de-de-formal.hamlet b/templates/i18n/info-lecturer/de-de-formal.hamlet index 815f88e3a..740a859dc 100644 --- a/templates/i18n/info-lecturer/de-de-formal.hamlet +++ b/templates/i18n/info-lecturer/de-de-formal.hamlet @@ -312,9 +312,9 @@ $newline text Anmeldezeitraums anmelden. Die Teilnehmerlisten können online oder per CSV Export/Import bearbeitet werden. -

^{plannedFeat} Prüfungszuteilung +
^{newFeat 2020 01 29} Prüfungszuteilung
- Auf Wunsch kann Uni2work in Zukunft die Zuteilung der Teilnehmer auf die Prüfungen (Räume bzw. Prüfungstermine) + Auf Wunsch kann Uni2work die Zuteilung der Teilnehmer auf die Prüfungen (Räume bzw. Prüfungstermine) nach verschiedenen Kriterien wie Name oder Matrikelnummer vornehmen.
^{plannedFeat} Korrekturen @@ -333,7 +333,7 @@ $newline text

Die Berechnung der Prüfungsergebnisse kann automatisch durch Uni2work erfolgen. Dabei muss ein Notenschlüssel angegeben werden, an dem die Endnote der Studenten automatisch anhand der erreichten Punktezahl abgelesen wird.

- ^{newU2WFeat} Die automatisch abgelesenen Noten werden den Kursverwaltern dabei zunächst als Vorschlag angezeigt. Die Vorschläge müssen erst von einem Kursverwalter akzeptiert werden, bevor sie als Ergebnisse den Teilnehmern angezeigt und an die Prüfungsämter gemeldet werden können. + ^{newU2WFeat} Die automatisch abgelesenen Noten werden den Kursverwaltern dabei zunächst als Vorschlag angezeigt. Die Vorschläge müssen erst von einem Kursverwalter akzeptiert werden, bevor sie als Ergebnisse den Teilnehmern angezeigt und an die Prüfungsverwaltung gemeldet werden können.

^{newU2WFeat} Die Vorschläge können auch unabhängig vom eingetragenen Notenschlüssel manuell überschrieben werden. @@ -354,15 +354,15 @@ $newline text

^{newFeat 2019 9 16} Notenmeldung

- Endnoten können automatisiert an die Prüfungsämter gemeldet werden. + Endnoten können automatisiert an die Prüfungsverwaltung gemeldet werden.

- Nach dem Abschließen einer Prüfung (durch einen Knopf über der Teilnehmerliste) werden alle mit den geprüften Teilnehmern assoziierten Prüfungsämter darüber informiert, dass die Notengebung abgeschlossen ist. + Nach dem Abschließen einer Prüfung (durch einen Knopf über der Teilnehmerliste) werden alle mit den geprüften Teilnehmern assoziierten Prüfungsverwalter darüber informiert, dass die Notengebung abgeschlossen ist.

- ^{newU2WFeat} Hat ein Prüfungsamt die Note eines Teilnehmers zur Kenntnis genommen, erscheint ein Häkchen auf der Seite "Prüfungsleistungen". Falls auch nach längerer Zeit noch kein assoziiertes Prüfungsamt die Note eines Teilnehmers zur Kenntnis genommen hat, so sollte ein Kursverwalter der Notenmeldung für den betroffenen Teilnehmer nachgehen (z.B. durch Ausstellen eines Scheins). + ^{newU2WFeat} Hat ein Prüfungsverwalter die Note eines Teilnehmers zur Kenntnis genommen, erscheint ein Häkchen auf der Seite "Prüfungsleistungen". Falls auch nach längerer Zeit noch keine assoziierte Prüfungsverwaltung die Note eines Teilnehmers zur Kenntnis genommen hat, so sollte ein Kursverwalter der Notenmeldung für den betroffenen Teilnehmer nachgehen (z.B. durch Ausstellen eines Scheins).

- ^{newU2WFeat} Dozenten dürfen nach der Übergabe an das Prüfungsamt + ^{newU2WFeat} Dozenten dürfen nach der Übergabe an die Prüfungsverwaltung nachträgliche Änderungen an den Prüfungen vornehmen. - Diejenigen Prüfungsämter, die von der nachträglichen Änderung betroffen sind, erhalten + Diejenigen Prüfungsverwalter, die von der nachträglichen Änderung betroffen sind, erhalten hierüber automatisiert eine Benachrichtigung. diff --git a/templates/i18n/info-lecturer/en-eu.hamlet b/templates/i18n/info-lecturer/en-eu.hamlet index 72221d0f1..bad719af5 100644 --- a/templates/i18n/info-lecturer/en-eu.hamlet +++ b/templates/i18n/info-lecturer/en-eu.hamlet @@ -303,7 +303,7 @@ $newline text

^{newU2WFeat} At first, the calculated results will be shown as suggestions only. These suggestions then require manual approval by the course administrator before being accepted - as exam results, which can afterwards be sent to the Prüfungsämter (Examination Offices). + as exam results, which can afterwards be sent to the relevant exam offices.

^{newU2WFeat} The calculated suggestions can also be overriden manually, independent of the grading scale. @@ -324,19 +324,19 @@ $newline text

^{newFeat 2019 9 16} Reporting Exam Results

- Final grades can be automatically sent to the Prüfungsämter (Exam Offices). + Final grades can be automatically sent to the relevant exam offices.

After finalizing an exam (by clicking a button on the list of exam participants), all associated - Prüfungsämter will be notified that the grading process is finished for this exam. + exom offices will be notified that the grading process is finished for this exam.

- ^{newU2WFeat} Once a Prüfungsamt has acknowledged the exam result of a participant, a check mark of the + ^{newU2WFeat} Once an exam office has acknowledged the exam result of a participant, a check mark of the page "Exam Results" will be shown.
In case of this check mark being absent even after a long period of time, a course administrator should look into reporting the exam result for this participant (e.g. by issueing a proof of participation).

^{newU2WFeat} Course administrators are allowed to edit final results even after reporting the grades - to the Prüfungsämter. The offices that are affected by these changes will be automatically notified. + to the exam offices. The offices that are affected by these changes will be automatically notified.

diff --git a/templates/i18n/info-lecturer/en.hamlet b/templates/i18n/info-lecturer/en.hamlet deleted file mode 100644 index 3ed3b9d02..000000000 --- a/templates/i18n/info-lecturer/en.hamlet +++ /dev/null @@ -1,380 +0,0 @@ -$newline text -
-
-

Courses - -
-
Course Names -
-

- ^{newU2WFeat} Any course needs a shorthand for identification, e.g. DBS, PXD, DM, ... -

- The combination of shorthand, department and semester needs to be unique.
- Creating a course with a shorthand that is not unique for the selected department and semester will be rejected (a corresponding error message will be shown). -

- Recommendations for course shorthands: -