{- This file is part of Vervis. - - Written in 2016, 2018, 2019, 2020 by fr33domlover <fr33domlover@riseup.net>. - - ♡ Copying is an act of love. Please copy, reuse and share. - - The author(s) have dedicated all copyright and related and neighboring - rights to this software to the public domain worldwide. This software is - distributed without any warranty. - - You should have received a copy of the CC0 Public Domain Dedication along - with this software. If not, see - <http://creativecommons.org/publicdomain/zero/1.0/>. -} module Vervis.Handler.Ticket ( getTicketsR , getTicketTreeR , getTicketNewR , getTicketR , putTicketR , deleteTicketR , postTicketR , getTicketEditR , postTicketAcceptR , postTicketCloseR , postTicketOpenR , postTicketClaimR , postTicketUnclaimR , getTicketAssignR , postTicketAssignR , postTicketUnassignR , getClaimRequestsPersonR , getClaimRequestsProjectR , getClaimRequestsTicketR , postClaimRequestsTicketR , getClaimRequestNewR , getTicketDiscussionR , postTicketDiscussionR , getMessageR , postTicketMessageR , getTicketTopReplyR , getTicketReplyR , getTicketDepsR , postTicketDepsR , getTicketDepNewR , postTicketDepOldR , deleteTicketDepOldR , getTicketReverseDepsR , getTicketDepR , getTicketParticipantsR , getTicketTeamR , getTicketEventsR , getSharerTicketsR , getSharerTicketR , getSharerTicketDiscussionR , getSharerTicketDepsR , getSharerTicketReverseDepsR , getSharerTicketFollowersR , getSharerTicketTeamR , getSharerTicketEventsR ) where import Control.Applicative (liftA2) import Control.Monad import Control.Monad.IO.Class (liftIO) import Control.Monad.Logger.CallStack import Control.Monad.Trans.Except import Data.Aeson (encode) import Data.Bifunctor import Data.Bitraversable import Data.Bool (bool) import Data.Default.Class (def) import Data.Foldable (traverse_) import Data.Maybe import Data.Monoid ((<>)) import Data.Text (Text) import Data.Time.Calendar (Day (..)) import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Time.Format (formatTime, defaultTimeLocale) import Data.Traversable (for) import Database.Persist import Network.HTTP.Types (StdMethod (DELETE, POST)) import Text.Blaze.Html (Html, toHtml, preEscapedToHtml) import Text.Blaze.Html.Renderer.Text import Text.HTML.SanitizeXSS import Yesod.Auth (requireAuthId, maybeAuthId) import Yesod.Core hiding (logWarn) import Yesod.Core.Handler import Yesod.Form.Functions (runFormGet, runFormPost) import Yesod.Form.Types (FormResult (..)) import Yesod.Persist.Core (runDB, get404, getBy404) import qualified Data.ByteString.Lazy as BL import qualified Data.Text as T (filter, intercalate, pack) import qualified Data.Text.Lazy as TL import qualified Database.Esqueleto as E import Database.Persist.Sql.Graph.TransitiveReduction (trrFix) import Data.Aeson.Encode.Pretty.ToEncoding import Data.MediaType import Network.FedURI import Web.ActivityPub hiding (Ticket (..), TicketDependency) import Yesod.ActivityPub import Yesod.Auth.Unverified import Yesod.FedURI import Yesod.Hashids import Yesod.MonadSite import Yesod.RenderSource import qualified Web.ActivityPub as AP import Data.Either.Local import Data.Maybe.Local (partitionMaybePairs) import Data.Paginate.Local import Database.Persist.Local import Yesod.Persist.Local import Vervis.API import Vervis.Federation import Vervis.FedURI import Vervis.Form.Ticket import Vervis.Foundation import Vervis.Handler.Discussion import Vervis.GraphProxy (ticketDepGraph) import Vervis.Model import Vervis.Model.Ident import Vervis.Model.Ticket import Vervis.Model.Workflow import Vervis.Paginate import Vervis.Settings import Vervis.Style import Vervis.Ticket import Vervis.TicketFilter (filterTickets) import Vervis.Time (showDate) import Vervis.Widget (buttonW) import Vervis.Widget.Discussion (discussionW) import Vervis.Widget.Sharer import Vervis.Widget.Ticket getTicketsR :: ShrIdent -> PrjIdent -> Handler TypedContent getTicketsR shr prj = selectRep $ do provideRep $ do ((filtResult, filtWidget), filtEnctype) <- runFormGet ticketFilterForm let tf = case filtResult of FormSuccess filt -> filt FormMissing -> def FormFailure l -> error $ "Ticket filter form failed: " ++ show l (total, pages, mpage) <- runDB $ do Entity sid _ <- getBy404 $ UniqueSharer shr Entity jid _ <- getBy404 $ UniqueProject prj sid let countAllTickets = count [TicketProjectLocalProject ==. jid] selectTickets off lim = getTicketSummaries (filterTickets tf) (Just $ \ t -> [E.asc $ t E.^. TicketId]) (Just (off, lim)) jid getPageAndNavCount countAllTickets selectTickets case mpage of Nothing -> redirectFirstPage here Just (rows, navModel) -> let pageNav = navWidget navModel in defaultLayout $(widgetFile "ticket/list") provideAP' $ do (total, pages, mpage) <- runDB $ do Entity sid _ <- getBy404 $ UniqueSharer shr Entity jid _ <- getBy404 $ UniqueProject prj sid let countAllTickets = count [TicketProjectLocalProject ==. jid] selectTickets off lim = do tids <- map (ticketProjectLocalTicket . entityVal) <$> selectList [TicketProjectLocalProject ==. jid] [ Desc TicketProjectLocalTicket , OffsetBy off , LimitTo lim ] selectKeysList [LocalTicketTicket <-. tids] [Desc LocalTicketTicket] getPageAndNavCount countAllTickets selectTickets encodeRouteHome <- getEncodeRouteHome encodeRouteLocal <- getEncodeRouteLocal encodeRoutePageLocal <- getEncodeRoutePageLocal let pageUrl = encodeRoutePageLocal here host <- asksSite siteInstanceHost encodeTicketKey <- getEncodeKeyHashid let ticketUrl = TicketR shr prj . encodeTicketKey return $ case mpage of Nothing -> encodeStrict $ Doc host $ Collection { collectionId = encodeRouteLocal here , collectionType = CollectionTypeOrdered , collectionTotalItems = Just total , collectionCurrent = Nothing , collectionFirst = Just $ pageUrl 1 , collectionLast = Just $ pageUrl pages , collectionItems = [] :: [Text] } Just (tickets, navModel) -> let current = nmCurrent navModel in encodeStrict $ Doc host $ CollectionPage { collectionPageId = pageUrl current , collectionPageType = CollectionPageTypeOrdered , collectionPageTotalItems = Nothing , collectionPageCurrent = Just $ pageUrl current , collectionPageFirst = Just $ pageUrl 1 , collectionPageLast = Just $ pageUrl pages , collectionPagePartOf = encodeRouteLocal here , collectionPagePrev = if current > 1 then Just $ pageUrl $ current - 1 else Nothing , collectionPageNext = if current < pages then Just $ pageUrl $ current + 1 else Nothing , collectionPageStartIndex = Nothing , collectionPageItems = map (encodeRouteHome . ticketUrl) tickets } where here = TicketsR shr prj encodeStrict = BL.toStrict . encode getTicketTreeR :: ShrIdent -> PrjIdent -> Handler Html getTicketTreeR shr prj = do (summaries, deps) <- runDB $ do Entity sid _ <- getBy404 $ UniqueSharer shr Entity jid _ <- getBy404 $ UniqueProject prj sid (,) <$> getTicketSummaries Nothing Nothing Nothing jid <*> getTicketDepEdges jid defaultLayout $ ticketTreeDW shr prj summaries deps getTicketNewR :: ShrIdent -> PrjIdent -> Handler Html getTicketNewR shr prj = do wid <- runDB $ do Entity sid _ <- getBy404 $ UniqueSharer shr Entity _ j <- getBy404 $ UniqueProject prj sid return $ projectWorkflow j ((_result, widget), enctype) <- runFormPost $ newTicketForm wid defaultLayout $(widgetFile "ticket/new") getTicketR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler TypedContent getTicketR shar proj ltkhid = do mpid <- maybeAuthId ( wshr, wfl, author, massignee, mcloser, ticket, lticket, tparams, eparams, cparams, deps, rdeps) <- runDB $ do (jid, wshr, wid, wfl) <- do Entity s sharer <- getBy404 $ UniqueSharer shar Entity p project <- getBy404 $ UniqueProject proj s w <- get404 $ projectWorkflow project wsharer <- if workflowSharer w == s then return sharer else get404 $ workflowSharer w return ( p , sharerIdent wsharer , projectWorkflow project , workflowIdent w ) ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket Entity tplid tpl <- getBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == jid) notFound author <- requireEitherAlt (do mtal <- getValBy $ UniqueTicketAuthorLocal ltid for mtal $ \ tal -> do _ <- getBy404 $ UniqueTicketUnderProjectProject tplid p <- getJust $ ticketAuthorLocalAuthor tal getJust $ personIdent p ) (do mtar <- getValBy $ UniqueTicketAuthorRemote tplid for mtar $ \ tar -> do ra <- getJust $ ticketAuthorRemoteAuthor tar ro <- getJust $ remoteActorIdent ra i <- getJust $ remoteObjectInstance ro return (i, ro, ra) ) "Ticket doesn't have author" "Ticket has both local and remote author" ticket <- get404 tid massignee <- for (ticketAssignee ticket) $ \ apid -> do person <- get404 apid sharer <- get404 $ personIdent person return (sharer, fromMaybe False $ (== apid) <$> mpid) mcloser <- case ticketStatus ticket of TSClosed -> case ticketCloser ticket of Just pidCloser -> Just <$> do person <- getJust pidCloser getJust $ personIdent person Nothing -> error "Closer not set for closed ticket" _ -> case ticketCloser ticket of Just _ -> error "Closer set for open ticket" Nothing -> return Nothing tparams <- getTicketTextParams tid wid eparams <- getTicketEnumParams tid wid cparams <- getTicketClasses tid wid deps <- E.select $ E.from $ \ (dep `E.InnerJoin` t `E.InnerJoin` lt) -> do E.on $ t E.^. TicketId E.==. lt E.^. LocalTicketTicket E.on $ dep E.^. TicketDependencyChild E.==. t E.^. TicketId E.where_ $ dep E.^. TicketDependencyParent E.==. E.val tid return (lt E.^. LocalTicketId, t) rdeps <- E.select $ E.from $ \ (dep `E.InnerJoin` t `E.InnerJoin` lt) -> do E.on $ t E.^. TicketId E.==. lt E.^. LocalTicketTicket E.on $ dep E.^. TicketDependencyParent E.==. t E.^. TicketId E.where_ $ dep E.^. TicketDependencyChild E.==. E.val tid return (lt E.^. LocalTicketId, t) return ( wshr, wfl , author, massignee, mcloser, ticket, lticket , tparams, eparams, cparams , deps, rdeps ) encodeHid <- getEncodeKeyHashid let desc :: Widget desc = toWidget $ preEscapedToMarkup $ ticketDescription ticket discuss = discussionW (return $ localTicketDiscuss lticket) (TicketTopReplyR shar proj ltkhid) (TicketReplyR shar proj ltkhid . encodeHid) cRelevant <- newIdent cIrrelevant <- newIdent let relevant filt = bool cIrrelevant cRelevant $ case ticketStatus ticket of TSNew -> wffNew filt TSTodo -> wffTodo filt TSClosed -> wffClosed filt hLocal <- getsYesod siteInstanceHost encodeRouteLocal <- getEncodeRouteLocal encodeRouteHome <- getEncodeRouteHome let host = case author of Left _ -> hLocal Right (i, _, _) -> instanceHost i ticketAP = AP.Ticket { AP.ticketLocal = Just ( hLocal , AP.TicketLocal { AP.ticketId = encodeRouteLocal $ TicketR shar proj ltkhid , AP.ticketContext = encodeRouteLocal $ ProjectR shar proj , AP.ticketReplies = encodeRouteLocal $ TicketDiscussionR shar proj ltkhid , AP.ticketParticipants = encodeRouteLocal $ TicketParticipantsR shar proj ltkhid , AP.ticketTeam = encodeRouteLocal $ TicketTeamR shar proj ltkhid , AP.ticketEvents = encodeRouteLocal $ TicketEventsR shar proj ltkhid , AP.ticketDeps = encodeRouteLocal $ TicketDepsR shar proj ltkhid , AP.ticketReverseDeps = encodeRouteLocal $ TicketReverseDepsR shar proj ltkhid } ) , AP.ticketAttributedTo = case author of Left sharer -> encodeRouteLocal $ SharerR $ sharerIdent sharer Right (_inztance, object, _actor) -> remoteObjectIdent object , AP.ticketPublished = Just $ ticketCreated ticket , AP.ticketUpdated = Nothing -- , AP.ticketName = Just $ "#" <> T.pack (show num) , AP.ticketSummary = TextHtml $ ticketTitle ticket , AP.ticketContent = TextHtml $ ticketDescription ticket , AP.ticketSource = TextPandocMarkdown $ ticketSource ticket , AP.ticketAssignedTo = encodeRouteHome . SharerR . sharerIdent . fst <$> massignee , AP.ticketIsResolved = ticketStatus ticket == TSClosed } provideHtmlAndAP' host ticketAP $ let followButton = followW (TicketFollowR shar proj ltkhid) (TicketUnfollowR shar proj ltkhid) (return $ localTicketFollowers lticket) in $(widgetFile "ticket/one") putTicketR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html putTicketR shr prj ltkhid = do (tid, ticket, wid) <- runDB $ do Entity sid _sharer <- getBy404 $ UniqueSharer shr Entity pid project <- getBy404 $ UniqueProject prj sid ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == pid) notFound return (tid, ticket, projectWorkflow project) ((result, widget), enctype) <- runFormPost $ editTicketContentForm tid ticket wid case result of FormSuccess (ticket', tparams, eparams, cparams) -> do newDescHtml <- case renderPandocMarkdown $ ticketSource ticket' of Left err -> do setMessage $ toHtml err redirect $ TicketEditR shr prj ltkhid Right t -> return t let ticket'' = ticket' { ticketDescription = newDescHtml } runDB $ do replace tid ticket'' let (tdel, tins, tupd) = partitionMaybePairs tparams deleteWhere [TicketParamTextId <-. tdel] let mktparam (fid, v) = TicketParamText { ticketParamTextTicket = tid , ticketParamTextField = fid , ticketParamTextValue = v } insertMany_ $ map mktparam tins traverse_ (\ (aid, (_fid, v)) -> update aid [TicketParamTextValue =. v] ) tupd let (edel, eins, eupd) = partitionMaybePairs eparams deleteWhere [TicketParamEnumId <-. edel] let mkeparam (fid, v) = TicketParamEnum { ticketParamEnumTicket = tid , ticketParamEnumField = fid , ticketParamEnumValue = v } insertMany_ $ map mkeparam eins traverse_ (\ (aid, (_fid, v)) -> update aid [TicketParamEnumValue =. v] ) eupd let (cdel, cins, _ckeep) = partitionMaybePairs cparams deleteWhere [TicketParamClassId <-. cdel] let mkcparam fid = TicketParamClass { ticketParamClassTicket = tid , ticketParamClassField = fid } insertMany_ $ map mkcparam cins setMessage "Ticket updated." redirect $ TicketR shr prj ltkhid FormMissing -> do setMessage "Field(s) missing." defaultLayout $(widgetFile "ticket/edit") FormFailure _l -> do setMessage "Ticket update failed, see errors below." defaultLayout $(widgetFile "ticket/edit") deleteTicketR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html deleteTicketR _shr _prj _ltkhid = --TODO: I can easily implement this, but should it even be possible to --delete tickets? error "Not implemented" postTicketR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketR shr prj ltkhid = do mmethod <- lookupPostParam "_method" case mmethod of Just "PUT" -> putTicketR shr prj ltkhid Just "DELETE" -> deleteTicketR shr prj ltkhid _ -> notFound getTicketEditR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html getTicketEditR shr prj ltkhid = do (tid, ticket, wid) <- runDB $ do Entity sid _sharer <- getBy404 $ UniqueSharer shr Entity pid project <- getBy404 $ UniqueProject prj sid ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == pid) notFound return (tid, ticket, projectWorkflow project) ((_result, widget), enctype) <- runFormPost $ editTicketContentForm tid ticket wid defaultLayout $(widgetFile "ticket/edit") postTicketAcceptR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketAcceptR shr prj ltkhid = do succ <- runDB $ do Entity tid ticket <- do Entity s _ <- getBy404 $ UniqueSharer shr Entity p _ <- getBy404 $ UniqueProject prj s ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == p) notFound return $ Entity tid ticket case ticketStatus ticket of TSNew -> do update tid [TicketStatus =. TSTodo] return True _ -> return False setMessage $ if succ then "Ticket accepted." else "Ticket is already accepted." redirect $ TicketR shr prj ltkhid postTicketCloseR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketCloseR shr prj ltkhid = do pid <- requireAuthId now <- liftIO getCurrentTime succ <- runDB $ do Entity tid ticket <- do Entity s _ <- getBy404 $ UniqueSharer shr Entity p _ <- getBy404 $ UniqueProject prj s ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == p) notFound return $ Entity tid ticket case ticketStatus ticket of TSClosed -> return False _ -> do update tid [ TicketAssignee =. Nothing , TicketStatus =. TSClosed , TicketClosed =. now , TicketCloser =. Just pid ] return True setMessage $ if succ then "Ticket closed." else "Ticket is already closed." redirect $ TicketR shr prj ltkhid postTicketOpenR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketOpenR shr prj ltkhid = do pid <- requireAuthId now <- liftIO getCurrentTime succ <- runDB $ do Entity tid ticket <- do Entity s _ <- getBy404 $ UniqueSharer shr Entity p _ <- getBy404 $ UniqueProject prj s ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == p) notFound return $ Entity tid ticket case ticketStatus ticket of TSClosed -> do update tid [ TicketStatus =. TSTodo , TicketCloser =. Nothing ] return True _ -> return False setMessage $ if succ then "Ticket reopened" else "Ticket is already open." redirect $ TicketR shr prj ltkhid postTicketClaimR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketClaimR shr prj ltkhid = do pid <- requireAuthId mmsg <- runDB $ do Entity tid ticket <- do Entity s _ <- getBy404 $ UniqueSharer shr Entity p _ <- getBy404 $ UniqueProject prj s ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == p) notFound return $ Entity tid ticket case (ticketStatus ticket, ticketAssignee ticket) of (TSNew, _) -> return $ Just "The ticket isn’t accepted yet. Can’t claim it." (TSClosed, _) -> return $ Just "The ticket is closed. Can’t claim closed tickets." (TSTodo, Just _) -> return $ Just "The ticket is already assigned to someone." (TSTodo, Nothing) -> do update tid [TicketAssignee =. Just pid] return Nothing setMessage $ fromMaybe "The ticket is now assigned to you." mmsg redirect $ TicketR shr prj ltkhid postTicketUnclaimR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketUnclaimR shr prj ltkhid = do pid <- requireAuthId mmsg <- runDB $ do Entity tid ticket <- do Entity s _ <- getBy404 $ UniqueSharer shr Entity p _ <- getBy404 $ UniqueProject prj s ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == p) notFound return $ Entity tid ticket case ((== pid) <$> ticketAssignee ticket, ticketStatus ticket) of (Nothing, _) -> return $ Just "The ticket is already unassigned." (Just False, _) -> return $ Just "The ticket is assigned to someone else." (Just True, TSNew) -> do logWarn "Found a new claimed ticket, this is invalid" return $ Just "The ticket isn’t accepted yet. Can’t unclaim it." (Just True, TSClosed) -> do logWarn "Found a closed claimed ticket, this is invalid" return $ Just "The ticket is closed. Can’t unclaim closed tickets." (Just True, TSTodo) -> do update tid [TicketAssignee =. Nothing] return Nothing setMessage $ fromMaybe "The ticket is now unassigned." mmsg redirect $ TicketR shr prj ltkhid getTicketAssignR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html getTicketAssignR shr prj ltkhid = do vpid <- requireAuthId (jid, Entity tid ticket) <- runDB $ do Entity s _ <- getBy404 $ UniqueSharer shr Entity j _ <- getBy404 $ UniqueProject prj s ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == j) notFound return (j, Entity tid ticket) let msg t = do setMessage t redirect $ TicketR shr prj ltkhid case (ticketStatus ticket, ticketAssignee ticket) of (TSNew, _) -> msg "The ticket isn’t accepted yet. Can’t assign it." (TSClosed, _) -> msg "The ticket is closed. Can’t assign it." (TSTodo, Just _) -> msg "The ticket is already assigned to someone." (TSTodo, Nothing) -> do ((_result, widget), enctype) <- runFormPost $ assignTicketForm vpid jid defaultLayout $(widgetFile "ticket/assign") postTicketAssignR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketAssignR shr prj ltkhid = do vpid <- requireAuthId (jid, Entity tid ticket) <- runDB $ do Entity s _ <- getBy404 $ UniqueSharer shr Entity j _ <- getBy404 $ UniqueProject prj s ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == j) notFound return (j, Entity tid ticket) let msg t = do setMessage t redirect $ TicketR shr prj ltkhid case (ticketStatus ticket, ticketAssignee ticket) of (TSNew, _) -> msg "The ticket isn’t accepted yet. Can’t assign it." (TSClosed, _) -> msg "The ticket is closed. Can’t assign it." (TSTodo, Just _) -> msg "The ticket is already assigned to someone." (TSTodo, Nothing) -> do ((result, widget), enctype) <- runFormPost $ assignTicketForm vpid jid case result of FormSuccess pid -> do sharer <- runDB $ do update tid [TicketAssignee =. Just pid] person <- getJust pid getJust $ personIdent person let si = sharerIdent sharer msg $ toHtml $ "The ticket is now assigned to " <> shr2text si <> "." FormMissing -> do setMessage "Field(s) missing." defaultLayout $(widgetFile "ticket/assign") FormFailure _l -> do setMessage "Ticket assignment failed, see errors below." defaultLayout $(widgetFile "ticket/assign") postTicketUnassignR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketUnassignR shr prj ltkhid = do pid <- requireAuthId mmsg <- runDB $ do Entity tid ticket <- do Entity s _ <- getBy404 $ UniqueSharer shr Entity p _ <- getBy404 $ UniqueProject prj s ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket ticket <- getJust tid tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == p) notFound return $ Entity tid ticket case ((== pid) <$> ticketAssignee ticket, ticketStatus ticket) of (Nothing, _) -> return $ Just "The ticket is already unassigned." (Just True, _) -> return $ Just "The ticket is assigned to you, unclaim instead." (Just False, TSNew) -> do logWarn "Found a new claimed ticket, this is invalid" return $ Just "The ticket isn’t accepted yet. Can’t unclaim it." (Just False, TSClosed) -> do logWarn "Found a closed claimed ticket, this is invalid" return $ Just "The ticket is closed. Can’t unclaim closed tickets." (Just False, TSTodo) -> do update tid [TicketAssignee =. Nothing] return Nothing setMessage $ fromMaybe "The ticket is now unassigned." mmsg redirect $ TicketR shr prj ltkhid -- | The logged-in user gets a list of the ticket claim requests they have -- opened, in any project. getClaimRequestsPersonR :: Handler Html getClaimRequestsPersonR = do pid <- requireAuthId rqs <- runDB $ E.select $ E.from $ \ (tcr `E.InnerJoin` ticket `E.InnerJoin` lticket `E.InnerJoin` tpl `E.InnerJoin` project `E.InnerJoin` sharer) -> do E.on $ project E.^. ProjectSharer E.==. sharer E.^. SharerId E.on $ tpl E.^. TicketProjectLocalProject E.==. project E.^. ProjectId E.on $ ticket E.^. TicketId E.==. tpl E.^. TicketProjectLocalTicket E.on $ ticket E.^. TicketId E.==. lticket E.^. LocalTicketTicket E.on $ tcr E.^. TicketClaimRequestTicket E.==. ticket E.^. TicketId E.where_ $ tcr E.^. TicketClaimRequestPerson E.==. E.val pid E.orderBy [E.desc $ tcr E.^. TicketClaimRequestCreated] return ( sharer E.^. SharerIdent , project E.^. ProjectIdent , lticket E.^. LocalTicketId , ticket E.^. TicketTitle , tcr E.^. TicketClaimRequestCreated ) encodeHid <- getEncodeKeyHashid defaultLayout $(widgetFile "person/claim-requests") -- | Get a list of ticket claim requests for a given project. getClaimRequestsProjectR :: ShrIdent -> PrjIdent -> Handler Html getClaimRequestsProjectR shr prj = do rqs <- runDB $ do Entity sid _ <- getBy404 $ UniqueSharer shr Entity jid _ <- getBy404 $ UniqueProject prj sid E.select $ E.from $ \ ( tcr `E.InnerJoin` ticket `E.InnerJoin` lticket `E.InnerJoin` tpl `E.InnerJoin` person `E.InnerJoin` sharer ) -> do E.on $ person E.^. PersonIdent E.==. sharer E.^. SharerId E.on $ tcr E.^. TicketClaimRequestPerson E.==. person E.^. PersonId E.on $ ticket E.^. TicketId E.==. tpl E.^. TicketProjectLocalTicket E.on $ ticket E.^. TicketId E.==. lticket E.^. LocalTicketTicket E.on $ tcr E.^. TicketClaimRequestTicket E.==. ticket E.^. TicketId E.where_ $ tpl E.^. TicketProjectLocalProject E.==. E.val jid E.orderBy [E.desc $ tcr E.^. TicketClaimRequestCreated] return ( sharer , lticket E.^. LocalTicketId , ticket E.^. TicketTitle , tcr E.^. TicketClaimRequestCreated ) encodeHid <- getEncodeKeyHashid defaultLayout $(widgetFile "project/claim-request/list") -- | Get a list of ticket claim requests for a given ticket. getClaimRequestsTicketR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html getClaimRequestsTicketR shr prj ltkhid = do rqs <- runDB $ do Entity sid _ <- getBy404 $ UniqueSharer shr Entity jid _ <- getBy404 $ UniqueProject prj sid ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == jid) notFound E.select $ E.from $ \ (tcr `E.InnerJoin` person `E.InnerJoin` sharer) -> do E.on $ person E.^. PersonIdent E.==. sharer E.^. SharerId E.on $ tcr E.^. TicketClaimRequestPerson E.==. person E.^. PersonId E.where_ $ tcr E.^. TicketClaimRequestTicket E.==. E.val tid E.orderBy [E.desc $ tcr E.^. TicketClaimRequestCreated] return (sharer, tcr) defaultLayout $(widgetFile "ticket/claim-request/list") getClaimRequestNewR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html getClaimRequestNewR shr prj ltkhid = do ((_result, widget), etype) <- runFormPost claimRequestForm defaultLayout $(widgetFile "ticket/claim-request/new") postClaimRequestsTicketR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postClaimRequestsTicketR shr prj ltkhid = do ((result, widget), etype) <- runFormPost claimRequestForm case result of FormSuccess msg -> do now <- liftIO getCurrentTime pid <- requireAuthId runDB $ do tid <- do Entity s _ <- getBy404 $ UniqueSharer shr Entity j _ <- getBy404 $ UniqueProject prj s ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == j) notFound return tid let cr = TicketClaimRequest { ticketClaimRequestPerson = pid , ticketClaimRequestTicket = tid , ticketClaimRequestMessage = msg , ticketClaimRequestCreated = now } insert_ cr setMessage "Ticket claim request opened." redirect $ TicketR shr prj ltkhid FormMissing -> do setMessage "Field(s) missing." defaultLayout $(widgetFile "ticket/claim-request/new") FormFailure _l -> do setMessage "Submission failed, see errors below." defaultLayout $(widgetFile "ticket/claim-request/new") selectDiscussionId :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> AppDB DiscussionId selectDiscussionId shr prj ltkhid = do Entity sid _sharer <- getBy404 $ UniqueSharer shr Entity jid _project <- getBy404 $ UniqueProject prj sid ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == jid) notFound return $ localTicketDiscuss lticket getTicketDiscussionR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html getTicketDiscussionR shar proj ltkhid = do encodeHid <- getEncodeKeyHashid getDiscussion (TicketReplyR shar proj ltkhid . encodeHid) (TicketTopReplyR shar proj ltkhid) (selectDiscussionId shar proj ltkhid) postTicketDiscussionR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketDiscussionR shr prj ltkhid = do hLocal <- getsYesod $ appInstanceHost . appSettings postTopReply hLocal [ProjectR shr prj] [ ProjectFollowersR shr prj , TicketParticipantsR shr prj ltkhid , TicketTeamR shr prj ltkhid ] (TicketR shr prj ltkhid) (ProjectR shr prj) (TicketDiscussionR shr prj ltkhid) (const $ TicketR shr prj ltkhid) getMessageR :: ShrIdent -> KeyHashid LocalMessage -> Handler TypedContent getMessageR shr hid = do lmid <- decodeKeyHashid404 hid getDiscussionMessage shr lmid postTicketMessageR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> KeyHashid Message -> Handler Html postTicketMessageR shr prj ltkhid mkhid = do encodeHid <- getEncodeKeyHashid mid <- decodeKeyHashid404 mkhid hLocal <- getsYesod $ appInstanceHost . appSettings postReply hLocal [ProjectR shr prj] [ ProjectFollowersR shr prj , TicketParticipantsR shr prj ltkhid , TicketTeamR shr prj ltkhid ] (TicketR shr prj ltkhid) (ProjectR shr prj) (TicketReplyR shr prj ltkhid . encodeHid) (TicketMessageR shr prj ltkhid . encodeHid) (const $ TicketR shr prj ltkhid) (selectDiscussionId shr prj ltkhid) mid getTicketTopReplyR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html getTicketTopReplyR shr prj ltkhid = getTopReply $ TicketDiscussionR shr prj ltkhid getTicketReplyR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> KeyHashid Message -> Handler Html getTicketReplyR shr prj ltkhid mkhid = do encodeHid <- getEncodeKeyHashid mid <- decodeKeyHashid404 mkhid getReply (TicketReplyR shr prj ltkhid . encodeHid) (TicketMessageR shr prj ltkhid . encodeHid) (selectDiscussionId shr prj ltkhid) mid getTicketDeps :: Bool -> ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler TypedContent getTicketDeps forward shr prj ltkhid = do (deps, rows) <- unzip <$> runDB getDepsFromDB depsAP <- makeDepsCollection deps encodeHid <- getEncodeKeyHashid provideHtmlAndAP depsAP $(widgetFile "ticket/dep/list") where getDepsFromDB = do let from' = if forward then TicketDependencyParent else TicketDependencyChild to' = if forward then TicketDependencyChild else TicketDependencyParent Entity sid _ <- getBy404 $ UniqueSharer shr Entity jid _ <- getBy404 $ UniqueProject prj sid ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == jid) notFound fmap (map toRow) $ E.select $ E.from $ \ ( td `E.InnerJoin` t `E.InnerJoin` lt `E.InnerJoin` tpl `E.LeftOuterJoin` (tal `E.InnerJoin` p `E.InnerJoin` s) `E.LeftOuterJoin` (tar `E.InnerJoin` ra `E.InnerJoin` ro `E.InnerJoin` i) ) -> do E.on $ ro E.?. RemoteObjectInstance E.==. i E.?. InstanceId E.on $ ra E.?. RemoteActorIdent E.==. ro E.?. RemoteObjectId E.on $ tar E.?. TicketAuthorRemoteAuthor E.==. ra E.?. RemoteActorId E.on $ E.just (tpl E.^. TicketProjectLocalId) E.==. tar E.?. TicketAuthorRemoteTicket E.on $ p E.?. PersonIdent E.==. s E.?. SharerId E.on $ tal E.?. TicketAuthorLocalAuthor E.==. p E.?. PersonId E.on $ E.just (lt E.^. LocalTicketId) E.==. tal E.?. TicketAuthorLocalTicket E.on $ t E.^. TicketId E.==. tpl E.^. TicketProjectLocalTicket E.on $ t E.^. TicketId E.==. lt E.^. LocalTicketTicket E.on $ td E.^. to' E.==. t E.^. TicketId E.where_ $ td E.^. from' E.==. E.val tid E.orderBy [E.asc $ t E.^. TicketId] return ( td E.^. TicketDependencyId , lt E.^. LocalTicketId , s , i , ro , ra , t E.^. TicketTitle , t E.^. TicketStatus ) where toRow (E.Value dep, E.Value ltid, ms, mi, mro, mra, E.Value title, E.Value status) = ( dep , ( ltid , case (ms, mi, mro, mra) of (Just s, Nothing, Nothing, Nothing) -> Left $ entityVal s (Nothing, Just i, Just ro, Just ra) -> Right (entityVal i, entityVal ro, entityVal ra) _ -> error "Ticket author DB invalid state" , title , status ) ) makeDepsCollection tdids = do encodeRouteLocal <- getEncodeRouteLocal encodeRouteHome <- getEncodeRouteHome encodeKeyHashid <- getEncodeKeyHashid let here = let route = if forward then TicketDepsR else TicketReverseDepsR in route shr prj ltkhid return Collection { collectionId = encodeRouteLocal here , collectionType = CollectionTypeUnordered , collectionTotalItems = Just $ length tdids , collectionCurrent = Nothing , collectionFirst = Nothing , collectionLast = Nothing , collectionItems = map (encodeRouteHome . TicketDepR . encodeKeyHashid) tdids } getTicketDepsR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler TypedContent getTicketDepsR = getTicketDeps True postTicketDepsR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html postTicketDepsR shr prj ltkhid = do (jid, tid) <- runDB $ do Entity sid _ <- getBy404 $ UniqueSharer shr Entity jid _ <- getBy404 $ UniqueProject prj sid ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == jid) notFound return (jid, tid) ((result, widget), enctype) <- runFormPost $ ticketDepForm jid tid case result of FormSuccess ctid -> do pidAuthor <- requireVerifiedAuthId now <- liftIO getCurrentTime runDB $ do let td = TicketDependency { ticketDependencyParent = tid , ticketDependencyChild = ctid , ticketDependencyAuthor = pidAuthor , ticketDependencySummary = "(A ticket dependency)" , ticketDependencyCreated = now } insert_ td trrFix td ticketDepGraph setMessage "Ticket dependency added." redirect $ TicketR shr prj ltkhid FormMissing -> do setMessage "Field(s) missing." defaultLayout $(widgetFile "ticket/dep/new") FormFailure _l -> do setMessage "Submission failed, see errors below." defaultLayout $(widgetFile "ticket/dep/new") getTicketDepNewR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler Html getTicketDepNewR shr prj ltkhid = do (jid, tid) <- runDB $ do Entity sid _ <- getBy404 $ UniqueSharer shr Entity jid _ <- getBy404 $ UniqueProject prj sid ltid <- decodeKeyHashid404 ltkhid lticket <- get404 ltid let tid = localTicketTicket lticket tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == jid) notFound return (jid, tid) ((_result, widget), enctype) <- runFormPost $ ticketDepForm jid tid defaultLayout $(widgetFile "ticket/dep/new") postTicketDepOldR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> KeyHashid LocalTicket -> Handler Html postTicketDepOldR shr prj pnum cnum = do mmethod <- lookupPostParam "_method" case mmethod of Just "DELETE" -> deleteTicketDepOldR shr prj pnum cnum _ -> notFound deleteTicketDepOldR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> KeyHashid LocalTicket -> Handler Html deleteTicketDepOldR shr prj pnum cnum = do runDB $ do Entity sid _ <- getBy404 $ UniqueSharer shr Entity jid _ <- getBy404 $ UniqueProject prj sid pltid <- decodeKeyHashid404 pnum plt <- get404 pltid let ptid = localTicketTicket plt ptpl <- getValBy404 $ UniqueTicketProjectLocal ptid unless (ticketProjectLocalProject ptpl == jid) notFound cltid <- decodeKeyHashid404 cnum clt <- get404 cltid let ctid = localTicketTicket clt ctpl <- getValBy404 $ UniqueTicketProjectLocal ctid unless (ticketProjectLocalProject ctpl == jid) notFound Entity tdid _ <- getBy404 $ UniqueTicketDependency ptid ctid delete tdid setMessage "Ticket dependency removed." redirect $ TicketDepsR shr prj pnum getTicketReverseDepsR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler TypedContent getTicketReverseDepsR = getTicketDeps False getTicketDepR :: KeyHashid TicketDependency -> Handler TypedContent getTicketDepR tdkhid = do tdid <- decodeKeyHashid404 tdkhid ( td, (sParent, jParent, ltParent), (sChild, jChild, ltChild), (sAuthor, pAuthor) ) <- runDB $ do tdep <- get404 tdid (,,,) tdep <$> getTicket (ticketDependencyParent tdep) <*> getTicket (ticketDependencyChild tdep) <*> getAuthor (ticketDependencyAuthor tdep) encodeRouteLocal <- getEncodeRouteLocal encodeRouteHome <- getEncodeRouteHome encodeHid <- getEncodeKeyHashid let ticketRoute s j lt = TicketR (sharerIdent s) (projectIdent j) (encodeHid lt) here = TicketDepR tdkhid tdepAP = AP.TicketDependency { ticketDepId = Just $ encodeRouteHome here , ticketDepParent = encodeRouteHome $ ticketRoute sParent jParent ltParent , ticketDepChild = encodeRouteHome $ ticketRoute sChild jChild ltChild , ticketDepAttributedTo = encodeRouteLocal $ SharerR $ sharerIdent sAuthor , ticketDepPublished = Just $ ticketDependencyCreated td , ticketDepUpdated = Just $ ticketDependencyCreated td , ticketDepSummary = TextHtml $ ticketDependencySummary td } provideHtmlAndAP tdepAP $ redirectToPrettyJSON here where getTicket tid = do ltid <- do mltid <- getKeyBy $ UniqueLocalTicket tid case mltid of Nothing -> error "No LocalTicket" Just v -> return v tpl <- do mtpl <- getValBy $ UniqueTicketProjectLocal tid case mtpl of Nothing -> error "No TicketProjectLocal" Just v -> return v j <- getJust $ ticketProjectLocalProject tpl s <- getJust $ projectSharer j return (s, j, ltid) getAuthor pid = do p <- getJust pid s <- getJust $ personIdent p return (s, p) getTicketParticipantsR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler TypedContent getTicketParticipantsR shr prj ltkhid = getFollowersCollection here getFsid where here = TicketParticipantsR shr prj ltkhid getFsid = do sid <- getKeyBy404 $ UniqueSharer shr jid <- getKeyBy404 $ UniqueProject prj sid ltid <- decodeKeyHashid404 ltkhid lt <- get404 ltid let tid = localTicketTicket lt tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == jid) notFound return $ localTicketFollowers lt getTicketTeamR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler TypedContent getTicketTeamR shr prj ltkhid = do memberShrs <- runDB $ do sid <- getKeyBy404 $ UniqueSharer shr jid <- getKeyBy404 $ UniqueProject prj sid ltid <- decodeKeyHashid404 ltkhid lt <- get404 ltid let tid = localTicketTicket lt tpl <- getValBy404 $ UniqueTicketProjectLocal tid unless (ticketProjectLocalProject tpl == jid) notFound id_ <- requireEitherAlt (getKeyBy $ UniquePersonIdent sid) (getKeyBy $ UniqueGroup sid) "Found sharer that is neither person nor group" "Found sharer that is both person and group" case id_ of Left pid -> return [shr] Right gid -> do pids <- map (groupMemberPerson . entityVal) <$> selectList [GroupMemberGroup ==. gid] [] sids <- map (personIdent . entityVal) <$> selectList [PersonId <-. pids] [] map (sharerIdent . entityVal) <$> selectList [SharerId <-. sids] [] let here = TicketTeamR shr prj ltkhid encodeRouteLocal <- getEncodeRouteLocal encodeRouteHome <- getEncodeRouteHome let team = Collection { collectionId = encodeRouteLocal here , collectionType = CollectionTypeUnordered , collectionTotalItems = Just $ length memberShrs , collectionCurrent = Nothing , collectionFirst = Nothing , collectionLast = Nothing , collectionItems = map (encodeRouteHome . SharerR) memberShrs } provideHtmlAndAP team $ redirectToPrettyJSON here getTicketEventsR :: ShrIdent -> PrjIdent -> KeyHashid LocalTicket -> Handler TypedContent getTicketEventsR _shr _prj _ltkhid = error "TODO not implemented" getSharerTicket :: ShrIdent -> KeyHashid TicketAuthorLocal -> AppDB ( Entity TicketAuthorLocal , Entity LocalTicket , Entity Ticket , Either (Entity TicketProjectLocal) () ) getSharerTicket shr talkhid = do pid <- do sid <- getKeyBy404 $ UniqueSharer shr getKeyBy404 $ UniquePersonIdent sid talid <- decodeKeyHashid404 talkhid tal <- get404 talid unless (ticketAuthorLocalAuthor tal == pid) notFound let ltid = ticketAuthorLocalTicket tal lt <- getJust ltid let tid = localTicketTicket lt t <- getJust tid project <- requireEitherAlt (do mtpl <- getBy $ UniqueTicketProjectLocal tid for mtpl $ \ etpl@(Entity tplid tpl) -> do mtup1 <- getBy $ UniqueTicketUnderProjectProject tplid mtup2 <- getBy $ UniqueTicketUnderProjectAuthor talid unless (isJust mtup1 == isJust mtup2) $ error "TUP points to unrelated TAL and TPL!" unless (isNothing mtup1) notFound return etpl ) (return Nothing ) "Ticket doesn't have project" "Ticket has both local and remote project" return (Entity talid tal, Entity ltid lt, Entity tid t, project) getSharerTicketsR :: ShrIdent -> Handler TypedContent getSharerTicketsR shr = do (total, pages, mpage) <- runDB $ do sid <- getKeyBy404 $ UniqueSharer shr pid <- getKeyBy404 $ UniquePersonIdent sid getPageAndNavCount (countTickets pid) (selectTickets pid) encodeRouteHome <- getEncodeRouteHome encodeRouteLocal <- getEncodeRouteLocal encodeRoutePageLocal <- getEncodeRoutePageLocal let pageUrl = encodeRoutePageLocal here encodeTicketKey <- getEncodeKeyHashid let ticketUrl = SharerTicketR shr . encodeTicketKey case mpage of Nothing -> provide $ Collection { collectionId = encodeRouteLocal here , collectionType = CollectionTypeOrdered , collectionTotalItems = Just total , collectionCurrent = Nothing , collectionFirst = Just $ pageUrl 1 , collectionLast = Just $ pageUrl pages , collectionItems = [] :: [Text] } Just (tickets, navModel) -> let current = nmCurrent navModel in provide $ CollectionPage { collectionPageId = pageUrl current , collectionPageType = CollectionPageTypeOrdered , collectionPageTotalItems = Nothing , collectionPageCurrent = Just $ pageUrl current , collectionPageFirst = Just $ pageUrl 1 , collectionPageLast = Just $ pageUrl pages , collectionPagePartOf = encodeRouteLocal here , collectionPagePrev = if current > 1 then Just $ pageUrl $ current - 1 else Nothing , collectionPageNext = if current < pages then Just $ pageUrl $ current + 1 else Nothing , collectionPageStartIndex = Nothing , collectionPageItems = map (encodeRouteHome . ticketUrl . E.unValue) tickets } where here = SharerTicketsR shr provide :: ActivityPub a => a URIMode -> Handler TypedContent provide a = provideHtmlAndAP a $ redirectToPrettyJSON here countTickets pid = fmap toOne $ E.select $ E.from $ \ (tal `E.LeftOuterJoin` tup) -> do E.on $ E.just (tal E.^. TicketAuthorLocalId) E.==. tup E.?. TicketUnderProjectAuthor E.where_ $ tal E.^. TicketAuthorLocalAuthor E.==. E.val pid E.&&. E.isNothing (tup E.?. TicketUnderProjectId) return $ E.count $ tal E.^. TicketAuthorLocalId where toOne [x] = E.unValue x toOne [] = error "toOne = 0" toOne _ = error "toOne > 1" selectTickets pid off lim = E.select $ E.from $ \ (tal `E.LeftOuterJoin` tup) -> do E.on $ E.just (tal E.^. TicketAuthorLocalId) E.==. tup E.?. TicketUnderProjectAuthor E.where_ $ tal E.^. TicketAuthorLocalAuthor E.==. E.val pid E.&&. E.isNothing (tup E.?. TicketUnderProjectId) E.orderBy [E.desc $ tal E.^. TicketAuthorLocalId] E.offset $ fromIntegral off E.limit $ fromIntegral lim return $ tal E.^. TicketAuthorLocalId getSharerTicketR :: ShrIdent -> KeyHashid TicketAuthorLocal -> Handler TypedContent getSharerTicketR shr talkhid = do (ticket, project, massignee) <- runDB $ do (_, _, Entity _ t, tp) <- getSharerTicket shr talkhid (,,) t <$> bitraverse (\ (Entity _ tpl) -> do j <- getJust $ ticketProjectLocalProject tpl s <- getJust $ projectSharer j return (s, j) ) return tp <*> (for (ticketAssignee t) $ \ pidAssignee -> do p <- getJust pidAssignee getJust $ personIdent p ) hLocal <- getsYesod siteInstanceHost encodeRouteLocal <- getEncodeRouteLocal encodeRouteHome <- getEncodeRouteHome let ticketAP = AP.Ticket { AP.ticketLocal = Just ( hLocal , AP.TicketLocal { AP.ticketId = encodeRouteLocal $ SharerTicketR shr talkhid , AP.ticketContext = encodeRouteLocal $ case project of Left (s, j) -> ProjectR (sharerIdent s) (projectIdent j) Right () -> error "No TPR yet!" , AP.ticketReplies = encodeRouteLocal $ SharerTicketDiscussionR shr talkhid , AP.ticketParticipants = encodeRouteLocal $ SharerTicketFollowersR shr talkhid , AP.ticketTeam = encodeRouteLocal $ SharerTicketTeamR shr talkhid , AP.ticketEvents = encodeRouteLocal $ SharerTicketEventsR shr talkhid , AP.ticketDeps = encodeRouteLocal $ SharerTicketDepsR shr talkhid , AP.ticketReverseDeps = encodeRouteLocal $ SharerTicketReverseDepsR shr talkhid } ) , AP.ticketAttributedTo = encodeRouteLocal $ SharerR shr , AP.ticketPublished = Just $ ticketCreated ticket , AP.ticketUpdated = Nothing , AP.ticketSummary = TextHtml $ ticketTitle ticket , AP.ticketContent = TextHtml $ ticketDescription ticket , AP.ticketSource = TextPandocMarkdown $ ticketSource ticket , AP.ticketAssignedTo = encodeRouteHome . SharerR . sharerIdent <$> massignee , AP.ticketIsResolved = ticketStatus ticket == TSClosed } provideHtmlAndAP ticketAP $ redirectToPrettyJSON here where here = SharerTicketR shr talkhid getSharerTicketDiscussionR :: ShrIdent -> KeyHashid TicketAuthorLocal -> Handler TypedContent getSharerTicketDiscussionR shr talkhid = do (locals, remotes) <- runDB $ do (_, Entity _ lt, _, _) <- getSharerTicket shr talkhid let did = localTicketDiscuss lt (,) <$> selectLocals did <*> selectRemotes did encodeRouteLocal <- getEncodeRouteLocal encodeRouteHome <- getEncodeRouteHome encodeHid <- getEncodeKeyHashid let localUri' = localUri encodeRouteHome encodeHid replies = Collection { collectionId = encodeRouteLocal here , collectionType = CollectionTypeUnordered , collectionTotalItems = Just $ length locals + length remotes , collectionCurrent = Nothing , collectionFirst = Nothing , collectionLast = Nothing , collectionItems = map localUri' locals ++ map remoteUri remotes } provideHtmlAndAP replies $ redirectToPrettyJSON here where here = SharerTicketDiscussionR shr talkhid selectLocals did = E.select $ E.from $ \ (m `E.InnerJoin` lm `E.InnerJoin` p `E.InnerJoin` s) -> do E.on $ p E.^. PersonIdent E.==. s E.^. SharerId E.on $ lm E.^. LocalMessageAuthor E.==. p E.^. PersonId E.on $ m E.^. MessageId E.==. lm E.^. LocalMessageRest E.where_ $ m E.^. MessageRoot E.==. E.val did E.&&. E.isNothing (m E.^. MessageParent) E.&&. E.isNothing (lm E.^. LocalMessageUnlinkedParent) return (s E.^. SharerIdent, lm E.^. LocalMessageId) selectRemotes did = E.select $ E.from $ \ (m `E.InnerJoin` rm `E.InnerJoin` ro `E.InnerJoin` i) -> do E.on $ ro E.^. RemoteObjectInstance E.==. i E.^. InstanceId E.on $ rm E.^. RemoteMessageIdent E.==. ro E.^. RemoteObjectId E.on $ m E.^. MessageId E.==. rm E.^. RemoteMessageRest E.where_ $ m E.^. MessageRoot E.==. E.val did E.&&. E.isNothing (m E.^. MessageParent) E.&&. E.isNothing (rm E.^. RemoteMessageLostParent) return (i E.^. InstanceHost, ro E.^. RemoteObjectIdent) localUri encR encH (E.Value shrAuthor, E.Value lmid) = encR $ MessageR shrAuthor (encH lmid) remoteUri (E.Value h, E.Value lu) = ObjURI h lu getSharerTicketDeps :: Bool -> ShrIdent -> KeyHashid TicketAuthorLocal -> Handler TypedContent getSharerTicketDeps forward shr talkhid = do tdids <- runDB $ do (_, _, Entity tid _, _) <- getSharerTicket shr talkhid let (from, to) = if forward then (TicketDependencyParent, TicketDependencyChild) else (TicketDependencyChild, TicketDependencyParent) E.select $ E.from $ \ (td `E.InnerJoin` t) -> do E.on $ td E.^. to E.==. t E.^. TicketId E.where_ $ td E.^. from E.==. E.val tid return $ td E.^. TicketDependencyId encodeRouteLocal <- getEncodeRouteLocal encodeRouteHome <- getEncodeRouteHome encodeHid <- getEncodeKeyHashid let deps = Collection { collectionId = encodeRouteLocal here , collectionType = CollectionTypeUnordered , collectionTotalItems = Just $ length tdids , collectionCurrent = Nothing , collectionFirst = Nothing , collectionLast = Nothing , collectionItems = map (encodeRouteHome . TicketDepR . encodeHid . E.unValue) tdids } provideHtmlAndAP deps $ redirectToPrettyJSON here where here = let route = if forward then SharerTicketDepsR else SharerTicketReverseDepsR in route shr talkhid getSharerTicketDepsR :: ShrIdent -> KeyHashid TicketAuthorLocal -> Handler TypedContent getSharerTicketDepsR = getSharerTicketDeps True getSharerTicketReverseDepsR :: ShrIdent -> KeyHashid TicketAuthorLocal -> Handler TypedContent getSharerTicketReverseDepsR = getSharerTicketDeps False getSharerTicketFollowersR :: ShrIdent -> KeyHashid TicketAuthorLocal -> Handler TypedContent getSharerTicketFollowersR shr talkhid = getFollowersCollection here getFsid where here = SharerTicketFollowersR shr talkhid getFsid = do (_, Entity _ lt, _, _) <- getSharerTicket shr talkhid return $ localTicketFollowers lt getSharerTicketTeamR :: ShrIdent -> KeyHashid TicketAuthorLocal -> Handler TypedContent getSharerTicketTeamR shr talkhid = do _ <- runDB $ getSharerTicket shr talkhid encodeRouteLocal <- getEncodeRouteLocal let team = Collection { collectionId = encodeRouteLocal here , collectionType = CollectionTypeUnordered , collectionTotalItems = Just 0 , collectionCurrent = Nothing , collectionFirst = Nothing , collectionLast = Nothing , collectionItems = [] :: [Text] } provideHtmlAndAP team $ redirectToPrettyJSON here where here = SharerTicketTeamR shr talkhid getSharerTicketEventsR :: ShrIdent -> KeyHashid TicketAuthorLocal -> Handler TypedContent getSharerTicketEventsR shr talkhid = do _ <- runDB $ getSharerTicket shr talkhid encodeRouteLocal <- getEncodeRouteLocal let team = Collection { collectionId = encodeRouteLocal here , collectionType = CollectionTypeOrdered , collectionTotalItems = Just 0 , collectionCurrent = Nothing , collectionFirst = Nothing , collectionLast = Nothing , collectionItems = [] :: [Text] } provideHtmlAndAP team $ redirectToPrettyJSON here where here = SharerTicketEventsR shr talkhid