fix(html-field): introduce stored-markup
BREAKING CHANGE: StoredMarkup
This commit is contained in:
parent
549b95882d
commit
e25e8a2f4c
@ -3,8 +3,8 @@ Allocation -- attributes with prefix staff- affect lecturers only, but are invis
|
|||||||
school SchoolId -- school that manages this central allocation, not necessarily school of courses
|
school SchoolId -- school that manages this central allocation, not necessarily school of courses
|
||||||
shorthand AllocationShorthand -- practical shorthand
|
shorthand AllocationShorthand -- practical shorthand
|
||||||
name AllocationName
|
name AllocationName
|
||||||
description Html Maybe -- description for prospective students
|
description StoredMarkup Maybe -- description for prospective students
|
||||||
staffDescription Html Maybe -- description seen by prospective lecturers only
|
staffDescription StoredMarkup Maybe -- description seen by prospective lecturers only
|
||||||
staffRegisterFrom UTCTime Maybe -- lectureres may register courses
|
staffRegisterFrom UTCTime Maybe -- lectureres may register courses
|
||||||
staffRegisterTo UTCTime Maybe -- course registration stops
|
staffRegisterTo UTCTime Maybe -- course registration stops
|
||||||
-- staffDeregisterUntil not needed: staff may make arbitrary changes until staffRegisterTo, always frozen afterwards
|
-- staffDeregisterUntil not needed: staff may make arbitrary changes until staffRegisterTo, always frozen afterwards
|
||||||
|
|||||||
@ -5,7 +5,7 @@ DegreeCourse json -- for which degree programmes this course is appropriate fo
|
|||||||
UniqueDegreeCourse course degree terms
|
UniqueDegreeCourse course degree terms
|
||||||
Course -- Information about a single course; contained info is always visible to all users
|
Course -- Information about a single course; contained info is always visible to all users
|
||||||
name (CI Text)
|
name (CI Text)
|
||||||
description Html Maybe -- user-defined large Html, ought to contain module description
|
description StoredMarkup Maybe -- user-defined large Html, ought to contain module description
|
||||||
linkExternal Text Maybe -- arbitrary user-defined url for external course page
|
linkExternal Text Maybe -- arbitrary user-defined url for external course page
|
||||||
shorthand (CI Text) -- practical shorthand of course name, used for identification
|
shorthand (CI Text) -- practical shorthand of course name, used for identification
|
||||||
term TermId -- semester this course is taught
|
term TermId -- semester this course is taught
|
||||||
@ -21,7 +21,7 @@ Course -- Information about a single course; contained info is always visible
|
|||||||
registerSecret Text Maybe -- enrolement maybe protected by a simple common passphrase
|
registerSecret Text Maybe -- enrolement maybe protected by a simple common passphrase
|
||||||
materialFree Bool -- False: only enrolled users may see course materials not stored in this table
|
materialFree Bool -- False: only enrolled users may see course materials not stored in this table
|
||||||
applicationsRequired Bool default=false
|
applicationsRequired Bool default=false
|
||||||
applicationsInstructions Html Maybe
|
applicationsInstructions StoredMarkup Maybe
|
||||||
applicationsText Bool default=false
|
applicationsText Bool default=false
|
||||||
applicationsFiles UploadMode "default='{\"mode\": \"no-upload\"}'::jsonb"
|
applicationsFiles UploadMode "default='{\"mode\": \"no-upload\"}'::jsonb"
|
||||||
applicationsRatingsVisible Bool default=false
|
applicationsRatingsVisible Bool default=false
|
||||||
@ -33,7 +33,7 @@ CourseEvent
|
|||||||
course CourseId
|
course CourseId
|
||||||
room Text
|
room Text
|
||||||
time Occurrences
|
time Occurrences
|
||||||
note Html Maybe
|
note StoredMarkup Maybe
|
||||||
lastChanged UTCTime default=now()
|
lastChanged UTCTime default=now()
|
||||||
|
|
||||||
CourseAppInstructionFile
|
CourseAppInstructionFile
|
||||||
@ -72,7 +72,7 @@ CourseParticipant -- course enrolement
|
|||||||
CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student
|
CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student
|
||||||
course CourseId
|
course CourseId
|
||||||
user UserId
|
user UserId
|
||||||
note Html -- arbitrary user-defined text; visible only to lecturer of this course
|
note StoredMarkup -- arbitrary user-defined text; visible only to lecturer of this course
|
||||||
UniqueCourseUserNote user course
|
UniqueCourseUserNote user course
|
||||||
CourseUserNoteEdit -- who edited a participants course note when
|
CourseUserNoteEdit -- who edited a participants course note when
|
||||||
user UserId
|
user UserId
|
||||||
|
|||||||
@ -2,7 +2,7 @@ Material -- course material for disemination to course participants
|
|||||||
course CourseId
|
course CourseId
|
||||||
name (CI Text)
|
name (CI Text)
|
||||||
type (CI Text) Maybe
|
type (CI Text) Maybe
|
||||||
description Html Maybe
|
description StoredMarkup Maybe
|
||||||
visibleFrom UTCTime Maybe -- Invisible to enrolled participants before
|
visibleFrom UTCTime Maybe -- Invisible to enrolled participants before
|
||||||
lastEdit UTCTime
|
lastEdit UTCTime
|
||||||
UniqueMaterial course name
|
UniqueMaterial course name
|
||||||
|
|||||||
@ -3,8 +3,8 @@ CourseNews
|
|||||||
visibleFrom UTCTime Maybe
|
visibleFrom UTCTime Maybe
|
||||||
participantsOnly Bool
|
participantsOnly Bool
|
||||||
title Text Maybe
|
title Text Maybe
|
||||||
content Html
|
content StoredMarkup
|
||||||
summary Html Maybe
|
summary StoredMarkup Maybe
|
||||||
lastEdit UTCTime
|
lastEdit UTCTime
|
||||||
CourseNewsFile
|
CourseNewsFile
|
||||||
news CourseNewsId
|
news CourseNewsId
|
||||||
|
|||||||
@ -16,7 +16,7 @@ Exam
|
|||||||
closed UTCTime Maybe -- Prüfungsamt hat Einsicht (notification)
|
closed UTCTime Maybe -- Prüfungsamt hat Einsicht (notification)
|
||||||
publicStatistics Bool
|
publicStatistics Bool
|
||||||
gradingMode ExamGradingMode
|
gradingMode ExamGradingMode
|
||||||
description Html Maybe
|
description StoredMarkup Maybe
|
||||||
examMode ExamMode
|
examMode ExamMode
|
||||||
staff Text Maybe
|
staff Text Maybe
|
||||||
UniqueExam course name
|
UniqueExam course name
|
||||||
@ -35,7 +35,7 @@ ExamOccurrence
|
|||||||
capacity Natural
|
capacity Natural
|
||||||
start UTCTime
|
start UTCTime
|
||||||
end UTCTime Maybe
|
end UTCTime Maybe
|
||||||
description Html Maybe
|
description StoredMarkup Maybe
|
||||||
UniqueExamOccurrence exam name
|
UniqueExamOccurrence exam name
|
||||||
ExamRegistration
|
ExamRegistration
|
||||||
exam ExamId
|
exam ExamId
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
Sheet -- exercise sheet for a given course
|
Sheet -- exercise sheet for a given course
|
||||||
course CourseId
|
course CourseId
|
||||||
name (CI Text)
|
name (CI Text)
|
||||||
description Html Maybe
|
description StoredMarkup Maybe
|
||||||
type SheetType -- Does it count towards overall course grade?
|
type SheetType -- Does it count towards overall course grade?
|
||||||
grouping SheetGroup -- May participants submit in groups of certain sizes?
|
grouping SheetGroup -- May participants submit in groups of certain sizes?
|
||||||
markingText Html Maybe -- Instructons for correctors, included in marking templates
|
markingText StoredMarkup Maybe -- Instructons for correctors, included in marking templates
|
||||||
visibleFrom UTCTime Maybe -- Invisible to enrolled participants before
|
visibleFrom UTCTime Maybe -- Invisible to enrolled participants before
|
||||||
activeFrom UTCTime Maybe -- Download of questions and submission is permitted afterwards
|
activeFrom UTCTime Maybe -- Download of questions and submission is permitted afterwards
|
||||||
activeTo UTCTime Maybe -- Submission is only permitted before
|
activeTo UTCTime Maybe -- Submission is only permitted before
|
||||||
|
|||||||
@ -11,14 +11,14 @@ SystemMessage
|
|||||||
lastChanged UTCTime default=now()
|
lastChanged UTCTime default=now()
|
||||||
lastUnhide UTCTime default=now()
|
lastUnhide UTCTime default=now()
|
||||||
defaultLanguage Lang -- Language of @content@ and @summary@
|
defaultLanguage Lang -- Language of @content@ and @summary@
|
||||||
content Html -- Detailed message shown when clicking on the @summary@-popup or when no @summary@ is specified
|
content StoredMarkup -- Detailed message shown when clicking on the @summary@-popup or when no @summary@ is specified
|
||||||
summary Html Maybe
|
summary StoredMarkup Maybe
|
||||||
|
|
||||||
SystemMessageTranslation -- Translation of a @SystemMessage@ into another language; which language to choose is determined by user-sent HTTP-headers
|
SystemMessageTranslation -- Translation of a @SystemMessage@ into another language; which language to choose is determined by user-sent HTTP-headers
|
||||||
message SystemMessageId
|
message SystemMessageId
|
||||||
language Lang
|
language Lang
|
||||||
content Html
|
content StoredMarkup
|
||||||
summary Html Maybe
|
summary StoredMarkup Maybe
|
||||||
UniqueSystemMessageTranslation message language
|
UniqueSystemMessageTranslation message language
|
||||||
|
|
||||||
SystemMessageHidden
|
SystemMessageHidden
|
||||||
|
|||||||
@ -25,7 +25,7 @@ module Database.Esqueleto.Utils
|
|||||||
, max, min
|
, max, min
|
||||||
, abs
|
, abs
|
||||||
, SqlProject(..)
|
, SqlProject(..)
|
||||||
, (->.)
|
, (->.), (#>>.)
|
||||||
, fromSqlKey
|
, fromSqlKey
|
||||||
, selectCountRows
|
, selectCountRows
|
||||||
, selectMaybe
|
, selectMaybe
|
||||||
@ -367,6 +367,12 @@ infixl 8 ->.
|
|||||||
(->.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value b)
|
(->.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value b)
|
||||||
(->.) expr t = E.unsafeSqlBinOp "->" expr $ E.val t
|
(->.) expr t = E.unsafeSqlBinOp "->" expr $ E.val t
|
||||||
|
|
||||||
|
infixl 8 #>>.
|
||||||
|
|
||||||
|
(#>>.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value (Maybe Text))
|
||||||
|
(#>>.) expr t = E.unsafeSqlBinOp "#>>" expr $ E.val t
|
||||||
|
|
||||||
|
|
||||||
fromSqlKey :: (ToBackendKey SqlBackend entity, PersistField (Key entity)) => E.SqlExpr (E.Value (Key entity)) -> E.SqlExpr (E.Value Int64)
|
fromSqlKey :: (ToBackendKey SqlBackend entity, PersistField (Key entity)) => E.SqlExpr (E.Value (Key entity)) -> E.SqlExpr (E.Value Int64)
|
||||||
fromSqlKey = E.veryUnsafeCoerceSqlExprValue
|
fromSqlKey = E.veryUnsafeCoerceSqlExprValue
|
||||||
|
|
||||||
|
|||||||
@ -454,7 +454,7 @@ applySystemMessages = liftHandler . maybeT_ . catchMPlus (Proxy @CryptoIDError)
|
|||||||
case summary of
|
case summary of
|
||||||
Just s ->
|
Just s ->
|
||||||
addMessageWidget systemMessageSeverity $ msgModal (toWidget s) (Left . SomeRoute $ MessageR cID)
|
addMessageWidget systemMessageSeverity $ msgModal (toWidget s) (Left . SomeRoute $ MessageR cID)
|
||||||
Nothing -> addMessage systemMessageSeverity content
|
Nothing -> addMessage systemMessageSeverity $ toHtml content
|
||||||
|
|
||||||
tellRegisteredCookieJson CookieSystemMessageState . MergeHashMap $
|
tellRegisteredCookieJson CookieSystemMessageState . MergeHashMap $
|
||||||
HashMap.singleton cID mempty{ userSystemMessageShown = Just now }
|
HashMap.singleton cID mempty{ userSystemMessageShown = Just now }
|
||||||
|
|||||||
@ -34,14 +34,14 @@ data CourseForm = CourseForm
|
|||||||
, cfShort :: CourseShorthand
|
, cfShort :: CourseShorthand
|
||||||
, cfSchool :: SchoolId
|
, cfSchool :: SchoolId
|
||||||
, cfTerm :: TermId
|
, cfTerm :: TermId
|
||||||
, cfDesc :: Maybe Html
|
, cfDesc :: Maybe StoredMarkup
|
||||||
, cfLink :: Maybe Text
|
, cfLink :: Maybe Text
|
||||||
, cfVisFrom :: Maybe UTCTime
|
, cfVisFrom :: Maybe UTCTime
|
||||||
, cfVisTo :: Maybe UTCTime
|
, cfVisTo :: Maybe UTCTime
|
||||||
, cfMatFree :: Bool
|
, cfMatFree :: Bool
|
||||||
, cfAllocation :: Maybe AllocationCourseForm
|
, cfAllocation :: Maybe AllocationCourseForm
|
||||||
, cfAppRequired :: Bool
|
, cfAppRequired :: Bool
|
||||||
, cfAppInstructions :: Maybe Html
|
, cfAppInstructions :: Maybe StoredMarkup
|
||||||
, cfAppInstructionFiles :: Maybe FileUploads
|
, cfAppInstructionFiles :: Maybe FileUploads
|
||||||
, cfAppText :: Bool
|
, cfAppText :: Bool
|
||||||
, cfAppFiles :: UploadMode
|
, cfAppFiles :: UploadMode
|
||||||
|
|||||||
@ -15,7 +15,7 @@ data CourseEventForm = CourseEventForm
|
|||||||
{ cefType :: CI Text
|
{ cefType :: CI Text
|
||||||
, cefRoom :: Text
|
, cefRoom :: Text
|
||||||
, cefTime :: Occurrences
|
, cefTime :: Occurrences
|
||||||
, cefNote :: Maybe Html
|
, cefNote :: Maybe StoredMarkup
|
||||||
}
|
}
|
||||||
|
|
||||||
courseEventForm :: Maybe CourseEventForm -> Form CourseEventForm
|
courseEventForm :: Maybe CourseEventForm -> Form CourseEventForm
|
||||||
|
|||||||
@ -81,7 +81,12 @@ makeCourseTable whereClause colChoices psValidator = do
|
|||||||
return (course, participants, registered, school)
|
return (course, participants, registered, school)
|
||||||
lecturerQuery cid (user `E.InnerJoin` lecturer) = do
|
lecturerQuery cid (user `E.InnerJoin` lecturer) = do
|
||||||
E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
|
E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
|
||||||
E.where_ $ cid E.==. lecturer E.^. LecturerCourse E.&&. lecturer E.^. LecturerType E.==. E.val CourseLecturer
|
E.where_ $ cid E.==. lecturer E.^. LecturerCourse
|
||||||
|
E.&&. lecturer E.^. LecturerType E.==. E.val CourseLecturer
|
||||||
|
return user
|
||||||
|
isCourseAdminQuery cid (user `E.InnerJoin` lecturer) = do
|
||||||
|
E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
|
||||||
|
E.where_ $ cid E.==. lecturer E.^. LecturerCourse
|
||||||
return user
|
return user
|
||||||
isEditorQuery course user = E.where_ $ mayEditCourse' muid ata course
|
isEditorQuery course user = E.where_ $ mayEditCourse' muid ata course
|
||||||
E.&&. E.just (user E.^. UserId) E.==. E.val muid
|
E.&&. E.just (user E.^. UserId) E.==. E.val muid
|
||||||
@ -135,8 +140,8 @@ makeCourseTable whereClause colChoices psValidator = do
|
|||||||
, ( "lecturer", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterias -> if
|
, ( "lecturer", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterias -> if
|
||||||
| Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool)
|
| Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
| otherwise -> E.exists $ E.from $ \t -> do
|
| otherwise -> E.exists $ E.from $ \t -> do
|
||||||
user <- lecturerQuery (course E.^. CourseId) t
|
user <- isCourseAdminQuery (course E.^. CourseId) t
|
||||||
E.where_ $ E.any (E.hasInfix (user E.^. UserSurname) . E.val) (criterias :: Set.Set Text)
|
E.where_ $ E.any (E.hasInfix (user E.^. UserDisplayName) . E.val) (criterias :: Set.Set Text)
|
||||||
)
|
)
|
||||||
, ( "openregistration", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterion -> case getLast (criterion :: Last Bool) of
|
, ( "openregistration", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterion -> case getLast (criterion :: Last Bool) of
|
||||||
Nothing -> E.val True
|
Nothing -> E.val True
|
||||||
@ -167,7 +172,7 @@ makeCourseTable whereClause colChoices psValidator = do
|
|||||||
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
|
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
Just needle -> (E.castString (course E.^. CourseName) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
|
Just needle -> (E.castString (course E.^. CourseName) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
|
||||||
E.||. (E.castString (course E.^. CourseShorthand) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
|
E.||. (E.castString (course E.^. CourseShorthand) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
|
||||||
E.||. (E.castString (course E.^. CourseDescription) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
|
E.||. (E.maybe (E.val mempty) (E.castString . esqueletoMarkupOutput) (course E.^. CourseDescription) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
, dbtFilterUI = \mPrev -> mconcat $ catMaybes
|
, dbtFilterUI = \mPrev -> mconcat $ catMaybes
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import qualified Data.Conduit.List as C
|
|||||||
|
|
||||||
data CourseNewsForm = CourseNewsForm
|
data CourseNewsForm = CourseNewsForm
|
||||||
{ cnfTitle :: Maybe Text
|
{ cnfTitle :: Maybe Text
|
||||||
, cnfSummary :: Maybe Html
|
, cnfSummary :: Maybe StoredMarkup
|
||||||
, cnfContent :: Html
|
, cnfContent :: StoredMarkup
|
||||||
, cnfParticipantsOnly :: Bool
|
, cnfParticipantsOnly :: Bool
|
||||||
, cnfVisibleFrom :: Maybe UTCTime
|
, cnfVisibleFrom :: Maybe UTCTime
|
||||||
, cnfFiles :: Maybe FileUploads
|
, cnfFiles :: Maybe FileUploads
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import qualified Data.Text as Text
|
|||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
import qualified Data.Conduit.Combinators as C
|
import qualified Data.Conduit.Combinators as C
|
||||||
|
import qualified Data.Text.Lazy as LT
|
||||||
|
|
||||||
|
|
||||||
data ExamAction = ExamDeregister
|
data ExamAction = ExamDeregister
|
||||||
@ -226,7 +227,7 @@ courseUserNoteSection (Entity cid Course{..}) (Entity uid _) = do
|
|||||||
maybeM (return ()) (\nk -> deleteWhere [CourseUserNoteEditNote ==. nk]) (getKeyBy thisUniqueNote)
|
maybeM (return ()) (\nk -> deleteWhere [CourseUserNoteEditNote ==. nk]) (getKeyBy thisUniqueNote)
|
||||||
deleteBy thisUniqueNote
|
deleteBy thisUniqueNote
|
||||||
addMessageI Info MsgCourseUserNoteDeleted
|
addMessageI Info MsgCourseUserNoteDeleted
|
||||||
_ | (renderHtml <$> mbNote) == (renderHtml <$> noteText) -> return () -- no changes
|
_ | ((==) `on` fmap (LT.strip . renderHtml . markupOutput)) mbNote noteText -> return () -- no changes
|
||||||
(Just note) -> do
|
(Just note) -> do
|
||||||
dozentId <- requireAuthId
|
dozentId <- requireAuthId
|
||||||
(Entity noteKey _) <- upsertBy thisUniqueNote (CourseUserNote cid uid note) [CourseUserNoteNote =. note]
|
(Entity noteKey _) <- upsertBy thisUniqueNote (CourseUserNote cid uid note) [CourseUserNoteNote =. note]
|
||||||
|
|||||||
@ -174,7 +174,7 @@ data UserTableCsv = UserTableCsv
|
|||||||
, csvUserStudyFeatures :: UserTableStudyFeatures
|
, csvUserStudyFeatures :: UserTableStudyFeatures
|
||||||
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
|
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
|
||||||
, csvUserRegistration :: UTCTime
|
, csvUserRegistration :: UTCTime
|
||||||
, csvUserNote :: Maybe Html
|
, csvUserNote :: Maybe StoredMarkup
|
||||||
, csvUserTutorials :: ([TutorialName], Map (CI Text) (Maybe TutorialName))
|
, csvUserTutorials :: ([TutorialName], Map (CI Text) (Maybe TutorialName))
|
||||||
, csvUserExams :: [ExamName]
|
, csvUserExams :: [ExamName]
|
||||||
, csvUserSheets :: Map SheetName (SheetType, Maybe Points)
|
, csvUserSheets :: Map SheetName (SheetType, Maybe Points)
|
||||||
|
|||||||
@ -22,12 +22,14 @@ import qualified Database.Esqueleto as E
|
|||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
import qualified Control.Monad.State.Class as State
|
import qualified Control.Monad.State.Class as State
|
||||||
import Text.Blaze.Html.Renderer.String (renderHtml)
|
import Text.Blaze.Html.Renderer.Text (renderHtml)
|
||||||
|
|
||||||
|
import qualified Data.Text.Lazy as LT
|
||||||
|
|
||||||
|
|
||||||
data ExamForm = ExamForm
|
data ExamForm = ExamForm
|
||||||
{ efName :: ExamName
|
{ efName :: ExamName
|
||||||
, efDescription :: Maybe Html
|
, efDescription :: Maybe StoredMarkup
|
||||||
, efStart :: Maybe UTCTime
|
, efStart :: Maybe UTCTime
|
||||||
, efEnd :: Maybe UTCTime
|
, efEnd :: Maybe UTCTime
|
||||||
, efVisibleFrom :: Maybe UTCTime
|
, efVisibleFrom :: Maybe UTCTime
|
||||||
@ -56,7 +58,7 @@ data ExamOccurrenceForm = ExamOccurrenceForm
|
|||||||
, eofCapacity :: Natural
|
, eofCapacity :: Natural
|
||||||
, eofStart :: UTCTime
|
, eofStart :: UTCTime
|
||||||
, eofEnd :: Maybe UTCTime
|
, eofEnd :: Maybe UTCTime
|
||||||
, eofDescription :: Maybe Html
|
, eofDescription :: Maybe StoredMarkup
|
||||||
} deriving (Read, Show, Eq, Generic, Typeable)
|
} deriving (Read, Show, Eq, Generic, Typeable)
|
||||||
|
|
||||||
instance Ord ExamOccurrenceForm where
|
instance Ord ExamOccurrenceForm where
|
||||||
@ -231,7 +233,7 @@ examOccurrenceForm prev = wFormToAForm $ do
|
|||||||
<*> eofCapacityRes
|
<*> eofCapacityRes
|
||||||
<*> eofStartRes
|
<*> eofStartRes
|
||||||
<*> eofEndRes
|
<*> eofEndRes
|
||||||
<*> (assertM (not . null . renderHtml) <$> eofDescRes)
|
<*> eofDescRes
|
||||||
, $(widgetFile "widgets/massinput/examRooms/form")
|
, $(widgetFile "widgets/massinput/examRooms/form")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -430,7 +432,7 @@ validateExam cId oldExam = do
|
|||||||
[ (/=) `on` eofRoom
|
[ (/=) `on` eofRoom
|
||||||
, (/=) `on` eofStart
|
, (/=) `on` eofStart
|
||||||
, (/=) `on` eofEnd
|
, (/=) `on` eofEnd
|
||||||
, (/=) `on` fmap renderHtml . eofDescription
|
, (/=) `on` fmap (LT.strip . renderHtml . markupOutput) . eofDescription
|
||||||
]
|
]
|
||||||
|
|
||||||
guardValidation (MsgExamOccurrenceDuplicateName $ eofName a) $ ((/=) `on` eofName) a b
|
guardValidation (MsgExamOccurrenceDuplicateName $ eofName a) $ ((/=) `on` eofName) a b
|
||||||
|
|||||||
@ -181,7 +181,7 @@ data ExamUserTableCsv = ExamUserTableCsv
|
|||||||
, csvEUserBonus :: Maybe (Maybe Points)
|
, csvEUserBonus :: Maybe (Maybe Points)
|
||||||
, csvEUserExamPartResults :: Map ExamPartNumber (Maybe ExamResultPoints)
|
, csvEUserExamPartResults :: Map ExamPartNumber (Maybe ExamResultPoints)
|
||||||
, csvEUserExamResult :: Maybe ExamResultPassedGrade
|
, csvEUserExamResult :: Maybe ExamResultPassedGrade
|
||||||
, csvEUserCourseNote :: Maybe Html
|
, csvEUserCourseNote :: Maybe StoredMarkup
|
||||||
}
|
}
|
||||||
deriving (Generic)
|
deriving (Generic)
|
||||||
makeLenses_ ''ExamUserTableCsv
|
makeLenses_ ''ExamUserTableCsv
|
||||||
@ -345,7 +345,7 @@ data ExamUserCsvAction
|
|||||||
}
|
}
|
||||||
| ExamUserCsvSetCourseNoteData
|
| ExamUserCsvSetCourseNoteData
|
||||||
{ examUserCsvActUser :: UserId
|
{ examUserCsvActUser :: UserId
|
||||||
, examUserCsvActCourseNote :: Maybe Html
|
, examUserCsvActCourseNote :: Maybe StoredMarkup
|
||||||
}
|
}
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
deriveJSON defaultOptions
|
deriveJSON defaultOptions
|
||||||
|
|||||||
@ -67,7 +67,7 @@ helpForm mReferer mUid = renderWForm FormStandard $ do
|
|||||||
<$> hfReferer'
|
<$> hfReferer'
|
||||||
<*> hfUserId'
|
<*> hfUserId'
|
||||||
<*> hfSubject'
|
<*> hfSubject'
|
||||||
<*> hfRequest'
|
<*> (fmap markupOutput <$> hfRequest')
|
||||||
<*> hfError'
|
<*> hfError'
|
||||||
|
|
||||||
validateHelpForm :: FormValidator HelpForm Handler ()
|
validateHelpForm :: FormValidator HelpForm Handler ()
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import Handler.Utils.Delete
|
|||||||
data MaterialForm = MaterialForm
|
data MaterialForm = MaterialForm
|
||||||
{ mfName :: MaterialName
|
{ mfName :: MaterialName
|
||||||
, mfType :: Maybe (CI Text)
|
, mfType :: Maybe (CI Text)
|
||||||
, mfDescription :: Maybe Html
|
, mfDescription :: Maybe StoredMarkup
|
||||||
, mfVisibleFrom :: Maybe UTCTime
|
, mfVisibleFrom :: Maybe UTCTime
|
||||||
, mfFiles :: Maybe FileUploads
|
, mfFiles :: Maybe FileUploads
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ type Loads = Map (Either UserEmail UserId) (InvitationData SheetCorrector)
|
|||||||
|
|
||||||
data SheetForm = SheetForm
|
data SheetForm = SheetForm
|
||||||
{ sfName :: SheetName
|
{ sfName :: SheetName
|
||||||
, sfDescription :: Maybe Html
|
, sfDescription :: Maybe StoredMarkup
|
||||||
, sfRequireExamRegistration :: Maybe ExamId
|
, sfRequireExamRegistration :: Maybe ExamId
|
||||||
, sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe FileUploads
|
, sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe FileUploads
|
||||||
, sfPersonalF :: Maybe SheetPersonalisedFilesForm
|
, sfPersonalF :: Maybe SheetPersonalisedFilesForm
|
||||||
@ -39,7 +39,7 @@ data SheetForm = SheetForm
|
|||||||
, sfGrouping :: SheetGroup
|
, sfGrouping :: SheetGroup
|
||||||
, sfType :: SheetType
|
, sfType :: SheetType
|
||||||
, sfAutoDistribute :: Bool
|
, sfAutoDistribute :: Bool
|
||||||
, sfMarkingText :: Maybe Html
|
, sfMarkingText :: Maybe StoredMarkup
|
||||||
, sfAnonymousCorrection :: Bool
|
, sfAnonymousCorrection :: Bool
|
||||||
, sfCorrectors :: Loads
|
, sfCorrectors :: Loads
|
||||||
-- Keine SheetId im Formular!
|
-- Keine SheetId im Formular!
|
||||||
|
|||||||
@ -210,7 +210,7 @@ commR CommunicationRoute{..} = do
|
|||||||
<$> recipientAForm
|
<$> recipientAForm
|
||||||
<* aformMessage recipientsListMsg
|
<* aformMessage recipientsListMsg
|
||||||
<*> aopt textField (fslI MsgCommSubject) Nothing
|
<*> aopt textField (fslI MsgCommSubject) Nothing
|
||||||
<*> areq htmlField (fslI MsgCommBody) Nothing
|
<*> (markupOutput <$> areq htmlField (fslI MsgCommBody) Nothing)
|
||||||
formResult commRes $ \case
|
formResult commRes $ \case
|
||||||
(comm, BtnCommunicationSend) -> do
|
(comm, BtnCommunicationSend) -> do
|
||||||
runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs
|
runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs
|
||||||
|
|||||||
@ -2124,25 +2124,25 @@ examModeForm mPrev = examMode
|
|||||||
where
|
where
|
||||||
examMode examSynchronicity examOnline examAids examRequiredEquipment = ExamMode{..}
|
examMode examSynchronicity examOnline examAids examRequiredEquipment = ExamMode{..}
|
||||||
|
|
||||||
examAidsEither :: Iso' ExamAids (Either Html ExamAidsPreset)
|
examAidsEither :: Iso' ExamAids (Either StoredMarkup ExamAidsPreset)
|
||||||
examAidsEither = iso examAidsToEither examAidsFromEither
|
examAidsEither = iso examAidsToEither examAidsFromEither
|
||||||
where examAidsToEither (ExamAidsPreset p) = Right p
|
where examAidsToEither (ExamAidsPreset p) = Right p
|
||||||
examAidsToEither (ExamAidsCustom c) = Left c
|
examAidsToEither (ExamAidsCustom c) = Left c
|
||||||
examAidsFromEither (Right p) = ExamAidsPreset p
|
examAidsFromEither (Right p) = ExamAidsPreset p
|
||||||
examAidsFromEither (Left c) = ExamAidsCustom c
|
examAidsFromEither (Left c) = ExamAidsCustom c
|
||||||
examOnlineEither :: Iso' ExamOnline (Either Html ExamOnlinePreset)
|
examOnlineEither :: Iso' ExamOnline (Either StoredMarkup ExamOnlinePreset)
|
||||||
examOnlineEither = iso examOnlineToEither examOnlineFromEither
|
examOnlineEither = iso examOnlineToEither examOnlineFromEither
|
||||||
where examOnlineToEither (ExamOnlinePreset p) = Right p
|
where examOnlineToEither (ExamOnlinePreset p) = Right p
|
||||||
examOnlineToEither (ExamOnlineCustom c) = Left c
|
examOnlineToEither (ExamOnlineCustom c) = Left c
|
||||||
examOnlineFromEither (Right p) = ExamOnlinePreset p
|
examOnlineFromEither (Right p) = ExamOnlinePreset p
|
||||||
examOnlineFromEither (Left c) = ExamOnlineCustom c
|
examOnlineFromEither (Left c) = ExamOnlineCustom c
|
||||||
examSynchronicityEither :: Iso' ExamSynchronicity (Either Html ExamSynchronicityPreset)
|
examSynchronicityEither :: Iso' ExamSynchronicity (Either StoredMarkup ExamSynchronicityPreset)
|
||||||
examSynchronicityEither = iso examSynchronicityToEither examSynchronicityFromEither
|
examSynchronicityEither = iso examSynchronicityToEither examSynchronicityFromEither
|
||||||
where examSynchronicityToEither (ExamSynchronicityPreset p) = Right p
|
where examSynchronicityToEither (ExamSynchronicityPreset p) = Right p
|
||||||
examSynchronicityToEither (ExamSynchronicityCustom c) = Left c
|
examSynchronicityToEither (ExamSynchronicityCustom c) = Left c
|
||||||
examSynchronicityFromEither (Right p) = ExamSynchronicityPreset p
|
examSynchronicityFromEither (Right p) = ExamSynchronicityPreset p
|
||||||
examSynchronicityFromEither (Left c) = ExamSynchronicityCustom c
|
examSynchronicityFromEither (Left c) = ExamSynchronicityCustom c
|
||||||
examRequiredEquipmentEither :: Iso' ExamRequiredEquipment (Either Html ExamRequiredEquipmentPreset)
|
examRequiredEquipmentEither :: Iso' ExamRequiredEquipment (Either StoredMarkup ExamRequiredEquipmentPreset)
|
||||||
examRequiredEquipmentEither = iso examRequiredEquipmentToEither examRequiredEquipmentFromEither
|
examRequiredEquipmentEither = iso examRequiredEquipmentToEither examRequiredEquipmentFromEither
|
||||||
where examRequiredEquipmentToEither (ExamRequiredEquipmentPreset p) = Right p
|
where examRequiredEquipmentToEither (ExamRequiredEquipmentPreset p) = Right p
|
||||||
examRequiredEquipmentToEither (ExamRequiredEquipmentCustom c) = Left c
|
examRequiredEquipmentToEither (ExamRequiredEquipmentCustom c) = Left c
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import Import.NoFoundation
|
|||||||
import Handler.Utils.I18n
|
import Handler.Utils.I18n
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
|
import qualified Data.Text.Lazy as LT
|
||||||
|
|
||||||
|
import Control.Monad.Error.Class (liftEither)
|
||||||
|
|
||||||
import qualified Text.Pandoc as P
|
import qualified Text.Pandoc as P
|
||||||
|
|
||||||
@ -24,27 +27,39 @@ data HtmlFieldKind
|
|||||||
instance Universe HtmlFieldKind
|
instance Universe HtmlFieldKind
|
||||||
instance Finite HtmlFieldKind
|
instance Finite HtmlFieldKind
|
||||||
|
|
||||||
htmlField, htmlFieldSmall :: MonadLogger m => Field m Html
|
htmlField, htmlFieldSmall :: MonadLogger m => Field m StoredMarkup
|
||||||
htmlField = htmlField' HtmlFieldNormal
|
htmlField = htmlField' HtmlFieldNormal
|
||||||
htmlFieldSmall = htmlField' HtmlFieldSmall
|
htmlFieldSmall = htmlField' HtmlFieldSmall
|
||||||
|
|
||||||
|
|
||||||
htmlField' :: MonadLogger m => HtmlFieldKind -> Field m Html
|
htmlField' :: MonadLogger m => HtmlFieldKind -> Field m StoredMarkup
|
||||||
htmlField' fieldKind = Field{..}
|
htmlField' fieldKind = Field{..}
|
||||||
where
|
where
|
||||||
fieldEnctype = UrlEncoded
|
fieldEnctype = UrlEncoded
|
||||||
|
|
||||||
fieldParse (t : _) _
|
fieldParse ((Text.strip -> t) : _) _ = runExceptT . runMaybeT $ do
|
||||||
= return . fmap (assertM' $ not . null . renderHtml) . parseMarkdown $ Text.strip t
|
html <- assertM' (not . null . LT.strip . renderHtml) =<< liftEither (parseMarkdown t)
|
||||||
|
return StoredMarkup
|
||||||
|
{ markupInputFormat = MarkupMarkdown
|
||||||
|
, markupInput = fromStrict t
|
||||||
|
, markupOutput = html
|
||||||
|
}
|
||||||
fieldParse [] _ = return $ Right Nothing
|
fieldParse [] _ = return $ Right Nothing
|
||||||
|
|
||||||
fieldView theId name attrs val isReq = do
|
fieldView theId name attrs val isReq = do
|
||||||
val' <- either return (maybeT (return mempty) . renderMarkdown) val
|
val'
|
||||||
|
<- let toMarkdown StoredMarkup{..} = case markupInputFormat of
|
||||||
|
MarkupMarkdown -> return $ toStrict markupInput
|
||||||
|
MarkupHtml -> renderMarkdown markupOutput
|
||||||
|
MarkupPlaintext -> plaintextToMarkdown $ toStrict markupInput
|
||||||
|
in either return (maybeT (return mempty) . toMarkdown) val
|
||||||
|
|
||||||
let markdownExplanation = $(i18nWidgetFile "markdown-explanation")
|
let markdownExplanation = $(i18nWidgetFile "markdown-explanation")
|
||||||
$(widgetFile "widgets/html-field")
|
$(widgetFile "widgets/html-field")
|
||||||
|
|
||||||
parseMarkdown = parseMarkdownWith markdownReaderOptions htmlWriterOptions
|
parseMarkdown = parseMarkdownWith markdownReaderOptions htmlWriterOptions
|
||||||
renderMarkdown = renderMarkdownWith htmlReaderOptions markdownWriterOptions
|
renderMarkdown = renderMarkdownWith htmlReaderOptions markdownWriterOptions
|
||||||
|
plaintextToMarkdown = plaintextToMarkdownWith markdownWriterOptions
|
||||||
|
|
||||||
parseMarkdownWith :: P.ReaderOptions -> P.WriterOptions -> Text -> Either (SomeMessage site) Html
|
parseMarkdownWith :: P.ReaderOptions -> P.WriterOptions -> Text -> Either (SomeMessage site) Html
|
||||||
parseMarkdownWith readerOptions writerOptions text =
|
parseMarkdownWith readerOptions writerOptions text =
|
||||||
@ -60,6 +75,14 @@ renderMarkdownWith readerOptions writerOptions html =
|
|||||||
where
|
where
|
||||||
logPandocError = $logErrorS "renderMarkdown" . tshow
|
logPandocError = $logErrorS "renderMarkdown" . tshow
|
||||||
|
|
||||||
|
plaintextToMarkdownWith :: (MonadLogger m, MonadPlus m) => P.WriterOptions -> Text -> m Text
|
||||||
|
plaintextToMarkdownWith writerOptions text =
|
||||||
|
either (\e -> logPandocError e >> mzero) return . P.runPure $
|
||||||
|
P.writeMarkdown writerOptions pandoc
|
||||||
|
where
|
||||||
|
logPandocError = $logErrorS "renderMarkdown" . tshow
|
||||||
|
pandoc = P.Pandoc mempty [P.Plain [P.Str text]]
|
||||||
|
|
||||||
|
|
||||||
htmlReaderOptions, markdownReaderOptions :: P.ReaderOptions
|
htmlReaderOptions, markdownReaderOptions :: P.ReaderOptions
|
||||||
htmlReaderOptions = markdownReaderOptions
|
htmlReaderOptions = markdownReaderOptions
|
||||||
|
|||||||
@ -990,6 +990,27 @@ customMigrations = Map.fromListWith (>>)
|
|||||||
, ( AppliedMigrationKey [migrationVersion|42.0.0|] [version|43.0.0|]
|
, ( AppliedMigrationKey [migrationVersion|42.0.0|] [version|43.0.0|]
|
||||||
, return () -- Unused; used to create and fill `ChangelogItemFirstSeen`
|
, return () -- Unused; used to create and fill `ChangelogItemFirstSeen`
|
||||||
)
|
)
|
||||||
|
, ( AppliedMigrationKey [migrationVersion|43.0.0|] [version|44.0.0|]
|
||||||
|
, [executeQQ|
|
||||||
|
ALTER TABLE IF EXISTS ^{Allocation} ALTER COLUMN @{AllocationDescription} TYPE jsonb USING (CASE WHEN @{AllocationDescription} IS NOT NULL THEN to_json(@{AllocationDescription}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{Allocation} ALTER COLUMN @{AllocationStaffDescription} TYPE jsonb USING (CASE WHEN @{AllocationStaffDescription} IS NOT NULL THEN to_json(@{AllocationStaffDescription}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{Course} ALTER COLUMN @{CourseDescription} TYPE jsonb USING (CASE WHEN @{CourseDescription} IS NOT NULL THEN to_json(@{CourseDescription}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{Course} ALTER COLUMN @{CourseApplicationsInstructions} TYPE jsonb USING (CASE WHEN @{CourseApplicationsInstructions} IS NOT NULL THEN to_json(@{CourseApplicationsInstructions}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{CourseEvent} ALTER COLUMN @{CourseEventNote} TYPE jsonb USING (CASE WHEN @{CourseEventNote} IS NOT NULL THEN to_json(@{CourseEventNote}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{CourseUserNote} ALTER COLUMN @{CourseUserNoteNote} TYPE jsonb USING (CASE WHEN @{CourseUserNoteNote} IS NOT NULL THEN to_json(@{CourseUserNoteNote}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{Material} ALTER COLUMN @{MaterialDescription} TYPE jsonb USING (CASE WHEN @{MaterialDescription} IS NOT NULL THEN to_json(@{MaterialDescription}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{CourseNews} ALTER COLUMN @{CourseNewsContent} TYPE jsonb USING (CASE WHEN @{CourseNewsContent} IS NOT NULL THEN to_json(@{CourseNewsContent}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{CourseNews} ALTER COLUMN @{CourseNewsSummary} TYPE jsonb USING (CASE WHEN @{CourseNewsSummary} IS NOT NULL THEN to_json(@{CourseNewsSummary}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{Exam} ALTER COLUMN @{ExamDescription} TYPE jsonb USING (CASE WHEN @{ExamDescription} IS NOT NULL THEN to_json(@{ExamDescription}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{ExamOccurrence} ALTER COLUMN @{ExamOccurrenceDescription} TYPE jsonb USING (CASE WHEN @{ExamOccurrenceDescription} IS NOT NULL THEN to_json(@{ExamOccurrenceDescription}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{Sheet} ALTER COLUMN @{SheetDescription} TYPE jsonb USING (CASE WHEN @{SheetDescription} IS NOT NULL THEN to_json(@{SheetDescription}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{Sheet} ALTER COLUMN @{SheetMarkingText} TYPE jsonb USING (CASE WHEN @{SheetMarkingText} IS NOT NULL THEN to_json(@{SheetMarkingText}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{SystemMessage} ALTER COLUMN @{SystemMessageContent} TYPE jsonb USING (CASE WHEN @{SystemMessageContent} IS NOT NULL THEN to_json(@{SystemMessageContent}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{SystemMessage} ALTER COLUMN @{SystemMessageSummary} TYPE jsonb USING (CASE WHEN @{SystemMessageSummary} IS NOT NULL THEN to_json(@{SystemMessageSummary}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{SystemMessageTranslation} ALTER COLUMN @{SystemMessageTranslationContent} TYPE jsonb USING (CASE WHEN @{SystemMessageTranslationContent} IS NOT NULL THEN to_json(@{SystemMessageTranslationContent}) ELSE NULL END);
|
||||||
|
ALTER TABLE IF EXISTS ^{SystemMessageTranslation} ALTER COLUMN @{SystemMessageTranslationSummary} TYPE jsonb USING (CASE WHEN @{SystemMessageTranslationSummary} IS NOT NULL THEN to_json(@{SystemMessageTranslationSummary}) ELSE NULL END);
|
||||||
|
|]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -18,3 +18,4 @@ import Model.Types.Languages as Types
|
|||||||
import Model.Types.File as Types
|
import Model.Types.File as Types
|
||||||
import Model.Types.User as Types
|
import Model.Types.User as Types
|
||||||
import Model.Types.Changelog as Types
|
import Model.Types.Changelog as Types
|
||||||
|
import Model.Types.Markup as Types
|
||||||
|
|||||||
@ -38,6 +38,7 @@ classifyChangelogItem = \case
|
|||||||
ChangelogExamAchievementParticipantDuplication -> ChangelogItemBugfix
|
ChangelogExamAchievementParticipantDuplication -> ChangelogItemBugfix
|
||||||
ChangelogFormsTimesReset -> ChangelogItemBugfix
|
ChangelogFormsTimesReset -> ChangelogItemBugfix
|
||||||
ChangelogAllocationCourseAcceptSubstitutesFixed -> ChangelogItemBugfix
|
ChangelogAllocationCourseAcceptSubstitutesFixed -> ChangelogItemBugfix
|
||||||
|
ChangelogStoredMarkup -> ChangelogItemBugfix
|
||||||
_other -> ChangelogItemFeature
|
_other -> ChangelogItemFeature
|
||||||
|
|
||||||
changelogItemDays :: Map ChangelogItem Day
|
changelogItemDays :: Map ChangelogItem Day
|
||||||
|
|||||||
@ -43,6 +43,7 @@ module Model.Types.Exam
|
|||||||
import Import.NoModel
|
import Import.NoModel
|
||||||
import Model.Types.Common
|
import Model.Types.Common
|
||||||
import Model.Types.TH.PathPiece
|
import Model.Types.TH.PathPiece
|
||||||
|
import Model.Types.Markup
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
@ -439,7 +440,7 @@ instance Enum ExamPartNumber where
|
|||||||
|
|
||||||
data ExamAids
|
data ExamAids
|
||||||
= ExamAidsPreset { examAidsPreset :: ExamAidsPreset }
|
= ExamAidsPreset { examAidsPreset :: ExamAidsPreset }
|
||||||
| ExamAidsCustom { examAidsCustom :: Html }
|
| ExamAidsCustom { examAidsCustom :: StoredMarkup }
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
data ExamAidsPreset
|
data ExamAidsPreset
|
||||||
@ -460,7 +461,7 @@ pathPieceJSON ''ExamAidsPreset
|
|||||||
|
|
||||||
data ExamOnline
|
data ExamOnline
|
||||||
= ExamOnlinePreset { examOnlinePreset :: ExamOnlinePreset }
|
= ExamOnlinePreset { examOnlinePreset :: ExamOnlinePreset }
|
||||||
| ExamOnlineCustom { examOnlineCustom :: Html }
|
| ExamOnlineCustom { examOnlineCustom :: StoredMarkup }
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
data ExamOnlinePreset
|
data ExamOnlinePreset
|
||||||
@ -481,7 +482,7 @@ pathPieceJSON ''ExamOnlinePreset
|
|||||||
|
|
||||||
data ExamSynchronicity
|
data ExamSynchronicity
|
||||||
= ExamSynchronicityPreset { examSynchronicityPreset :: ExamSynchronicityPreset }
|
= ExamSynchronicityPreset { examSynchronicityPreset :: ExamSynchronicityPreset }
|
||||||
| ExamSynchronicityCustom { examSynchronicityCustom :: Html }
|
| ExamSynchronicityCustom { examSynchronicityCustom :: StoredMarkup }
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
data ExamSynchronicityPreset
|
data ExamSynchronicityPreset
|
||||||
@ -502,7 +503,7 @@ pathPieceJSON ''ExamSynchronicityPreset
|
|||||||
|
|
||||||
data ExamRequiredEquipment
|
data ExamRequiredEquipment
|
||||||
= ExamRequiredEquipmentPreset { examRequiredEquipmentPreset :: ExamRequiredEquipmentPreset }
|
= ExamRequiredEquipmentPreset { examRequiredEquipmentPreset :: ExamRequiredEquipmentPreset }
|
||||||
| ExamRequiredEquipmentCustom { examRequiredEquipmentCustom :: Html }
|
| ExamRequiredEquipmentCustom { examRequiredEquipmentCustom :: StoredMarkup }
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
data ExamRequiredEquipmentPreset
|
data ExamRequiredEquipmentPreset
|
||||||
|
|||||||
133
src/Model/Types/Markup.hs
Normal file
133
src/Model/Types/Markup.hs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
module Model.Types.Markup
|
||||||
|
( MarkupFormat(..)
|
||||||
|
, StoredMarkup(..)
|
||||||
|
, htmlToStoredMarkup, plaintextToStoredMarkup, preEscapedToStoredMarkup
|
||||||
|
, esqueletoMarkupOutput
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import.NoModel
|
||||||
|
|
||||||
|
import Text.Blaze (ToMarkup(..))
|
||||||
|
import Text.Blaze.Html.Renderer.Text (renderHtml)
|
||||||
|
|
||||||
|
import qualified Data.Text.Lazy as LT
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import Data.Text.Encoding (decodeUtf8')
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
import qualified Database.Esqueleto.Internal.Internal as E
|
||||||
|
import Database.Persist.Sql
|
||||||
|
|
||||||
|
|
||||||
|
data MarkupFormat
|
||||||
|
= MarkupMarkdown
|
||||||
|
| MarkupHtml
|
||||||
|
| MarkupPlaintext
|
||||||
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Universe, Finite)
|
||||||
|
nullaryPathPiece ''MarkupFormat $ camelToPathPiece' 1
|
||||||
|
pathPieceJSON ''MarkupFormat
|
||||||
|
|
||||||
|
data StoredMarkup = StoredMarkup
|
||||||
|
{ markupInputFormat :: MarkupFormat
|
||||||
|
, markupInput :: LT.Text
|
||||||
|
, markupOutput :: Html
|
||||||
|
} deriving (Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
htmlToStoredMarkup :: Html -> StoredMarkup
|
||||||
|
htmlToStoredMarkup html = StoredMarkup
|
||||||
|
{ markupInputFormat = MarkupHtml
|
||||||
|
, markupInput = renderHtml html
|
||||||
|
, markupOutput = html
|
||||||
|
}
|
||||||
|
plaintextToStoredMarkup :: Textual t => t -> StoredMarkup
|
||||||
|
plaintextToStoredMarkup (repack -> t) = StoredMarkup
|
||||||
|
{ markupInputFormat = MarkupPlaintext
|
||||||
|
, markupInput = t
|
||||||
|
, markupOutput = toMarkup t
|
||||||
|
}
|
||||||
|
preEscapedToStoredMarkup :: Textual t => t -> StoredMarkup
|
||||||
|
preEscapedToStoredMarkup (repack -> t) = StoredMarkup
|
||||||
|
{ markupInputFormat = MarkupHtml
|
||||||
|
, markupInput = fromStrict t
|
||||||
|
, markupOutput = preEscapedToMarkup t
|
||||||
|
}
|
||||||
|
|
||||||
|
esqueletoMarkupOutput :: E.SqlExpr (E.Value StoredMarkup) -> E.SqlExpr (E.Value Html)
|
||||||
|
esqueletoMarkupOutput sMarkup = E.maybe (E.val mempty) E.veryUnsafeCoerceSqlExprValue $ E.maybe (sMarkup E.#>>. "{}") E.just (sMarkup E.#>>. "{\"markup-output\"}")
|
||||||
|
|
||||||
|
instance Eq StoredMarkup where
|
||||||
|
(==) = (==) `on` LT.strip . renderHtml . markupOutput
|
||||||
|
instance Ord StoredMarkup where
|
||||||
|
compare = comparing $ LT.strip . renderHtml . markupOutput
|
||||||
|
|
||||||
|
instance ToJSON StoredMarkup where
|
||||||
|
toJSON StoredMarkup{..}
|
||||||
|
| markupInputFormat == MarkupHtml
|
||||||
|
, renderHtml markupOutput == markupInput
|
||||||
|
= Aeson.String $ toStrict markupInput
|
||||||
|
| otherwise
|
||||||
|
= Aeson.object
|
||||||
|
[ "input-format" Aeson..= markupInputFormat
|
||||||
|
, "markup-input" Aeson..= markupInput
|
||||||
|
, "markup-output" Aeson..= markupOutput
|
||||||
|
]
|
||||||
|
instance FromJSON StoredMarkup where
|
||||||
|
parseJSON v = case v of
|
||||||
|
Aeson.String t -> return $ preEscapedToStoredMarkup t
|
||||||
|
Aeson.Object o -> do
|
||||||
|
markupInputFormat <- o Aeson..: "input-format"
|
||||||
|
markupInput <- o Aeson..: "markup-input"
|
||||||
|
markupOutput <- o Aeson..: "markup-output"
|
||||||
|
return StoredMarkup{..}
|
||||||
|
other -> Aeson.typeMismatch "StoredMarkup" other
|
||||||
|
|
||||||
|
instance IsString StoredMarkup where
|
||||||
|
fromString = preEscapedToStoredMarkup
|
||||||
|
instance ToMarkup StoredMarkup where
|
||||||
|
toMarkup = markupOutput
|
||||||
|
instance ToWidget site StoredMarkup where
|
||||||
|
toWidget = toWidget . toMarkup
|
||||||
|
|
||||||
|
instance Semigroup StoredMarkup where
|
||||||
|
a <> b
|
||||||
|
| markupInputFormat a == markupInputFormat b
|
||||||
|
= StoredMarkup
|
||||||
|
{ markupInputFormat = markupInputFormat a
|
||||||
|
, markupInput = ((<>) `on` markupInput) a b -- this seems optimistic...
|
||||||
|
, markupOutput = ((<>) `on` markupOutput) a b
|
||||||
|
}
|
||||||
|
| null $ markupInput a
|
||||||
|
= b
|
||||||
|
| null $ markupInput b
|
||||||
|
= a
|
||||||
|
| otherwise
|
||||||
|
= StoredMarkup
|
||||||
|
{ markupInputFormat = MarkupHtml
|
||||||
|
, markupInput = renderHtml $ ((<>) `on` markupOutput) a b
|
||||||
|
, markupOutput = ((<>) `on` markupOutput) a b
|
||||||
|
}
|
||||||
|
instance Monoid StoredMarkup where
|
||||||
|
mempty = fromString mempty
|
||||||
|
|
||||||
|
instance Csv.ToField StoredMarkup where
|
||||||
|
toField = Csv.toField . markupOutput
|
||||||
|
instance Csv.FromField StoredMarkup where
|
||||||
|
parseField = fmap htmlToStoredMarkup . Csv.parseField
|
||||||
|
|
||||||
|
instance PersistField StoredMarkup where
|
||||||
|
fromPersistValue (PersistDbSpecific bs) = first pack $ Aeson.eitherDecodeStrict' bs
|
||||||
|
fromPersistValue (PersistByteString bs) = first pack $ Aeson.eitherDecodeStrict' bs
|
||||||
|
<|> bimap show preEscapedToStoredMarkup (decodeUtf8' bs)
|
||||||
|
fromPersistValue (PersistText t) = first pack $ Aeson.eitherDecodeStrict' (encodeUtf8 t)
|
||||||
|
<|> return (preEscapedToStoredMarkup t)
|
||||||
|
fromPersistValue _ = Left "StoredMarkup values must be converted from PersistDbSpecific, PersistText, or PersistByteString"
|
||||||
|
toPersistValue = PersistDbSpecific . LBS.toStrict . Aeson.encode
|
||||||
|
instance PersistFieldSql StoredMarkup where
|
||||||
|
sqlType _ = SqlOther "jsonb"
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
$newline never
|
||||||
|
Html-Felder speichern nun den genauen Markdown-Eingabetext, sodass erneutes Editieren nicht mehr zu verändertem oder invaliden Markup führen sollte.
|
||||||
2
templates/i18n/changelog/stored-markup.en-eu.hamlet
Normal file
2
templates/i18n/changelog/stored-markup.en-eu.hamlet
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
$newline never
|
||||||
|
Html fields now store the exact markdown input. Therefore repeated editing should no longer result in changed or invalid markup.
|
||||||
@ -556,7 +556,7 @@ fillDb = do
|
|||||||
nbrs = [1,2,3,27,7,1]
|
nbrs = [1,2,3,27,7,1]
|
||||||
ffp <- insert' Course
|
ffp <- insert' Course
|
||||||
{ courseName = "Fortgeschrittene Funktionale Programmierung"
|
{ courseName = "Fortgeschrittene Funktionale Programmierung"
|
||||||
, courseDescription = Just [shamlet|
|
, courseDescription = Just $ htmlToStoredMarkup [shamlet|
|
||||||
<h2>It is fun!
|
<h2>It is fun!
|
||||||
<p>Come to where the functional is!
|
<p>Come to where the functional is!
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import Yesod.Auth.Util.PasswordStore
|
|||||||
import Database.Persist.Sql (SqlBackend, fromSqlKey, toSqlKey)
|
import Database.Persist.Sql (SqlBackend, fromSqlKey, toSqlKey)
|
||||||
|
|
||||||
import Text.Blaze.Html
|
import Text.Blaze.Html
|
||||||
import Text.Blaze.Renderer.Text
|
import Text.Blaze.Html.Renderer.Text (renderHtml)
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
@ -215,7 +215,13 @@ instance {-# OVERLAPPABLE #-} ToBackendKey SqlBackend record => Arbitrary (Key r
|
|||||||
|
|
||||||
instance Arbitrary Html where
|
instance Arbitrary Html where
|
||||||
arbitrary = (preEscapedToHtml :: String -> Html) . getPrintableString <$> arbitrary
|
arbitrary = (preEscapedToHtml :: String -> Html) . getPrintableString <$> arbitrary
|
||||||
shrink = map preEscapedToHtml . shrink . renderMarkup
|
shrink = map preEscapedToHtml . shrink . renderHtml
|
||||||
|
|
||||||
|
instance Arbitrary StoredMarkup where
|
||||||
|
arbitrary = oneof
|
||||||
|
[ htmlToStoredMarkup <$> arbitrary
|
||||||
|
, (plaintextToStoredMarkup :: Text -> StoredMarkup) <$> arbitrary
|
||||||
|
]
|
||||||
|
|
||||||
instance Arbitrary OccurrenceSchedule where
|
instance Arbitrary OccurrenceSchedule where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
@ -389,6 +395,8 @@ spec = do
|
|||||||
[ persistFieldLaws, jsonLaws, binaryLaws ]
|
[ persistFieldLaws, jsonLaws, binaryLaws ]
|
||||||
lawsCheckHspec (Proxy @ExamPartNumber)
|
lawsCheckHspec (Proxy @ExamPartNumber)
|
||||||
[ persistFieldLaws, jsonLaws, pathPieceLaws, csvFieldLaws, eqLaws, ordLaws ]
|
[ persistFieldLaws, jsonLaws, pathPieceLaws, csvFieldLaws, eqLaws, ordLaws ]
|
||||||
|
lawsCheckHspec (Proxy @StoredMarkup)
|
||||||
|
[ persistFieldLaws, jsonLaws, eqLaws, ordLaws, showReadLaws, monoidLaws, semigroupLaws, semigroupMonoidLaws, csvFieldLaws ]
|
||||||
|
|
||||||
describe "TermIdentifier" $ do
|
describe "TermIdentifier" $ do
|
||||||
it "has compatible encoding/decoding to/from Text" . property $
|
it "has compatible encoding/decoding to/from Text" . property $
|
||||||
@ -448,6 +456,19 @@ spec = do
|
|||||||
t "{\"user\": {\"mode\": \"upload-specific\", \"specific-files\": [{\"name\": \"abgabe6.pdf\", \"label\": \"Abgabe 6\", \"required\": true}]}, \"corrector\": false}"
|
t "{\"user\": {\"mode\": \"upload-specific\", \"specific-files\": [{\"name\": \"abgabe6.pdf\", \"label\": \"Abgabe 6\", \"required\": true}]}, \"corrector\": false}"
|
||||||
t "{\"user\": {\"mode\": \"upload-specific\", \"specific-files\": [{\"name\": \"abgabe10.pdf\", \"label\": \"Abgabe 10\", \"required\": true}, {\"name\": \"deckblatt10.pdf\", \"label\": \"Deckblatt 10\", \"required\": true}]}, \"corrector\": false}"
|
t "{\"user\": {\"mode\": \"upload-specific\", \"specific-files\": [{\"name\": \"abgabe10.pdf\", \"label\": \"Abgabe 10\", \"required\": true}, {\"name\": \"deckblatt10.pdf\", \"label\": \"Deckblatt 10\", \"required\": true}]}, \"corrector\": false}"
|
||||||
t "{\"user\": {\"mode\": \"no-upload\"}, \"corrector\": false}"
|
t "{\"user\": {\"mode\": \"no-upload\"}, \"corrector\": false}"
|
||||||
|
describe "StoredMarkup" $ do
|
||||||
|
it "decodes from Html via json" . property $
|
||||||
|
\html -> case Aeson.eitherDecode (Aeson.encode html) of
|
||||||
|
Left _ -> False
|
||||||
|
Right StoredMarkup{..} -> ((==) `on` renderHtml) markupOutput html
|
||||||
|
&& markupInputFormat == MarkupHtml
|
||||||
|
&& renderHtml html == markupInput
|
||||||
|
it "decodes from Html via persistent" . property $
|
||||||
|
\html -> case fromPersistValue (toPersistValue html) of
|
||||||
|
Left _ -> False
|
||||||
|
Right StoredMarkup{..} -> ((==) `on` renderHtml) markupOutput html
|
||||||
|
&& markupInputFormat == MarkupHtml
|
||||||
|
&& renderHtml html == markupInput
|
||||||
|
|
||||||
termExample :: (TermIdentifier, Text) -> Expectation
|
termExample :: (TermIdentifier, Text) -> Expectation
|
||||||
termExample (term, encoded) = example $ do
|
termExample (term, encoded) = example $ do
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user