diff --git a/models/users.model b/models/users.model index 216f9ecb8..57ae421cf 100644 --- a/models/users.model +++ b/models/users.model @@ -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 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) - 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 warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos csvOptions CsvOptions "default='{}'::jsonb" diff --git a/src/Foundation.hs b/src/Foundation.hs index 9294d7d8e..608d784ac 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -43,7 +43,9 @@ import Data.Map (Map, (!?)) import qualified Data.Map as Map import qualified Data.HashSet as HashSet -import Data.List (nubBy, (!!), findIndex) +import Data.List (nubBy, (!!), findIndex, inits) + +import Web.Cookie import Data.Monoid (Any(..)) @@ -3487,7 +3489,7 @@ upsertCampusUser ldapData Creds{..} = do , userWarningDays = userDefaultWarningDays , userShowSex = userDefaultShowSex , userNotificationSettings = def - , userMailLanguages = def + , userLanguages = Nothing , userCsvOptions = def , userTokensIssuedAfter = Nothing , userCreated = now @@ -3607,6 +3609,49 @@ associateUserSchoolsByTerms uid = do , 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 type AuthId UniWorX = UserId @@ -3698,7 +3743,12 @@ instance YesodAuth UniWorX where 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 addMessage Error $ toHtml msg diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index 3b921bca3..307c7acf9 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -58,7 +58,7 @@ emailTestForm :: AForm (HandlerFor UniWorX) (Email, MailContext) emailTestForm = (,) <$> areq emailField (fslI MsgMailTestFormEmail) Nothing <*> ( MailContext - <$> (MailLanguages <$> areq (reorderField appLanguagesOpts) (fslI MsgMailTestFormLanguages) Nothing) + <$> (Languages <$> areq (reorderField appLanguagesOpts) (fslI MsgMailTestFormLanguages) Nothing) <*> (toMailDateTimeFormat <$> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) Nothing <*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs index 7b9f922b1..7e133098e 100644 --- a/src/Handler/Profile.hs +++ b/src/Handler/Profile.hs @@ -30,8 +30,6 @@ import qualified Data.CaseInsensitive as CI import Jobs -import Web.Cookie - data SettingsForm = SettingsForm { stgDisplayName :: UserDisplayName @@ -847,18 +845,12 @@ postLangR :: Handler () postLangR = do ((langRes, _), _) <- runFormPost $ identifyForm FIDLanguage langForm - now <- liftIO getCurrentTime formResult langRes $ \(lang, route) -> do - setCookie $ def - { setCookieName = "_LANG" - , setCookieValue = encodeUtf8 lang - , setCookieExpires = Just $ addUTCTime (400 * avgNominalYear) now - } - setLanguage lang - -- TODO: Write to user - + lang' <- runDB . updateUserLanguage $ Just lang + 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 redirect route diff --git a/src/Handler/Users/Add.hs b/src/Handler/Users/Add.hs index 96fe9e8a7..07858b9f1 100644 --- a/src/Handler/Users/Add.hs +++ b/src/Handler/Users/Add.hs @@ -76,7 +76,7 @@ postAdminUserAddR = do , userWarningDays = userDefaultWarningDays , userShowSex = userDefaultShowSex , userNotificationSettings = def - , userMailLanguages = def + , userLanguages = Nothing , userCsvOptions = def , userTokensIssuedAfter = Nothing , userCreated = now diff --git a/src/Handler/Utils/DateTime.hs b/src/Handler/Utils/DateTime.hs index cf337d3d1..6ff98943e 100644 --- a/src/Handler/Utils/DateTime.hs +++ b/src/Handler/Utils/DateTime.hs @@ -90,7 +90,7 @@ formatTimeW :: (HasLocalTime t) => SelDateTimeFormat -> t -> Widget formatTimeW s t = toWidget =<< formatTime s t 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 = getTimeLocale' <$> languages diff --git a/src/Handler/Utils/Mail.hs b/src/Handler/Utils/Mail.hs index 814657010..bccbd0ce8 100644 --- a/src/Handler/Utils/Mail.hs +++ b/src/Handler/Utils/Mail.hs @@ -47,14 +47,14 @@ userMailT :: ( MonadHandler m ) => UserId -> MailT m a -> m a userMailT uid mAct = do user@User - { userMailLanguages + { userLanguages , userDateTimeFormat , userDateFormat , userTimeFormat } <- liftHandler . runDB $ getJust uid let ctx = MailContext - { mcLanguages = userMailLanguages + { mcLanguages = fromMaybe def userLanguages , mcDateTimeFormat = \case SelFormatDateTime -> userDateTimeFormat SelFormatDate -> userDateFormat diff --git a/src/Mail.hs b/src/Mail.hs index 5a308636c..81457b574 100644 --- a/src/Mail.hs +++ b/src/Mail.hs @@ -10,7 +10,7 @@ module Mail , MailT, defMailT , MailSmtpData(..) , _MailSmtpDataSet - , MailContext(..), MailLanguages(..) + , MailContext(..) , MonadMail(..) , getMailMessageRender, getMailMsgRenderer -- * YesodMail @@ -38,7 +38,7 @@ module Mail import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFieldJSON) -import Model.Types.TH.JSON +import Model.Types.Languages import Network.Mail.Mime hiding (addPart, addAttachment) 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.Hashable () -import GHC.Exts (IsList) - import Control.Monad.Trans.Maybe (MaybeT(..)) 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 - { mcLanguages :: MailLanguages + { mcLanguages :: Languages , mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat } deriving (Eq, Ord, Read, Show, Generic, Typeable) @@ -191,7 +178,7 @@ instance Default MailContext where makeLenses_ ''MailContext class (MonadHandler m, MonadState Mail m) => MonadMail m where - askMailLanguages :: m MailLanguages + askMailLanguages :: m Languages askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat tellMailSmtpData :: MailSmtpData -> m () @@ -214,7 +201,7 @@ getMailMessageRender :: ( MonadMail m , HandlerSite m ~ site , RenderMessage site msg ) => m (msg -> Text) -getMailMessageRender = renderMessage <$> getYesod <*> (mailLanguages <$> askMailLanguages) +getMailMessageRender = renderMessage <$> getYesod <*> (view _Wrapped <$> askMailLanguages) getMailMsgRenderer :: forall site m. ( MonadMail m @@ -559,6 +546,3 @@ setMailSmtpData = do in tell $ mempty { smtpEnvelopeFrom = Last $ Just verp } | otherwise -> tell $ mempty { smtpEnvelopeFrom = Last $ Just from } - - -derivePersistFieldJSON ''MailLanguages diff --git a/src/Model/Migration.hs b/src/Model/Migration.hs index 5f8ab418c..b22bae8fd 100644 --- a/src/Model/Migration.hs +++ b/src/Model/Migration.hs @@ -577,6 +577,14 @@ customMigrations = Map.fromListWith (>>) 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"; + |] + ) ] diff --git a/src/Model/Types.hs b/src/Model/Types.hs index 2dff836fe..3797b0647 100644 --- a/src/Model/Types.hs +++ b/src/Model/Types.hs @@ -14,3 +14,4 @@ import Model.Types.Submission as Types import Model.Types.Misc as Types import Model.Types.School as Types import Model.Types.Allocation as Types +import Model.Types.Languages as Types diff --git a/src/Model/Types/Languages.hs b/src/Model/Types/Languages.hs new file mode 100644 index 000000000..8ed789fb6 --- /dev/null +++ b/src/Model/Types/Languages.hs @@ -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 diff --git a/src/Utils/DateTime.hs b/src/Utils/DateTime.hs index 71d120861..3120f49f5 100644 --- a/src/Utils/DateTime.hs +++ b/src/Utils/DateTime.hs @@ -19,6 +19,8 @@ import Data.Time (TimeLocale(..), NominalDiffTime, nominalDay) import Data.Time.Zones (TZ) import Data.Time.Zones.TH (includeSystemTZ) +import qualified Data.List.NonEmpty as NonEmpty + import Language.Haskell.TH import Language.Haskell.TH.Syntax (Lift(..)) import Instances.TH.Lift () @@ -49,21 +51,18 @@ timeLocaleMap extra@((_, defLocale):_) = do localeMap <- newName "localeMap" 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 = 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, localeStr) = do 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 = lift <=< runIO . getLocale . Just diff --git a/src/Utils/Lang.hs b/src/Utils/Lang.hs index da8eafcda..c0de743ba 100644 --- a/src/Utils/Lang.hs +++ b/src/Utils/Lang.hs @@ -25,7 +25,7 @@ selectLanguage' avL (l:ls) , l' <- NonEmpty.toList avL , langMatches (Text.intercalate "-" lParts') l' ] - = fromMaybe (selectLanguage' avL ls) $ listToMaybe found + = fromMaybe (selectLanguage' avL ls) . listToMaybe $ sortOn (Down . length) found | otherwise = selectLanguage' avL ls langMatches :: Lang -- ^ Needle diff --git a/templates/mail/support.hamlet b/templates/mail/support.hamlet index da915339e..86148b851 100644 --- a/templates/mail/support.hamlet +++ b/templates/mail/support.hamlet @@ -15,7 +15,7 @@ $newline never $of Left Nothing $of Right Nothing