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

View File

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

View File

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

View File

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

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

View File

@ -8,13 +8,26 @@ table[uw-exam-correct]
th.uw-exam-correct--user-cell, td.uw-exam-correct--user-cell
min-width: 200px
th.uw-exam-correct--part-cell, td.uw-exam-correct--part-cell
width: 85px
width: min-content
text-align: center
white-space: nowrap
input
width: 70px
padding: 4px 8px
.uw-exam-correct--delete-exam-part ~ .fa-trash
opacity: .5
cursor: pointer
margin-left: 5px
.uw-exam-correct--delete-exam-part ~ .fa-trash:hover
opacity: 1
.uw-exam-correct--delete-exam-part:checked ~ .fa-trash
opacity: 1
color: var(--color-error)
td#uw-exam-correct__result
width: min-content
select
@ -34,6 +47,16 @@ table[uw-exam-correct]
td#uw-exam-correct__result__grade select.grade-hidden
visibility: hidden
td.exam-correct--status-cell
font-size: .9rem
font-weight: 600
color: var(--color-fontsec)
font-style: italic
.fas
font-size: 1rem
text-align: center
padding-right: .25rem
[uw-exam-correct] input:invalid:not(.no-value)

View File

@ -1,48 +1,91 @@
import { Utility } from '../../core/utility';
import './navbar.sass';
import * as throttle from 'lodash.throttle';
export const LANGUAGE_SELECT_UTIL_SELECTOR = '[uw-language-select]';
const LANGUAGE_SELECT_INITIALIZED_CLASS = 'language-select--initialized';
export const HEADER_CONTAINER_UTIL_SELECTOR = '.navbar__list-item--container-selector .navbar__link-wrapper';
const HEADER_CONTAINER_INITIALIZED_CLASS = 'navbar-header-container--initialized';
@Utility({
selector: LANGUAGE_SELECT_UTIL_SELECTOR,
selector: HEADER_CONTAINER_UTIL_SELECTOR,
})
export class LanguageSelectUtil {
export class NavHeaderContainerUtil {
_element;
checkbox;
radioButton;
closeButton;
container;
wasOpen;
_throttleUpdateWasOpen;
constructor(element) {
if (!element) {
throw new Error('Language Select utility needs to be passed an element!');
throw new Error('Navbar Header Container utility needs to be passed an element!');
}
if (element.classList.contains(LANGUAGE_SELECT_INITIALIZED_CLASS)) {
return false;
if (element.classList.contains(HEADER_CONTAINER_INITIALIZED_CLASS)) {
return;
}
this._element = element;
this.checkbox = element.querySelector('#lang-checkbox');
this.radioButton = document.getElementById(`${this._element.id}-radio`);
if (!this.radioButton) {
throw new Error('Navbar Header Container utility could not find associated radio button!');
}
window.addEventListener('click', event => this.close(event));
this.closeButton = document.getElementById('container-radio-none');
if (!this.closeButton) {
throw new Error('Navbar Header Container utility could not find radio button for closing!');
}
this.container = document.getElementById(`${this._element.id}-container`);
if (!this.container) {
throw new Error('Navbar Header Container utility could not find associated container!');
}
element.classList.add(LANGUAGE_SELECT_INITIALIZED_CLASS);
const closer = this.container.querySelector('.navbar__container-list-closer');
if (closer) {
closer.classList.add('navbar__container-list-closer--hidden');
}
this.updateWasOpen();
this.throttleUpdateWasOpen = throttle(this.updateWasOpen.bind(this), 100, { leading: false, trailing: true });
this._element.classList.add(HEADER_CONTAINER_INITIALIZED_CLASS);
}
close(event) {
if (!this._element.contains(event.target) && window.document.contains(event.target)) {
this.checkbox.checked = false;
start() {
if (!this.container)
return;
window.addEventListener('click', this.clickHandler.bind(this));
this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this));
}
clickHandler() {
if (!this.container.contains(event.target) && window.document.contains(event.target) && this.wasOpen) {
this.close();
}
}
destroy() {
// TODO
close() {
this.radioButton.checked = false;
this.throttleUpdateWasOpen();
}
isOpen() {
return this.radioButton.checked;
}
updateWasOpen() {
this.wasOpen = this.isOpen();
}
destroy() { /* TODO */ }
}
export const NavbarUtils = [
LanguageSelectUtil,
NavHeaderContainerUtil,
];

View File

@ -1,49 +1,137 @@
.navbar-container
position: relative
.navbar-shadow
position: fixed
right: 0
top: 0
height: var(--header-height-collapsed)
width: 20px
z-index: 50
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.4), transparent)
transition: height 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
@media (min-width: 768px)
.navbar-shadow
height: var(--header-height)
@media (min-width: 1025px)
.navbar-shadow
display: none
.navbar
position: fixed
display: flex
flex-direction: row
align-items: center
justify-content: flex-start
right: 0
top: 0
left: var(--asidenav-width-xl)
height: var(--header-height)
min-height: var(--header-height)
background-color: var(--color-primary)
color: white
z-index: 20
z-index: 22
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2)
overflow: auto
transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
margin: 0
padding: 10px 0
@media (max-width: 1199px)
.navbar
@media (max-width: 1199px)
left: var(--asidenav-width-lg)
@media (max-width: 768px)
.navbar
@media (max-width: 768px), (max-height: 500px)
min-height: var(--header-height-collapsed)
padding: 0
@media (max-width: 768px)
left: 0
display: flex
& > *
flex-grow: 1
.navbar__stack
display: flex
flex-flow: column nowrap
& > *
flex-grow: 1
.navbar__list-wrapper
display: flex
flex-flow: row nowrap
justify-content: space-between
align-items: center
margin: 0
@media (min-width: 769px)
margin: 0 40px
.navbar__list
display: flex
flex-flow: row nowrap
justify-content: flex-end
align-items: center
list-style-type: none
margin: 0
&.navbar__list-left
justify-content: flex-start
margin-right: 40px
& > *
display: block
.navbar__container-list
/* margin: 10px 0 0 0 */
position: relative
padding: 0 40px
overflow: hidden
display: flex
flex-grow: 1
& > ul
display: flex
flex-grow: 1
flex-flow: row nowrap
align-items: center
overflow: overlay
list-style-type: none
justify-content: flex-end
margin: 0
& > *
display: block
@media (min-width: 501px)
margin-right: 12px
&:last-child
margin-right: 0
&.navbar__container-list--left > ul
justify-content: flex-start
@media (max-width: 768px)
padding: 0
margin: 0
height: 0
transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
.navbar__container-list-closer
position: absolute
top: 5px
right: 10px
width: 20px
height: 20px
text-align: center
transform-origin: 10px 10px
transform: rotate(-0.25turn)
opacity: 0.5
transition: transform 0.2s, opacity 0.2s ease
&:hover
opacity: 1
transform: scale(1.4)
&.navbar__container-list-closer--hidden
visibility: hidden
@media (max-width: 768px)
visibility: hidden
&.navbar__container-list--left .navbar__container-list-closer
transform: rotate(0.25turn)
right: auto
left: 10px
&:hover
transform: scale(1.4)
// links
.navbar__link-wrapper
display: flex
@ -57,6 +145,10 @@
overflow: hidden
cursor: pointer
@media (max-width: 768px), (max-height: 500px)
height: var(--header-height-collapsed)
.navbar__link-icon
opacity: 0.7
transition: opacity 0.2s ease
@ -67,12 +159,13 @@
padding: 2px 4px
text-transform: uppercase
font-weight: 600
font-size: 16px
@media (min-width: 769px)
@media (min-width: 769px) and (min-height: 501px)
.navbar__link-wrapper
border: 1px solid rgba(255, 255, 255, 0.7)
@media (max-width: 768px)
@media (max-width: 768px), (max-height: 500px)
.navbar__link-wrapper
box-shadow: none
min-width: 0
@ -86,72 +179,56 @@
transform: scale(0.65)
margin-bottom: 0
// navbar list
.navbar__list
.navbar__container-link
display: block
@media (min-width: 769px) and (min-height: 501px)
border: 1px solid rgba(255, 255, 255, 0.7)
height: 30px
color: var(--color-lightwhite) !important
background-color: rgba(0, 0, 0, 0) !important
padding: 5px 10px
text-transform: uppercase
font-weight: 600
font-size: 16px
outline: 0
min-width: 0
transition: none
cursor: pointer
white-space: nowrap
+ .navbar__list
margin-left: 12px
&:not(.navbar__container-link--active):hover
background-color: var(--color-dark) !important
color: var(--color-lightwhite) !important
@media (min-width: 769px)
.navbar__list:last-of-type
padding-right: 40px
&.navbar__container-link--active
background-color: var(--color-lightwhite) !important
color: var(--color-dark) !important
@media (max-width: 768px)
.navbar__list
+ .navbar__list
margin-left: 0
padding-right: 40px
// list item
.navbar__list-item
position: relative
transition: background-color .1s ease
&:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper
margin-left: 12px
&:not(.navbar__list-item--favorite) + .navbar__list-item
& + .navbar__list-item
margin-left: 12px
@media (max-width: 500px)
.navbar__list-item
min-width: 60px
&:not(.navbar__list-item--favorite) + .navbar__list-item
& + .navbar__list-item
margin-left: 0
&:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper
margin-left: 0
.navbar__list-left
flex: 5
padding-left: 40px
@media (max-width: 768px)
.navbar__list-left
padding-left: 0
// "Favorites" list item, only visible on small screens and logged in
.navbar__list-item
&.navbar__list-item--favorite
display: none
.navbar__list-item--favorite
display: none
background-color: var(--color-primary)
.logged-in
.navbar__list
li.navbar__list-item--favorite,
.navbar__list-item--favorite
display: inline-block
@media (min-width: 426px)
.logged-in
.navbar__list
.navbar__list-item--favorite
display: none !important
.navbar__list-item--favorite
display: none !important
& + .navbar__list-item
margin-left: 0
.navbar__list-item--active
background-color: var(--color-lightwhite)
@ -163,7 +240,7 @@
.navbar__list-item--active .navbar__link-wrapper
color: var(--color-dark)
.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper, #lang-checkbox:checked ~ * .navbar__link-wrapper
.navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-wrapper
background-color: var(--color-dark)
color: var(--color-lightwhite)
@ -186,43 +263,5 @@
display: block
height: var(--header-height-collapsed)
@media (max-width: 768px)
.navbar,
.navbar__pushdown
height: var(--header-height-collapsed)
.navbar__link-wrapper
height: var(--header-height-collapsed)
@media (max-height: 500px)
.navbar,
.navbar__pushdown
height: var(--header-height-collapsed)
.navbar__link-wrapper
height: var(--header-height-collapsed)
#lang-dropdown
.navbar__container-radio--none, .navbar__container-radio
display: none
position: fixed
top: var(--header-height)
right: 0
min-width: 200px
z-index: 10
background-color: white
border-radius: 2px
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3)
select
display: block
button
display: block
width: 100%
#lang-checkbox:checked ~ #lang-dropdown
display: block
@media (max-width: 768px)
#lang-dropdown
top: var(--header-height-collapsed)

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 { CourseTeaser } from './course-teaser/course-teaser';
import { NavbarUtils } from './navbar/navbar';
import { PageActionsUtils } from './pageactions/pageactions';
import { HideColumns } from './hide-columns/hide-columns';
import { ExamCorrect } from './exam-correct/exam-correct';
import { SortTable } from './sort-table/sort-table';
@ -30,6 +31,7 @@ export const Utils = [
Tooltip,
CourseTeaser,
...NavbarUtils,
...PageActionsUtils,
HideColumns,
ExamCorrect,
SortTable,

View File

@ -494,7 +494,7 @@ CorrectorsPlaceholder: Korrektoren...
CorrectorsDefaulted: Korrektoren-Liste wurde aus bisherigen Übungsblättern diesen Kurses generiert. Es sind keine Daten gespeichert.
Users: Benutzer
HomeHeading: Aktuelle Termine
NewsHeading: Aktuelles
LoginHeading: Authentifizierung
LoginTitle: Authentifizierung
ProfileHeading: Benutzereinstellungen
@ -509,9 +509,9 @@ NotificationSettingsHeading displayName@Text: Benachrichtigungs-Einstellungen f
TokensLastReset: Tokens zuletzt invalidiert
TokensResetSuccess: Authorisierungs-Tokens invalidiert
HomeOpenAllocations: Offene Zentralanmeldungen
HomeUpcomingSheets: Anstehende Übungsblätter
HomeUpcomingExams: Bevorstehende Prüfungen
NewsOpenAllocations: Offene Zentralanmeldungen
NewsUpcomingSheets: Anstehende Übungsblätter
NewsUpcomingExams: Bevorstehende Prüfungen
NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"}
CloseAlert: Schliessen
@ -886,9 +886,9 @@ MailSubjectSubmissionsUnassigned csh@CourseShorthand sheetName@SheetName: Abgabe
MailSubmissionsUnassignedIntro n@Int courseName@Text termDesc@Text sheetName@SheetName: #{n} Abgaben zu #{sheetName} im Kurs #{courseName} (#{termDesc}) konnten nicht automatisiert verteilt werden.
MailSubjectSheetSoonInactive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} kann nur noch kurze Zeit abgegeben werden
MailSheetSoonInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) endet in Kürze.
MailSheetSoonInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Abgabefrist für #{sheetName} im Kurs #{courseName} (#{termDesc}) endet in Kürze.
MailSubjectSheetInactive csh@CourseShorthand sheetName@SheetName: Abgabezeitraum für #{sheetName} in #{csh} abgelaufen
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName n@Int num@Int64: Die Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet. Es gab #{noneOneMoreDE n "Keine Abgaben" "Nur eine Abgabe von " (toMessage n <> " Abgaben von ")}#{noneOneMoreDE num "" "einem Teilnehmer" (toMessage num <> " Teilnehmern")}.
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName n@Int num@Int64: Die Abgabefrist für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet. Es gab #{noneOneMoreDE n "Keine Abgaben" "Nur eine Abgabe von " (toMessage n <> " Abgaben von ")}#{noneOneMoreDE num "" "einem Teilnehmer" (toMessage num <> " Teilnehmern")}.
MailSubjectCorrectionsAssigned csh@CourseShorthand sheetName@SheetName: Ihnen wurden Korrekturen zu #{sheetName} in #{csh} zugeteilt
MailCorrectionsAssignedIntro courseName@Text termDesc@Text sheetName@SheetName n@Int: #{n} #{pluralDE n "Abgabe wurde" "Abgaben wurden"} Ihnen zur Korrektur für #{sheetName} im Kurs #{courseName} (#{termDesc}) zugeteilt.
@ -1115,7 +1115,7 @@ InvalidRoute: Konnte URL nicht interpretieren
MenuOpenCourses: Kurse mit offener Registrierung
MenuOpenAllocations: Aktive Zentralanmeldungen
MenuHome: Aktuell
MenuNews: Aktuell
MenuInformation: Informationen
MenuLegal: Rechtliche Informationen
MenuDataProt: Datenschutzerklärung
@ -1186,7 +1186,7 @@ MenuTutorialEdit: Tutorium editieren
MenuTutorialComm: Mitteilung an Teilnehmer
MenuExamList: Prüfungen
MenuExamNew: Neue Prüfung anlegen
MenuExamEdit: Bearbeiten
MenuExamEdit: Prüfung bearbeiten
MenuExamUsers: Teilnehmer
MenuExamGrades: Prüfungsleistungen
MenuExamAddMembers: Prüfungsteilnehmer hinzufügen
@ -1272,8 +1272,8 @@ BreadcrumbExternalExamGrades: Prüfungsleistungen
BreadcrumbExternalExamStaffInvite: Einladung zum Prüfer
BreadcrumbParticipantsList: Kursteilnehmerlisten
BreadcrumbParticipants: Kursteilnehmerliste
BreadcrumbStorageKey: Lokalen Schlüssel generieren
BreadcrumbExamAutoOccurrence: Automatische Termin-/Raumverteilung
BreadcrumbStorageKey: Lokalen Schlüssel generieren
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn}
ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn}
@ -1399,10 +1399,10 @@ ExamRegistrationInviteHeading examn@ExamName: Einladung zum Teilnehmer für #{ex
ExamRegistrationInviteExplanation: Sie wurden eingeladen, Prüfungsteilnehmer zu sein.
ExamCorrectHeading examname@Text: Prüfungsergebnisse für #{examname} eintragen
ExamCorrectExamResultDelete: Prüfungsergebnis löschen
ExamCorrectHeadDate: Zeit
ExamCorrectHeadParticipant: Teilnehmer
ExamCorrectHeadParticipantTooltip: Geben Sie hier einen beliebigen eindeutigen Identifikator des Teilnehmers an. Definitiv eindeutig ist die Matrikelnummer des Teilnehmers, aber auch der Name oder ein Teil der Matrikelnummer können unter Umständen (je nach Liste aller Prüfungsteilnehmer) bereits eindeutig sein.
ExamCorrectHeadPart exampartnum@ExamPartNumber: #{exampartnum}
ExamCorrectHeadPartName exampartname@ExamPartName: #{exampartname}
ExamCorrectHeadStatus: Status
@ -1411,6 +1411,8 @@ ExamCorrectButtonSend: Senden
ExamCorrectErrorMultipleMatchingParticipants: Dem Identifikator konnten mehrere Prüfungsteilnehmer zugeordnet werden.
ExamCorrectErrorNoMatchingParticipants: Dem Identifikator konnte kein Prüfungsteilnehmer zugeordnet werden.
ExamCorrectErrorPartResultOutOfBounds examPartNumber@ExamPartNumber: Prüfungsergebnis für Teil #{examPartNumber} ist nicht größer Null.
ExamCorrectErrorPartResultOutOfBoundsMax examPartNumber@ExamPartNumber maxPoints@Points: Prüfungsergebnis für Teil #{examPartNumber} liegt nicht zwischen 0 und #{maxPoints}.
SubmissionUserInvitationAccepted shn@SheetName: Sie wurden als Mitabgebende(r) für eine Abgabe zu #{shn} eingetragen
SubmissionUserInvitationDeclined shn@SheetName: Sie haben die Einladung, Mitabgebende(r) für #{shn} zu werden, abgelehnt
@ -1553,9 +1555,9 @@ ExamDeregisterUntil: Abmeldung bis
ExamPublishOccurrenceAssignments: Termin- bzw. Raumzuteilung den Teilnehmern mitteilen um
ExamPublishOccurrenceAssignmentsTip: Ab diesem Zeitpunkt Teilnehmer einsehen zu welcher Teilprüfung bzw. welchen Raum sie angemeldet sind
ExamPublishOccurrenceAssignmentsParticipant: Termin- bzw. Raumzuteilung einsehbar ab
ExamFinished: Bewertung abgeschlossen ab
ExamFinished: Ergebnisse sichtbar ab
ExamFinishedOffice: Noten bekannt gegeben
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen
ExamFinishedParticipant: Bewertung voraussichtlich abgeschlossen
ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden
ExamClosed: Noten gemeldet
ExamClosedTip: Prüfungsbeauftraget, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert
@ -1580,7 +1582,6 @@ ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten
ExamBonusManual': Manuelle Berechnung
ExamGradesExplanation: Diese Ansicht zeigt die selben Daten an, wie die Tabelle von Prüfungsteilnehmern. Anpassen der Teilnehmerdaten und Ergebnisse ist nur dort möglich. Hier können Sie vor Allem einsehen und markieren, welche Prüfungsleistungen von den zuständigen Prüfungsbeauftragten bereits vollständig bearbeitet wurden.
ExamRegisterForOccurrence: Anmeldung zur Klausur erfolgt durch Anmeldung zu einem Termin/Raum
@ -1673,12 +1674,12 @@ ExamLoginToRegister: Um sich zum Kurs anzumelden müssen Sie zunächst in Uni2wo
ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen
ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen
ExamStartMustBeAfterPublishOccurrenceAssignments: Start muss nach Veröffentlichung der Termin- bzw. Raumzuordnung liegen
ExamStartMustBeAfterPublishOccurrenceAssignments: Beginn muss nach Veröffentlichung der Termin- bzw. Raumzuordnung liegen
ExamEndMustBeAfterStart: Beginn der Prüfung muss vor ihrem Ende liegen
ExamFinishedMustBeAfterEnd: "Bewertung abgeschlossen ab" muss nach Ende liegen
ExamFinishedMustBeAfterStart: "Bewertung abgeschlossen ab" muss nach Start liegen
ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Bewertung abgeschlossen ab" liegen
ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Start liegen
ExamFinishedMustBeAfterEnd: "Ergebnisse sichtbar ab" muss nach Ende liegen
ExamFinishedMustBeAfterStart: "Ergebnisse sichtbar ab" muss nach Beginn liegen
ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Ergebnisse sichtbar ab" liegen
ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Beginn liegen
ExamClosedMustBeAfterEnd: "Noten stehen fest ab" muss nach Ende liegen
ExamOccurrenceEndMustBeAfterStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss vor seinem Ende liegen

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.
Users: Users
HomeHeading: Home
NewsHeading: News
LoginHeading: Authentication
LoginTitle: Authentication
ProfileHeading: Settings
@ -507,9 +507,9 @@ NotificationSettingsHeading displayName: Notification settings for #{displayName
TokensLastReset: Tokens last reset
TokensResetSuccess: Successfully invalidated all authorisation tokens
HomeOpenAllocations: Active central allocations
HomeUpcomingSheets: Upcoming exercise sheets
HomeUpcomingExams: Upcoming exams
NewsOpenAllocations: Active central allocations
NewsUpcomingSheets: Upcoming exercise sheets
NewsUpcomingExams: Upcoming exams
NumCourses num: #{num} #{pluralEN num "course" "courses"}
CloseAlert: Close
@ -1114,7 +1114,7 @@ InvalidRoute: Could not interpret url
MenuOpenCourses: Courses with open registration
MenuOpenAllocations: Active central allocations
MenuHome: Home
MenuNews: News
MenuInformation: Information
MenuLegal: Legal
MenuDataProt: Data protection
@ -1185,7 +1185,7 @@ MenuTutorialEdit: Edit tutorial
MenuTutorialComm: Send course message
MenuExamList: Exams
MenuExamNew: Create new exam
MenuExamEdit: Edit
MenuExamEdit: Edit exam
MenuExamUsers: Participants
MenuExamGrades: Exam results
MenuExamAddMembers: Add exam participants
@ -1271,8 +1271,8 @@ BreadcrumbExternalExamGrades: Exam results
BreadcrumbExternalExamStaffInvite: Invitation
BreadcrumbParticipantsList: Lists of course participants
BreadcrumbParticipants: Course participants
BreadcrumbStorageKey: Generate storage key
BreadcrumbExamAutoOccurrence: Automatic occurrence/room distribution
BreadcrumbStorageKey: Generate storage key
ExternalExamEdit coursen examn: Edit: #{coursen}, #{examn}
ExternalExamGrades coursen examn: Exam achievements: #{coursen}, #{examn}
@ -1400,7 +1400,6 @@ ExamCorrectHeading examname: Submit corrections for #{examname}
ExamCorrectHeadDate: Time
ExamCorrectHeadParticipant: Participant
ExamCorrectHeadParticipantTooltip: Enter any string that uniquely identifies the participant. Their matriculation number is definitely unique, but also their name or a part of their matriculation number may already be unique for this participant (depending on the list of all participants).
ExamCorrectHeadPart exampartnum: #{exampartnum}
ExamCorrectHeadPartName exampartname: #{exampartname}
ExamCorrectHeadStatus: Status
@ -1409,6 +1408,10 @@ ExamCorrectButtonSend: Submit
ExamCorrectErrorMultipleMatchingParticipants: This identifier matches on multiple exam participants.
ExamCorrectErrorNoMatchingParticipants: This identifier does not match on any exam participant.
ExamCorrectErrorPartResultOutOfBounds examPartNumber: Exam part result for #{examPartNumber} ist not greater zero.
ExamCorrectErrorPartResultOutOfBoundsMax examPartNumber maxPoints: Exam part result for #{examPartNumber} is not between 0 and #{maxPoints}.
ExamCorrectExamResultDelete: Delete exam result
SubmissionUserInvitationAccepted shn: You now participate in a submission for #{shn}
SubmissionUserInvitationDeclined shn: You have declined the invitation to participate in a submission for #{shn}
@ -1551,7 +1554,7 @@ ExamDeregisterUntil: Deregister until
ExamPublishOccurrenceAssignments: Publish occurrence/room-assignments
ExamPublishOccurrenceAssignmentsTip: At this time participants are informed to which occurrence/room they are assigned
ExamPublishOccurrenceAssignmentsParticipant: Occurrence/room-assignments published
ExamFinished: Marking finished
ExamFinished: Results visible from
ExamFinishedOffice: Exam achievements published
ExamFinishedParticipant: Marking expected to be finished
ExamFinishedTip: At this participants are informed of their exam achievements
@ -1578,7 +1581,6 @@ ExamBonusRule: Bonus points from exercises
ExamNoBonus': No automatic exam bonus
ExamBonusPoints': Compute from exercise achievements
ExamBonusManual': Manual computation
ExamGradesExplanation: This view shows the same data as the table of exam participants. Changing participant's data and achievements is only possible via the table of exam participants. Primarily, this view allows you to check and adjust which exam achievements were properly handled by the relevant exam offices.
ExamRegisterForOccurrence: Registration for this exam is done by registering for an occurrence/room
@ -1631,6 +1633,7 @@ ExamFormCorrection: Correction
ExamFormParts: Exam parts
ExamCorrectors: Correctors
ExamCorrectorsTip: Correctors configured here may, after the start of the exam and until "Results visible from", enter exam part results for all exam parts and participants.
ExamCorrectorAlreadyAdded: A corrector with this email address already exists
ExamParts: Exam parts/questions
@ -1672,9 +1675,9 @@ ExamRegisterToMustBeAfterRegisterFrom: "Register to" must be after "register fro
ExamDeregisterUntilMustBeAfterRegisterFrom: "Deregister until" must be after "register from"
ExamStartMustBeAfterPublishOccurrenceAssignments: "Start" must be after "publish occurrence/room-assignments"
ExamEndMustBeAfterStart: "End" must be after "start"
ExamFinishedMustBeAfterEnd: "Marking finished" must be after "end"
ExamFinishedMustBeAfterStart: "Marking finished" must be after "start"
ExamClosedMustBeAfterFinished: "Exam achievements registered" must be after "marking finished"
ExamFinishedMustBeAfterEnd: "Results visible from" must be after "end"
ExamFinishedMustBeAfterStart: "Results visible from" must be after "start"
ExamClosedMustBeAfterFinished: "Exam achievements registered" must be after "results visible from"
ExamClosedMustBeAfterStart: "Exam achievements registered" must be after "start"
ExamClosedMustBeAfterEnd: "Exam achievements registered" must be after "end"

View File

@ -789,5 +789,18 @@
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/pageactions/pageactions.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
]
}

4
routes
View File

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

View File

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

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
| otherwise = lang
where
mr = renderMessage foundation ls
mr = renderMessage foundation $ lang : filter (/= lang) ls
embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel

View File

@ -11,5 +11,5 @@ import Handler.Exam.Edit as Handler.Exam
import Handler.Exam.Show as Handler.Exam
import Handler.Exam.Users as Handler.Exam
import Handler.Exam.AddUser as Handler.Exam
import Handler.Exam.Correct as Handler.Exam
import Handler.Exam.AutoOccurrence as Handler.Exam
import Handler.Exam.Correct as Handler.Exam

View File

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

View File

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

View File

@ -1089,5 +1089,6 @@ postEUsersR tid ssh csh examn = do
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do
setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading
let computedValuesTip = $(i18nWidgetFile "exam-users/computed-values-tip")
let computedValuesTip = notificationWidget NotificationBroad Warning
$(i18nWidgetFile "exam-users/computed-values-tip")
$(widgetFile "exam-users")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -630,6 +630,15 @@ assertM' f x = x <$ guard (f x)
guardOn :: Alternative m => Bool -> a -> m a
guardOn b x = x <$ guard b
guardOnM :: Alternative m => Bool -> m a -> m a
guardOnM b x = guard b *> x
guardMOn :: MonadPlus m => m Bool -> a -> m a
guardMOn b x = x <$ guardM b
guardMOnM :: MonadPlus m => m Bool -> m a -> m a
guardMOnM b x = guardM b *> x
-- Some Utility Functions from Agda.Utils.Monad
-- | Monadic if-then-else.
ifM :: Monad m => m Bool -> m a -> m a -> m a

View File

@ -46,6 +46,7 @@ import Data.Scientific
import Data.Time.Clock (NominalDiffTime, nominalDay)
import Utils
import Utils.Frontend.Notification
-- import Utils.Message
-- import Utils.PathPiece
-- import Utils.Route
@ -869,26 +870,15 @@ wformMessage :: (MonadHandler m) => Message -> WForm m ()
wformMessage = void . aFormToWForm . aformMessage
formMessage :: (MonadHandler m) => Message -> MForm m (FormResult (), FieldView site)
formMessage Message{..} = do
formMessage msg = do
return (FormSuccess (), FieldView
{ fvLabel = mempty
, fvTooltip = Nothing
, fvId = idFormMessageNoinput
, fvErrors = Nothing
, fvRequired = False
, fvInput = [whamlet|
$newline never
<div .notification .notification-#{toPathPiece messageStatus} .fa-#{maybe defaultIcon iconText messageIcon}>
<div .notification__content>
#{messageContent}
|]
, fvInput = notification NotificationNarrow msg
})
where
defaultIcon = case messageStatus of
Success -> "check-circle"
Info -> "info-circle"
Warning -> "exclamation-circle"
Error -> "exclamation-triangle"
---------------------
-- 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
| IconApplicationFiles
| IconTooltipDefault
deriving (Eq, Ord, Enum, Bounded, Show, Read)
| IconNotificationSuccess
| IconNotificationInfo
| IconNotificationWarning
| IconNotificationError
| IconFavourite
| IconLanguage
| IconNavContainerClose | IconPageActionChildrenClose
| IconMenuNews
| IconMenuHelp
| IconMenuProfile
| IconMenuLogin | IconMenuLogout
| IconBreadcrumbsHome
| IconMenuExtra
| IconMenuCourseList
| IconMenuCorrections
| IconMenuExams
| IconMenuAdmin
| IconPageActionPrimaryExpand | IconPageActionSecondary
| IconBreadcrumbSeparator
deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic, Typeable)
iconText :: Icon -> Text
iconText = \case
IconNew -> "seedling"
IconOK -> "check"
IconNotOK -> "times"
IconWarning -> "exclamation"
IconProblem -> "bolt"
IconVisible -> "eye"
IconInvisible -> "eye-slash"
IconCourse -> "graduation-cap"
IconEnrolTrue -> "user-plus"
IconEnrolFalse -> "user-slash"
IconPlanned -> "cog"
IconAnnounce -> "bullhorn"
IconExam -> "poll-h"
IconExamRegisterTrue -> "calendar-check"
IconExamRegisterFalse -> "calendar-times"
IconCommentTrue -> "comment-alt"
IconCommentFalse -> "comment-slash" -- comment-alt-slash is not available for free
IconLink -> "link"
IconFileDownload -> "file-download"
IconFileUpload -> "file-upload"
IconFileZip -> "file-archive"
IconFileCSV -> "file-csv"
IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar)
IconSFTHint -> "life-ring" -- for SheetFileType only
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope"
IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash"
IconNoCorrectors -> "user-slash"
IconApplicationVeto -> "times"
IconApplicationFiles -> "file-alt"
IconTooltipDefault -> "question-circle"
IconNew -> "seedling"
IconOK -> "check"
IconNotOK -> "times"
IconWarning -> "exclamation"
IconProblem -> "bolt"
IconVisible -> "eye"
IconInvisible -> "eye-slash"
IconCourse -> "graduation-cap"
IconEnrolTrue -> "user-plus"
IconEnrolFalse -> "user-slash"
IconPlanned -> "cog"
IconAnnounce -> "bullhorn"
IconExam -> "poll-h"
IconExamRegisterTrue -> "calendar-check"
IconExamRegisterFalse -> "calendar-times"
IconCommentTrue -> "comment-alt"
IconCommentFalse -> "comment-alt-slash"
IconLink -> "link"
IconFileDownload -> "file-download"
IconFileUpload -> "file-upload"
IconFileZip -> "file-archive"
IconFileCSV -> "file-csv"
IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar)
IconSFTHint -> "life-ring" -- for SheetFileType only
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope"
IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash"
IconNoCorrectors -> "user-slash"
IconApplicationVeto -> "times"
IconApplicationFiles -> "file-alt"
IconTooltipDefault -> "question-circle"
IconNotificationSuccess -> "check-circle"
IconNotificationInfo -> "info-circle"
IconNotificationWarning -> "exclamation-circle"
IconNotificationError -> "exclamation-triangle"
IconFavourite -> "star"
IconLanguage -> "flag-alt"
IconNavContainerClose -> "chevron-up"
IconPageActionChildrenClose -> "chevron-up"
IconMenuNews -> "megaphone"
IconMenuHelp -> "question"
IconMenuProfile -> "cogs"
IconMenuLogin -> "sign-in-alt"
IconMenuLogout -> "sign-out-alt"
IconBreadcrumbsHome -> "home"
IconMenuExtra -> "ellipsis-h"
IconMenuCourseList -> "graduation-cap"
IconMenuCorrections -> "check"
IconMenuExams -> "poll-h"
IconMenuAdmin -> "screwdriver"
IconPageActionPrimaryExpand -> "bars"
IconPageActionSecondary -> "ellipsis-h"
IconBreadcrumbSeparator -> "angle-right"
instance Universe Icon
instance Finite Icon

View File

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

View File

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

View File

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

View File

@ -1,5 +1,11 @@
$newline never
$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 -->
^{asidenav}
<!-- navigation -->
@ -15,7 +21,7 @@ $if not isModal
$if not isModal
<!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute
$if not $ Just NewsR == mcurrentRoute
^{breadcrumbsWgt}
<div .main__content-body>
@ -26,7 +32,7 @@ $if not isModal
<a .breadcrumbs__link href="@{fst back}">#{snd back} -->
^{headline}
$if not isModal && hasPageActions
$if hasPageActions
<!-- page actions -->
^{pageaction}

View File

@ -1,24 +1,18 @@
$newline never
<section>
^{examCorrectExplanation}
<section>
<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>
<tr .table__row .table__row--head>
<th .table__th .uw-exam-correct--date-cell uw-exam-correct-header="date" uw-hide-column-header="date">
_{MsgExamCorrectHeadDate}
<th .table__th .uw-exam-correct--user-cell uw-exam-correct-header="user" uw-hide-column-header="user">
_{MsgExamCorrectHeadParticipant}
^{iconTooltip participantHeadTooltip Nothing True}
$forall ExamPart{examPartNumber,examPartName} <- examParts
$forall ExamPart{examPartNumber} <- examParts
<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}
$if mayEditResults
<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
<td .table__td .uw-exam-correct--part-cell>
^{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
<td .table__td #uw-exam-correct__result>
<select>
@ -52,6 +48,8 @@ $newline never
_{MsgExamResultVoided}
<option value="no-show">
_{MsgExamResultNoShow}
<option value="delete">
_{MsgExamCorrectExamResultDelete}
<td .table__td #uw-exam-correct__result__grade>
<select>
$forall grade <- (toPathPiece <$> examGrades)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,23 @@
$newline never
<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.
<p>
Bedenken Sie, dass die Meldung der Prüfungsleistungen direkt in Uni2work den #
Verwaltungsaufwand (und die damit verbunden Dauer) für die ordnungsgemäße #
Anrechung Ihrer Leistungen drastisch reduziert.
<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 />
Dies geschieht nur in begründeten Einzelfällen (z.B. bei Studierenden im #
ERASMUS-Programm).
<p>
Nutzer, die unabhängig von diesen Einstellungen, Einsicht in Ihre #
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 #
aufgeführt sind).
außerhalb von Uni2work weitermelden
$if hasForced
<p>
Wenn Sie der Meldung an einzelne Prüfungsämter nicht widersprechen können, #
so hat das jeweilige Prüfungsamt angegeben, dass die Einsicht, entweder #
Wenn Sie der Meldung an einzelne Prüfungsverwalter nicht widersprechen können, #
so hat der jeweilige Prüfungsverwalter angegeben, dass die Einsicht, entweder #
aufgrund einer Ihrer Studiengänge (z.B. aufgrund der Studienordnung) oder #
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
<div .notification .notification-warning .fa-exclamation-triangle .notification--broad>
<div .notification__content>
<p>
Die Tabelle enthält Werte, die automatisch berechnet wurden.
<p>
Automatisch berechnete Werte (Bonus und Prüfungsergebnis) werden weder dem #
entsprechenden Teilnehmer angezeigt, noch an das Prüfungsamt gemeldet #
bevor sie manuell übernommen wurden.<br />
Hierzu können Sie die Aktion „Berechnetes Prüfungsergebnis übernehmen“ #
verwenden.
<p>
Sie können die automatisch berechneten Werte auch manuell (via CSV-Import) #
überschreiben.<br />
Wenn die so gesetzten Werte nicht den automatisch Berechneten entsprechen #
sind sie <i>inkonsistent</i>.
<p>
Automatisch berechnete Werte sind gekennzeichnet wie folgt:
<p>
Die Tabelle enthält Werte, die automatisch berechnet wurden.
<p>
Automatisch berechnete Werte (Bonus und Prüfungsergebnis) werden weder dem #
entsprechenden Teilnehmer angezeigt, noch an Prüfungsverwalter gemeldet #
bevor sie manuell übernommen wurden.<br />
Hierzu können Sie die Aktion „Berechnetes Prüfungsergebnis übernehmen“ #
verwenden.
<p>
Sie können die automatisch berechneten Werte auch manuell (via CSV-Import) #
überschreiben.<br />
Wenn die so gesetzten Werte nicht den automatisch Berechneten entsprechen #
sind sie <i>inkonsistent</i>.
<p>
Automatisch berechnete Werte sind gekennzeichnet wie folgt:
<table style="font-weight: normal">
<tr>
<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 0 0 7px" .table__td .table__td--overriden>Inkonsistent
<table style="font-weight: normal">
<tr>
<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 0 0 7px" .table__td .table__td--overriden>Inkonsistent

View File

@ -1,23 +1,21 @@
$newline never
<div .notification .notification-warning .fa-exclamation-triangle .notification--broad>
<div .notification__content>
<p>
This table contains values that were computed automatically.
<p>
Values computed automatically (bonus and result) are shown to neither the #
participant nor relevant exam offices until they are manually accepted.<br />
To do this you may use the action “Accept computed result”.
<p>
You are also able to override the automatically computed values manually #
(via CSV import).<br />
<p>
This table contains values that were computed automatically.
<p>
Values computed automatically (bonus and result) are shown to neither the #
participant nor relevant exam offices until they are manually accepted.<br />
To do this you may use the action “Accept computed result”.
<p>
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 #
they are considered <i>inconsistent</i>.
<p>
Automatically computed values are marked as follows:
If values thus overriden do not match the automatically computed values #
they are considered <i>inconsistent</i>.
<p>
Automatically computed values are marked as follows:
<table style="font-weight: normal">
<tr>
<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 0 0 7px" .table__td .table__td--overriden>Inconsistent
<table style="font-weight: normal">
<tr>
<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 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 #
einer Prüfung direkt in Uni2work hinterlegen.<br />
Dies dient sowohl der Rückmeldung an den jeweiligen Studierenden als auch #
können Prüfungsämter, mit den notwendigen Berechtigungen, die #
können Prüfungsverwalter, mit den notwendigen Berechtigungen, die #
Prüfungsleistungen direkt aus Uni2work mit ihren eigenen Verwaltungssystemen #
synchronisieren.

View File

@ -5,7 +5,7 @@ $newline never
benötigen um sicherzustellen, dass Studienmodalitäten erfüllt und Leistungen #
korrekt anerkannt werden.<br />
<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 />
Diese Benutzer haben dann u.A. Zugriff auf alle beim relevanten Institut #
erbrachten und in Uni2work hinterlegten Prüfungsleistungen.

View File

@ -312,9 +312,9 @@ $newline text
Anmeldezeitraums anmelden.
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>
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.
<dt .deflist__dt> ^{plannedFeat} Korrekturen
@ -333,7 +333,7 @@ $newline text
<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.
<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>
^{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
<dd .deflist__dd>
<p>
Endnoten können automatisiert an die Prüfungsämter gemeldet werden.
Endnoten können automatisiert an die Prüfungsverwaltung gemeldet werden.
<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>
^{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>
^{newU2WFeat} Dozenten dürfen nach der Übergabe an das Prüfungsamt
^{newU2WFeat} Dozenten dürfen nach der Übergabe an die Prüfungsverwaltung
nachträgliche Änderungen an den Prüfungen vornehmen.
Diejenigen Prüfungsämter, die von der nachträglichen Änderung betroffen sind, erhalten
Diejenigen Prüfungsverwalter, die von der nachträglichen Änderung betroffen sind, erhalten
hierüber automatisiert eine Benachrichtigung.

View File

@ -303,7 +303,7 @@ $newline text
<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).
as exam results, which can afterwards be sent to the relevant exam offices.
<p>
^{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
<dd .deflist__dd>
<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>
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>
^{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 />
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.
to the exam offices. The offices that are affected by these changes will be automatically notified.
<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
<section>
<h2>_{MsgHomeUpcomingExams}
<h2>_{MsgNewsUpcomingExams}
$if hasExams
^{examTable}
$else

View File

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

View File

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

View File

@ -1,10 +1,17 @@
$newline never
<div .breadcrumbs__container>
<a .breadcrumbs__home href=@{NewsR}>
<i .fas .fa-fw .fa-#{iconText IconBreadcrumbsHome}>
<ul .breadcrumbs__list.list--inline>
$forall (bcRoute, bcTitle, hasAccess) <- parents
<li .breadcrumbs__item>
$if hasAccess
<a .breadcrumbs__link href="@{bcRoute}">#{bcTitle}
<a .breadcrumbs__link href="@{bcRoute}">
#{bcTitle}
$else
<span .breadcrumbs__link>#{bcTitle}
<li .breadcrumbs__last-item>#{title}
<span .breadcrumbs__link>
#{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
<footer .footer>
<div .footer-links>
$forall (MenuItem{menuItemType, menuItemRoute = _, menuItemIcon = _, menuItemLabel, menuItemModal = _}, menuIdent, route) <- menuTypes
$case menuItemType
$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 _
<ul .footer-links .list--inline>
$forall n <- filter isNavFooter nav
^{navWidget n}

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
<a .navbar__link-wrapper href=#{route} ##{menuIdent}>
$if isJust menuItemIcon
<div .navbar__link-icon>
<i .fas .fa-2x .fa-#{fromMaybe "none" menuItemIcon}>
<div .navbar__link-label>_{SomeMessage menuItemLabel}
<a .navbar__link-wrapper href=#{route} ##{ident}>
<div .navbar__link-icon>
<i .fas .fa-2x .fa-#{iconText navIcon}>
<div .navbar__link-label>_{SomeMessage navLabel}

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
<div .navbar-container>
<div .navbar-shadow>
<nav .navbar.js-sticky-navbar>
<nav .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>
$# 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-star>
<div .navbar__link-label>_{MsgNavigationFavourites}
$forall n <- filter isNavHeaderPrimary nav
$case view _1 n
$of NavHeader{ navLink }
<li .navbar__list-item :highlightNav navLink:.navbar__list-item--active>
^{navWidget n}
$of NavHeaderContainer{}
<li .navbar__list-item.navbar__list-item--container-selector>
^{navWidget n}
$of _
$forall (menuItem@MenuItem{menuItemType, menuItemRoute, menuItemModal}, menuIdent, _) <- menuTypes
$case menuItemType
$of NavbarAside
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal
^{navbarModal (menuItem, menuIdent)}
$else
^{navbarItem (menuItem, menuIdent)}
$of _
<ul .navbar__list>
$forall n <- filter isNavHeaderSecondary $ reverse nav
$case view _1 n
$of NavHeader{ navLink }
<li .navbar__list-item :highlightNav navLink:.navbar__list-item--active>
^{navWidget n}
$of NavHeaderContainer{}
<li .navbar__list-item.navbar__list-item--container-selector>
^{navWidget n}
$of _
<ul .navbar__list.list--inline>
$forall (menuItem@MenuItem{menuItemType, menuItemRoute, menuItemModal}, menuIdent, _) <- menuTypes
$case menuItemType
$of NavbarRight
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal
^{navbarModal (menuItem, menuIdent)}
$else
^{navbarItem (menuItem, menuIdent)}
$of NavbarSecondary
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal
^{navbarModal (menuItem, menuIdent)}
$else
^{navbarItem (menuItem, menuIdent)}
$of _
<li .navbar__list-item--lang-wrapper uw-language-select>
<input type="checkbox" id="lang-checkbox" uw-no-checkbox>
<div id="lang-dropdown">
^{langFormView'}
<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}
$forall n@(NavHeaderContainer{ navHeaderRole }, containerIdent, _, ns) <- filter isNavHeaderContainer nav
<div .navbar__container-list :navHeaderRole == NavHeaderPrimary:.navbar__container-list--left ##{containerIdent}-container>
<label .navbar__container-list-closer for=container-radio-none>
<i .fas .fa-fw .fa-#{iconText IconNavContainerClose}>
<ul>
$forall iN@(nl, _, _) <- ns
<li .navbar__container-list-item :highlightNav nl:.navbar__container-list-item--active>
^{navContainerItemWidget n iN}
$# <li .navbar__list-item--lang-wrapper uw-language-select>
$# <input type="checkbox" id="lang-checkbox" uw-no-checkbox>
$# <div id="lang-dropdown">
$# ^{langFormView'}
$# <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
<div .pagenav>
$if hasPrimaryPageActions
<div .pagenav-prime>
$forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes
$case menuItemType
$of PageActionPrime
<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 || hasPrimarySubActions
<input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-none type=radio checked>
<ul .pagenav>
$forall n <- filter isPageActionPrimary nav
<li .pagenav__list-item>
^{navWidget n}
$if hasSecondaryPageActions
<div .pagenav-secondary>
<div .pagenav-secondary__list>
$forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes
$case menuItemType
$of PageActionSecondary
<div .pagenav__list-item.pagenav__list-item--secondary>
$if menuItemModal
<div uw-modal data-modal-trigger=#{menuIdent} data-modal-closeable>
<a .pagenav__link-wrapper.pagenav__link-wrapper--secondary href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
$of _
<li .pagenav__list-item .pagenav-secondary>
<input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-secondary type=radio>
<label .pagenav-item__expand-label for=pageaction-item__expand-secondary>
<i .fas .fa-fw .fa-#{iconText IconPageActionSecondary}>
<div .pagenav-item__children-wrapper>
<ul .pagenav-item__children>
$forall n <- filter isPageActionSecondary nav
<li>
^{navWidget n}
<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}