Merge branch 'master' into 126-ubungsbetrieb-statistik-seiten-pro-kurs

This commit is contained in:
Steffen Jost 2019-03-21 16:21:30 +01:00
commit 2ddb637b6e
22 changed files with 623 additions and 62 deletions

29
clean.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
case $1 in
"")
exec -- stack clean
;;
*)
target=".stack-work-${1}"
if [[ ! -d "${target}" ]]; then
printf "%s does not exist or is no directory\n" "${target}" >&2
exit 1
fi
if [[ -e .stack-work-clean ]]; then
printf ".stack-work-clean exists\n" >&2
exit 1
fi
move-back() {
mv -v .stack-work "${target}"
[[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work
}
mv -v .stack-work .stack-work-clean
mv -v "${target}" .stack-work
trap move-back EXIT
stack clean
;;
esac

View File

@ -681,3 +681,6 @@ DeleteConfirmation: Bestätigung
DeleteConfirmationWrong: Bestätigung muss genau dem angezeigten Text entsprechen.
DBTIRowsMissing n@Int: #{pluralDE n "Eine Zeile ist" "Einige Zeile sind"} aus der Datenbank verschwunden, seit das Formular für Sie generiert wurde
MassInputAddDimension: Hinzufügen
MassInputDeleteCell: Entfernen

View File

@ -114,6 +114,7 @@ dependencies:
- memcached-binary
- directory-tree
- lifted-base
- lattices
- hsass
other-extensions:

View File

@ -259,6 +259,32 @@ instance RenderMessage UniWorX SheetType where
mr :: RenderMessage UniWorX msg => msg -> Text
mr = renderMessage foundation ls
instance RenderMessage UniWorX StudyDegree where
renderMessage _found _ls StudyDegree{..} = fromMaybe (tshow studyDegreeKey) (studyDegreeName <|> studyDegreeShorthand)
newtype ShortStudyDegree = ShortStudyDegree StudyDegree
instance RenderMessage UniWorX ShortStudyDegree where
renderMessage _found _ls (ShortStudyDegree StudyDegree{..}) = fromMaybe (tshow studyDegreeKey) studyDegreeShorthand
instance RenderMessage UniWorX StudyTerms where
renderMessage _found _ls StudyTerms{..} = fromMaybe (tshow studyTermsKey) (studyTermsName <|> studyTermsShorthand)
newtype ShortStudyTerms = ShortStudyTerms StudyTerms
instance RenderMessage UniWorX ShortStudyTerms where
renderMessage _found _ls (ShortStudyTerms StudyTerms{..}) = fromMaybe (tshow studyTermsKey) studyTermsShorthand
data StudyDegreeTerm = StudyDegreeTerm StudyDegree StudyTerms
instance RenderMessage UniWorX StudyDegreeTerm where
renderMessage foundation ls (StudyDegreeTerm deg trm) = (mr trm) <> " (" <> (mr $ ShortStudyDegree deg) <> ")"
where
mr :: RenderMessage UniWorX msg => msg -> Text
mr = renderMessage foundation ls
newtype ErrorResponseTitle = ErrorResponseTitle ErrorResponse
embedRenderMessageVariant ''UniWorX ''ErrorResponseTitle ("ErrorResponseTitle" <>)
@ -315,6 +341,7 @@ data instance ButtonClass UniWorX
| BCWarning
| BCDanger
| BCLink
| BCMassInputAdd | BCMassInputDelete
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe (ButtonClass UniWorX)
instance Finite (ButtonClass UniWorX)

View File

@ -1,8 +1,9 @@
module Handler.Admin where
import Import
import Jobs
import Handler.Utils
import Handler.Utils.Form.MassInput
import Jobs
import Data.Aeson.Encode.Pretty (encodePrettyToTextBuilder)
import Control.Monad.Trans.Except
@ -11,6 +12,7 @@ import Control.Monad.Trans.Writer (mapWriterT)
import Utils.Lens
-- import Data.Time
import Data.Char (isDigit)
import qualified Data.Text as Text
-- import Data.Function ((&))
-- import Yesod.Form.Bootstrap3
@ -31,8 +33,6 @@ import qualified Handler.Utils.TermCandidates as Candidates
-- import qualified Data.UUID.Cryptographic as UUID
getAdminR :: Handler Html
getAdminR = -- do
siteLayoutMsg MsgAdminHeading $ do
@ -145,6 +145,61 @@ postAdminTestR = do
<li>#{m}
|]
{- The following demonstrates the use of @massInput@.
@massInput@ takes as arguments:
- A configuration struct describing how the Widget should behave (how is the space of sub-forms structured, how many dimensions does it have, which additions/deletions are permitted, what data do they need to operate and what should their effect on the overall shape be?)
- Information on how the resulting field fits into the form as a whole (@FieldSettings@ and whether the @massInput@ should be marked required)
- An initial value to pre-fill the field with
@massInput@ then returns an @MForm@ structured for easy downstream consumption of the result
-}
let
-- We define the fields of the configuration struct @MassInput@:
-- | Make a form for adding a point/line/plane/hyperplane/... (in this case: cell)
--
-- This /needs/ to replace all occurances of @mreq@ with @mpreq@ (no fields should be /actually/ required)
mkAddForm :: ListPosition -- ^ Approximate position of the add-widget
-> Natural -- ^ Dimension Index, outermost dimension ist 0 i.e. if dimension is 3 hyperplane-adders get passed 0, planes get passed 1, lines get 2, and points get 3
-> (Text -> Text) -- ^ Nudge deterministic field ids so they're unique
-> FieldView UniWorX -- ^ Submit-Button for this add-widget
-> Maybe (Form (ListLength -> (ListPosition, Int))) -- ^ Nothing iff adding further cells in this position/dimension makes no sense; returns callback to determine index of new cell and data needed to initialize cell
mkAddForm 0 0 nudge submitBtn = Just $ \csrf -> do
(addRes, addView) <- mpreq textField ("" & addName (nudge "text")) Nothing -- Any old field; for demonstration
let addRes' = fromMaybe 0 . readMay . Text.filter isDigit <$> addRes -- Do something semi-interesting on the result of the @textField@ to demonstrate that further processing can be done
addRes'' = (\dat l -> (fromIntegral l, dat)) <$> addRes' -- Construct the callback to determine new cell position and data within @FormResult@ as required
return (addRes'', toWidget csrf >> fvInput addView >> fvInput submitBtn)
mkAddForm _pos _dim _ _ = error "Dimension and Position is always 0 for our 1-dimensional form"
-- | Make a single massInput-Cell
--
-- This /needs/ to use @nudge@ and deterministic field naming (this allows for correct value-shifting when cells are deleted)
mkCellForm :: ListPosition -- ^ Position of this cell
-> Int -- ^ Data needed to initialize the cell (see return of @mkAddForm@)
-> Maybe Int -- ^ Initial cell result from Argument to `massInput`
-> (Text -> Text) -- ^ Nudge deterministic field ids so they're unique
-> Form Int
mkCellForm _pos cData initial nudge csrf = do -- Extremely simple cell
(intRes, intView) <- mreq intField ("" & addName (nudge "int")) $ initial <|> Just cData
return (intRes, toWidget csrf >> fvInput intView)
-- | How does the shape (`ListLength`) change if a certain cell is deleted?
deleteCell :: ListLength -- ^ Current shape
-> ListPosition -- ^ Coordinate to delete
-> MaybeT (MForm (HandlerT UniWorX IO)) (Map ListPosition ListPosition) -- ^ Monadically compute a set of new positions and their associated old positions
deleteCell l pos
| l >= 2 = return . Map.fromSet (\pos' -> bool pos' (succ pos') $ pos' >= pos) $ Set.fromList [0..fromIntegral (l - 2)] -- Close gap left by deleted position with values from further down the list; try prohibiting certain deletions by utilising `guard`
| otherwise = return Map.empty
-- | Make a decision on whether an add widget should be allowed to further cells, given the current @liveliness@ (i.e. before performing the addition)
allowAdd :: ListPosition -> Natural -> ListLength -> Bool
allowAdd _ _ l = l < 7 -- Limit list length; much more complicated checks are possible (this could in principle be monadic, but @massInput@ is probably already complicated enough to cover just current (2019-03) usecases)
-- The actual call to @massInput@ is comparatively simple:
((miResult, fvInput -> miForm), miEnc) <- runFormPost . identifyForm ("massinput" :: Text) $ massInput (MassInput mkAddForm mkCellForm deleteCell allowAdd) "" True Nothing
let locallyDefinedPageHeading = [whamlet|Admin TestPage for Uni2work|]
siteLayout locallyDefinedPageHeading $ do
-- defaultLayout $ do
@ -155,6 +210,22 @@ postAdminTestR = do
$(widgetFile "formPage")
showDemoResult
[whamlet|
<h2>Mass-Input
<form enctype=#{miEnc} method=POST>
^{miForm}
^{submitButtonView}
$case miResult
$of FormMissing
$of FormFailure errs
<ul>
$forall err <- errs
<li>#{err}
$of FormSuccess res
<p style="white-space:pre-wrap; font-family:monospace;">
#{tshow res}
|]
getAdminErrMsgR, postAdminErrMsgR :: Handler Html
getAdminErrMsgR = postAdminErrMsgR

View File

@ -734,19 +734,19 @@ colUserSemester = sortable (Just "semesternr") (i18nCell MsgStudyFeatureAge) $
colUserField :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserField = sortable (Just "field") (i18nCell MsgCourseStudyFeature) $
foldMap htmlCell . view (_userTableFeatures . _3)
foldMap i18nCell . view (_userTableFeatures . _3)
colUserFieldShort :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserFieldShort = sortable (Just "field-short") (i18nCell MsgCourseStudyFeature) $
foldMap (htmlCell . shortStudyTerms) . view (_userTableFeatures . _3)
foldMap (i18nCell . ShortStudyTerms) . view (_userTableFeatures . _3)
colUserDegree :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserDegree = sortable (Just "degree") (i18nCell MsgStudyFeatureDegree) $
foldMap htmlCell . preview (_userTableFeatures . _2 . _Just)
foldMap i18nCell . preview (_userTableFeatures . _2 . _Just)
colUserDegreeShort :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserDegreeShort = sortable (Just "degree-short") (i18nCell MsgStudyFeatureDegree) $
foldMap (htmlCell . shortStudyDegree) . preview (_userTableFeatures . _2 . _Just)
foldMap (i18nCell . ShortStudyDegree) . preview (_userTableFeatures . _2 . _Just)
makeCourseUserTable :: CourseId -> _ -> _ -> DB Widget
makeCourseUserTable cid colChoices psValidator =

View File

@ -193,7 +193,7 @@ deleteUser duid = do
getProfileDataR :: Handler Html
getProfileDataR = do
(uid, User{..}) <- requireAuthPair
-- mr <- getMessageRender
-- MsgRenderer mr <- getMsgRenderer
(admin_rights,lecturer_rights,lecture_corrector,studies) <- runDB $ (,,,) <$>
E.select
( E.from $ \(adright `E.InnerJoin` school) -> do
@ -222,14 +222,7 @@ getProfileDataR = do
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId
E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId
return ( ( studydegree E.^. StudyDegreeName
, studydegree E.^. StudyDegreeKey
)
, ( studyterms E.^. StudyTermsName
, studyterms E.^. StudyTermsKey
)
, studyfeat E.^. StudyFeaturesType
, studyfeat E.^. StudyFeaturesSemester)
return (studyfeat, studydegree, studyterms)
)
( (hasRows, ownedCoursesTable)
, enrolledCoursesTable

View File

@ -215,7 +215,6 @@ schoolFieldFor :: [SchoolId] -> Field Handler SchoolId
schoolFieldFor userSchools = selectField $ optionsPersistKey [SchoolShorthand <-. map unSchoolKey userSchools] [Asc SchoolName] schoolName
-- | Select one of the user's primary active courses, or from a given list of StudyFeatures (regardless of user)
-- (too many special cases, hence not used in course registration anymore)
studyFeaturesPrimaryFieldFor :: [StudyFeaturesId] -> Maybe UserId -> Field Handler (Maybe StudyFeaturesId)
studyFeaturesPrimaryFieldFor oldFeatures mbuid = selectField $ do
-- we need a join, so we cannot just use optionsPersistCryptoId
@ -225,8 +224,8 @@ studyFeaturesPrimaryFieldFor oldFeatures mbuid = selectField $ do
E.where_ $ ((feature E.^. StudyFeaturesId) `E.in_` E.valList oldFeatures)
E.||. isPrimaryActiveUserStudyFeature feature
return (feature E.^. StudyFeaturesId, degree, field)
mr <- getMessageRender
mkOptionList . nonEmptyOptions (mr MsgNoPrimaryStudyField) <$> mapM procOptions rawOptions
MsgRenderer mr <- getMsgRenderer
mkOptionList . nonEmptyOptions (mr MsgNoPrimaryStudyField) <$> mapM (procOptions mr) rawOptions
where
isPrimaryActiveUserStudyFeature feature = case mbuid of
Nothing -> E.val False
@ -234,13 +233,11 @@ studyFeaturesPrimaryFieldFor oldFeatures mbuid = selectField $ do
E.&&. feature E.^. StudyFeaturesValid E.==. E.val True
E.&&. feature E.^. StudyFeaturesType E.==. E.val FieldPrimary
procOptions :: (E.Value StudyFeaturesId, Entity StudyDegree, Entity StudyTerms) -> Handler (Option (Maybe StudyFeaturesId))
procOptions (E.Value sfid, Entity dgid StudyDegree{..}, Entity stid StudyTerms{..}) = do
let dgname = fromMaybe (tshow dgid) (studyDegreeShorthand <|> studyDegreeName)
stname = fromMaybe (tshow stid) (studyTermsShorthand <|> studyTermsName )
procOptions :: (StudyDegreeTerm -> Text) -> (E.Value StudyFeaturesId, Entity StudyDegree, Entity StudyTerms) -> Handler (Option (Maybe StudyFeaturesId))
procOptions mr (E.Value sfid, Entity _dgid sdegree, Entity _stid sterm) = do
cfid <- encrypt sfid
return Option
{ optionDisplay = stname <> " (" <> dgname <> ")"
{ optionDisplay = mr $ StudyDegreeTerm sdegree sterm
, optionInternalValue = Just sfid
, optionExternalValue = toPathPiece (cfid :: CryptoID UUID StudyFeaturesId)
}

View File

@ -0,0 +1,352 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Handler.Utils.Form.MassInput
( MassInput(..)
, massInput
, BoxDimension(..)
, IsBoxCoord(..), boxDimension
, Liveliness(..)
, ListLength(..), ListPosition(..)
) where
import Import
import Utils.Form
import Utils.Lens
import Handler.Utils.Form (secretJsonField)
import Data.Aeson
import Algebra.Lattice
import Text.Blaze (Markup)
import qualified Data.Text as Text
import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Data.Foldable as Fold
import Data.List (genericLength, genericIndex, iterate)
import Control.Monad.Trans.Maybe
import Control.Monad.Reader.Class (MonadReader(local))
import Control.Monad.Fix
data BoxDimension x = forall n. (Enum n, Num n) => BoxDimension (Lens' x n)
class (PathPiece x, ToJSONKey x, FromJSONKey x, Eq x, Ord x) => IsBoxCoord x where
boxDimensions :: [BoxDimension x]
boxOrigin :: x
boxDimension :: IsBoxCoord x => Natural -> BoxDimension x
boxDimension n
| n < genericLength dims = genericIndex dims n
| otherwise = error "boxDimension: insufficient dimensions"
where
dims = boxDimensions
-- zeroDimension :: IsBoxCoord x => Natural -> x -> x
-- zeroDimension (boxDimension -> BoxDimension dim) = set dim $ boxOrigin ^. dim
class (IsBoxCoord (BoxCoord a), Lattice a, BoundedJoinSemiLattice a) => Liveliness a where
type BoxCoord a :: *
liveCoords :: Prism' (Set (BoxCoord a)) a
liveCoord :: BoxCoord a -> Prism' Bool a
liveCoord bC = prism' (\l -> bC `Set.member` review liveCoords l) (bool (Just bottom) (preview liveCoords $ Set.singleton bC))
newtype ListLength = ListLength { unListLength :: Natural }
deriving newtype (Num, Integral, Real, Enum, PathPiece)
deriving (Eq, Ord, Generic, Typeable, Read, Show)
makeWrapped ''ListLength
instance JoinSemiLattice ListLength where
(\/) = max
instance MeetSemiLattice ListLength where
(/\) = min
instance Lattice ListLength
instance BoundedJoinSemiLattice ListLength where
bottom = 0
newtype ListPosition = ListPosition { unListPosition :: Natural }
deriving newtype (Num, Integral, Real, Enum, PathPiece, ToJSONKey, FromJSONKey)
deriving (Eq, Ord, Generic, Typeable, Read, Show)
makeWrapped ''ListPosition
instance IsBoxCoord ListPosition where
boxDimensions = [BoxDimension id]
boxOrigin = 0
instance Liveliness ListLength where
type BoxCoord ListLength = ListPosition
liveCoords = prism' toSet fromSet
where
toSet n
| n > 0 = Set.fromList [0..pred (fromIntegral n)]
| otherwise = Set.empty
fromSet ns
| ns == maybe Set.empty (\n -> Set.fromList [0..n]) max'
= fmap (succ . fromIntegral) max' <|> Just 0
| otherwise
= Nothing
where
max' = Set.lookupMax ns
liveCoord (ListPosition n) = prism' (\(ListLength l) -> n < l) (bool (Just 0) (1 <$ guard (n == 0)))
data ButtonMassInput coord
= MassInputAddDimension Natural coord
| MassInputDeleteCell coord
deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance PathPiece coord => PathPiece (ButtonMassInput coord) where
toPathPiece = \case
MassInputAddDimension n c -> "add__" <> toPathPiece n <> "__" <> toPathPiece c
MassInputDeleteCell c -> "delete__" <> toPathPiece c
fromPathPiece t = addDim <|> delCell
where
addDim = do
(dimT, Text.stripPrefix "__" -> Just coordT) <- Text.breakOn "__" <$> stripPrefix "add__" t
MassInputAddDimension <$> fromPathPiece dimT <*> fromPathPiece coordT
delCell = do
coordT <- stripPrefix "delete__" t
MassInputDeleteCell <$> fromPathPiece coordT
instance RenderMessage UniWorX (ButtonMassInput coord) where
renderMessage f ls = \case
MassInputAddDimension _ _ -> mr MsgMassInputAddDimension
MassInputDeleteCell _ -> mr MsgMassInputDeleteCell
where
mr = renderMessage f ls
instance PathPiece coord => Button UniWorX (ButtonMassInput coord) where
btnValidate _ _ = False
btnClasses (MassInputAddDimension _ _) = [BCIsButton, BCDefault, BCMassInputAdd]
btnClasses (MassInputDeleteCell _) = [BCIsButton, BCWarning, BCMassInputDelete]
data MassInputFieldName coord
= MassInputShape { miName :: Text }
| MassInputAddWidget { miName :: Text, miCoord :: coord, miAddWidgetField :: Text }
| MassInputAddButton { miName :: Text, miCoord :: coord }
| MassInputDeleteButton { miName :: Text, miCoord :: coord }
| MassInputCell { miName :: Text, miCoord :: coord, miCellField :: Text }
deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance IsBoxCoord coord => PathPiece (MassInputFieldName coord) where
toPathPiece = \case
MassInputShape{..} -> [st|#{miName}__shape|]
MassInputAddWidget{..} -> [st|#{miName}__add__#{toPathPiece miCoord}__fields__#{miAddWidgetField}|]
MassInputAddButton{..} -> [st|#{miName}__add__#{toPathPiece miCoord}__submit|]
MassInputDeleteButton{..} -> [st|#{miName}__delete__#{toPathPiece miCoord}|]
MassInputCell{..} -> [st|#{miName}__cells__#{toPathPiece miCoord}__#{miCellField}|]
fromPathPiece t = do
(miName, Text.stripPrefix "__" -> Just t') <- return $ Text.breakOn "__" t
choice
[ do
guard $ t' == "shape"
return MassInputShape{..}
, do
t'' <- Text.stripPrefix "add__" t'
(coordT, Text.stripPrefix "__" -> Just rest) <- return $ Text.breakOn "__" t''
miAddWidgetField <- Text.stripPrefix "fields__" rest
miCoord <- fromPathPiece coordT
return MassInputAddWidget{..}
, do
t'' <- Text.stripPrefix "add__" t'
(coordT, Text.stripPrefix "__" -> Just ident) <- return $ Text.breakOn "__" t''
guard $ ident == "submit"
miCoord <- fromPathPiece coordT
return MassInputAddButton{..}
, do
t'' <- Text.stripPrefix "delete__" t'
(coordT, rest) <- return $ Text.breakOn "__" t''
guard $ Text.null rest
miCoord <- fromPathPiece coordT
return MassInputDeleteButton{..}
, do
t'' <- Text.stripPrefix "cells__" t'
(coordT, Text.stripPrefix "__" -> Just miCellField) <- return $ Text.breakOn "__" t''
miCoord <- fromPathPiece coordT
return MassInputCell{..}
]
data MassInputException = MassInputInvalidShape
deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Exception MassInputException
data MassInput handler liveliness cellData cellResult = MassInput
{ miAdd :: BoxCoord liveliness -- Position (dimensions after @dimIx@ are zero)
-> Natural -- Zero-based dimension index @dimIx@
-> (Text -> Text) -- Nudge deterministic field ids
-> FieldView UniWorX -- Submit button
-> Maybe (Markup -> MForm handler (FormResult (liveliness -> (BoxCoord liveliness, cellData)), Widget)) -- ^ Construct a Cell-Addition Widget
, miCell :: BoxCoord liveliness -- Position
-> cellData -- @cellData@ from @miAdd@
-> Maybe cellResult -- Initial result from Argument to @massInput@
-> (Text -> Text) -- Nudge deterministic field ids
-> (Markup -> MForm handler (FormResult cellResult, Widget)) -- ^ Construct a singular cell
, miDelete :: liveliness
-> BoxCoord liveliness
-> MaybeT (MForm handler) (Map (BoxCoord liveliness) (BoxCoord liveliness)) -- ^ Decide whether a deletion-operation should be permitted and produce a finite map of new coordinates to their old correspondants
, miAllowAdd :: BoxCoord liveliness
-> Natural
-> liveliness
-> Bool -- ^ Decide whether an addition-operation should be permitted
}
massInput :: forall handler cellData cellResult liveliness.
( MonadHandler handler, HandlerSite handler ~ UniWorX
, ToJSON cellData, FromJSON cellData
, Liveliness liveliness
, MonadFix handler, MonadLogger handler
)
=> MassInput handler liveliness cellData cellResult
-> FieldSettings UniWorX
-> Bool -- ^ Required?
-> Maybe (Map (BoxCoord liveliness) (cellData, cellResult))
-> (Markup -> MForm handler (FormResult (Map (BoxCoord liveliness) (cellData, cellResult)), FieldView UniWorX))
massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
let initialShape = fmap fst <$> initialResult
miName <- maybe newFormIdent return fsName
let
shapeName :: MassInputFieldName (BoxCoord liveliness)
shapeName = MassInputShape{..}
shapeField = secretJsonField
sentShape <- runMaybeT $ do
ts <- fromMaybe [] . Map.lookup (toPathPiece shapeName) <$> MaybeT askParams
fs <- fromMaybe [] . Map.lookup (toPathPiece shapeName) <$> MaybeT askFiles
MaybeT $ either (const Nothing) id <$> lift (fieldParse shapeField ts fs)
sentShape' <- if
| Just s <- sentShape -> return s
| Just iS <- initialShape -> return iS
| Set.null $ review liveCoords (bottom :: liveliness) -> return Map.empty
| otherwise -> throwM MassInputInvalidShape
sentLiveliness <- maybe (throwM MassInputInvalidShape) return $ Map.keysSet sentShape' ^? liveCoords :: MForm handler liveliness
let addForm :: [BoxDimension (BoxCoord liveliness)] -> MForm handler (Map (Natural, BoxCoord liveliness) (FormResult (Maybe (liveliness -> (BoxCoord liveliness, cellData))), Maybe Widget))
addForm = addForm' boxOrigin . zip [0..]
where
addForm' _ [] = return Map.empty
addForm' miCoord ((dimIx, _) : remDims) = do
let nudgeAddWidgetName :: Text -> Text
nudgeAddWidgetName miAddWidgetField = toPathPiece MassInputAddWidget{..}
(btnRes', btnView) <- mopt (buttonField $ MassInputAddDimension dimIx miCoord) ("" & addName MassInputAddButton{..}) Nothing
let btnRes
| FormSuccess Nothing <- btnRes' = FormMissing
| FormSuccess (Just x) <- btnRes' = FormSuccess x
| otherwise = error "Value of btnRes should only be inspected if FormSuccess" <$ btnRes'
addRes' <- over (mapped . _Just . _1) (btnRes *>) . local (bool id (set _1 Nothing) $ is _FormMissing btnRes) . traverse ($ mempty) $
miAdd miCoord dimIx nudgeAddWidgetName btnView
let dimRes' = Map.singleton (dimIx, miCoord) (maybe (Nothing <$ btnRes) (fmap Just) $ fmap fst addRes', fmap snd addRes')
case remDims of
[] -> return dimRes'
((_, BoxDimension dim) : _) -> do
let
miCoords = Set.fromList . takeWhile (\c -> review (liveCoord c) sentLiveliness) $ iterate (over dim succ) miCoord
dimRess <- sequence $ Map.fromSet (\c -> addForm' c remDims) miCoords
return $ dimRes' `Map.union` fold dimRess
addResults <- addForm boxDimensions
let addShape
| [((dimIx, miCoord), FormSuccess (Just mkResult))] <- Map.toList . Map.filter (is $ _FormSuccess . _Just) $ fmap fst addResults
= Just $ maybe id (uncurry Map.insert) (mkResult sentLiveliness <$ guard (miAllowAdd miCoord dimIx sentLiveliness)) sentShape'
| otherwise = Nothing
addedShape <- if
| Just s <- addShape -> return s
| otherwise -> return sentShape'
addedLiveliness <- maybe (throwM MassInputInvalidShape) return $ Map.keysSet addedShape ^? liveCoords :: MForm handler liveliness
let
delForm :: BoxCoord liveliness -> MaybeT (MForm handler) (FormResult (Map (BoxCoord liveliness) (BoxCoord liveliness)), FieldView UniWorX)
delForm miCoord = do
(delRes, delView) <- lift $ mpreq (buttonField $ MassInputDeleteCell miCoord) ("" & addName MassInputDeleteButton{..}) Nothing
shapeUpdate <- miDelete addedLiveliness miCoord
guard $ isJust (Map.keysSet shapeUpdate ^? liveCoords :: Maybe liveliness)
return (shapeUpdate <$ delRes, delView)
delResults <- fmap (Map.mapMaybe id) . sequence $ Map.fromSet (runMaybeT . delForm) (Map.keysSet addedShape)
let
delShapeUpdate
| [FormSuccess shapeUpdate'] <- Map.elems . Map.filter (is _FormSuccess) $ fmap fst delResults = Just shapeUpdate'
| otherwise = Nothing
delShape = traverse (flip Map.lookup addedShape) =<< delShapeUpdate
let shapeChanged = Fold.any (isn't _FormMissing . view _1) addResults || Fold.any (is _FormSuccess . view _1) delResults
shape <- if
| Just s <- addShape -> return s
| Just s <- delShape -> return s
| otherwise -> return sentShape'
liveliness <- maybe (throwM MassInputInvalidShape) return $ Map.keysSet shape ^? liveCoords :: MForm handler liveliness
shapeId <- newIdent
let shapeInput = fieldView shapeField shapeId (toPathPiece shapeName) [] (Right shape) True
let
applyDelShapeUpdate :: Maybe (Env, FileEnv) -> Maybe (Env, FileEnv)
applyDelShapeUpdate prevEnv
| Just delShapeUpdate' <- delShapeUpdate
, Just (env, fEnv) <- prevEnv
= let reverseUpdate = Map.fromList . map swap $ Map.toList delShapeUpdate'
in Just . (, fEnv) . flip (Map.mapKeysWith mappend) env $ \k -> fromMaybe k $ do
cell@MassInputCell{miCoord} <- fromPathPiece k
newCoord <- Map.lookup miCoord reverseUpdate
return $ toPathPiece cell{ miCoord = newCoord }
| otherwise = prevEnv
justAdded :: Set (BoxCoord liveliness)
justAdded = Set.fromList . mapMaybe (addedCoord . fst) $ Map.elems addResults
where
addedCoord res
| FormSuccess (Just mkResult) <- res
= Just . fst $ mkResult sentLiveliness
| otherwise = Nothing
restrictJustAdded :: BoxCoord liveliness -> Maybe a -> Maybe a
restrictJustAdded miCoord env = env <* guard (not $ Set.member miCoord justAdded)
cellResults <- flip Map.traverseWithKey shape $ \miCoord cData -> do
let
nudgeCellName :: Text -> Text
nudgeCellName miCellField = toPathPiece (MassInputCell{..} :: MassInputFieldName (BoxCoord liveliness))
local (over _1 (applyDelShapeUpdate . restrictJustAdded miCoord)) $ (cData, ) <$> miCell miCoord cData (fmap snd . Map.lookup miCoord =<< initialResult) nudgeCellName mempty
let result :: FormResult (Map (BoxCoord liveliness) (cellData, cellResult))
result
| shapeChanged = FormMissing
| otherwise = traverse (\(cData, (cResult, _)) -> (cData, ) <$> cResult) cellResults
let miWidget' :: BoxCoord liveliness -> [(Natural, BoxDimension (BoxCoord liveliness))] -> Widget
miWidget' _ [] = mempty
miWidget' miCoord ((dimIx, BoxDimension dim) : remDims) =
let coords = takeWhile (\c -> review (liveCoord c) liveliness) $ iterate (over dim succ) miCoord
cells
| [] <- remDims = do
coord <- coords
Just (_data, (_cellRes, cellWdgt)) <- return $ Map.lookup coord cellResults
let deleteButton = snd <$> Map.lookup coord delResults
return (coord, $(widgetFile "widgets/massinput/cell"))
| otherwise =
[ (coord, miWidget' coord remDims) | coord <- coords ]
addWidget = (\(_, mWgt) -> mWgt <* guard (miAllowAdd miCoord dimIx liveliness)) =<< Map.lookup (dimIx, miCoord) addResults
in $(widgetFile "widgets/massinput/row")
miWidget = miWidget' boxOrigin $ zip [0..] boxDimensions
MsgRenderer mr <- getMsgRenderer
fvId <- maybe newIdent return fsId
let
fvLabel = toHtml $ mr fsLabel
fvTooltip = toHtml . mr <$> fsTooltip
fvInput = $(widgetFile "widgets/massinput/massinput")
fvErrors = Nothing
in return (result, FieldView{..})

View File

@ -61,6 +61,7 @@ import Database.Esqueleto.Instances as Import ()
import Database.Persist.Sql.Instances as Import ()
import Database.Persist.Sql as Import (SqlReadT,SqlWriteT)
import Numeric.Natural.Instances as Import ()
import System.Random as Import (Random)
import Control.Monad.Random.Class as Import (MonadRandom(..))

View File

@ -19,7 +19,6 @@ import Data.Aeson (Value)
import Data.CaseInsensitive (CI)
import Data.CaseInsensitive.Instances ()
import Text.Blaze (ToMarkup, toMarkup, Markup)
import Utils.Message (MessageStatus)
import Settings.Cluster (ClusterSettingsKey)
@ -43,20 +42,3 @@ deriving instance Binary (Key Term)
submissionRatingDone :: Submission -> Bool
submissionRatingDone Submission{..} = isJust submissionRatingTime
-- Do these instances belong here?
instance ToMarkup StudyDegree where
toMarkup StudyDegree{..} = toMarkup $
fromMaybe (tshow studyDegreeKey) (studyDegreeName <|> studyDegreeShorthand)
shortStudyDegree :: StudyDegree -> Markup
shortStudyDegree StudyDegree{..} = toMarkup $
fromMaybe (tshow studyDegreeKey) studyDegreeShorthand
instance ToMarkup StudyTerms where
toMarkup StudyTerms{..} = toMarkup $
fromMaybe (tshow studyTermsKey) (studyTermsName <|> studyTermsShorthand)
shortStudyTerms :: StudyTerms -> Markup
shortStudyTerms StudyTerms{..} = toMarkup $
fromMaybe (tshow studyTermsKey) studyTermsShorthand

View File

@ -0,0 +1,13 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Numeric.Natural.Instances
(
) where
import ClassyPrelude
import Numeric.Natural
import Web.PathPieces
instance PathPiece Natural where
toPathPiece = tshow
fromPathPiece = readMay

View File

@ -68,6 +68,8 @@ import qualified Crypto.Data.PKCS7 as PKCS7
import Data.Fixed (Centi)
import Data.Ratio ((%))
{-# ANN choice ("HLint: ignore Use asum" :: String) #-}
-----------
@ -276,6 +278,21 @@ stepTextCounter text
-- Data.Text.groupBy ((==) `on` isDigit) $ Data.Text.pack "12.ProMo Ue3bung00322 34 (H)"
-- ["12",".ProMo Ue","3","bung","00322"," ","34"," (H)"]
-- | Ignore warnings for unused variables with a more specific type
notUsedT :: a -> Text
notUsedT = notUsed
------------
-- Monoid --
------------
-- | Ignore warnings for unused variables
notUsed :: Monoid m => a -> m
notUsed = const mempty
------------
-- Tuples --
------------
@ -523,7 +540,7 @@ assertM f x = x >>= assertM' f
assertM_ :: MonadPlus m => (a -> Bool) -> m a -> m ()
assertM_ f x = guard . f =<< x
assertM' :: MonadPlus m => (a -> Bool) -> a -> m a
assertM' :: Alternative m => (a -> Bool) -> a -> m a
assertM' f x = x <$ guard (f x)
-- Some Utility Functions from Agda.Utils.Monad
@ -576,6 +593,12 @@ mconcatMapM f = foldM (\x my -> mappend x <$> my) mempty . map f . Fold.toList
mconcatForM :: (Monoid b, Monad m, Foldable f) => f a -> (a -> m b) -> m b
mconcatForM = flip mconcatMapM
-----------------
-- Alternative --
-----------------
choice :: forall f mono a. (Alternative f, MonoFoldable mono, Element mono ~ f a) => mono -> f a
choice = foldr (<|>) empty
--------------
-- Sessions --

View File

@ -145,8 +145,8 @@ addClass = addAttr "class"
addClasses :: [Text] -> FieldSettings site -> FieldSettings site
addClasses = addAttrs "class"
addName :: Text -> FieldSettings site -> FieldSettings site
addName nm fs = fs { fsName = Just nm }
addName :: PathPiece p => p -> FieldSettings site -> FieldSettings site
addName nm fs = fs { fsName = Just $ toPathPiece nm }
addNameClass :: Text -> Text -> FieldSettings site -> FieldSettings site
addNameClass gName gClass fs = fs { fsName = Just gName, fsAttrs = ("class",gClass) : fsAttrs fs }
@ -250,6 +250,7 @@ identifyForm' resLens identVal form fragment = do
identifyForm :: (Monad m, PathPiece ident, Eq ident) => ident -> (Html -> MForm m (FormResult a, widget)) -> (Html -> MForm m (FormResult a, widget))
identifyForm = identifyForm' id
{- Hinweise zur Erinnerung:
- identForm primär, wenn es mehr als ein Formular pro Handler gibt
@ -574,6 +575,12 @@ apreq f fs mx = formToAForm $ do
mr <- getMessageRender
over _1 (maybe (FormFailure [mr MsgValueRequired]) return =<<) . over _2 (pure . (\fv -> fv { fvRequired = True } )) <$> mopt f fs (Just <$> mx)
mpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> MForm m (FormResult a, FieldView site)
mpreq f fs mx = do
mr <- getMessageRender
over _1 (maybe (FormFailure [mr MsgValueRequired]) return =<<) . over _2 (\fv -> fv { fvRequired = True } ) <$> mopt f fs (Just <$> mx)
wpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a)
wpreq f fs mx = mFormToWForm $ do

View File

@ -80,6 +80,8 @@ makeLenses_ ''SheetType
makePrisms ''AuthResult
makePrisms ''FormResult
makeLenses_ ''StudyFeatures
makeLenses_ ''StudyDegree

View File

@ -34,7 +34,7 @@
options = options || {};
var tableIdent = options.dbtIdent;
var formId = formElement.querySelector('[name="_formid"]').value;
var formId = formElement.querySelector('[name="form-identifier"]').value;
var inputs = {
search: [],
input: [],
@ -127,7 +127,7 @@
function serializeFormToURL() {
var url = new URL(options.currentUrl || window.location.href);
url.searchParams.set('_formid', formId);
url.searchParams.set('form-identifier', formId);
url.searchParams.set('_hasdata', 'true');
url.searchParams.set(tableIdent + '-page', '0');

View File

@ -229,4 +229,30 @@
};
}
// Override implicit submit (pressing enter) behaviour to trigger a specified submit button instead of the default
window.utils.implicitSubmit = function(input, options) {
var submit = options.submit;
console.log('implicitSubmit', input, submit);
if (!submit) {
throw new Error('window.utils.implicitSubmit(input, options) needs to be passed a submit element via options');
}
var doSubmit = function(event) {
if (event.keyCode == 13) {
event.preventDefault();
submit.click();
}
};
input.addEventListener('keypress', doSubmit);
return {
scope: input,
destroy: function() {
input.removeEventListener('keypress', doSubmit);
},
};
}
})();

View File

@ -12,7 +12,7 @@
<dd .deflist__dd> #{display userIdent}
<dt .deflist__dt> _{MsgLastLogin}
<dd .deflist__dd>
$maybe llogin <- lastLogin
$maybe llogin <- lastLogin
#{llogin}
$nothing
_{MsgNever}
@ -45,25 +45,23 @@
<div .scrolltable>
<table .table.table--striped.table--hover.table--condensed>
<tr .table__row>
<th .table__th> Abschluss
<th .table__th> Studiengang
<th .table__th> Abschluss
<th .table__th> Studienart
<th .table__th> Semester
<th .table__th> Aktiv
<th .table__th> Update
$forall ((degree, degreeKey),(field, fieldKey),fieldtype,semester) <- studies
$forall ((Entity _ StudyFeatures{..}), (Entity _ degree), (Entity _ field)) <- studies
$with _ <- notUsedT studyFeaturesUser
<tr.table__row>
<td .table__td>
$maybe name <- E.unValue degree
#{display name}
$nothing
#{display degreeKey}
<td .table__td>
$maybe name <- E.unValue field
#{display name}
$nothing
#{display fieldKey}
<td .table__td>_{E.unValue fieldtype}
<td .table__td>#{display semester}
<td .table__td>_{field}#{notUsedT studyFeaturesField}
<td .table__td>_{degree}#{notUsedT studyFeaturesDegree}
<td .table__td>_{studyFeaturesType}
<td .table__td>#{display studyFeaturesSemester}
<td .table__td>#{hasTickmark studyFeaturesValid}
<td .table__td>^{formatTimeW SelFormatDateTime studyFeaturesUpdated}
<div .container>
$if hasRows

View File

@ -0,0 +1,3 @@
^{cellWdgt}
$maybe delWdgt <- fmap fvInput deleteButton
^{delWdgt}

View File

@ -0,0 +1,5 @@
$newline never
<div .massinput ##{fvId}>
#{csrf}
^{shapeInput}
^{miWidget}

View File

@ -0,0 +1,21 @@
document.addEventListener('DOMContentLoaded', function() {
var form = document.getElementById(#{String fvId}).closest('form');
var formSubmit = form.querySelector('input[type=submit], button[type=submit]:not(.btn-mass-input-add):not(.btn-mass-input-delete)');
var cellInputs = Array.from(form.querySelectorAll('.massinput--cell input:not([type=hidden])'));
cellInputs.forEach(function(input) {
window.utils.setup('implicitSubmit', input, { submit: formSubmit });
});
Array.from(form.querySelectorAll('.massinput--add')).forEach(function(wrapper) {
var addSubmit = wrapper.querySelector('.btn-mass-input-add');
var addInputs = Array.from(wrapper.querySelectorAll('input:not([type=hidden]):not(.btn-mass-input-add)'));
addInputs.forEach(function(input) {
window.utils.setup('implicitSubmit', input, { submit: addSubmit });
});
});
});

View File

@ -0,0 +1,7 @@
<ul .massinput--row .#{"massinput--dim" <> toPathPiece dimIx}>
$forall (cellCoord, cell) <- cells
<li .massinput--cell data-massinput-coord=#{toPathPiece cellCoord}>
^{cell}
$maybe add <- addWidget
<li .massinput--add>
^{add}