Merge branch 'feat/timezones' into 'staging'

Split DateTimeFormat up into three fields

See merge request !47
This commit is contained in:
Gregor Kleen 2018-07-10 10:55:50 +02:00
commit e28e32a14f
11 changed files with 91 additions and 55 deletions

View File

@ -42,10 +42,9 @@ ldap:
default-favourites: 12 default-favourites: 12
default-theme: Default default-theme: Default
default-date-time-format: default-date-time-format: "%a %d %b %Y %R"
dateTimeFormat: "%a %d %b %Y %R" default-date-format: "%d.%m.%Y"
dateFormat: "%d.%m.%Y" default-time-format: "%R"
timeFormat: "%R"
cryptoid-keyfile: "_env:CRYPTOID_KEYFILE:cryptoid_key.bf" cryptoid-keyfile: "_env:CRYPTOID_KEYFILE:cryptoid_key.bf"

8
models
View File

@ -4,9 +4,11 @@ User json
matrikelnummer Text Maybe matrikelnummer Text Maybe
email Text email Text
displayName Text displayName Text
maxFavourites Int maxFavourites Int default=12
theme Theme theme Theme default='Default'
dateTimeFormat DateTimeFormat dateTimeFormat DateTimeFormat "default='%a %d %b %Y %R'"
dateFormat DateTimeFormat "default='%d.%m.%Y'"
timeFormat DateTimeFormat "default='%R'"
UniqueAuthentication plugin ident UniqueAuthentication plugin ident
UniqueEmail email UniqueEmail email
deriving Show deriving Show

View File

@ -1009,6 +1009,8 @@ instance YesodAuth UniWorX where
userMaxFavourites = appDefaultMaxFavourites userMaxFavourites = appDefaultMaxFavourites
userTheme = appDefaultTheme userTheme = appDefaultTheme
userDateTimeFormat = appDefaultDateTimeFormat userDateTimeFormat = appDefaultDateTimeFormat
userDateFormat = appDefaultDateFormat
userTimeFormat = appDefaultTimeFormat
newUser = User{..} newUser = User{..}
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
, UserDisplayName =. userDisplayName , UserDisplayName =. userDisplayName

View File

@ -62,8 +62,8 @@ getTermCourseListR tid = do
[ sortable (Just "shorthand") (textCell MsgCourse) $ anchorCell' [ sortable (Just "shorthand") (textCell MsgCourse) $ anchorCell'
(\(Entity _ Course{..}, _) -> CourseR courseTerm courseShorthand CShowR) (\(Entity _ Course{..}, _) -> CourseR courseTerm courseShorthand CShowR)
(\(Entity _ Course{..}, _) -> toWidget courseShorthand) (\(Entity _ Course{..}, _) -> toWidget courseShorthand)
, sortable (Just "register-from") (textCell MsgRegisterFrom) $ \(Entity _ Course{..}, _) -> cell $ traverse (formatTime dateTimeFormat) courseRegisterFrom >>= maybe mempty toWidget , sortable (Just "register-from") (textCell MsgRegisterFrom) $ \(Entity _ Course{..}, _) -> cell $ traverse (formatTime SelFormatDateTime) courseRegisterFrom >>= maybe mempty toWidget
, sortable (Just "register-to") (textCell MsgRegisterTo) $ \(Entity _ Course{..}, _) -> cell $ traverse (formatTime dateTimeFormat) courseRegisterTo >>= maybe mempty toWidget , sortable (Just "register-to") (textCell MsgRegisterTo) $ \(Entity _ Course{..}, _) -> cell $ traverse (formatTime SelFormatDateTime) courseRegisterTo >>= maybe mempty toWidget
, sortable (Just "members") (textCell MsgCourseMembers) $ \(Entity _ Course{..}, E.Value num) -> textCell $ case courseCapacity of , sortable (Just "members") (textCell MsgCourseMembers) $ \(Entity _ Course{..}, E.Value num) -> textCell $ case courseCapacity of
Nothing -> MsgCourseMembersCount num Nothing -> MsgCourseMembersCount num
Just max -> MsgCourseMembersCountLimited num max Just max -> MsgCourseMembersCountLimited num max
@ -110,8 +110,8 @@ getCShowR tid csh = do
let course = entityVal courseEnt let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerForm registered $ courseRegisterSecret course (regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerForm registered $ courseRegisterSecret course
registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid csh CRegisterR) True registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid csh CRegisterR) True
mRegFrom <- traverse (formatTime dateTimeFormat) $ courseRegisterFrom course mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course
mRegTo <- traverse (formatTime dateTimeFormat) $ courseRegisterTo course mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course
defaultLayout $ do defaultLayout $ do
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|] setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|]
$(widgetFile "course") $(widgetFile "course")

View File

@ -77,7 +77,7 @@ homeAnonymous = do
, sortable (Just "term") (textCell MsgTerm) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> , sortable (Just "term") (textCell MsgTerm) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } ->
textCell $ display $ courseTerm course textCell $ display $ courseTerm course
, sortable (Just "deadline") (textCell MsgRegisterTo) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> , sortable (Just "deadline") (textCell MsgRegisterTo) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } ->
cell $ traverse (formatTime dateTimeFormat) (courseRegisterTo course) >>= maybe mempty toWidget cell $ traverse (formatTime SelFormatDateTime) (courseRegisterTo course) >>= maybe mempty toWidget
] ]
courseTable <- dbTable def $ DBTable courseTable <- dbTable def $ DBTable
{ dbtSQLQuery = tableData { dbtSQLQuery = tableData
@ -154,7 +154,7 @@ homeUser uid = do
, sortable (Just "sheet") (textCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _, _) } -> , sortable (Just "sheet") (textCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _, _) } ->
cell [whamlet|<a href=@{CSheetR tid csh shn SShowR}>#{display shn}|] cell [whamlet|<a href=@{CSheetR tid csh shn SShowR}>#{display shn}|]
, sortable (Just "deadline") (textCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, E.Value deadline, _) } -> , sortable (Just "deadline") (textCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, E.Value deadline, _) } ->
cell $ formatTime dateTimeFormat deadline >>= toWidget cell $ formatTime SelFormatDateTime deadline >>= toWidget
, sortable (Just "done") (textCell MsgDone) $ \(DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _, E.Value mbsid) }) -> , sortable (Just "done") (textCell MsgDone) $ \(DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _, E.Value mbsid) }) ->
case mbsid of case mbsid of
Nothing -> mempty Nothing -> mempty

View File

@ -160,8 +160,8 @@ getSheetListR tid csh = do
let tid = courseTerm course let tid = courseTerm course
let colBase = mconcat let colBase = mconcat
[ headed "Blatt" $ \(sid,sheet,_) -> simpleLink (toWgt $ sheetName sheet) $ CSheetR tid csh (sheetName sheet) SShowR [ headed "Blatt" $ \(sid,sheet,_) -> simpleLink (toWgt $ sheetName sheet) $ CSheetR tid csh (sheetName sheet) SShowR
, headed "Abgabe ab" $ \(_,Sheet{..},_) -> formatTime dateTimeFormat sheetActiveFrom >>= toWidget , headed "Abgabe ab" $ \(_,Sheet{..},_) -> formatTime SelFormatDateTime sheetActiveFrom >>= toWidget
, headed "Abgabe bis" $ \(_,Sheet{..},_) -> formatTime dateTimeFormat sheetActiveTo >>= toWidget , headed "Abgabe bis" $ \(_,Sheet{..},_) -> formatTime SelFormatDateTime sheetActiveTo >>= toWidget
, headed "Bewertung" $ toWgt . display . sheetType . snd3 , headed "Bewertung" $ toWgt . display . sheetType . snd3
] ]
let colAdmin = mconcat -- only show edit button for allowed course assistants let colAdmin = mconcat -- only show edit button for allowed course assistants
@ -222,7 +222,7 @@ getSShowR tid csh shn = do
SheetExercise -> textCell $ display $ sheetActiveFrom sheet SheetExercise -> textCell $ display $ sheetActiveFrom sheet
SheetHint -> textCell $ display $ sheetHintFrom sheet SheetHint -> textCell $ display $ sheetHintFrom sheet
SheetSolution -> textCell $ display $ sheetSolutionFrom sheet SheetSolution -> textCell $ display $ sheetSolutionFrom sheet
, sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> cell $ formatTime dateTimeFormat (modified :: UTCTime) >>= toWidget , sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> cell $ formatTime SelFormatDateTime (modified :: UTCTime) >>= toWidget
] ]
fileTable <- dbTable def $ DBTable fileTable <- dbTable def $ DBTable
{ dbtSQLQuery = fileData { dbtSQLQuery = fileData
@ -244,8 +244,8 @@ getSShowR tid csh shn = do
} }
defaultLayout $ do defaultLayout $ do
setTitle $ toHtml $ T.append "Übung " $ sheetName sheet setTitle $ toHtml $ T.append "Übung " $ sheetName sheet
sheetFrom <- formatTime dateTimeFormat $ sheetActiveFrom sheet sheetFrom <- formatTime SelFormatDateTime $ sheetActiveFrom sheet
sheetTo <- formatTime dateTimeFormat $ sheetActiveTo sheet sheetTo <- formatTime SelFormatDateTime $ sheetActiveTo sheet
$(widgetFile "sheetShow") $(widgetFile "sheetShow")
getSFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent getSFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent

View File

@ -160,7 +160,7 @@ submissionHelper tid csh shn (SubmissionMode mcid) = do
E.orderBy [E.desc $ submissionEdit E.^. SubmissionEditTime] E.orderBy [E.desc $ submissionEdit E.^. SubmissionEditTime]
E.limit numberOfSubmissionEditDates E.limit numberOfSubmissionEditDates
return $ (user E.^. UserDisplayName, submissionEdit E.^. SubmissionEditTime) return $ (user E.^. UserDisplayName, submissionEdit E.^. SubmissionEditTime)
lastEdits <- forM lastEditValues $ \(E.Value name, E.Value time) -> (name, ) <$> formatTime dateTimeFormat time lastEdits <- forM lastEditValues $ \(E.Value name, E.Value time) -> (name, ) <$> formatTime SelFormatDateTime time
return (sheet,buddies,lastEdits) return (sheet,buddies,lastEdits)
let unpackZips = True -- undefined -- TODO let unpackZips = True -- undefined -- TODO
((res,formWidget), formEnctype) <- runFormPost $ makeSubmissionForm msmid unpackZips sheetGrouping $ map E.unValue buddies ((res,formWidget), formEnctype) <- runFormPost $ makeSubmissionForm msmid unpackZips sheetGrouping $ map E.unValue buddies

View File

@ -46,20 +46,20 @@ getTermShowR = do
anchorCell' (\(Entity tid _, _) -> TermCourseListR tid) anchorCell' (\(Entity tid _, _) -> TermCourseListR tid)
(\(Entity tid _, _) -> [whamlet|#{display tid}|]) (\(Entity tid _, _) -> [whamlet|#{display tid}|])
, sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) ->
cell $ formatTime dateFormat termLectureStart >>= toWidget cell $ formatTime SelFormatDate termLectureStart >>= toWidget
, sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
cell $ formatTime dateFormat termLectureEnd >>= toWidget cell $ formatTime SelFormatDate termLectureEnd >>= toWidget
, sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) -> , sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) ->
textCell $ (bool "" tickmark termActive :: Text) textCell $ (bool "" tickmark termActive :: Text)
, sortable Nothing "Kurse" $ \(_, E.Value numCourses) -> , sortable Nothing "Kurse" $ \(_, E.Value numCourses) ->
cell [whamlet|_{MsgNumCourses numCourses}|] cell [whamlet|_{MsgNumCourses numCourses}|]
, sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) -> , sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) ->
cell $ formatTime dateFormat termStart >>= toWidget cell $ formatTime SelFormatDate termStart >>= toWidget
, sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) -> , sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) ->
cell $ formatTime dateFormat termEnd >>= toWidget cell $ formatTime SelFormatDate termEnd >>= toWidget
, sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) -> , sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
cell $ do cell $ do
termHolidays' <- mapM (formatTime dateFormat) termHolidays termHolidays' <- mapM (formatTime SelFormatDate) termHolidays
[whamlet| [whamlet|
<ul .list--inline .list--comma-separated> <ul .list--inline .list--comma-separated>
$forall holiday <- termHolidays' $forall holiday <- termHolidays'

View File

@ -31,7 +31,7 @@ utcToLocalTime = TZ.utcToLocalTimeTZ appTZ
localTimeToUTC :: LocalTime -> LocalToUTCResult localTimeToUTC :: LocalTime -> LocalToUTCResult
localTimeToUTC = TZ.localTimeToUTCFull appTZ localTimeToUTC = TZ.localTimeToUTCFull appTZ
formatTime' :: (FormatTime t, MonadHandler m, HandlerSite m ~ UniWorX, IsString str) => String -> t -> m str formatTime' :: (FormatTime t, MonadHandler m, HandlerSite m ~ UniWorX) => String -> t -> m Text
formatTime' fmtStr t = fmap fromString $ Time.formatTime <$> getTimeLocale <*> pure fmtStr <*> pure t formatTime' fmtStr t = fmap fromString $ Time.formatTime <$> getTimeLocale <*> pure fmtStr <*> pure t
class FormatTime t => HasLocalTime t where class FormatTime t => HasLocalTime t where
@ -41,47 +41,81 @@ instance HasLocalTime LocalTime where
toLocalTime = id toLocalTime = id
instance HasLocalTime Day where instance HasLocalTime Day where
toLocalTime d = toLocalTime $ UTCTime d 0 toLocalTime d = LocalTime d midnight
instance HasLocalTime UTCTime where instance HasLocalTime UTCTime where
toLocalTime t = utcToLocalTime t toLocalTime t = utcToLocalTime t
-- formatTime :: (FormatTime t, MonadHandler m, HandlerSite m ~ UniWorX, IsString str) => (DateTimeFormat -> String) -> t -> m str -- formatTime :: (FormatTime t, MonadHandler m, HandlerSite m ~ UniWorX, IsString str) => (DateTimeFormat -> String) -> t -> m str
-- Restricted type for safety -- Restricted type for safety
formatTime :: (HasLocalTime t, MonadHandler m, HandlerSite m ~ UniWorX) => (DateTimeFormat -> String) -> t -> m Text formatTime :: (HasLocalTime t, MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> t -> m Text
formatTime proj t = flip formatTime' (toLocalTime t) =<< (proj <$> getDateTimeFormat) formatTime proj t = flip formatTime' (toLocalTime t) =<< (unDateTimeFormat <$> getDateTimeFormat proj)
getTimeLocale :: (MonadHandler m, HandlerSite m ~ UniWorX) => m TimeLocale getTimeLocale :: (MonadHandler m, HandlerSite m ~ UniWorX) => m TimeLocale
getTimeLocale = getTimeLocale' <$> languages getTimeLocale = getTimeLocale' <$> languages
getDateTimeFormat :: (MonadHandler m, HandlerSite m ~ UniWorX) => m DateTimeFormat getDateTimeFormat :: (MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> m DateTimeFormat
getDateTimeFormat = do getDateTimeFormat sel = do
mauth <- liftHandlerT maybeAuth mauth <- liftHandlerT maybeAuth
AppSettings{..} <- getsYesod appSettings
let let
fmt fmt
| Just (Entity _ User{..}) <- mauth | Just (Entity _ User{..}) <- mauth
= userDateTimeFormat = case sel of
SelFormatDateTime -> userDateTimeFormat
SelFormatDate -> userDateFormat
SelFormatTime -> userTimeFormat
| otherwise | otherwise
= def = case sel of
SelFormatDateTime -> appDefaultDateTimeFormat
SelFormatDate -> appDefaultDateFormat
SelFormatTime -> appDefaultTimeFormat
return fmt return fmt
validDateTimeFormats :: Set DateTimeFormat validDateTimeFormats :: TimeLocale -> SelDateTimeFormat -> Set DateTimeFormat
validDateTimeFormats = Set.fromList $ -- ^ We use a whitelist instead of just letting the user specify their own format string since vulnerabilities in printf-like functions are not uncommon
[ DateTimeFormat "%a %d %b %Y %R" "%d.%m.%Y" "%R" validDateTimeFormats _ SelFormatDateTime = Set.fromList $
, DateTimeFormat "%a %d %b %Y %T" "%d.%m.%Y" "%T" [ DateTimeFormat "%a %d %b %Y %R"
, DateTimeFormat "%a %d %b %Y %R" "%Y-%m-%d" "%R" , DateTimeFormat "%A, %d %B %Y %R"
, DateTimeFormat "%a %d %b %Y %T" "%Y-%m-%d" "%T" , DateTimeFormat "%a %d %b %Y %T"
, DateTimeFormat "%A, %d %B %Y %T"
, DateTimeFormat "%d.%m.%Y %R"
, DateTimeFormat "%d.%m.%Y %T"
, DateTimeFormat "%R %d.%m.%Y"
, DateTimeFormat "%T %d.%m.%Y"
, DateTimeFormat "%Y-%m-%d %R"
, DateTimeFormat "%Y-%m-%d %T"
, DateTimeFormat "%Y-%m-%dT%T"
]
validDateTimeFormats _ SelFormatDate = Set.fromList $
[ DateTimeFormat "%a %d %b %Y"
, DateTimeFormat "%A, %d %B %Y"
, DateTimeFormat "%d.%m.%Y"
, DateTimeFormat "%Y-%m-%d"
]
validDateTimeFormats TimeLocale{..} SelFormatTime = Set.fromList . concat . catMaybes $
[ Just
[ DateTimeFormat "%R"
, DateTimeFormat "%T"
]
, do
guard $ uncurry (/=) amPm
Just
[ DateTimeFormat "%I:%M %p"
, DateTimeFormat "%I:%M %P"
, DateTimeFormat "%I:%M:%S %p"
, DateTimeFormat "%I:%M:%S %P"
]
] ]
dateTimeFormatOptions :: (MonadHandler m, HandlerSite m ~ UniWorX) => m (OptionList DateTimeFormat) dateTimeFormatOptions :: (MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> m (OptionList DateTimeFormat)
dateTimeFormatOptions = do dateTimeFormatOptions sel = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
tl <- getTimeLocale
let let
toOption fmt@DateTimeFormat{..} = do toOption fmt@DateTimeFormat{..} = do
dateTime <- formatTime' dateTimeFormat now dateTime <- formatTime' unDateTimeFormat now
date <- formatTime' dateFormat now return $ (dateTime, fmt)
time <- formatTime' timeFormat now
return $ (MsgDateTimeFormatOption dateTime date time, fmt)
optionsPairs <=< mapM toOption $ Set.toList validDateTimeFormats optionsPairs <=< mapM toOption . Set.toList $ validDateTimeFormats tl sel

View File

@ -3,7 +3,7 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric, DeriveDataTypeable, GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-- # LANGUAGE ExistentialQuantification #-} -- for DA type {-- # LANGUAGE ExistentialQuantification #-} -- for DA type
@ -38,8 +38,6 @@ import Data.Aeson.TH (deriveJSON, defaultOptions)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import Data.Default
type Points = Centi type Points = Centi
@ -297,11 +295,8 @@ instance PathPiece obj => PathPiece (ZIPArchiveName obj) where
toPathPiece (ZIPArchiveName obj) = toPathPiece obj <> ".zip" toPathPiece (ZIPArchiveName obj) = toPathPiece obj <> ".zip"
data DateTimeFormat = DateTimeFormat newtype DateTimeFormat = DateTimeFormat { unDateTimeFormat :: String }
{ dateTimeFormat, dateFormat, timeFormat :: String } deriving (Eq, Ord, Read, Show, ToJSON, FromJSON, PersistField, PersistFieldSql)
deriving (Eq, Ord, Read, Show)
$(deriveJSON defaultOptions ''DateTimeFormat)
derivePersistFieldJSON "DateTimeFormat"
instance Default DateTimeFormat where data SelDateTimeFormat = SelFormatDateTime | SelFormatDate | SelFormatTime
def = DateTimeFormat "%a %d %b %Y %R" "%d.%m.%Y" "%R" deriving (Eq, Ord, Read, Show, Enum, Bounded)

View File

@ -64,6 +64,8 @@ data AppSettings = AppSettings
, appDefaultTheme :: Theme , appDefaultTheme :: Theme
, appDefaultMaxFavourites :: Int , appDefaultMaxFavourites :: Int
, appDefaultDateTimeFormat :: DateTimeFormat , appDefaultDateTimeFormat :: DateTimeFormat
, appDefaultDateFormat :: DateTimeFormat
, appDefaultTimeFormat :: DateTimeFormat
-- Example app-specific configuration values. -- Example app-specific configuration values.
, appCopyright :: Text , appCopyright :: Text
@ -108,6 +110,8 @@ instance FromJSON AppSettings where
appDefaultMaxFavourites <- o .: "default-favourites" appDefaultMaxFavourites <- o .: "default-favourites"
appDefaultTheme <- o .: "default-theme" appDefaultTheme <- o .: "default-theme"
appDefaultDateTimeFormat <- o .: "default-date-time-format" appDefaultDateTimeFormat <- o .: "default-date-time-format"
appDefaultDateFormat <- o .: "default-date-format"
appDefaultTimeFormat <- o .: "default-time-format"
appCopyright <- o .: "copyright" appCopyright <- o .: "copyright"
appAnalytics <- o .:? "analytics" appAnalytics <- o .:? "analytics"