fix(number-input-fields): number inputs made HTML5 compatible

number inputs of rational fields allow rational inputs and optionally
provide min-max attributes for HTML5

Closes #412
This commit is contained in:
Steffen Jost 2019-07-04 13:56:04 +02:00
parent 8ec00220da
commit 609821595b
5 changed files with 60 additions and 21 deletions

View File

@ -449,6 +449,7 @@ RatingPercent: Erreicht
RatingFiles: Korrigierte Dateien RatingFiles: Korrigierte Dateien
PointsNotPositive: Punktzahl darf nicht negativ sein PointsNotPositive: Punktzahl darf nicht negativ sein
PointsTooHigh maxPoints@Points: Punktzahl darf nicht höher als #{maxPoints} sein PointsTooHigh maxPoints@Points: Punktzahl darf nicht höher als #{maxPoints} sein
PointsTooLow minPoints@Points: Punktzahl darf nicht kleiner als #{minPoints} sein
RatingPointsDone: Abgabe zählt als korrigiert, gdw. Punktezahl gesetzt ist RatingPointsDone: Abgabe zählt als korrigiert, gdw. Punktezahl gesetzt ist
ColumnRatingPoints: Punktzahl ColumnRatingPoints: Punktzahl
Pseudonyms: Pseudonyme Pseudonyms: Pseudonyme

View File

@ -215,7 +215,7 @@ colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ for
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _) } -> return subId) (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _) } -> return subId)
(\DBRow{ dbrOutput=(Entity _ Submission{..}, Entity _ Sheet{..}, _, _, _, _) } mkUnique -> case sheetType of (\DBRow{ dbrOutput=(Entity _ Submission{..}, Entity _ Sheet{..}, _, _, _, _) } mkUnique -> case sheetType of
NotGraded -> over (_1.mapped) (_2 .~) <$> pure (FormSuccess Nothing, mempty) NotGraded -> over (_1.mapped) (_2 .~) <$> pure (FormSuccess Nothing, mempty)
_other -> over (_1.mapped) (_2 .~) . over _2 fvInput <$> mopt pointsField (fsUniq mkUnique "points") (Just submissionRatingPoints) _other -> over (_1.mapped) (_2 .~) . over _2 fvInput <$> mopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) (fsUniq mkUnique "points") (Just submissionRatingPoints)
) )
colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData))) colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData)))

View File

@ -241,30 +241,38 @@ htmlField' = htmlField
} }
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
natFieldI msg = convertField fromInteger toInteger $ checkBool (>= 0) msg intField natFieldI msg = convertField fromInteger toInteger $ checkBool (>= 0) msg $ intMinField 0
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
natField d = convertField fromInteger toInteger $ checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") intField natField d = convertField fromInteger toInteger $ checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") $ intMinField 0
natIntField ::(Monad m, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m Integer natIntField ::(Monad m, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m Integer
natIntField = natField natIntField = natField
posIntField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i posIntField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
posIntField d = convertField fromInteger toInteger $ checkBool (> 0) (T.append d " muss eine positive Zahl sein.") intField posIntField d = convertField fromInteger toInteger $ checkBool (> 0) (T.append d " muss eine positive Zahl sein.") $ intMinField 1
posIntFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i posIntFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
posIntFieldI msg = convertField fromInteger toInteger $ checkBool (> 0) msg intField posIntFieldI msg = convertField fromInteger toInteger $ checkBool (> 0) msg $ intMinField 0
-- | Field to request integral number > 'm' -- | Field to request integral number > 'm'
minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Text -> Field m i minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Text -> Field m i
minIntField m d = checkBool (> m) (T.concat [d," muss größer als ", T.pack $ show m, " sein."]) intField minIntField m d = checkBool (> m) (T.concat [d," muss größer als ", T.pack $ show m, " sein."]) $ intMinField m
pointsField :: (Monad m, HandlerSite m ~ UniWorX) => Field m Points pointsField :: (Monad m, HandlerSite m ~ UniWorX) => Field m Points
pointsField = checkBool (>= 0) MsgPointsNotPositive fixedPrecField pointsField = pointsFieldMinMax (Just 0) Nothing
pointsFieldMax :: (Monad m, HandlerSite m ~ UniWorX) => Maybe Points -> Field m Points pointsFieldMax :: (Monad m, HandlerSite m ~ UniWorX) => Maybe Points -> Field m Points
pointsFieldMax Nothing = pointsField pointsFieldMax limit = pointsFieldMinMax (Just 0) limit
pointsFieldMax (Just maxp) = checkBool (<= maxp) (MsgPointsTooHigh maxp) pointsField
pointsFieldMinMax :: (Monad m, HandlerSite m ~ UniWorX) => Maybe Points -> Maybe Points -> Field m Points
pointsFieldMinMax lower upper = checklower $ checkupper $ fixedPrecMinMaxField lower upper -- NOTE: fixedPrecMinMaxField uses HTML5 input attributes min & max for better browser supprt, but may not be supported by all browsers yet
where
checklower | Just 0 <- lower = checkBool (>= 0) MsgPointsNotPositive
| Just minp <- lower = checkBool (>= minp) $ MsgPointsTooLow minp
| otherwise = id
checkupper | Just maxp <- upper = checkBool (<= maxp) $ MsgPointsTooHigh maxp
| otherwise = id
matriculationField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Text matriculationField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Text
matriculationField = textField -- no restrictions, since not everyone has a matriculation and pupils need special tags here matriculationField = textField -- no restrictions, since not everyone has a matriculation and pupils need special tags here

View File

@ -67,6 +67,13 @@ validateRating ratingSheetType Rating'{ratingPoints=Just rp, ..}
| (Just PassBinary) <- ratingSheetType ^? _grading | (Just PassBinary) <- ratingSheetType ^? _grading
, not (rp == 0 || rp == 1) , not (rp == 0 || rp == 1)
= [RatingBinaryExpected] = [RatingBinaryExpected]
-- QUICKFIX: Vorkorrektur füllt keine Punkte ein und sollte trotzdem akzeptiert werden!
-- Alternative: Fehler fangen und ignorieren, falls der Benutzer Dozent/Assistent/Admin ist
-- validateRating ratingSheetType Rating'{ .. }
-- | has _grading ratingSheetType
-- , is _Nothing ratingPoints
-- , isn't _Nothing ratingTime
-- = [RatingPointsRequired]
validateRating _ _ = [] validateRating _ _ = []
getRating :: SubmissionId -> YesodDB UniWorX (Maybe Rating) getRating :: SubmissionId -> YesodDB UniWorX (Maybe Rating)

View File

@ -508,36 +508,59 @@ fractionalField = Field{..}
fieldView theId name attrs (fmap $ view scientific' -> val) isReq fieldView theId name attrs (fmap $ view scientific' -> val) isReq
= [whamlet| = [whamlet|
$newline never $newline never
<input id=#{theId} name=#{name} *{attrs} type=number :isReq:required value=#{either id (pack . formatScientific Fixed Nothing) val}> <input id=#{theId} name=#{name} *{attrs} type=number step=any :isReq:required value=#{either id (pack . formatScientific Fixed Nothing) val}>
|] |]
fieldParse = parseHelper $ \t -> fieldParse = parseHelper $ \t ->
maybe (Left $ MsgInvalidNumber t) (Right . review scientific') (readMay t :: Maybe Scientific) maybe (Left $ MsgInvalidNumber (t<>"HERE")) (Right . review scientific') (readMay t :: Maybe Scientific)
fixedPrecField :: forall m p. fixedPrecField :: forall m p.
( Monad m ( Monad m
, RenderMessage (HandlerSite m) FormMessage , RenderMessage (HandlerSite m) FormMessage
, HasResolution p , HasResolution p
) => Field m (Fixed p) ) => Field m (Fixed p)
fixedPrecField = Field{..} fixedPrecField = fixedPrecMinMaxField Nothing Nothing
fixedPrecMinMaxField :: forall m p.
( Monad m
, RenderMessage (HandlerSite m) FormMessage
, HasResolution p
) => Maybe (Fixed p) -> Maybe (Fixed p) -> Field m (Fixed p)
fixedPrecMinMaxField lower upper = Field{..}
where where
resolution' :: Integer resolution' :: Integer
resolution' = resolution $ Proxy @p resolution' = resolution $ Proxy @p
step = showFixed True (fromRational $ 1 % resolution' :: Fixed p) showF = showFixed True
step = showFixed True (fromRational $ 1 % resolution' :: Fixed p)
fieldEnctype = UrlEncoded fieldEnctype = UrlEncoded
fieldView theId name attrs val isReq fieldView theId name attrs val isReq
= [whamlet| = [whamlet|
$newline never $newline never
<input id=#{theId} name=#{name} *{attrs} type=number step=#{step} :isReq:required value=#{either id (pack . showFixed True) val}> <input id=#{theId} name=#{name} *{attrs} type=number step=#{step} :hasMin:min="#{showF vMin}" :hasMax:max="#{showF vMax}" :isReq:required value=#{either id (pack . showFixed True) val}>
|] |]
fieldParse = parseHelper $ \t -> do fieldParse = parseHelper $ \t -> do
sci <- maybe (Left $ MsgInvalidNumber t) Right (readMay t :: Maybe Scientific) sci <- maybe (Left $ MsgInvalidNumber t) Right (readMay t :: Maybe Scientific)
return . fromRational $ round (sci * fromIntegral resolution') % resolution' return . fromRational $ round (sci * fromIntegral resolution') % resolution'
(hasMin, vMin) = maybe (False, 0) (True,) lower
(hasMax, vMax) = maybe (False, 0) (True,) upper
rationalField :: (MonadHandler m, RenderMessage (HandlerSite m) FormMessage) => Field m Rational rationalField :: (MonadHandler m, RenderMessage (HandlerSite m) FormMessage) => Field m Rational
rationalField = fractionalField rationalField = fractionalField
-- | Sepcify lower bound via HTML5 min attribute, may not work in older browser, so better use `Handler.Utils.Form.minIntField` (which in turn calls this function)
intMinField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Field m i
intMinField lower = intMinMaxField (Just lower) Nothing
-- | Sepcify lower/upper bounds via HTML5 min attribute, may not work in older browser, so better use `Handler.Utils.Form.minIntField` (which in turn calls this function)
intMinMaxField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => Maybe i -> Maybe i -> Field m i
intMinMaxField lower upper = intF{ fieldView=newView }
where
intF@Field{ fieldView=oldView } = intField
newView theId name attrs val isReq = oldView theId name (newAttrs <> attrs) val isReq
newAttrs = [ (a,tshow v) | (a,Just v) <- [("min", lower),("max", upper)] ]
data SecretJSONFieldException = SecretJSONFieldDecryptFailure data SecretJSONFieldException = SecretJSONFieldDecryptFailure
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Exception SecretJSONFieldException instance Exception SecretJSONFieldException