From 352bdba1a4377bbd3ecf76b72c6aee6fdd861316 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 18 Oct 2019 20:12:34 +0200 Subject: [PATCH 001/126] feat(i18n): basic language switching --- frontend/src/utils/inputs/checkbox.js | 2 +- frontend/src/utils/inputs/checkbox.scss | 19 +++++---- messages/uniworx/de.msg | 7 +++- messages/uniworx/en.msg | 2 + package.yaml | 1 + routes | 1 + src/Foundation.hs | 42 +++++++++++++++---- src/Handler/Profile.hs | 26 ++++++++++++ src/Utils/Form.hs | 1 + src/Utils/Lang.hs | 11 ++--- templates/widgets/navbar/navbar.hamlet | 12 +++++- templates/widgets/navbar/navbar.lucius | 56 ++++++++++++++++++++----- 12 files changed, 145 insertions(+), 35 deletions(-) create mode 100644 messages/uniworx/en.msg diff --git a/frontend/src/utils/inputs/checkbox.js b/frontend/src/utils/inputs/checkbox.js index e93c88856..6f52ea3ae 100644 --- a/frontend/src/utils/inputs/checkbox.js +++ b/frontend/src/utils/inputs/checkbox.js @@ -5,7 +5,7 @@ var CHECKBOX_CLASS = 'checkbox'; var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized'; @Utility({ - selector: 'input[type="checkbox"]', + selector: 'input[type="checkbox"]:not([uw-no-checkbox])', }) export class Checkbox { diff --git a/frontend/src/utils/inputs/checkbox.scss b/frontend/src/utils/inputs/checkbox.scss index 5dc0c9995..817aa5f3a 100644 --- a/frontend/src/utils/inputs/checkbox.scss +++ b/frontend/src/utils/inputs/checkbox.scss @@ -1,19 +1,20 @@ /* CUSTOM CHECKBOXES */ /* Completely replaces legacy checkbox */ +.checkbox [type='checkbox'], #lang-checkbox { + position: fixed; + top: -1px; + left: -1px; + width: 1px; + height: 1px; + overflow: hidden; + display: none; +} + .checkbox { position: relative; display: inline-block; - [type='checkbox'] { - position: fixed; - top: -1px; - left: -1px; - width: 1px; - height: 1px; - overflow: hidden; - } - label { display: block; height: 20px; diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg index aef920e2b..825036b04 100644 --- a/messages/uniworx/de.msg +++ b/messages/uniworx/de.msg @@ -778,6 +778,8 @@ MailTestDateTime: Test der Datumsformattierung: German: Deutsch GermanGermany: Deutsch (Deutschland) +English: English +EnglishEurope: English (Europe) MailSubjectSubmissionRated csh@CourseShorthand: Ihre #{csh}-Abgabe wurde korrigiert MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{courseName} (#{termDesc}) wurde korrigiert. @@ -1979,4 +1981,7 @@ ShortSexFemale: w ShortSexNotApplicable: k.A. ShowSex: Geschlechter anderer Nutzer anzeigen -ShowSexTip: Sollen in Kursteilnehmer-Tabellen u.Ä. die Geschlechter der Nutzer angezeigt werden? \ No newline at end of file +ShowSexTip: Sollen in Kursteilnehmer-Tabellen u.Ä. die Geschlechter der Nutzer angezeigt werden? + +MenuLanguage: Sprache +LanguageChanged: Sprache erfolgreich geändert \ No newline at end of file diff --git a/messages/uniworx/en.msg b/messages/uniworx/en.msg new file mode 100644 index 000000000..c6e1dc4ad --- /dev/null +++ b/messages/uniworx/en.msg @@ -0,0 +1,2 @@ +MenuLanguage: Language +LanguageChanged: Language changed successfully \ No newline at end of file diff --git a/package.yaml b/package.yaml index 45137e732..e002f165f 100644 --- a/package.yaml +++ b/package.yaml @@ -140,6 +140,7 @@ dependencies: - retry - generic-lens - array + - cookie other-extensions: - GeneralizedNewtypeDeriving diff --git a/routes b/routes index 582834521..b64e379b5 100644 --- a/routes +++ b/routes @@ -73,6 +73,7 @@ /user/authpreds AuthPredsR GET POST !free /user/set-display-email SetDisplayEmailR GET POST !free /user/csv-options CsvOptionsR GET POST !free +/user/lang LangR POST !free /exam-office ExamOfficeR !exam-office: / EOExamsR GET diff --git a/src/Foundation.hs b/src/Foundation.hs index 4b54a9fd1..9294d7d8e 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -72,6 +72,7 @@ import Utils.SystemMessage import Text.Shakespeare.Text (st) import Yesod.Form.I18n.German +import Yesod.Form.I18n.English import qualified Yesod.Auth.Message as Auth import qualified Data.Conduit.List as C @@ -257,11 +258,6 @@ mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de" mkMessageVariant "UniWorX" "Button" "messages/button" "de" mkMessageVariant "UniWorX" "Frontend" "messages/frontend" "de" --- This instance is required to use forms. You can modify renderMessage to --- achieve customized and internationalized form validation messages. -instance RenderMessage UniWorX FormMessage where - renderMessage _ _ = germanFormMessage -- TODO - instance RenderMessage UniWorX TermIdentifier where renderMessage foundation ls TermIdentifier{..} = case season of Summer -> renderMessage' $ MsgSummerTerm year @@ -304,6 +300,8 @@ instance RenderMessage UniWorX MsgLanguage where renderMessage foundation ls (MsgLanguage lang@(Text.splitOn "-" -> lang')) | ["de", "DE"] <- lang' = mr MsgGermanGermany | ("de" : _) <- lang' = mr MsgGerman + | ["en", "EU"] <- lang' = mr MsgEnglishEurope + | ("en" : _) <- lang' = mr MsgEnglish | otherwise = lang where mr = renderMessage foundation ls @@ -511,13 +509,13 @@ instance Button UniWorX ButtonSubmit where getTimeLocale' :: [Lang] -> TimeLocale -getTimeLocale' = $(timeLocaleMap [("de", "de_DE.utf8")]) +getTimeLocale' = $(timeLocaleMap [("de", "de_DE.utf8"), ("en", "en_IE.utf8")]) appTZ :: TZ appTZ = $(includeSystemTZ "Europe/Berlin") appLanguages :: NonEmpty Lang -appLanguages = "de-DE" :| [] +appLanguages = "de" :| ["en"] appLanguagesOpts :: ( MonadHandler m , HandlerSite m ~ UniWorX @@ -533,6 +531,13 @@ appLanguagesOpts = do langOptions = map mkOption $ toList appLanguages return $ mkOptionList langOptions +-- This instance is required to use forms. You can modify renderMessage to +-- achieve customized and internationalized form validation messages. +instance RenderMessage UniWorX FormMessage where + renderMessage _ ls = case lang of + ("en" : _) -> englishFormMessage + _other -> germanFormMessage + where lang = Text.splitOn "-" $ selectLanguage' appLanguages ls instance RenderMessage UniWorX WeekDay where renderMessage _ ls wDay = pack $ map fst (wDays $ getTimeLocale' ls) !! fromEnum wDay @@ -1683,6 +1688,17 @@ instance Yesod UniWorX where makeLogger = readTVarIO . snd . appLogger +langForm :: Form (Lang, Route UniWorX) +langForm csrf = do + lang <- selectLanguage appLanguages + route <- getCurrentRoute + (urlRes, urlView) <- mreq hiddenField ("" & addName ("referer" :: Text)) route + (langBoxRes, langBoxView) <- mreq + (selectField appLanguagesOpts) + ("" & addAttr "multiple" "multiple" & addAttr "size" (tshow . min 10 $ length appLanguages) & addAutosubmit & addName ("lang" :: Text)) + (Just lang) + return ((,) <$> langBoxRes <*> urlRes, toWidget csrf <> fvInput urlView <> fvInput langBoxView) + siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX) => msg -> Widget -> Handler Html siteLayoutMsg msg widget = do mr <- getMessageRender @@ -1785,6 +1801,13 @@ siteLayout' headingOverride widget = do \authTag -> addMessageWidget Info $ msgModal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute])) getMessages + (langFormView, langFormEnctype) <- generateFormPost $ identifyForm FIDLanguage langForm + let langFormView' = wrapForm langFormView def + { formAction = Just $ SomeRoute LangR + , formSubmit = FormAutoSubmit + , formEncoding = langFormEnctype + } + let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority highlight = let crumbs = mcons mcurrentRoute $ fst <$> reverse parents navItems = map (view _2) favourites ++ map (urlRoute . menuItemRoute . view _1) menuTypes @@ -3681,7 +3704,10 @@ instance YesodAuth UniWorX where addMessage Error $ toHtml msg redirect dest - renderAuthMessage _ _ = Auth.germanMessage -- TODO + renderAuthMessage _ ls = case lang of + ("en" : _) -> Auth.englishMessage + _other -> Auth.germanMessage + where lang = Text.splitOn "-" $ selectLanguage' appLanguages ls instance YesodAuthPersist UniWorX diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs index fbb3093cf..7b9f922b1 100644 --- a/src/Handler/Profile.hs +++ b/src/Handler/Profile.hs @@ -5,6 +5,7 @@ module Handler.Profile , getUserNotificationR, postUserNotificationR , getSetDisplayEmailR, postSetDisplayEmailR , getCsvOptionsR, postCsvOptionsR + , postLangR ) where import Import @@ -22,11 +23,15 @@ import qualified Data.Set as Set import qualified Database.Esqueleto as E import qualified Database.Esqueleto.Utils as E -- import Database.Esqueleto ((^.)) +import qualified Data.Text as Text +import Data.List (inits) import qualified Data.CaseInsensitive as CI import Jobs +import Web.Cookie + data SettingsForm = SettingsForm { stgDisplayName :: UserDisplayName @@ -837,3 +842,24 @@ postCsvOptionsR = do , formEncoding = optionsEnctype , formAttrs = [ asyncSubmitAttr | isModal ] } + +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 + + app <- getYesod + let mr = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang + addMessage Success . toHtml $ mr MsgLanguageChanged + redirect route + + invalidArgs ["Language form required"] diff --git a/src/Utils/Form.hs b/src/Utils/Form.hs index eb212bc1a..52024081d 100644 --- a/src/Utils/Form.hs +++ b/src/Utils/Form.hs @@ -228,6 +228,7 @@ data FormIdentifier | FIDAssignSubmissions | FIDUserAuthMode | FIDAllUsersAction + | FIDLanguage deriving (Eq, Ord, Read, Show) instance PathPiece FormIdentifier where diff --git a/src/Utils/Lang.hs b/src/Utils/Lang.hs index a8ebe1878..da8eafcda 100644 --- a/src/Utils/Lang.hs +++ b/src/Utils/Lang.hs @@ -6,6 +6,7 @@ import qualified Data.List.NonEmpty as NonEmpty import Data.List.NonEmpty (NonEmpty(..)) import qualified Data.Text as Text +import qualified Data.List as List selectLanguage :: MonadHandler m @@ -18,13 +19,13 @@ selectLanguage' :: NonEmpty Lang -- ^ Available translations, first is default -> Lang selectLanguage' (defL :| _) [] = defL selectLanguage' avL (l:ls) - | not $ null l - , Just l' <- find (== l) (NonEmpty.toList avL) - = l' | not $ null l , Just lParts <- NonEmpty.nonEmpty $ Text.splitOn "-" l - , found <- find ((NonEmpty.toList lParts `isPrefixOf`) . Text.splitOn "-") avL - = flip fromMaybe found $ selectLanguage' avL $ Text.intercalate "-" (NonEmpty.tail lParts) : ls + , found <- [ l' | lParts' <- reverse . List.inits $ NonEmpty.toList lParts + , l' <- NonEmpty.toList avL + , langMatches (Text.intercalate "-" lParts') l' + ] + = fromMaybe (selectLanguage' avL ls) $ listToMaybe found | otherwise = selectLanguage' avL ls langMatches :: Lang -- ^ Needle diff --git a/templates/widgets/navbar/navbar.hamlet b/templates/widgets/navbar/navbar.hamlet index 966223643..77e1184a9 100644 --- a/templates/widgets/navbar/navbar.hamlet +++ b/templates/widgets/navbar/navbar.hamlet @@ -7,7 +7,8 @@ $newline never $# manually add favorites to navbar for small screens
  • - +
    +
    _{MsgNavigationFavourites} $forall (menuItem@MenuItem{menuItemType, menuItemRoute, menuItemModal}, menuIdent, _) <- menuTypes @@ -36,3 +37,12 @@ $newline never $else ^{navbarItem (menuItem, menuIdent)} $of _ +
  • + +
    + ^{langFormView'} +
    +
    + +
    _{MsgMenuLanguage} diff --git a/templates/widgets/navbar/navbar.lucius b/templates/widgets/navbar/navbar.lucius index c3885f975..888d8d5cb 100644 --- a/templates/widgets/navbar/navbar.lucius +++ b/templates/widgets/navbar/navbar.lucius @@ -68,14 +68,7 @@ color: var(--color-lightwhite); transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1); overflow: hidden; - - &:hover { - color: var(--color-lightwhite); - - .navbar__link-icon { - opacity: 1; - } - } + cursor: pointer; } .navbar__link-icon { @@ -88,6 +81,7 @@ transition: opacity .2s ease; padding: 2px 4px; text-transform: uppercase; + font-weight: 600; } @media (min-width: 769px) { @@ -146,7 +140,9 @@ .navbar__list-item { position: relative; transition: background-color .1s ease; - + &:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper { + margin-left: 12px; + } &:not(.navbar__list-item--favorite) + .navbar__list-item { margin-left: 12px; } @@ -160,6 +156,9 @@ &:not(.navbar__list-item--favorite) + .navbar__list-item { margin-left: 0; } + &:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper { + margin-left: 0; + } } } @@ -219,9 +218,13 @@ color: var(--color-dark); } -.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper { +.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper, #lang-checkbox:checked ~ * .navbar__link-wrapper { background-color: var(--color-dark); color: var(--color-lightwhite); + + .navbar__link-icon { + opacity: 1; + } } /* sticky state */ @@ -267,3 +270,36 @@ height: var(--header-height-collapsed); } } + + +#lang-dropdown { + display: none; + + position: fixed; + top: var(--header-height); + right: 0; + min-width: 200px; + z-index: 10; + background-color: white; + border-radius: 2px; + box-shadow: 0 0 10px rgba(0,0,0,0.3); + + select { + display: block; + } + + button { + display: block; + width: 100%; + } +} + +#lang-checkbox:checked ~ #lang-dropdown { + display: block; +} + +@media (max-width: 768px) { + #lang-dropdown { + top: var(--header-height-collapsed); + } +} From 717cf1ddfd05d63151fcc9df85b782d48a655f41 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sun, 20 Oct 2019 10:40:43 +0200 Subject: [PATCH 002/126] style(i18n): prettier language select --- frontend/src/utils/inputs/inputs.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/inputs/inputs.scss b/frontend/src/utils/inputs/inputs.scss index 6f12d81b8..f19a17bda 100644 --- a/frontend/src/utils/inputs/inputs.scss +++ b/frontend/src/utils/inputs/inputs.scss @@ -150,7 +150,6 @@ textarea { padding: 4px 13px; font-size: 1rem; font-family: var(--font-base); - -webkit-appearance: none; appearance: none; border: 1px solid #dbdbdb; border-radius: 2px; @@ -184,8 +183,8 @@ textarea { /* OPTIONS */ -select { - -webkit-appearance: menulist; +select[size = "1"], select:not([size]) { + appearance: menulist; } select, From 59b8bb982d33b14a01133819c375884a8a7f7ce9 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sun, 20 Oct 2019 13:54:15 +0200 Subject: [PATCH 003/126] fix(mail): use only RFC822-timezones --- src/Mail.hs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Mail.hs b/src/Mail.hs index 53b0e6611..5a308636c 100644 --- a/src/Mail.hs +++ b/src/Mail.hs @@ -89,7 +89,7 @@ import qualified Data.Binary as Binary import "network-bsd" Network.BSD (getHostName) import Data.Time.Zones (TZ, utcTZ, utcToLocalTimeTZ, timeZoneForUTCTime) -import Data.Time.LocalTime (ZonedTime(..)) +import Data.Time.LocalTime (ZonedTime(..), TimeZone(..)) import Data.Time.Format (rfc822DateFormat) import Network.HaskellNet.SMTP (SMTPConnection) @@ -515,8 +515,24 @@ setDateCurrent = setDate =<< liftIO getCurrentTime setDate :: (MonadHandler m, YesodMail (HandlerSite m)) => UTCTime -> MailT m () setDate time = do tz <- mailDateTZ - let timeStr = formatTime defaultTimeLocale rfc822DateFormat $ ZonedTime (utcToLocalTimeTZ tz time) (timeZoneForUTCTime tz time) + let timeStr = formatTime defaultTimeLocale rfc822DateFormat $ ZonedTime (utcToLocalTimeTZ tz time) (rfc822zone $ timeZoneForUTCTime tz time) replaceMailHeader "Date" . Just $ pack timeStr + where + rfc822zone tz' + | tz' `elem` rfc822zones = tz' + | otherwise = tz' { timeZoneName = "" } + rfc822zones = + [ TimeZone 0 False "UT" + , TimeZone 0 False "GMT" + , TimeZone (-5 * 60) False "EST" + , TimeZone (-4 * 60) True "EDT" + , TimeZone (-6 * 60) False "CST" + , TimeZone (-5 * 60) True "CDT" + , TimeZone (-7 * 60) False "MST" + , TimeZone (-6 * 60) True "MDT" + , TimeZone (-8 * 60) False "PST" + , TimeZone (-7 * 60) True "PDT" + ] setMailSmtpData :: (MonadHandler m, YesodMail (HandlerSite m), MonadThrow m) => MailT m () From f0f94112f4d8b9af96c2048a046e43ce7ae7351b Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sun, 20 Oct 2019 15:55:02 +0200 Subject: [PATCH 004/126] feat(i18n): store language in user account --- models/users.model | 2 +- src/Foundation.hs | 56 +++++++++++++++++++++++++++++++++-- src/Handler/Admin.hs | 2 +- src/Handler/Profile.hs | 16 +++------- src/Handler/Users/Add.hs | 2 +- src/Handler/Utils/DateTime.hs | 2 +- src/Handler/Utils/Mail.hs | 4 +-- src/Mail.hs | 26 ++++------------ src/Model/Migration.hs | 8 +++++ src/Model/Types.hs | 1 + src/Model/Types/Languages.hs | 25 ++++++++++++++++ src/Utils/DateTime.hs | 13 ++++---- src/Utils/Lang.hs | 2 +- templates/mail/support.hamlet | 11 +++---- test/Database.hs | 12 ++++---- test/ModelSpec.hs | 5 +++- 16 files changed, 125 insertions(+), 62 deletions(-) create mode 100644 src/Model/Types/Languages.hs 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
    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}))
    Name
    ^{const (const (nameHtml userDisplayName userSurname))}
    Identifikation @@ -25,10 +25,11 @@ $newline never $maybe matrnr <- userMatrikelnummer
    Matrikelnummer
    #{matrnr} - $if not (null (mailLanguages userMailLanguages)) -
    Präferierte E-Mail Sprachen - $forall lang <- mailLanguages userMailLanguages -
    #{lang} + $maybe langs <- fmap (view _Wrapped) userLanguages + $if not (null langs) +
    Präferierte E-Mail Sprachen + $forall lang <- langs +
    #{lang}
    Zeit
    #{rtime} $maybe referer <- jReferer diff --git a/test/Database.hs b/test/Database.hs index a3ca55a07..274bfa77f 100755 --- a/test/Database.hs +++ b/test/Database.hs @@ -108,7 +108,7 @@ fillDb = do , userTimeFormat = userDefaultTimeFormat , userDownloadFiles = userDefaultDownloadFiles , userWarningDays = userDefaultWarningDays - , userMailLanguages = MailLanguages ["en"] + , userLanguages = Just $ Languages ["en"] , userNotificationSettings = def , userCreated = now , userLastLdapSynchronisation = Nothing @@ -136,7 +136,7 @@ fillDb = do , userTimeFormat = userDefaultTimeFormat , userDownloadFiles = userDefaultDownloadFiles , userWarningDays = userDefaultWarningDays - , userMailLanguages = MailLanguages ["de"] + , userLanguages = Nothing , userNotificationSettings = def , userCreated = now , userLastLdapSynchronisation = Nothing @@ -164,7 +164,7 @@ fillDb = do , userTimeFormat = userDefaultTimeFormat , userDownloadFiles = userDefaultDownloadFiles , userWarningDays = userDefaultWarningDays - , userMailLanguages = MailLanguages ["de"] + , userLanguages = Nothing , userNotificationSettings = def , userCreated = now , userLastLdapSynchronisation = Nothing @@ -192,7 +192,7 @@ fillDb = do , userTimeFormat = userDefaultTimeFormat , userDownloadFiles = userDefaultDownloadFiles , userWarningDays = userDefaultWarningDays - , userMailLanguages = MailLanguages ["de"] + , userLanguages = Just $ Languages ["de"] , userNotificationSettings = def , userCreated = now , userLastLdapSynchronisation = Nothing @@ -220,7 +220,7 @@ fillDb = do , userTimeFormat = userDefaultTimeFormat , userDownloadFiles = userDefaultDownloadFiles , userWarningDays = userDefaultWarningDays - , userMailLanguages = MailLanguages ["de"] + , userLanguages = Just $ Languages ["sn"] , userNotificationSettings = def , userCreated = now , userLastLdapSynchronisation = Nothing @@ -248,7 +248,7 @@ fillDb = do , userTimeFormat = userDefaultTimeFormat , userDownloadFiles = userDefaultDownloadFiles , userWarningDays = userDefaultWarningDays - , userMailLanguages = MailLanguages ["de"] + , userLanguages = Nothing , userNotificationSettings = def , userCreated = now , userLastLdapSynchronisation = Nothing diff --git a/test/ModelSpec.hs b/test/ModelSpec.hs index cdd916f3b..936eccbbd 100644 --- a/test/ModelSpec.hs +++ b/test/ModelSpec.hs @@ -106,7 +106,10 @@ instance Arbitrary User where userDownloadFiles <- arbitrary userWarningDays <- arbitrary - userMailLanguages <- fmap MailLanguages $ sublistOf =<< shuffle (toList appLanguages) + userLanguages <- choose + [ pure Nothing + , fmap (Just . Languages) $ sublistOf =<< shuffle (toList appLanguages) + ] userNotificationSettings <- arbitrary userCsvOptions <- arbitrary userShowSex <- arbitrary From 97a29ec68cb64e4264afc1d6a95e73e608b33748 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sun, 20 Oct 2019 16:48:14 +0200 Subject: [PATCH 005/126] feat(i18n): close language select on click anywhere --- frontend/src/utils/navbar/navbar.js | 48 +++++++++++++++++++ .../src/utils/navbar/navbar.scss | 0 frontend/src/utils/utils.js | 2 + templates/widgets/navbar/navbar.hamlet | 2 +- 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 frontend/src/utils/navbar/navbar.js rename templates/widgets/navbar/navbar.lucius => frontend/src/utils/navbar/navbar.scss (100%) diff --git a/frontend/src/utils/navbar/navbar.js b/frontend/src/utils/navbar/navbar.js new file mode 100644 index 000000000..95af958f4 --- /dev/null +++ b/frontend/src/utils/navbar/navbar.js @@ -0,0 +1,48 @@ +import { Utility } from '../../core/utility'; +import './navbar.scss'; + + +export const LANGUAGE_SELECT_UTIL_SELECTOR = '[uw-language-select]'; +const LANGUAGE_SELECT_INITIALIZED_CLASS = 'language-select--initialized'; + + +@Utility({ + selector: LANGUAGE_SELECT_UTIL_SELECTOR, +}) +export class LanguageSelectUtil { + _element; + checkbox; + + constructor(element) { + if (!element) { + throw new Error('Language Select utility needs to be passed an element!'); + } + + if (element.classList.contains(LANGUAGE_SELECT_INITIALIZED_CLASS)) { + return false; + } + + this._element = element; + this.checkbox = element.querySelector('#lang-checkbox'); + + window.addEventListener('click', event => this.close(event)); + + element.classList.add(LANGUAGE_SELECT_INITIALIZED_CLASS); + } + + close(event) { + if (!this._element.contains(event.target) && window.document.contains(event.target)) { + this.checkbox.checked = false; + } + } + + destroy() { + // TODO + } + +} + + +export const NavbarUtils = [ + LanguageSelectUtil, +]; diff --git a/templates/widgets/navbar/navbar.lucius b/frontend/src/utils/navbar/navbar.scss similarity index 100% rename from templates/widgets/navbar/navbar.lucius rename to frontend/src/utils/navbar/navbar.scss diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 06e095af6..b539edb44 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -10,6 +10,7 @@ import { MassInput } from './mass-input/mass-input'; import { Modal } from './modal/modal'; import { Tooltip } from './tooltips/tooltips'; import { CourseTeaser } from './course-teaser/course-teaser'; +import { NavbarUtils } from './navbar/navbar'; export const Utils = [ Alerts, @@ -25,4 +26,5 @@ export const Utils = [ ShowHide, Tooltip, CourseTeaser, + ...NavbarUtils, ]; diff --git a/templates/widgets/navbar/navbar.hamlet b/templates/widgets/navbar/navbar.hamlet index 77e1184a9..0f6d227fb 100644 --- a/templates/widgets/navbar/navbar.hamlet +++ b/templates/widgets/navbar/navbar.hamlet @@ -37,7 +37,7 @@ $newline never $else ^{navbarItem (menuItem, menuIdent)} $of _ -
  • +
  • ^{langFormView'} From 205d7688bf821b1c899b9f3b4d3759a9d89de3cb Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Mon, 21 Oct 2019 14:02:09 +0200 Subject: [PATCH 006/126] fix(i18n): custom language inference --- .../frontend/{de.msg => de-de-formal.msg} | 2 +- messages/uniworx/{de.msg => de-de-formal.msg} | 0 src/Foundation.hs | 39 ++++++------ src/Utils/Lang.hs | 59 +++++++++++++++---- 4 files changed, 69 insertions(+), 31 deletions(-) rename messages/frontend/{de.msg => de-de-formal.msg} (50%) rename messages/uniworx/{de.msg => de-de-formal.msg} (100%) diff --git a/messages/frontend/de.msg b/messages/frontend/de-de-formal.msg similarity index 50% rename from messages/frontend/de.msg rename to messages/frontend/de-de-formal.msg index f01c31640..a17c4540c 100644 --- a/messages/frontend/de.msg +++ b/messages/frontend/de-de-formal.msg @@ -1,4 +1,4 @@ FilesSelected: Dateien ausgewählt SelectFile: Datei auswählen SelectFiles: Datei(en) auswählen -AsyncFormFailure: Da ist etwas schief gelaufen, das tut uns Leid. Falls das erneut passiert schicke uns gerne eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben. Vielen Dank für deine Hilfe! \ No newline at end of file +AsyncFormFailure: Da ist etwas schief gelaufen, das tut uns Leid. Falls das erneut passiert schicken Sie uns bitte eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben. Vielen Dank für Ihre Hilfe! \ No newline at end of file diff --git a/messages/uniworx/de.msg b/messages/uniworx/de-de-formal.msg similarity index 100% rename from messages/uniworx/de.msg rename to messages/uniworx/de-de-formal.msg diff --git a/src/Foundation.hs b/src/Foundation.hs index 608d784ac..9f973bfd5 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -253,12 +253,12 @@ maybeToMessage _ Nothing _ = mempty maybeToMessage before (Just x) after = before <> (toMessage x) <> after -- Messages creates type UniWorXMessage and RenderMessage UniWorX instance -mkMessage "UniWorX" "messages/uniworx" "de" +mkMessage "UniWorX" "messages/uniworx" "de-de-formal" mkMessageVariant "UniWorX" "Campus" "messages/campus" "de" mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de" mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de" mkMessageVariant "UniWorX" "Button" "messages/button" "de" -mkMessageVariant "UniWorX" "Frontend" "messages/frontend" "de" +mkMessageVariant "UniWorX" "Frontend" "messages/frontend" "de-de-formal" instance RenderMessage UniWorX TermIdentifier where renderMessage foundation ls TermIdentifier{..} = case season of @@ -299,11 +299,11 @@ instance RenderMessage UniWorX Load where newtype MsgLanguage = MsgLanguage Lang deriving (Eq, Ord, Show, Read) instance RenderMessage UniWorX MsgLanguage where - renderMessage foundation ls (MsgLanguage lang@(Text.splitOn "-" -> lang')) - | ["de", "DE"] <- lang' = mr MsgGermanGermany - | ("de" : _) <- lang' = mr MsgGerman - | ["en", "EU"] <- lang' = mr MsgEnglishEurope - | ("en" : _) <- lang' = mr MsgEnglish + renderMessage foundation ls (MsgLanguage lang@(map mk . Text.splitOn "-" -> lang')) + | ("de" : "DE" : _) <- lang' = mr MsgGermanGermany + | ("de" : _) <- lang' = mr MsgGerman + | ("en" : "EU" : _) <- lang' = mr MsgEnglishEurope + | ("en" : _) <- lang' = mr MsgEnglish | otherwise = lang where mr = renderMessage foundation ls @@ -511,13 +511,13 @@ instance Button UniWorX ButtonSubmit where getTimeLocale' :: [Lang] -> TimeLocale -getTimeLocale' = $(timeLocaleMap [("de", "de_DE.utf8"), ("en", "en_IE.utf8")]) +getTimeLocale' = $(timeLocaleMap [("de-de", "de_DE.utf8"), ("en-eu", "en_IE.utf8")]) appTZ :: TZ appTZ = $(includeSystemTZ "Europe/Berlin") appLanguages :: NonEmpty Lang -appLanguages = "de" :| ["en"] +appLanguages = "de-de-formal" :| ["en-eu"] appLanguagesOpts :: ( MonadHandler m , HandlerSite m ~ UniWorX @@ -1571,7 +1571,7 @@ instance Yesod UniWorX where -- b) Validates that incoming write requests include that token in either a header or POST parameter. -- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware -- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package. - yesodMiddleware = headerMessagesMiddleware . defaultYesodMiddleware . normalizeRouteMiddleware . defaultCsrfMiddleware . updateFavouritesMiddleware + yesodMiddleware = languagesMiddleware appLanguages . headerMessagesMiddleware . defaultYesodMiddleware . normalizeRouteMiddleware . defaultCsrfMiddleware . updateFavouritesMiddleware where updateFavouritesMiddleware :: Handler a -> Handler a updateFavouritesMiddleware handler = (*> handler) . runMaybeT $ do @@ -3632,24 +3632,25 @@ updateUserLanguage (Just lang) = do 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 : _), _) + setLangs <- toList . selectLanguages appLanguages <$> languages + highPrioSetLangs <- toList . selectLanguages appLanguages <$> highPrioRequestedLangs + let userLanguages' = toList . selectLanguages appLanguages <$> userLanguages ^? _Just . _Wrapped + lang <- case (userLanguages', setLangs, highPrioSetLangs) of + (_, _, hpl : _) + -> lift $ hpl <$ update uid [ UserLanguages =. Just (Languages highPrioSetLangs) ] + (Just (l : _), _, _) -> return l - (Nothing, l : _) + (Nothing, l : _, _) -> lift $ l <$ update uid [ UserLanguages =. Just (Languages setLangs) ] - (Just [], l : _) + (Just [], l : _, _) -> return l - (_, []) + (_, [], _) -> mzero setLangCookie lang - setLanguage lang return lang diff --git a/src/Utils/Lang.hs b/src/Utils/Lang.hs index c0de743ba..82103b848 100644 --- a/src/Utils/Lang.hs +++ b/src/Utils/Lang.hs @@ -3,11 +3,22 @@ module Utils.Lang where import ClassyPrelude.Yesod import qualified Data.List.NonEmpty as NonEmpty -import Data.List.NonEmpty (NonEmpty(..)) +import Data.List.NonEmpty (NonEmpty(..), nonEmpty) import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text import qualified Data.List as List +import qualified Data.CaseInsensitive as CI + +import Control.Lens (none) + +import Yesod.Core.Types (HandlerData(handlerRequest), YesodRequest(reqLangs)) +import qualified Network.Wai.Parse as NWP + +import Control.Monad.Trans.Maybe (MaybeT(..)) +import Control.Monad.Reader.Class (local) + selectLanguage :: MonadHandler m => NonEmpty Lang -- ^ Available translations, first is default @@ -17,18 +28,44 @@ selectLanguage avL = selectLanguage' avL <$> languages selectLanguage' :: NonEmpty Lang -- ^ Available translations, first is default -> [Lang] -- ^ Languages in preference order -> Lang -selectLanguage' (defL :| _) [] = defL -selectLanguage' avL (l:ls) +selectLanguage' avL ls = NonEmpty.head $ selectLanguages avL ls + +selectLanguages :: NonEmpty Lang -> [Lang] -> NonEmpty Lang +selectLanguages (defL :| _) [] = defL :| [] +selectLanguages avL (l:ls) | not $ null l - , Just lParts <- NonEmpty.nonEmpty $ Text.splitOn "-" l - , found <- [ l' | lParts' <- reverse . List.inits $ NonEmpty.toList lParts - , l' <- NonEmpty.toList avL - , langMatches (Text.intercalate "-" lParts') l' - ] - = fromMaybe (selectLanguage' avL ls) . listToMaybe $ sortOn (Down . length) found - | otherwise = selectLanguage' avL ls + , Just lParts <- nonEmpty $ matchesFor l + , found <- List.nub + [ l'' | lParts' <- NonEmpty.toList lParts + , l' <- NonEmpty.toList avL + , l'' <- matchesFor l' + , langMatches lParts' l'' + ] + = let now = nonEmpty . filter (\l' -> none (((==) `on` CI.mk) l') ls) $ sortOn (Down . length) found + others = selectLanguages avL ls + in maybe id (\now' others' -> NonEmpty.fromList $ toList now' ++ filter (`notElem` toList now') (toList others')) now others + | otherwise = selectLanguages avL ls langMatches :: Lang -- ^ Needle -> Lang -- ^ Haystack -> Bool -langMatches = isPrefixOf `on` Text.splitOn "-" +langMatches (CI.foldCase -> needle) (CI.foldCase -> haystack) = needle `elem` matchesFor haystack + +matchesFor :: Lang -> [Lang] +matchesFor = mapMaybe (\frags -> Text.intercalate "-" frags <$ guard (not $ null frags)) . reverse . List.inits . Text.splitOn "-" + + +highPrioRequestedLangs, lowPrioRequestedLangs :: forall m. MonadHandler m => m [Lang] +highPrioRequestedLangs = fmap (concatMap $ fromMaybe []) . mapM runMaybeT $ + [ lookupGetParams "_LANG" + , lookupCookies "_LANG" + , fmap pure . MaybeT $ lookupSession "_LANG" + ] +lowPrioRequestedLangs = fromMaybe [] . fmap (mapMaybe (either (const Nothing) Just . Text.decodeUtf8') . NWP.parseHttpAccept) <$> lookupHeader "Accept-Language" + +languagesMiddleware :: forall site a. NonEmpty Lang -> HandlerFor site a -> HandlerFor site a +languagesMiddleware avL act = do + pLangs <- fmap List.nub $ (<>) <$> highPrioRequestedLangs <*> lowPrioRequestedLangs + let langs = toList $ selectLanguages avL pLangs + setLangs hData = hData{ handlerRequest = (handlerRequest hData){ reqLangs = langs } } + local setLangs $ ($logDebugS "languages" . tshow . (pLangs,langs,) =<< languages) *> act From 331ba1fed3ca5e25942c906b10f670e2ed03299b Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Mon, 21 Oct 2019 15:14:30 +0200 Subject: [PATCH 007/126] feat(i18n): 12h-clock for english locales --- src/Foundation.hs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Foundation.hs b/src/Foundation.hs index 9f973bfd5..3ba6f09bb 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -511,7 +511,7 @@ instance Button UniWorX ButtonSubmit where getTimeLocale' :: [Lang] -> TimeLocale -getTimeLocale' = $(timeLocaleMap [("de-de", "de_DE.utf8"), ("en-eu", "en_IE.utf8")]) +getTimeLocale' = $(timeLocaleMap [("de-de", "de_DE.utf8"), ("en-GB", "en_GB.utf8")]) appTZ :: TZ appTZ = $(includeSystemTZ "Europe/Berlin") @@ -3625,14 +3625,11 @@ updateUserLanguage (Just lang) = do 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 - return $ Just lang + for_ muid $ \uid -> do + langs <- languages + update uid [ UserLanguages =. Just (Languages $ lang : nub (filter ((&&) <$> (`elem` appLanguages) <*> (/= lang)) langs)) ] + setLangCookie lang + return $ Just lang updateUserLanguage Nothing = runMaybeT $ do uid <- MaybeT maybeAuthId User{..} <- MaybeT $ get uid From 281c98fe916d1d44805605d5ffca3c6b3aecca36 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Mon, 21 Oct 2019 15:19:50 +0200 Subject: [PATCH 008/126] fix(i18n): prepare translation file for en-eu --- messages/uniworx/{en.msg => en-eu.msg} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename messages/uniworx/{en.msg => en-eu.msg} (100%) diff --git a/messages/uniworx/en.msg b/messages/uniworx/en-eu.msg similarity index 100% rename from messages/uniworx/en.msg rename to messages/uniworx/en-eu.msg From 33ddbfb7ccd648bf875152ba2c626c0220088bb2 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Mon, 21 Oct 2019 15:43:22 +0200 Subject: [PATCH 009/126] fix(i18n): rename i18nWidgetFiles to proper language code --- templates/i18n/allocation-info/{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/changelog/{de.hamlet => de-de-formal.hamlet} | 0 .../{de.hamlet => de-de-formal.hamlet} | 0 .../{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/data-protection/{de.hamlet => de-de-formal.hamlet} | 0 .../computed-values-tip/{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/featureList/{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/html-input/{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/implementation/{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/imprint/{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/info-lecturer/{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/knownBugs/{de.hamlet => de-de-formal.hamlet} | 0 .../profile/displayNameRules/{de.hamlet => de-de-formal.hamlet} | 0 .../profile/tokenExplanation/{de.hamlet => de-de-formal.hamlet} | 0 .../i18n/set-display-email/{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/sheet-edit/{de.hamlet => de-de-formal.hamlet} | 0 .../csv-import-explanation/{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/unauth-home/{de.hamlet => de-de-formal.hamlet} | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename templates/i18n/allocation-info/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/changelog/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/corrections-upload-instructions/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/course-exam-office-explanation/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/data-protection/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/exam-users/computed-values-tip/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/featureList/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/html-input/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/implementation/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/imprint/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/info-lecturer/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/knownBugs/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/profile/displayNameRules/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/profile/tokenExplanation/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/set-display-email/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/sheet-edit/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/table/csv-import-explanation/{de.hamlet => de-de-formal.hamlet} (100%) rename templates/i18n/unauth-home/{de.hamlet => de-de-formal.hamlet} (100%) diff --git a/templates/i18n/allocation-info/de.hamlet b/templates/i18n/allocation-info/de-de-formal.hamlet similarity index 100% rename from templates/i18n/allocation-info/de.hamlet rename to templates/i18n/allocation-info/de-de-formal.hamlet diff --git a/templates/i18n/changelog/de.hamlet b/templates/i18n/changelog/de-de-formal.hamlet similarity index 100% rename from templates/i18n/changelog/de.hamlet rename to templates/i18n/changelog/de-de-formal.hamlet diff --git a/templates/i18n/corrections-upload-instructions/de.hamlet b/templates/i18n/corrections-upload-instructions/de-de-formal.hamlet similarity index 100% rename from templates/i18n/corrections-upload-instructions/de.hamlet rename to templates/i18n/corrections-upload-instructions/de-de-formal.hamlet diff --git a/templates/i18n/course-exam-office-explanation/de.hamlet b/templates/i18n/course-exam-office-explanation/de-de-formal.hamlet similarity index 100% rename from templates/i18n/course-exam-office-explanation/de.hamlet rename to templates/i18n/course-exam-office-explanation/de-de-formal.hamlet diff --git a/templates/i18n/data-protection/de.hamlet b/templates/i18n/data-protection/de-de-formal.hamlet similarity index 100% rename from templates/i18n/data-protection/de.hamlet rename to templates/i18n/data-protection/de-de-formal.hamlet diff --git a/templates/i18n/exam-users/computed-values-tip/de.hamlet b/templates/i18n/exam-users/computed-values-tip/de-de-formal.hamlet similarity index 100% rename from templates/i18n/exam-users/computed-values-tip/de.hamlet rename to templates/i18n/exam-users/computed-values-tip/de-de-formal.hamlet diff --git a/templates/i18n/featureList/de.hamlet b/templates/i18n/featureList/de-de-formal.hamlet similarity index 100% rename from templates/i18n/featureList/de.hamlet rename to templates/i18n/featureList/de-de-formal.hamlet diff --git a/templates/i18n/html-input/de.hamlet b/templates/i18n/html-input/de-de-formal.hamlet similarity index 100% rename from templates/i18n/html-input/de.hamlet rename to templates/i18n/html-input/de-de-formal.hamlet diff --git a/templates/i18n/implementation/de.hamlet b/templates/i18n/implementation/de-de-formal.hamlet similarity index 100% rename from templates/i18n/implementation/de.hamlet rename to templates/i18n/implementation/de-de-formal.hamlet diff --git a/templates/i18n/imprint/de.hamlet b/templates/i18n/imprint/de-de-formal.hamlet similarity index 100% rename from templates/i18n/imprint/de.hamlet rename to templates/i18n/imprint/de-de-formal.hamlet diff --git a/templates/i18n/info-lecturer/de.hamlet b/templates/i18n/info-lecturer/de-de-formal.hamlet similarity index 100% rename from templates/i18n/info-lecturer/de.hamlet rename to templates/i18n/info-lecturer/de-de-formal.hamlet diff --git a/templates/i18n/knownBugs/de.hamlet b/templates/i18n/knownBugs/de-de-formal.hamlet similarity index 100% rename from templates/i18n/knownBugs/de.hamlet rename to templates/i18n/knownBugs/de-de-formal.hamlet diff --git a/templates/i18n/profile/displayNameRules/de.hamlet b/templates/i18n/profile/displayNameRules/de-de-formal.hamlet similarity index 100% rename from templates/i18n/profile/displayNameRules/de.hamlet rename to templates/i18n/profile/displayNameRules/de-de-formal.hamlet diff --git a/templates/i18n/profile/tokenExplanation/de.hamlet b/templates/i18n/profile/tokenExplanation/de-de-formal.hamlet similarity index 100% rename from templates/i18n/profile/tokenExplanation/de.hamlet rename to templates/i18n/profile/tokenExplanation/de-de-formal.hamlet diff --git a/templates/i18n/set-display-email/de.hamlet b/templates/i18n/set-display-email/de-de-formal.hamlet similarity index 100% rename from templates/i18n/set-display-email/de.hamlet rename to templates/i18n/set-display-email/de-de-formal.hamlet diff --git a/templates/i18n/sheet-edit/de.hamlet b/templates/i18n/sheet-edit/de-de-formal.hamlet similarity index 100% rename from templates/i18n/sheet-edit/de.hamlet rename to templates/i18n/sheet-edit/de-de-formal.hamlet diff --git a/templates/i18n/table/csv-import-explanation/de.hamlet b/templates/i18n/table/csv-import-explanation/de-de-formal.hamlet similarity index 100% rename from templates/i18n/table/csv-import-explanation/de.hamlet rename to templates/i18n/table/csv-import-explanation/de-de-formal.hamlet diff --git a/templates/i18n/unauth-home/de.hamlet b/templates/i18n/unauth-home/de-de-formal.hamlet similarity index 100% rename from templates/i18n/unauth-home/de.hamlet rename to templates/i18n/unauth-home/de-de-formal.hamlet From 268d9e0b1cfc2a41079d42415fccc075ba909ee7 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Mon, 21 Oct 2019 15:51:22 +0200 Subject: [PATCH 010/126] fix(i18n): get started on i18n-breadcrumbs --- src/Foundation.hs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Foundation.hs b/src/Foundation.hs index 3ba6f09bb..64e0bd6ee 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1908,16 +1908,27 @@ applySystemMessages = liftHandler . runDB . runConduit $ selectSource [] [] .| C Nothing -> addMessage systemMessageSeverity content -- Define breadcrumbs. +i18nCrumb :: ( RenderMessage (HandlerSite m) msg, MonadHandler m ) + => msg + -> Maybe (Route (HandlerSite m)) + -> m (Text, Maybe (Route (HandlerSite m))) +i18nCrumb msg mbR = do + mr <- getMessageRender + return (mr msb, mbR) + instance YesodBreadcrumbs UniWorX where - breadcrumb (AuthR _) = return ("Login" , Just HomeR) - breadcrumb HomeR = return ("Uni2work" , Nothing) - breadcrumb UsersR = return ("Benutzer" , Just AdminR) - breadcrumb AdminUserAddR = return ("Benutzer anlegen", Just UsersR) - breadcrumb (AdminUserR _) = return ("Users" , Just UsersR) - breadcrumb AdminR = return ("Administration", Nothing) - breadcrumb AdminFeaturesR = return ("Test" , Just AdminR) - breadcrumb AdminTestR = return ("Test" , Just AdminR) - breadcrumb AdminErrMsgR = return ("Test" , Just AdminR) + breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just HomeR + breadcrumb HomeR = i18nCrumb MsgMenuHome Nothing + breadcrumb UsersR = i18nCrumb MsgMenuUsers $ Just AdminR + breadcrumb AdminUserAddR = i18nCrumb MsgMenuUserAdd $ Just UsersR + breadcrumb (AdminUserR cID) = do + uid <- decrypt cID + User{..} <- runDB $ get404 uid + return (userDisplayName, Just UsersR + breadcrumb AdminR = i18nCrumb MsgAdminHeading Nothing + breadcrumb AdminFeaturesR = i18nCrumb MsgAdminFeaturesHeading $ Just AdminR + breadcrumb AdminTestR = i18nCrumb MsgMenuAdminTest $ Just AdminR + breadcrumb AdminErrMsgR = i18nCrumb MsgAdminErrMsg $ Just AdminR breadcrumb SchoolListR = return ("Institute" , Just AdminR) breadcrumb (SchoolR ssh SchoolEditR) = return (original (unSchoolKey ssh), Just SchoolListR) From 6ca87f0d66c40ba651f4dbe0e2155790b74613c8 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Tue, 22 Oct 2019 00:13:22 +0200 Subject: [PATCH 011/126] refactor(i18n): i18n for breadcrumbs --- messages/uniworx/de-de-formal.msg | 57 ++++- src/Foundation.hs | 337 +++++++++++++++++++++--------- src/Handler/CryptoIDDispatch.hs | 4 +- src/Utils/Lang.hs | 2 +- test/MailSpec.hs | 7 +- test/Model/Types/LanguagesSpec.hs | 14 ++ test/Model/TypesSpec.hs | 2 + test/ModelSpec.hs | 7 +- test/TestImport.hs | 2 +- 9 files changed, 313 insertions(+), 119 deletions(-) create mode 100644 test/Model/Types/LanguagesSpec.hs diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 825036b04..45a6a6e53 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1067,6 +1067,7 @@ MenuProfileData: Persönliche Daten MenuTermCreate: Neues Semester anlegen MenuCourseNew: Neuen Kurs anlegen MenuTermEdit: Semester editieren +MenuTermCurrent: Aktuelles Semester MenuCorrection: Korrektur MenuCorrections: Korrekturen MenuCorrectionsOwn: Meine Korrekturen @@ -1095,8 +1096,8 @@ MenuCorrectionsUpload: Korrekturen hochladen MenuCorrectionsDownload: Offene Abgaben herunterladen MenuCorrectionsCreate: Abgaben registrieren MenuCorrectionsGrade: Abgaben online korrigieren -MenuCorrectionsAssign: Zuteilung Korrekturen -MenuCorrectionsAssignSheet name@Text: Zuteilung Korrekturen von #{name} +MenuCorrectionsAssign: Zuteilung der Korrekturen +MenuCorrectionsAssignSheet name@Text: Zuteilung der Korrekturen von #{name} MenuAuthPreds: Authorisierungseinstellungen MenuTutorialDelete: Tutorium löschen MenuTutorialEdit: Tutorium editieren @@ -1120,6 +1121,58 @@ MenuCourseNewsEdit: Kursnachricht bearbeiten MenuCourseEventNew: Neuer Kurstermin MenuCourseEventEdit: Kurstermin bearbeiten +BreadcrumbSubmissionFile: Datei +BreadcrumbSubmissionUserInvite: Einladung zur Abgabe +BreadcrumbCryptoIDDispatch: CryptoID-Weiterleitung +BreadcrumbCourseAppsFiles: Bewerbungsdateien +BreadcrumbCourseNotes: Kursnotizen +BreadcrumbHiWis: Korrektoren +BreadcrumbMaterial: Material +BreadcrumbSheet: Übungsblatt +BreadcrumbTutorial: Tutorium +BreadcrumbExam: Prüfung +BreadcrumbApplicant: Bewerber +BreadcrumbCourseRegister: Anmelden +BreadcrumbCourseRegisterTemplate: Bewerbungsvorlagen +BreadcrumbCourseFavourite: Favorisieren +BreadcrumbCourse: Kurs +BreadcrumbAllocationRegister: Teilnahme registrieren +BreadcrumbAllocation: Zentralanmeldung +BreadcrumbTerm: Semester +BreadcrumbSchool: Institut +BreadcrumbUser: Benutzer +BreadcrumbStatic: Statische Resource +BreadcrumbFavicon: Favicon +BreadcrumbRobots: robots.txt +BreadcrumbLecturerInvite: Einladung zum Kursverwalter +BreadcrumbExamOfficeUserInvite: Einladung bzgl. Prüfungsleistungen +BreadcrumbFunctionaryInvite: Einladung zum Instituts-Funktionär +BreadcrumbUserDelete: Nutzer-Account löschen +BreadcrumbUserHijack: Nutzer-Sitzung übernehmen +BreadcrumbSystemMessage: Statusmeldung +BreadcrumbSubmission: Abgabe +BreadcrumbCourseNews: Kursnachricht +BreadcrumbCourseNewsDelete: Kursnachricht löschen +BreadcrumbCourseEventDelete: Kurstermin löschen +BreadcrumbProfile: Einstellungen +BreadcrumbAllocationInfo: Ablauf einer Zentralanmeldung +BreadcrumbCourseParticipantInvitation: Einladung zum Kursteilnehmer +BreadcrumbMaterialArchive: Archiv +BreadcrumbMaterialFile: Datei +BreadcrumbSheetArchive: Dateien +BreadcrumbSheetIsCorrector: Korrektor-Überprüfung +BreadcrumbSheetPseudonym: Pseudonym +BreadcrumbSheetCorrectorInvite: Einladung zum Korrektor +BreadcrumbSheetFile: Datei +BreadcrumbTutorialRegister: Anmelden +BreadcrumbTutorInvite: Einladung zum Tutor +BreadcrumbExamCorrectorInvite: Einladung zum Prüfungskorrektor +BreadcrumbExamParticipantInvite: Einladung zum Prüfungsteilnehmer +BreadcrumbExamRegister: Anmelden +BreadcrumbApplicationFiles: Bewerbungsdateien +BreadcrumbCourseNewsArchive: Archiv +BreadcrumbCourseNewsFile: Datei + AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist, d.h. bis ihr Browser-Cookie abgelaufen ist. Durch Abwahl von Prädikaten kann man sich höchstens temporär aussperren. AuthPredsActive: Aktive Authorisierungsprädikate AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert diff --git a/src/Foundation.hs b/src/Foundation.hs index 64e0bd6ee..1b17acd0c 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1511,6 +1511,15 @@ redirectAccess url = do Authorized -> redirect url _ -> permissionDeniedI MsgUnauthorizedRedirect +redirectAccessWith :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Status -> Route UniWorX -> m a +redirectAccessWith status url = do + -- must hide URL if not authorized + access <- evalAccess url False + case access of + Authorized -> redirectWith status url + _ -> permissionDeniedI MsgUnauthorizedRedirect + + -- | Verify that the currently logged in user is lecturer or corrector for at least one sheet for the given course evalAccessCorrector :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => TermId -> SchoolId -> CourseShorthand -> m AuthResult @@ -1725,7 +1734,21 @@ siteLayout' headingOverride widget = do mcurrentRoute <- getCurrentRoute -- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance. - (title, parents) <- breadcrumbs + let + breadcrumbs' mcRoute = do + mr <- getMessageRender + case mcRoute of + Nothing -> return (mr MsgErrorResponseTitleNotFound, []) + Just cRoute -> do + (title, next) <- breadcrumb cRoute + crumbs <- go [] next + return (title, crumbs) + where + go crumbs Nothing = return crumbs + go crumbs (Just cRoute) = do + (title, next) <- breadcrumb cRoute + go ((cRoute, title) : crumbs) next + (title, parents) <- breadcrumbs' mcurrentRoute -- let isParent :: Route UniWorX -> Bool -- isParent r = r == (fst parents) @@ -1914,141 +1937,253 @@ i18nCrumb :: ( RenderMessage (HandlerSite m) msg, MonadHandler m ) -> m (Text, Maybe (Route (HandlerSite m))) i18nCrumb msg mbR = do mr <- getMessageRender - return (mr msb, mbR) - + return (mr msg, mbR) + +-- `breadcrumb` _really_ needs to be total for _all_ routes +-- +-- Even if routes are POST only or don't usually use `siteLayout` they will if +-- an error occurs. +-- +-- Keep in mind that Breadcrumbs are also shown by the 403-Handler, +-- i.e. information might be leaked by not performing permission checks if the +-- breadcrumb value depends on sensitive content (like an user's name). instance YesodBreadcrumbs UniWorX where breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just HomeR + breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing + breadcrumb FaviconR = i18nCrumb MsgBreadcrumbFavicon Nothing + breadcrumb RobotsR = i18nCrumb MsgBreadcrumbRobots Nothing + breadcrumb HomeR = i18nCrumb MsgMenuHome Nothing breadcrumb UsersR = i18nCrumb MsgMenuUsers $ Just AdminR breadcrumb AdminUserAddR = i18nCrumb MsgMenuUserAdd $ Just UsersR - breadcrumb (AdminUserR cID) = do + breadcrumb (AdminUserR cID) = maybeT (i18nCrumb MsgBreadcrumbUser $ Just UsersR) $ do + guardM . hasReadAccessTo $ AdminUserR cID uid <- decrypt cID - User{..} <- runDB $ get404 uid - return (userDisplayName, Just UsersR + User{..} <- MaybeT . runDB $ get uid + return (userDisplayName, Just UsersR) + breadcrumb (AdminUserDeleteR cID) = i18nCrumb MsgBreadcrumbUserDelete . Just $ AdminUserR cID + breadcrumb (AdminHijackUserR cID) = i18nCrumb MsgBreadcrumbUserHijack . Just $ AdminUserR cID + breadcrumb (UserNotificationR cID) = do + mayList <- hasReadAccessTo UsersR + if + | mayList + -> i18nCrumb MsgMenuUserNotifications . Just $ AdminUserR cID + | otherwise + -> i18nCrumb MsgMenuUserNotifications $ Just ProfileR + breadcrumb (UserPasswordR cID) = do + mayList <- hasReadAccessTo UsersR + if + | mayList + -> i18nCrumb MsgMenuUserPassword . Just $ AdminUserR cID + | otherwise + -> i18nCrumb MsgMenuUserPassword $ Just ProfileR + breadcrumb AdminNewFunctionaryInviteR = i18nCrumb MsgMenuLecturerInvite $ Just UsersR + breadcrumb AdminFunctionaryInviteR = i18nCrumb MsgBreadcrumbFunctionaryInvite Nothing + breadcrumb AdminR = i18nCrumb MsgAdminHeading Nothing breadcrumb AdminFeaturesR = i18nCrumb MsgAdminFeaturesHeading $ Just AdminR breadcrumb AdminTestR = i18nCrumb MsgMenuAdminTest $ Just AdminR - breadcrumb AdminErrMsgR = i18nCrumb MsgAdminErrMsg $ Just AdminR + breadcrumb AdminErrMsgR = i18nCrumb MsgMenuAdminErrMsg $ Just AdminR - breadcrumb SchoolListR = return ("Institute" , Just AdminR) - breadcrumb (SchoolR ssh SchoolEditR) = return (original (unSchoolKey ssh), Just SchoolListR) - breadcrumb SchoolNewR = return ("Neu" , Just SchoolListR) + breadcrumb SchoolListR = i18nCrumb MsgMenuSchoolList $ Just AdminR + breadcrumb (SchoolR ssh SchoolEditR) = maybeT (i18nCrumb MsgBreadcrumbSchool $ Just SchoolListR) $ do + School{..} <- MaybeT . runDB $ get ssh + return (original schoolName, Just SchoolListR) + breadcrumb SchoolNewR = i18nCrumb MsgMenuSchoolNew $ Just SchoolListR - breadcrumb (ExamOfficeR EOExamsR) = return ("Prüfungen", Nothing) - breadcrumb (ExamOfficeR EOFieldsR) = return ("Fächer" , Just $ ExamOfficeR EOExamsR) - breadcrumb (ExamOfficeR EOUsersR) = return ("Benutzer" , Just $ ExamOfficeR EOExamsR) + breadcrumb (ExamOfficeR EOExamsR) = i18nCrumb MsgMenuExamOfficeExams Nothing + breadcrumb (ExamOfficeR EOFieldsR) = i18nCrumb MsgMenuExamOfficeFields . Just $ ExamOfficeR EOExamsR + breadcrumb (ExamOfficeR EOUsersR) = i18nCrumb MsgMenuExamOfficeUsers . Just $ ExamOfficeR EOExamsR + breadcrumb (ExamOfficeR EOUsersInviteR) = i18nCrumb MsgBreadcrumbExamOfficeUserInvite Nothing - breadcrumb InfoR = return ("Information" , Nothing) - breadcrumb InfoLecturerR = return ("Veranstalter" , Just InfoR) - breadcrumb DataProtR = return ("Datenschutz" , Just InfoR) - breadcrumb InfoAllocationR = return ("Zentralanmeldungen", Just InfoR) - breadcrumb ImpressumR = return ("Impressum" , Just InfoR) - breadcrumb VersionR = return ("Versionsgeschichte", Just InfoR) + breadcrumb InfoR = i18nCrumb MsgMenuInformation Nothing + breadcrumb InfoLecturerR = i18nCrumb MsgInfoLecturerTitle $ Just InfoR + breadcrumb DataProtR = i18nCrumb MsgMenuDataProt $ Just InfoR + breadcrumb InfoAllocationR = i18nCrumb MsgBreadcrumbAllocationInfo $ Just InfoR + breadcrumb ImpressumR = i18nCrumb MsgMenuImpressum $ Just InfoR + breadcrumb VersionR = i18nCrumb MsgMenuVersion $ Just InfoR - breadcrumb HelpR = return ("Hilfe" , Just HomeR) + breadcrumb HelpR = i18nCrumb MsgMenuHelp Nothing - breadcrumb HealthR = return ("Status" , Nothing) - breadcrumb InstanceR = return ("Identifikation", Nothing) + breadcrumb HealthR = i18nCrumb MsgMenuHealth Nothing + breadcrumb InstanceR = i18nCrumb MsgMenuInstance Nothing + breadcrumb ProfileR = i18nCrumb MsgBreadcrumbProfile Nothing + breadcrumb SetDisplayEmailR = i18nCrumb MsgUserDisplayEmail $ Just ProfileR + breadcrumb ProfileDataR = i18nCrumb MsgMenuProfileData $ Just ProfileR + breadcrumb AuthPredsR = i18nCrumb MsgMenuAuthPreds $ Just ProfileR + breadcrumb CsvOptionsR = i18nCrumb MsgCsvOptions $ Just ProfileR + breadcrumb LangR = i18nCrumb MsgMenuLanguage $ Just ProfileR - breadcrumb ProfileR = return ("Einstellungen" , Just HomeR) - breadcrumb SetDisplayEmailR = return ("Öffentliche E-Mail Adresse", Just ProfileR) - breadcrumb ProfileDataR = return ("Persönliche Daten", Just ProfileR) - breadcrumb AuthPredsR = return ("Authorisierung" , Just ProfileR) + breadcrumb TermShowR = i18nCrumb MsgMenuTermShow $ Just HomeR + breadcrumb TermCurrentR = i18nCrumb MsgMenuTermCurrent $ Just TermShowR + breadcrumb TermEditR = i18nCrumb MsgMenuTermCreate $ Just TermShowR + breadcrumb (TermEditExistR tid) = i18nCrumb MsgMenuTermEdit . Just $ TermCourseListR tid + breadcrumb (TermCourseListR tid) = maybeT (i18nCrumb MsgBreadcrumbTerm $ Just CourseListR) $ do -- redirect only, used in other breadcrumbs + guardM . lift . runDB $ isJust <$> get tid + i18nCrumb (ShortTermIdentifier $ unTermKey tid) $ Just CourseListR - breadcrumb TermShowR = return ("Semester" , Just HomeR) - breadcrumb TermCurrentR = return ("Aktuell" , Just TermShowR) - breadcrumb TermEditR = return ("Neu" , Just TermCurrentR) - breadcrumb (TermEditExistR tid) = return ("Editieren" , Just $ TermCourseListR tid) - breadcrumb (TermCourseListR (unTermKey -> tid)) = getMessageRender <&> \mr -> (mr $ ShortTermIdentifier tid, Just CourseListR) + breadcrumb (TermSchoolCourseListR tid ssh) = maybeT (i18nCrumb MsgBreadcrumbSchool . Just $ TermCourseListR tid) $ do -- redirect only, used in other breadcrumbs + guardM . lift . runDB $ + (&&) <$> fmap isJust (get ssh) + <*> fmap isJust (get tid) + return (original $ unSchoolKey ssh, Just $ TermCourseListR tid) - breadcrumb (TermSchoolCourseListR tid ssh) = return (original $ unSchoolKey ssh, Just $ TermCourseListR tid) - - breadcrumb AllocationListR = return ("Zentralanmeldungen", Just HomeR) - breadcrumb (AllocationR tid ssh ash AShowR) = do + breadcrumb AllocationListR = i18nCrumb MsgAllocationListTitle $ Just HomeR + breadcrumb (AllocationR tid ssh ash AShowR) = maybeT (i18nCrumb MsgBreadcrumbAllocation $ Just AllocationListR) $ do mr <- getMessageRender - Entity _ Allocation{allocationName} <- runDB . getBy404 $ TermSchoolAllocationShort tid ssh ash + Entity _ Allocation{allocationName} <- MaybeT . runDB . getBy $ TermSchoolAllocationShort tid ssh ash return ([st|#{allocationName} (#{mr (ShortTermIdentifier (unTermKey tid))}, #{original (unSchoolKey ssh)})|], Just $ AllocationListR) + breadcrumb (AllocationR tid ssh ash ARegisterR) = i18nCrumb MsgBreadcrumbAllocationRegister . Just $ AllocationR tid ssh ash AShowR + breadcrumb (AllocationR tid ssh ash (AApplyR cID)) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ AllocationR tid ssh ash AShowR) $ do + cid <- decrypt cID + Course{..} <- hoist runDB $ do + aid <- MaybeT . getKeyBy $ TermSchoolAllocationShort tid ssh ash + guardM . lift $ exists [ AllocationCourseAllocation ==. aid, AllocationCourseCourse ==. cid ] + MaybeT $ get cid + return (original courseName, Just $ AllocationR tid ssh ash AShowR) - breadcrumb CourseListR = return ("Kurse" , Nothing) - breadcrumb CourseNewR = return ("Neu" , Just CourseListR) - breadcrumb (CourseR tid ssh csh CShowR) = return (original csh, Just $ TermSchoolCourseListR tid ssh) - -- (CourseR tid ssh csh CRegisterR) -- is POST only - breadcrumb (CourseR tid ssh csh CEditR) = return ("Editieren" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CUsersR) = return ("Anmeldungen", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CAddUserR) = return ("Kursteilnehmer hinzufügen", Just $ CourseR tid ssh csh CUsersR) - breadcrumb (CourseR tid ssh csh CInviteR) = return ("Einladung", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CExamOfficeR) = return ("Prüfungsamter", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh (CUserR cID)) = do + breadcrumb CourseListR = i18nCrumb MsgMenuCourseList Nothing + breadcrumb CourseNewR = i18nCrumb MsgMenuCourseNew $ Just CourseListR + breadcrumb (CourseR tid ssh csh CShowR) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ TermSchoolCourseListR tid ssh) $ do + guardM . lift . runDB . existsBy $ TermSchoolCourseShort tid ssh csh + return (original csh, Just $ TermSchoolCourseListR tid ssh) + breadcrumb (CourseR tid ssh csh CEditR) = i18nCrumb MsgMenuCourseEdit . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CUsersR) = i18nCrumb MsgMenuCourseMembers . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CAddUserR) = i18nCrumb MsgMenuCourseAddMembers . Just $ CourseR tid ssh csh CUsersR + breadcrumb (CourseR tid ssh csh CInviteR) = i18nCrumb MsgBreadcrumbCourseParticipantInvitation . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CExamOfficeR) = i18nCrumb MsgMenuCourseExamOffice . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh (CUserR cID)) = maybeT (i18nCrumb MsgBreadcrumbUser . Just $ CourseR tid ssh csh CUsersR) $ do + guardM . hasReadAccessTo . CourseR tid ssh csh $ CUserR cID uid <- decrypt cID - User{userDisplayName} <- runDB $ get404 uid - return (userDisplayName, Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CCorrectionsR) = return ("Abgaben" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CAssignR) = return ("Zuteilung Korrekturen" , Just $ CourseR tid ssh csh CCorrectionsR) - breadcrumb (CourseR tid ssh csh SheetListR) = return ("Übungen" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR) - breadcrumb (CourseR tid ssh csh SheetCurrentR) = return ("Aktuelles Blatt", Just $ CourseR tid ssh csh SheetListR) - breadcrumb (CourseR tid ssh csh SheetOldUnassignedR) = return ("Offene Abgaben", Just $ CourseR tid ssh csh SheetListR) - breadcrumb (CourseR tid ssh csh CCommR ) = return ("Kursmitteilung", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CTutorialListR) = return ("Tutorien", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CTutorialNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CTutorialListR) + User{userDisplayName} <- MaybeT . runDB $ get uid + return (userDisplayName, Just $ CourseR tid ssh csh CUsersR) + breadcrumb (CourseR tid ssh csh CCorrectionsR) = i18nCrumb MsgMenuSubmissions . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CAssignR) = i18nCrumb MsgMenuCorrectionsAssign . Just $ CourseR tid ssh csh CCorrectionsR + breadcrumb (CourseR tid ssh csh SheetListR) = i18nCrumb MsgMenuSheetList . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh SheetNewR ) = i18nCrumb MsgMenuSheetNew . Just $ CourseR tid ssh csh SheetListR + breadcrumb (CourseR tid ssh csh SheetCurrentR) = i18nCrumb MsgMenuSheetCurrent . Just $ CourseR tid ssh csh SheetListR + breadcrumb (CourseR tid ssh csh SheetOldUnassignedR) = i18nCrumb MsgMenuSheetOldUnassigned . Just $ CourseR tid ssh csh SheetListR + breadcrumb (CourseR tid ssh csh CCommR ) = i18nCrumb MsgMenuCourseCommunication . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CTutorialListR) = i18nCrumb MsgMenuTutorialList . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CTutorialNewR) = i18nCrumb MsgMenuTutorialNew . Just $ CourseR tid ssh csh CTutorialListR + breadcrumb (CourseR tid ssh csh CFavouriteR) = i18nCrumb MsgBreadcrumbCourseFavourite . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CRegisterR) = i18nCrumb MsgBreadcrumbCourseRegister . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CRegisterTemplateR) = i18nCrumb MsgBreadcrumbCourseRegisterTemplate . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CLecInviteR) = i18nCrumb MsgBreadcrumbLecturerInvite . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CDeleteR) = i18nCrumb MsgMenuCourseDelete . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CHiWisR) = i18nCrumb MsgBreadcrumbHiWis . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CNotesR) = i18nCrumb MsgBreadcrumbCourseNotes . Just $ CourseR tid ssh csh CShowR - breadcrumb (CourseR tid ssh csh CNewsNewR) = return ("Neue Nachricht", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CNewsR tid ssh csh _ CNShowR) = return ("Kursnachricht" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CNewsR tid ssh csh cID CNEditR) = return ("Bearbeiten" , Just $ CNewsR tid ssh csh cID CNShowR) - breadcrumb (CNewsR tid ssh csh cID CNDeleteR) = return ("Löschen" , Just $ CNewsR tid ssh csh cID CNShowR) + breadcrumb (CourseR tid ssh csh CNewsNewR) = i18nCrumb MsgMenuCourseNewsNew . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh (CourseNewsR cID sRoute)) = case sRoute of + CNShowR -> i18nCrumb MsgBreadcrumbCourseNews . Just $ CourseR tid ssh csh CShowR + CNEditR -> i18nCrumb MsgMenuCourseNewsEdit . Just $ CNewsR tid ssh csh cID CNShowR + CNDeleteR -> i18nCrumb MsgBreadcrumbCourseNewsDelete . Just $ CNewsR tid ssh csh cID CNShowR + CNArchiveR -> i18nCrumb MsgBreadcrumbCourseNewsArchive . Just $ CNewsR tid ssh csh cID CNShowR + CNFileR _ -> i18nCrumb MsgBreadcrumbCourseNewsFile . Just $ CNewsR tid ssh csh cID CNShowR + + breadcrumb (CourseR tid ssh csh CEventsNewR) = i18nCrumb MsgMenuCourseEventNew . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh (CourseEventR _cID sRoute)) = case sRoute of + CEvEditR -> i18nCrumb MsgMenuCourseEventEdit . Just $ CourseR tid ssh csh CShowR + CEvDeleteR -> i18nCrumb MsgBreadcrumbCourseEventDelete . Just $ CourseR tid ssh csh CShowR - breadcrumb (CourseR tid ssh csh CExamListR) = return ("Prüfungen", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CExamNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CExamListR) + breadcrumb (CourseR tid ssh csh CExamListR) = i18nCrumb MsgMenuExamList . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CExamNewR) = i18nCrumb MsgMenuExamNew . Just $ CourseR tid ssh csh CExamListR - breadcrumb (CourseR tid ssh csh CApplicationsR) = return ("Bewerbungen", Just $ CourseR tid ssh csh CShowR) + breadcrumb (CourseR tid ssh csh CApplicationsR) = i18nCrumb MsgMenuCourseApplications . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CAppsFilesR) = i18nCrumb MsgBreadcrumbCourseAppsFiles . Just $ CourseR tid ssh csh CApplicationsR - breadcrumb (CApplicationR tid ssh csh _ CAEditR) = return ("Bewerbung", Just $ CourseR tid ssh csh CApplicationsR) + breadcrumb (CourseR tid ssh csh (CourseApplicationR cID sRoute)) = case sRoute of + CAEditR -> maybeT (i18nCrumb MsgBreadcrumbApplicant . Just $ CourseR tid ssh csh CApplicationsR) $ do + guardM . hasReadAccessTo $ CApplicationR tid ssh csh cID CAEditR + appId <- decrypt cID + User{..} <- hoist runDB $ MaybeT (get appId) >>= MaybeT . get . courseApplicationUser + return (userDisplayName, Just $ CourseR tid ssh csh CApplicationsR) + CAFilesR -> i18nCrumb MsgBreadcrumbApplicationFiles . Just $ CApplicationR tid ssh csh cID CAEditR - breadcrumb (CExamR tid ssh csh examn EShowR) = return (original examn, Just $ CourseR tid ssh csh CExamListR) - breadcrumb (CExamR tid ssh csh examn EEditR) = return ("Bearbeiten", Just $ CExamR tid ssh csh examn EShowR) - breadcrumb (CExamR tid ssh csh examn EUsersR) = return ("Teilnehmer", Just $ CExamR tid ssh csh examn EShowR) - breadcrumb (CExamR tid ssh csh examn EAddUserR) = return ("Prüfungsteilnehmer hinzufügen", Just $ CExamR tid ssh csh examn EUsersR) - breadcrumb (CExamR tid ssh csh examn EGradesR) = return ("Prüfungsleistungen", Just $ CExamR tid ssh csh examn EShowR) + breadcrumb (CourseR tid ssh csh (ExamR examn sRoute)) = case sRoute of + EShowR -> maybeT (i18nCrumb MsgBreadcrumbExam . Just $ CourseR tid ssh csh CExamListR) $ do + guardM . hasReadAccessTo $ CExamR tid ssh csh examn EShowR + return (original examn, Just $ CourseR tid ssh csh CExamListR) + EEditR -> i18nCrumb MsgMenuExamEdit . Just $ CExamR tid ssh csh examn EShowR + EUsersR -> i18nCrumb MsgMenuExamUsers . Just $ CExamR tid ssh csh examn EShowR + EAddUserR -> i18nCrumb MsgMenuExamAddMembers . Just $ CExamR tid ssh csh examn EUsersR + EGradesR -> i18nCrumb MsgMenuExamGrades . Just $ CExamR tid ssh csh examn EShowR + ECInviteR -> i18nCrumb MsgBreadcrumbExamCorrectorInvite . Just $ CExamR tid ssh csh examn EShowR + EInviteR -> i18nCrumb MsgBreadcrumbExamParticipantInvite . Just $ CExamR tid ssh csh examn EShowR + ERegisterR -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR - breadcrumb (CTutorialR tid ssh csh tutn TUsersR) = return (original tutn, Just $ CourseR tid ssh csh CTutorialListR) - breadcrumb (CTutorialR tid ssh csh tutn TEditR) = return ("Bearbeiten", Just $ CTutorialR tid ssh csh tutn TUsersR) - breadcrumb (CTutorialR tid ssh csh tutn TDeleteR) = return ("Löschen", Just $ CTutorialR tid ssh csh tutn TUsersR) - breadcrumb (CTutorialR tid ssh csh tutn TCommR) = return ("Mitteilung", Just $ CTutorialR tid ssh csh tutn TUsersR) + breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of + TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do + guardM . hasReadAccessTo $ CTutorialR tid ssh csh tutn TUsersR + return (original tutn, Just $ CourseR tid ssh csh CTutorialListR) + TEditR -> i18nCrumb MsgMenuTutorialEdit . Just $ CTutorialR tid ssh csh tutn TUsersR + TDeleteR -> i18nCrumb MsgMenuTutorialDelete . Just $ CTutorialR tid ssh csh tutn TUsersR + TCommR -> i18nCrumb MsgMenuTutorialComm . Just $ CTutorialR tid ssh csh tutn TUsersR + TRegisterR -> i18nCrumb MsgBreadcrumbTutorialRegister . Just $ CourseR tid ssh csh CShowR + TInviteR -> i18nCrumb MsgBreadcrumbTutorInvite . Just $ CTutorialR tid ssh csh tutn TUsersR - breadcrumb (CSheetR tid ssh csh shn SShowR) = return (original shn, Just $ CourseR tid ssh csh SheetListR) - breadcrumb (CSheetR tid ssh csh shn SEditR) = return ("Bearbeiten" , Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSheetR tid ssh csh shn SDelR ) = return ("Löschen" , Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSheetR tid ssh csh shn SSubsR) = return ("Abgaben" , Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSheetR tid ssh csh shn SAssignR) = return ("Zuteilung Korrekturen" , Just $ CSheetR tid ssh csh shn SSubsR) - breadcrumb (CSheetR tid ssh csh shn SubmissionNewR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSheetR tid ssh csh shn SubmissionOwnR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSubmissionR tid ssh csh shn _ SubShowR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR) --- (CSubmissionR tid ssh csh shn _ SubArchiveR) -- just for Download - breadcrumb (CSubmissionR tid ssh csh shn cid CorrectionR) = return ("Korrektur", Just $ CSubmissionR tid ssh csh shn cid SubShowR) --- (CSubmissionR tid ssh csh shn _ SubDownloadR) -- just for Download - breadcrumb (CSheetR tid ssh csh shn SCorrR) = return ("Korrektoren", Just $ CSheetR tid ssh csh shn SShowR) - -- (CSheetR tid ssh csh shn SFileR) -- just for Downloads + breadcrumb (CourseR tid ssh csh (SheetR shn sRoute)) = case sRoute of + SShowR -> maybeT (i18nCrumb MsgBreadcrumbSheet . Just $ CourseR tid ssh csh SheetListR) $ do + guardM . hasReadAccessTo $ CSheetR tid ssh csh shn SShowR + return (original shn, Just $ CourseR tid ssh csh SheetListR) + SEditR -> i18nCrumb MsgMenuSheetEdit . Just $ CSheetR tid ssh csh shn SShowR + SDelR -> i18nCrumb MsgMenuSheetDelete . Just $ CSheetR tid ssh csh shn SShowR + SSubsR -> i18nCrumb MsgMenuSubmissions . Just $ CSheetR tid ssh csh shn SShowR + SAssignR -> i18nCrumb MsgMenuCorrectionsAssign . Just $ CSheetR tid ssh csh shn SSubsR + SubmissionNewR -> i18nCrumb MsgMenuSubmissionNew . Just $ CSheetR tid ssh csh shn SShowR + SubmissionOwnR -> i18nCrumb MsgMenuSubmissionOwn . Just $ CSheetR tid ssh csh shn SShowR + SubmissionR cid sRoute' -> case sRoute' of + SubShowR -> do + mayList <- hasReadAccessTo $ CSheetR tid ssh csh shn SSubsR + if + | mayList + -> i18nCrumb MsgBreadcrumbSubmission . Just $ CSheetR tid ssh csh shn SSubsR + | otherwise + -> i18nCrumb MsgBreadcrumbSubmission . Just $ CSheetR tid ssh csh shn SShowR + CorrectionR -> i18nCrumb MsgMenuCorrection . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SubDelR -> i18nCrumb MsgMenuSubmissionDelete . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SubAssignR -> i18nCrumb MsgCorrectorAssignTitle . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SInviteR -> i18nCrumb MsgBreadcrumbSubmissionUserInvite . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SubArchiveR sft -> i18nCrumb sft . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SubDownloadR _ _ -> i18nCrumb MsgBreadcrumbSubmissionFile . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SCorrR -> i18nCrumb MsgMenuCorrectors . Just $ CSheetR tid ssh csh shn SShowR + SArchiveR -> i18nCrumb MsgBreadcrumbSheetArchive . Just $ CSheetR tid ssh csh shn SShowR + SIsCorrR -> i18nCrumb MsgBreadcrumbSheetIsCorrector . Just $ CSheetR tid ssh csh shn SShowR + SPseudonymR -> i18nCrumb MsgBreadcrumbSheetPseudonym . Just $ CSheetR tid ssh csh shn SShowR + SCorrInviteR -> i18nCrumb MsgBreadcrumbSheetCorrectorInvite . Just $ CSheetR tid ssh csh shn SShowR + SZipR sft -> i18nCrumb sft . Just $ CSheetR tid ssh csh shn SShowR + SFileR _ _ -> i18nCrumb MsgBreadcrumbSheetFile . Just $ CSheetR tid ssh csh shn SShowR - breadcrumb (CourseR tid ssh csh MaterialListR) = return ("Material" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh MaterialNewR ) = return ("Neu" , Just $ CourseR tid ssh csh MaterialListR) - breadcrumb (CMaterialR tid ssh csh mnm MShowR) = return (original mnm, Just $ CourseR tid ssh csh MaterialListR) - breadcrumb (CMaterialR tid ssh csh mnm MEditR) = return ("Bearbeiten" , Just $ CMaterialR tid ssh csh mnm MShowR) - breadcrumb (CMaterialR tid ssh csh mnm MDelR) = return ("Löschen" , Just $ CMaterialR tid ssh csh mnm MShowR) - -- (CMaterialR tid ssh csh mnm MFileR) -- just for Downloads + breadcrumb (CourseR tid ssh csh MaterialListR) = i18nCrumb MsgMenuMaterialList . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh MaterialNewR ) = i18nCrumb MsgMenuMaterialNew . Just $ CourseR tid ssh csh MaterialListR + breadcrumb (CourseR tid ssh csh (MaterialR mnm sRoute)) = case sRoute of + MShowR -> maybeT (i18nCrumb MsgBreadcrumbMaterial . Just $ CourseR tid ssh csh MaterialListR) $ do + guardM . hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR + return (original mnm, Just $ CourseR tid ssh csh MaterialListR) + MEditR -> i18nCrumb MsgMenuMaterialEdit . Just $ CMaterialR tid ssh csh mnm MShowR + MDelR -> i18nCrumb MsgMenuMaterialDelete . Just $ CMaterialR tid ssh csh mnm MShowR + MArchiveR -> i18nCrumb MsgBreadcrumbMaterialArchive . Just $ CMaterialR tid ssh csh mnm MShowR + MFileR _ -> i18nCrumb MsgBreadcrumbMaterialFile . Just $ CMaterialR tid ssh csh mnm MShowR - -- Others - breadcrumb (CorrectionsR) = return ("Korrekturen", Just HomeR) - breadcrumb (CorrectionsUploadR) = return ("Hochladen", Just CorrectionsR) + breadcrumb CorrectionsR = i18nCrumb MsgMenuCorrections Nothing + breadcrumb CorrectionsUploadR = i18nCrumb MsgMenuCorrectionsUpload $ Just CorrectionsR + breadcrumb CorrectionsCreateR = i18nCrumb MsgMenuCorrectionsCreate $ Just CorrectionsR + breadcrumb CorrectionsGradeR = i18nCrumb MsgMenuCorrectionsGrade $ Just CorrectionsR + breadcrumb CorrectionsDownloadR = i18nCrumb MsgMenuCorrectionsDownload $ Just CorrectionsR + + breadcrumb (CryptoUUIDDispatchR _) = i18nCrumb MsgBreadcrumbCryptoIDDispatch Nothing + breadcrumb (MessageR _) = do mayList <- (== Authorized) <$> evalAccess MessageListR False - return $ if - | mayList -> ("Statusmeldung", Just MessageListR) - | otherwise -> ("Statusmeldung", Just HomeR) - breadcrumb (MessageListR) = return ("Statusmeldungen", Just AdminR) - breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all + if + | mayList -> i18nCrumb MsgBreadcrumbSystemMessage $ Just MessageListR + | otherwise -> i18nCrumb MsgBreadcrumbSystemMessage $ Just HomeR + breadcrumb (MessageListR) = i18nCrumb MsgMenuMessageList $ Just AdminR + -- breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all submissionList :: TermId -> CourseShorthand -> SheetName -> UserId -> DB [E.Value SubmissionId] submissionList tid csh shn uid = E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` submissionUser) -> do diff --git a/src/Handler/CryptoIDDispatch.hs b/src/Handler/CryptoIDDispatch.hs index 8a34cde8d..9270fffe2 100644 --- a/src/Handler/CryptoIDDispatch.hs +++ b/src/Handler/CryptoIDDispatch.hs @@ -66,7 +66,7 @@ instance (CryptoRoute ciphertext plaintext, Dispatch ciphertext ps) => Dispatch getCryptoUUIDDispatchR :: UUID -> Handler () -getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith found302) +getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectAccessWith movedPermanently301) where p :: Proxy '[ SubmissionId , UserId @@ -74,7 +74,7 @@ getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith p = Proxy getCryptoFileNameDispatchR :: CI FilePath -> Handler () -getCryptoFileNameDispatchR path = dispatchID p path >>= maybe notFound (redirectWith found302) +getCryptoFileNameDispatchR path = dispatchID p path >>= maybe notFound (redirectAccessWith movedPermanently301) where p :: Proxy '[ SubmissionId ] p = Proxy diff --git a/src/Utils/Lang.hs b/src/Utils/Lang.hs index 82103b848..fe41e110f 100644 --- a/src/Utils/Lang.hs +++ b/src/Utils/Lang.hs @@ -61,7 +61,7 @@ highPrioRequestedLangs = fmap (concatMap $ fromMaybe []) . mapM runMaybeT $ , lookupCookies "_LANG" , fmap pure . MaybeT $ lookupSession "_LANG" ] -lowPrioRequestedLangs = fromMaybe [] . fmap (mapMaybe (either (const Nothing) Just . Text.decodeUtf8') . NWP.parseHttpAccept) <$> lookupHeader "Accept-Language" +lowPrioRequestedLangs = maybe [] (mapMaybe (either (const Nothing) Just . Text.decodeUtf8') . NWP.parseHttpAccept) <$> lookupHeader "Accept-Language" languagesMiddleware :: forall site a. NonEmpty Lang -> HandlerFor site a -> HandlerFor site a languagesMiddleware avL act = do diff --git a/test/MailSpec.hs b/test/MailSpec.hs index ad54385c6..b386d3972 100644 --- a/test/MailSpec.hs +++ b/test/MailSpec.hs @@ -2,6 +2,7 @@ module MailSpec where import TestImport import Utils.DateTimeSpec () +import Model.Types.LanguagesSpec () import Mail @@ -9,10 +10,6 @@ instance Arbitrary MailSmtpData where arbitrary = genericArbitrary shrink = genericShrink -instance Arbitrary MailLanguages where - arbitrary = fmap MailLanguages $ shuffle =<< sublistOf (toList appLanguages) - shrink = genericShrink - instance Arbitrary MailContext where arbitrary = genericArbitrary shrink = genericShrink @@ -26,8 +23,6 @@ spec = do parallel $ do lawsCheckHspec (Proxy @MailSmtpData) [ eqLaws, ordLaws, showReadLaws, monoidLaws ] - lawsCheckHspec (Proxy @MailLanguages) - [ eqLaws, ordLaws, showReadLaws, isListLaws, jsonLaws, hashableLaws, persistFieldLaws ] lawsCheckHspec (Proxy @MailContext) [ eqLaws, ordLaws, showReadLaws, jsonLaws, hashableLaws ] lawsCheckHspec (Proxy @VerpMode) diff --git a/test/Model/Types/LanguagesSpec.hs b/test/Model/Types/LanguagesSpec.hs new file mode 100644 index 000000000..7b2ed56dc --- /dev/null +++ b/test/Model/Types/LanguagesSpec.hs @@ -0,0 +1,14 @@ +module Model.Types.LanguagesSpec where + +import TestImport + + +instance Arbitrary Languages where + arbitrary = fmap Languages $ shuffle =<< sublistOf (toList appLanguages) + shrink = genericShrink + + +spec :: Spec +spec = do + lawsCheckHspec (Proxy @Languages) + [ eqLaws, ordLaws, showReadLaws, isListLaws, jsonLaws, hashableLaws, persistFieldLaws ] diff --git a/test/Model/TypesSpec.hs b/test/Model/TypesSpec.hs index f5f7d2efd..321d4c2e6 100644 --- a/test/Model/TypesSpec.hs +++ b/test/Model/TypesSpec.hs @@ -11,6 +11,7 @@ import qualified Data.Aeson as Aeson import qualified Data.Aeson.Types as Aeson import MailSpec () +import Model.Types.LanguagesSpec () import System.IO.Unsafe import Yesod.Auth.Util.PasswordStore @@ -279,6 +280,7 @@ instance Arbitrary Sex where arbitrary = genericArbitrary + spec :: Spec spec = do parallel $ do diff --git a/test/ModelSpec.hs b/test/ModelSpec.hs index 936eccbbd..9c616915f 100644 --- a/test/ModelSpec.hs +++ b/test/ModelSpec.hs @@ -22,8 +22,6 @@ import Utils import System.FilePath import Data.Time -import Mail (MailLanguages(..)) - instance Arbitrary EmailAddress where arbitrary = do @@ -106,10 +104,7 @@ instance Arbitrary User where userDownloadFiles <- arbitrary userWarningDays <- arbitrary - userLanguages <- choose - [ pure Nothing - , fmap (Just . Languages) $ sublistOf =<< shuffle (toList appLanguages) - ] + userLanguages <- arbitrary userNotificationSettings <- arbitrary userCsvOptions <- arbitrary userShowSex <- arbitrary diff --git a/test/TestImport.hs b/test/TestImport.hs index 1b2704d88..46abcbe9a 100644 --- a/test/TestImport.hs +++ b/test/TestImport.hs @@ -138,7 +138,7 @@ createUser adjUser = do userDownloadFiles = userDefaultDownloadFiles userWarningDays = userDefaultWarningDays userShowSex = userDefaultShowSex - userMailLanguages = def + userLanguages = Nothing userNotificationSettings = def userCreated = now userLastLdapSynchronisation = Nothing From 3fe278ec3087b7cb219a94c3b9fe28c0f3e204fa Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Tue, 22 Oct 2019 11:04:34 +0200 Subject: [PATCH 012/126] fix(i18n): i18n for all widgets --- messages/uniworx/de-de-formal.msg | 25 +++++++- src/Handler/Admin.hs | 2 +- src/Handler/Corrections.hs | 10 ++-- src/Handler/Course/List.hs | 2 +- src/Handler/ExamOffice/Exam.hs | 2 +- src/Handler/Profile.hs | 1 + src/Handler/Users.hs | 2 +- src/Handler/Utils/Form.hs | 4 +- src/Handler/Utils/Table/Columns.hs | 6 +- templates/adminFeatures.hamlet | 23 ++++---- templates/adminUser.hamlet | 6 +- templates/deletedUser.hamlet | 10 ++-- .../admin-test/de-de-formal.hamlet} | 0 .../data-delete/de-de-formal.hamlet} | 0 .../i18n/profile-remarks/de-de-formal.hamlet | 25 ++++++++ .../messages/submissionFilesIgnored.hamlet | 2 +- templates/profileData.hamlet | 59 +++++-------------- templates/submission.hamlet | 9 ++- templates/widgets/asidenav/asidenav.hamlet | 3 +- templates/widgets/fields/bool.hamlet | 7 ++- templates/widgets/lipsum/lipsum.hamlet | 10 ---- 21 files changed, 112 insertions(+), 96 deletions(-) rename templates/{adminTest.hamlet => i18n/admin-test/de-de-formal.hamlet} (100%) rename templates/{widgets/data-delete/data-delete.hamlet => i18n/data-delete/de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/profile-remarks/de-de-formal.hamlet delete mode 100644 templates/widgets/lipsum/lipsum.hamlet diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 45a6a6e53..1ca265cc8 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1,5 +1,7 @@ PrintDebugForStupid name@Text: Debug message "#{name}" +Logo: Uni2work + BtnSubmit: Senden BtnAbort: Abbrechen BtnDelete: Löschen @@ -143,6 +145,7 @@ CourseDeregisterUntilTip: Abmeldung ist ab "Anmeldungen von" bis zu diesem Zeitp CourseFilterSearch: Volltext-Suche CourseFilterRegistered: Registriert CourseFilterNone: Egal +BoolIrrelevant: Egal CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen? CourseDeleted: Kurs gelöscht CourseUserTutorials: Angemeldete Tutorien @@ -610,6 +613,7 @@ RatingExceedsMax: Bewertung übersteigt die erlaubte Maximalpunktzahl RatingNotExpected: Keine Bewertungen erlaubt RatingBinaryExpected: Bewertung muss 0 (=durchgefallen) oder 1 (=bestanden) sein RatingPointsRequired: Bewertung erfordert für dieses Blatt eine Punktzahl +RatingFile: Bewertungsdatei SubmissionSinkExceptionDuplicateFileTitle file@FilePath: Dateiname #{show file} kommt mehrfach im Zip-Archiv vor SubmissionSinkExceptionDuplicateRating: Mehr als eine Bewertung gefunden. @@ -740,6 +744,8 @@ StudyTerms: Studiengänge StudyTerm: Studiengang NoStudyTermsKnown: Nicht eingeschrieben StudyFeatureInference: Studiengangschlüssel-Inferenz +StudyFeatureInferenceNoConflicts: Keine Konflikte beobachtet +StudyFeatureInferenceConflictsHeading: Studiengangseinträge mit beobachteten Konflikten StudyFeatureAge: Fachsemester StudyFeatureDegree: Abschluss FieldPrimary: Hauptfach @@ -958,6 +964,10 @@ SheetCreateExisting: Folgende Pseudonyme haben bereits abgegeben: CorrGrade: Korrekturen eintragen UserAccountDeleted name@Text: Konto für #{name} wurde gelöscht! +UserSubmissionsDeleted n@Int: #{tshow n} Abgaben wurden unwiderruflich gelöscht. +UserGroupSubmissionsKept n@Int: #{tshow n} Gruppenabgaben verbleiben in der Datenbank, aber die Zuordnung zum Benutzer wurde gelöscht. Gruppenabgaben können dadurch zu Einzelabgaben werden, die dann mit dem letzten Benutzer gelöscht werden. +UserSubmissionGroupsDeleted count@Int64: #{tshow count} benannte Abgabengruppen wurden gelöscht, da sie ohne den Nutzer leer wären. +UserAccountDeleteWarning: Achtung, dies löscht den kompletten Benutzer unwiderruflich und mit allen assoziierten Daten aus der Datenbank. Prüfungsdaten müssen jedoch langfristig gespeichert bleiben! HelpTitle : Hilfe HelpAnswer: Antworten an @@ -2037,4 +2047,17 @@ ShowSex: Geschlechter anderer Nutzer anzeigen ShowSexTip: Sollen in Kursteilnehmer-Tabellen u.Ä. die Geschlechter der Nutzer angezeigt werden? MenuLanguage: Sprache -LanguageChanged: Sprache erfolgreich geändert \ No newline at end of file +LanguageChanged: Sprache erfolgreich geändert + +ProfileCorrector: Korrektor +ProfileCourses: Eigene Kurse +ProfileCourseParticipations: Kursanmeldungen +ProfileCourseExamResults: Prüfungsleistungen +ProfileTutorials: Eigene Tutorien +ProfileTutorialParticipations: Tutorien +ProfileSubmissionGroups: Abgabegruppen +ProfileSubmissions: Abgaben +ProfileRemark: Hinweis +ProfileGroupSubmissionDates: Bei Gruppenabgaben wird kein Datum angezeigt, wenn Sie die Gruppenabgabe nie selbst hochgeladen haben. +ProfileCorrectorRemark: Die oberhalb angezeigte Tabelle zeigt nur prinzipielle Einteilungen als Korrektor zu einem Übungsblatt. Auch ohne Einteilung können Korrekturen einzeln zugewiesen werden, welche hier dann nicht aufgeführt werden. +ProfileCorrections: Auflistung aller zugewiesenen Korrekturen \ No newline at end of file diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index 307c7acf9..5bcf4359a 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -214,7 +214,7 @@ postAdminTestR = do siteLayout locallyDefinedPageHeading $ do -- defaultLayout $ do setTitle "Uni2work Admin Testpage" - $(widgetFile "adminTest") + $(i18nWidgetFile "admin-test") [whamlet|

    Formular Demonstration|] wrapForm formWidget FormSettings diff --git a/src/Handler/Corrections.hs b/src/Handler/Corrections.hs index 4d2c37aba..9b3937a5d 100644 --- a/src/Handler/Corrections.hs +++ b/src/Handler/Corrections.hs @@ -605,7 +605,7 @@ postCorrectionsR = do , prismAForm (singletonFilter "term" ) mPrev $ aopt (lift `hoistField` selectField termOptions) (fslI MsgTerm) , prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectField schoolOptions) (fslI MsgCourseSchool) , Map.singleton "sheet-search" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime) + , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime) ] courseOptions = runDB $ do courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) @@ -650,8 +650,8 @@ postCCorrectionsR tid ssh csh = do -- "pseudonym" TODO DB only stores Word24 , Map.singleton "sheet-search" . maybeToList <$> aopt textField (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) , prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector) - , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgHasCorrector) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime) + , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgHasCorrector) + , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime) ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway correctionsR whereClause colonnade filterUI psValidator $ Map.fromList @@ -681,8 +681,8 @@ postSSubsR tid ssh csh shn = do [ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgCourseMembers) , prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgMatrikelNr) , prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector) - , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgHasCorrector) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime) + , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgHasCorrector) + , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime) -- "pseudonym" TODO DB only stores Word24 ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway diff --git a/src/Handler/Course/List.hs b/src/Handler/Course/List.hs index 7e815fba2..a9075cee3 100644 --- a/src/Handler/Course/List.hs +++ b/src/Handler/Course/List.hs @@ -176,7 +176,7 @@ makeCourseTable whereClause colChoices psValidator = do , Just $ prismAForm (singletonFilter "lecturer") mPrev $ aopt textField (fslI MsgCourseLecturer) , Just $ prismAForm (singletonFilter "search") mPrev $ aopt textField (fslI MsgCourseFilterSearch) , Just $ prismAForm (singletonFilter "openregistration" . maybePrism _PathPiece) mPrev $ fmap (\x -> if isJust x && not (fromJust x) then Nothing else x) . aopt checkBoxField (fslI MsgCourseRegisterOpen) - , muid $> prismAForm (singletonFilter "registered" . maybePrism _PathPiece) mPrev (aopt boolField (fslI MsgCourseFilterRegistered)) + , muid $> prismAForm (singletonFilter "registered" . maybePrism _PathPiece) mPrev (aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgCourseFilterRegistered)) ] , dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout diff --git a/src/Handler/ExamOffice/Exam.hs b/src/Handler/ExamOffice/Exam.hs index 87f5bbf10..320822663 100644 --- a/src/Handler/ExamOffice/Exam.hs +++ b/src/Handler/ExamOffice/Exam.hs @@ -369,7 +369,7 @@ postEGradesR tid ssh csh examn = do , fltrStudyDegreeUI , fltrStudyFeaturesSemesterUI , fltrExamResultPointsUI examShowGrades - , \mPrev -> prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgExamUserSynchronised) + , \mPrev -> prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamUserSynchronised) ] dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } dbtParams = DBParamsForm diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs index 7e133098e..170ce5dc4 100644 --- a/src/Handler/Profile.hs +++ b/src/Handler/Profile.hs @@ -373,6 +373,7 @@ makeProfileData (Entity uid User{..}) = do let ownTutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|] let tutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|] lastLogin <- traverse (formatTime SelFormatDateTime) userLastAuthentication + let profileRemarks = $(i18nWidgetFile "profile-remarks") return $(widgetFile "profileData") diff --git a/src/Handler/Users.hs b/src/Handler/Users.hs index 2d405561e..e11f6a557 100644 --- a/src/Handler/Users.hs +++ b/src/Handler/Users.hs @@ -390,7 +390,7 @@ postAdminUserR uuid = do } userDataWidget <- runDB $ makeProfileData $ Entity uid user siteLayout heading $ do - let deleteWidget = $(widgetFile "widgets/data-delete/data-delete") + let deleteWidget = $(i18nWidgetFile "data-delete") $(widgetFile "adminUser") diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index 937d3eec3..73bcdb18e 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -925,8 +925,8 @@ jsonField hide = Field{..} boolField :: ( MonadHandler m , HandlerSite m ~ UniWorX ) - => Field m Bool -boolField = Field + => Maybe (SomeMessage UniWorX) -> Field m Bool +boolField mkNone = Field { fieldParse = \e _ -> return $ boolParser e , fieldView = \theId name attrs val isReq -> $(widgetFile "widgets/fields/bool") , fieldEnctype = UrlEncoded diff --git a/src/Handler/Utils/Table/Columns.hs b/src/Handler/Utils/Table/Columns.hs index a5a0f72e2..150b3ffcd 100644 --- a/src/Handler/Utils/Table/Columns.hs +++ b/src/Handler/Utils/Table/Columns.hs @@ -203,7 +203,7 @@ fltrAllocationActive cTime queryAllocation = singletonMap "active" . FilterColum E.&&. E.maybe E.true (\t -> E.val cTime E.<=. t) (allocation E.^. AllocationRegisterTo) fltrAllocationActiveUI :: DBFilterUI -fltrAllocationActiveUI mPrev = prismAForm (singletonFilter "active" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgAllocationActive) +fltrAllocationActiveUI mPrev = prismAForm (singletonFilter "active" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgAllocationActive) ----------- @@ -355,7 +355,7 @@ fltrApplicationVeto :: OpticFilterColumn t Bool fltrApplicationVeto queryVeto = singletonMap "veto" . FilterColumn . mkExactFilter $ view queryVeto fltrApplicationVetoUI :: DBFilterUI -fltrApplicationVetoUI mPrev = prismAForm (singletonFilter "veto" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgCourseApplicationVeto) +fltrApplicationVetoUI mPrev = prismAForm (singletonFilter "veto" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgCourseApplicationVeto) colApplicationRatingComment :: OpticColonnade (Maybe Text) colApplicationRatingComment resultComment = Colonnade.singleton (fromSortable header) body @@ -407,7 +407,7 @@ fltrApplicationFiles :: OpticFilterColumn t Bool fltrApplicationFiles queryFiles = singletonMap "has-files" . FilterColumn . mkExactFilter $ view queryFiles fltrApplicationFilesUI :: DBFilterUI -fltrApplicationFilesUI mPrev = prismAForm (singletonFilter "has-files" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgCourseApplicationFiles) +fltrApplicationFilesUI mPrev = prismAForm (singletonFilter "has-files" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgCourseApplicationFiles) --------------- -- Files diff --git a/templates/adminFeatures.hamlet b/templates/adminFeatures.hamlet index ea5e214b6..db3db4626 100644 --- a/templates/adminFeatures.hamlet +++ b/templates/adminFeatures.hamlet @@ -1,18 +1,19 @@ +$newline never
    ^{degreeTable}
    ^{studytermsTable}
    -

    _{MsgStudyFeatureInference} +

    _{MsgStudyFeatureInference} + $if null infConflicts

    - $if null infConflicts - Kein Konflikte beobachtet. - $else -

    Studiengangseingträge mit beobachteten Konflikten: -