forked from filipg/gonito
1419 lines
62 KiB
Haskell
1419 lines
62 KiB
Haskell
{-# LANGUAGE DeriveGeneric #-}
|
|
{-# LANGUAGE DoAndIfThenElse #-}
|
|
|
|
module Handler.ShowChallenge where
|
|
|
|
import Import hiding (Proxy, fromList)
|
|
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3, bfs)
|
|
|
|
import qualified Data.Text.Lazy as TL
|
|
import Text.Markdown
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import qualified Data.HashMap.Strict as HMS
|
|
|
|
import qualified Yesod.Table as Table
|
|
|
|
import Control.Concurrent.Lifted (threadDelay)
|
|
|
|
import Handler.Extract
|
|
import Handler.Shared
|
|
import Handler.Runner
|
|
import Handler.Tables
|
|
import Handler.TagUtils
|
|
import Handler.MakePublic
|
|
import Handler.Dashboard
|
|
import Handler.Common
|
|
import Handler.Evaluate
|
|
import Handler.JWT
|
|
|
|
import Database.Persist.Sql (fromSqlKey)
|
|
|
|
import qualified Data.Map as Map
|
|
|
|
|
|
|
|
import Data.Maybe (fromJust)
|
|
|
|
import Text.Blaze
|
|
|
|
import Data.Aeson
|
|
|
|
import Gonito.ExtractMetadata (ExtractionOptions(..),
|
|
extractMetadataFromRepoDir,
|
|
GonitoMetadata(..),
|
|
parseTags,
|
|
Link(..))
|
|
|
|
import qualified Text.Read as TR
|
|
|
|
import GEval.Core
|
|
import GEval.EvaluationScheme
|
|
|
|
import PersistSHA1
|
|
|
|
import System.IO (readFile)
|
|
|
|
import Data.Text (pack, unpack)
|
|
|
|
import Data.List (nub)
|
|
|
|
import qualified Database.Esqueleto as E
|
|
import Database.Esqueleto ((^.))
|
|
|
|
import Data.Swagger hiding (get)
|
|
import qualified Data.Swagger as DS
|
|
|
|
import Data.Swagger.Declare
|
|
import Control.Lens hiding ((.=), (^.))
|
|
import Data.Proxy as DPR
|
|
import Data.HashMap.Strict.InsOrd (fromList)
|
|
|
|
instance ToJSON LeaderboardEntry where
|
|
toJSON entry = object
|
|
[ "submitter" .= (formatSubmitter $ leaderboardUser entry)
|
|
, "team" .= (teamIdent <$> entityVal <$> leaderboardTeam entry)
|
|
, "when" .= (submissionStamp $ leaderboardBestSubmission entry)
|
|
, "version" .= (formatVersion $ leaderboardVersion entry)
|
|
, "description" .= descriptionToBeShown (leaderboardBestSubmission entry)
|
|
(leaderboardBestVariant entry)
|
|
(leaderboardParams entry)
|
|
, "times" .= leaderboardNumberOfSubmissions entry
|
|
, "hash" .= (fromSHA1ToText $ submissionCommit $ leaderboardBestSubmission entry)
|
|
, "isPublic" .= (submissionIsPublic $ leaderboardBestSubmission entry)
|
|
, "isReevaluable" .= (leaderboardIsReevaluable entry)
|
|
, "isVisible" .= (leaderboardIsVisible entry)
|
|
]
|
|
|
|
declareLeaderboardSwagger :: Declare (Definitions Schema) Swagger
|
|
declareLeaderboardSwagger = do
|
|
-- param schemas
|
|
let challengeNameSchema = toParamSchema (Proxy :: Proxy String)
|
|
|
|
leaderboardResponse <- declareResponse (Proxy :: Proxy LeaderboardView)
|
|
|
|
return $ mempty
|
|
& paths .~
|
|
fromList [ ("/api/leaderboard/{challengeName}",
|
|
mempty & DS.get ?~ (mempty
|
|
& parameters .~ [ Inline $ mempty
|
|
& name .~ "challengeName"
|
|
& required ?~ True
|
|
& schema .~ ParamOther (mempty
|
|
& in_ .~ ParamPath
|
|
& paramSchema .~ challengeNameSchema) ]
|
|
& produces ?~ MimeList ["application/json"]
|
|
& description ?~ "Returns a leaderboard for a given challenge"
|
|
& at 200 ?~ Inline leaderboardResponse))
|
|
]
|
|
|
|
|
|
leaderboardApi :: Swagger
|
|
leaderboardApi = spec & definitions .~ defs
|
|
where
|
|
(defs, spec) = runDeclare declareLeaderboardSwagger mempty
|
|
|
|
data LeaderboardView = LeaderboardView {
|
|
leaderboardViewTests :: [Entity Test],
|
|
leaderboardViewEntries :: [LeaderboardEntryView]
|
|
}
|
|
|
|
instance ToJSON LeaderboardView where
|
|
toJSON v = object
|
|
[ "tests" .= (map getTestReference $ leaderboardViewTests v)
|
|
, "entries" .= leaderboardViewEntries v
|
|
]
|
|
|
|
instance ToSchema LeaderboardView where
|
|
declareNamedSchema _ = do
|
|
testsSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy [TestReference])
|
|
entriesSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy [LeaderboardEntryView])
|
|
return $ NamedSchema (Just "Leaderboard") $ mempty
|
|
& type_ .~ SwaggerObject
|
|
& properties .~
|
|
fromList [ ("tests", testsSchema)
|
|
, ("entries", entriesSchema)
|
|
]
|
|
& required .~ [ "tests", "entries" ]
|
|
|
|
getLeaderboardJsonR :: Text -> Handler Value
|
|
getLeaderboardJsonR challengeName = do
|
|
Entity challengeId challenge <- runDB $ getBy404 $ UniqueName challengeName
|
|
leaderboardStyle <- determineLeaderboardStyle challenge
|
|
(leaderboard, (_, tests)) <- getLeaderboardEntries 1 leaderboardStyle challengeId
|
|
return $ toJSON $ LeaderboardView {
|
|
leaderboardViewTests = tests,
|
|
leaderboardViewEntries = map (toLeaderboardEntryView tests) leaderboard }
|
|
|
|
data LeaderboardEntryView = LeaderboardEntryView {
|
|
leaderboardEntryViewEntry :: LeaderboardEntry,
|
|
leaderboardEntryViewEvaluations :: [EvaluationView]
|
|
}
|
|
|
|
addJsonKey :: Text -> Value -> Value -> Value
|
|
addJsonKey key val (Object xs) = Object $ HMS.insert key val xs
|
|
addJsonKey _ _ xs = xs
|
|
|
|
instance ToJSON LeaderboardEntryView where
|
|
toJSON v = addJsonKey "evaluations"
|
|
(toJSON $ leaderboardEntryViewEvaluations v)
|
|
(toJSON $ leaderboardEntryViewEntry v)
|
|
|
|
instance ToSchema LeaderboardEntryView where
|
|
declareNamedSchema _ = do
|
|
stringSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy String)
|
|
intSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy Int)
|
|
boolSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy Bool)
|
|
evaluationsSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy [EvaluationView])
|
|
return $ NamedSchema (Just "LeaderboardEntry") $ mempty
|
|
& type_ .~ SwaggerObject
|
|
& properties .~
|
|
fromList [ ("submitter", stringSchema)
|
|
, ("team", stringSchema)
|
|
, ("when", stringSchema)
|
|
, ("version", stringSchema)
|
|
, ("description", stringSchema)
|
|
, ("times", intSchema)
|
|
, ("hash", stringSchema)
|
|
, ("evaluations", evaluationsSchema)
|
|
, ("isPublic", boolSchema)
|
|
, ("isReevaluable", boolSchema)
|
|
, ("isVisible", boolSchema)
|
|
]
|
|
& required .~ [ "submitter", "when", "version", "description", "times", "hash", "evaluations" ]
|
|
|
|
toLeaderboardEntryView :: [(Entity Test)] -> LeaderboardEntry -> LeaderboardEntryView
|
|
toLeaderboardEntryView tests entry = LeaderboardEntryView {
|
|
leaderboardEntryViewEntry = entry,
|
|
leaderboardEntryViewEvaluations = catMaybes $
|
|
map (convertEvaluationToView (leaderboardEvaluationMap entry)) tests
|
|
}
|
|
|
|
determineLeaderboardStyle :: Challenge -> Handler LeaderboardStyle
|
|
determineLeaderboardStyle challenge = do
|
|
app <- getYesod
|
|
let leaderboardStyle = appLeaderboardStyle $ appSettings app
|
|
return $ case challengeIsCompetition challenge of
|
|
Just True -> BySubmitter
|
|
_ -> leaderboardStyle
|
|
|
|
getShowChallengeR :: Text -> Handler Html
|
|
getShowChallengeR challengeName = do
|
|
app <- getYesod
|
|
challengeEnt@(Entity challengeId challenge) <- runDB $ getBy404 $ UniqueName challengeName
|
|
leaderboardStyle <- determineLeaderboardStyle challenge
|
|
|
|
isHealthy <- isChallengeHealthy challenge
|
|
|
|
Just repo <- runDB $ get $ challengePublicRepo challenge
|
|
(leaderboard, (entries, tests)) <- getLeaderboardEntries 1 leaderboardStyle challengeId
|
|
|
|
showAltLeaderboard <- runDB $ hasMetricsOfSecondPriority challengeId
|
|
|
|
(altLeaderboard, altTests) <- if showAltLeaderboard
|
|
then
|
|
do
|
|
(leaderboard', (_, tests')) <- getLeaderboardEntries 3 ByTag challengeId
|
|
return $ (Just leaderboard', Just tests')
|
|
else
|
|
return (Nothing, Nothing)
|
|
|
|
mauth <- maybeAuth
|
|
|
|
let params = getNumericalParams entries
|
|
|
|
let scheme = appRepoScheme $ appSettings app
|
|
|
|
challengeRepo <- runDB $ get404 $ challengePublicRepo challenge
|
|
|
|
challengeLayout True challenge (showChallengeWidget mauth
|
|
challengeEnt
|
|
scheme
|
|
challengeRepo
|
|
repo
|
|
leaderboard
|
|
altLeaderboard
|
|
params
|
|
tests
|
|
altTests
|
|
isHealthy)
|
|
|
|
hasMetricsOfSecondPriority :: (PersistQueryRead backend, MonadIO m, BaseBackend backend ~ SqlBackend) => Key Challenge -> ReaderT backend m Bool
|
|
hasMetricsOfSecondPriority challengeId = do
|
|
tests' <- selectList [TestChallenge ==. challengeId, TestActive ==. True] []
|
|
let tests = filter (\t -> (evaluationSchemePriority $ testMetric $ entityVal t) == 2) tests'
|
|
return $ not (null tests)
|
|
|
|
|
|
getChallengeReadmeR :: Text -> Handler Html
|
|
getChallengeReadmeR challengeName = do
|
|
(Entity _ challenge) <- runDB $ getBy404 $ UniqueName challengeName
|
|
readme <- challengeReadme challengeName
|
|
challengeLayout False challenge $ toWidget readme
|
|
|
|
challengeReadmeInMarkdownApi :: Swagger
|
|
challengeReadmeInMarkdownApi = spec & definitions .~ defs
|
|
where
|
|
(defs, spec) = runDeclare declareChallengeReadmeInMarkdownSwagger mempty
|
|
|
|
declareChallengeReadmeInMarkdownSwagger :: Declare (Definitions Schema) Swagger
|
|
declareChallengeReadmeInMarkdownSwagger = do
|
|
-- param schemas
|
|
let challengeNameSchema = toParamSchema (Proxy :: Proxy String)
|
|
|
|
return $ mempty
|
|
& paths .~
|
|
fromList [ ("/api/challenge-readme/{challengeName}/markdown",
|
|
mempty & DS.get ?~ (mempty
|
|
& parameters .~ [ Inline $ mempty
|
|
& name .~ "challengeName"
|
|
& required ?~ True
|
|
& schema .~ ParamOther (mempty
|
|
& in_ .~ ParamPath
|
|
& paramSchema .~ challengeNameSchema) ]
|
|
& produces ?~ MimeList ["application/text"]
|
|
& description ?~ "Returns the challenge README in Markdown"))
|
|
]
|
|
|
|
getChallengeReadmeInMarkdownR :: Text -> Handler TL.Text
|
|
getChallengeReadmeInMarkdownR challengeName = doChallengeReadmeContents challengeName
|
|
|
|
challengeReadme :: Text -> Handler Html
|
|
challengeReadme challengeName = do
|
|
theContents <- doChallengeReadmeContents challengeName
|
|
return $ markdown def theContents
|
|
|
|
-- Checks whether the directories with repos are available
|
|
isChallengeHealthy :: Challenge -> Handler Bool
|
|
isChallengeHealthy challenge = do
|
|
publicRepoDirExists <- doesRepoExistsOnTheDisk $ challengePublicRepo challenge
|
|
privateRepoDirExists <- doesRepoExistsOnTheDisk $ challengePrivateRepo challenge
|
|
return $ publicRepoDirExists && privateRepoDirExists
|
|
|
|
doChallengeReadmeContents :: Text -> Handler TL.Text
|
|
doChallengeReadmeContents challengeName = do
|
|
(Entity _ challenge) <- runDB $ getBy404 $ UniqueName challengeName
|
|
let repoId = challengePublicRepo challenge
|
|
repoDir <- getRepoDir repoId
|
|
let readmeFilePath = repoDir </> readmeFile
|
|
theContents <- liftIO $ System.IO.readFile readmeFilePath
|
|
return $ TL.pack theContents
|
|
|
|
showChallengeWidget :: Maybe (Entity User)
|
|
-> Entity Challenge
|
|
-> RepoScheme
|
|
-> Repo
|
|
-> Repo
|
|
-> [LeaderboardEntry]
|
|
-> (Maybe [LeaderboardEntry])
|
|
-> [Text]
|
|
-> [Entity Test]
|
|
-> (Maybe [Entity Test])
|
|
-> Bool
|
|
-> WidgetFor App ()
|
|
showChallengeWidget mUserEnt
|
|
(Entity challengeId challenge)
|
|
scheme
|
|
challengeRepo
|
|
repo
|
|
leaderboard
|
|
mAltLeaderboard
|
|
params
|
|
tests
|
|
mAltTests
|
|
isHealthy
|
|
= $(widgetFile "show-challenge")
|
|
where leaderboardWithRanks = zip [1..] leaderboard
|
|
mAltLeaderboardWithRanks = zip [1..] <$> mAltLeaderboard
|
|
maybeRepoLink = getRepoLink repo
|
|
delta = Number 4
|
|
higherTheBetterArray = getIsHigherTheBetterArray $ map entityVal tests
|
|
mUserId = entityKey <$> mUserEnt
|
|
|
|
getRepoLink :: Repo -> Maybe Text
|
|
getRepoLink repo
|
|
| sitePrefix `isPrefixOf` theUrl = Just $ (browsableGitRepo bareRepoName) ++ "/" ++ (repoBranch repo)
|
|
| otherwise = Nothing
|
|
where sitePrefix = "git://gonito.net/" :: Text
|
|
sitePrefixLen = length sitePrefix
|
|
theUrl = repoUrl repo
|
|
bareRepoName = drop sitePrefixLen theUrl
|
|
|
|
getChallengeHowToR :: Text -> Handler Html
|
|
getChallengeHowToR challengeName = do
|
|
(Entity _ challenge) <- runDB $ getBy404 $ UniqueName challengeName
|
|
maybeUser <- maybeAuth
|
|
|
|
app <- getYesod
|
|
let settings = appSettings app
|
|
|
|
let publicRepoId = challengePublicRepo challenge
|
|
repo <- runDB $ get404 publicRepoId
|
|
|
|
case maybeUser of
|
|
Just (Entity userId user) -> do
|
|
enableTriggerToken userId (userTriggerToken user)
|
|
Nothing -> return ()
|
|
|
|
let mToken = case maybeUser of
|
|
Just (Entity _ user) -> userTriggerToken user
|
|
Nothing -> Nothing
|
|
|
|
let isIDSet = case maybeUser of
|
|
Just (Entity _ user) -> isJust $ userLocalId user
|
|
Nothing -> False
|
|
isSSHUploaded <- case maybeUser of
|
|
Just (Entity userId _) -> do
|
|
ukeys <- runDB $ selectList [PublicKeyUser ==. userId] []
|
|
return $ not (null ukeys)
|
|
Nothing -> return False
|
|
challengeLayout False challenge (challengeHowTo
|
|
challenge
|
|
settings
|
|
repo
|
|
(idToBeShown challenge maybeUser)
|
|
isIDSet
|
|
isSSHUploaded
|
|
(join $ userAltRepoScheme <$> entityVal <$> maybeUser)
|
|
mToken)
|
|
|
|
idToBeShown :: p -> Maybe (Entity User) -> Text
|
|
idToBeShown _ maybeUser =
|
|
case maybeUser of
|
|
Just user -> case userLocalId $ entityVal user of
|
|
Just localId -> localId
|
|
Nothing -> defaultIdToBe
|
|
Nothing -> defaultIdToBe
|
|
where defaultIdToBe = "YOURID" :: Text
|
|
|
|
defaultRepo :: RepoScheme -> Text -> Challenge -> Repo -> Maybe (Entity User) -> Text
|
|
defaultRepo SelfHosted repoHost challenge _ maybeUser = repoHost ++ (idToBeShown challenge maybeUser) ++ "/" ++ (challengeName challenge)
|
|
defaultRepo Branches _ _ repo _ = repoUrl repo
|
|
|
|
defaultBranch :: IsString a => RepoScheme -> Maybe a
|
|
defaultBranch SelfHosted = Just "master"
|
|
defaultBranch Branches = Nothing
|
|
|
|
challengeHowTo :: Challenge -> AppSettings -> Repo -> Text -> Bool -> Bool -> Maybe Text -> Maybe Text -> WidgetFor App ()
|
|
challengeHowTo challenge settings repo shownId isIDSet isSSHUploaded mAltRepoScheme mToken = $(widgetFile "challenge-how-to")
|
|
where myBranch = "my-brilliant-branch" :: Text
|
|
urlToYourRepo = case mAltRepoScheme of
|
|
Just altRepoScheme -> encodeSlash (altRepoScheme <> (challengeName challenge))
|
|
Nothing -> "URL_TO_YOUR_REPO"
|
|
|
|
postHealR :: ChallengeId -> Handler TypedContent
|
|
postHealR challengeId = runViewProgress $ doHeal challengeId
|
|
|
|
doHeal :: Key Challenge -> Channel -> HandlerFor App ()
|
|
doHeal challengeId chan = do
|
|
challenge <- runDB $ get404 challengeId
|
|
_ <- getRepoDirOrClone (challengePrivateRepo challenge) chan
|
|
_ <- getRepoDirOrClone (challengePublicRepo challenge) chan
|
|
return ()
|
|
|
|
postArchiveR :: ChallengeId -> Handler Html
|
|
postArchiveR challengeId = doSetArchive True challengeId
|
|
|
|
postUnarchiveR :: ChallengeId -> Handler Html
|
|
postUnarchiveR challengeId = doSetArchive False challengeId
|
|
|
|
doSetArchive :: Bool -> ChallengeId -> Handler Html
|
|
doSetArchive status challengeId = do
|
|
runDB $ update challengeId [ChallengeArchived =. Just status]
|
|
challenge <- runDB $ get404 challengeId
|
|
getShowChallengeR $ challengeName challenge
|
|
|
|
|
|
archiveForm :: ChallengeId -> Form ChallengeId
|
|
archiveForm challengeId = renderBootstrap3 BootstrapBasicForm $ areq hiddenField "" (Just challengeId)
|
|
|
|
getChallengeSubmissionR :: Text -> Handler Html
|
|
getChallengeSubmissionR challengeName = do
|
|
(Entity _ challenge) <- runDB $ getBy404 $ UniqueName challengeName
|
|
maybeUser <- maybeAuth
|
|
|
|
Just repo <- runDB $ get $ challengePublicRepo challenge
|
|
app <- getYesod
|
|
let scheme = appRepoScheme $ appSettings app
|
|
let repoHost = appRepoHost $ appSettings app
|
|
|
|
let defaultUrl = fromMaybe (defaultRepo scheme repoHost challenge repo maybeUser)
|
|
((<> challengeName) <$> (join $ userAltRepoScheme <$> entityVal <$> maybeUser))
|
|
|
|
Entity userId _ <- requireAuth
|
|
|
|
(formWidget, formEnctype) <- generateFormPost $ submissionForm userId (Just defaultUrl) (defaultBranch scheme) (repoGitAnnexRemote repo)
|
|
challengeLayout True challenge $ challengeSubmissionWidget formWidget formEnctype challenge
|
|
|
|
|
|
declareChallengeSubmissionSwagger :: Declare (Definitions Schema) Swagger
|
|
declareChallengeSubmissionSwagger = do
|
|
-- param schemas
|
|
let challengeNameSchema = toParamSchema (Proxy :: Proxy String)
|
|
let stringSchema = toParamSchema (Proxy :: Proxy String)
|
|
|
|
challengeSubmissionResponse <- declareResponse (Proxy :: Proxy Int)
|
|
wrongSubmissionResponse <- declareResponse (Proxy :: Proxy GonitoStatus)
|
|
|
|
return $ mempty
|
|
& paths .~
|
|
fromList [ ("/api/challenge-submission/{challengeName}",
|
|
mempty & DS.post ?~ (mempty
|
|
& parameters .~ [ Inline $ mempty
|
|
& name .~ "challengeName"
|
|
& required ?~ True
|
|
& schema .~ ParamOther (mempty
|
|
& in_ .~ ParamPath
|
|
& paramSchema .~ challengeNameSchema),
|
|
Inline $ mempty
|
|
& name .~ "f1"
|
|
& description .~ Just "submission description"
|
|
& required ?~ False
|
|
& schema .~ ParamOther (mempty
|
|
& in_ .~ ParamFormData
|
|
& paramSchema .~ stringSchema),
|
|
Inline $ mempty
|
|
& name .~ "f2"
|
|
& description .~ Just "submission tags"
|
|
& required ?~ False
|
|
& schema .~ ParamOther (mempty
|
|
& in_ .~ ParamFormData
|
|
& paramSchema .~ stringSchema),
|
|
Inline $ mempty
|
|
& name .~ "f3"
|
|
& description .~ Just "repo URL"
|
|
& required ?~ True
|
|
& schema .~ ParamOther (mempty
|
|
& in_ .~ ParamFormData
|
|
& paramSchema .~ stringSchema),
|
|
Inline $ mempty
|
|
& name .~ "f4"
|
|
& description .~ Just "repo branch"
|
|
& required ?~ True
|
|
& schema .~ ParamOther (mempty
|
|
& in_ .~ ParamFormData
|
|
& paramSchema .~ stringSchema),
|
|
|
|
Inline $ mempty
|
|
& name .~ "f5"
|
|
& description .~ Just "git-annex remote specification"
|
|
& required ?~ False
|
|
& schema .~ ParamOther (mempty
|
|
& in_ .~ ParamFormData
|
|
& paramSchema .~ stringSchema)]
|
|
& produces ?~ MimeList ["application/json"]
|
|
& description ?~ "Initiates a submission based on a given repo URL/branch. Returns an asynchrous job ID."
|
|
& at 200 ?~ Inline challengeSubmissionResponse
|
|
& at 422 ?~ Inline wrongSubmissionResponse))
|
|
]
|
|
|
|
challengeSubmissionApi :: Swagger
|
|
challengeSubmissionApi = spec & definitions .~ defs
|
|
where
|
|
(defs, spec) = runDeclare declareChallengeSubmissionSwagger mempty
|
|
|
|
|
|
data ChallangeSubmissionStatus = SubmissionOK | SubmissionWrong Text
|
|
deriving (Eq, Show)
|
|
|
|
data GonitoStatus = GonitoStatus {
|
|
detail :: Text
|
|
} deriving (Eq, Show, Generic)
|
|
|
|
instance ToJSON GonitoStatus
|
|
instance ToSchema GonitoStatus
|
|
|
|
postChallengeSubmissionJsonR :: Text -> Handler Value
|
|
postChallengeSubmissionJsonR challengeName = do
|
|
Entity userId _ <- requireAuthPossiblyByToken
|
|
|
|
challengeEnt@(Entity challengeId _) <- runDB $ getBy404 $ UniqueName challengeName
|
|
((result, _), _) <- runFormPostNoToken $ submissionForm userId Nothing Nothing Nothing
|
|
let submissionData' = case result of
|
|
FormSuccess res -> Just res
|
|
_ -> Nothing
|
|
Just submissionData = submissionData'
|
|
|
|
status <- checkSubmission challengeEnt submissionData
|
|
|
|
case status of
|
|
SubmissionOK -> runViewProgressAsynchronously $ doCreateSubmission userId challengeId submissionData
|
|
SubmissionWrong errorMsg -> sendResponseStatus status422 $ toJSON (GonitoStatus errorMsg)
|
|
|
|
checkSubmission :: Entity Challenge -> ChallengeSubmissionData -> Handler ChallangeSubmissionStatus
|
|
checkSubmission (Entity _ challenge) submissionData = do
|
|
let repo = challengeSubmissionDataRepo submissionData
|
|
if (null $ repoSpecUrl repo)
|
|
then
|
|
return $ SubmissionWrong "empty URL"
|
|
else
|
|
do
|
|
if (null $ repoSpecBranch repo)
|
|
then
|
|
return $ SubmissionWrong "empty branch"
|
|
else
|
|
do
|
|
if (willClone challenge submissionData)
|
|
then
|
|
do
|
|
return SubmissionOK
|
|
else
|
|
do
|
|
return $ SubmissionWrong "Refusing to clone the submission from this URL."
|
|
|
|
postChallengeSubmissionR :: Text -> Handler TypedContent
|
|
postChallengeSubmissionR challengeName = do
|
|
userId <- requireAuthId
|
|
|
|
(Entity challengeId _) <- runDB $ getBy404 $ UniqueName challengeName
|
|
((result, _), _) <- runFormPost $ submissionForm userId Nothing Nothing Nothing
|
|
let submissionData' = case result of
|
|
FormSuccess res -> Just res
|
|
_ -> Nothing
|
|
Just submissionData = submissionData'
|
|
|
|
runViewProgress $ doCreateSubmission userId challengeId submissionData
|
|
|
|
postTriggerLocallyR :: Handler TypedContent
|
|
postTriggerLocallyR = do
|
|
(Just challengeName) <- lookupPostParam "challenge"
|
|
(Just localId) <- lookupPostParam "user"
|
|
mBranch <- lookupPostParam "branch"
|
|
mGitAnnexRemote <- lookupPostParam "git-annex-remote"
|
|
[Entity userId _] <- runDB $ selectList [UserLocalId ==. Just localId] []
|
|
|
|
app <- getYesod
|
|
let repoHost = appRepoHost $ appSettings app
|
|
|
|
let localRepo = repoHost ++ localId ++ "/" ++ challengeName
|
|
trigger userId challengeName localRepo mBranch mGitAnnexRemote
|
|
|
|
postTriggerRemotelyR :: Handler TypedContent
|
|
postTriggerRemotelyR = do
|
|
(Just challengeName) <- lookupPostParam "challenge"
|
|
(Just theUrl) <- lookupPostParam "url"
|
|
(Just token) <- lookupPostParam "token"
|
|
mBranch <- lookupPostParam "branch"
|
|
mGitAnnexRemote <- lookupPostParam "git-annex-remote"
|
|
doTrigger token challengeName theUrl mBranch mGitAnnexRemote
|
|
|
|
postTriggerRemotelySimpleR :: Text -> Text -> Text -> Text -> Handler TypedContent
|
|
postTriggerRemotelySimpleR token challengeName theUrl branch =
|
|
doTrigger token challengeName (decodeSlash theUrl) (Just branch) Nothing
|
|
|
|
getTriggerRemotelySimpleR :: Text -> Text -> Text -> Text -> Handler TypedContent
|
|
getTriggerRemotelySimpleR token challengeName theUrl branch =
|
|
doTrigger token challengeName (decodeSlash theUrl) (Just branch) Nothing
|
|
|
|
data GitServerPayload = GitServerPayload {
|
|
gitServerPayloadRef :: Text,
|
|
-- Unfortunately, the URL is given in "ssh_url" field
|
|
-- for Gogs and "git_ssh_url" for GitLab, hence two
|
|
-- fields here
|
|
gitServerPayloadSshUrl :: Maybe Text,
|
|
gitServerPayloadGitSshUrl :: Maybe Text
|
|
}
|
|
deriving (Show, Eq)
|
|
|
|
instance FromJSON GitServerPayload where
|
|
parseJSON (Object o) = GitServerPayload
|
|
<$> o .: "ref"
|
|
<*> ((o .: "repository") >>= (.:? "ssh_url"))
|
|
<*> ((o .: "repository") >>= (.:? "git_ssh_url"))
|
|
|
|
postTriggerByWebhookR :: Text -> Text -> Handler TypedContent
|
|
postTriggerByWebhookR token challengeName = do
|
|
payload <- requireJsonBody :: Handler GitServerPayload
|
|
let ref = gitServerPayloadRef payload
|
|
let refPrefix = "refs/heads/"
|
|
if refPrefix `isPrefixOf` ref
|
|
then
|
|
do
|
|
let branch = T.replace refPrefix "" ref
|
|
let theUrl = fromMaybe (fromJust $ gitServerPayloadGitSshUrl payload)
|
|
(gitServerPayloadSshUrl payload)
|
|
doTrigger token challengeName theUrl (Just branch) Nothing
|
|
else
|
|
error $ "unexpected ref `" ++ (T.unpack ref) ++ "`"
|
|
|
|
|
|
doTrigger :: Text -> Text -> Text -> Maybe Text -> Maybe Text -> Handler TypedContent
|
|
doTrigger token challengeName theUrl mBranch mGitAnnexRemote = do
|
|
[Entity userId _] <- runDB $ selectList [UserTriggerToken ==. Just token] []
|
|
trigger userId challengeName theUrl mBranch mGitAnnexRemote
|
|
|
|
trigger :: UserId -> Text -> Text -> Maybe Text -> Maybe Text -> Handler TypedContent
|
|
trigger userId challengeName theUrl mBranch mGitAnnexRemote = do
|
|
let branch = fromMaybe "master" mBranch
|
|
mChallengeEnt <- runDB $ getBy $ UniqueName challengeName
|
|
|
|
let defSubmission = ChallengeSubmissionData {
|
|
challengeSubmissionDataDescription = Nothing,
|
|
challengeSubmissionDataTags = Nothing,
|
|
challengeSubmissionDataRepo = RepoSpec {
|
|
repoSpecUrl=theUrl,
|
|
repoSpecBranch=branch,
|
|
repoSpecGitAnnexRemote=mGitAnnexRemote},
|
|
challengeSubmissionDataTeam = Nothing
|
|
}
|
|
|
|
case mChallengeEnt of
|
|
Just (Entity challengeId _) -> runOpenViewProgress $ doCreateSubmission userId challengeId defSubmission
|
|
Nothing -> return $ toTypedContent (("Unknown challenge `" ++ (Data.Text.unpack challengeName) ++ "`. Cannot be triggered, must be submitted manually at Gonito.net!\n") :: String)
|
|
|
|
isBefore :: UTCTime -> Maybe UTCTime -> Bool
|
|
isBefore _ Nothing = True
|
|
isBefore moment (Just deadline) = moment <= deadline
|
|
|
|
-- | An attempt to filtre out mistaken/unwanted submissions (without cloning
|
|
-- the submission repo, just by looking at the metadata)
|
|
willClone :: Challenge -> ChallengeSubmissionData -> Bool
|
|
willClone challenge submissionData =
|
|
(challengeName challenge) `isInfixOf` theUrl && branch /= dontPeek && not (dontPeek `isInfixOf` theUrl)
|
|
where theUrl = repoSpecUrl $ challengeSubmissionDataRepo submissionData
|
|
branch = repoSpecBranch $ challengeSubmissionDataRepo submissionData
|
|
dontPeek = "dont-peek"
|
|
|
|
|
|
-- | Main place where submission is done (whether manually or by trigger)
|
|
doCreateSubmission :: UserId -> Key Challenge -> ChallengeSubmissionData -> Channel -> Handler ()
|
|
doCreateSubmission userId challengeId challengeSubmissionData chan = do
|
|
challenge <- runDB $ get404 challengeId
|
|
|
|
theVersion <- runDB $ getBy404 $ UniqueVersionByCommit $ challengeVersion challenge
|
|
theNow <- liftIO getCurrentTime
|
|
|
|
if theNow `isBefore` (versionDeadline $ entityVal theVersion)
|
|
then
|
|
do
|
|
let wanted = willClone challenge challengeSubmissionData
|
|
if wanted
|
|
then
|
|
doCreateSubmission' (challengeArchived challenge) userId challengeId challengeSubmissionData chan
|
|
else
|
|
msg chan "Refusing to clone the submission from this URL"
|
|
else
|
|
msg chan "Submission is past the deadline, no submission will be accepted from now on."
|
|
|
|
doCreateSubmission' :: Maybe Bool -> UserId -> Key Challenge -> ChallengeSubmissionData -> Channel -> Handler ()
|
|
doCreateSubmission' (Just True) _ _ _ chan = msg chan "This challenge is archived, you cannot submit to it. Ask the site admin to unarchive it."
|
|
doCreateSubmission' _ userId challengeId challengeSubmissionData chan = do
|
|
let mDescription = challengeSubmissionDataDescription challengeSubmissionData
|
|
let mTags = challengeSubmissionDataTags challengeSubmissionData
|
|
let repoSpec = challengeSubmissionDataRepo challengeSubmissionData
|
|
|
|
maybeRepoKey <- getSubmissionRepo userId challengeId repoSpec chan
|
|
case maybeRepoKey of
|
|
Just repoId -> do
|
|
|
|
challenge <- runDB $ get404 challengeId
|
|
user <- runDB $ get404 userId
|
|
|
|
relevantIndicators <- getOngoingTargets challengeId
|
|
|
|
(Entity mainTestId mainTest) <- runDB $ fetchMainTest challengeId
|
|
|
|
(Entity _ currentVersion) <- runDB $ getBy404 $ UniqueVersionByCommit $ challengeVersion challenge
|
|
let submittedMajorVersion = versionMajor currentVersion
|
|
|
|
let orderDirection = case getMetricOrdering (evaluationSchemeMetric $ testMetric mainTest) of
|
|
TheHigherTheBetter -> E.desc
|
|
TheLowerTheBetter -> E.asc
|
|
|
|
bestResultSoFar <- runDB $ E.select $ E.from $ \(evaluation, submission, variant, out, test, theVersion) -> do
|
|
E.where_ (submission ^. SubmissionChallenge E.==. E.val challengeId
|
|
E.&&. submission ^. SubmissionIsHidden E.==. E.val False
|
|
E.&&. variant ^. VariantSubmission E.==. submission ^. SubmissionId
|
|
E.&&. evaluation ^. EvaluationChecksum E.==. out ^. OutChecksum
|
|
E.&&. (E.not_ (E.isNothing (evaluation ^. EvaluationScore)))
|
|
E.&&. out ^. OutVariant E.==. variant ^. VariantId
|
|
E.&&. evaluation ^. EvaluationTest E.==. test ^. TestId
|
|
E.&&. test ^. TestChallenge E.==. E.val challengeId
|
|
E.&&. test ^. TestName E.==. E.val (testName mainTest)
|
|
E.&&. test ^. TestMetric E.==. E.val (testMetric mainTest)
|
|
E.&&. test ^. TestActive
|
|
E.&&. (evaluation ^. EvaluationVersion E.==. theVersion ^. VersionCommit)
|
|
E.&&. theVersion ^. VersionCommit E.==. test ^. TestCommit
|
|
E.&&. theVersion ^. VersionMajor E.>=. E.val submittedMajorVersion)
|
|
E.orderBy [orderDirection (evaluation ^. EvaluationScore)]
|
|
E.limit 1
|
|
return evaluation
|
|
let bestScoreSoFar = join (evaluationScore <$> entityVal <$> (listToMaybe bestResultSoFar))
|
|
|
|
case bestScoreSoFar of
|
|
Just s -> msg chan ("best score so far is: " ++ (Data.Text.pack $ show s))
|
|
Nothing -> msg chan "first submission so far"
|
|
|
|
repo <- runDB $ get404 repoId
|
|
|
|
repoDir <- getRepoDirOrClone repoId chan
|
|
|
|
gonitoMetadata <- liftIO
|
|
$ extractMetadataFromRepoDir repoDir (ExtractionOptions {
|
|
extractionOptionsDescription = mDescription,
|
|
extractionOptionsTags = Just $ parseTags mTags,
|
|
extractionOptionsGeneralParams = Nothing,
|
|
extractionOptionsUnwantedParams = Nothing,
|
|
extractionOptionsParamFiles = Nothing,
|
|
extractionOptionsMLRunPath = Nothing,
|
|
extractionOptionsExternalLinks = Nothing,
|
|
extractionOptionsDependencies = Nothing })
|
|
|
|
let mTeamId = challengeSubmissionDataTeam challengeSubmissionData
|
|
|
|
submissionId <- getSubmission userId
|
|
mTeamId
|
|
repoId
|
|
(repoCurrentCommit repo)
|
|
challengeId
|
|
(gonitoMetadataDescription gonitoMetadata)
|
|
chan
|
|
|
|
_ <- runDB $ mapM insert $ map (\l -> ExternalLink {
|
|
externalLinkSubmission = submissionId,
|
|
externalLinkTitle = linkTitle l,
|
|
externalLinkUrl = linkUrl l }) $ gonitoMetadataExternalLinks gonitoMetadata
|
|
|
|
_ <- runDB $ mapM insertUnique $ map (\s -> Dependency {
|
|
dependencySubRepoCommit = s,
|
|
dependencySuperRepoCommit = (repoCurrentCommit repo) }) $ gonitoMetadataDependencies gonitoMetadata
|
|
|
|
outs <- getOuts False chan submissionId (gonitoMetadataGeneralParams gonitoMetadata)
|
|
|
|
currentTagIds <- runDB $ selectList [SubmissionTagSubmission ==. submissionId] []
|
|
|
|
runDB $ addTags submissionId (gonitoMetadataTags gonitoMetadata) (
|
|
map (submissionTagTag . entityVal) currentTagIds)
|
|
msg chan "SUBMISSION CREATED"
|
|
|
|
app <- getYesod
|
|
|
|
newScores <- mapM (getScoreForOut mainTestId) outs
|
|
let newScores' = catMaybes newScores
|
|
let newScores'' = case getMetricOrdering (evaluationSchemeMetric $ testMetric mainTest) of
|
|
TheHigherTheBetter -> reverse $ sort newScores'
|
|
TheLowerTheBetter -> sort newScores'
|
|
let compOp = case getMetricOrdering (evaluationSchemeMetric $ testMetric mainTest) of
|
|
TheLowerTheBetter -> (<)
|
|
TheHigherTheBetter -> (>)
|
|
|
|
let submissionLink = slackLink app "submission" ("q/" ++ (fromSHA1ToText (repoCurrentCommit repo)))
|
|
|
|
case bestScoreSoFar of
|
|
Just b -> case newScores'' of
|
|
(s:_) -> if compOp s b
|
|
then
|
|
do
|
|
let challengeLink = slackLink app (challengeTitle challenge) ("challenge/"
|
|
++ (challengeName challenge))
|
|
let message = ("Whoa! New best result for "
|
|
++ challengeLink
|
|
++ " challenge by "
|
|
++ (fromMaybe "???" $ userName user)
|
|
++ ", "
|
|
++ (T.pack $ show $ testMetric mainTest)
|
|
++ ": "
|
|
++ (formatScore (testPrecision mainTest) s)
|
|
++ " ("
|
|
++ (if s > b
|
|
then "+"
|
|
else "")
|
|
++ (formatScore (testPrecision mainTest) (s-b))
|
|
++ ")."
|
|
++ " See " ++ submissionLink ++ "."
|
|
++ " :clap:")
|
|
msg chan message
|
|
case appNewBestResultSlackHook $ appSettings app of
|
|
Just "" -> return ()
|
|
Just hook -> liftIO $ runSlackHook hook message
|
|
|
|
Nothing -> return ()
|
|
else return ()
|
|
[] -> return ()
|
|
Nothing -> return ()
|
|
|
|
if appAutoOpening $ appSettings app
|
|
then
|
|
doMakePublic userId submissionId chan
|
|
else
|
|
return ()
|
|
|
|
if not (null relevantIndicators)
|
|
then
|
|
checkIndicators user challengeId submissionId submissionLink relevantIndicators chan
|
|
else
|
|
return ()
|
|
|
|
Nothing -> return ()
|
|
|
|
checkIndicators :: User -> ChallengeId -> SubmissionId -> Text -> [IndicatorEntry] -> Channel -> Handler ()
|
|
checkIndicators user challengeId submissionId submissionLink relevantIndicators chan = do
|
|
msg chan "Checking indicators..."
|
|
theNow <- liftIO $ getCurrentTime
|
|
mapM_ (\indicator -> checkIndicator theNow user challengeId submissionId submissionLink indicator chan) relevantIndicators
|
|
|
|
checkIndicator :: UTCTime -> User -> ChallengeId -> SubmissionId -> Text -> IndicatorEntry -> Channel -> Handler ()
|
|
checkIndicator theNow user challengeId submissionId submissionLink indicator chan = do
|
|
(entries, _) <- runDB $ getChallengeSubmissionInfos 1 (\(Entity sid _) -> sid == submissionId) (const True) id challengeId
|
|
mapM_ (\t -> checkTarget theNow user submissionLink entries indicator t chan) (indicatorEntryTargets indicator)
|
|
|
|
checkTarget :: UTCTime -> User -> Text -> [TableEntry] -> IndicatorEntry -> Entity Target -> Channel -> Handler ()
|
|
checkTarget theNow user submissionLink entries indicator target chan = do
|
|
app <- getYesod
|
|
let status = getTargetStatus theNow entries indicator target
|
|
if status == TargetPassed
|
|
then
|
|
do
|
|
let message = "Congratulations!!! The target " ++ indicatorText
|
|
++ " was beaten by "
|
|
++ (fromMaybe "???" $ userName user)
|
|
++ ", "
|
|
++ " See " ++ submissionLink ++ "."
|
|
++ (T.replicate 10 " :champagne: ") ++ " :mleczko: "
|
|
msg chan message
|
|
case appNewBestResultSlackHook $ appSettings app of
|
|
Just "" -> return ()
|
|
Just hook -> liftIO $ runSlackHook hook message
|
|
Nothing -> return ()
|
|
else
|
|
return ()
|
|
where indicatorText = prettyIndicatorEntry indicator
|
|
|
|
getScoreForOut :: (PersistQueryRead (YesodPersistBackend site), YesodPersist site, BaseBackend (YesodPersistBackend site) ~ SqlBackend) => Key Test -> Out -> HandlerFor site (Maybe Double)
|
|
getScoreForOut mainTestId out = do
|
|
mEvaluation <- runDB $ selectFirst [EvaluationChecksum ==. (outChecksum out),
|
|
EvaluationTest ==. mainTestId]
|
|
[]
|
|
return $ case mEvaluation of
|
|
Just evaluation -> evaluationScore $ entityVal evaluation
|
|
Nothing -> Nothing
|
|
|
|
getSubmission :: UserId -> Maybe TeamId -> Key Repo -> SHA1 -> Key Challenge -> Text -> Channel -> Handler (Key Submission)
|
|
getSubmission userId mTeamId repoId commit challengeId subDescription chan = do
|
|
challenge <- runDB $ get404 challengeId
|
|
maybeSubmission <- runDB $ getBy $ UniqueSubmissionRepoCommitChallenge repoId commit challengeId
|
|
case maybeSubmission of
|
|
Just (Entity submissionId _) -> do
|
|
msg chan "Submission already there, re-checking"
|
|
return submissionId
|
|
Nothing -> do
|
|
msg chan "Creating new submission"
|
|
time <- liftIO getCurrentTime
|
|
runDB $ insert $ Submission {
|
|
submissionRepo=repoId,
|
|
submissionCommit=commit,
|
|
submissionChallenge=challengeId,
|
|
submissionDescription=subDescription,
|
|
submissionStamp=time,
|
|
submissionSubmitter=userId,
|
|
submissionIsPublic=False,
|
|
submissionIsHidden=False,
|
|
submissionVersion=challengeVersion challenge,
|
|
submissionTeam=mTeamId }
|
|
|
|
getSubmissionRepo :: UserId -> Key Challenge -> RepoSpec -> Channel -> Handler (Maybe (Key Repo))
|
|
getSubmissionRepo userId challengeId repoSpec chan = getPossiblyExistingRepo checkRepoAvailibility userId challengeId repoSpec chan
|
|
|
|
checkRepoAvailibility :: Key Challenge -> Key Repo -> Channel -> Handler Bool
|
|
checkRepoAvailibility challengeId repoId chan = do
|
|
maybeOtherChallengeId <- runDB $ selectFirst ( [ChallengePublicRepo ==. repoId]
|
|
||. [ChallengePrivateRepo ==. repoId]) []
|
|
case maybeOtherChallengeId of
|
|
Just _ -> do
|
|
err chan "Repository already used as a challenge repo, please use a different repo or a different branch"
|
|
return False
|
|
Nothing -> do
|
|
maybeOtherSubmissionId <- runDB $ selectFirst [SubmissionRepo ==. repoId,
|
|
SubmissionChallenge !=. challengeId] []
|
|
case maybeOtherSubmissionId of
|
|
Just _ -> do
|
|
err chan "Repository already used as a submission repo for a different challenge, please use a different repo or a different branch"
|
|
return False
|
|
Nothing -> return True
|
|
|
|
challengeSubmissionWidget :: (ToMarkup a1, ToWidget App a2) => a2 -> a1 -> Challenge -> WidgetFor App ()
|
|
challengeSubmissionWidget formWidget formEnctype challenge = $(widgetFile "challenge-submission")
|
|
|
|
data ChallengeSubmissionData = ChallengeSubmissionData {
|
|
challengeSubmissionDataDescription :: Maybe Text,
|
|
challengeSubmissionDataTags :: Maybe Text,
|
|
challengeSubmissionDataRepo :: RepoSpec,
|
|
challengeSubmissionDataTeam :: Maybe TeamId }
|
|
|
|
|
|
|
|
submissionForm userId defaultUrl defBranch defaultGitAnnexRemote = renderBootstrap3 BootstrapBasicForm $ ChallengeSubmissionData
|
|
<$> aopt textField (fieldWithTooltip MsgSubmissionDescription MsgSubmissionDescriptionTooltip) Nothing
|
|
<*> aopt textField (tagsfs MsgSubmissionTags) Nothing
|
|
<*> (RepoSpec <$> areq textField (bfs MsgSubmissionUrl) defaultUrl
|
|
<*> areq textField (bfs MsgSubmissionBranch) defBranch
|
|
<*> aopt textField (bfs MsgSubmissionGitAnnexRemote) (Just defaultGitAnnexRemote))
|
|
<*> aopt (selectField teams) (bfs MsgAsTeam) Nothing
|
|
where teams = do
|
|
myTeams <- runDB $ E.select $ E.from $ \(team, teamMember) -> do
|
|
E.where_ (teamMember ^. TeamMemberTeam E.==. team ^. TeamId
|
|
E.&&. teamMember ^. TeamMemberUser E.==. E.val userId)
|
|
E.orderBy [E.asc (team ^. TeamIdent)]
|
|
E.limit 1
|
|
return team
|
|
|
|
optionsPairs $ map (\t -> (teamIdent $ entityVal t, entityKey t)) myTeams
|
|
|
|
|
|
getUserInfoR :: Handler Value
|
|
getUserInfoR = do
|
|
(Entity _ user) <- requireAuthPossiblyByToken
|
|
return $ String $ userIdent user
|
|
|
|
getAddUserR :: Handler Value
|
|
getAddUserR = do
|
|
mInfo <- authorizationTokenAuth
|
|
case mInfo of
|
|
Just infos -> do
|
|
let ident = jwtAuthInfoIdent infos
|
|
x <- runDB $ getBy $ UniqueUser ident
|
|
case x of
|
|
Just _ -> return $ Bool False
|
|
Nothing -> do
|
|
_ <- runDB $ insert User
|
|
{ userIdent = ident
|
|
, userPassword = Nothing
|
|
, userName = Nothing
|
|
, userIsAdmin = False
|
|
, userLocalId = Nothing
|
|
, userIsAnonymous = False
|
|
, userAvatar = Nothing
|
|
, userVerificationKey = Nothing
|
|
, userKeyExpirationDate = Nothing
|
|
, userTriggerToken = Nothing
|
|
, userAltRepoScheme = Nothing
|
|
}
|
|
return $ Bool True
|
|
Nothing -> return $ Bool False
|
|
|
|
addUserApi :: Swagger
|
|
addUserApi = spec & definitions .~ defs
|
|
where
|
|
(defs, spec) = runDeclare declareAddUserApi mempty
|
|
|
|
declareAddUserApi :: Declare (Definitions Schema) Swagger
|
|
declareAddUserApi = do
|
|
-- param schemas
|
|
response <- declareResponse (Proxy :: Proxy Bool)
|
|
|
|
return $ mempty
|
|
& paths .~
|
|
fromList [ ("/api/add-user",
|
|
mempty & DS.get ?~ (mempty
|
|
& parameters .~ [ ]
|
|
& produces ?~ MimeList ["application/json"]
|
|
& description ?~ "Creates a new user"
|
|
& at 200 ?~ Inline response))
|
|
]
|
|
|
|
|
|
userInfoApi :: Swagger
|
|
userInfoApi = spec & definitions .~ defs
|
|
where
|
|
(defs, spec) = runDeclare declareUserInfoApi mempty
|
|
|
|
declareUserInfoApi :: Declare (Definitions Schema) Swagger
|
|
declareUserInfoApi = do
|
|
-- param schemas
|
|
response <- declareResponse (Proxy :: Proxy String)
|
|
|
|
return $ mempty
|
|
& paths .~
|
|
fromList [ ("/api/user-info",
|
|
mempty & DS.get ?~ (mempty
|
|
& parameters .~ [ ]
|
|
& produces ?~ MimeList ["application/json"]
|
|
& description ?~ "Returns the identifier of the user"
|
|
& at 200 ?~ Inline response))
|
|
]
|
|
|
|
|
|
|
|
declareAllSubmissionsApi :: String -> String -> Declare (Definitions Schema) Swagger
|
|
declareAllSubmissionsApi q d = do
|
|
-- param schemas
|
|
let challengeNameSchema = toParamSchema (Proxy :: Proxy String)
|
|
|
|
allSubmissionsResponse <- declareResponse (Proxy :: Proxy SubmissionsView)
|
|
|
|
return $ mempty
|
|
& paths .~
|
|
fromList [ ("/api/" ++ q ++ "/{challengeName}",
|
|
mempty & DS.get ?~ (mempty
|
|
& parameters .~ [ Inline $ mempty
|
|
& name .~ "challengeName"
|
|
& required ?~ True
|
|
& schema .~ ParamOther (mempty
|
|
& in_ .~ ParamPath
|
|
& paramSchema .~ challengeNameSchema) ]
|
|
& produces ?~ MimeList ["application/json"]
|
|
& description ?~ T.pack d
|
|
& at 200 ?~ Inline allSubmissionsResponse))
|
|
]
|
|
|
|
|
|
allSubmissionsApi :: Swagger
|
|
allSubmissionsApi = spec & definitions .~ defs
|
|
where
|
|
(defs, spec) = runDeclare (declareAllSubmissionsApi "challenge-all-submissions" "Returns all submissions for a challenge") mempty
|
|
|
|
mySubmissionsApi :: Swagger
|
|
mySubmissionsApi = spec & definitions .~ defs
|
|
where
|
|
(defs, spec) = runDeclare (declareAllSubmissionsApi "challenge-my-submissions" "Returns all submissions for a challenge for the user") mempty
|
|
|
|
getChallengeAllSubmissionsJsonR :: Text -> Handler Value
|
|
getChallengeAllSubmissionsJsonR challengeName = do
|
|
v <- fetchAllSubmissionsView challengeName
|
|
return $ toJSON v
|
|
|
|
getChallengeMySubmissionsJsonR :: Text -> Handler Value
|
|
getChallengeMySubmissionsJsonR challengeName = do
|
|
v <- fetchMySubmissionsView challengeName
|
|
return $ toJSON v
|
|
|
|
fetchAllSubmissionsView :: Text -> Handler SubmissionsView
|
|
fetchAllSubmissionsView challengeName = do
|
|
fetchChallengeSubmissionsView (const True) challengeName
|
|
|
|
fetchMySubmissionsView :: Text -> Handler SubmissionsView
|
|
fetchMySubmissionsView challengeName = do
|
|
Entity userId _ <- requireAuthPossiblyByToken
|
|
fetchChallengeSubmissionsView (\(Entity _ submission) -> (submissionSubmitter submission == userId)) challengeName
|
|
|
|
convertTagInfoToView :: (Entity Import.Tag, Entity SubmissionTag) -> TagView
|
|
convertTagInfoToView tagInfo =
|
|
TagView {
|
|
tagViewName = tagName $ entityVal $ fst tagInfo,
|
|
tagViewDescription = tagDescription $ entityVal $ fst tagInfo,
|
|
tagViewAccepted = submissionTagAccepted $ entityVal $ snd tagInfo
|
|
}
|
|
|
|
convertEvaluationToView :: Map TestReference Evaluation -> Entity Test -> Maybe EvaluationView
|
|
convertEvaluationToView theMapping entTest =
|
|
case join $ evaluationScore <$> mEvaluation of
|
|
Just s ->
|
|
Just $ EvaluationView {
|
|
evaluationViewScore = formatTruncatedScore formattingOps mEvaluation,
|
|
evaluationViewFullScore = s,
|
|
evaluationViewTest = testRef
|
|
}
|
|
Nothing -> Nothing
|
|
where mEvaluation = Map.lookup testRef theMapping
|
|
formattingOps = getTestFormattingOpts $ entityVal entTest
|
|
testRef = getTestReference entTest
|
|
|
|
-- convertTableEntryToView :: Maybe UserId -> [Entity Test] -> TableEntry -> SubmissionView
|
|
convertTableEntryToView :: [Entity Test] -> TableEntry -> HandlerFor App SubmissionView
|
|
convertTableEntryToView tests entry = do
|
|
mUserId <- maybeAuthPossiblyByToken
|
|
|
|
isReevaluable <- runDB $ canBeReevaluated $ entityKey $ tableEntrySubmission entry
|
|
isVisible <- runDB $ checkWhetherVisible submission (entityKey <$> mUserId)
|
|
|
|
return $ SubmissionView {
|
|
submissionViewId = fromSqlKey $ entityKey $ tableEntrySubmission entry,
|
|
submissionViewVariantId = fromSqlKey $ entityKey $ tableEntryVariant entry,
|
|
submissionViewRank = tableEntryRank entry,
|
|
submissionViewSubmitter = formatSubmitter $ entityVal $ tableEntrySubmitter entry,
|
|
submissionViewWhen = submissionStamp submission,
|
|
submissionViewVersion = tableEntryVersion entry,
|
|
submissionViewDescription = submissionDescription submission,
|
|
submissionViewTags = Import.map convertTagInfoToView $ tableEntryTagsInfo entry,
|
|
submissionViewHash = fromSHA1ToText $ submissionCommit submission,
|
|
submissionViewEvaluations = catMaybes $ Import.map (convertEvaluationToView $ tableEntryMapping entry) tests,
|
|
submissionViewIsOwner = (entityKey <$> mUserId) == Just (submissionSubmitter submission),
|
|
submissionViewIsReevaluable = isReevaluable,
|
|
submissionViewIsVisible = isVisible,
|
|
submissionViewIsPublic = submissionIsPublic submission,
|
|
submissionViewTeam = teamIdent <$> entityVal <$> tableEntryTeam entry
|
|
}
|
|
where submission = entityVal $ tableEntrySubmission entry
|
|
|
|
fetchChallengeSubmissionsView :: ((Entity Submission) -> Bool) -> Text -> Handler SubmissionsView
|
|
fetchChallengeSubmissionsView condition challengeName = do
|
|
Entity challengeId _ <- runDB $ getBy404 $ UniqueName challengeName
|
|
(evaluationMaps, tests') <- runDB $ getChallengeSubmissionInfos 1 condition (const True) id challengeId
|
|
let tests = sortBy testComparator tests'
|
|
|
|
submissions <- mapM (convertTableEntryToView tests) evaluationMaps
|
|
|
|
return $ SubmissionsView {
|
|
submissionsViewSubmissions = submissions,
|
|
submissionsViewTests = map getTestReference tests
|
|
}
|
|
|
|
|
|
-- TODO switch to fetchChallengeSubmissionSview
|
|
getChallengeMySubmissionsR :: Text -> Handler Html
|
|
getChallengeMySubmissionsR challengeName = do
|
|
userId <- requireAuthId
|
|
getChallengeSubmissions (\(Entity _ submission) -> (submissionSubmitter submission == userId)) challengeName
|
|
|
|
getChallengeAllSubmissionsR :: Text -> Handler Html
|
|
getChallengeAllSubmissionsR challengeName = getChallengeSubmissions (\_ -> True) challengeName
|
|
|
|
data EvaluationView = EvaluationView {
|
|
evaluationViewScore :: Text,
|
|
evaluationViewFullScore :: Double,
|
|
evaluationViewTest :: TestReference
|
|
}
|
|
|
|
instance ToJSON EvaluationView where
|
|
toJSON e = object
|
|
[ "score" .= evaluationViewScore e
|
|
, "full-score" .= evaluationViewFullScore e
|
|
, "test" .= evaluationViewTest e
|
|
]
|
|
|
|
instance ToSchema EvaluationView where
|
|
declareNamedSchema _ = do
|
|
stringSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy String)
|
|
doubleSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy Double)
|
|
testRefSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy TestReference)
|
|
return $ NamedSchema (Just "Evaluation") $ mempty
|
|
& type_ .~ SwaggerObject
|
|
& properties .~
|
|
fromList [ ("score", stringSchema)
|
|
, ("full-score", doubleSchema)
|
|
, ("test", testRefSchema)
|
|
]
|
|
& required .~ [ "score", "full-score", "test" ]
|
|
|
|
|
|
data TagView = TagView {
|
|
tagViewName :: Text,
|
|
tagViewDescription :: Maybe Text,
|
|
tagViewAccepted :: Maybe Bool }
|
|
|
|
instance ToJSON TagView where
|
|
toJSON t = object
|
|
[ "name" .= tagViewName t
|
|
, "description" .= tagViewDescription t
|
|
, "accepted" .= tagViewAccepted t
|
|
]
|
|
|
|
instance ToSchema TagView where
|
|
declareNamedSchema _ = do
|
|
stringSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy String)
|
|
boolSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy Bool)
|
|
return $ NamedSchema (Just "TagView") $ mempty
|
|
& type_ .~ SwaggerObject
|
|
& properties .~
|
|
fromList [ ("name", stringSchema)
|
|
, ("description", stringSchema)
|
|
, ("accepted", boolSchema)
|
|
]
|
|
& required .~ [ "name", "description" ]
|
|
|
|
|
|
data SubmissionView = SubmissionView {
|
|
submissionViewId :: Int64,
|
|
submissionViewVariantId :: Int64,
|
|
submissionViewRank :: Int,
|
|
submissionViewSubmitter :: Text,
|
|
submissionViewWhen :: UTCTime,
|
|
submissionViewVersion :: (Int, Int, Int),
|
|
submissionViewDescription :: Text,
|
|
submissionViewTags :: [TagView],
|
|
submissionViewHash :: Text,
|
|
submissionViewEvaluations :: [EvaluationView],
|
|
submissionViewIsOwner :: Bool,
|
|
submissionViewIsReevaluable :: Bool,
|
|
submissionViewIsVisible :: Bool,
|
|
submissionViewIsPublic :: Bool,
|
|
submissionViewTeam :: Maybe Text
|
|
}
|
|
|
|
instance ToJSON SubmissionView where
|
|
toJSON s = object
|
|
["id" .= submissionViewId s
|
|
, "variant" .= submissionViewVariantId s
|
|
, "rank" .= submissionViewRank s
|
|
, "submitter" .= submissionViewSubmitter s
|
|
, "when" .= submissionViewWhen s
|
|
, "version" .= submissionViewVersion s
|
|
, "description" .= submissionViewDescription s
|
|
, "tags" .= submissionViewTags s
|
|
, "hash" .= submissionViewHash s
|
|
, "evaluations" .= submissionViewEvaluations s
|
|
, "isOwner" .= submissionViewIsOwner s
|
|
, "isReevaluable" .= submissionViewIsReevaluable s
|
|
, "isVisible" .= submissionViewIsVisible s
|
|
, "isPublic" .= submissionViewIsPublic s
|
|
, "team" .= submissionViewTeam s
|
|
]
|
|
|
|
instance ToSchema SubmissionView where
|
|
declareNamedSchema _ = do
|
|
stringSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy String)
|
|
boolSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy Bool)
|
|
intSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy Int)
|
|
intsSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy [Int])
|
|
tagsSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy [TagView])
|
|
evalsSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy [EvaluationView])
|
|
return $ NamedSchema (Just "SubmissionView") $ mempty
|
|
& type_ .~ SwaggerObject
|
|
& properties .~
|
|
fromList [ ("id", intSchema)
|
|
, ("variant", intSchema)
|
|
, ("rank", intSchema)
|
|
, ("submitter", stringSchema)
|
|
, ("when", stringSchema)
|
|
, ("version", intsSchema)
|
|
, ("description", stringSchema)
|
|
, ("tags", tagsSchema)
|
|
, ("hash", stringSchema)
|
|
, ("evaluations", evalsSchema)
|
|
, ("isOwner", boolSchema)
|
|
, ("isReevaluable", boolSchema)
|
|
, ("isVisible", boolSchema)
|
|
, ("isPublic", boolSchema)
|
|
, ("team", stringSchema)
|
|
]
|
|
& required .~ [ "id", "variant", "rank", "submitter", "when", "version",
|
|
"description", "tags", "hash", "evaluations",
|
|
"isOwner", "isReevaluable", "isVisible", "isPublic" ]
|
|
|
|
data SubmissionsView = SubmissionsView {
|
|
submissionsViewSubmissions :: [SubmissionView],
|
|
submissionsViewTests :: [TestReference]
|
|
}
|
|
|
|
instance ToJSON SubmissionsView where
|
|
toJSON ss = object
|
|
[ "tests" .= submissionsViewTests ss,
|
|
"submissions" .= submissionsViewSubmissions ss
|
|
]
|
|
|
|
instance ToSchema SubmissionsView where
|
|
declareNamedSchema _ = do
|
|
submissionViewsSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy [SubmissionView])
|
|
testRefsSchema <- declareSchemaRef (DPR.Proxy :: DPR.Proxy [TestReference])
|
|
return $ NamedSchema (Just "SubmissionsView") $ mempty
|
|
& type_ .~ SwaggerObject
|
|
& properties .~
|
|
fromList [ ("submissions", submissionViewsSchema)
|
|
, ("tests", testRefsSchema)
|
|
]
|
|
& required .~ [ "tests", "submission" ]
|
|
|
|
getChallengeSubmissions :: ((Entity Submission) -> Bool) -> Text -> Handler Html
|
|
getChallengeSubmissions condition challengeName = do
|
|
Entity challengeId challenge <- runDB $ getBy404 $ UniqueName challengeName
|
|
(evaluationMaps, tests') <- runDB $ getChallengeSubmissionInfos 1 condition (const True) id challengeId
|
|
let tests = sortBy testComparator tests'
|
|
mauth <- maybeAuth
|
|
let muserId = (\(Entity uid _) -> uid) <$> mauth
|
|
|
|
app <- getYesod
|
|
let scheme = appRepoScheme $ appSettings app
|
|
|
|
challengeRepo <- runDB $ get404 $ challengePublicRepo challenge
|
|
|
|
let params = getNumericalParams evaluationMaps
|
|
|
|
challengeLayout True challenge (challengeAllSubmissionsWidget muserId
|
|
challenge
|
|
scheme
|
|
challengeRepo
|
|
evaluationMaps
|
|
tests
|
|
params)
|
|
|
|
getNumericalParams :: [TableEntry] -> [Text]
|
|
getNumericalParams entries = filter (isNumericalParam entries) $ getAllParams entries
|
|
|
|
isNumericalParam :: [TableEntry] -> Text -> Bool
|
|
isNumericalParam entries param =
|
|
all doesTextRepresentNumber
|
|
$ concat
|
|
$ map ((map parameterValue)
|
|
. (filter (\p -> parameterName p == param))
|
|
. (map entityVal)
|
|
. tableEntryParams) entries
|
|
|
|
doesTextRepresentNumber :: Text -> Bool
|
|
doesTextRepresentNumber t = isJust $ ((TR.readMaybe $ T.unpack t) :: Maybe Double)
|
|
|
|
getAllParams :: [TableEntry] -> [Text]
|
|
getAllParams entries = sort
|
|
$ nub
|
|
$ concat
|
|
$ map (\entry -> map (parameterName . entityVal) (tableEntryParams entry)) entries
|
|
|
|
|
|
challengeAllSubmissionsWidget :: Maybe UserId
|
|
-> Challenge
|
|
-> RepoScheme
|
|
-> Repo
|
|
-> [TableEntry]
|
|
-> [Entity Test]
|
|
-> [Text]
|
|
-> WidgetFor App ()
|
|
challengeAllSubmissionsWidget muserId challenge scheme challengeRepo submissions tests params =
|
|
$(widgetFile "challenge-all-submissions")
|
|
where delta = Number 4
|
|
higherTheBetterArray = getIsHigherTheBetterArray $ map entityVal tests
|
|
|
|
paramGraphsWidget :: Challenge -> [Entity Test] -> [Text] -> WidgetFor App ()
|
|
paramGraphsWidget challenge tests params = $(widgetFile "param-graphs")
|
|
where chartJSs = getChartJss challenge selectedTests params
|
|
selectedTests = reverse $ getMainTests tests
|
|
|
|
getChartJss :: Challenge -> [Entity Test] -> [Text] -> JavascriptUrl (Route App)
|
|
getChartJss challenge tests params =
|
|
mconcat $ [(getChartJs challenge test param) | test <- tests, param <- params]
|
|
|
|
getChartJs :: Challenge
|
|
-> Entity Test
|
|
-> Text
|
|
-> JavascriptUrl (Route App)
|
|
getChartJs challenge (Entity testId test) param = [julius|
|
|
$.getJSON("@{ChallengeParamGraphDataR (challengeName challenge) testId param}", function(data) {
|
|
c3.generate({
|
|
bindto: '#chart-' + #{toJSON param} + '-' + #{toJSON testId},
|
|
data: data,
|
|
axis: {
|
|
x: {
|
|
label: #{toJSON param},
|
|
},
|
|
y: {
|
|
label: #{toJSON testFormatted},
|
|
}
|
|
}
|
|
}) });
|
|
|]
|
|
where testFormatted = formatTest test
|
|
|
|
|
|
challengeLayout :: Bool -> Challenge -> WidgetFor App () -> HandlerFor App Html
|
|
challengeLayout withHeader challenge widget = do
|
|
tagsAvailableAsJSON <- runDB $ getAvailableTagsAsJSON
|
|
theVersion <- runDB $ getBy404 $ UniqueVersionByCommit $ challengeVersion challenge
|
|
let versionFormatted = formatVersion ((versionMajor $ entityVal theVersion),
|
|
(versionMinor $ entityVal theVersion),
|
|
(versionPatch $ entityVal theVersion))
|
|
maybeUser <- maybeAuth
|
|
bc <- widgetToPageContent widget
|
|
defaultLayout $ do
|
|
setTitle "Challenge"
|
|
$(widgetFile "challenge")
|
|
|
|
getTestProgressR :: Int -> Int -> Handler TypedContent
|
|
getTestProgressR m d = runViewProgress $ doTestProgress m d
|
|
|
|
doTestProgress :: Int -> Int -> Channel -> Handler ()
|
|
doTestProgress m d chan = do
|
|
_ <- forM [1..m] $ (\i -> do
|
|
msg chan $ (Data.Text.pack $ ("GO\n" ++ show i))
|
|
liftIO $ threadDelay (d * 1000000)
|
|
return ())
|
|
return ()
|