feat(allocations): upload of priorities
This commit is contained in:
parent
2735d465eb
commit
a590f45cc1
@ -33,3 +33,35 @@ fieldset
|
|||||||
|
|
||||||
.label-pagesize
|
.label-pagesize
|
||||||
margin-right: 13px
|
margin-right: 13px
|
||||||
|
|
||||||
|
.explained-selection-field
|
||||||
|
display: flex
|
||||||
|
flex-flow: row wrap
|
||||||
|
margin: -5px
|
||||||
|
|
||||||
|
.explained-selection-field__option
|
||||||
|
display: grid
|
||||||
|
grid-gap: 5px 7px
|
||||||
|
grid-template-columns: 30px 1fr
|
||||||
|
grid-template-rows: 2em 1fr
|
||||||
|
grid-template-areas: 'radiobox title' '. explanation'
|
||||||
|
margin: 5px
|
||||||
|
max-width: 500px
|
||||||
|
|
||||||
|
.explained-selection-field__input
|
||||||
|
grid-area: radiobox
|
||||||
|
place-self: center center
|
||||||
|
width: 20px
|
||||||
|
height: 20px
|
||||||
|
|
||||||
|
.explained-selection-field__label
|
||||||
|
grid-area: title
|
||||||
|
place-self: center stretch
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
.explained-selection-field__explanation
|
||||||
|
grid-area: explanation
|
||||||
|
place-self: stretch stretch
|
||||||
|
font-weight: 600
|
||||||
|
font-size: .9rem
|
||||||
|
color: var(--color-fontsec)
|
||||||
|
|||||||
@ -2,25 +2,33 @@ import { Utility } from '../../core/utility';
|
|||||||
import './checkbox.sass';
|
import './checkbox.sass';
|
||||||
|
|
||||||
var CHECKBOX_CLASS = 'checkbox';
|
var CHECKBOX_CLASS = 'checkbox';
|
||||||
|
var RADIOBOX_CLASS = 'radiobox';
|
||||||
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
|
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
|
||||||
|
|
||||||
@Utility({
|
@Utility({
|
||||||
selector: 'input[type="checkbox"]:not([uw-no-checkbox])',
|
selector: 'input[type="checkbox"]:not([uw-no-checkbox]), input[type="radio"]:not([uw-no-radiobox])',
|
||||||
})
|
})
|
||||||
export class Checkbox {
|
export class Checkbox {
|
||||||
|
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Checkbox utility cannot be setup without an element!');
|
throw new Error('Checkbox utility cannot be setup without an element!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRadio = element.type === 'radio';
|
||||||
|
const box_class = isRadio ? RADIOBOX_CLASS : CHECKBOX_CLASS;
|
||||||
|
|
||||||
|
if (isRadio && element.closest('.radio-group')) {
|
||||||
|
// Don't initialize radiobox, if radio is part of a group
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) {
|
if (element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) {
|
||||||
// throw new Error('Checkbox utility already initialized!');
|
// throw new Error('Checkbox utility already initialized!');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.parentElement.classList.contains(CHECKBOX_CLASS)) {
|
if (element.parentElement.classList.contains(box_class)) {
|
||||||
// throw new Error('Checkbox element\'s wrapper already has class '' + CHECKBOX_CLASS + ''!');
|
// throw new Error('Checkbox element\'s wrapper already has class '' + box_class + ''!');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +36,7 @@ export class Checkbox {
|
|||||||
var parentEl = element.parentElement;
|
var parentEl = element.parentElement;
|
||||||
|
|
||||||
var wrapperEl = document.createElement('div');
|
var wrapperEl = document.createElement('div');
|
||||||
wrapperEl.classList.add(CHECKBOX_CLASS);
|
wrapperEl.classList.add(box_class);
|
||||||
|
|
||||||
var labelEl = document.createElement('label');
|
var labelEl = document.createElement('label');
|
||||||
labelEl.setAttribute('for', element.id);
|
labelEl.setAttribute('for', element.id);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// CUSTOM CHECKBOXES
|
// CUSTOM CHECKBOXES
|
||||||
// Completely replaces legacy checkbox
|
// Completely replaces legacy checkbox
|
||||||
.checkbox [type='checkbox'], #lang-checkbox
|
.checkbox [type='checkbox'], .radiobox [type='radio'], #lang-checkbox
|
||||||
position: fixed
|
position: fixed
|
||||||
top: -1px
|
top: -1px
|
||||||
left: -1px
|
left: -1px
|
||||||
@ -9,7 +9,7 @@
|
|||||||
overflow: hidden
|
overflow: hidden
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
.checkbox
|
.checkbox, .radiobox
|
||||||
position: relative
|
position: relative
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
|
||||||
@ -17,47 +17,77 @@
|
|||||||
display: block
|
display: block
|
||||||
height: 20px
|
height: 20px
|
||||||
width: 20px
|
width: 20px
|
||||||
background-color: #f3f3f3
|
background-color: var(--color-grey-lighter)
|
||||||
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05)
|
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05)
|
||||||
border: 2px solid var(--color-primary)
|
border: 2px solid var(--color-primary)
|
||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
color: white
|
color: white
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
label::before,
|
&.radiobox label
|
||||||
label::after
|
border-radius: 10px
|
||||||
position: absolute
|
|
||||||
display: block
|
|
||||||
top: 12px
|
|
||||||
left: 8px
|
|
||||||
height: 2px
|
|
||||||
width: 8px
|
|
||||||
background-color: var(--color-font)
|
|
||||||
|
|
||||||
\:checked + label
|
|
||||||
background-color: var(--color-primary)
|
|
||||||
|
|
||||||
[type='checkbox']:focus + label
|
&.checkbox
|
||||||
border-color: #3273dc
|
label::before,
|
||||||
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25)
|
label::after
|
||||||
outline: 0
|
position: absolute
|
||||||
|
display: block
|
||||||
|
top: 12px
|
||||||
|
left: 8px
|
||||||
|
height: 2px
|
||||||
|
width: 8px
|
||||||
|
background-color: var(--color-font)
|
||||||
|
|
||||||
\:checked + label::before,
|
\:checked + label
|
||||||
:checked + label::after
|
background-color: var(--color-primary)
|
||||||
content: ''
|
|
||||||
|
|
||||||
\:checked + label::before
|
[type='checkbox']:focus + label
|
||||||
background-color: white
|
border-color: #3273dc
|
||||||
transform: rotate(45deg)
|
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25)
|
||||||
left: 2px
|
outline: 0
|
||||||
top: 11px
|
|
||||||
|
|
||||||
\:checked + label::after
|
\:checked + label::before,
|
||||||
background-color: white
|
:checked + label::after
|
||||||
transform: rotate(-45deg)
|
content: ''
|
||||||
top: 9px
|
|
||||||
width: 12px
|
\:checked + label::before
|
||||||
left: 7px
|
background-color: white
|
||||||
|
transform: rotate(45deg)
|
||||||
|
left: 2px
|
||||||
|
top: 11px
|
||||||
|
|
||||||
|
\:checked + label::after
|
||||||
|
background-color: white
|
||||||
|
transform: rotate(-45deg)
|
||||||
|
top: 9px
|
||||||
|
width: 12px
|
||||||
|
left: 7px
|
||||||
|
|
||||||
|
&.radiobox
|
||||||
|
label::before
|
||||||
|
position: absolute
|
||||||
|
display: block
|
||||||
|
top: 6.5px
|
||||||
|
left: 6.5px
|
||||||
|
height: 7px
|
||||||
|
width: 7px
|
||||||
|
border-radius: 3.5px
|
||||||
|
background-color: var(--color-font)
|
||||||
|
|
||||||
|
\:checked + label
|
||||||
|
background-color: var(--color-primary)
|
||||||
|
|
||||||
|
[type='radio']:focus + label
|
||||||
|
border-color: #3273dc
|
||||||
|
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25)
|
||||||
|
outline: 0
|
||||||
|
|
||||||
|
\:checked + label::before
|
||||||
|
content: ''
|
||||||
|
|
||||||
|
\:checked + label::before
|
||||||
|
background-color: white
|
||||||
|
|
||||||
[disabled] + label
|
[disabled] + label
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Checkbox } from './checkbox';
|
|||||||
import { FileInput } from './file-input';
|
import { FileInput } from './file-input';
|
||||||
|
|
||||||
import './inputs.sass';
|
import './inputs.sass';
|
||||||
import './radio.sass';
|
import './radio-group.sass';
|
||||||
|
|
||||||
export const InputUtils = [
|
export const InputUtils = [
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
|||||||
55
frontend/src/utils/inputs/radio-group.sass
Normal file
55
frontend/src/utils/inputs/radio-group.sass
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// CUSTOM RADIO BOXES
|
||||||
|
// Completely replaces native radiobox
|
||||||
|
|
||||||
|
.radio-group
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
.radio
|
||||||
|
position: relative
|
||||||
|
display: inline-block
|
||||||
|
|
||||||
|
[type='radio']
|
||||||
|
position: fixed
|
||||||
|
top: -1px
|
||||||
|
left: -1px
|
||||||
|
width: 1px
|
||||||
|
height: 1px
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
label
|
||||||
|
display: block
|
||||||
|
height: 34px
|
||||||
|
min-width: 42px
|
||||||
|
line-height: 34px
|
||||||
|
text-align: center
|
||||||
|
padding: 0 13px
|
||||||
|
background-color: #f3f3f3
|
||||||
|
box-shadow: inset 2px 1px 2px 1px rgba(50, 50, 50, 0.05)
|
||||||
|
color: var(--color-font)
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
\:checked + label
|
||||||
|
background-color: var(--color-primary)
|
||||||
|
color: var(--color-lightwhite)
|
||||||
|
box-shadow: inset -2px -1px 2px 1px rgba(255, 255, 255, 0.15)
|
||||||
|
|
||||||
|
\:focus + label
|
||||||
|
border-color: #3273dc
|
||||||
|
box-shadow: 0 0 0.125em 0 rgba(50, 115, 220, 0.8)
|
||||||
|
outline: 0
|
||||||
|
|
||||||
|
[disabled] + label
|
||||||
|
pointer-events: none
|
||||||
|
border: none
|
||||||
|
opacity: 0.6
|
||||||
|
filter: grayscale(1)
|
||||||
|
|
||||||
|
.radio:first-child
|
||||||
|
label
|
||||||
|
border-top-left-radius: 4px
|
||||||
|
border-bottom-left-radius: 4px
|
||||||
|
|
||||||
|
.radio:last-child
|
||||||
|
label
|
||||||
|
border-top-right-radius: 4px
|
||||||
|
border-bottom-right-radius: 4px
|
||||||
47
frontend/src/utils/inputs/radio.js
Normal file
47
frontend/src/utils/inputs/radio.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Utility } from '../../core/utility';
|
||||||
|
import './radio.sass';
|
||||||
|
|
||||||
|
var RADIO_CLASS = 'radiobox';
|
||||||
|
var RADIO_INITIALIZED_CLASS = 'radio--initialized';
|
||||||
|
|
||||||
|
@Utility({
|
||||||
|
selector: 'input[type="radio"]:not([uw-no-radio])',
|
||||||
|
})
|
||||||
|
export class Radio {
|
||||||
|
|
||||||
|
constructor(element) {
|
||||||
|
if (!element) {
|
||||||
|
throw new Error('Radio utility cannot be setup without an element!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.closest('.radio-group')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.classList.contains(RADIO_INITIALIZED_CLASS)) {
|
||||||
|
// throw new Error('Radio utility already initialized!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.parentElement.classList.contains(RADIO_CLASS)) {
|
||||||
|
// throw new Error('Radio element\'s wrapper already has class '' + RADIO_CLASS + ''!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var siblingEl = element.nextSibling;
|
||||||
|
var parentEl = element.parentElement;
|
||||||
|
|
||||||
|
var wrapperEl = document.createElement('div');
|
||||||
|
wrapperEl.classList.add(RADIO_CLASS);
|
||||||
|
|
||||||
|
var labelEl = document.createElement('label');
|
||||||
|
labelEl.setAttribute('for', element.id);
|
||||||
|
|
||||||
|
wrapperEl.appendChild(element);
|
||||||
|
wrapperEl.appendChild(labelEl);
|
||||||
|
|
||||||
|
parentEl.insertBefore(wrapperEl, siblingEl);
|
||||||
|
|
||||||
|
element.classList.add(RADIO_INITIALIZED_CLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,55 +1,55 @@
|
|||||||
// CUSTOM RADIO BOXES
|
// GROUPS OF RADIO BUTTONS
|
||||||
// Completely replaces native radiobox
|
// Completely replaces native radiobox
|
||||||
|
|
||||||
.radio-group
|
.radio-group
|
||||||
display: flex
|
display: flex
|
||||||
|
|
||||||
.radio
|
& > .radio
|
||||||
position: relative
|
position: relative
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
|
||||||
[type='radio']
|
[type='radio']
|
||||||
position: fixed
|
position: fixed
|
||||||
top: -1px
|
top: -1px
|
||||||
left: -1px
|
left: -1px
|
||||||
width: 1px
|
width: 1px
|
||||||
height: 1px
|
height: 1px
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
|
||||||
label
|
label
|
||||||
display: block
|
display: block
|
||||||
height: 34px
|
height: 34px
|
||||||
min-width: 42px
|
min-width: 42px
|
||||||
line-height: 34px
|
line-height: 34px
|
||||||
text-align: center
|
text-align: center
|
||||||
padding: 0 13px
|
padding: 0 13px
|
||||||
background-color: #f3f3f3
|
background-color: #f3f3f3
|
||||||
box-shadow: inset 2px 1px 2px 1px rgba(50, 50, 50, 0.05)
|
box-shadow: inset 2px 1px 2px 1px rgba(50, 50, 50, 0.05)
|
||||||
color: var(--color-font)
|
color: var(--color-font)
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
\:checked + label
|
\:checked + label
|
||||||
background-color: var(--color-primary)
|
background-color: var(--color-primary)
|
||||||
color: var(--color-lightwhite)
|
color: var(--color-lightwhite)
|
||||||
box-shadow: inset -2px -1px 2px 1px rgba(255, 255, 255, 0.15)
|
box-shadow: inset -2px -1px 2px 1px rgba(255, 255, 255, 0.15)
|
||||||
|
|
||||||
\:focus + label
|
\:focus + label
|
||||||
border-color: #3273dc
|
border-color: #3273dc
|
||||||
box-shadow: 0 0 0.125em 0 rgba(50, 115, 220, 0.8)
|
box-shadow: 0 0 0.125em 0 rgba(50, 115, 220, 0.8)
|
||||||
outline: 0
|
outline: 0
|
||||||
|
|
||||||
[disabled] + label
|
[disabled] + label
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
border: none
|
border: none
|
||||||
opacity: 0.6
|
opacity: 0.6
|
||||||
filter: grayscale(1)
|
filter: grayscale(1)
|
||||||
|
|
||||||
.radio:first-child
|
&:first-child
|
||||||
label
|
label
|
||||||
border-top-left-radius: 4px
|
border-top-left-radius: 4px
|
||||||
border-bottom-left-radius: 4px
|
border-bottom-left-radius: 4px
|
||||||
|
|
||||||
.radio:last-child
|
&:last-child
|
||||||
label
|
label
|
||||||
border-top-right-radius: 4px
|
border-top-right-radius: 4px
|
||||||
border-bottom-right-radius: 4px
|
border-bottom-right-radius: 4px
|
||||||
|
|||||||
@ -1213,6 +1213,7 @@ MenuExternalExamList: Externe Prüfungen
|
|||||||
MenuParticipantsList: Kursteilnehmerlisten
|
MenuParticipantsList: Kursteilnehmerlisten
|
||||||
MenuParticipantsIntersect: Überschneidung von Kursteilnehmern
|
MenuParticipantsIntersect: Überschneidung von Kursteilnehmern
|
||||||
MenuAllocationUsers: Bewerber
|
MenuAllocationUsers: Bewerber
|
||||||
|
MenuAllocationPriorities: Zentrale Dringlichkeiten
|
||||||
|
|
||||||
BreadcrumbSubmissionFile: Datei
|
BreadcrumbSubmissionFile: Datei
|
||||||
BreadcrumbSubmissionUserInvite: Einladung zur Abgabe
|
BreadcrumbSubmissionUserInvite: Einladung zur Abgabe
|
||||||
@ -1279,6 +1280,7 @@ BreadcrumbParticipants: Kursteilnehmerliste
|
|||||||
BreadcrumbExamAutoOccurrence: Automatische Termin-/Raumverteilung
|
BreadcrumbExamAutoOccurrence: Automatische Termin-/Raumverteilung
|
||||||
BreadcrumbStorageKey: Lokalen Schlüssel generieren
|
BreadcrumbStorageKey: Lokalen Schlüssel generieren
|
||||||
BreadcrumbAllocationUsers: Bewerber
|
BreadcrumbAllocationUsers: Bewerber
|
||||||
|
BreadcrumbAllocationPriorities: Zentrale Dringlichkeiten
|
||||||
|
|
||||||
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn}
|
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn}
|
||||||
ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn}
|
ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn}
|
||||||
@ -2357,4 +2359,12 @@ CsvColumnAllocationUserRequested: Maximale Anzahl von Plätzen, die der Bewerber
|
|||||||
CsvColumnAllocationUserApplied: Anzahl von Bewerbungen, die der Bewerber eingereicht hat
|
CsvColumnAllocationUserApplied: Anzahl von Bewerbungen, die der Bewerber eingereicht hat
|
||||||
CsvColumnAllocationUserVetos: Anzahl von Bewerbungen, die von Kursverwaltern ein Veto oder eine Note erhalten haben, die äquivalent ist zu "Nicht Bestanden" (5.0)
|
CsvColumnAllocationUserVetos: Anzahl von Bewerbungen, die von Kursverwaltern ein Veto oder eine Note erhalten haben, die äquivalent ist zu "Nicht Bestanden" (5.0)
|
||||||
CsvColumnAllocationUserAssigned: Anzahl von Plätzen, die der Bewerber durch diese Zentralanmeldung bereits erhalten hat
|
CsvColumnAllocationUserAssigned: Anzahl von Plätzen, die der Bewerber durch diese Zentralanmeldung bereits erhalten hat
|
||||||
AllocationUsersCsvName tid@TermId ssh@SchoolId ash@AllocationShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash}-bewerber
|
AllocationUsersCsvName tid@TermId ssh@SchoolId ash@AllocationShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash}-bewerber
|
||||||
|
|
||||||
|
AllocationPrioritiesMode: Modus
|
||||||
|
AllocationPrioritiesNumeric: Numerische Dringlichkeiten
|
||||||
|
AllocationPrioritiesOrdinal: Dringlichkeiten durch Sortierung
|
||||||
|
AllocationPrioritiesTitle tid@TermId ssh@SchoolId ash@AllocationShorthand: #{tid}-#{ssh}-#{ash}: Zentrale Dringlichkeiten
|
||||||
|
AllocationPrioritiesFile: CSV-Datei
|
||||||
|
AllocationPrioritiesSunk num@Int64: Zentrale Prioritäten für #{num} Bewerber erfolgreich hinterlegt
|
||||||
|
AllocationPrioritiesMissing num@Int64: Für #{num} Bewerber ist keine zentrale Priorität hinterlegt, da in der hochgeladenen CSV-Datei die #{pluralDE num "entsprechende Matrikelnummer" "entsprechenden Matrikelnummern"} nicht gefunden #{pluralDE num "wurde" "wurden"}
|
||||||
26
records.json
26
records.json
@ -802,5 +802,31 @@
|
|||||||
"usedIds": []
|
"usedIds": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/inputs/radio-group.sass": [
|
||||||
|
{
|
||||||
|
"modules": {
|
||||||
|
"byIdentifier": {},
|
||||||
|
"usedIds": {}
|
||||||
|
},
|
||||||
|
"chunks": {
|
||||||
|
"byName": {},
|
||||||
|
"bySource": {},
|
||||||
|
"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/inputs/radiobox.sass": [
|
||||||
|
{
|
||||||
|
"modules": {
|
||||||
|
"byIdentifier": {},
|
||||||
|
"usedIds": {}
|
||||||
|
},
|
||||||
|
"chunks": {
|
||||||
|
"byName": {},
|
||||||
|
"bySource": {},
|
||||||
|
"usedIds": []
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
1
routes
1
routes
@ -109,6 +109,7 @@
|
|||||||
/register ARegisterR POST !time
|
/register ARegisterR POST !time
|
||||||
/course/#CryptoUUIDCourse/apply AApplyR POST !timeANDallocation-registered
|
/course/#CryptoUUIDCourse/apply AApplyR POST !timeANDallocation-registered
|
||||||
/users AUsersR GET POST !allocation-admin
|
/users AUsersR GET POST !allocation-admin
|
||||||
|
/priorities APriosR GET POST !allocation-admin
|
||||||
|
|
||||||
/participants ParticipantsListR GET !evaluation
|
/participants ParticipantsListR GET !evaluation
|
||||||
/participants/#TermId/#SchoolId ParticipantsR GET !evaluation
|
/participants/#TermId/#SchoolId ParticipantsR GET !evaluation
|
||||||
|
|||||||
@ -21,6 +21,7 @@ module Database.Esqueleto.Utils
|
|||||||
, SqlProject(..)
|
, SqlProject(..)
|
||||||
, (->.)
|
, (->.)
|
||||||
, fromSqlKey
|
, fromSqlKey
|
||||||
|
, selectCountRows
|
||||||
, module Database.Esqueleto.Utils.TH
|
, module Database.Esqueleto.Utils.TH
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@ -257,3 +258,13 @@ instance (PersistEntity val, PersistField typ) => SqlProject val typ (Maybe (E.E
|
|||||||
|
|
||||||
fromSqlKey :: (ToBackendKey SqlBackend entity, PersistField (Key entity)) => E.SqlExpr (E.Value (Key entity)) -> E.SqlExpr (E.Value Int64)
|
fromSqlKey :: (ToBackendKey SqlBackend entity, PersistField (Key entity)) => E.SqlExpr (E.Value (Key entity)) -> E.SqlExpr (E.Value Int64)
|
||||||
fromSqlKey = E.veryUnsafeCoerceSqlExprValue
|
fromSqlKey = E.veryUnsafeCoerceSqlExprValue
|
||||||
|
|
||||||
|
|
||||||
|
selectCountRows :: (Num a, PersistField a, MonadIO m) => E.SqlQuery ignored -> E.SqlReadT m a
|
||||||
|
selectCountRows q = do
|
||||||
|
res <- E.select $ E.countRows <$ q
|
||||||
|
case res of
|
||||||
|
[E.Value res']
|
||||||
|
-> return res'
|
||||||
|
_other
|
||||||
|
-> error "E.countRows did not return exactly one result"
|
||||||
|
|||||||
@ -2029,6 +2029,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
MaybeT $ get cid
|
MaybeT $ get cid
|
||||||
return (CI.original courseName, Just $ AllocationR tid ssh ash AShowR)
|
return (CI.original courseName, Just $ AllocationR tid ssh ash AShowR)
|
||||||
AUsersR -> i18nCrumb MsgBreadcrumbAllocationUsers . Just $ AllocationR tid ssh ash AShowR
|
AUsersR -> i18nCrumb MsgBreadcrumbAllocationUsers . Just $ AllocationR tid ssh ash AShowR
|
||||||
|
APriosR -> i18nCrumb MsgBreadcrumbAllocationPriorities . Just $ AllocationR tid ssh ash AUsersR
|
||||||
|
|
||||||
breadcrumb ParticipantsListR = i18nCrumb MsgBreadcrumbParticipantsList $ Just CourseListR
|
breadcrumb ParticipantsListR = i18nCrumb MsgBreadcrumbParticipantsList $ Just CourseListR
|
||||||
breadcrumb (ParticipantsR _ _) = i18nCrumb MsgBreadcrumbParticipants $ Just ParticipantsListR
|
breadcrumb (ParticipantsR _ _) = i18nCrumb MsgBreadcrumbParticipants $ Just ParticipantsListR
|
||||||
@ -3021,6 +3022,19 @@ pageActions (AllocationR tid ssh ash AShowR) = return
|
|||||||
, navChildren = []
|
, navChildren = []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
pageActions (AllocationR tid ssh ash AUsersR) = return
|
||||||
|
[ NavPageActionPrimary
|
||||||
|
{ navLink = NavLink
|
||||||
|
{ navLabel = MsgMenuAllocationPriorities
|
||||||
|
, navRoute = AllocationR tid ssh ash APriosR
|
||||||
|
, navAccess' = return True
|
||||||
|
, navType = NavTypeLink { navModal = True }
|
||||||
|
, navQuick' = mempty
|
||||||
|
, navForceActive = False
|
||||||
|
}
|
||||||
|
, navChildren = []
|
||||||
|
}
|
||||||
|
]
|
||||||
pageActions CourseListR = do
|
pageActions CourseListR = do
|
||||||
participantsSecondary <- pageQuickActions NavQuickViewPageActionSecondary ParticipantsListR
|
participantsSecondary <- pageQuickActions NavQuickViewPageActionSecondary ParticipantsListR
|
||||||
return
|
return
|
||||||
|
|||||||
@ -8,3 +8,4 @@ import Handler.Allocation.Application as Handler.Allocation
|
|||||||
import Handler.Allocation.Register as Handler.Allocation
|
import Handler.Allocation.Register as Handler.Allocation
|
||||||
import Handler.Allocation.List as Handler.Allocation
|
import Handler.Allocation.List as Handler.Allocation
|
||||||
import Handler.Allocation.Users as Handler.Allocation
|
import Handler.Allocation.Users as Handler.Allocation
|
||||||
|
import Handler.Allocation.Prios as Handler.Allocation
|
||||||
|
|||||||
82
src/Handler/Allocation/Prios.hs
Normal file
82
src/Handler/Allocation/Prios.hs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
module Handler.Allocation.Prios
|
||||||
|
( getAPriosR, postAPriosR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Csv
|
||||||
|
import Handler.Utils.Allocation
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
|
|
||||||
|
data AllocationPrioritiesMode
|
||||||
|
= AllocationPrioritiesNumeric
|
||||||
|
| AllocationPrioritiesOrdinal
|
||||||
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe AllocationPrioritiesMode
|
||||||
|
instance Finite AllocationPrioritiesMode
|
||||||
|
|
||||||
|
nullaryPathPiece ''AllocationPrioritiesMode $ camelToPathPiece' 2
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''AllocationPrioritiesMode id
|
||||||
|
|
||||||
|
|
||||||
|
getAPriosR, postAPriosR :: TermId -> SchoolId -> AllocationShorthand -> Handler Html
|
||||||
|
getAPriosR = postAPriosR
|
||||||
|
postAPriosR tid ssh ash = do
|
||||||
|
doNumericPrios <- runDB $ do
|
||||||
|
Entity aId _ <- getBy404 $ TermSchoolAllocationShort tid ssh ash
|
||||||
|
|
||||||
|
numericPrios <- E.selectCountRows . E.from $ \allocationUser -> do
|
||||||
|
E.where_ $ allocationUser E.^. AllocationUserAllocation E.==. E.val aId
|
||||||
|
E.where_ . E.maybe E.false sqlAllocationPriorityNumeric $ allocationUser E.^. AllocationUserPriority
|
||||||
|
|
||||||
|
ordinalPrios <- E.selectCountRows . E.from $ \allocationUser -> do
|
||||||
|
E.where_ $ allocationUser E.^. AllocationUserAllocation E.==. E.val aId
|
||||||
|
E.where_ . E.maybe E.false (E.not_ . sqlAllocationPriorityNumeric) $ allocationUser E.^. AllocationUserPriority
|
||||||
|
let doNumericPrios = ((>=) :: Int64 -> Int64 -> Bool) numericPrios ordinalPrios
|
||||||
|
|
||||||
|
return doNumericPrios
|
||||||
|
|
||||||
|
let explainAllocationPrioMode = \case
|
||||||
|
AllocationPrioritiesNumeric -> return $(i18nWidgetFile "allocation-priority-explanation/numeric")
|
||||||
|
AllocationPrioritiesOrdinal -> return $(i18nWidgetFile "allocation-priority-explanation/ordinal")
|
||||||
|
|
||||||
|
((priosRes, priosView), priosEnctype) <- runFormPost . renderAForm FormStandard $ (,)
|
||||||
|
<$> apopt (explainedSelectionField Nothing (explainOptionList optionsFinite explainAllocationPrioMode)) (fslI MsgAllocationPrioritiesMode) (Just $ bool AllocationPrioritiesOrdinal AllocationPrioritiesNumeric doNumericPrios)
|
||||||
|
<*> areq fileField (fslI MsgAllocationPrioritiesFile) Nothing
|
||||||
|
|
||||||
|
formResult priosRes $ \(mode, fInfo) -> do
|
||||||
|
let sourcePrios = case mode of
|
||||||
|
AllocationPrioritiesNumeric -> fileSourceCsvPositional Csv.NoHeader fInfo
|
||||||
|
AllocationPrioritiesOrdinal -> fileSourceCsvPositional Csv.NoHeader fInfo .| C.map Csv.fromOnly .| ordinalPriorities
|
||||||
|
|
||||||
|
(matrSunk, matrMissing) <- runDB $ do
|
||||||
|
Entity aId _ <- getBy404 $ TermSchoolAllocationShort tid ssh ash
|
||||||
|
updateWhere
|
||||||
|
[ AllocationUserAllocation ==. aId ]
|
||||||
|
[ AllocationUserPriority =. Nothing ]
|
||||||
|
matrSunk <- runConduit $ sourcePrios .| sinkAllocationPriorities aId
|
||||||
|
matrMissing <- fromIntegral <$> count [ AllocationUserAllocation ==. aId, AllocationUserPriority ==. Nothing ]
|
||||||
|
return (matrSunk, matrMissing)
|
||||||
|
|
||||||
|
when (matrSunk > 0) $
|
||||||
|
addMessageI Success $ MsgAllocationPrioritiesSunk matrSunk
|
||||||
|
when (matrMissing > 0) $
|
||||||
|
addMessageI Error $ MsgAllocationPrioritiesMissing matrMissing
|
||||||
|
redirect $ AllocationR tid ssh ash AUsersR
|
||||||
|
|
||||||
|
siteLayoutMsg MsgMenuAllocationPriorities $ do
|
||||||
|
setTitleI $ MsgAllocationPrioritiesTitle tid ssh ash
|
||||||
|
|
||||||
|
wrapForm priosView def
|
||||||
|
{ formEncoding = priosEnctype
|
||||||
|
, formAction = Just . SomeRoute $ AllocationR tid ssh ash APriosR
|
||||||
|
}
|
||||||
@ -46,9 +46,9 @@ ordinalPriorities :: Monad m => ConduitT UserMatriculation (Map UserMatriculatio
|
|||||||
ordinalPriorities = evalStateC 0 . C.mapM $ \matr -> singletonMap matr <$> (AllocationPriorityOrdinal <$> State.get <* State.modify' succ)
|
ordinalPriorities = evalStateC 0 . C.mapM $ \matr -> singletonMap matr <$> (AllocationPriorityOrdinal <$> State.get <* State.modify' succ)
|
||||||
|
|
||||||
sinkAllocationPriorities :: AllocationId
|
sinkAllocationPriorities :: AllocationId
|
||||||
-> ConduitT (Map UserMatriculation AllocationPriority) Void DB ()
|
-> ConduitT (Map UserMatriculation AllocationPriority) Void DB Int64
|
||||||
sinkAllocationPriorities allocId = C.mapM_ . imapM_ $ \matr prio ->
|
sinkAllocationPriorities allocId = fmap getSum . C.foldMapM . ifoldMapM $ \matr prio ->
|
||||||
E.update $ \allocationUser -> do
|
fmap Sum . E.updateCount $ \allocationUser -> do
|
||||||
E.set allocationUser [ AllocationUserPriority E.=. E.val (Just prio) ]
|
E.set allocationUser [ AllocationUserPriority E.=. E.val (Just prio) ]
|
||||||
E.where_ $ allocationUser E.^. AllocationUserAllocation E.==. E.val allocId
|
E.where_ $ allocationUser E.^. AllocationUserAllocation E.==. E.val allocId
|
||||||
E.where_ . E.exists . E.from $ \user ->
|
E.where_ . E.exists . E.from $ \user ->
|
||||||
|
|||||||
@ -7,7 +7,7 @@ module Handler.Utils.Csv
|
|||||||
, encodeDefaultOrderedCsv
|
, encodeDefaultOrderedCsv
|
||||||
, respondCsv, respondCsvDB
|
, respondCsv, respondCsvDB
|
||||||
, respondDefaultOrderedCsv, respondDefaultOrderedCsvDB
|
, respondDefaultOrderedCsv, respondDefaultOrderedCsvDB
|
||||||
, fileSourceCsv
|
, fileSourceCsv, fileSourceCsvPositional
|
||||||
, partIsAttachmentCsv
|
, partIsAttachmentCsv
|
||||||
, CsvParseError(..)
|
, CsvParseError(..)
|
||||||
, ToNamedRecord(..), FromNamedRecord(..)
|
, ToNamedRecord(..), FromNamedRecord(..)
|
||||||
@ -210,6 +210,16 @@ fileSourceCsv :: ( FromNamedRecord csv
|
|||||||
-> ConduitT () csv m ()
|
-> ConduitT () csv m ()
|
||||||
fileSourceCsv = (.| decodeCsv) . fileSource
|
fileSourceCsv = (.| decodeCsv) . fileSource
|
||||||
|
|
||||||
|
fileSourceCsvPositional :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, MonadThrow m
|
||||||
|
, FromRecord csv
|
||||||
|
)
|
||||||
|
=> HasHeader
|
||||||
|
-> FileInfo
|
||||||
|
-> ConduitT () csv m ()
|
||||||
|
fileSourceCsvPositional hdr = (.| decodeCsvPositional hdr) . fileSource
|
||||||
|
|
||||||
|
|
||||||
instance ToWidget UniWorX CsvRendered where
|
instance ToWidget UniWorX CsvRendered where
|
||||||
toWidget CsvRendered{..} = liftWidget $(widgetFile "widgets/csvRendered")
|
toWidget CsvRendered{..} = liftWidget $(widgetFile "widgets/csvRendered")
|
||||||
|
|||||||
@ -1479,3 +1479,46 @@ csvOptionsForm :: forall m.
|
|||||||
csvOptionsForm mPrev = hoistAForm liftHandler $ CsvOptions
|
csvOptionsForm mPrev = hoistAForm liftHandler $ CsvOptions
|
||||||
<$> csvFormatOptionsForm (fslI MsgCsvFormatOptions & setTooltip MsgCsvOptionsTip) (csvFormat <$> mPrev)
|
<$> csvFormatOptionsForm (fslI MsgCsvFormatOptions & setTooltip MsgCsvOptionsTip) (csvFormat <$> mPrev)
|
||||||
<*> apopt checkBoxField (fslI MsgCsvTimestamp & setTooltip MsgCsvTimestampTip) (csvTimestamp <$> mPrev)
|
<*> apopt checkBoxField (fslI MsgCsvTimestamp & setTooltip MsgCsvTimestampTip) (csvTimestamp <$> mPrev)
|
||||||
|
|
||||||
|
|
||||||
|
explainedSelectionField :: forall m a.
|
||||||
|
( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, Eq a
|
||||||
|
)
|
||||||
|
=> Maybe (SomeMessage UniWorX, Maybe Widget) -- ^ Label for none option
|
||||||
|
-> Handler ([(Option a, Maybe Widget)], Text -> Maybe a)
|
||||||
|
-> Field m a
|
||||||
|
explainedSelectionField optMsg' mkOpts = Field{..}
|
||||||
|
where
|
||||||
|
fieldEnctype = UrlEncoded
|
||||||
|
fieldParse ts _ = do
|
||||||
|
(_, parser) <- liftHandler mkOpts
|
||||||
|
if
|
||||||
|
| t : _ <- ts
|
||||||
|
, Just t' <- parser t
|
||||||
|
-> return . Right $ Just t'
|
||||||
|
| t : _ <- ts
|
||||||
|
, null t
|
||||||
|
-> return $ Right Nothing
|
||||||
|
| t : _ <- ts
|
||||||
|
-> return . Left . SomeMessage $ MsgInvalidEntry t
|
||||||
|
| otherwise
|
||||||
|
-> return $ Right Nothing
|
||||||
|
fieldView theId name attrs val isReq = do
|
||||||
|
(opts, _) <- liftHandler mkOpts
|
||||||
|
let optMsg = guardOnM (not isReq) optMsg'
|
||||||
|
inputId optExternal = [st|#{theId}__input--#{optExternal}|]
|
||||||
|
matchesVal Nothing = is _Left val
|
||||||
|
matchesVal (Just x) = val == Right x
|
||||||
|
$(widgetFile "widgets/explained-selection-field")
|
||||||
|
|
||||||
|
explainOptionList :: forall a.
|
||||||
|
Handler (OptionList a)
|
||||||
|
-> (a -> MaybeT Handler Widget)
|
||||||
|
-> Handler ([(Option a, Maybe Widget)], Text -> Maybe a)
|
||||||
|
explainOptionList ol mkExplanation = do
|
||||||
|
OptionList{..} <- ol
|
||||||
|
olOptions' <- forM olOptions $ \opt@Option{..} -> (opt, ) <$> runMaybeT (mkExplanation optionInternalValue)
|
||||||
|
return (olOptions', olReadExternal)
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import ClassyPrelude.Yesod as Import
|
|||||||
, HasHttpManager(..)
|
, HasHttpManager(..)
|
||||||
, embed
|
, embed
|
||||||
, try, embed, catches, handle, catch, bracket, bracketOnError, bracket_, catchJust, finally, handleJust, mask, mask_, onException, tryJust, uninterruptibleMask, uninterruptibleMask_
|
, try, embed, catches, handle, catch, bracket, bracketOnError, bracket_, catchJust, finally, handleJust, mask, mask_, onException, tryJust, uninterruptibleMask, uninterruptibleMask_
|
||||||
, htmlField
|
, htmlField, fileField
|
||||||
)
|
)
|
||||||
|
|
||||||
import UnliftIO.Async.Utils as Import
|
import UnliftIO.Async.Utils as Import
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
module Model.Types.Allocation
|
module Model.Types.Allocation
|
||||||
( AllocationPriority(..)
|
( AllocationPriority(..)
|
||||||
|
, sqlAllocationPriorityNumeric
|
||||||
, AllocationPriorityComparison(..)
|
, AllocationPriorityComparison(..)
|
||||||
, AllocationFingerprint
|
, AllocationFingerprint
|
||||||
, module Utils.Allocation
|
, module Utils.Allocation
|
||||||
@ -16,6 +17,10 @@ import qualified Data.Map.Strict as Map
|
|||||||
|
|
||||||
import Crypto.Hash (SHAKE128)
|
import Crypto.Hash (SHAKE128)
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Internal.Sql as E
|
||||||
|
import qualified Database.Esqueleto.PostgreSQL.JSON as E
|
||||||
|
|
||||||
{-# ANN module ("HLint: ignore Use newtype instead of data"::String) #-}
|
{-# ANN module ("HLint: ignore Use newtype instead of data"::String) #-}
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +36,9 @@ deriveJSON defaultOptions
|
|||||||
, unwrapUnaryRecords = False
|
, unwrapUnaryRecords = False
|
||||||
, tagSingleConstructors = True
|
, tagSingleConstructors = True
|
||||||
} ''AllocationPriority
|
} ''AllocationPriority
|
||||||
derivePersistFieldJSON ''AllocationPriority
|
|
||||||
|
deriving via E.JSONB AllocationPriority instance E.PersistField AllocationPriority
|
||||||
|
deriving via E.JSONB AllocationPriority instance E.PersistFieldSql AllocationPriority
|
||||||
|
|
||||||
instance Binary AllocationPriority
|
instance Binary AllocationPriority
|
||||||
|
|
||||||
@ -43,6 +50,10 @@ instance Csv.FromRecord (Map UserMatriculation AllocationPriority) where
|
|||||||
| otherwise = mzero
|
| otherwise = mzero
|
||||||
|
|
||||||
|
|
||||||
|
sqlAllocationPriorityNumeric :: E.SqlExpr (E.Value AllocationPriority) -> E.SqlExpr (E.Value Bool)
|
||||||
|
sqlAllocationPriorityNumeric prio = E.veryUnsafeCoerceSqlExprValue prio E.->. "mode" E.==. E.jsonbVal ("numeric" :: Text)
|
||||||
|
|
||||||
|
|
||||||
data AllocationPriorityComparison
|
data AllocationPriorityComparison
|
||||||
= AllocationPriorityComparisonNumeric { allocationGradeScale :: Rational }
|
= AllocationPriorityComparisonNumeric { allocationGradeScale :: Rational }
|
||||||
| AllocationPriorityComparisonOrdinal { allocationCloneIndex :: Down Natural, allocationOrdinalScale :: Rational }
|
| AllocationPriorityComparisonOrdinal { allocationCloneIndex :: Down Natural, allocationOrdinalScale :: Rational }
|
||||||
|
|||||||
@ -670,7 +670,10 @@ ofoldl1M f (otoList -> x:xs) = foldlM f x xs
|
|||||||
ofoldl1M _ _ = error "otoList of NonNull is empty"
|
ofoldl1M _ _ = error "otoList of NonNull is empty"
|
||||||
|
|
||||||
foldMapM :: (Foldable f, Monad m, Monoid b) => (a -> m b) -> f a -> m b
|
foldMapM :: (Foldable f, Monad m, Monoid b) => (a -> m b) -> f a -> m b
|
||||||
foldMapM f = foldrM (\x xs -> (<>) <$> f x <*> pure xs) mempty
|
foldMapM f = foldrM (\x xs -> (<> xs) <$> f x) mempty
|
||||||
|
|
||||||
|
ifoldMapM :: (FoldableWithIndex i f, Monad m, Monoid b) => (i -> a -> m b) -> f a -> m b
|
||||||
|
ifoldMapM f = ifoldrM (\i x xs -> (<> xs) <$> f i x) mempty
|
||||||
|
|
||||||
partitionM :: forall mono m .
|
partitionM :: forall mono m .
|
||||||
( MonoFoldable mono
|
( MonoFoldable mono
|
||||||
|
|||||||
@ -639,17 +639,30 @@ secretJsonField :: forall m a.
|
|||||||
secretJsonField = secretJsonField' $ fieldView (hiddenField :: Field m Text)
|
secretJsonField = secretJsonField' $ fieldView (hiddenField :: Field m Text)
|
||||||
|
|
||||||
fileFieldMultiple :: Monad m => Field m [FileInfo]
|
fileFieldMultiple :: Monad m => Field m [FileInfo]
|
||||||
fileFieldMultiple = Field
|
fileFieldMultiple = Field{..}
|
||||||
{ fieldParse = \_ files -> return $ case files of
|
where
|
||||||
|
fieldEnctype = Multipart
|
||||||
|
fieldParse _ files = return $ case files of
|
||||||
[] -> Right Nothing
|
[] -> Right Nothing
|
||||||
fs -> Right $ Just fs
|
fs -> Right $ Just fs
|
||||||
, fieldView = \id' name attrs _ isReq ->
|
fieldView id' name attrs _ isReq =
|
||||||
[whamlet|
|
[whamlet|
|
||||||
$newline never
|
$newline never
|
||||||
<input type="file" uw-file-input id=#{id'} name=#{name} *{attrs} multiple :isReq:required="required">
|
<input type="file" uw-file-input ##{id'} name=#{name} *{attrs} multiple :isReq:required>
|
||||||
|
|]
|
||||||
|
|
||||||
|
fileField :: Monad m => Field m FileInfo
|
||||||
|
fileField = Field{..}
|
||||||
|
where
|
||||||
|
fieldEnctype = Multipart
|
||||||
|
fieldParse _ files = return $ case files of
|
||||||
|
[] -> Right Nothing
|
||||||
|
f : _ -> Right $ Just f
|
||||||
|
fieldView id' name attrs _ isReq =
|
||||||
|
[whamlet|
|
||||||
|
$newline never
|
||||||
|
<input type=file uw-file-input ##{id'} name=#{name} *{attrs} :isReq:required>
|
||||||
|]
|
|]
|
||||||
, fieldEnctype = Multipart
|
|
||||||
}
|
|
||||||
|
|
||||||
guardField :: Functor m => (a -> Bool) -> Field m a -> Field m a
|
guardField :: Functor m => (a -> Bool) -> Field m a -> Field m a
|
||||||
guardField p field = field { fieldParse = \ts fs -> fieldParse field ts fs <&> \case
|
guardField p field = field { fieldParse = \ts fs -> fieldParse field ts fs <&> \case
|
||||||
|
|||||||
@ -2,9 +2,9 @@ $newline never
|
|||||||
$if not isModal
|
$if not isModal
|
||||||
$with containers <- filter isNavHeaderContainer nav
|
$with containers <- filter isNavHeaderContainer nav
|
||||||
$if not (null containers)
|
$if not (null containers)
|
||||||
<input name=nav-container type=radio .navbar__container-radio--none checked #container-radio-none>
|
<input name=nav-container type=radio .navbar__container-radio--none checked #container-radio-none uw-no-radiobox>
|
||||||
$forall (_, containerIdent, _, _) <- containers
|
$forall (_, containerIdent, _, _) <- containers
|
||||||
<input name=nav-container type=radio .navbar__container-radio ##{containerIdent}-radio>
|
<input name=nav-container type=radio .navbar__container-radio ##{containerIdent}-radio uw-no-radiobox>
|
||||||
|
|
||||||
<!-- secondary navigation at the side -->
|
<!-- secondary navigation at the side -->
|
||||||
^{asidenav}
|
^{asidenav}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
$newline never
|
||||||
|
|
||||||
|
Es wird erwartet, dass die erste Spalte der hochgeladenen CSV-Datei die #
|
||||||
|
Matrikelnummer der Zentralanmeldungs-Bewerber enthält.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
Alle weiteren Spalten werden als ganze Zahlen interpretiert und #
|
||||||
|
kodieren die jeweilige zentrale Dringlichkeit bei der Vergabe der #
|
||||||
|
Plätze. #
|
||||||
|
|
||||||
|
Hierbei wird die erste Dringlichkeits-Spalte verwendet zur Vergabe des #
|
||||||
|
jeweils ersten Platzes, die zweite Spalte für den zweiten Platz, usw. #
|
||||||
|
|
||||||
|
Größere Zahlen kodieren eine höhere Dringlichkeit.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
Die CSV-Datei darf keine Spaltenüberschriften enthalten.
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
$newline never
|
||||||
|
|
||||||
|
Es wird erwartet, dass die hochgeladene CSV-Datei genau eine Spalte #
|
||||||
|
mit den Matrikelnummern der Zentralanmeldungs-Bewerber enthält.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
Die zentrale Dringlichkeit ergibt sich ausschließlich anhand der #
|
||||||
|
Sortierung der CSV-Datei. #
|
||||||
|
|
||||||
|
Bewerber, deren Matrikelnummer später in der Datei vorkommt, erhalten #
|
||||||
|
für alle ihre Plätze eine höhere Dringlichkeit, als Bewerber, deren #
|
||||||
|
Matrikelnummern in der Datei früher vorkommen. #
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
Die CSV-Datei darf keine Spaltenüberschriften enthalten.
|
||||||
@ -7,7 +7,7 @@ $newline never
|
|||||||
<li>
|
<li>
|
||||||
Alle HTML-Eingabefelder akzeptieren nun stattdessen Markdown
|
Alle HTML-Eingabefelder akzeptieren nun stattdessen Markdown
|
||||||
<li>
|
<li>
|
||||||
Alle ausgehenden HTML E-Mails haben nun auch einen \
|
Alle ausgehenden HTML E-Mails haben nun auch einen #
|
||||||
Markdown-Teil
|
Markdown-Teil
|
||||||
|
|
||||||
<dt .deflist__dt>
|
<dt .deflist__dt>
|
||||||
@ -15,8 +15,8 @@ $newline never
|
|||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Prüfungen können nun angeben in welchem Format Leistungen \
|
Prüfungen können nun angeben in welchem Format Leistungen #
|
||||||
eingetragen werden dürfen (Bestanden/Nicht Bestanden, \
|
eingetragen werden dürfen (Bestanden/Nicht Bestanden, #
|
||||||
Numerische Noten oder Gemischt)
|
Numerische Noten oder Gemischt)
|
||||||
|
|
||||||
<dt .deflist__dt>
|
<dt .deflist__dt>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ $newline never
|
|||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Exams may now specify in which format results are expected to \
|
Exams may now specify in which format results are expected to #
|
||||||
entered (passed/failed, numeric grades, or mixed)
|
entered (passed/failed, numeric grades, or mixed)
|
||||||
|
|
||||||
<dt .deflist__dt>
|
<dt .deflist__dt>
|
||||||
|
|||||||
21
templates/widgets/explained-selection-field.hamlet
Normal file
21
templates/widgets/explained-selection-field.hamlet
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
$newline never
|
||||||
|
<div ##{theId} *{attrs} .explained-selection-field>
|
||||||
|
$maybe (msg, wgt) <- optMsg
|
||||||
|
<div .explained-selection-field__option>
|
||||||
|
<label .explained-selection-field__input .explained-selection-field__input--none for=#{inputId "none"}>
|
||||||
|
<input ##{inputId "none"} type=radio name=#{name} value="" :matchesVal Nothing:checked>
|
||||||
|
<label .explained-selection-field__label .explained-selection-field__label--none for=#{inputId "none"}>
|
||||||
|
_{msg}
|
||||||
|
$maybe w <- wgt
|
||||||
|
<div .explained-selection-field__explanation .explained-selection-field__explanation--none>
|
||||||
|
^{w}
|
||||||
|
$forall (Option{..}, optionExplanation) <- opts
|
||||||
|
<div .explained-selection-field__option>
|
||||||
|
<label .explained-selection-field__input for=#{inputId optionExternalValue}>
|
||||||
|
<input ##{inputId optionExternalValue} type=radio name=#{name} value=#{optionExternalValue} :matchesVal (Just optionInternalValue):checked>
|
||||||
|
<label .explained-selection-field__label for=#{inputId optionExternalValue}>
|
||||||
|
#{optionDisplay}
|
||||||
|
$maybe w <- optionExplanation
|
||||||
|
<div .explained-selection-field__explanation>
|
||||||
|
^{w}
|
||||||
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
$newline never
|
$newline never
|
||||||
$if hasSecondaryPageActions || hasPrimarySubActions
|
$if hasSecondaryPageActions || hasPrimarySubActions
|
||||||
<input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-none type=radio checked>
|
<input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-none type=radio checked uw-no-radiobox>
|
||||||
<ul .pagenav>
|
<ul .pagenav>
|
||||||
$forall n <- filter isPageActionPrimary nav
|
$forall n <- filter isPageActionPrimary nav
|
||||||
<li .pagenav__list-item>
|
<li .pagenav__list-item>
|
||||||
^{navWidget n}
|
^{navWidget n}
|
||||||
$if hasSecondaryPageActions
|
$if hasSecondaryPageActions
|
||||||
<li .pagenav__list-item .pagenav-secondary>
|
<li .pagenav__list-item .pagenav-secondary>
|
||||||
<input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-secondary type=radio>
|
<input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-secondary type=radio uw-no-radiobox>
|
||||||
<label .pagenav-item__expand-label for=pageaction-item__expand-secondary>
|
<label .pagenav-item__expand-label for=pageaction-item__expand-secondary>
|
||||||
<i .fas .fa-fw .fa-#{iconText IconPageActionSecondary}>
|
<i .fas .fa-fw .fa-#{iconText IconPageActionSecondary}>
|
||||||
<div .pagenav-item__children-wrapper>
|
<div .pagenav-item__children-wrapper>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
$newline never
|
$newline never
|
||||||
^{pWidget}
|
^{pWidget}
|
||||||
$if not (null sWidgets)
|
$if not (null sWidgets)
|
||||||
<input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-#{navIdent} type=radio>
|
<input .pagenav-item__expand-radio name=pagenav-item__expand-radio id=pageaction-item__expand-#{navIdent} type=radio uw-no-radiobox>
|
||||||
<label .pagenav-item__expand-label for=pageaction-item__expand-#{navIdent}>
|
<label .pagenav-item__expand-label for=pageaction-item__expand-#{navIdent}>
|
||||||
<i .fas .fa-fw .fa-#{iconText IconPageActionPrimaryExpand}>
|
<i .fas .fa-fw .fa-#{iconText IconPageActionPrimaryExpand}>
|
||||||
<div .pagenav-item__children-wrapper>
|
<div .pagenav-item__children-wrapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user