fix(html-field): introduce stored-markup

BREAKING CHANGE: StoredMarkup
This commit is contained in:
Gregor Kleen 2020-11-06 20:39:43 +01:00
parent 549b95882d
commit e25e8a2f4c
32 changed files with 277 additions and 58 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 ()

View File

@ -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
} }

View File

@ -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!

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
|]
)
] ]

View File

@ -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

View File

@ -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

View File

@ -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
View 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"

View File

@ -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.

View 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.

View File

@ -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>

View File

@ -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