feat(allocations): implement application interface

This commit is contained in:
Gregor Kleen 2019-08-19 14:54:03 +02:00
parent ef625cd901
commit 4dcc82a770
25 changed files with 577 additions and 110 deletions

View File

@ -9,11 +9,9 @@
grid-gap: 5px; grid-gap: 5px;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
padding: 4px 0;
border-left: 2px solid transparent;
+ .form-group { + .form-group, + .form-section-legend, + .form-section-notification {
margin-top: 7px; margin-top: 11px;
} }
+ .form-section-title { + .form-section-title {

View File

@ -170,6 +170,18 @@ CourseApplicationInstructionsRegistration: Anweisungen zur Anmeldung
CourseApplicationTemplateApplication: Bewerbungsvorlage(n) CourseApplicationTemplateApplication: Bewerbungsvorlage(n)
CourseApplicationTemplateRegistration: Anmeldungsvorlage(n) CourseApplicationTemplateRegistration: Anmeldungsvorlage(n)
CourseApplicationTemplateArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungsvorlagen CourseApplicationTemplateArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungsvorlagen
CourseApplication: Bewerbung
CourseApplicationExists: Sie haben sich bereits für diesen Kurs beworben
CourseApplicationInvalidAction: Angegeben Aktion kann nicht durchgeführt werden
CourseApplicationCreated csh@CourseShorthand: Erfolgreich zu #{csh} beworben
CourseApplicationEdited csh@CourseShorthand: Bewerbung zu #{csh} erfolgreich angepasst
CourseApplicationNotEdited csh@CourseShorthand: Bewerbung zu #{csh} hat sich nicht verändert
CourseApplicationRated: Bewertung erfolgreich angepasst
CourseApplicationRatingDeleted: Bewertung erfolgreich entfernt
CourseApplicationDeleted csh@CourseShorthand: Bewerbung zu #{csh} erfolgreich zurückgezogen
CourseApplicationTitle displayName@Text csh@CourseShorthand: Bewerbung für #{csh}: #{displayName}
CourseApplicationText: Text-Bewerbung CourseApplicationText: Text-Bewerbung
CourseApplicationFollowInstructions: Beachten Sie die Anweisungen zur Bewerbung! CourseApplicationFollowInstructions: Beachten Sie die Anweisungen zur Bewerbung!
@ -183,6 +195,8 @@ CourseRegistrationFile: Datei zur Anmeldung
CourseRegistrationFiles: Datei(en) zur Anmeldung CourseRegistrationFiles: Datei(en) zur Anmeldung
CourseRegistrationArchive: Zip-Archiv der Datei(en) zur Anmeldung CourseRegistrationArchive: Zip-Archiv der Datei(en) zur Anmeldung
CourseApplicationNoFiles: Keine Datei(en) CourseApplicationNoFiles: Keine Datei(en)
CourseApplicationFilesNeedReupload: Bewerbungsdateien müssen neu hochgeladen werden, wann immer die Bewerbung angepasst wird
CourseRegistrationFilesNeedReupload: Dateien zur Anmeldung müssen neu hochgeladen werden, wann immer die Anmeldung angepasst wird
CourseApplicationDeleteToEdit: Um Ihre Bewerbung zu editieren müssen Sie sie zunächst zurückziehen und sich erneut bewerben. CourseApplicationDeleteToEdit: Um Ihre Bewerbung zu editieren müssen Sie sie zunächst zurückziehen und sich erneut bewerben.
CourseRegistrationDeleteToEdit: Um Ihre Anmeldungsdaten zu editieren müssen Sie sich zunächst ab- und dann erneut anmelden. CourseRegistrationDeleteToEdit: Um Ihre Anmeldungsdaten zu editieren müssen Sie sich zunächst ab- und dann erneut anmelden.
@ -372,7 +386,7 @@ MaterialFree: Kursmaterialien ohne Anmeldung zugänglich
UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung
UnauthorizedSystemMessageTime: Diese Systemnachricht ist noch nicht oder nicht mehr einsehbar. UnauthorizedSystemMessageTime: Diese Systemnachricht ist noch nicht oder nicht mehr einsehbar.
UnauthorizedSystemMessageAuth: Diese Systemnachricht ist nur für angemeldete Benutzer einsehbar. UnauthorizedSystemMessageAuth: Diese Systemnachricht ist nur für angemeldete Benutzer einsehbar.
UnsupportedAuthPredicate authTagT@Text shownRoute@String: "#{authTagT}" wurde auf eine Route angewandt, die dies nicht unterstützt: #{shownRoute} UnsupportedAuthPredicate authTagT@Text shownRoute@Text: „#{authTagT}“ wurde auf eine Route angewandt, die dies nicht unterstützt: #{shownRoute}
UnauthorizedDisabledTag authTag@AuthTag: Authorisierungsprädikat "#{toPathPiece authTag}" ist für Ihre Sitzung nicht aktiv UnauthorizedDisabledTag authTag@AuthTag: Authorisierungsprädikat "#{toPathPiece authTag}" ist für Ihre Sitzung nicht aktiv
UnknownAuthPredicate tag@String: Authorisierungsprädikat "#{tag}" ist dem System nicht bekannt UnknownAuthPredicate tag@String: Authorisierungsprädikat "#{tag}" ist dem System nicht bekannt
UnauthorizedRedirect: Die angeforderte Seite existiert nicht oder Sie haben keine Berechtigung, die angeforderte Seite zu sehen. UnauthorizedRedirect: Die angeforderte Seite existiert nicht oder Sie haben keine Berechtigung, die angeforderte Seite zu sehen.
@ -1472,6 +1486,7 @@ AllocationRegistrationEdited: Einstellungen zur Teilnahme an der Zentralanmeldun
BtnAllocationRegister: Teilnahme registrieren BtnAllocationRegister: Teilnahme registrieren
BtnAllocationRegistrationEdit: Teilnahme anpassen BtnAllocationRegistrationEdit: Teilnahme anpassen
AllocationParticipation: Teilnahme an der Zentralanmeldung AllocationParticipation: Teilnahme an der Zentralanmeldung
AllocationParticipationLoginFirst: Um an der Zentralanmeldung teilzunehmen, loggen Sie sich bitte zunächst ein.
AllocationCourses: Kurse AllocationCourses: Kurse
AllocationData: Organisatorisches AllocationData: Organisatorisches
AllocationCoursePriority i@Natural: #{i}. Wahl AllocationCoursePriority i@Natural: #{i}. Wahl

View File

@ -0,0 +1,22 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Crypto.Hash.Instances
() where
import ClassyPrelude
import Crypto.Hash
import Database.Persist
import Database.Persist.Sql
import Data.ByteArray (convert)
instance HashAlgorithm hash => PersistField (Digest hash) where
toPersistValue = PersistByteString . convert
fromPersistValue (PersistByteString bs) = maybe (Left "Could not convert Digest from ByteString") Right $ digestFromByteString bs
fromPersistValue _ = Left "Digest values must be converted from PersistByteString"
instance HashAlgorithm hash => PersistFieldSql (Digest hash) where
sqlType _ = SqlBlob

View File

@ -15,10 +15,13 @@ module Database.Esqueleto.Utils
, orderByOrd, orderByEnum , orderByOrd, orderByEnum
, lower, ciEq , lower, ciEq
, selectExists , selectExists
, SqlHashable
, sha256
, maybe
) where ) where
import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust) import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust, maybe)
import Data.Universe import Data.Universe
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.List as List import qualified Data.List as List
@ -27,6 +30,11 @@ import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E import qualified Database.Esqueleto.Internal.Sql as E
import Database.Esqueleto.Utils.TH import Database.Esqueleto.Utils.TH
import qualified Data.Text.Lazy as Lazy (Text)
import qualified Data.ByteString.Lazy as Lazy (ByteString)
import Crypto.Hash (Digest, SHA256)
{-# ANN any ("HLint: ignore Use any" :: String) #-} {-# ANN any ("HLint: ignore Use any" :: String) #-}
{-# ANN all ("HLint: ignore Use all" :: String) #-} {-# ANN all ("HLint: ignore Use all" :: String) #-}
@ -199,3 +207,28 @@ selectExists query = do
case res of case res of
[E.Value b] -> return b [E.Value b] -> return b
_other -> error "SELECT EXISTS ... returned zero or more than one rows" _other -> error "SELECT EXISTS ... returned zero or more than one rows"
class SqlHashable a
instance SqlHashable Text
instance SqlHashable ByteString
instance SqlHashable Lazy.Text
instance SqlHashable Lazy.ByteString
sha256 :: SqlHashable a => E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value (Digest SHA256))
sha256 = E.unsafeSqlFunction "digest" . (, E.val "sha256" :: E.SqlExpr (E.Value Text))
maybe :: (PersistField a, PersistField b)
=> E.SqlExpr (E.Value b)
-> (E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value b))
-> E.SqlExpr (E.Value (Maybe a))
-> E.SqlExpr (E.Value b)
maybe onNothing onJust val = E.case_
[ E.when_
(E.not_ $ E.isNothing val)
E.then_
(onJust $ E.veryUnsafeCoerceSqlExprValue val)
]
(E.else_ onNothing)

View File

@ -283,8 +283,12 @@ instance RenderMessage UniWorX MsgLanguage where
where where
mr = renderMessage foundation ls mr = renderMessage foundation ls
instance RenderMessage UniWorX (UnsupportedAuthPredicate (Route UniWorX)) where instance RenderMessage UniWorX (UnsupportedAuthPredicate AuthTag (Route UniWorX)) where
renderMessage f ls (UnsupportedAuthPredicate tag route) = renderMessage f ls $ MsgUnsupportedAuthPredicate tag (show route) renderMessage f ls (UnsupportedAuthPredicate tag route) = mr . MsgUnsupportedAuthPredicate (mr tag) $ Text.intercalate "/" pieces
where
mr :: forall msg. RenderMessage UniWorX msg => msg -> Text
mr = renderMessage f ls
(pieces, _) = renderRoute route
embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>) embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel
@ -1189,6 +1193,10 @@ tagAccessPredicate AuthSelf = APDB $ \mAuthId route _ -> exceptT return return $
appId <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt cID appId <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt cID
CourseApplication{..} <- maybeMExceptT (unauthorizedI MsgUnauthorizedSelf) . $cachedHereBinary appId $ get appId CourseApplication{..} <- maybeMExceptT (unauthorizedI MsgUnauthorizedSelf) . $cachedHereBinary appId $ get appId
return $ Right courseApplicationUser return $ Right courseApplicationUser
AllocationR _ _ _ (AApplicationR cID) -> do
appId <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt cID
CourseApplication{..} <- maybeMExceptT (unauthorizedI MsgUnauthorizedSelf) . $cachedHereBinary appId $ get appId
return $ Right courseApplicationUser
_other -> throwError =<< $unsupportedAuthPredicate AuthSelf route _other -> throwError =<< $unsupportedAuthPredicate AuthSelf route
referencedUser <- case referencedUser' of referencedUser <- case referencedUser' of
Right uid -> return uid Right uid -> return uid

View File

@ -21,7 +21,7 @@ import qualified Data.Map as Map
import Database.Persist.Sql (fromSqlKey) import Database.Persist.Sql (fromSqlKey)
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import Database.Esqueleto.Utils as E import Database.Esqueleto.Utils (mkExactFilter, mkContainsFilter)
import Handler.Utils.Table.Cells import Handler.Utils.Table.Cells
import qualified Handler.Utils.TermCandidates as Candidates import qualified Handler.Utils.TermCandidates as Candidates

View File

@ -9,12 +9,23 @@ module Handler.Allocation.Application
, getAApplicationR, postAApplicationR , getAApplicationR, postAApplicationR
) where ) where
import Import import Import hiding (hash)
import Handler.Utils import Handler.Utils
import Utils.Lens import Utils.Lens
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Set as Set
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import qualified Data.Conduit.List as C
import Crypto.Hash (hash)
import Control.Monad.Trans.State (execStateT)
import Control.Monad.State.Class (modify)
data AllocationApplicationButton = BtnAllocationApply data AllocationApplicationButton = BtnAllocationApply
@ -27,6 +38,7 @@ instance Finite AllocationApplicationButton
nullaryPathPiece ''AllocationApplicationButton $ camelToPathPiece' 1 nullaryPathPiece ''AllocationApplicationButton $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''AllocationApplicationButton id embedRenderMessage ''UniWorX ''AllocationApplicationButton id
makePrisms ''AllocationApplicationButton
instance Button UniWorX AllocationApplicationButton where instance Button UniWorX AllocationApplicationButton where
btnClasses BtnAllocationApplicationRetract = [BCIsButton, BCDanger] btnClasses BtnAllocationApplicationRetract = [BCIsButton, BCDanger]
@ -55,6 +67,7 @@ data ApplicationFormMode = ApplicationFormMode
, afmApplicantEdit :: Bool -- ^ Allow editing text, files, priority (if shown) , afmApplicantEdit :: Bool -- ^ Allow editing text, files, priority (if shown)
, afmLecturer :: Bool -- ^ Allow editing rating , afmLecturer :: Bool -- ^ Allow editing rating
} }
data ApplicationFormException = ApplicationFormNoApplication -- ^ Could not fill forced fields of application form with data from application data ApplicationFormException = ApplicationFormNoApplication -- ^ Could not fill forced fields of application form with data from application
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
@ -63,22 +76,27 @@ instance Exception ApplicationFormException
applicationForm :: AllocationId applicationForm :: AllocationId
-> CourseId -> CourseId
-> UserId -> UserId
-> Natural -- ^ Maximum @courseApplicationAllocationPriority@ among all applications
-> ApplicationFormMode -- ^ Which parts of the shared form to display -> ApplicationFormMode -- ^ Which parts of the shared form to display
-> Html -> MForm Handler (FormResult ApplicationForm, ApplicationFormView) -> Html -> MForm Handler (FormResult ApplicationForm, ApplicationFormView)
applicationForm aId cid uid maxPrio ApplicationFormMode{..} csrf = do applicationForm aId cid uid ApplicationFormMode{..} csrf = do
(mApp, coursesNum, Course{..}) <- liftHandlerT . runDB $ do (mApp, coursesNum, Course{..}, maxPrio) <- liftHandlerT . runDB $ do
mApplication <- listToMaybe <$> selectList [CourseApplicationAllocation ==. Just aId, CourseApplicationUser ==. uid, CourseApplicationCourse ==. cid] [LimitTo 1] mApplication <- listToMaybe <$> selectList [CourseApplicationAllocation ==. Just aId, CourseApplicationUser ==. uid, CourseApplicationCourse ==. cid] [LimitTo 1]
coursesNum <- fromIntegral <$> count [AllocationCourseAllocation ==. aId] coursesNum <- fromIntegral <$> count [AllocationCourseAllocation ==. aId]
course <- getJust cid course <- getJust cid
return (mApplication, coursesNum, course) [E.Value (fromMaybe 0 -> maxPrio)] <- E.select . E.from $ \courseApplication -> do
E.where_ $ courseApplication E.^. CourseApplicationCourse E.==. E.val cid
E.&&. courseApplication E.^. CourseApplicationUser E.==. E.val uid
E.&&. courseApplication E.^. CourseApplicationAllocation E.==. E.just (E.val aId)
E.&&. E.not_ (E.isNothing $ courseApplication E.^. CourseApplicationAllocationPriority)
return . E.joinV . E.max_ $ courseApplication E.^. CourseApplicationAllocationPriority
return (mApplication, coursesNum, course, maxPrio)
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
let let
oldPrio :: Maybe Natural oldPrio :: Maybe Natural
oldPrio = mApp >>= courseApplicationAllocationPriority . entityVal oldPrio = mApp >>= courseApplicationAllocationPriority . entityVal
coursesNum' = succ maxPrio `max` pred coursesNum coursesNum' = succ maxPrio `max` coursesNum
mkPrioOption :: Natural -> Option Natural mkPrioOption :: Natural -> Option Natural
mkPrioOption i = Option mkPrioOption i = Option
@ -89,7 +107,7 @@ applicationForm aId cid uid maxPrio ApplicationFormMode{..} csrf = do
prioOptions :: OptionList Natural prioOptions :: OptionList Natural
prioOptions = OptionList prioOptions = OptionList
{ olOptions = sortOn (Down . optionInternalValue) . map mkPrioOption $ [0 .. coursesNum'] { olOptions = sortOn (Down . optionInternalValue) . map mkPrioOption $ [0 .. pred coursesNum']
, olReadExternal = readMay , olReadExternal = readMay
} }
prioField = selectField' (Just $ SomeMessage MsgAllocationCourseNoApplication) $ return prioOptions prioField = selectField' (Just $ SomeMessage MsgAllocationCourseNoApplication) $ return prioOptions
@ -153,6 +171,12 @@ applicationForm aId cid uid maxPrio ApplicationFormMode{..} csrf = do
| otherwise | otherwise
-> return Nothing -> return Nothing
filesWarningView <- if
| fromMaybe False hasFiles && isn't _NoUpload courseApplicationsFiles && afmApplicantEdit
-> fmap (Just . snd) . formMessage =<< messageIconI Info IconFileUpload MsgCourseApplicationFilesNeedReupload
| otherwise
-> return Nothing
(filesRes, filesView) <- (filesRes, filesView) <-
let mkFs = bool MsgCourseApplicationFile MsgCourseApplicationArchive let mkFs = bool MsgCourseApplicationFile MsgCourseApplicationArchive
in if in if
@ -181,10 +205,10 @@ applicationForm aId cid uid maxPrio ApplicationFormMode{..} csrf = do
let let
buttons = catMaybes buttons = catMaybes
[ guardOn (not afmApplicantEdit && is _Just mApp) BtnAllocationApplicationRate [ guardOn (not afmApplicantEdit && is _Just mApp && afmLecturer) BtnAllocationApplicationRate
, guardOn ( afmApplicantEdit && is _Just mApp) BtnAllocationApplicationEdit , guardOn ( afmApplicantEdit && is _Just mApp ) BtnAllocationApplicationEdit
, guardOn ( afmApplicantEdit && is _Nothing mApp) BtnAllocationApply , guardOn ( afmApplicantEdit && is _Nothing mApp ) BtnAllocationApply
, guardOn ( afmApplicantEdit && is _Just mApp) BtnAllocationApplicationRetract , guardOn ( afmApplicantEdit && is _Just mApp ) BtnAllocationApplicationRetract
] ]
(actionRes, buttonsView) <- buttonForm' buttons csrf (actionRes, buttonsView) <- buttonForm' buttons csrf
@ -203,6 +227,7 @@ applicationForm aId cid uid maxPrio ApplicationFormMode{..} csrf = do
[ Just fieldView' [ Just fieldView'
, textView , textView
, filesLinkView , filesLinkView
, filesWarningView
] ++ maybe [] (map Just) filesView ++ ] ++ maybe [] (map Just) filesView ++
[ vetoView [ vetoView
, pointsView , pointsView
@ -215,9 +240,205 @@ applicationForm aId cid uid maxPrio ApplicationFormMode{..} csrf = do
postAApplyR :: TermId -> SchoolId -> AllocationShorthand -> CryptoUUIDCourse -> Handler Void editApplicationR :: AllocationId
postAApplyR = fail "Not implemented" -> UserId
-> CourseId
-> Maybe CourseApplicationId
-> ApplicationFormMode
-> (AllocationApplicationButton -> Bool)
-> SomeRoute UniWorX
-> Handler (ApplicationFormView, Enctype)
editApplicationR aId uid cid mAppId afMode allowAction postAction = do
Course{..} <- runDB $ get404 cid
getAApplicationR, postAApplicationR :: TermId -> SchoolId -> AllocationShorthand -> CryptoFileNameCourseApplication -> Handler Void ((appRes, appView), appEnc) <- runFormPost $ applicationForm aId cid uid afMode
formResult appRes $ \ApplicationForm{..} -> do
if
| BtnAllocationApply <- afAction
, allowAction afAction
-> runDB $ do
haveOld <- exists [ CourseApplicationCourse ==. cid
, CourseApplicationUser ==. uid
, CourseApplicationAllocation ==. Just aId
]
when haveOld $
invalidArgsI [MsgCourseApplicationExists]
now <- liftIO getCurrentTime
let rated = afRatingVeto || is _Just afRatingPoints
appId <- insert CourseApplication
{ courseApplicationCourse = cid
, courseApplicationUser = uid
, courseApplicationField = afField
, courseApplicationText = afText
, courseApplicationRatingVeto = afRatingVeto
, courseApplicationRatingPoints = afRatingPoints
, courseApplicationRatingComment = afRatingComment
, courseApplicationAllocation = Just aId
, courseApplicationAllocationPriority = afPriority
, courseApplicationTime = now
, courseApplicationRatingTime = guardOn rated now
}
let
sinkFile' file = do
fId <- insert file
insert_ $ CourseApplicationFile appId fId
forM_ afFiles $ \afFiles' ->
runConduit $ transPipe liftHandlerT afFiles' .| C.mapM_ sinkFile'
audit $ TransactionCourseApplicationEdit cid uid appId
addMessageI Success $ MsgCourseApplicationCreated courseShorthand
| is _BtnAllocationApplicationEdit afAction || is _BtnAllocationApplicationRate afAction
, allowAction afAction
, Just appId <- mAppId
-> runDB $ do
now <- liftIO getCurrentTime
changes <- if
| afmApplicantEdit afMode -> do
oldFiles <- Set.fromList . map (courseApplicationFileFile . entityVal) <$> selectList [CourseApplicationFileApplication ==. appId] []
changes <- flip execStateT oldFiles . forM_ afFiles $ \afFiles' ->
let sinkFile' file = do
oldFiles' <- lift . E.select . E.from $ \(courseApplicationFile `E.InnerJoin` file') -> do
E.on $ courseApplicationFile E.^. CourseApplicationFileFile E.==. file' E.^. FileId
E.where_ $ file' E.^. FileTitle E.==. E.val (fileTitle file)
E.&&. E.maybe
(E.val . is _Nothing $ fileContent file)
(\fc' -> maybe E.false (\fc -> E.sha256 fc' E.==. E.val (hash fc)) $ fileContent file)
(file' E.^. FileContent)
E.&&. file' E.^. FileId `E.in_` E.valList (Set.toList oldFiles)
return $ file' E.^. FileId
if
| [E.Value oldFileId] <- oldFiles'
-> modify $ Set.delete oldFileId
| otherwise
-> do
fId <- lift $ insert file
lift . insert_ $ CourseApplicationFile appId fId
modify $ Set.insert fId
in runConduit $ transPipe liftHandlerT afFiles' .| C.mapM_ sinkFile'
deleteCascadeWhere [ FileId <-. Set.toList (oldFiles `Set.intersection` changes) ]
return changes
| otherwise
-> return Set.empty
oldApp <- get404 appId
let newApp = oldApp
{ courseApplicationField = afField
, courseApplicationText = afText
, courseApplicationRatingVeto = afRatingVeto
, courseApplicationRatingPoints = afRatingPoints
, courseApplicationRatingComment = afRatingComment
, courseApplicationAllocation = Just aId
, courseApplicationAllocationPriority = afPriority
}
newRating = any (\f -> f oldApp newApp)
[ (/=) `on` courseApplicationRatingVeto
, (/=) `on` courseApplicationRatingPoints
, (/=) `on` courseApplicationRatingComment
]
hasRating = any ($ newApp)
[ courseApplicationRatingVeto
, is _Just . courseApplicationRatingPoints
]
appChanged = any (\f -> f oldApp newApp)
[ (/=) `on` courseApplicationField
, (/=) `on` courseApplicationText
, \_ _ -> not $ Set.null changes
]
newApp' = newApp
& bool id (set _courseApplicationRatingTime Nothing) appChanged
& bool id (set _courseApplicationRatingTime $ Just now) (newRating && hasRating)
& bool id (set _courseApplicationTime now) appChanged
replace appId newApp'
audit $ TransactionCourseApplicationEdit cid uid appId
uncurry addMessageI =<< case (afmLecturer afMode, newRating, hasRating, appChanged) of
(_, False, _, True) -> return (Success, MsgCourseApplicationEdited courseShorthand)
(_, False, _, False) -> return (Info, MsgCourseApplicationNotEdited courseShorthand)
(True, True, True, _) -> return (Success, MsgCourseApplicationRated)
(True, True, False, _) -> return (Success, MsgCourseApplicationRatingDeleted)
(False, True, _, _) -> permissionDenied "rating changed without lecturer rights"
| is _BtnAllocationApplicationRetract afAction
, allowAction afAction
, Just appId <- mAppId
-> runDB $ do
deleteCascade appId
audit $ TransactionCourseApplicationDeleted cid uid appId
addMessageI Success $ MsgCourseApplicationDeleted courseShorthand
| otherwise
-> invalidArgsI [MsgCourseApplicationInvalidAction]
redirect postAction
return (appView, appEnc)
postAApplyR :: TermId -> SchoolId -> AllocationShorthand -> CryptoUUIDCourse -> Handler Void
postAApplyR tid ssh ash cID = do
uid <- requireAuthId
cid <- decrypt cID
(aId, Course{..}) <- runDB $ do
aId <- getKeyBy404 $ TermSchoolAllocationShort tid ssh ash
course <- get404 cid
return (aId, course)
afmLecturer <- hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CEditR
let afMode = ApplicationFormMode
{ afmApplicant = True
, afmApplicantEdit = True
, afmLecturer
}
void . editApplicationR aId uid cid Nothing afMode (== BtnAllocationApply) . SomeRoute $ AllocationR tid ssh ash AShowR :#: cID
invalidArgs ["Application form required"]
getAApplicationR, postAApplicationR :: TermId -> SchoolId -> AllocationShorthand -> CryptoFileNameCourseApplication -> Handler Html
getAApplicationR = postAApplicationR getAApplicationR = postAApplicationR
postAApplicationR = fail "Not implemented" postAApplicationR tid ssh ash cID = do
uid <- requireAuthId
appId <- decrypt cID
(Entity aId Allocation{..}, Entity cid Course{..}, CourseApplication{..}, isAdmin, User{..}) <- runDB $ do
alloc <- getBy404 $ TermSchoolAllocationShort tid ssh ash
app <- get404 appId
Just course <- getEntity $ courseApplicationCourse app
Just appUser <- get $ courseApplicationUser app
isAdmin <- exists [UserAdminUser ==. uid, UserAdminSchool ==. alloc ^. _entityVal . _allocationSchool]
return (alloc, course, app, isAdmin, appUser)
afmLecturer <- hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CEditR
afmApplicantEdit <- hasWriteAccessTo . AllocationR tid ssh ash $ AApplicationR cID
courseCID <- encrypt cid :: Handler CryptoUUIDCourse
let afMode = ApplicationFormMode
{ afmApplicant = uid == courseApplicationUser || isAdmin
, afmApplicantEdit
, afmLecturer
}
(ApplicationFormView{..}, appEnc) <- editApplicationR aId uid cid (Just appId) afMode (/= BtnAllocationApply) $ if
| uid == courseApplicationUser
-> SomeRoute $ AllocationR tid ssh ash AShowR :#: courseCID
| otherwise
-> SomeRoute . AllocationR tid ssh ash $ AApplicationR cID
let title = MsgCourseApplicationTitle userDisplayName courseShorthand
siteLayoutMsg title $ do
setTitleI title
wrapForm ((<> snd afvButtons) . renderFieldViews FormStandard . maybe id (:) afvPriority$ afvForm) FormSettings
{ formMethod = POST
, formAction = Just . SomeRoute . AllocationR tid ssh ash $ AApplicationR cID
, formEncoding = appEnc
, formAttrs = []
, formSubmit = FormNoSubmit
, formAnchor = Nothing :: Maybe Text
}

View File

@ -60,8 +60,6 @@ getAShowR tid ssh ash = do
, formSubmit = FormSubmit , formSubmit = FormSubmit
, formAnchor = Nothing :: Maybe Text , formAnchor = Nothing :: Maybe Text
} }
let
maxPrio = maybe 0 maximum . fromNullable $ courses ^.. folded . resultCourseApplication . _entityVal . _courseApplicationAllocationPriority . _Just
siteLayoutMsg title $ do siteLayoutMsg title $ do
setTitleI shortTitle setTitleI shortTitle
@ -73,20 +71,26 @@ getAShowR tid ssh ash = do
cID <- encrypt cid :: WidgetT UniWorX IO CryptoUUIDCourse cID <- encrypt cid :: WidgetT UniWorX IO CryptoUUIDCourse
mayApply <- hasWriteAccessTo . AllocationR tid ssh ash $ AApplyR cID mayApply <- hasWriteAccessTo . AllocationR tid ssh ash $ AApplyR cID
isLecturer <- hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CEditR isLecturer <- hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CEditR
mApplyFormView <- liftHandlerT . for muid $ \uid -> generateFormPost . applicationForm aId cid uid maxPrio $ ApplicationFormMode True mayApply isLecturer mApplyFormView <- liftHandlerT . for muid $ \uid -> generateFormPost . applicationForm aId cid uid $ ApplicationFormMode True mayApply isLecturer
subRoute <- fmap (fromMaybe $ AApplyR cID) . for mApp $ \(Entity appId _) -> AApplicationR <$> encrypt appId subRoute <- fmap (fromMaybe $ AApplyR cID) . for mApp $ \(Entity appId _) -> AApplicationR <$> encrypt appId
let mApplyFormView' = view _1 <$> mApplyFormView let mApplyFormView' = view _1 <$> mApplyFormView
overrideVisible = not mayApply && is _Just mApp
case mApplyFormView of case mApplyFormView of
Just (_, appFormEnctype) Just (_, appFormEnctype)
-> wrapForm $(widgetFile "allocation/show/course") FormSettings -> wrapForm $(widgetFile "allocation/show/course") FormSettings
{ formMethod = POST { formMethod = POST
, formAction = Just . SomeRoute $ AllocationR tid ssh ash subRoute , formAction = Just . SomeRoute $ AllocationR tid ssh ash subRoute
, formEncoding = appFormEnctype , formEncoding = appFormEnctype
, formAttrs = [] , formAttrs = [ ("class", "allocation-course")
]
, formSubmit = FormNoSubmit , formSubmit = FormNoSubmit
, formAnchor = Nothing :: Maybe Text , formAnchor = Just cID
} }
Nothing Nothing
-> $(widgetFile "allocation/show/course") -> let wdgt = $(widgetFile "allocation/show/course")
in [whamlet|
<div .allocation-course ##{toPathPiece cID}>
^{wdgt}
|]
$(widgetFile "allocation/show") $(widgetFile "allocation/show")

View File

@ -25,10 +25,6 @@ getCAFilesR tid ssh csh cID = do
, ssh == courseSchool , ssh == courseSchool
, csh == courseShorthand , csh == courseShorthand
] ]
forM_ courseApplicationAllocation $ \aId -> do
Allocation{..} <- get404 aId
cCourse <- encrypt courseApplicationCourse :: DB CryptoUUIDCourse
redirectWith movedPermanently301 $ AllocationR courseTerm courseSchool allocationShorthand AShowR :#: toPathPiece cCourse
unless matches . redirectWith movedPermanently301 $ CApplicationR courseTerm courseSchool courseShorthand cID CAFilesR unless matches . redirectWith movedPermanently301 $ CApplicationR courseTerm courseSchool courseShorthand cID CAFilesR
get404 courseApplicationUser get404 courseApplicationUser

View File

@ -114,26 +114,26 @@ courseRegisterForm (Entity cid Course{..}) = liftHandlerT $ do
let appFilesInfo = (,) <$> hasFiles <*> appCID let appFilesInfo = (,) <$> hasFiles <*> appCID
filesMsg = bool MsgCourseRegistrationFiles MsgCourseApplicationFiles courseApplicationsRequired filesMsg = bool MsgCourseRegistrationFiles MsgCourseApplicationFiles courseApplicationsRequired
if when (isn't _NoUpload courseApplicationsFiles || fromMaybe False hasFiles) $
| isn't _NoUpload courseApplicationsFiles || fromMaybe False hasFiles let filesLinkField = Field{..}
-> let filesLinkField = Field{..} where
where fieldParse _ _ = return $ Right Nothing
fieldParse _ _ = return $ Right Nothing fieldEnctype = mempty
fieldEnctype = mempty fieldView theId _ attrs _ _
fieldView theId _ attrs _ _ = [whamlet|
= [whamlet| $newline never
$newline never $case appFilesInfo
$case appFilesInfo $of Just (True, appCID)
$of Just (True, appCID) <a ##{theId} *{attrs} href=@{CApplicationR courseTerm courseSchool courseShorthand appCID CAFilesR}>
<a ##{theId} *{attrs} href=@{CApplicationR courseTerm courseSchool courseShorthand appCID CAFilesR}> _{filesMsg}
_{filesMsg} $of _
$of _ <span ##{theId} *{attrs}>
<span ##{theId} *{attrs}> _{MsgCourseApplicationNoFiles}
_{MsgCourseApplicationNoFiles} |]
|] in void $ wforced filesLinkField (fslI filesMsg) Nothing
in void $ wforced filesLinkField (fslI filesMsg) Nothing
| otherwise when (fromMaybe False hasFiles && isn't _NoUpload courseApplicationsFiles) $
-> return () wformMessage <=< messageIconI Info IconFileUpload $ bool MsgCourseRegistrationFilesNeedReupload MsgCourseApplicationFilesNeedReupload courseApplicationsRequired
appFilesRes <- let mkFs | courseApplicationsRequired = bool MsgCourseApplicationFile MsgCourseApplicationArchive appFilesRes <- let mkFs | courseApplicationsRequired = bool MsgCourseApplicationFile MsgCourseApplicationArchive
| otherwise = bool MsgCourseRegistrationFile MsgCourseRegistrationArchive | otherwise = bool MsgCourseRegistrationFile MsgCourseRegistrationArchive

View File

@ -726,7 +726,7 @@ correctorForm shid = wFormToAForm $ do
-- when (not (Map.null loads) && applyDefaultLoads) $ -- Alert Message -- when (not (Map.null loads) && applyDefaultLoads) $ -- Alert Message
-- addMessageI Warning MsgCorrectorsDefaulted -- addMessageI Warning MsgCorrectorsDefaulted
when (not (Map.null loads) && applyDefaultLoads) $ -- Alert Notification when (not (Map.null loads) && applyDefaultLoads) $ -- Alert Notification
wformMessage =<< messageI Warning MsgCorrectorsDefaulted wformMessage =<< messageIconI Warning IconNoCorrectors MsgCorrectorsDefaulted
let let

View File

@ -12,7 +12,7 @@ import Import
-- import Text.Blaze (ToMarkup(..)) -- import Text.Blaze (ToMarkup(..))
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import Database.Esqueleto.Utils as E import Database.Esqueleto.Utils (mkExactFilter, mkExactFilterWith, mkContainsFilter, mkContainsFilterWith, anyFilter)
import Utils.Lens import Utils.Lens
import Handler.Utils import Handler.Utils

View File

@ -113,6 +113,7 @@ import Data.UUID.Instances as Import ()
import System.FilePath.Instances as Import () import System.FilePath.Instances as Import ()
import Net.IP.Instances as Import () import Net.IP.Instances as Import ()
import Data.Void.Instances as Import () import Data.Void.Instances as Import ()
import Crypto.Hash.Instances as Import ()
import Control.Monad.Trans.RWS (RWST) import Control.Monad.Trans.RWS (RWST)

View File

@ -127,7 +127,7 @@ requiresMigration = mapReaderT (exceptT return return) $ do
initialMigration :: Migration initialMigration :: Migration
-- ^ Manual migrations to go to InitialVersion below: -- ^ Manual migrations to go to InitialVersion below:
initialMigration = do initialMigration = do
migrateEnableExtension "citext" mapM_ migrateEnableExtension ["citext", "pgcrypto"]
migrateDBVersioning migrateDBVersioning
getMissingMigrations :: forall m m'. getMissingMigrations :: forall m m'.

View File

@ -105,16 +105,17 @@ guardAuthResult AuthenticationRequired = notAuthenticated
guardAuthResult (Unauthorized t) = permissionDenied t guardAuthResult (Unauthorized t) = permissionDenied t
guardAuthResult Authorized = return () guardAuthResult Authorized = return ()
data UnsupportedAuthPredicate route = UnsupportedAuthPredicate Text route data UnsupportedAuthPredicate tag route = UnsupportedAuthPredicate tag route
deriving (Eq, Ord, Typeable, Show) deriving (Eq, Ord, Typeable, Show)
instance (Show route, Typeable route) => Exception (UnsupportedAuthPredicate route) instance (Show tag, Typeable tag, Show route, Typeable route) => Exception (UnsupportedAuthPredicate tag route)
unsupportedAuthPredicate :: ExpQ unsupportedAuthPredicate :: ExpQ
unsupportedAuthPredicate = do unsupportedAuthPredicate = do
logFunc <- logErrorS logFunc <- logErrorS
[e| \tag route -> do [e| \tag route -> do
$(return logFunc) "AccessControl" [st|"!#{toPathPiece tag}" used on route that doesn't support it: #{tshow route}|] tRoute <- toTextUrl route
unauthorizedI (UnsupportedAuthPredicate (toPathPiece tag) route) $(return logFunc) "AccessControl" $ "!" <> toPathPiece tag <> " used on route that doesn't support it: " <> tRoute
unauthorizedI (UnsupportedAuthPredicate tag route)
|] |]
-- | allows conditional attributes in hamlet via *{..} syntax -- | allows conditional attributes in hamlet via *{..} syntax

View File

@ -48,6 +48,10 @@ existsKey :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity r
=> Key record -> ReaderT backend m Bool => Key record -> ReaderT backend m Bool
existsKey = fmap isJust . get -- TODO optimize, so that DB does not deliver entire record existsKey = fmap isJust . get -- TODO optimize, so that DB does not deliver entire record
exists :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistQueryRead backend, MonadIO m)
=> [Filter record] -> ReaderT backend m Bool
exists = fmap (not . null) . flip selectKeysList [LimitTo 1]
updateBy :: (PersistUniqueRead backend, PersistStoreWrite backend, MonadIO m, PersistRecordBackend record backend ) updateBy :: (PersistUniqueRead backend, PersistStoreWrite backend, MonadIO m, PersistRecordBackend record backend )
=> Unique record -> [Update record] -> ReaderT backend m () => Unique record -> [Update record] -> ReaderT backend m ()
updateBy uniq updates = do updateBy uniq updates = do

View File

@ -808,15 +808,26 @@ wformMessage :: (MonadHandler m) => Message -> WForm m ()
wformMessage = void . aFormToWForm . aformMessage wformMessage = void . aFormToWForm . aformMessage
formMessage :: (MonadHandler m) => Message -> MForm m (FormResult (), FieldView site) formMessage :: (MonadHandler m) => Message -> MForm m (FormResult (), FieldView site)
formMessage Message{ messageIcon = _, ..} = do -- custom icons are not currently implemented for `.notification` formMessage Message{..} = do
return (FormSuccess (), FieldView return (FormSuccess (), FieldView
{ fvLabel = mempty { fvLabel = mempty
, fvTooltip = Nothing , fvTooltip = Nothing
, fvId = idFormMessageNoinput , fvId = idFormMessageNoinput
, fvErrors = Nothing , fvErrors = Nothing
, fvRequired = False , fvRequired = False
, fvInput = [whamlet|<div .notification .notification-#{toPathPiece messageStatus}>#{messageContent}|] , fvInput = [whamlet|
$newline never
<div .notification .notification-#{toPathPiece messageStatus} .fa-#{maybe defaultIcon iconText messageIcon}>
<div .notification__content>
#{messageContent}
|]
}) })
where
defaultIcon = case messageStatus of
Success -> "check-circle"
Info -> "info-circle"
Warning -> "exclamation-circle"
Error -> "exclamation-triangle"
--------------------- ---------------------
-- Form evaluation -- -- Form evaluation --

View File

@ -47,6 +47,7 @@ data Icon
| IconCommentFalse | IconCommentFalse
| IconLink | IconLink
| IconFileDownload | IconFileDownload
| IconFileUpload
| IconFileZip | IconFileZip
| IconFileCSV | IconFileCSV
| IconSFTQuestion -- for SheetFileType only | IconSFTQuestion -- for SheetFileType only
@ -57,6 +58,7 @@ data Icon
| IconRegisterTemplate | IconRegisterTemplate
| IconApplyTrue | IconApplyTrue
| IconApplyFalse | IconApplyFalse
| IconNoCorrectors
deriving (Eq, Ord, Enum, Bounded, Show, Read) deriving (Eq, Ord, Enum, Bounded, Show, Read)
iconText :: Icon -> Text iconText :: Icon -> Text
@ -78,6 +80,7 @@ iconText = \case
IconCommentFalse -> "comment-slash" -- comment-alt-slash is not available for free IconCommentFalse -> "comment-slash" -- comment-alt-slash is not available for free
IconLink -> "link" IconLink -> "link"
IconFileDownload -> "file-download" IconFileDownload -> "file-download"
IconFileUpload -> "file-upload"
IconFileZip -> "file-archive" IconFileZip -> "file-archive"
IconFileCSV -> "file-csv" IconFileCSV -> "file-csv"
IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar) IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar)
@ -88,6 +91,7 @@ iconText = \case
IconRegisterTemplate -> "file-alt" IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt" IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash" IconApplyFalse -> "trash"
IconNoCorrectors -> "user-slash"
instance Universe Icon instance Universe Icon
instance Finite Icon instance Finite Icon

View File

@ -155,6 +155,8 @@ makePrisms ''AuthenticationMode
makeLenses_ ''CourseUserNote makeLenses_ ''CourseUserNote
makeLenses_ ''CourseApplication makeLenses_ ''CourseApplication
makeLenses_ ''Allocation
-- makeClassy_ ''Load -- makeClassy_ ''Load

View File

@ -6,6 +6,7 @@ module Utils.Message
, addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget , addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget
, statusToUrgencyClass , statusToUrgencyClass
, Message(..) , Message(..)
, messageIconI
, messageI, messageIHamlet, messageFile, messageWidget , messageI, messageIHamlet, messageFile, messageWidget
) where ) where
@ -140,6 +141,11 @@ messageI messageStatus msg = do
let messageIcon = Nothing let messageIcon = Nothing
return Message{..} return Message{..}
messageIconI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => MessageStatus -> Icon -> msg -> m Message
messageIconI messageStatus (Just -> messageIcon) msg = do
messageContent <- toHtml . ($ msg) <$> getMessageRender
return Message{..}
addMessageIHamlet :: ( MonadHandler m addMessageIHamlet :: ( MonadHandler m
, RenderMessage (HandlerSite m) msg , RenderMessage (HandlerSite m) msg
, HandlerSite m ~ site , HandlerSite m ~ site

View File

@ -34,28 +34,34 @@ $newline never
<dd .deflist__dd> <dd .deflist__dd>
^{formatTimeRangeW SelFormatDateTime fromT allocationStaffAllocationTo} ^{formatTimeRangeW SelFormatDateTime fromT allocationStaffAllocationTo}
$if mayRegister || is _Just registration $if is _Just muid
$if mayRegister || is _Just registration
<section id=allocation-participation>
<h2>
_{MsgAllocationParticipation}
$if mayRegister
^{registerForm'}
$else
$maybe Entity _ AllocationUser{allocationUserTotalCourses} <- registration
<dl .deflist>
<dt .deflist__dt>
_{MsgAllocationTotalCourses}
<dd .deflist__dd>
#{allocationUserTotalCourses}
$else
<section id=allocation-participation> <section id=allocation-participation>
<h2> <h2>
_{MsgAllocationParticipation} _{MsgAllocationParticipation}
$if mayRegister <p>
^{registerForm'} _{MsgAllocationParticipationLoginFirst}
$else
$maybe Entity _ AllocationUser{allocationUserTotalCourses} <- registration
<dl .deflist>
<dt .deflist__dt>
_{MsgAllocationTotalCourses}
<dd .deflist__dd>
#{allocationUserTotalCourses}
$if not (null courseWidgets) $if not (null courseWidgets)
<section .allocation> <section .allocation>
<h2> <h2>
_{MsgAllocationCourses} _{MsgAllocationCourses}
<div .allocation__explanation> <div .allocation__explanation .allocation__label>
<p>_{MsgAllocationPriorityTip} <p>_{MsgAllocationPriorityTip}
<p>_{MsgAllocationPriorityRelative} <p>_{MsgAllocationPriorityRelative}
<div .allocation__priority-label> <div .allocation__courses>
_{MsgAllocationPriority} $forall courseWgt <- courseWidgets
$forall courseWgt <- courseWidgets ^{courseWgt}
^{courseWgt}

View File

@ -0,0 +1,85 @@
.allocation__label {
color: var(--color-fontsec);
font-style: italic;
}
.allocation__courses {
margin-top: 20px;
}
.allocation-course {
display: grid;
grid-template-columns: 140px 1fr;
grid-template-areas:
'. name '
'prio-label prio '
'instr-label instr '
'form-label form ';
grid-gap: 5px 7px;
padding: 12px 10px;
&:last-child {
padding: 12px 10px 0 10px;
}
& + .allocation-course {
border-top: 1px solid var(--color-grey);
}
.allocation-course__priority {
grid-area: prio;
}
.allocation-course__priority-label {
grid-area: prio-label;
justify-self: end;
align-self: center;
text-align: right;
}
.allocation-course__name {
grid-area: name;
align-self: center;
font-size: 1.2rem;
}
.allocation-course__instructions {
grid-area: instr;
}
.allocation-course__instructions-label {
grid-area: instr-label;
justify-self: end;
text-align: right;
}
.allocation-course__application {
grid-area: form;
}
.allocation-course__application-label {
grid-area: form-label;
justify-self: end;
text-align: right;
padding-top: 6px;
}
}
@media (max-width: 426px) {
.allocation-course {
grid-template-columns: 1fr;
grid-template-areas:
'name '
'prio-label '
'prio '
'instr-label'
'instr '
'form-label '
'form ';
}
.allocation-course__application-label {
padding-top: 0;
}
}

View File

@ -1,20 +1,27 @@
<div .allocation-course id=#{toPathPiece cID}> $if is _Just muid
<div .allocation-course__priority-label .allocation__label>
_{MsgAllocationPriority}
<div .allocation-course__priority> <div .allocation-course__priority>
$maybe prioView <- mApplyFormView' >>= afvPriority $maybe prioView <- mApplyFormView' >>= afvPriority
^{fvInput prioView} ^{fvInput prioView}
$nothing $nothing
_{MsgAllocationNoApplication} _{MsgAllocationNoApplication}
<a .allocation-course__name href=@{CourseR courseTerm courseSchool courseShorthand CShowR}> <a .allocation-course__name href=@{CourseR courseTerm courseSchool courseShorthand CShowR} target="_blank">
#{courseName} #{courseName}
$maybe aInst <- courseApplicationsInstructions $if hasApplicationTemplate || is _Just courseApplicationsInstructions
<div .allocation-course__instructions> <div .allocation-course__instructions-label .allocation__label>
_{MsgCourseApplicationInstructionsApplication}
<div .allocation-course__instructions>
$maybe aInst <- courseApplicationsInstructions
<p> <p>
#{aInst} #{aInst}
$if hasApplicationTemplate $if hasApplicationTemplate
<p> <p>
<a href=@{CourseR courseTerm courseSchool courseShorthand CRegisterTemplateR}> <a href=@{CourseR courseTerm courseSchool courseShorthand CRegisterTemplateR}>
#{iconRegisterTemplate} _{MsgCourseApplicationTemplateApplication} #{iconRegisterTemplate} _{MsgCourseApplicationTemplateApplication}
$maybe ApplicationFormView{ ..} <- mApplyFormView' $maybe ApplicationFormView{ ..} <- mApplyFormView'
<div .allocation-course__application uw-interactive-fieldset data-conditional-input=#{maybe "" fvId afvPriority} data-conditional-value="" data-conditional-negated> <div .allocation-course__application-label .allocation__label :not overrideVisible:uw-interactive-fieldset data-conditional-input=#{maybe "" fvId afvPriority} data-conditional-value="" data-conditional-negated>
^{renderFieldViews FormStandard afvForm} _{MsgCourseApplication}
^{snd afvButtons} <div .allocation-course__application :not overrideVisible:uw-interactive-fieldset data-conditional-input=#{maybe "" fvId afvPriority} data-conditional-value="" data-conditional-negated>
^{renderFieldViews FormStandard afvForm}
^{snd afvButtons}

View File

@ -180,11 +180,15 @@ h4 {
} }
p { p {
margin: 10px 0; margin: 0.5rem 0;
}
p:last-child { &:last-child {
margin: 10px 0 0; margin: 0.5rem 0 0;
}
&:first-of-type {
margin: 0;
}
} }
} }
@ -546,6 +550,7 @@ section {
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
padding-bottom: 0px;
} }
} }
@ -564,33 +569,64 @@ section {
border-radius: 3px; border-radius: 3px;
padding: 10px 20px 20px; padding: 10px 20px 20px;
margin: 40px 0; margin: 40px 0;
color: var(--color-dark);
box-shadow: 0 0 4px 2px inset currentColor; box-shadow: 0 0 4px 2px inset currentColor;
padding-left: 20%; padding-left: 100px;
min-height: 100px; min-height: 100px;
max-width: 700px;
font-weight: 600;
vertical-align: center;
display: grid;
grid-column: 2;
&::before { &::before {
content: 'i'; font-family: "Font Awesome 5 Free";
font-weight: 900;
position: absolute; position: absolute;
display: flex; display: flex;
left: 0; left: 0;
top: 0; top: 0;
height: 100%; height: 100%;
width: 20%; width: 100px;
font-size: 100px; font-size: 50px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.notification__content {
grid-column: 1;
align-self: center;
}
} }
.form-group__input > .notification { .form-section-notification {
margin: 0; display: grid;
grid-template-columns: 1fr 3fr;
grid-gap: 5px;
.notification {
margin: 0;
}
+ .form-group, + .form-section-legend, + .form-section-notification {
margin-top: 11px;
}
+ .form-section-title {
margin-top: 40px;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.form-section-notification {
grid-template-columns: 1fr;
margin-top: 17px;
}
.notification { .notification {
grid-column: 1;
max-width: none;
padding-left: 40px; padding-left: 40px;
&::before { &::before {
@ -602,16 +638,20 @@ section {
} }
} }
.notification-danger { .notification-error {
color: #c51919 ; color: var(--color-error) ;
&::before {
content: '!';
}
} }
.notification__content { .notification-warning {
color: var(--color-font); color: var(--color-warning) ;
}
.notification-info {
color: var(--color-lightblack) ;
}
.notification-success {
color: var(--color-warning) ;
} }

View File

@ -11,6 +11,9 @@ $case formLayout
$if fvId view == idFormSectionNoinput $if fvId view == idFormSectionNoinput
<h3 .form-section-title> <h3 .form-section-title>
^{fvLabel view} ^{fvLabel view}
$elseif fvId view == idFormMessageNoinput
<div .form-section-notification>
^{fvInput view}
$else $else
<div .form-group .interactive-fieldset__target :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error> <div .form-group .interactive-fieldset__target :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
$if not (Blaze.null $ fvLabel view) $if not (Blaze.null $ fvLabel view)