feat(i18n): store language in user account

This commit is contained in:
Gregor Kleen 2019-10-20 15:55:02 +02:00
parent 59b8bb982d
commit f0f94112f4
16 changed files with 125 additions and 62 deletions

View File

@ -28,7 +28,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
dateFormat DateTimeFormat "default='%d.%m.%Y'" -- preferred Date-only display format for user; user-defined dateFormat DateTimeFormat "default='%d.%m.%Y'" -- preferred Date-only display format for user; user-defined
timeFormat DateTimeFormat "default='%R'" -- preferred Time-only display format for user; user-defined timeFormat DateTimeFormat "default='%R'" -- preferred Time-only display format for user; user-defined
downloadFiles Bool default=false -- Should files be opened in browser or downloaded? (users often oblivious that their browser has a setting for this) downloadFiles Bool default=false -- Should files be opened in browser or downloaded? (users often oblivious that their browser has a setting for this)
mailLanguages MailLanguages "default='[]'::jsonb" -- Preferred language for eMail; i18n not yet implemented; user-defined languages Languages Maybe -- Preferred language; user-defined
notificationSettings NotificationSettings -- Bit-array for which events email notifications are requested by user; user-defined notificationSettings NotificationSettings -- Bit-array for which events email notifications are requested by user; user-defined
warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos
csvOptions CsvOptions "default='{}'::jsonb" csvOptions CsvOptions "default='{}'::jsonb"

View File

@ -43,7 +43,9 @@ import Data.Map (Map, (!?))
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.HashSet as HashSet import qualified Data.HashSet as HashSet
import Data.List (nubBy, (!!), findIndex) import Data.List (nubBy, (!!), findIndex, inits)
import Web.Cookie
import Data.Monoid (Any(..)) import Data.Monoid (Any(..))
@ -3487,7 +3489,7 @@ upsertCampusUser ldapData Creds{..} = do
, userWarningDays = userDefaultWarningDays , userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex , userShowSex = userDefaultShowSex
, userNotificationSettings = def , userNotificationSettings = def
, userMailLanguages = def , userLanguages = Nothing
, userCsvOptions = def , userCsvOptions = def
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userCreated = now , userCreated = now
@ -3607,6 +3609,49 @@ associateUserSchoolsByTerms uid = do
, userSchoolIsOptOut = False , userSchoolIsOptOut = False
} }
setLangCookie :: MonadHandler m => Lang -> m ()
setLangCookie lang = do
now <- liftIO getCurrentTime
setCookie $ def
{ setCookieName = "_LANG"
, setCookieValue = encodeUtf8 lang
, setCookieExpires = Just $ addUTCTime (400 * avgNominalYear) now
, setCookiePath = Just "/"
}
updateUserLanguage :: Maybe Lang -> DB (Maybe Lang)
updateUserLanguage (Just lang) = do
unless (lang `elem` appLanguages) $
invalidArgs ["Unsupported language"]
muid <- maybeAuthId
case muid of
Just uid -> do
langs <- languages
update uid [ UserLanguages =. Just (Languages $ lang : nub (filter ((&&) <$> (`elem` appLanguages) <*> (/= lang)) langs)) ]
updateUserLanguage Nothing
Nothing -> do
setLangCookie lang
setLanguage lang
return $ Just lang
updateUserLanguage Nothing = runMaybeT $ do
uid <- MaybeT maybeAuthId
User{..} <- MaybeT $ get uid
setLangs <- nub . filter (`elem` appLanguages) <$> languages
let userLanguages' = nub . filter (`elem` appLanguages) <$> userLanguages ^? _Just . _Wrapped
lang <- case (userLanguages', setLangs) of
(Just (l : _), _)
-> return l
(Nothing, l : _)
-> lift $ l <$ update uid [ UserLanguages =. Just (Languages setLangs) ]
(Just [], l : _)
-> return l
(_, [])
-> mzero
setLangCookie lang
setLanguage lang
return lang
instance YesodAuth UniWorX where instance YesodAuth UniWorX where
type AuthId UniWorX = UserId type AuthId UniWorX = UserId
@ -3698,7 +3743,12 @@ instance YesodAuth UniWorX where
authHttpManager = getsYesod appHttpManager authHttpManager = getsYesod appHttpManager
onLogin = addMessageI Success Auth.NowLoggedIn onLogin = liftHandler $ do
mlang <- runDB $ updateUserLanguage Nothing
app <- getYesod
let mr | Just lang <- mlang = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang
| otherwise = renderMessage app []
addMessage Success . toHtml $ mr Auth.NowLoggedIn
onErrorHtml dest msg = do onErrorHtml dest msg = do
addMessage Error $ toHtml msg addMessage Error $ toHtml msg

View File

@ -58,7 +58,7 @@ emailTestForm :: AForm (HandlerFor UniWorX) (Email, MailContext)
emailTestForm = (,) emailTestForm = (,)
<$> areq emailField (fslI MsgMailTestFormEmail) Nothing <$> areq emailField (fslI MsgMailTestFormEmail) Nothing
<*> ( MailContext <*> ( MailContext
<$> (MailLanguages <$> areq (reorderField appLanguagesOpts) (fslI MsgMailTestFormLanguages) Nothing) <$> (Languages <$> areq (reorderField appLanguagesOpts) (fslI MsgMailTestFormLanguages) Nothing)
<*> (toMailDateTimeFormat <*> (toMailDateTimeFormat
<$> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) Nothing <$> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) Nothing
<*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing <*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing

View File

@ -30,8 +30,6 @@ import qualified Data.CaseInsensitive as CI
import Jobs import Jobs
import Web.Cookie
data SettingsForm = SettingsForm data SettingsForm = SettingsForm
{ stgDisplayName :: UserDisplayName { stgDisplayName :: UserDisplayName
@ -847,18 +845,12 @@ postLangR :: Handler ()
postLangR = do postLangR = do
((langRes, _), _) <- runFormPost $ identifyForm FIDLanguage langForm ((langRes, _), _) <- runFormPost $ identifyForm FIDLanguage langForm
now <- liftIO getCurrentTime
formResult langRes $ \(lang, route) -> do formResult langRes $ \(lang, route) -> do
setCookie $ def lang' <- runDB . updateUserLanguage $ Just lang
{ setCookieName = "_LANG"
, setCookieValue = encodeUtf8 lang
, setCookieExpires = Just $ addUTCTime (400 * avgNominalYear) now
}
setLanguage lang
-- TODO: Write to user
app <- getYesod app <- getYesod
let mr = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang let mr | Just lang'' <- lang' = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang''
| otherwise = renderMessage app []
addMessage Success . toHtml $ mr MsgLanguageChanged addMessage Success . toHtml $ mr MsgLanguageChanged
redirect route redirect route

View File

@ -76,7 +76,7 @@ postAdminUserAddR = do
, userWarningDays = userDefaultWarningDays , userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex , userShowSex = userDefaultShowSex
, userNotificationSettings = def , userNotificationSettings = def
, userMailLanguages = def , userLanguages = Nothing
, userCsvOptions = def , userCsvOptions = def
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userCreated = now , userCreated = now

View File

@ -90,7 +90,7 @@ formatTimeW :: (HasLocalTime t) => SelDateTimeFormat -> t -> Widget
formatTimeW s t = toWidget =<< formatTime s t formatTimeW s t = toWidget =<< formatTime s t
formatTimeMail :: (MonadMail m, HasLocalTime t) => SelDateTimeFormat -> t -> m Text formatTimeMail :: (MonadMail m, HasLocalTime t) => SelDateTimeFormat -> t -> m Text
formatTimeMail sel t = fmap fromString $ Time.formatTime <$> (getTimeLocale' . mailLanguages <$> askMailLanguages) <*> (unDateTimeFormat <$> askMailDateTimeFormat sel) <*> pure (toLocalTime t) formatTimeMail sel t = fmap fromString $ Time.formatTime <$> (getTimeLocale' . view _Wrapped <$> askMailLanguages) <*> (unDateTimeFormat <$> askMailDateTimeFormat sel) <*> pure (toLocalTime t)
getTimeLocale :: MonadHandler m => m TimeLocale getTimeLocale :: MonadHandler m => m TimeLocale
getTimeLocale = getTimeLocale' <$> languages getTimeLocale = getTimeLocale' <$> languages

View File

@ -47,14 +47,14 @@ userMailT :: ( MonadHandler m
) => UserId -> MailT m a -> m a ) => UserId -> MailT m a -> m a
userMailT uid mAct = do userMailT uid mAct = do
user@User user@User
{ userMailLanguages { userLanguages
, userDateTimeFormat , userDateTimeFormat
, userDateFormat , userDateFormat
, userTimeFormat , userTimeFormat
} <- liftHandler . runDB $ getJust uid } <- liftHandler . runDB $ getJust uid
let let
ctx = MailContext ctx = MailContext
{ mcLanguages = userMailLanguages { mcLanguages = fromMaybe def userLanguages
, mcDateTimeFormat = \case , mcDateTimeFormat = \case
SelFormatDateTime -> userDateTimeFormat SelFormatDateTime -> userDateTimeFormat
SelFormatDate -> userDateFormat SelFormatDate -> userDateFormat

View File

@ -10,7 +10,7 @@ module Mail
, MailT, defMailT , MailT, defMailT
, MailSmtpData(..) , MailSmtpData(..)
, _MailSmtpDataSet , _MailSmtpDataSet
, MailContext(..), MailLanguages(..) , MailContext(..)
, MonadMail(..) , MonadMail(..)
, getMailMessageRender, getMailMsgRenderer , getMailMessageRender, getMailMsgRenderer
-- * YesodMail -- * YesodMail
@ -38,7 +38,7 @@ module Mail
import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFieldJSON) import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFieldJSON)
import Model.Types.TH.JSON import Model.Types.Languages
import Network.Mail.Mime hiding (addPart, addAttachment) import Network.Mail.Mime hiding (addPart, addAttachment)
import qualified Network.Mail.Mime as Mime (addPart) import qualified Network.Mail.Mime as Mime (addPart)
@ -109,8 +109,6 @@ import Data.Universe.Instances.Reverse ()
import Data.Universe.Instances.Reverse.JSON () import Data.Universe.Instances.Reverse.JSON ()
import Data.Universe.Instances.Reverse.Hashable () import Data.Universe.Instances.Reverse.Hashable ()
import GHC.Exts (IsList)
import Control.Monad.Trans.Maybe (MaybeT(..)) import Control.Monad.Trans.Maybe (MaybeT(..))
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
@ -160,19 +158,8 @@ _MailSmtpDataSet = to $ \MailSmtpData{..} -> none id
] ]
newtype MailLanguages = MailLanguages { mailLanguages :: [Lang] }
deriving (Eq, Ord, Show, Read, Generic, Typeable)
deriving newtype (FromJSON, ToJSON, IsList)
instance Default MailLanguages where
def = MailLanguages []
instance Hashable MailLanguages
instance NFData MailLanguages
data MailContext = MailContext data MailContext = MailContext
{ mcLanguages :: MailLanguages { mcLanguages :: Languages
, mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat , mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat
} deriving (Eq, Ord, Read, Show, Generic, Typeable) } deriving (Eq, Ord, Read, Show, Generic, Typeable)
@ -191,7 +178,7 @@ instance Default MailContext where
makeLenses_ ''MailContext makeLenses_ ''MailContext
class (MonadHandler m, MonadState Mail m) => MonadMail m where class (MonadHandler m, MonadState Mail m) => MonadMail m where
askMailLanguages :: m MailLanguages askMailLanguages :: m Languages
askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat
tellMailSmtpData :: MailSmtpData -> m () tellMailSmtpData :: MailSmtpData -> m ()
@ -214,7 +201,7 @@ getMailMessageRender :: ( MonadMail m
, HandlerSite m ~ site , HandlerSite m ~ site
, RenderMessage site msg , RenderMessage site msg
) => m (msg -> Text) ) => m (msg -> Text)
getMailMessageRender = renderMessage <$> getYesod <*> (mailLanguages <$> askMailLanguages) getMailMessageRender = renderMessage <$> getYesod <*> (view _Wrapped <$> askMailLanguages)
getMailMsgRenderer :: forall site m. getMailMsgRenderer :: forall site m.
( MonadMail m ( MonadMail m
@ -559,6 +546,3 @@ setMailSmtpData = do
in tell $ mempty { smtpEnvelopeFrom = Last $ Just verp } in tell $ mempty { smtpEnvelopeFrom = Last $ Just verp }
| otherwise | otherwise
-> tell $ mempty { smtpEnvelopeFrom = Last $ Just from } -> tell $ mempty { smtpEnvelopeFrom = Last $ Just from }
derivePersistFieldJSON ''MailLanguages

View File

@ -577,6 +577,14 @@ customMigrations = Map.fromListWith (>>)
ALTER TABLE "allocation" DROP COLUMN "matching_log"; ALTER TABLE "allocation" DROP COLUMN "matching_log";
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|26.0.0|] [version|27.0.0|]
, whenM (tableExists "user") $
[executeQQ|
ALTER TABLE "user" ADD COLUMN "languages" jsonb;
UPDATE TABLE "user" SET "languages" = "mail_languages" where "mail_languages" <> '[]';
ALTER TABLE "user" DROP COLUMN "mail_languages";
|]
)
] ]

View File

@ -14,3 +14,4 @@ import Model.Types.Submission as Types
import Model.Types.Misc as Types import Model.Types.Misc as Types
import Model.Types.School as Types import Model.Types.School as Types
import Model.Types.Allocation as Types import Model.Types.Allocation as Types
import Model.Types.Languages as Types

View File

@ -0,0 +1,25 @@
{-# LANGUAGE GeneralizedNewtypeDeriving, UndecidableInstances #-}
module Model.Types.Languages
( Languages(..)
) where
import ClassyPrelude.Yesod hiding (derivePersistFieldJSON)
import GHC.Exts (IsList)
import Model.Types.TH.JSON
import Control.Lens.TH (makeWrapped)
newtype Languages = Languages [Lang]
deriving (Eq, Ord, Show, Read, Generic, Typeable)
deriving newtype (FromJSON, ToJSON, IsList)
instance Default Languages where
def = Languages []
instance Hashable Languages
instance NFData Languages
derivePersistFieldJSON ''Languages
makeWrapped ''Languages

View File

@ -19,6 +19,8 @@ import Data.Time (TimeLocale(..), NominalDiffTime, nominalDay)
import Data.Time.Zones (TZ) import Data.Time.Zones (TZ)
import Data.Time.Zones.TH (includeSystemTZ) import Data.Time.Zones.TH (includeSystemTZ)
import qualified Data.List.NonEmpty as NonEmpty
import Language.Haskell.TH import Language.Haskell.TH
import Language.Haskell.TH.Syntax (Lift(..)) import Language.Haskell.TH.Syntax (Lift(..))
import Instances.TH.Lift () import Instances.TH.Lift ()
@ -49,21 +51,18 @@ timeLocaleMap extra@((_, defLocale):_) = do
localeMap <- newName "localeMap" localeMap <- newName "localeMap"
let let
localeMap' = funD localeMap $ map matchLang extra ++ [reduceLangList, defaultLang] langs = NonEmpty.fromList $ map fst extra
localeMap' = funD localeMap $ map matchLang extra ++ [defaultLang]
defaultLang :: ClauseQ defaultLang :: ClauseQ
defaultLang = defaultLang =
clause [listP []] (normalB $ localeExp defLocale) [] clause [listP []] (normalB $ localeExp defLocale) []
reduceLangList :: ClauseQ
reduceLangList = do
ls <- newName "ls"
clause [[p|(_ : $(varP ls))|]] (normalB [e|$(varE localeMap) $(varE ls)|]) []
matchLang :: (Lang, String) -> ClauseQ matchLang :: (Lang, String) -> ClauseQ
matchLang (lang, localeStr) = do matchLang (lang, localeStr) = do
lang' <- newName "lang" lang' <- newName "lang"
clause [[p|($(varP lang') : _)|]] (guardedB [(,) <$> normalG [e|$(varE lang') == lang|] <*> localeExp localeStr]) [] clause [varP lang'] (guardedB [(,) <$> normalG [e|selectLanguage' langs $(varE lang') == lang|] <*> localeExp localeStr]) []
localeExp :: String -> ExpQ localeExp :: String -> ExpQ
localeExp = lift <=< runIO . getLocale . Just localeExp = lift <=< runIO . getLocale . Just

View File

@ -25,7 +25,7 @@ selectLanguage' avL (l:ls)
, l' <- NonEmpty.toList avL , l' <- NonEmpty.toList avL
, langMatches (Text.intercalate "-" lParts') l' , langMatches (Text.intercalate "-" lParts') l'
] ]
= fromMaybe (selectLanguage' avL ls) $ listToMaybe found = fromMaybe (selectLanguage' avL ls) . listToMaybe $ sortOn (Down . length) found
| otherwise = selectLanguage' avL ls | otherwise = selectLanguage' avL ls
langMatches :: Lang -- ^ Needle langMatches :: Lang -- ^ Needle

View File

@ -15,7 +15,7 @@ $newline never
$of Left Nothing $of Left Nothing
$of Right Nothing $of Right Nothing
<dt>Ungültige UserId erhalten! <dt>Ungültige UserId erhalten!
$of Right (Just (Entity _ User{userDisplayName, userSurname, userIdent, userEmail, userMatrikelnummer, userMailLanguages})) $of Right (Just (Entity _ User{userDisplayName, userSurname, userIdent, userEmail, userMatrikelnummer, userLanguages}))
<dt>Name <dt>Name
<dd>^{const (const (nameHtml userDisplayName userSurname))} <dd>^{const (const (nameHtml userDisplayName userSurname))}
<dt>Identifikation <dt>Identifikation
@ -25,10 +25,11 @@ $newline never
$maybe matrnr <- userMatrikelnummer $maybe matrnr <- userMatrikelnummer
<dt>Matrikelnummer <dt>Matrikelnummer
<dd>#{matrnr} <dd>#{matrnr}
$if not (null (mailLanguages userMailLanguages)) $maybe langs <- fmap (view _Wrapped) userLanguages
<dt>Präferierte E-Mail Sprachen $if not (null langs)
$forall lang <- mailLanguages userMailLanguages <dt>Präferierte E-Mail Sprachen
<dd>#{lang} $forall lang <- langs
<dd>#{lang}
<dt>Zeit <dt>Zeit
<dd>#{rtime} <dd>#{rtime}
$maybe referer <- jReferer $maybe referer <- jReferer

View File

@ -108,7 +108,7 @@ fillDb = do
, userTimeFormat = userDefaultTimeFormat , userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays , userWarningDays = userDefaultWarningDays
, userMailLanguages = MailLanguages ["en"] , userLanguages = Just $ Languages ["en"]
, userNotificationSettings = def , userNotificationSettings = def
, userCreated = now , userCreated = now
, userLastLdapSynchronisation = Nothing , userLastLdapSynchronisation = Nothing
@ -136,7 +136,7 @@ fillDb = do
, userTimeFormat = userDefaultTimeFormat , userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays , userWarningDays = userDefaultWarningDays
, userMailLanguages = MailLanguages ["de"] , userLanguages = Nothing
, userNotificationSettings = def , userNotificationSettings = def
, userCreated = now , userCreated = now
, userLastLdapSynchronisation = Nothing , userLastLdapSynchronisation = Nothing
@ -164,7 +164,7 @@ fillDb = do
, userTimeFormat = userDefaultTimeFormat , userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays , userWarningDays = userDefaultWarningDays
, userMailLanguages = MailLanguages ["de"] , userLanguages = Nothing
, userNotificationSettings = def , userNotificationSettings = def
, userCreated = now , userCreated = now
, userLastLdapSynchronisation = Nothing , userLastLdapSynchronisation = Nothing
@ -192,7 +192,7 @@ fillDb = do
, userTimeFormat = userDefaultTimeFormat , userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays , userWarningDays = userDefaultWarningDays
, userMailLanguages = MailLanguages ["de"] , userLanguages = Just $ Languages ["de"]
, userNotificationSettings = def , userNotificationSettings = def
, userCreated = now , userCreated = now
, userLastLdapSynchronisation = Nothing , userLastLdapSynchronisation = Nothing
@ -220,7 +220,7 @@ fillDb = do
, userTimeFormat = userDefaultTimeFormat , userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays , userWarningDays = userDefaultWarningDays
, userMailLanguages = MailLanguages ["de"] , userLanguages = Just $ Languages ["sn"]
, userNotificationSettings = def , userNotificationSettings = def
, userCreated = now , userCreated = now
, userLastLdapSynchronisation = Nothing , userLastLdapSynchronisation = Nothing
@ -248,7 +248,7 @@ fillDb = do
, userTimeFormat = userDefaultTimeFormat , userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays , userWarningDays = userDefaultWarningDays
, userMailLanguages = MailLanguages ["de"] , userLanguages = Nothing
, userNotificationSettings = def , userNotificationSettings = def
, userCreated = now , userCreated = now
, userLastLdapSynchronisation = Nothing , userLastLdapSynchronisation = Nothing

View File

@ -106,7 +106,10 @@ instance Arbitrary User where
userDownloadFiles <- arbitrary userDownloadFiles <- arbitrary
userWarningDays <- arbitrary userWarningDays <- arbitrary
userMailLanguages <- fmap MailLanguages $ sublistOf =<< shuffle (toList appLanguages) userLanguages <- choose
[ pure Nothing
, fmap (Just . Languages) $ sublistOf =<< shuffle (toList appLanguages)
]
userNotificationSettings <- arbitrary userNotificationSettings <- arbitrary
userCsvOptions <- arbitrary userCsvOptions <- arbitrary
userShowSex <- arbitrary userShowSex <- arbitrary