feat(allocations): upload of priorities

This commit is contained in:
Gregor Kleen 2020-02-27 16:31:38 +01:00
parent 2735d465eb
commit a590f45cc1
29 changed files with 558 additions and 104 deletions

View File

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

View File

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

View File

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

View File

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

View 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

View 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);
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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}

View File

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

View File

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