feat(allocations): upload of priorities
This commit is contained in:
parent
2735d465eb
commit
a590f45cc1
@ -33,3 +33,35 @@ fieldset
|
||||
|
||||
.label-pagesize
|
||||
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';
|
||||
|
||||
var CHECKBOX_CLASS = 'checkbox';
|
||||
var RADIOBOX_CLASS = 'radiobox';
|
||||
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
|
||||
|
||||
@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 {
|
||||
|
||||
constructor(element) {
|
||||
if (!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)) {
|
||||
// throw new Error('Checkbox utility already initialized!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (element.parentElement.classList.contains(CHECKBOX_CLASS)) {
|
||||
// throw new Error('Checkbox element\'s wrapper already has class '' + CHECKBOX_CLASS + ''!');
|
||||
if (element.parentElement.classList.contains(box_class)) {
|
||||
// throw new Error('Checkbox element\'s wrapper already has class '' + box_class + ''!');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -28,7 +36,7 @@ export class Checkbox {
|
||||
var parentEl = element.parentElement;
|
||||
|
||||
var wrapperEl = document.createElement('div');
|
||||
wrapperEl.classList.add(CHECKBOX_CLASS);
|
||||
wrapperEl.classList.add(box_class);
|
||||
|
||||
var labelEl = document.createElement('label');
|
||||
labelEl.setAttribute('for', element.id);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// CUSTOM CHECKBOXES
|
||||
// Completely replaces legacy checkbox
|
||||
.checkbox [type='checkbox'], #lang-checkbox
|
||||
.checkbox [type='checkbox'], .radiobox [type='radio'], #lang-checkbox
|
||||
position: fixed
|
||||
top: -1px
|
||||
left: -1px
|
||||
@ -9,7 +9,7 @@
|
||||
overflow: hidden
|
||||
display: none
|
||||
|
||||
.checkbox
|
||||
.checkbox, .radiobox
|
||||
position: relative
|
||||
display: inline-block
|
||||
|
||||
@ -17,47 +17,77 @@
|
||||
display: block
|
||||
height: 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)
|
||||
border: 2px solid var(--color-primary)
|
||||
border-radius: 4px
|
||||
color: white
|
||||
cursor: pointer
|
||||
|
||||
label::before,
|
||||
label::after
|
||||
position: absolute
|
||||
display: block
|
||||
top: 12px
|
||||
left: 8px
|
||||
height: 2px
|
||||
width: 8px
|
||||
background-color: var(--color-font)
|
||||
&.radiobox label
|
||||
border-radius: 10px
|
||||
|
||||
\:checked + label
|
||||
background-color: var(--color-primary)
|
||||
|
||||
[type='checkbox']:focus + label
|
||||
border-color: #3273dc
|
||||
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25)
|
||||
outline: 0
|
||||
&.checkbox
|
||||
label::before,
|
||||
label::after
|
||||
position: absolute
|
||||
display: block
|
||||
top: 12px
|
||||
left: 8px
|
||||
height: 2px
|
||||
width: 8px
|
||||
background-color: var(--color-font)
|
||||
|
||||
\:checked + label::before,
|
||||
:checked + label::after
|
||||
content: ''
|
||||
\:checked + label
|
||||
background-color: var(--color-primary)
|
||||
|
||||
\:checked + label::before
|
||||
background-color: white
|
||||
transform: rotate(45deg)
|
||||
left: 2px
|
||||
top: 11px
|
||||
[type='checkbox']:focus + label
|
||||
border-color: #3273dc
|
||||
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25)
|
||||
outline: 0
|
||||
|
||||
\:checked + label::after
|
||||
background-color: white
|
||||
transform: rotate(-45deg)
|
||||
top: 9px
|
||||
width: 12px
|
||||
left: 7px
|
||||
\:checked + label::before,
|
||||
:checked + label::after
|
||||
content: ''
|
||||
|
||||
\:checked + label::before
|
||||
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
|
||||
pointer-events: none
|
||||
|
||||
@ -2,7 +2,7 @@ import { Checkbox } from './checkbox';
|
||||
import { FileInput } from './file-input';
|
||||
|
||||
import './inputs.sass';
|
||||
import './radio.sass';
|
||||
import './radio-group.sass';
|
||||
|
||||
export const InputUtils = [
|
||||
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
|
||||
|
||||
.radio-group
|
||||
display: flex
|
||||
|
||||
.radio
|
||||
position: relative
|
||||
display: inline-block
|
||||
& > .radio
|
||||
position: relative
|
||||
display: inline-block
|
||||
|
||||
[type='radio']
|
||||
position: fixed
|
||||
top: -1px
|
||||
left: -1px
|
||||
width: 1px
|
||||
height: 1px
|
||||
overflow: hidden
|
||||
[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
|
||||
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)
|
||||
\: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
|
||||
\: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)
|
||||
[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
|
||||
&: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
|
||||
&:last-child
|
||||
label
|
||||
border-top-right-radius: 4px
|
||||
border-bottom-right-radius: 4px
|
||||
|
||||
@ -1213,6 +1213,7 @@ MenuExternalExamList: Externe Prüfungen
|
||||
MenuParticipantsList: Kursteilnehmerlisten
|
||||
MenuParticipantsIntersect: Überschneidung von Kursteilnehmern
|
||||
MenuAllocationUsers: Bewerber
|
||||
MenuAllocationPriorities: Zentrale Dringlichkeiten
|
||||
|
||||
BreadcrumbSubmissionFile: Datei
|
||||
BreadcrumbSubmissionUserInvite: Einladung zur Abgabe
|
||||
@ -1279,6 +1280,7 @@ BreadcrumbParticipants: Kursteilnehmerliste
|
||||
BreadcrumbExamAutoOccurrence: Automatische Termin-/Raumverteilung
|
||||
BreadcrumbStorageKey: Lokalen Schlüssel generieren
|
||||
BreadcrumbAllocationUsers: Bewerber
|
||||
BreadcrumbAllocationPriorities: Zentrale Dringlichkeiten
|
||||
|
||||
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{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
|
||||
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
|
||||
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": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"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
|
||||
/course/#CryptoUUIDCourse/apply AApplyR POST !timeANDallocation-registered
|
||||
/users AUsersR GET POST !allocation-admin
|
||||
/priorities APriosR GET POST !allocation-admin
|
||||
|
||||
/participants ParticipantsListR GET !evaluation
|
||||
/participants/#TermId/#SchoolId ParticipantsR GET !evaluation
|
||||
|
||||
@ -21,6 +21,7 @@ module Database.Esqueleto.Utils
|
||||
, SqlProject(..)
|
||||
, (->.)
|
||||
, fromSqlKey
|
||||
, selectCountRows
|
||||
, module Database.Esqueleto.Utils.TH
|
||||
) 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 = 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
|
||||
return (CI.original courseName, 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 (ParticipantsR _ _) = i18nCrumb MsgBreadcrumbParticipants $ Just ParticipantsListR
|
||||
@ -3021,6 +3022,19 @@ pageActions (AllocationR tid ssh ash AShowR) = return
|
||||
, 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
|
||||
participantsSecondary <- pageQuickActions NavQuickViewPageActionSecondary ParticipantsListR
|
||||
return
|
||||
|
||||
@ -8,3 +8,4 @@ import Handler.Allocation.Application as Handler.Allocation
|
||||
import Handler.Allocation.Register as Handler.Allocation
|
||||
import Handler.Allocation.List 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)
|
||||
|
||||
sinkAllocationPriorities :: AllocationId
|
||||
-> ConduitT (Map UserMatriculation AllocationPriority) Void DB ()
|
||||
sinkAllocationPriorities allocId = C.mapM_ . imapM_ $ \matr prio ->
|
||||
E.update $ \allocationUser -> do
|
||||
-> ConduitT (Map UserMatriculation AllocationPriority) Void DB Int64
|
||||
sinkAllocationPriorities allocId = fmap getSum . C.foldMapM . ifoldMapM $ \matr prio ->
|
||||
fmap Sum . E.updateCount $ \allocationUser -> do
|
||||
E.set allocationUser [ AllocationUserPriority E.=. E.val (Just prio) ]
|
||||
E.where_ $ allocationUser E.^. AllocationUserAllocation E.==. E.val allocId
|
||||
E.where_ . E.exists . E.from $ \user ->
|
||||
|
||||
@ -7,7 +7,7 @@ module Handler.Utils.Csv
|
||||
, encodeDefaultOrderedCsv
|
||||
, respondCsv, respondCsvDB
|
||||
, respondDefaultOrderedCsv, respondDefaultOrderedCsvDB
|
||||
, fileSourceCsv
|
||||
, fileSourceCsv, fileSourceCsvPositional
|
||||
, partIsAttachmentCsv
|
||||
, CsvParseError(..)
|
||||
, ToNamedRecord(..), FromNamedRecord(..)
|
||||
@ -210,6 +210,16 @@ fileSourceCsv :: ( FromNamedRecord csv
|
||||
-> ConduitT () csv m ()
|
||||
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
|
||||
toWidget CsvRendered{..} = liftWidget $(widgetFile "widgets/csvRendered")
|
||||
|
||||
@ -1479,3 +1479,46 @@ csvOptionsForm :: forall m.
|
||||
csvOptionsForm mPrev = hoistAForm liftHandler $ CsvOptions
|
||||
<$> csvFormatOptionsForm (fslI MsgCsvFormatOptions & setTooltip MsgCsvOptionsTip) (csvFormat <$> 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(..)
|
||||
, embed
|
||||
, 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
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
module Model.Types.Allocation
|
||||
( AllocationPriority(..)
|
||||
, sqlAllocationPriorityNumeric
|
||||
, AllocationPriorityComparison(..)
|
||||
, AllocationFingerprint
|
||||
, module Utils.Allocation
|
||||
@ -16,6 +17,10 @@ import qualified Data.Map.Strict as Map
|
||||
|
||||
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) #-}
|
||||
|
||||
|
||||
@ -31,7 +36,9 @@ deriveJSON defaultOptions
|
||||
, unwrapUnaryRecords = False
|
||||
, tagSingleConstructors = True
|
||||
} ''AllocationPriority
|
||||
derivePersistFieldJSON ''AllocationPriority
|
||||
|
||||
deriving via E.JSONB AllocationPriority instance E.PersistField AllocationPriority
|
||||
deriving via E.JSONB AllocationPriority instance E.PersistFieldSql AllocationPriority
|
||||
|
||||
instance Binary AllocationPriority
|
||||
|
||||
@ -43,6 +50,10 @@ instance Csv.FromRecord (Map UserMatriculation AllocationPriority) where
|
||||
| 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
|
||||
= AllocationPriorityComparisonNumeric { allocationGradeScale :: 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"
|
||||
|
||||
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 .
|
||||
( MonoFoldable mono
|
||||
|
||||
@ -639,17 +639,30 @@ secretJsonField :: forall m a.
|
||||
secretJsonField = secretJsonField' $ fieldView (hiddenField :: Field m Text)
|
||||
|
||||
fileFieldMultiple :: Monad m => Field m [FileInfo]
|
||||
fileFieldMultiple = Field
|
||||
{ fieldParse = \_ files -> return $ case files of
|
||||
fileFieldMultiple = Field{..}
|
||||
where
|
||||
fieldEnctype = Multipart
|
||||
fieldParse _ files = return $ case files of
|
||||
[] -> Right Nothing
|
||||
fs -> Right $ Just fs
|
||||
, fieldView = \id' name attrs _ isReq ->
|
||||
fieldView id' name attrs _ isReq =
|
||||
[whamlet|
|
||||
$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 p field = field { fieldParse = \ts fs -> fieldParse field ts fs <&> \case
|
||||
|
||||
@ -2,9 +2,9 @@ $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>
|
||||
<input name=nav-container type=radio .navbar__container-radio--none checked #container-radio-none uw-no-radiobox>
|
||||
$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 -->
|
||||
^{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>
|
||||
Alle HTML-Eingabefelder akzeptieren nun stattdessen Markdown
|
||||
<li>
|
||||
Alle ausgehenden HTML E-Mails haben nun auch einen \
|
||||
Alle ausgehenden HTML E-Mails haben nun auch einen #
|
||||
Markdown-Teil
|
||||
|
||||
<dt .deflist__dt>
|
||||
@ -15,8 +15,8 @@ $newline never
|
||||
<dd .deflist__dd>
|
||||
<ul>
|
||||
<li>
|
||||
Prüfungen können nun angeben in welchem Format Leistungen \
|
||||
eingetragen werden dürfen (Bestanden/Nicht Bestanden, \
|
||||
Prüfungen können nun angeben in welchem Format Leistungen #
|
||||
eingetragen werden dürfen (Bestanden/Nicht Bestanden, #
|
||||
Numerische Noten oder Gemischt)
|
||||
|
||||
<dt .deflist__dt>
|
||||
|
||||
@ -14,7 +14,7 @@ $newline never
|
||||
<dd .deflist__dd>
|
||||
<ul>
|
||||
<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)
|
||||
|
||||
<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
|
||||
$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>
|
||||
$forall n <- filter isPageActionPrimary nav
|
||||
<li .pagenav__list-item>
|
||||
^{navWidget n}
|
||||
$if hasSecondaryPageActions
|
||||
<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>
|
||||
<i .fas .fa-fw .fa-#{iconText IconPageActionSecondary}>
|
||||
<div .pagenav-item__children-wrapper>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
$newline never
|
||||
^{pWidget}
|
||||
$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}>
|
||||
<i .fas .fa-fw .fa-#{iconText IconPageActionPrimaryExpand}>
|
||||
<div .pagenav-item__children-wrapper>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user