Merge branch '476-interface-fur-klausurkorrekturen' into 476-interface-fur-klausurkorrekturen-dev

This commit is contained in:
Sarah Vaupel 2020-02-07 19:29:55 +01:00
commit 66317a41dc
78 changed files with 2948 additions and 2204 deletions

View File

@ -160,7 +160,7 @@ h4
--current-header-height: var(--header-height-collapsed) --current-header-height: var(--header-height-collapsed)
position: relative position: relative
background-color: white 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-top: var(--current-header-height)
margin-left: 0 margin-left: 0
@ -173,6 +173,12 @@ h4
> .container > .container
margin: 20px 0 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 .main__content, .modal__content
a a
text-decoration: underline text-decoration: underline
@ -180,19 +186,25 @@ h4
p, form, .div-p p, form, .div-p
margin: 0.5rem 0 margin: 0.5rem 0
&:first-child
margin: 0 0 0.5rem 0
&:last-child &:last-child
margin: 0.5rem 0 0 margin: 0.5rem 0 0 0
&:first-child &:first-child
margin: 0 margin: 0
@media (min-width: 769px) and (min-height: 501px)
.main__content
--current-header-height: var(--header-height)
@media (min-width: 426px) @media (min-width: 426px)
.main__content .main__content
margin-left: var(--asidenav-width-md, 50px) margin-left: var(--asidenav-width-md, 50px)
@media (min-width: 769px) @media (min-width: 769px)
.main__content .main__content
--current-header-height: var(--header-height)
margin-left: var(--asidenav-width-lg, 20%) margin-left: var(--asidenav-width-lg, 20%)
@media (min-width: 1200px) @media (min-width: 1200px)
@ -228,6 +240,7 @@ input[type="submit"],
input[type="button"], input[type="button"],
button, button,
.btn .btn
font-family: var(--font-base)
outline: 0 outline: 0
border: 0 border: 0
box-shadow: 0 box-shadow: 0
@ -524,6 +537,9 @@ section
display: grid display: grid
grid-column: 2 grid-column: 2
h1 + &
margin: 0 auto 0.5rem
&::before &::before
@extend .fas @extend .fas
@ -582,7 +598,7 @@ section
&::before &::before
height: auto height: auto
width: 45px width: 45px
font-size: 40px font-size: 20px
top: 15px top: 15px
.notification-error .notification-error
@ -598,15 +614,15 @@ section
color: var(--color-warning) color: var(--color-warning)
// "Heated" element. // "Heated" element.
Set custom property "--hotness" to a value from 0 to 1 to turn // 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. // the element's background to a color on a gradient from green to red.
TBD: // TBD:
- move to a proper place // - move to a proper place
- think about font-weight... // - think about font-weight...
Example: // Example:
<div .heated style="--hotness: 0.2">Lorem ipsum // <div .heated style="--hotness: 0.2">Lorem ipsum
.heated .heated
--hotness: 0 --hotness: 0
@ -625,7 +641,7 @@ section
.ribbon .ribbon
position: fixed position: fixed
top: calc(40px + var(--header-height)) 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 right: -63px
transform: rotate(45deg) transform: rotate(45deg)
width: 250px width: 250px
@ -636,15 +652,21 @@ section
font-size: 1.25rem font-size: 1.25rem
line-height: 2em line-height: 2em
box-shadow: 0 0 3px rgba(0, 0, 0, 0.4) box-shadow: 0 0 3px rgba(0, 0, 0, 0.4)
z-index: 19 z-index: 21
pointer-events: none 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 .ribbon
top: calc(20px + var(--header-height-collapsed)) top: calc(20px + var(--header-height-collapsed))
right: -83px right: -83px
transform: rotate(45deg) scale(0.6) transform: rotate(45deg) scale(0.6)
.navbar__container-radio:checked ~ &
top: calc(50px + var(--header-height-collapsed))
#admin-studyterms #admin-studyterms
select, option, input select, option, input
min-width: 50px min-width: 50px
@ -981,54 +1003,63 @@ th, td
.breadcrumbs__container .breadcrumbs__container
position: relative position: relative
color: var(--color-lightwhite) color: var(--color-lightwhite)
padding: 4px 13px padding: 4px 20px 4px 40px
background-color: var(--color-dark) background-color: var(--color-dark)
line-height: 30px
a
color: var(--color-lightwhite)
@media (min-width: 426px) @media (min-width: 426px)
.breadcrumbs__container .breadcrumbs__container
padding: 7px 20px padding: 7px 20px 7px 40px
@media (min-width: 769px) @media (min-width: 769px)
.breadcrumbs__container .breadcrumbs__container
padding: 7px 40px padding: 7px 40px
.breadcrumbs__link ul.breadcrumbs__list
color: var(--color-lightwhite) display: flex
align-items: center
height: 30px
margin: 0 -5px
&:hover & > li
color: var(--color-white) display: block
.breadcrumbs__item .breadcrumbs__item
padding-right: 14px
position: relative
line-height: 28px
opacity: 0.8 opacity: 0.8
z-index: 1 margin: 0 5px
margin-right: 10px
&:hover &:hover
opacity: 1 opacity: 1
&::after .breadcrumbs__item-separator
content: '' line-height: 0
position: absolute opacity: 0.5
top: 11px margin: 0 5px
right: 0 margin-top: 1px
width: 7px
height: 7px a.breadcrumbs__home
border-style: solid position: absolute
border-width: 0 left: 10px
border-bottom-width: 1px top: 5px
border-right-width: 1px width: 20px
border-color: var(--color-white) height: 30px
transform: rotate(-45deg) opacity: 0.5
z-index: 10 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 .breadcrumbs__last-item
line-height: 28px
vertical-align: bottom
font-weight: 600 font-weight: 600
opacity: 1
.recipient-category .recipient-category
max-width: 400px max-width: 400px
@ -1096,10 +1127,15 @@ th, td
overflow: auto overflow: auto
.footer .footer
display: flex
flex-flow: row wrap
justify-content: center
align-items: baseline
align-content: flex-start
text-align: center text-align: center
padding: 20px padding: 20px
position: relative position: relative
margin: 40px 0 margin: 40px 0 0 0
&::before &::before
content: '' content: ''
@ -1110,9 +1146,9 @@ th, td
height: 2px height: 2px
background-color: var(--color-grey-light) background-color: var(--color-grey-light)
.footer-links * .footer-links > *
margin-right: 0.5em margin-right: 0.5em
display: inline-block display: block
&:last &:last
margin-right: 0 margin-right: 0
@ -1138,70 +1174,3 @@ th, td
.checkbox .checkbox
display: inline-block display: inline-block
margin-left: 7px 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)

View File

@ -58,7 +58,7 @@ export class UtilRegistry {
if (utilInstance) { if (utilInstance) {
const instance = utilInstance.instance; const instance = utilInstance.instance;
if (instance && typeof instance.start === 'function') { if (instance && typeof instance.start === 'function') {
instance.start(); instance.start.bind(instance)();
startedInstances.push(instance); startedInstances.push(instance);
} }
} }
@ -91,6 +91,7 @@ export class UtilRegistry {
if (DEBUG_MODE > 0) { if (DEBUG_MODE > 0) {
console.error('Error while trying to initialize a utility!', { util , element, err }); console.error('Error while trying to initialize a utility!', { util , element, err });
} }
utilInstance = null;
} }
if (utilInstance) { if (utilInstance) {

View File

@ -1,7 +1,6 @@
.main__aside .main__aside
position: fixed position: fixed
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) box-shadow: 0 0 10px rgba(0, 0, 0, 0.3)
z-index: 1
top: 0 top: 0
left: 0 left: 0
width: var(--asidenav-width-lg, 20%) width: var(--asidenav-width-lg, 20%)
@ -9,16 +8,7 @@
flex: 0 0 0 flex: 0 0 0
flex-basis: var(--asidenav-width-lg, 20%) flex-basis: var(--asidenav-width-lg, 20%)
transition: all .2s ease-out transition: all .2s ease-out
z-index: 20
&::before
position: absolute
z-index: -1
left: 0
top: 0
width: 100%
height: 100%
background-color: var(--color-dark)
opacity: 0.05
&::after &::after
content: '' content: ''
@ -85,6 +75,7 @@
padding: 10px 13px padding: 10px 13px
margin: 0 margin: 0
border-bottom: 1px solid var(--color-grey) border-bottom: 1px solid var(--color-grey)
height: 44px
.asidenav-term-identifier--long .asidenav-term-identifier--long
display: inherit display: inherit
@ -232,7 +223,7 @@
// hover sub-menus // hover sub-menus
.asidenav__nested-list-wrapper .asidenav__nested-list-wrapper
position: absolute position: absolute
z-index: 10 z-index: 22
display: none display: none
color: var(--color-font) color: var(--color-font)
background-color: var(--color-grey-light) background-color: var(--color-grey-light)
@ -269,6 +260,10 @@
min-height: calc(100% - var(--header-height-collapsed)) min-height: calc(100% - var(--header-height-collapsed))
top: 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 .asidenav__box-title
width: var(--asidenav-width-md, 50px) width: var(--asidenav-width-md, 50px)
font-size: 18px font-size: 18px
@ -277,6 +272,7 @@
word-break: break-all word-break: break-all
background-color: var(--color-dark) background-color: var(--color-dark)
color: var(--color-lightwhite) color: var(--color-lightwhite)
border: none
&:hover &:hover
background-color: var(--color-darker) background-color: var(--color-darker)

View File

@ -5,7 +5,7 @@ const CHECKBOX_SELECTOR = '[type="checkbox"]';
const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized'; const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized';
@Utility({ @Utility({
selector: 'table', selector: 'table:not([uw-no-check-all])',
}) })
export class CheckAll { export class CheckAll {

View File

@ -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_INPUT_BODY_ID = 'exam-correct__new';
const EXAM_CORRECT_USER_ATTR = 'exam-correct--user-id'; const EXAM_CORRECT_USER_ATTR = 'exam-correct--user-id';
const EXAM_CORRECT_USER_DNAME_ATTR = 'exam-correct--user-dname'; const EXAM_CORRECT_USER_DNAME_ATTR = 'exam-correct--user-dname';
const EXAM_CORRECT_STATUS_CELL_CLASS = 'exam-correct--status-cell';
const STATUS = { const STATUS = {
NONE: null, NONE: null,
@ -162,7 +163,7 @@ export class ExamCorrect {
}).then( }).then(
(response) => response.json() (response) => response.json()
).then( ).then(
(response) => this._processResponse(response, body.user) (response) => this._processResponse(body, response, body.user)
).catch((error) => { ).catch((error) => {
console.error('Error while validating user input', 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) // abort send if there are no results (after validation)
if (Object.keys(results).length <= 0) return; if (Object.keys(results).length <= 0) return;
const rowInfo = { const rowInfo = {
users: [userId || user], users: [user],
results: results, results: results,
status: STATUS.LOADING, status: STATUS.LOADING,
}; };
if (results) rowInfo.results = results;
if (result) rowInfo.result = result === 'delete' ? null : result;
this._addRow(rowInfo); this._addRow(rowInfo);
// clear inputs on validation success // clear inputs on validation success
@ -219,8 +224,11 @@ export class ExamCorrect {
const body = { const body = {
user: userId || user, 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({ this._app.httpClient.post({
url: EXAM_CORRECT_URL_POST, url: EXAM_CORRECT_URL_POST,
@ -229,13 +237,13 @@ export class ExamCorrect {
}).then( }).then(
(response) => response.json() (response) => response.json()
).then( ).then(
(response) => this._processResponse(response, body.user, results) (response) => this._processResponse(body, response, user, undefined, { results: results, result: result })
).catch((error) => { ).catch((error) => {
console.error('Error while processing response', error); console.error('Error while processing response', error);
}); });
} }
_processResponse(response, user, results) { _processResponse(request, response, user, targetRow, ...results) {
if (response) { if (response) {
if (response.status === 'no-op') { if (response.status === 'no-op') {
if (response.users) { if (response.users) {
@ -292,19 +300,22 @@ export class ExamCorrect {
let newEntry = { let newEntry = {
users: null, users: null,
results: null, results: null,
result: null,
status: STATUS.FAILURE, status: STATUS.FAILURE,
message: null,
date: null, date: null,
}; };
console.log('response', response); 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')); let userElem = row.cells.item(this._cIndices.get('user'));
const userIdent = userElem && userElem.getAttribute(EXAM_CORRECT_USER_ATTR); // TODO use other attribute identifier const userIdent = userElem && userElem.getAttribute(EXAM_CORRECT_USER_ATTR); // TODO use other attribute identifier
if (userIdent === user) { if (userIdent === user) {
console.log('response-update', row);
let status = STATUS.FAILURE; let status = STATUS.FAILURE;
switch (response.status) { switch (response.status) {
// TODO fetch update time from response and replace
case 'success': case 'success':
status = STATUS.SUCCESS; status = STATUS.SUCCESS;
if (response.user) { if (response.user) {
@ -315,24 +326,27 @@ export class ExamCorrect {
timeElem.classList.remove('exam-correct--local-time'); timeElem.classList.remove('exam-correct--local-time');
newEntry.users = [response.user]; newEntry.users = [response.user];
newEntry.results = response.results; newEntry.results = response.results;
newEntry.result = response.grade;
} }
// TODO replace results with results from response
// TODO set edit button visibility // TODO set edit button visibility
break; break;
case 'ambiguous': case 'ambiguous':
// TODO show tooltip with error message
// TODO set edit button visibility // TODO set edit button visibility
status = STATUS.AMBIGUOUS; status = STATUS.AMBIGUOUS;
if (response.users) { if (response.users) {
userElem = this._showUserList(row, response.users, results); userElem = this._showUserList(row, response.users, results);
newEntry.users = response.users; newEntry.users = response.users;
newEntry.results = results; newEntry.results = results.partResults;
newEntry.result = results.result;
} }
newEntry.message = response.message || null;
break; break;
case 'failure': case 'failure':
status = STATUS.FAILURE; status = STATUS.FAILURE;
newEntry.users = [user]; newEntry.users = (response.user && [response.user]) || null;
newEntry.results = results; newEntry.results = results;
newEntry.message = response.message || null;
newEntry.result = results.result;
break; break;
default: default:
// TODO show tooltip with 'invalid response' // TODO show tooltip with 'invalid response'
@ -344,6 +358,36 @@ export class ExamCorrect {
}); });
newEntry.status = status || STATUS.FAILURE; newEntry.status = status || STATUS.FAILURE;
newEntry.date = response.time || moment().utc().format(); 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); savedEntries.push(newEntry);
this._storageManager.save('entries', savedEntries); this._storageManager.save('entries', savedEntries);
return; return;
@ -392,14 +436,15 @@ export class ExamCorrect {
const timeElem = row.cells.item(0); const timeElem = row.cells.item(0);
timeElem.innerHTML = now.format(this._dateFormat); timeElem.innerHTML = now.format(this._dateFormat);
timeElem.classList.add('exam-correct--local-time'); 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'); const statusElem = row.querySelector('.exam-correct--ambiguous');
setStatus(statusElem, STATUS.LOADING); setStatus(statusElem, STATUS.LOADING);
const body = { const body = {
user: listItem.getAttribute(EXAM_CORRECT_USER_ATTR), user: listItem.getAttribute(EXAM_CORRECT_USER_ATTR),
results: results, results: results.partResults,
grade: results.result,
}; };
this._app.httpClient.post({ this._app.httpClient.post({
@ -408,37 +453,8 @@ export class ExamCorrect {
body: JSON.stringify(body), body: JSON.stringify(body),
}).then( }).then(
(response) => response.json() (response) => response.json()
).then((response) => { ).then((response) => this._processResponse(body, response, userElem.getAttribute(EXAM_CORRECT_USER_ATTR), row, { results: results.partResults, result: results.result })
switch (response.status) { ).catch(console.error);
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);
});
} }
_addRow(rowInfo) { _addRow(rowInfo) {
@ -458,6 +474,7 @@ export class ExamCorrect {
cells.set(this._cIndices.get('date'), dateCell); cells.set(this._cIndices.get('date'), dateCell);
let userCell = document.createElement('TD'); let userCell = document.createElement('TD');
userCell.classList.add('uw-exam-correct--user-cell');
if (!rowInfo.users || rowInfo.users.length === 0) { if (!rowInfo.users || rowInfo.users.length === 0) {
console.error('Found rowInfo without users info!'); console.error('Found rowInfo without users info!');
} else if (rowInfo.users.length === 1) { } else if (rowInfo.users.length === 1) {
@ -465,7 +482,7 @@ export class ExamCorrect {
userCell.innerHTML = userToHTML(user); userCell.innerHTML = userToHTML(user);
userCell.setAttribute(EXAM_CORRECT_USER_ATTR, user); userCell.setAttribute(EXAM_CORRECT_USER_ATTR, user);
} else { } 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); cells.set(this._cIndices.get('user'), userCell);
@ -483,12 +500,15 @@ export class ExamCorrect {
const resultCell = document.createElement('TD'); const resultCell = document.createElement('TD');
resultCell.colSpan = 2; resultCell.colSpan = 2;
if (rowInfo.result)
resultCell.innerHTML = rowInfo.result;
cells.set(this._cIndices.get('result'), resultCell); cells.set(this._cIndices.get('result'), resultCell);
const statusCell = document.createElement('TD'); const statusCell = document.createElement('TD');
const statusDiv = document.createElement('DIV'); statusCell.classList.add(EXAM_CORRECT_STATUS_CELL_CLASS);
setStatus(statusDiv, rowInfo.status); const statusSymbol = document.createElement('I');
statusCell.appendChild(statusDiv); setStatus(statusSymbol, rowInfo.status);
statusCell.appendChild(statusSymbol);
cells.set(this._cIndices.get('status'), statusCell); cells.set(this._cIndices.get('status'), statusCell);
for (let i = 0; i <= this._lastColumnIndex; i++) { for (let i = 0; i <= this._lastColumnIndex; i++) {

View File

@ -8,13 +8,26 @@ table[uw-exam-correct]
th.uw-exam-correct--user-cell, td.uw-exam-correct--user-cell th.uw-exam-correct--user-cell, td.uw-exam-correct--user-cell
min-width: 200px min-width: 200px
th.uw-exam-correct--part-cell, td.uw-exam-correct--part-cell th.uw-exam-correct--part-cell, td.uw-exam-correct--part-cell
width: 85px width: min-content
text-align: center text-align: center
white-space: nowrap
input input
width: 70px width: 70px
padding: 4px 8px 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 td#uw-exam-correct__result
width: min-content width: min-content
select select
@ -34,6 +47,16 @@ table[uw-exam-correct]
td#uw-exam-correct__result__grade select.grade-hidden td#uw-exam-correct__result__grade select.grade-hidden
visibility: 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) [uw-exam-correct] input:invalid:not(.no-value)

View File

@ -1,48 +1,91 @@
import { Utility } from '../../core/utility'; import { Utility } from '../../core/utility';
import './navbar.sass'; import './navbar.sass';
import * as throttle from 'lodash.throttle';
export const HEADER_CONTAINER_UTIL_SELECTOR = '.navbar__list-item--container-selector .navbar__link-wrapper';
export const LANGUAGE_SELECT_UTIL_SELECTOR = '[uw-language-select]'; const HEADER_CONTAINER_INITIALIZED_CLASS = 'navbar-header-container--initialized';
const LANGUAGE_SELECT_INITIALIZED_CLASS = 'language-select--initialized';
@Utility({ @Utility({
selector: LANGUAGE_SELECT_UTIL_SELECTOR, selector: HEADER_CONTAINER_UTIL_SELECTOR,
}) })
export class LanguageSelectUtil { export class NavHeaderContainerUtil {
_element; _element;
checkbox; radioButton;
closeButton;
container;
wasOpen;
_throttleUpdateWasOpen;
constructor(element) { constructor(element) {
if (!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)) { if (element.classList.contains(HEADER_CONTAINER_INITIALIZED_CLASS)) {
return false; return;
} }
this._element = element; 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) { start() {
if (!this._element.contains(event.target) && window.document.contains(event.target)) { if (!this.container)
this.checkbox.checked = false; 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() { close() {
// TODO this.radioButton.checked = false;
this.throttleUpdateWasOpen();
} }
isOpen() {
return this.radioButton.checked;
}
updateWasOpen() {
this.wasOpen = this.isOpen();
}
destroy() { /* TODO */ }
} }
export const NavbarUtils = [ export const NavbarUtils = [
LanguageSelectUtil, NavHeaderContainerUtil,
]; ];

View File

@ -1,49 +1,137 @@
.navbar-container .navbar-container
position: relative 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 .navbar
position: fixed position: fixed
display: flex
flex-direction: row
align-items: center
justify-content: flex-start
right: 0 right: 0
top: 0 top: 0
left: var(--asidenav-width-xl) left: var(--asidenav-width-xl)
height: var(--header-height) min-height: var(--header-height)
background-color: var(--color-primary) background-color: var(--color-primary)
color: white color: white
z-index: 20 z-index: 22
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2) box-shadow: 0 0 4px rgba(0, 0, 0, 0.2)
overflow: auto overflow: auto
transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1) transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
margin: 0
padding: 10px 0
@media (max-width: 1199px) @media (max-width: 1199px)
.navbar
left: var(--asidenav-width-lg) left: var(--asidenav-width-lg)
@media (max-width: 768px) @media (max-width: 768px), (max-height: 500px)
.navbar min-height: var(--header-height-collapsed)
padding: 0
@media (max-width: 768px)
left: 0 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 // links
.navbar__link-wrapper .navbar__link-wrapper
display: flex display: flex
@ -57,6 +145,10 @@
overflow: hidden overflow: hidden
cursor: pointer cursor: pointer
@media (max-width: 768px), (max-height: 500px)
height: var(--header-height-collapsed)
.navbar__link-icon .navbar__link-icon
opacity: 0.7 opacity: 0.7
transition: opacity 0.2s ease transition: opacity 0.2s ease
@ -67,12 +159,13 @@
padding: 2px 4px padding: 2px 4px
text-transform: uppercase text-transform: uppercase
font-weight: 600 font-weight: 600
font-size: 16px
@media (min-width: 769px) @media (min-width: 769px) and (min-height: 501px)
.navbar__link-wrapper .navbar__link-wrapper
border: 1px solid rgba(255, 255, 255, 0.7) border: 1px solid rgba(255, 255, 255, 0.7)
@media (max-width: 768px) @media (max-width: 768px), (max-height: 500px)
.navbar__link-wrapper .navbar__link-wrapper
box-shadow: none box-shadow: none
min-width: 0 min-width: 0
@ -86,72 +179,56 @@
transform: scale(0.65) transform: scale(0.65)
margin-bottom: 0 margin-bottom: 0
// navbar list .navbar__container-link
.navbar__list 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 white-space: nowrap
+ .navbar__list &:not(.navbar__container-link--active):hover
margin-left: 12px background-color: var(--color-dark) !important
color: var(--color-lightwhite) !important
@media (min-width: 769px) &.navbar__container-link--active
.navbar__list:last-of-type background-color: var(--color-lightwhite) !important
padding-right: 40px color: var(--color-dark) !important
@media (max-width: 768px)
.navbar__list
+ .navbar__list
margin-left: 0
padding-right: 40px
// list item // list item
.navbar__list-item .navbar__list-item
position: relative position: relative
transition: background-color .1s ease transition: background-color .1s ease
&:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper & + .navbar__list-item
margin-left: 12px
&:not(.navbar__list-item--favorite) + .navbar__list-item
margin-left: 12px margin-left: 12px
@media (max-width: 500px) @media (max-width: 500px)
.navbar__list-item .navbar__list-item
min-width: 60px min-width: 60px
&:not(.navbar__list-item--favorite) + .navbar__list-item & + .navbar__list-item
margin-left: 0 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 // "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) @media (min-width: 426px)
.logged-in .navbar__list-item--favorite
.navbar__list display: none !important
.navbar__list-item--favorite
display: none !important & + .navbar__list-item
margin-left: 0
.navbar__list-item--active .navbar__list-item--active
background-color: var(--color-lightwhite) background-color: var(--color-lightwhite)
@ -163,7 +240,7 @@
.navbar__list-item--active .navbar__link-wrapper .navbar__list-item--active .navbar__link-wrapper
color: var(--color-dark) 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) background-color: var(--color-dark)
color: var(--color-lightwhite) color: var(--color-lightwhite)
@ -186,43 +263,5 @@
display: block display: block
height: var(--header-height-collapsed) height: var(--header-height-collapsed)
@media (max-width: 768px) .navbar__container-radio--none, .navbar__container-radio
.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
display: none 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)

View File

@ -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,
];

View File

@ -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)

View File

@ -11,6 +11,7 @@ import { Modal } from './modal/modal';
import { Tooltip } from './tooltips/tooltips'; import { Tooltip } from './tooltips/tooltips';
import { CourseTeaser } from './course-teaser/course-teaser'; import { CourseTeaser } from './course-teaser/course-teaser';
import { NavbarUtils } from './navbar/navbar'; import { NavbarUtils } from './navbar/navbar';
import { PageActionsUtils } from './pageactions/pageactions';
import { HideColumns } from './hide-columns/hide-columns'; import { HideColumns } from './hide-columns/hide-columns';
import { ExamCorrect } from './exam-correct/exam-correct'; import { ExamCorrect } from './exam-correct/exam-correct';
import { SortTable } from './sort-table/sort-table'; import { SortTable } from './sort-table/sort-table';
@ -30,6 +31,7 @@ export const Utils = [
Tooltip, Tooltip,
CourseTeaser, CourseTeaser,
...NavbarUtils, ...NavbarUtils,
...PageActionsUtils,
HideColumns, HideColumns,
ExamCorrect, ExamCorrect,
SortTable, SortTable,

View File

@ -494,7 +494,7 @@ CorrectorsPlaceholder: Korrektoren...
CorrectorsDefaulted: Korrektoren-Liste wurde aus bisherigen Übungsblättern diesen Kurses generiert. Es sind keine Daten gespeichert. CorrectorsDefaulted: Korrektoren-Liste wurde aus bisherigen Übungsblättern diesen Kurses generiert. Es sind keine Daten gespeichert.
Users: Benutzer Users: Benutzer
HomeHeading: Aktuelle Termine NewsHeading: Aktuelles
LoginHeading: Authentifizierung LoginHeading: Authentifizierung
LoginTitle: Authentifizierung LoginTitle: Authentifizierung
ProfileHeading: Benutzereinstellungen ProfileHeading: Benutzereinstellungen
@ -509,9 +509,9 @@ NotificationSettingsHeading displayName@Text: Benachrichtigungs-Einstellungen f
TokensLastReset: Tokens zuletzt invalidiert TokensLastReset: Tokens zuletzt invalidiert
TokensResetSuccess: Authorisierungs-Tokens invalidiert TokensResetSuccess: Authorisierungs-Tokens invalidiert
HomeOpenAllocations: Offene Zentralanmeldungen NewsOpenAllocations: Offene Zentralanmeldungen
HomeUpcomingSheets: Anstehende Übungsblätter NewsUpcomingSheets: Anstehende Übungsblätter
HomeUpcomingExams: Bevorstehende Prüfungen NewsUpcomingExams: Bevorstehende Prüfungen
NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"} NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"}
CloseAlert: Schliessen 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. 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 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 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 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. 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 MenuOpenCourses: Kurse mit offener Registrierung
MenuOpenAllocations: Aktive Zentralanmeldungen MenuOpenAllocations: Aktive Zentralanmeldungen
MenuHome: Aktuell MenuNews: Aktuell
MenuInformation: Informationen MenuInformation: Informationen
MenuLegal: Rechtliche Informationen MenuLegal: Rechtliche Informationen
MenuDataProt: Datenschutzerklärung MenuDataProt: Datenschutzerklärung
@ -1186,7 +1186,7 @@ MenuTutorialEdit: Tutorium editieren
MenuTutorialComm: Mitteilung an Teilnehmer MenuTutorialComm: Mitteilung an Teilnehmer
MenuExamList: Prüfungen MenuExamList: Prüfungen
MenuExamNew: Neue Prüfung anlegen MenuExamNew: Neue Prüfung anlegen
MenuExamEdit: Bearbeiten MenuExamEdit: Prüfung bearbeiten
MenuExamUsers: Teilnehmer MenuExamUsers: Teilnehmer
MenuExamGrades: Prüfungsleistungen MenuExamGrades: Prüfungsleistungen
MenuExamAddMembers: Prüfungsteilnehmer hinzufügen MenuExamAddMembers: Prüfungsteilnehmer hinzufügen
@ -1272,8 +1272,8 @@ BreadcrumbExternalExamGrades: Prüfungsleistungen
BreadcrumbExternalExamStaffInvite: Einladung zum Prüfer BreadcrumbExternalExamStaffInvite: Einladung zum Prüfer
BreadcrumbParticipantsList: Kursteilnehmerlisten BreadcrumbParticipantsList: Kursteilnehmerlisten
BreadcrumbParticipants: Kursteilnehmerliste BreadcrumbParticipants: Kursteilnehmerliste
BreadcrumbStorageKey: Lokalen Schlüssel generieren
BreadcrumbExamAutoOccurrence: Automatische Termin-/Raumverteilung BreadcrumbExamAutoOccurrence: Automatische Termin-/Raumverteilung
BreadcrumbStorageKey: Lokalen Schlüssel generieren
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn} ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn}
ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{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. ExamRegistrationInviteExplanation: Sie wurden eingeladen, Prüfungsteilnehmer zu sein.
ExamCorrectHeading examname@Text: Prüfungsergebnisse für #{examname} eintragen ExamCorrectHeading examname@Text: Prüfungsergebnisse für #{examname} eintragen
ExamCorrectExamResultDelete: Prüfungsergebnis löschen
ExamCorrectHeadDate: Zeit ExamCorrectHeadDate: Zeit
ExamCorrectHeadParticipant: Teilnehmer 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} ExamCorrectHeadPart exampartnum@ExamPartNumber: #{exampartnum}
ExamCorrectHeadPartName exampartname@ExamPartName: #{exampartname} ExamCorrectHeadPartName exampartname@ExamPartName: #{exampartname}
ExamCorrectHeadStatus: Status ExamCorrectHeadStatus: Status
@ -1411,6 +1411,8 @@ ExamCorrectButtonSend: Senden
ExamCorrectErrorMultipleMatchingParticipants: Dem Identifikator konnten mehrere Prüfungsteilnehmer zugeordnet werden. ExamCorrectErrorMultipleMatchingParticipants: Dem Identifikator konnten mehrere Prüfungsteilnehmer zugeordnet werden.
ExamCorrectErrorNoMatchingParticipants: Dem Identifikator konnte kein 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 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 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 ExamPublishOccurrenceAssignments: Termin- bzw. Raumzuteilung den Teilnehmern mitteilen um
ExamPublishOccurrenceAssignmentsTip: Ab diesem Zeitpunkt Teilnehmer einsehen zu welcher Teilprüfung bzw. welchen Raum sie angemeldet sind ExamPublishOccurrenceAssignmentsTip: Ab diesem Zeitpunkt Teilnehmer einsehen zu welcher Teilprüfung bzw. welchen Raum sie angemeldet sind
ExamPublishOccurrenceAssignmentsParticipant: Termin- bzw. Raumzuteilung einsehbar ab ExamPublishOccurrenceAssignmentsParticipant: Termin- bzw. Raumzuteilung einsehbar ab
ExamFinished: Bewertung abgeschlossen ab ExamFinished: Ergebnisse sichtbar ab
ExamFinishedOffice: Noten bekannt gegeben ExamFinishedOffice: Noten bekannt gegeben
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen ExamFinishedParticipant: Bewertung voraussichtlich abgeschlossen
ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden
ExamClosed: Noten gemeldet ExamClosed: Noten gemeldet
ExamClosedTip: Prüfungsbeauftraget, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert 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 ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten ExamBonusPoints': Umrechnung von Übungspunkten
ExamBonusManual': Manuelle Berechnung 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 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 ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen
ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "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 ExamEndMustBeAfterStart: Beginn der Prüfung muss vor ihrem Ende liegen
ExamFinishedMustBeAfterEnd: "Bewertung abgeschlossen ab" muss nach Ende liegen ExamFinishedMustBeAfterEnd: "Ergebnisse sichtbar ab" muss nach Ende liegen
ExamFinishedMustBeAfterStart: "Bewertung abgeschlossen ab" muss nach Start liegen ExamFinishedMustBeAfterStart: "Ergebnisse sichtbar ab" muss nach Beginn liegen
ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Bewertung abgeschlossen ab" liegen ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Ergebnisse sichtbar ab" liegen
ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Start liegen ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Beginn liegen
ExamClosedMustBeAfterEnd: "Noten stehen fest ab" muss nach Ende liegen ExamClosedMustBeAfterEnd: "Noten stehen fest ab" muss nach Ende liegen
ExamOccurrenceEndMustBeAfterStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss vor seinem Ende liegen ExamOccurrenceEndMustBeAfterStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss vor seinem Ende liegen

View File

@ -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. CorrectorsDefaulted: List of correctors was automatically generated based on those of preceding sheets for this course. No data has been saved, yet.
Users: Users Users: Users
HomeHeading: Home NewsHeading: News
LoginHeading: Authentication LoginHeading: Authentication
LoginTitle: Authentication LoginTitle: Authentication
ProfileHeading: Settings ProfileHeading: Settings
@ -507,9 +507,9 @@ NotificationSettingsHeading displayName: Notification settings for #{displayName
TokensLastReset: Tokens last reset TokensLastReset: Tokens last reset
TokensResetSuccess: Successfully invalidated all authorisation tokens TokensResetSuccess: Successfully invalidated all authorisation tokens
HomeOpenAllocations: Active central allocations NewsOpenAllocations: Active central allocations
HomeUpcomingSheets: Upcoming exercise sheets NewsUpcomingSheets: Upcoming exercise sheets
HomeUpcomingExams: Upcoming exams NewsUpcomingExams: Upcoming exams
NumCourses num: #{num} #{pluralEN num "course" "courses"} NumCourses num: #{num} #{pluralEN num "course" "courses"}
CloseAlert: Close CloseAlert: Close
@ -1114,7 +1114,7 @@ InvalidRoute: Could not interpret url
MenuOpenCourses: Courses with open registration MenuOpenCourses: Courses with open registration
MenuOpenAllocations: Active central allocations MenuOpenAllocations: Active central allocations
MenuHome: Home MenuNews: News
MenuInformation: Information MenuInformation: Information
MenuLegal: Legal MenuLegal: Legal
MenuDataProt: Data protection MenuDataProt: Data protection
@ -1185,7 +1185,7 @@ MenuTutorialEdit: Edit tutorial
MenuTutorialComm: Send course message MenuTutorialComm: Send course message
MenuExamList: Exams MenuExamList: Exams
MenuExamNew: Create new exam MenuExamNew: Create new exam
MenuExamEdit: Edit MenuExamEdit: Edit exam
MenuExamUsers: Participants MenuExamUsers: Participants
MenuExamGrades: Exam results MenuExamGrades: Exam results
MenuExamAddMembers: Add exam participants MenuExamAddMembers: Add exam participants
@ -1271,8 +1271,8 @@ BreadcrumbExternalExamGrades: Exam results
BreadcrumbExternalExamStaffInvite: Invitation BreadcrumbExternalExamStaffInvite: Invitation
BreadcrumbParticipantsList: Lists of course participants BreadcrumbParticipantsList: Lists of course participants
BreadcrumbParticipants: Course participants BreadcrumbParticipants: Course participants
BreadcrumbStorageKey: Generate storage key
BreadcrumbExamAutoOccurrence: Automatic occurrence/room distribution BreadcrumbExamAutoOccurrence: Automatic occurrence/room distribution
BreadcrumbStorageKey: Generate storage key
ExternalExamEdit coursen examn: Edit: #{coursen}, #{examn} ExternalExamEdit coursen examn: Edit: #{coursen}, #{examn}
ExternalExamGrades coursen examn: Exam achievements: #{coursen}, #{examn} ExternalExamGrades coursen examn: Exam achievements: #{coursen}, #{examn}
@ -1400,7 +1400,6 @@ ExamCorrectHeading examname: Submit corrections for #{examname}
ExamCorrectHeadDate: Time ExamCorrectHeadDate: Time
ExamCorrectHeadParticipant: Participant 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} ExamCorrectHeadPart exampartnum: #{exampartnum}
ExamCorrectHeadPartName exampartname: #{exampartname} ExamCorrectHeadPartName exampartname: #{exampartname}
ExamCorrectHeadStatus: Status ExamCorrectHeadStatus: Status
@ -1409,6 +1408,10 @@ ExamCorrectButtonSend: Submit
ExamCorrectErrorMultipleMatchingParticipants: This identifier matches on multiple exam participants. ExamCorrectErrorMultipleMatchingParticipants: This identifier matches on multiple exam participants.
ExamCorrectErrorNoMatchingParticipants: This identifier does not match on any exam participant. 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} SubmissionUserInvitationAccepted shn: You now participate in a submission for #{shn}
SubmissionUserInvitationDeclined shn: You have declined the invitation to 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 ExamPublishOccurrenceAssignments: Publish occurrence/room-assignments
ExamPublishOccurrenceAssignmentsTip: At this time participants are informed to which occurrence/room they are assigned ExamPublishOccurrenceAssignmentsTip: At this time participants are informed to which occurrence/room they are assigned
ExamPublishOccurrenceAssignmentsParticipant: Occurrence/room-assignments published ExamPublishOccurrenceAssignmentsParticipant: Occurrence/room-assignments published
ExamFinished: Marking finished ExamFinished: Results visible from
ExamFinishedOffice: Exam achievements published ExamFinishedOffice: Exam achievements published
ExamFinishedParticipant: Marking expected to be finished ExamFinishedParticipant: Marking expected to be finished
ExamFinishedTip: At this participants are informed of their exam achievements ExamFinishedTip: At this participants are informed of their exam achievements
@ -1578,7 +1581,6 @@ ExamBonusRule: Bonus points from exercises
ExamNoBonus': No automatic exam bonus ExamNoBonus': No automatic exam bonus
ExamBonusPoints': Compute from exercise achievements ExamBonusPoints': Compute from exercise achievements
ExamBonusManual': Manual computation 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 ExamRegisterForOccurrence: Registration for this exam is done by registering for an occurrence/room
@ -1631,6 +1633,7 @@ ExamFormCorrection: Correction
ExamFormParts: Exam parts ExamFormParts: Exam parts
ExamCorrectors: Correctors 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 ExamCorrectorAlreadyAdded: A corrector with this email address already exists
ExamParts: Exam parts/questions ExamParts: Exam parts/questions
@ -1672,9 +1675,9 @@ ExamRegisterToMustBeAfterRegisterFrom: "Register to" must be after "register fro
ExamDeregisterUntilMustBeAfterRegisterFrom: "Deregister until" must be after "register from" ExamDeregisterUntilMustBeAfterRegisterFrom: "Deregister until" must be after "register from"
ExamStartMustBeAfterPublishOccurrenceAssignments: "Start" must be after "publish occurrence/room-assignments" ExamStartMustBeAfterPublishOccurrenceAssignments: "Start" must be after "publish occurrence/room-assignments"
ExamEndMustBeAfterStart: "End" must be after "start" ExamEndMustBeAfterStart: "End" must be after "start"
ExamFinishedMustBeAfterEnd: "Marking finished" must be after "end" ExamFinishedMustBeAfterEnd: "Results visible from" must be after "end"
ExamFinishedMustBeAfterStart: "Marking finished" must be after "start" ExamFinishedMustBeAfterStart: "Results visible from" must be after "start"
ExamClosedMustBeAfterFinished: "Exam achievements registered" must be after "marking finished" ExamClosedMustBeAfterFinished: "Exam achievements registered" must be after "results visible from"
ExamClosedMustBeAfterStart: "Exam achievements registered" must be after "start" ExamClosedMustBeAfterStart: "Exam achievements registered" must be after "start"
ExamClosedMustBeAfterEnd: "Exam achievements registered" must be after "end" ExamClosedMustBeAfterEnd: "Exam achievements registered" must be after "end"

View File

@ -789,5 +789,18 @@
"usedIds": [] "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": []
}
}
] ]
} }

4
routes
View File

@ -41,7 +41,7 @@
/metrics MetricsR GET /metrics MetricsR GET
/ HomeR GET !free / NewsR GET !free
/users UsersR GET POST -- no tags, i.e. admins only /users UsersR GET POST -- no tags, i.e. admins only
/users/#CryptoUUIDUser AdminUserR GET POST /users/#CryptoUUIDUser AdminUserR GET POST
/users/#CryptoUUIDUser/delete AdminUserDeleteR POST /users/#CryptoUUIDUser/delete AdminUserDeleteR POST
@ -189,8 +189,8 @@
/register ERegisterR POST !timeANDcourse-registeredAND¬exam-registered !timeANDexam-registeredAND¬exam-result /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 /register/#ExamOccurrenceName ERegisterOccR POST !exam-occurrence-registrationANDtimeANDcapacityANDcourse-registeredAND¬exam-occurrence-registered !exam-occurrence-registrationANDtimeANDexam-occurrence-registeredAND¬exam-result
/grades EGradesR GET POST !exam-office /grades EGradesR GET POST !exam-office
/correct ECorrectR GET POST !exam-correctorANDtime
/assign-occurrences EAutoOccurrenceR POST /assign-occurrences EAutoOccurrenceR POST
/correct ECorrectR GET POST !exam-correctorANDtime
/apps CApplicationsR GET POST /apps CApplicationsR GET POST
!/apps/files CAppsFilesR GET !/apps/files CAppsFilesR GET
/apps/#CryptoFileNameCourseApplication CourseApplicationR: /apps/#CryptoFileNameCourseApplication CourseApplicationR:

View File

@ -101,7 +101,7 @@ import Data.List (cycle)
-- Import all relevant handler modules here. -- Import all relevant handler modules here.
-- (HPack takes care to add new modules to our cabal file nowadays.) -- (HPack takes care to add new modules to our cabal file nowadays.)
import Handler.Home import Handler.News
import Handler.Info import Handler.Info
import Handler.Help import Handler.Help
import Handler.Profile import Handler.Profile

File diff suppressed because it is too large Load Diff

View File

@ -168,7 +168,7 @@ instance RenderMessage UniWorX MsgLanguage where
| ("en" : _) <- lang' = mr MsgEnglish | ("en" : _) <- lang' = mr MsgEnglish
| otherwise = lang | otherwise = lang
where where
mr = renderMessage foundation ls mr = renderMessage foundation $ lang : filter (/= lang) ls
embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>) embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel

View File

@ -11,5 +11,5 @@ import Handler.Exam.Edit as Handler.Exam
import Handler.Exam.Show as Handler.Exam import Handler.Exam.Show as Handler.Exam
import Handler.Exam.Users as Handler.Exam import Handler.Exam.Users as Handler.Exam
import Handler.Exam.AddUser 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.AutoOccurrence as Handler.Exam
import Handler.Exam.Correct as Handler.Exam

View File

@ -38,7 +38,8 @@ data CorrectInterfaceResponse
, ciraMessage :: Text , ciraMessage :: Text
} }
| CorrectInterfaceResponseFailure | CorrectInterfaceResponseFailure
{ cirfMessage :: Text { cirfUser :: Maybe CorrectInterfaceUser
, cirfMessage :: Text
} }
| CorrectInterfaceResponseNoOp | CorrectInterfaceResponseNoOp
{ cirnUsers :: Set CorrectInterfaceUser { cirnUsers :: Set CorrectInterfaceUser
@ -84,8 +85,6 @@ getECorrectR tid ssh csh examn = do
name <- newIdent name <- newIdent
fieldView (pointsField :: Field Handler Points) ("exam-correct__" <> toPathPiece n) name [("uw-exam-correct--part-input", toPathPiece n)] (Left "") False 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 :: [ExamGrade]
examGrades = universeF examGrades = universeF
@ -93,6 +92,7 @@ getECorrectR tid ssh csh examn = do
siteLayoutMsg heading $ do siteLayoutMsg heading $ do
setTitleI heading setTitleI heading
let examCorrectExplanation = $(i18nWidgetFile "exam-correct-explanation")
$(widgetFile "exam-correct") $(widgetFile "exam-correct")
@ -102,11 +102,11 @@ postECorrectR tid ssh csh examn = do
CorrectInterfaceRequest{..} <- requireCheckJsonBody CorrectInterfaceRequest{..} <- requireCheckJsonBody
response <- runDB $ do response <- exceptT return return . hoist runDB $ do
Entity eId Exam{..} <- fetchExam tid ssh csh examn Entity eId Exam{..} <- lift $ fetchExam tid ssh csh examn
euid <- traverse decrypt ciqUser 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.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId
@ -149,10 +149,10 @@ postECorrectR tid ssh csh examn = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
newExamPartResults <- if newExamPartResults <- if
| Just results <- ciqResults -> iforM (toNullable results) $ \partNumber mPartResult -> do | Just results <- ciqResults -> iforM (toNullable results) $ \partNumber mPartResult -> do
(Entity examPartId ExamPart{..}) <- getBy404 $ UniqueExamPartNumber eId partNumber (Entity examPartId ExamPart{..}) <- lift . getBy404 $ UniqueExamPartNumber eId partNumber
mOldResult <- getBy $ UniqueExamPartResult examPartId uid mOldResult <- lift . getBy $ UniqueExamPartResult examPartId uid
if if
| Just (Entity oldId _) <- mOldResult, is _Nothing mPartResult -> do | Just (Entity oldId _) <- mOldResult, is _Nothing mPartResult -> lift $ do
delete oldId delete oldId
audit $ TransactionExamPartResultDeleted examPartId uid audit $ TransactionExamPartResultDeleted examPartId uid
return Nothing return Nothing
@ -161,29 +161,36 @@ postECorrectR tid ssh csh examn = do
mNew = ExamAttended <$> mPartResult mNew = ExamAttended <$> mPartResult
resultVal = _entityVal . _examPartResultResult resultVal = _entityVal . _examPartResultResult
in if in if
| mOld /= mNew -> let | mOld /= mNew -> do
-- cut off part results that exceed the maximum number of points for this exam part for now let
-- TODO answer with new failure response type instead partResultAcceptable = 0 <= partResult
partResult' = if && maybe True (partResult <=) examPartMaxPoints
| Just maxPts <- examPartMaxPoints, maxPts < partResult -> maxPts guardMExceptT partResultAcceptable $
| otherwise -> partResult let
in do msg | Just maxPoints <- examPartMaxPoints
newExamPartResult <- upsert ExamPartResult = MsgExamCorrectErrorPartResultOutOfBoundsMax partNumber maxPoints
| otherwise
= MsgExamCorrectErrorPartResultOutOfBounds partNumber
in CorrectInterfaceResponseFailure
<$> (Just <$> userToResponse match)
<*> (getMessageRender <*> pure msg)
newExamPartResult <- lift $ upsert ExamPartResult
{ examPartResultExamPart = examPartId { examPartResultExamPart = examPartId
, examPartResultUser = uid , examPartResultUser = uid
, examPartResultResult = ExamAttended partResult' , examPartResultResult = ExamAttended partResult
, examPartResultLastChanged = now , examPartResultLastChanged = now
} }
[ ExamPartResultResult =. ExamAttended partResult' [ ExamPartResultResult =. ExamAttended partResult
, ExamPartResultLastChanged =. now , ExamPartResultLastChanged =. now
] ]
audit $ TransactionExamPartResultEdit examPartId uid lift . audit $ TransactionExamPartResultEdit examPartId uid
return $ newExamPartResult ^? resultVal return $ newExamPartResult ^? resultVal
| otherwise -> return $ mOldResult ^? _Just . resultVal | otherwise -> return $ mOldResult ^? _Just . resultVal
| otherwise -> return Nothing | otherwise -> return Nothing
| otherwise -> return mempty | otherwise -> return mempty
newExamResult <- do newExamResult <- lift $ do
mOldResult <- getBy $ UniqueExamResult eId uid mOldResult <- getBy $ UniqueExamResult eId uid
if if
| Just (Entity oldId _) <- mOldResult, is _Nothing ciqGrade -> do | 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 -- on match with no exam participant, answer with 400
| [] <- participantMatches -> return CorrectInterfaceResponseFailure | [] <- 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 -- on match with multiple exam participants, answer with 400 and a set of all matches

View File

@ -82,6 +82,7 @@ getEShowR tid ssh csh examn = do
examClosedShown = lecturerInfoShown examClosedShown = lecturerInfoShown
showCloseWidget = lecturerInfoShown showCloseWidget = lecturerInfoShown
showAutoOccurrenceCalculateWidget = lecturerInfoShown showAutoOccurrenceCalculateWidget = lecturerInfoShown
examFinishedMsg = if lecturerInfoShown then MsgExamFinished else MsgExamFinishedParticipant
sumMaxPoints = sum [ fromRational examPartWeight * mPoints | Entity _ ExamPart{..} <- examParts, mPoints <- examPartMaxPoints ^.. _Just ] sumMaxPoints = sum [ fromRational examPartWeight * mPoints | Entity _ ExamPart{..} <- examParts, mPoints <- examPartMaxPoints ^.. _Just ]

View File

@ -1089,5 +1089,6 @@ postEUsersR tid ssh csh examn = do
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do
setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading 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") $(widgetFile "exam-users")

View File

@ -441,4 +441,5 @@ postEGradesR tid ssh csh examn = do
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading) $ do siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading) $ do
setTitleI $ prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading setTitleI $ prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading
let examGradesExplanation = notificationWidget NotificationBroad Info $(i18nWidgetFile "exam-office/exam-grades-explanation")
$(widgetFile "exam-office/exam-results") $(widgetFile "exam-office/exam-results")

View File

@ -30,4 +30,5 @@ postEEGradesR tid ssh coursen examn = do
siteLayoutMsg (MsgExternalExamGrades coursen examn) $ do siteLayoutMsg (MsgExternalExamGrades coursen examn) $ do
setTitleI MsgBreadcrumbExternalExamGrades setTitleI MsgBreadcrumbExternalExamGrades
let examGradesExplanation = notificationWidget NotificationBroad Info $(i18nWidgetFile "exam-office/exam-grades-explanation")
$(widgetFile "exam-office/externalExamGrades") $(widgetFile "exam-office/externalExamGrades")

View File

@ -80,7 +80,7 @@ examOfficeUserInvitationConfig = InvitationConfig{..}
return res return res
invitationSuccessMsg _ _ = invitationSuccessMsg _ _ =
return $ SomeMessage MsgExamOfficeUserInvitationAccepted return $ SomeMessage MsgExamOfficeUserInvitationAccepted
invitationUltDest _ _ = return $ SomeRoute HomeR invitationUltDest _ _ = return $ SomeRoute NewsR
makeExamOfficeUsersForm :: Maybe (Set (Either UserEmail UserId)) -> Form (Set (Either UserEmail UserId)) makeExamOfficeUsersForm :: Maybe (Set (Either UserEmail UserId)) -> Form (Set (Either UserEmail UserId))

View File

@ -1,4 +1,4 @@
module Handler.Home where module Handler.News where
import Import import Import
@ -9,21 +9,25 @@ import Database.Esqueleto.Utils.TH
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
getHomeR :: Handler Html getNewsR :: Handler Html
getHomeR = do getNewsR = do
muid <- maybeAuthId muid <- maybeAuthId
defaultLayout $ do defaultLayout $ do
setTitleI MsgHomeHeading setTitleI MsgNewsHeading
when (is _Nothing muid) $
notificationWidget NotificationBroad Info $(i18nWidgetFile "pitch")
case muid of case muid of
Just uid -> do Just uid -> do
homeUpcomingExams uid newsUpcomingExams uid
homeUpcomingSheets uid newsUpcomingSheets uid
Nothing -> Nothing ->
$(i18nWidgetFile "unauth-home") $(i18nWidgetFile "unauth-news")
homeUpcomingSheets :: UserId -> Widget newsUpcomingSheets :: UserId -> Widget
homeUpcomingSheets uid = do newsUpcomingSheets uid = do
cTime <- liftIO getCurrentTime cTime <- liftIO getCurrentTime
let tableData :: E.LeftOuterJoin let tableData :: E.LeftOuterJoin
(E.InnerJoin (E.InnerJoin (E.SqlExpr (Entity CourseParticipant)) (E.SqlExpr (Entity Course))) (E.SqlExpr (Entity Sheet))) (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 , dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing , dbtCsvDecode = Nothing
} }
$(widgetFile "home/upcomingSheets") $(widgetFile "news/upcomingSheets")
homeUpcomingExams :: UserId -> Widget newsUpcomingExams :: UserId -> Widget
homeUpcomingExams uid = do newsUpcomingExams uid = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
((Any hasExams, examTable), warningDays) <- liftHandler . runDB $ do ((Any hasExams, examTable), warningDays) <- liftHandler . runDB $ do
User {userWarningDays} <- get404 uid User {userWarningDays} <- get404 uid
@ -255,6 +259,6 @@ homeUpcomingExams uid = do
(, userWarningDays) <$> dbTable examDBTableValidator examDBTable (, userWarningDays) <$> dbTable examDBTableValidator examDBTable
$(widgetFile "home/upcomingExams") $(widgetFile "news/upcomingExams")

View File

@ -850,17 +850,14 @@ postCsvOptionsR = do
, formAttrs = [ asyncSubmitAttr | isModal ] , formAttrs = [ asyncSubmitAttr | isModal ]
} }
postLangR :: Handler () postLangR :: Handler Void
postLangR = do postLangR = do
((langRes, _), _) <- runFormPost $ identifyForm FIDLanguage langForm requestedLang <- selectLanguage' appLanguages . hoistMaybe <$> lookupGlobalPostParam PostLanguage
lang' <- runDB . updateUserLanguage $ Just requestedLang
formResult langRes $ \(lang, route) -> do app <- getYesod
lang' <- runDB . updateUserLanguage $ Just lang let mr | Just lang'' <- lang' = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang''
| otherwise = renderMessage app []
app <- getYesod addMessage Success . toHtml $ mr MsgLanguageChanged
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
invalidArgs ["Language form required"] redirect . fromMaybe NewsR =<< lookupGlobalGetParam GetReferer

View File

@ -399,7 +399,7 @@ invitationR' InvitationConfig{..} = liftHandler $ do
Nothing -> do Nothing -> do
addMessageI Info MsgInvitationDeclined addMessageI Info MsgInvitationDeclined
deleteBy . UniqueInvitation itEmail $ invRef @junction fid deleteBy . UniqueInvitation itEmail $ invRef @junction fid
return . Just $ SomeRoute HomeR return . Just $ SomeRoute NewsR
Just (jData, formCtx) -> do Just (jData, formCtx) -> do
let junction = review _InvitableJunction (invitee, fid, jData) let junction = review _InvitableJunction (invitee, fid, jData)
mResult <- invitationInsertHook itEmail fEnt iData junction formCtx $ insertUniqueEntity junction mResult <- invitationInsertHook itEmail fEnt iData junction formCtx $ insertUniqueEntity junction

View File

@ -10,6 +10,7 @@ import Model.Submission as Import
import Model.Tokens as Import import Model.Tokens as Import
import Utils.Tokens as Import import Utils.Tokens as Import
import Utils.Frontend.Modal as Import import Utils.Frontend.Modal as Import
import Utils.Frontend.Notification as Import
import Utils.Lens as Import import Utils.Lens as Import
import Settings as Import import Settings as Import

View File

@ -630,6 +630,15 @@ assertM' f x = x <$ guard (f x)
guardOn :: Alternative m => Bool -> a -> m a guardOn :: Alternative m => Bool -> a -> m a
guardOn b x = x <$ guard b 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 -- Some Utility Functions from Agda.Utils.Monad
-- | Monadic if-then-else. -- | Monadic if-then-else.
ifM :: Monad m => m Bool -> m a -> m a -> m a ifM :: Monad m => m Bool -> m a -> m a -> m a

View File

@ -46,6 +46,7 @@ import Data.Scientific
import Data.Time.Clock (NominalDiffTime, nominalDay) import Data.Time.Clock (NominalDiffTime, nominalDay)
import Utils import Utils
import Utils.Frontend.Notification
-- import Utils.Message -- import Utils.Message
-- import Utils.PathPiece -- import Utils.PathPiece
-- import Utils.Route -- import Utils.Route
@ -869,26 +870,15 @@ wformMessage :: (MonadHandler m) => Message -> WForm m ()
wformMessage = void . aFormToWForm . aformMessage wformMessage = void . aFormToWForm . aformMessage
formMessage :: (MonadHandler m) => Message -> MForm m (FormResult (), FieldView site) formMessage :: (MonadHandler m) => Message -> MForm m (FormResult (), FieldView site)
formMessage Message{..} = do formMessage msg = do
return (FormSuccess (), FieldView return (FormSuccess (), FieldView
{ fvLabel = mempty { fvLabel = mempty
, fvTooltip = Nothing , fvTooltip = Nothing
, fvId = idFormMessageNoinput , fvId = idFormMessageNoinput
, fvErrors = Nothing , fvErrors = Nothing
, fvRequired = False , fvRequired = False
, fvInput = [whamlet| , fvInput = notification NotificationNarrow msg
$newline never
<div .notification .notification-#{toPathPiece messageStatus} .fa-#{maybe defaultIcon iconText messageIcon}>
<div .notification__content>
#{messageContent}
|]
}) })
where
defaultIcon = case messageStatus of
Success -> "check-circle"
Info -> "info-circle"
Warning -> "exclamation-circle"
Error -> "exclamation-triangle"
--------------------- ---------------------
-- Form evaluation -- -- Form evaluation --

View File

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

View File

@ -63,44 +63,85 @@ data Icon
| IconApplicationVeto | IconApplicationVeto
| IconApplicationFiles | IconApplicationFiles
| IconTooltipDefault | 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 :: Icon -> Text
iconText = \case iconText = \case
IconNew -> "seedling" IconNew -> "seedling"
IconOK -> "check" IconOK -> "check"
IconNotOK -> "times" IconNotOK -> "times"
IconWarning -> "exclamation" IconWarning -> "exclamation"
IconProblem -> "bolt" IconProblem -> "bolt"
IconVisible -> "eye" IconVisible -> "eye"
IconInvisible -> "eye-slash" IconInvisible -> "eye-slash"
IconCourse -> "graduation-cap" IconCourse -> "graduation-cap"
IconEnrolTrue -> "user-plus" IconEnrolTrue -> "user-plus"
IconEnrolFalse -> "user-slash" IconEnrolFalse -> "user-slash"
IconPlanned -> "cog" IconPlanned -> "cog"
IconAnnounce -> "bullhorn" IconAnnounce -> "bullhorn"
IconExam -> "poll-h" IconExam -> "poll-h"
IconExamRegisterTrue -> "calendar-check" IconExamRegisterTrue -> "calendar-check"
IconExamRegisterFalse -> "calendar-times" IconExamRegisterFalse -> "calendar-times"
IconCommentTrue -> "comment-alt" IconCommentTrue -> "comment-alt"
IconCommentFalse -> "comment-slash" -- comment-alt-slash is not available for free IconCommentFalse -> "comment-alt-slash"
IconLink -> "link" IconLink -> "link"
IconFileDownload -> "file-download" IconFileDownload -> "file-download"
IconFileUpload -> "file-upload" IconFileUpload -> "file-upload"
IconFileZip -> "file-archive" IconFileZip -> "file-archive"
IconFileCSV -> "file-csv" IconFileCSV -> "file-csv"
IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar) IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar)
IconSFTHint -> "life-ring" -- for SheetFileType only IconSFTHint -> "life-ring" -- for SheetFileType only
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope" IconEmail -> "envelope"
IconRegisterTemplate -> "file-alt" IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt" IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash" IconApplyFalse -> "trash"
IconNoCorrectors -> "user-slash" IconNoCorrectors -> "user-slash"
IconApplicationVeto -> "times" IconApplicationVeto -> "times"
IconApplicationFiles -> "file-alt" IconApplicationFiles -> "file-alt"
IconTooltipDefault -> "question-circle" 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 Universe Icon
instance Finite Icon instance Finite Icon

View File

@ -1,5 +1,6 @@
module Utils.Lens.TH module Utils.Lens.TH
( makeLenses_, makeClassyFor_ ( lensRules_
, makeLenses_, makeClassyFor_
, multifocusG, multifocusL , multifocusG, multifocusL
) where ) where

View File

@ -6,7 +6,7 @@ module Utils.Message
, addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget , addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget
, statusToUrgencyClass , statusToUrgencyClass
, Message(..) , Message(..)
, messageIconI , messageIconI, messageIconIHamlet, messageIconWidget
, messageI, messageIHamlet, messageFile, messageWidget, messageTooltip , messageI, messageIHamlet, messageFile, messageWidget, messageTooltip
) where ) where
@ -163,6 +163,15 @@ messageIHamlet ms iHamlet = do
let mi = Nothing let mi = Nothing
Message ms <$> withUrlRenderer (iHamlet $ toHtml . mr) <*> pure mi 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 :: MessageStatus -> FilePath -> ExpQ
addMessageFile mc tPath = [e|addMessageIHamlet mc $(ihamletFile tPath)|] addMessageFile mc tPath = [e|addMessageIHamlet mc $(ihamletFile tPath)|]
@ -189,6 +198,15 @@ messageWidget mc wgt = do
PageContent{pageBody} <- liftHandler $ widgetToPageContent wgt PageContent{pageBody} <- liftHandler $ widgetToPageContent wgt
messageIHamlet mc (const pageBody :: HtmlUrlI18n (SomeMessage site) (Route site)) 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 :: MonadHandler m => m [Message]
getMessages = fmap decodeMessage <$> ClassyPrelude.Yesod.getMessages getMessages = fmap decodeMessage <$> ClassyPrelude.Yesod.getMessages

View File

@ -58,6 +58,7 @@ data GlobalPostParam = PostFormIdentifier
| PostDBCsvImportAction | PostDBCsvImportAction
| PostLoginDummy | PostLoginDummy
| PostExamAutoOccurrencePrevious | PostExamAutoOccurrencePrevious
| PostLanguage
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Universe GlobalPostParam instance Universe GlobalPostParam

View File

@ -1,5 +1,11 @@
$newline never $newline never
$if not isModal $if not isModal
$with containers <- filter isNavHeaderContainer nav
$if not (null containers)
<input name=nav-container type=radio .navbar__container-radio--none checked #container-radio-none>
$forall (_, containerIdent, _, _) <- containers
<input name=nav-container type=radio .navbar__container-radio ##{containerIdent}-radio>
<!-- secondary navigation at the side --> <!-- secondary navigation at the side -->
^{asidenav} ^{asidenav}
<!-- navigation --> <!-- navigation -->
@ -15,7 +21,7 @@ $if not isModal
$if not isModal $if not isModal
<!-- breadcrumbs --> <!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute $if not $ Just NewsR == mcurrentRoute
^{breadcrumbsWgt} ^{breadcrumbsWgt}
<div .main__content-body> <div .main__content-body>
@ -26,7 +32,7 @@ $if not isModal
<a .breadcrumbs__link href="@{fst back}">#{snd back} --> <a .breadcrumbs__link href="@{fst back}">#{snd back} -->
^{headline} ^{headline}
$if not isModal && hasPageActions $if hasPageActions
<!-- page actions --> <!-- page actions -->
^{pageaction} ^{pageaction}

View File

@ -1,24 +1,18 @@
$newline never $newline never
<section>
^{examCorrectExplanation}
<section> <section>
<div uw-hide-columns="exam-correct" .scrolltable .scrolltable--bordered> <div uw-hide-columns="exam-correct" .scrolltable .scrolltable--bordered>
<table .table .table--striped table--hover uw-exam-correct=#{toPathPiece examCorrectIdent} uw-sort-table=exam-correct-#{toPathPiece examCorrectIdent}> <table .table .table--striped .table--hover uw-exam-correct=#{toPathPiece examCorrectIdent} uw-sort-table=exam-correct-#{toPathPiece examCorrectIdent} uw-no-check-all>
<thead> <thead>
<tr .table__row .table__row--head> <tr .table__row .table__row--head>
<th .table__th .uw-exam-correct--date-cell uw-exam-correct-header="date" uw-hide-column-header="date"> <th .table__th .uw-exam-correct--date-cell uw-exam-correct-header="date" uw-hide-column-header="date">
_{MsgExamCorrectHeadDate} _{MsgExamCorrectHeadDate}
<th .table__th .uw-exam-correct--user-cell uw-exam-correct-header="user" uw-hide-column-header="user"> <th .table__th .uw-exam-correct--user-cell uw-exam-correct-header="user" uw-hide-column-header="user">
_{MsgExamCorrectHeadParticipant} _{MsgExamCorrectHeadParticipant}
^{iconTooltip participantHeadTooltip Nothing True} $forall ExamPart{examPartNumber} <- examParts
$forall ExamPart{examPartNumber,examPartName} <- examParts
<th .table__th .uw-exam-correct--part-cell uw-exam-correct-header=#{examPartNumber} uw-hide-column-header=#{examPartNumber}> <th .table__th .uw-exam-correct--part-cell uw-exam-correct-header=#{examPartNumber} uw-hide-column-header=#{examPartNumber}>
$maybe name <- examPartName
<span .tooltip>
<span>
_{MsgExamCorrectHeadPart examPartNumber}
<span .tooltip__content>
_{MsgExamCorrectHeadPartName name}
$nothing
_{MsgExamCorrectHeadPart examPartNumber} _{MsgExamCorrectHeadPart examPartNumber}
$if mayEditResults $if mayEditResults
<th .table__th .uw-exam-correct--result-cell uw-exam-correct-header="result" uw-hide-column-header="result" colspan=2> <th .table__th .uw-exam-correct--result-cell uw-exam-correct-header="result" uw-hide-column-header="result" colspan=2>
@ -35,6 +29,8 @@ $newline never
$forall ExamPart{examPartNumber} <- examParts $forall ExamPart{examPartNumber} <- examParts
<td .table__td .uw-exam-correct--part-cell> <td .table__td .uw-exam-correct--part-cell>
^{ptsInput examPartNumber} ^{ptsInput examPartNumber}
<input #exam-correct__#{examPartNumber}--delete type="checkbox" style="display:none" uw-no-checkbox .uw-exam-correct--delete-exam-part>
<label for=exam-correct__#{examPartNumber}--delete .fas .fa-fw .fa-trash>
$if mayEditResults $if mayEditResults
<td .table__td #uw-exam-correct__result> <td .table__td #uw-exam-correct__result>
<select> <select>
@ -52,6 +48,8 @@ $newline never
_{MsgExamResultVoided} _{MsgExamResultVoided}
<option value="no-show"> <option value="no-show">
_{MsgExamResultNoShow} _{MsgExamResultNoShow}
<option value="delete">
_{MsgExamCorrectExamResultDelete}
<td .table__td #uw-exam-correct__result__grade> <td .table__td #uw-exam-correct__result__grade>
<select> <select>
$forall grade <- (toPathPiece <$> examGrades) $forall grade <- (toPathPiece <$> examGrades)

View File

@ -3,7 +3,5 @@ $newline never
^{closeWgt} ^{closeWgt}
<section> <section>
$if hasUsers $if hasUsers
<div .notification .notification-info .fa-question .notification--broad> ^{examGradesExplanation}
<div .notification__content>
_{MsgExamGradesExplanation}
^{examUsersTable} ^{examUsersTable}

View File

@ -1,6 +1,4 @@
$newline never $newline never
$if hasUsers $if hasUsers
<div .notification .notification-info .fa-question .notification--broad> ^{examGradesExplanation}
<div .notification__content>
_{MsgExamGradesExplanation}
^{table} ^{table}

View File

@ -56,7 +56,7 @@ $maybe desc <- examDescription
$maybe start <- examStart $maybe start <- examStart
^{formatTimeRangeW SelFormatDateTime start examEnd} ^{formatTimeRangeW SelFormatDateTime start examEnd}
$maybe finished <- examFinished $maybe finished <- examFinished
<dt .deflist__dt>_{MsgExamFinishedParticipant} <dt .deflist__dt>_{examFinishedMsg}
<dd .deflist__dd>^{formatTimeW SelFormatDateTime finished} <dd .deflist__dd>^{formatTimeW SelFormatDateTime finished}
$if examClosedShown $if examClosedShown
$maybe closed <- examClosed $maybe closed <- examClosed

View File

@ -1,5 +1,12 @@
$newline never $newline never
<dl .deflist> <dl .deflist>
<dt .deflist__dt>
^{formatGregorianW 2020 02 07}
<dd .deflist__dd>
<ul>
<li>
Überarbeitete Navigation
<dt .deflist__dt> <dt .deflist__dt>
^{formatGregorianW 2020 01 30} ^{formatGregorianW 2020 01 30}
<dd .deflist__dd> <dd .deflist__dd>
@ -109,9 +116,9 @@ $newline never
^{formatGregorianW 2019 09 16} ^{formatGregorianW 2019 09 16}
<dd .deflist__dd> <dd .deflist__dd>
<ul> <ul>
<li>Modellierung der Prüfungsämter im System inkl. direkte Einsicht in relevante Prüfungsleistungen <li>Prüfungsverwaltung im System inkl. direkte Einsicht in relevante Prüfungsleistungen
<li>E-Mail-Benachrichtigungen an zuständige Prüfungsämter bei Abschluss einer Klausur <li>E-Mail-Benachrichtigungen an zuständige Prüfungsverwalter bei Abschluss einer Klausur
<li>Abschluss von Klausuren (d.h. Melden der Prüfungsleistungen an die Prüfungsämter) jetzt als Button, statt als voreingestellter Zeitpunkt <li>Abschluss von Klausuren (d.h. Melden der Prüfungsleistungen an die Prüfungsverwalter) jetzt als Button, statt als voreingestellter Zeitpunkt
<dt .deflist__dt> <dt .deflist__dt>
^{formatGregorianW 2019 09 13} ^{formatGregorianW 2019 09 13}

View File

@ -1,5 +1,12 @@
$newline never $newline never
<dl .deflist> <dl .deflist>
<dt .deflist__dt>
^{formatGregorianW 2020 02 07}
<dd .deflist__dd>
<ul>
<li>
Reworked navigation
<dt .deflist__dt> <dt .deflist__dt>
^{formatGregorianW 2020 01 30} ^{formatGregorianW 2020 01 30}
<dd .deflist__dd> <dd .deflist__dd>

View File

@ -1,24 +1,23 @@
$newline never $newline never
<p> <p>
Hier können Sie der Meldung ihrer Prüfungsleistungen an Prüfungsämter # Hier können Sie der Meldung ihrer Prüfungsleistungen an Prüfungsverwalter #
bestimmter Institute (innerhalb von Uni2work) widersprechen. bestimmter Institute (innerhalb von Uni2work) widersprechen.
<p> <p>
Bedenken Sie, dass die Meldung der Prüfungsleistungen direkt in Uni2work den # Bedenken Sie, dass die Meldung der Prüfungsleistungen direkt in Uni2work den #
Verwaltungsaufwand (und die damit verbunden Dauer) für die ordnungsgemäße # Verwaltungsaufwand (und die damit verbunden Dauer) für die ordnungsgemäße #
Anrechung Ihrer Leistungen drastisch reduziert. Anrechung Ihrer Leistungen drastisch reduziert.
<p> <p>
Unter Umständen können Prüfungsämter ungeachtet der Angaben, die Sie hier # Unter Umständen können Prüfungsverwalter ungeachtet der Angaben, die Sie hier #
machen, Einsicht in Ihre Leistungen erlangen.<br /> machen, Einsicht in Ihre Leistungen erlangen.<br />
Dies geschieht nur in begründeten Einzelfällen (z.B. bei Studierenden im # Dies geschieht nur in begründeten Einzelfällen (z.B. bei Studierenden im #
ERASMUS-Programm). ERASMUS-Programm).
<p> <p>
Nutzer, die unabhängig von diesen Einstellungen, Einsicht in Ihre # Nutzer, die unabhängig von diesen Einstellungen, Einsicht in Ihre #
Prüfungsleistungen haben (z.B. die Kursverwalter) können Ihre Note natürlich # Prüfungsleistungen haben (z.B. die Kursverwalter) können Ihre Note natürlich #
außerhalb von Uni2work an Prüfungsämter melden (auch solche, die hier nicht # außerhalb von Uni2work weitermelden
aufgeführt sind).
$if hasForced $if hasForced
<p> <p>
Wenn Sie der Meldung an einzelne Prüfungsämter nicht widersprechen können, # Wenn Sie der Meldung an einzelne Prüfungsverwalter nicht widersprechen können, #
so hat das jeweilige Prüfungsamt angegeben, dass die Einsicht, entweder # so hat der jeweilige Prüfungsverwalter angegeben, dass die Einsicht, entweder #
aufgrund einer Ihrer Studiengänge (z.B. aufgrund der Studienordnung) oder # aufgrund einer Ihrer Studiengänge (z.B. aufgrund der Studienordnung) oder #
bei Ihnen spezifisch, zwingend erforderlich ist. bei Ihnen spezifisch, zwingend erforderlich ist.

View File

@ -0,0 +1,9 @@
$newline never
<p>
Um eine Prüfungsleistung einzutragen können Sie in der #
Teilnehmer-Spalte einen beliebigen eindeutigen Identifikator des #
Teilnehmers angeben.<br />
Vermutlich eindeutig ist die Matrikelnummer des Teilnehmers, aber #
auch der Name oder ein Teil der Matrikelnummer können unter #
Umständen bereits eindeutig sein. #

View File

@ -0,0 +1,8 @@
$newline never
<p>
To enter a participant's exam achievement you can submit any string #
that uniquely identifies the participant.<br />
Matriculation numbers are likely unique. #
The participant's name or a part of their matriculation number may #
also be sufficiently unique.

View File

@ -0,0 +1,9 @@
$newline never
<p>
Diese Ansicht zeigt die selben Daten an, wie die Tabelle von Prüfungsteilnehmern.<br />
Anpassen der Teilnehmerdaten und Ergebnisse ist nur dort möglich.
<p>
Hier können Sie vor Allem einsehen und markieren, welche #
Prüfungsleistungen von den zuständigen Prüfungsbeauftragten bereits #
vollständig bearbeitet wurden.

View File

@ -0,0 +1,9 @@
$newline never
<p>
This view shows the same data as the table of exam participants.<br />
Changing participant's data and achievements is only possible via #
the table of exam participants.
<p>
Primarily, this view allows you to check and adjust which exam #
achievements were properly handled by the relevant exam offices.

View File

@ -1,24 +1,22 @@
$newline never $newline never
<div .notification .notification-warning .fa-exclamation-triangle .notification--broad> <p>
<div .notification__content> Die Tabelle enthält Werte, die automatisch berechnet wurden.
<p> <p>
Die Tabelle enthält Werte, die automatisch berechnet wurden. Automatisch berechnete Werte (Bonus und Prüfungsergebnis) werden weder dem #
<p> entsprechenden Teilnehmer angezeigt, noch an Prüfungsverwalter gemeldet #
Automatisch berechnete Werte (Bonus und Prüfungsergebnis) werden weder dem # bevor sie manuell übernommen wurden.<br />
entsprechenden Teilnehmer angezeigt, noch an das Prüfungsamt gemeldet # Hierzu können Sie die Aktion „Berechnetes Prüfungsergebnis übernehmen“ #
bevor sie manuell übernommen wurden.<br /> verwenden.
Hierzu können Sie die Aktion „Berechnetes Prüfungsergebnis übernehmen“ # <p>
verwenden. Sie können die automatisch berechneten Werte auch manuell (via CSV-Import) #
<p> überschreiben.<br />
Sie können die automatisch berechneten Werte auch manuell (via CSV-Import) # Wenn die so gesetzten Werte nicht den automatisch Berechneten entsprechen #
überschreiben.<br /> sind sie <i>inkonsistent</i>.
Wenn die so gesetzten Werte nicht den automatisch Berechneten entsprechen # <p>
sind sie <i>inkonsistent</i>. Automatisch berechnete Werte sind gekennzeichnet wie folgt:
<p>
Automatisch berechnete Werte sind gekennzeichnet wie folgt:
<table style="font-weight: normal"> <table style="font-weight: normal">
<tr> <tr>
<td style="padding: 0 7px 0 0" .table__td .table__td--automatic>Automatisch berechnet <td style="padding: 0 7px 0 0" .table__td .table__td--automatic>Automatisch berechnet
<td style="padding: 0 7px" .table__td>Normaler Wert <td style="padding: 0 7px" .table__td>Normaler Wert
<td style="padding: 0 0 0 7px" .table__td .table__td--overriden>Inkonsistent <td style="padding: 0 0 0 7px" .table__td .table__td--overriden>Inkonsistent

View File

@ -1,23 +1,21 @@
$newline never $newline never
<div .notification .notification-warning .fa-exclamation-triangle .notification--broad> <p>
<div .notification__content> This table contains values that were computed automatically.
<p> <p>
This table contains values that were computed automatically. Values computed automatically (bonus and result) are shown to neither the #
<p> participant nor relevant exam offices until they are manually accepted.<br />
Values computed automatically (bonus and result) are shown to neither the # To do this you may use the action “Accept computed result”.
participant nor relevant exam offices until they are manually accepted.<br /> <p>
To do this you may use the action “Accept computed result”. You are also able to override the automatically computed values manually #
<p> (via CSV import).<br />
You are also able to override the automatically computed values manually #
(via CSV import).<br />
If values thus overriden do not match the automatically computed values # If values thus overriden do not match the automatically computed values #
they are considered <i>inconsistent</i>. they are considered <i>inconsistent</i>.
<p> <p>
Automatically computed values are marked as follows: Automatically computed values are marked as follows:
<table style="font-weight: normal"> <table style="font-weight: normal">
<tr> <tr>
<td style="padding: 0 7px 0 0" .table__td .table__td--automatic>Automatically computed <td style="padding: 0 7px 0 0" .table__td .table__td--automatic>Automatically computed
<td style="padding: 0 7px" .table__td>Normal value <td style="padding: 0 7px" .table__td>Normal value
<td style="padding: 0 0 0 7px" .table__td .table__td--overriden>Inconsistent <td style="padding: 0 0 0 7px" .table__td .table__td--overriden>Inconsistent

View File

@ -3,6 +3,6 @@ $newline never
Kursverwalter (und von ihnen beauftragte Korrektoren) können die Ergebnisse # Kursverwalter (und von ihnen beauftragte Korrektoren) können die Ergebnisse #
einer Prüfung direkt in Uni2work hinterlegen.<br /> einer Prüfung direkt in Uni2work hinterlegen.<br />
Dies dient sowohl der Rückmeldung an den jeweiligen Studierenden als auch # 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 # Prüfungsleistungen direkt aus Uni2work mit ihren eigenen Verwaltungssystemen #
synchronisieren. synchronisieren.

View File

@ -5,7 +5,7 @@ $newline never
benötigen um sicherzustellen, dass Studienmodalitäten erfüllt und Leistungen # benötigen um sicherzustellen, dass Studienmodalitäten erfüllt und Leistungen #
korrekt anerkannt werden.<br /> korrekt anerkannt werden.<br />
<p> <p>
<i>Teil des Prüfungsamts</i> ist eine Berechtigung die einzelnen Benutzern von # <i>Mit Prüfungsverwaltung beauftragt</i> ist eine Berechtigung die einzelnen Benutzern von #
einem Administrator für ein bestimmtes Institut eingeräumt wird.<br /> einem Administrator für ein bestimmtes Institut eingeräumt wird.<br />
Diese Benutzer haben dann u.A. Zugriff auf alle beim relevanten Institut # Diese Benutzer haben dann u.A. Zugriff auf alle beim relevanten Institut #
erbrachten und in Uni2work hinterlegten Prüfungsleistungen. erbrachten und in Uni2work hinterlegten Prüfungsleistungen.

View File

@ -312,9 +312,9 @@ $newline text
Anmeldezeitraums anmelden. Anmeldezeitraums anmelden.
Die Teilnehmerlisten können online oder per CSV Export/Import bearbeitet werden. Die Teilnehmerlisten können online oder per CSV Export/Import bearbeitet werden.
<dt .deflist__dt> ^{plannedFeat} Prüfungszuteilung <dt .deflist__dt> ^{newFeat 2020 01 29} Prüfungszuteilung
<dd .deflist__dd> <dd .deflist__dd>
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. nach verschiedenen Kriterien wie Name oder Matrikelnummer vornehmen.
<dt .deflist__dt> ^{plannedFeat} Korrekturen <dt .deflist__dt> ^{plannedFeat} Korrekturen
@ -333,7 +333,7 @@ $newline text
<p> <p>
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. 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.
<p> <p>
^{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.
<p> <p>
^{newU2WFeat} Die Vorschläge können auch unabhängig vom eingetragenen Notenschlüssel manuell überschrieben werden. ^{newU2WFeat} Die Vorschläge können auch unabhängig vom eingetragenen Notenschlüssel manuell überschrieben werden.
@ -354,15 +354,15 @@ $newline text
<dt .deflist__dt> ^{newFeat 2019 9 16} Notenmeldung <dt .deflist__dt> ^{newFeat 2019 9 16} Notenmeldung
<dd .deflist__dd> <dd .deflist__dd>
<p> <p>
Endnoten können automatisiert an die Prüfungsämter gemeldet werden. Endnoten können automatisiert an die Prüfungsverwaltung gemeldet werden.
<p> <p>
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.
<p> <p>
^{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).
<p> <p>
^{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. 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. hierüber automatisiert eine Benachrichtigung.

View File

@ -303,7 +303,7 @@ $newline text
<p> <p>
^{newU2WFeat} At first, the calculated results will be shown as suggestions only. ^{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 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.
<p> <p>
^{newU2WFeat} The calculated suggestions can also be overriden manually, independent of the grading scale. ^{newU2WFeat} The calculated suggestions can also be overriden manually, independent of the grading scale.
@ -324,19 +324,19 @@ $newline text
<dt .deflist__dt> ^{newFeat 2019 9 16} Reporting Exam Results <dt .deflist__dt> ^{newFeat 2019 9 16} Reporting Exam Results
<dd .deflist__dd> <dd .deflist__dd>
<p> <p>
Final grades can be automatically sent to the Prüfungsämter (Exam Offices). Final grades can be automatically sent to the relevant exam offices.
<p> <p>
After finalizing an exam (by clicking a button on the list of exam participants), all associated 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.
<p> <p>
^{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.<br /> page "Exam Results" will be shown.<br />
In case of this check mark being absent even after a long period of time, a course administrator should 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). look into reporting the exam result for this participant (e.g. by issueing a proof of participation).
<p> <p>
^{newU2WFeat} Course administrators are allowed to edit final results even after reporting the grades ^{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.
<section id="allocations"> <section id="allocations">

View File

@ -1,380 +0,0 @@
$newline text
<section>
<section>
<h2>Courses
<dl .deflist>
<dt .deflist__dt> Course Names
<dd .deflist__dd>
<p>
^{newU2WFeat} Any course needs a shorthand for identification, e.g. DBS, PXD, DM, ...
<p>
The combination of shorthand, department and semester needs to be unique.<br />
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).
<p>
Recommendations for course shorthands:
<ul>
<li> Try to keep the shorthand as short as possible (10 characters max.).
In particular, we advice against choosing the full course title as its shorthand.
A warning will be shown if a shorthand exceeds the recommended length.
<li> Avoid adding identifiers for the department (e.g. "MATH"), the semester (e.g. "WS19") or the type (e.g. "SEM") of a course in its shorthand.
<p>
^{plannedFeatInline} It is planned to enable courses to have types (e.g. "Bachelor Seminar" or "Practical Course").<br />
Students will then be able to explicitely search for courses of a specific type.
<dt .deflist__dt> Clone Courses
<dd .deflist__dd>
<p>
Lecturers are able to clone <em>any</em> course of their department for the current semester.
When cloning a course, its shorthand and description will be adopted;
but not exercise sheets, exams or registrations.
<p>
The course description can be composed in Html and
<em>should contain the module description!
<dt .deflist__dt> Support for Multiple Departments
<dd .deflist__dd>
<p>
^{newU2WFeat} Uni2work supports managing multiple departments; prefixing course titles with e.g. "[MATH]" are not necessary anymore.
Instead, there are now department filters for course lists.
<p>
The permissions of Uni2work administrators are limited to courses of their respective departments.
This means that a Uni2work administrator of the Institute of Informatics cannot access grades from courses of the Department of Maths.
<dt .deflist__dt> Access to Material
<dd .deflist__dd>
The access to exercise sheets, slides and other material can be made dependent on the registration for the course.
<dt .deflist__dt> Publish Material
<dd .deflist__dd>
Slides, code bundles etc. can now be distributed to the participants of a course; also password-protected if wanted.
<dt .deflist__dt> Course Passwords
<dd .deflist__dd> ^{newU2WFeat} The registration for a course can be password-protected.
<dt .deflist__dt> Course Lecturers and Assistants
<dd .deflist__dd>
<p>
Course administrators can assign <em>any</em> user as administrator of the course.
^{newU2WFeat} Within a course, all course administrators have the same permissions. Most notably, every course administrator is allowed to edit the list of administrators for this course.
<p>
^{newU2WFeat} Uni2work features "lecturer" and "assistant" as roles:
lecturers are essentially allowed to create new courses.
The lecturer permission is differentiated according to department.
Assistants have the same permissions as lecturers, but only for a particular course.
<p>
In UniWorX, there was the role "assistant",
i.e. every "administrator" also had to be "lecturer";
there was no differentiation between departments.
<dt .deflist__dt> Course Participants
<dd .deflist__dd>
<p>
Course participants are now displayed alongside information regarding their study programs.
Students of multiple programs need to select a main subject when registering for a course.
This can speed up the process of reporting exam results.
<p>
^{probFeatInline} Normally, information regarding students' study programs is up to date;
however, outdated data is shown in some cases.<br />
Therefore, no decisions with severe consequences should be made without consulting the student
first, if they are solely based on study program information (e.g. veto in a central allocation
of practical courses and seminars).
<p>
If only a number (key) is shown instead of a study program or degree, Uni2work did not yet learn a
mapping from this key to its corresponding program.<br />
Unfortunately, this needs to happen successively because we cannot get an up-to-date and complete
mapping of those keys from the Studentenkanzlei (Student Office).<br />
^{probFeatInline} It might happen that a more "general" study program is shown than the actual
one a student is enrolled in (e.g. Media Informatics instead of Human-Computer-Interaction).
This problem should be fixed in the near future.
<dt .deflist__dt> From a Student's Point of View
<dd .deflist__dd>
<p>
^{newU2WFeat} UniWorX had special links to view a page from a student's point of view ("Aus Studentensicht"), which are not necessary anymore in Uni2work.
Instead, one can now #
<a href=@{AuthPredsR}>temporarily withdraw one's own permissions here
. To view one's own course from a participant's point of view, one deactivates #
the permission check "_{MsgAuthTagLecturer}" and/or "_{MsgAuthTagCorrector}".
<dt .deflist__dt> News
<dd .deflist__dd>
<p>
^{newFeat 2019 10 7} On the course overview page, one can directly publish news concerning
the course ("News").
<p>
An RSS feed and (opt-in) email notifications for course news are planned in the future.
<dt .deflist__dt> Dates
<dd .deflist__dd>
<p>
^{newFeat 2019 10 9} On the course overview page, one can publish dates concerning the course
(e.g. weekly date of the lecture, one-time post-exam review, ...).
<p>
Email notifications on changes of the dates are planned in the future.
<section>
<h2>Course Exercises
<dl .deflist>
<dt .deflist__dt> Correctors
<dd .deflist__dd>
^{newU2WFeat} Correctors and correction method are selected by the course administrator ad-hoc per exercise sheet;
there is no entry for correctors in the course configuration.<br />
To grant tutors that are no correctors permission to view solutions before the submission deadline,
they should be registered as sheet correctors with 0 correction proportions.
When creating a new sheet, the settings of the previous sheet are suggested automatically.
<dt .deflist__dt> Distribution
<dd .deflist__dd>
^{newU2WFeat} Correctors can be marked as Absent or Excused per exercise sheet. This way, they will not be
automatically assigned as correctors of any submissions.
The difference between Absent and Excused correctors is that Absent correctors will be assigned more
submissions in the upcoming sheets automatically, to make up for the proportion of the sheet they missed.
For Excused correctors, no additional submissions will be assigned later on.
<dt .deflist__dt> Files
<dd .deflist__dd>
^{newU2WFeat} The exercise sheet and its solution can consist of several files of any type.
Instead of having to upload a ZIP file in case of multiple files, they can be uploaded one-by-one in Uni2work.
<dt .deflist__dt> Hints
<dd .deflist__dd>
In addition to exercise sheet and solution, hints (e.g. solutions to tutorial exercises) can be released
on a specific date before the submission deadline.
<dt .deflist__dt> Visibility
<dd .deflist__dd>
<p>
Exercise sheets can be hidden from the participants up to a date "Visible from".
This can be used to distribute preliminary sheets to tutors and correctors,
i.e. when grading and deadlines are not yet fixed.
<p>
The participants only get to see the sheet only once it is visible.
However, any files regarding the sheet can only be downloaded once the submission period has started,
similar to UniWorX.
<dt .deflist__dt> Timestamps
<dd .deflist__dd>
<p>
Every file of an exercise sheet is annotated with a timestamp which is visible for the participants.
<p>
A visual highlighting of edited/new files and corresponding notifications is planned,
but not yet implemented.
<dt .deflist__dt> External Submissions
<dd .deflist__dd>
External submissions (e.g. of analogue form)
can be managed using a pseudonym:
<ul>
<li>
Create sheets with submission method
<i>External Submission with Pseudonym</i>
<li>
On the overview page of a single sheet,
participants can generate a pseudonym for their solution.
Then they can mark their external submission with it.
<p>
The participants need to generate a new pseudonym for each exercise sheet to guarantee
the anonymity of their submissions.
However, no submission will be created in the system when generating a pseudonym,
since not every participant that generates a pseudonym actually submits a solution afterwards.
<li>
<p>
After the external submissions are distributed to the correctors,
the correctors need to
<a href="@{CorrectionsCreateR}">
Create a Submission
, which can then be corrected as usual.
<p>
Such submissions will be accounted for in the next distribution of
submissions according to the corrector's correction proportions.
<section>
<h2>Tutorials
<dl .deflist>
<dt .deflist__dt> Occurrences
<dd .deflist__dd>
Tutorials can consist of multiple regular occurrences which repeat on a weekly basis.
<br />
Additionally, an arbitrary number of <i>Exceptions</i> can be created.
<br />
An exception to an occurrence overrides the regular schedule. To add an exception, one enters an arbitrary date that lies inside the regular occurrence. <em>All</em> regular occurrences that contain the selected date will then be marked as exception.
<br />
An exception that an occurrence takes place in turn overrides exceptions that an occurrence does not take place.
<br />
This behavior can be used to shift single occurrences by selecting a regular occurrence as an exception ("does not take place") and another date as a second exception ("does take place").
<dt .deflist__dt> Tutors
<dd .deflist__dd>
<p>
Tutors are selected ad-hoc per tutorial.
<br />
A tutorial can be assigned to an arbitrary number of tutors, and a tutor can be assigned to an arbitrary number of tutorials.
<p>
Tutors have access to the names and study program information of all participants of their tutorials, can send messages to them (similar to course messages) and are able to remove participants from their tutorials.
<p>
^{newFeat 2019 10 14} Optionally, tutors can be given full control over their tutorials (except deleting
them), which means that they can edit rooms and dates of the tutorial.
<dt .deflist__dt> Registration
<dd .deflist__dd>
<p>
Students can register themselves for tutorials via the course page.
<br />
The registration proceeds on a <i>first come, first served</i> basis at the moment.
<br />
A prior registration for the course is required.
<p>
The registration period for a tutorial can be limited in time.
<p>
Tutorials can be provided with a <i>Registration Group</i>.
A registration group consists of an arbitrary text that is of no further significance.
However, participants are restricted on <i>one tutorial registration per registration group</i>.<br />
Empty registration groups (i.e. no registration groups have been assigned) count as distinct.
<p>
To allow registering for an arbitrary number of tutorials, one can leave every registration group empty.
<dt .deflist__dt> ^{newFeat 2019 10 10} Late Registrations
<dt .deflist__dd>
Course administrators can assign course participants to tutorials using the list of course participants.
<section id="exams">
<h2> Exams
<p> Large parts of the management of exams have been implemented and are already usable.<br />
^{newU2WFeat} In addition to UniWorX, Uni2work supports more general forms of exams (e.g. oral exams, practical exams), for which participants can be grouped and examined in different rooms and on different occurrences per group.
<dl .deflist>
<dt .deflist__dt> Create / Edit
<dd .deflist__dd>
Exams can be created by course administrators.
A number of optional settings can be made immediately or later on, e.g. visibility or registration period.
<dt .deflist__dt> Occurrences/Rooms
<dd .deflist__dd>
^{newU2WFeat} An exam can be divided into multiple Occurrences/Rooms; each Occurrence/Room can have an own Occurence and Room.
<p>
In its most simple form, there can be exams that can take place in multiple rooms.
<p>
There can also be exams that take place on different occurrences, e.g. oral exams for seminars or practical courses.<br />
These occurrences will be visible for the participants in forms of a table.
<dt .deflist__dt> Registrations
<dd .deflist__dd>
Participants can register for visible exams during the given registration period.
The list(s) of participants can be edited online or via CSV export/import.
<dt .deflist__dt> ^{plannedFeat} Exam Allocations
<dd .deflist__dd>
Upon request, Uni2work can manage the distribution of participants to exams (rooms/occurrences)
using some criteria, such as name or matriculation number.
<dt .deflist__dt> ^{plannedFeat} Markings
<dd .deflist__dd>
<p>
It is not possible to enter single exam markings (i.e. points per exam part) at the moment.<br />
This feature will be implemented in the near future.
<p>
Entering exam corrections will happen per exam part.
<p>
Optionally, course administrators will be able to assign exam correctors, who will then have permission to enter markings during the correction period.<br />
The permission to enter an exam marking for single exam parts can be granted to single exam correctors.
<dt .deflist__dt> ^{newFeat 2019 9 25} Results
<dt .deflist__dd>
<p>
The calculation of exam results can be done automatically via Uni2work.<br />
For this, a grading scale needs to be specified. This scale will then be used to calculate
the final results of all exam participants based on their achieved amount of points.
<p>
^{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).
<p>
^{newU2WFeat} The calculated suggestions can also be overriden manually, independent of the grading scale.
<dt .deflist__dt> ^{newFeat 2019 9 26} Exam Bonus
<dd .deflist__dd>
It is possible to calculate bonus points for an exam based on the number of exercise points achieved.<br />
The conditions for this calculation can be adjusted (e.g. only take bonus into account if the participant
already passed without the bonus points).<br />
An upper bound of possible bonus points needs to be specified. The achieved bonus will then be calculated
linearly by taking into account the proportion of achieved exercise points and the maximum number of
bonus points.
<dt .deflist__dt> ^{plannedFeat} Door Signs
<dd .deflist__dd>
The printing of door signs ("Quiet please!") with matching exam information is a planned feature that is not yet implemented.
<dt .deflist__dt> ^{newFeat 2019 9 16} Reporting Exam Results
<dd .deflist__dd>
<p>
Final grades can be automatically sent to the Prüfungsämter (Exam Offices).
<p>
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.
<p>
^{newU2WFeat} Once a Prüfungsamt has acknowledged the exam result of a participant, a check mark of the
page "Exam Results" will be shown.<br />
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).
<p>
^{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.
<section id="allocations">
<h2> Central Allocations
<dl .deflist>
<dt .deflist__dt> Naming Schema
<dd .deflist__dd>
Courses can possess an arbitrary title.<br />
^{newU2WFeat} Makeshift shorthands such as [SB], [ZP] etc. are not necessary anymore!
<dt .deflist__dt> Course Settings
<dd .deflist__dd>
<p>
^{newU2WFeat} Where appropriate, the course settings are being overriden by necessary settings of the
corresponding central allocation (meaning that course administrators cannot make any mistakes anymore).
<p>
In particular, the selected registration period will be ignored and any direct registration of students
will be prohibited (also coming from course administrators!), until the central allocation process is
finished and alternate students ("Nachrücker") are treated.
<dt .deflist__dt> Individual Applications
<dd .deflist__dd>
^{newU2WFeat} Students are able to apply for each course separately.
Application documents may consist of both free text and the submission of files (possibly with predefined
filenames).
<dt .deflist__dt> Feedback on Applications
<dd .deflist__dd>
If so desired, course administrators can send back feedback on student's applications.
<section>
<h2>Miscellaneous
<dl .deflist>
<dt .deflist__dt> Planned Maintenance
<dd .deflist__dd>
Planned maintenance will be conducted <i>without prior notice</i>
on 02:00 every night.<br />
Please refrain from scheduling any critical deadlines at exactly this time or shortly afterwards.

View File

@ -0,0 +1,11 @@
$newline never
<p>
Uni2work ist ein Lehrverwaltungssystem, welches an der #
Ludwig-Maximilians-Universität München entwickelt und eingesetzt #
wird.
<p>
Insbesondere unterstützt Uni2work teilnehmende Institute bei #
Übungsbetrieb, Prüfungs- und Notenverwaltung und bietet vollständige #
Kurshomepages inkl. Vorlesungsmaterial und Terminen.

View File

@ -0,0 +1,8 @@
$newline never
<p>
Uni2work is a teaching management system, developed and deployed at #
Ludwig-Maximilians-Universität München.
<p>
Uni2work supports participating departments in managing their course #
exercises, exams and exam achievements, and provides complete course #
homepages including course material and dates.

View File

@ -1,6 +1,6 @@
$newline never $newline never
<section> <section>
<h2>_{MsgHomeUpcomingExams} <h2>_{MsgNewsUpcomingExams}
$if hasExams $if hasExams
^{examTable} ^{examTable}
$else $else

View File

@ -1,4 +1,4 @@
$newline never $newline never
<section> <section>
<h2>_{MsgHomeUpcomingSheets} <h2>_{MsgNewsUpcomingSheets}
^{sheetTable} ^{sheetTable}

View File

@ -28,12 +28,9 @@ $newline never
<div .asidenav__link-label>#{courseName} <div .asidenav__link-label>#{courseName}
<div .asidenav__nested-list-wrapper> <div .asidenav__nested-list-wrapper>
<ul .asidenav__nested-list.list--iconless> <ul .asidenav__nested-list.list--iconless>
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions $forall (NavLink{navLabel}, route) <- pageActions
$case menuItemType <li .asidenav__nested-list-item>
$of PageActionPrime <a .asidenav__link-wrapper href=#{route}>_{navLabel}
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel}
$of _
<div .asidenav__sigillum> <div .asidenav__sigillum>
<img src=@{StaticR img_lmu_sigillum_svg}> <img src=@{StaticR img_lmu_sigillum_svg}>

View File

@ -1,10 +1,17 @@
$newline never $newline never
<div .breadcrumbs__container> <div .breadcrumbs__container>
<a .breadcrumbs__home href=@{NewsR}>
<i .fas .fa-fw .fa-#{iconText IconBreadcrumbsHome}>
<ul .breadcrumbs__list.list--inline> <ul .breadcrumbs__list.list--inline>
$forall (bcRoute, bcTitle, hasAccess) <- parents $forall (bcRoute, bcTitle, hasAccess) <- parents
<li .breadcrumbs__item> <li .breadcrumbs__item>
$if hasAccess $if hasAccess
<a .breadcrumbs__link href="@{bcRoute}">#{bcTitle} <a .breadcrumbs__link href="@{bcRoute}">
#{bcTitle}
$else $else
<span .breadcrumbs__link>#{bcTitle} <span .breadcrumbs__link>
<li .breadcrumbs__last-item>#{title} #{bcTitle}
<li .breadcrumbs__item-separator>
#{iconBreadcrumbSeparator}
<li .breadcrumbs__item .breadcrumbs__last-item>
#{title}

View File

@ -1,63 +0,0 @@
.breadcrumbs__container {
position: relative;
color: var(--color-lightwhite);
padding: 4px 13px;
background-color: var(--color-dark);
line-height: 30px;
}
@media (min-width: 426px) {
.breadcrumbs__container {
padding: 7px 20px;
}
}
@media (min-width: 769px) {
.breadcrumbs__container {
padding: 7px 40px;
}
}
a.breadcrumbs__link {
color: var(--color-lightwhite);
&:hover {
color: var(--color-white);
}
}
.breadcrumbs__item {
padding-right: 14px;
position: relative;
line-height: 28px;
opacity: 0.8;
z-index: 1;
margin-right: 10px;
&: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;
opacity: 1;
}
}
.breadcrumbs__last-item {
line-height: 28px;
vertical-align: bottom;
font-weight: 600;
}

View File

@ -1,13 +1,5 @@
$newline never $newline never
<footer .footer> <footer .footer>
<div .footer-links> <ul .footer-links .list--inline>
$forall (MenuItem{menuItemType, menuItemRoute = _, menuItemIcon = _, menuItemLabel, menuItemModal = _}, menuIdent, route) <- menuTypes $forall n <- filter isNavFooter nav
$case menuItemType ^{navWidget n}
$of Footer
$# Not used but available (remove ` = _` from the pattern match above, as needed):
$# highlight (urlRoute menuItemRoute) :: Bool -- ^ Is this menu item currently active (i.e.: are we on this page)
$# menuItemModal :: Bool -- ^ Should this menu item open a modal instead of being a normal link
$# menuItemIcon :: Maybe Text -- ^ Should this menu item have an icon, if yes, then the name of the icon
<a href=#{route} ##{menuIdent}>
_{SomeMessage menuItemLabel}
$of _

View File

@ -0,0 +1,4 @@
$newline never
<li>
<a href=#{route} ##{ident}>
_{navLink}

View File

@ -0,0 +1,14 @@
##{containerIdent}-radio:checked ~ * ##{containerIdent}-container
visibility: visible
height: 30px
@media (min-width: 769px) and (min-height: 501px)
##{containerIdent}-radio:checked ~ * ##{containerIdent}-container
margin: 14px 0 0 0
##{containerIdent}-radio:checked ~ * ##{containerIdent}
background-color: var(--color-dark)
color: var(--color-lightwhite)
.navbar__link-icon
opacity: 1

View File

@ -0,0 +1,5 @@
$newline never
<label .navbar__link-wrapper for=#{navIdent}-radio ##{navIdent}>
<div .navbar__link-icon>
<i .fas .fa-2x .fa-#{iconText navIcon}>
<div .navbar__link-label>_{SomeMessage navLabel}

View File

@ -1,6 +1,5 @@
$newline never $newline never
<a .navbar__link-wrapper href=#{route} ##{menuIdent}> <a .navbar__link-wrapper href=#{route} ##{ident}>
$if isJust menuItemIcon <div .navbar__link-icon>
<div .navbar__link-icon> <i .fas .fa-2x .fa-#{iconText navIcon}>
<i .fas .fa-2x .fa-#{fromMaybe "none" menuItemIcon}> <div .navbar__link-label>_{SomeMessage navLabel}
<div .navbar__link-label>_{SomeMessage menuItemLabel}

View File

@ -0,0 +1,7 @@
$newline never
$maybe csrf <- csrfToken
<input type=hidden name=#{defaultCsrfParamName} value=#{csrf}>
$forall (k, v) <- navData
<input type=hidden name=#{k} value=#{v}>
<button .navbar__container-link :highlightNav iN:.navbar__container-link--active type=submit>
_{SomeMessage navLabel}

View File

@ -0,0 +1,3 @@
$newline never
<a .navbar__container-link :highlightNav iN:.navbar__container-link--active href=#{route} ##{ident}>
_{SomeMessage navLabel}

View File

@ -1,48 +1,54 @@
$newline never $newline never
<div .navbar-container> <div .navbar-container>
<div .navbar-shadow> <nav .navbar>
<nav .navbar.js-sticky-navbar> <div .navbar__stack>
<div .navbar__list-wrapper>
<ul .navbar__list.navbar__list-left>
$# manually add favorites to navbar for small screens
<li .navbar__list-item.navbar__list-item--favorite>
<a .navbar__link-wrapper href="#">
<div .navbar__link-icon>
<i .fas .fa-2x .fa-#{iconText IconFavourite}>
<div .navbar__link-label>_{MsgNavigationFavourites}
<ul .navbar__list.list--inline.navbar__list-left> $forall n <- filter isNavHeaderPrimary nav
$# manually add favorites to navbar for small screens $case view _1 n
<li .navbar__list-item.navbar__list-item--favorite> $of NavHeader{ navLink }
<a .navbar__link-wrapper href="#"> <li .navbar__list-item :highlightNav navLink:.navbar__list-item--active>
<div .navbar__link-icon> ^{navWidget n}
<i .fas .fa-2x .fa-star> $of NavHeaderContainer{}
<div .navbar__link-label>_{MsgNavigationFavourites} <li .navbar__list-item.navbar__list-item--container-selector>
^{navWidget n}
$of _
$forall (menuItem@MenuItem{menuItemType, menuItemRoute, menuItemModal}, menuIdent, _) <- menuTypes <ul .navbar__list>
$case menuItemType $forall n <- filter isNavHeaderSecondary $ reverse nav
$of NavbarAside $case view _1 n
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active> $of NavHeader{ navLink }
$if menuItemModal <li .navbar__list-item :highlightNav navLink:.navbar__list-item--active>
^{navbarModal (menuItem, menuIdent)} ^{navWidget n}
$else $of NavHeaderContainer{}
^{navbarItem (menuItem, menuIdent)} <li .navbar__list-item.navbar__list-item--container-selector>
$of _ ^{navWidget n}
$of _
<ul .navbar__list.list--inline>
$forall (menuItem@MenuItem{menuItemType, menuItemRoute, menuItemModal}, menuIdent, _) <- menuTypes $forall n@(NavHeaderContainer{ navHeaderRole }, containerIdent, _, ns) <- filter isNavHeaderContainer nav
$case menuItemType <div .navbar__container-list :navHeaderRole == NavHeaderPrimary:.navbar__container-list--left ##{containerIdent}-container>
$of NavbarRight <label .navbar__container-list-closer for=container-radio-none>
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active> <i .fas .fa-fw .fa-#{iconText IconNavContainerClose}>
$if menuItemModal <ul>
^{navbarModal (menuItem, menuIdent)} $forall iN@(nl, _, _) <- ns
$else <li .navbar__container-list-item :highlightNav nl:.navbar__container-list-item--active>
^{navbarItem (menuItem, menuIdent)} ^{navContainerItemWidget n iN}
$of NavbarSecondary
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal $# <li .navbar__list-item--lang-wrapper uw-language-select>
^{navbarModal (menuItem, menuIdent)} $# <input type="checkbox" id="lang-checkbox" uw-no-checkbox>
$else $# <div id="lang-dropdown">
^{navbarItem (menuItem, menuIdent)} $# ^{langFormView'}
$of _ $# <div .navbar__list-item .navbar__list-item--language>
<li .navbar__list-item--lang-wrapper uw-language-select> $# <label .navbar__link-wrapper for="lang-checkbox">
<input type="checkbox" id="lang-checkbox" uw-no-checkbox> $# <div .navbar__link-icon>
<div id="lang-dropdown"> $# <i .fas .fa-2x .fa-flag-alt>
^{langFormView'} $# <div .navbar__link-label>_{MsgMenuLanguage}
<div .navbar__list-item .navbar__list-item--language>
<label .navbar__link-wrapper for="lang-checkbox">
<div .navbar__link-icon>
<i .fas .fa-2x .fa-flag-alt>
<div .navbar__link-label>_{MsgMenuLanguage}

View File

@ -0,0 +1,4 @@
$newline never
<div .notification .notification-#{toPathPiece messageStatus} .fa-#{iconText messageIcon} :is _NotificationBroad nType:.notification--broad>
<div .notification__content>
#{messageContent}

View File

@ -1,23 +1,19 @@
$newline never $newline never
<div .pagenav> $if hasSecondaryPageActions || hasPrimarySubActions
$if hasPrimaryPageActions <input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-none type=radio checked>
<div .pagenav-prime> <ul .pagenav>
$forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes $forall n <- filter isPageActionPrimary nav
$case menuItemType <li .pagenav__list-item>
$of PageActionPrime ^{navWidget n}
<div .pagenav__list-item>
$if menuItemModal
<div uw-modal data-modal-trigger=#{menuIdent} data-modal-closeable>
<a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
$of _
$if hasSecondaryPageActions $if hasSecondaryPageActions
<div .pagenav-secondary> <li .pagenav__list-item .pagenav-secondary>
<div .pagenav-secondary__list> <input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-secondary type=radio>
$forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes <label .pagenav-item__expand-label for=pageaction-item__expand-secondary>
$case menuItemType <i .fas .fa-fw .fa-#{iconText IconPageActionSecondary}>
$of PageActionSecondary <div .pagenav-item__children-wrapper>
<div .pagenav__list-item.pagenav__list-item--secondary> <ul .pagenav-item__children>
$if menuItemModal $forall n <- filter isPageActionSecondary nav
<div uw-modal data-modal-trigger=#{menuIdent} data-modal-closeable> <li>
<a .pagenav__link-wrapper.pagenav__link-wrapper--secondary href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel} ^{navWidget n}
$of _ <label .pagenav-item__close-label for=pageaction-item__expand-none>
#{iconPageActionChildrenClose}

View File

@ -0,0 +1,13 @@
$newline never
^{pWidget}
$if not (null sWidgets)
<input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-#{navIdent} type=radio>
<label .pagenav-item__expand-label for=pageaction-item__expand-#{navIdent}>
<i .fas .fa-fw .fa-#{iconText IconPageActionPrimaryExpand}>
<div .pagenav-item__children-wrapper>
<ul .pagenav-item__children>
$forall sWgt <- sWidgets
<li>
^{sWgt}
<label .pagenav-item__close-label for=pageaction-item__expand-none>
#{iconPageActionChildrenClose}

View File

@ -0,0 +1,3 @@
$newline never
<a .pagenav-item__link href=#{route} ##{ident}>
_{SomeMessage navLabel}

View File

@ -0,0 +1,3 @@
$newline never
<a .pagenav-item__link href=#{route} ##{ident}>
_{SomeMessage navLabel}