diff --git a/Handler/EditSubmission.hs b/Handler/EditSubmission.hs index 0083fa4..5b99cca 100644 --- a/Handler/EditSubmission.hs +++ b/Handler/EditSubmission.hs @@ -3,28 +3,76 @@ module Handler.EditSubmission where import Import +import Handler.Common (checkIfCanEdit) +import Handler.SubmissionView import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3, bfs) +import Data.Text as T + getEditSubmissionR :: SubmissionId -> Handler Html getEditSubmissionR submissionId = do - (formWidget, formEnctype) <- generateFormPost editSubmissionForm + submission <- runDB $ get404 submissionId + tags <- runDB $ getTags submissionId + let mTagsAsText = case tags of + [] -> Nothing + _ -> Just $ T.intercalate ", " $ Import.map tagName $ Import.catMaybes tags + (formWidget, formEnctype) <- generateFormPost $ editSubmissionForm (submissionDescription submission) mTagsAsText doEditSubmission formWidget formEnctype submissionId postEditSubmissionR :: SubmissionId -> Handler Html postEditSubmissionR submissionId = do - ((result, formWidget), formEnctype) <- runFormPost editSubmissionForm - doEditSubmission formWidget formEnctype submissionId + submission <- runDB $ get404 submissionId + ((result, _), _) <- runFormPost $ editSubmissionForm (submissionDescription submission) Nothing + let FormSuccess (description, tags) = result + isEditable <- checkIfCanEdit submissionId + if isEditable + then + runDB $ do + update submissionId [SubmissionDescription =. description] + + sts <- selectList [SubmissionTagSubmission ==. submissionId] [] + let currentTagIds = Import.map (submissionTagTag . entityVal) sts + + addTags submissionId tags currentTagIds + + return () + else + do + setMessage $ toHtml ("Only owner can edit a submission!!!" :: Text) + return () + getEditSubmissionR submissionId + + +addTags submissionId tagsAsText existingOnes = do + let newTags = case tagsAsText of + Just tags' -> Import.map T.strip $ T.split (== ',') tags' + Nothing -> [] + mTs <- mapM (\t -> getBy $ UniqueTagName t) newTags + let tids = Import.map entityKey $ Import.catMaybes mTs + + deleteWhere [SubmissionTagSubmission ==. submissionId, SubmissionTagTag /<-. tids] + + _ <- mapM (\tid -> insert $ SubmissionTag submissionId tid Nothing) (Import.filter (not . (`elem` existingOnes)) tids) + return () + + doEditSubmission formWidget formEnctype submissionId = do submission <- runDB $ get404 submissionId + submissionFull <- getFullInfo (Entity submissionId submission) + let view = queryResult submissionFull + + tagsAvailable <- runDB $ selectList [] [Asc TagName] + let tagsAvailableAsJSON = toJSON $ Import.map (tagName . entityVal) tagsAvailable + defaultLayout $ do setTitle "Edit a submission" $(widgetFile "edit-submission") -editSubmissionForm :: Form (Text, Maybe Text) -editSubmissionForm = renderBootstrap3 BootstrapBasicForm $ (,) - <$> areq textField (bfs MsgSubmissionDescription) Nothing - <*> aopt textField (tagsfs MsgSubmissionTags) Nothing +editSubmissionForm :: Text -> Maybe Text -> Form (Text, Maybe Text) +editSubmissionForm description mTags = renderBootstrap3 BootstrapBasicForm $ (,) + <$> areq textField (bfs MsgSubmissionDescription) (Just description) + <*> aopt textField (tagsfs MsgSubmissionTags) (Just mTags) tagsfs :: RenderMessage site msg => msg -> FieldSettings site tagsfs msg = attrs { fsAttrs = ("data-role"::Text,"tagsinput"::Text):(fsAttrs attrs)} diff --git a/Handler/Query.hs b/Handler/Query.hs index a851aee..859eb2d 100644 --- a/Handler/Query.hs +++ b/Handler/Query.hs @@ -3,6 +3,7 @@ module Handler.Query where import Import import Handler.Shared +import Handler.SubmissionView import PersistSHA1 import Database.Persist.Sql @@ -11,24 +12,6 @@ import Data.Text as T(pack) import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3, withSmallInput) -data FullSubmissionInfo = FullSubmissionInfo { - fsiSubmissionId :: SubmissionId, - fsiSubmission :: Submission, - fsiUser :: User, - fsiRepo :: Repo, - fsiChallenge :: Challenge } - -getFullInfo :: Entity Submission -> Handler FullSubmissionInfo -getFullInfo (Entity submissionId submission) = do - repo <- runDB $ get404 $ submissionRepo submission - user <- runDB $ get404 $ submissionSubmitter submission - challenge <- runDB $ get404 $ submissionChallenge submission - return $ FullSubmissionInfo { - fsiSubmissionId = submissionId, - fsiSubmission = submission, - fsiUser = user, - fsiRepo = repo, - fsiChallenge = challenge } findSubmissions :: Text -> Handler [FullSubmissionInfo] findSubmissions sha1Prefix = do @@ -69,14 +52,5 @@ processQuery query = do setTitle "query results" $(widgetFile "query-results") -queryResult submission = do - $(widgetFile "query-result") - where commitSha1AsText = fromSHA1ToText $ submissionCommit $ fsiSubmission submission - submitter = formatSubmitter $ fsiUser submission - publicSubmissionBranch = getPublicSubmissionBranch $ fsiSubmissionId submission - publicSubmissionRepo = getReadOnlySubmissionUrl $ challengeName $ fsiChallenge submission - browsableUrl = browsableGitRepoBranch (challengeName $ fsiChallenge submission) publicSubmissionBranch - stamp = T.pack $ show $ submissionStamp $ fsiSubmission submission - queryForm :: Form Text queryForm = renderBootstrap3 BootstrapBasicForm $ areq textField (fieldSettingsLabel MsgGitCommitSha1) Nothing diff --git a/Handler/SubmissionView.hs b/Handler/SubmissionView.hs new file mode 100644 index 0000000..2392e89 --- /dev/null +++ b/Handler/SubmissionView.hs @@ -0,0 +1,40 @@ +module Handler.SubmissionView where + +import Import +import Handler.Shared +import PersistSHA1 + +import Data.Text as T(pack) + +data FullSubmissionInfo = FullSubmissionInfo { + fsiSubmissionId :: SubmissionId, + fsiSubmission :: Submission, + fsiUser :: User, + fsiRepo :: Repo, + fsiChallenge :: Challenge } + +getFullInfo :: Entity Submission -> Handler FullSubmissionInfo +getFullInfo (Entity submissionId submission) = do + repo <- runDB $ get404 $ submissionRepo submission + user <- runDB $ get404 $ submissionSubmitter submission + challenge <- runDB $ get404 $ submissionChallenge submission + return $ FullSubmissionInfo { + fsiSubmissionId = submissionId, + fsiSubmission = submission, + fsiUser = user, + fsiRepo = repo, + fsiChallenge = challenge } + +queryResult submission = do + $(widgetFile "query-result") + where commitSha1AsText = fromSHA1ToText $ submissionCommit $ fsiSubmission submission + submitter = formatSubmitter $ fsiUser submission + publicSubmissionBranch = getPublicSubmissionBranch $ fsiSubmissionId submission + publicSubmissionRepo = getReadOnlySubmissionUrl $ challengeName $ fsiChallenge submission + browsableUrl = browsableGitRepoBranch (challengeName $ fsiChallenge submission) publicSubmissionBranch + stamp = T.pack $ show $ submissionStamp $ fsiSubmission submission + +getTags submissionId = do + sts <- selectList [SubmissionTagSubmission ==. submissionId] [] + tags <- mapM get $ Import.map (submissionTagTag . entityVal) sts + return tags diff --git a/config/models b/config/models index fe4716d..95e5005 100644 --- a/config/models +++ b/config/models @@ -78,4 +78,9 @@ Tag name Text description Text Maybe UniqueTagName name +SubmissionTag + submission SubmissionId + tag TagId + accepted Bool Maybe + UniqueSubmissionTag submission tag -- By default this file is used in Model.hs (which is imported by Foundation.hs) diff --git a/gonito.cabal b/gonito.cabal index ef07cba..8a9a8b4 100644 --- a/gonito.cabal +++ b/gonito.cabal @@ -47,6 +47,7 @@ library Handler.Presentation Handler.Tags Handler.EditSubmission + Handler.SubmissionView if flag(dev) || flag(library-only) cpp-options: -DDEVELOPMENT diff --git a/static/css/tagify.css b/static/css/tagify.css new file mode 100644 index 0000000..9c962f5 --- /dev/null +++ b/static/css/tagify.css @@ -0,0 +1,212 @@ +tags { + display: block; + border: 1px solid #DDD; + padding-right: 0.3em 0.5em; + cursor: text; + overflow: hidden; +} + +tags:hover { + border-color: #CCC; +} + +tags tag { + display: inline-block; + margin: 5px 0 5px 5px; + vertical-align: top; + position: relative; + cursor: default; + -webkit-transition: .13s ease-out; + transition: .13s ease-out; + -webkit-animation: .3s tags--bump 1 ease-out; + animation: .3s tags--bump 1 ease-out; +} + +tags tag > div { + vertical-align: top; + position: relative; + box-sizing: border-box; + max-width: 100%; + padding: 0.3em 0.5em; + color: black; + -webkit-transition: .13s ease-out; + transition: .13s ease-out; + padding-right: 1.5em; +} + +tags tag > div > span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: top; + width: 100%; + -webkit-transition: .1s; + transition: .1s; +} + +tags tag > div::before { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: #E5E5E5; + border-radius: 3px; + z-index: -1; + pointer-events: none; + -webkit-transition: 80ms ease; + transition: 80ms ease; +} + +tags tag:hover div::before { + top: -2px; + right: -2px; + bottom: -2px; + left: -2px; + background: #D3E2E2; + box-shadow: 0 0 0 0 #D39494 inset; +} + +tags tag.tagify--noAnim { + -webkit-animation: none; + animation: none; +} + +tags tag.tagify--hide { + width: 0 !important; + padding-left: 0; + padding-right: 0; + margin-left: 0; + margin-right: 0; + opacity: 0; + -webkit-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); + -webkit-transition: .3s; + transition: .3s; + pointer-events: none; +} + +tags tag.tagify--mark div::before { + -webkit-animation: .3s tagify--pulse 2 ease-out; + animation: .3s tagify--pulse 2 ease-out; +} + +tags tag.tagify--notAllowed div > span { + opacity: .5; +} + +tags tag.tagify--notAllowed div::before { + background: rgba(211, 148, 148, 0.44); + -webkit-transition: .2s; + transition: .2s; +} + +tags tag x { + font: 14px/14px Serif; + width: 14px; + height: 14px; + text-align: center; + border-radius: 50px; + position: absolute; + z-index: 1; + right: -webkit-calc(0.5em - 2px); + right: calc(0.5em - 2px); + top: 50%; + cursor: pointer; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + -webkit-transition: .2s ease-out; + transition: .2s ease-out; +} + +tags tag x::after { + content: "\00D7"; +} + +tags tag x:hover { + color: white; + background: #c77777; +} + +tags tag x:hover + div > span { + opacity: .5; +} + +tags tag x:hover + div::before { + background: rgba(211, 148, 148, 0.44); + -webkit-transition: .2s; + transition: .2s; +} + +tags input,tags textarea { + border: 0; + display: none; +} + +tags > div { + display: inline-block; + min-width: 10px; + margin: 5px; + padding: 0.3em 0.5em; + position: relative; + vertical-align: top; +} + +tags > div > input { + display: block; + min-width: 130px; +} + +tags > div > input:focus { + outline: none; +} + +tags > div > input.placeholder ~ span { + opacity: .5; + -webkit-transform: none; + -ms-transform: none; + transform: none; +} + +tags > div > span { + opacity: 0; + line-height: 1.8; + position: absolute; + top: 0; + z-index: 1; + white-space: nowrap; + pointer-events: none; + -webkit-transform: translatex(6px); + -ms-transform: translatex(6px); + transform: translatex(6px); + -webkit-transition: .15s ease-out; + transition: .15s ease-out; +} + +@-webkit-keyframes tags--bump { + 30% { + box-shadow: 0 0 0 4px #E5E5E5; + } +} + +@keyframes tags--bump { + 30% { + box-shadow: 0 0 0 4px #E5E5E5; + } +} + +@-webkit-keyframes tagify--pulse { + 25% { + background: rgba(211, 148, 148, 0.6); + } +} + +@keyframes tagify--pulse { + 25% { + background: rgba(211, 148, 148, 0.6); + } +} diff --git a/static/js/tagify.js b/static/js/tagify.js new file mode 100644 index 0000000..2b3ea4e --- /dev/null +++ b/static/js/tagify.js @@ -0,0 +1,238 @@ +/** + * Tagify - jQuery tags input plugin + * By Yair Even-Or (2016) + * Don't sell this code. (c) + * https://github.com/yairEO/tagify + */ +function Tagify( input, settings ){ + // protection + if( !input ){ + console.warn('Tagify: ', 'invalid input element ', input) + return this; + } + + settings = typeof settings == 'object' ? settings : {}; // make sure settings is an 'object' + + this.settings = { + duplicates : settings.duplicates || false, // flag - allow tuplicate tags + enforeWhitelist : settings.enforeWhitelist || false, // flag - should ONLY use tags allowed in whitelist + autocomplete : settings.autocomplete || true, // flag - show native suggeestions list as you type + whitelist : settings.whitelist || [], // is this list has any items, then only allow tags from this list + blacklist : settings.blacklist || [] // a list of non-allowed tags + }; + + this.id = Math.random().toString(36).substr(2,9), // almost-random ID (because, fuck it) + this.value = []; // An array holding all the (currently used) tags + this.DOM = {}; // Store all relevant DOM elements in an Object + this.build(input); + this.events(); +} + +Tagify.prototype = { + build : function( input ){ + var that = this, + value = input.value; + + this.DOM.originalInput = input; + this.DOM.scope = document.createElement('tags'); + this.DOM.scope.innerHTML = '