feat: implement submission (still needs testing)

This commit is contained in:
Johannes Frohnmeyer 2024-05-12 16:24:48 +02:00
parent c9a2aabc60
commit 74bbc91ea4
Signed by: Johannes
GPG Key ID: E76429612C2929F4
14 changed files with 290 additions and 33 deletions

View File

@ -3,8 +3,7 @@ S-DOM is a plugin for IntelliJ IDEA that allows you to submit your code to the D
It is currently in development and not yet ready for use. It is currently in development and not yet ready for use.
## TODO ## TODO
- Implement submissions - Implement reading detailed results
- Implement reading results
- Implement reading problems (and testcases) - Implement reading problems (and testcases)
## References ## References

View File

@ -7,19 +7,20 @@ import com.intellij.ide.passwordSafe.PasswordSafe
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
object SDCredentials { object SDCredentials {
private fun createCredentialAttributes(): CredentialAttributes { private fun createCredentialAttributes(name: String): CredentialAttributes {
return CredentialAttributes(generateServiceName("s-dom", "httpAuth")) return CredentialAttributes(generateServiceName("s-dom", name))
} }
var credentials: Pair<String?, String?> var credentials: Pair<String?, String?>
get() = PasswordSafe.instance[createCredentialAttributes()] get() = PasswordSafe.instance[createCredentialAttributes("httpAuth")]
.run { this?.userName to this?.getPasswordAsString() } .run { this?.userName to this?.getPasswordAsString() }
set(value) { set(value) {
PasswordSafe.instance[createCredentialAttributes()] = Credentials(value.first, value.second) PasswordSafe.instance[createCredentialAttributes("httpAuth")] = Credentials(value.first, value.second)
} }
fun logOut() { fun logOut() {
PasswordSafe.instance[createCredentialAttributes()] = null PasswordSafe.instance[createCredentialAttributes("httpAuth")] = null
PasswordSafe.instance[createCredentialAttributes("teamId")] = null
} }
var url: String var url: String
@ -27,4 +28,10 @@ object SDCredentials {
set(value) { set(value) {
ApplicationManager.getApplication().getService(SDSettings::class.java).state.url = value ApplicationManager.getApplication().getService(SDSettings::class.java).state.url = value
} }
var teamId: String?
get() = PasswordSafe.instance[createCredentialAttributes("teamId")]?.userName
set(value) {
PasswordSafe.instance[createCredentialAttributes("teamId")] = Credentials(value)
}
} }

View File

@ -8,10 +8,7 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import io.gitlab.jfronny.sdom.actions.SDGetContestsAction import io.gitlab.jfronny.sdom.actions.SDGetContestsAction
import io.gitlab.jfronny.sdom.actions.SDGetProblemsAction import io.gitlab.jfronny.sdom.actions.SDGetProblemsAction
import io.gitlab.jfronny.sdom.model.Contest import io.gitlab.jfronny.sdom.model.*
import io.gitlab.jfronny.sdom.model.Problem
import io.gitlab.jfronny.sdom.model.SDLoginResult
import io.gitlab.jfronny.sdom.model.SDResult
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.engine.java.* import io.ktor.client.engine.java.*
@ -21,8 +18,15 @@ import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.ByteArrayOutputStream
import java.util.Base64
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
object SDom { object SDom {
private const val CONTEST_ID_PROPERTY = "io.gitlab.jfronny.sdom.contestId" private const val CONTEST_ID_PROPERTY = "io.gitlab.jfronny.sdom.contestId"
@ -61,7 +65,7 @@ object SDom {
) )
}) })
private val logoutListeners: MutableList<() -> Unit> = mutableListOf() private val logoutListeners: MutableList<() -> Unit> = mutableListOf()
private val resultFlowListeners: MutableList<(SharedFlow<Result<SDResult>>, Problem) -> Unit> = mutableListOf() private val resultFlowListeners: MutableList<(SharedFlow<Result<SDJudgement>>, Contest, Problem) -> Unit> = mutableListOf()
fun registerLoginListener(listener: () -> Unit) { fun registerLoginListener(listener: () -> Unit) {
loginListeners.add(listener) loginListeners.add(listener)
@ -71,7 +75,7 @@ object SDom {
logoutListeners.add(listener) logoutListeners.add(listener)
} }
fun registerResultFlowListener(listener: (SharedFlow<Result<SDResult>>, Problem) -> Unit) { fun registerResultFlowListener(listener: (SharedFlow<Result<SDJudgement>>, Contest, Problem) -> Unit) {
resultFlowListeners.add(listener) resultFlowListeners.add(listener)
} }
@ -84,6 +88,7 @@ object SDom {
if (!result.enabled) throw Exception("User is not enabled") if (!result.enabled) throw Exception("User is not enabled")
SDCredentials.credentials = Pair(username, password) SDCredentials.credentials = Pair(username, password)
SDCredentials.teamId = result.teamId
SDCredentials.url = fixedApi SDCredentials.url = fixedApi
loginListeners.forEach { ApplicationManager.getApplication().invokeLater(it) } loginListeners.forEach { ApplicationManager.getApplication().invokeLater(it) }
@ -103,6 +108,7 @@ object SDom {
} }
var problems: List<Problem> = listOf() var problems: List<Problem> = listOf()
var currentProblem: Problem? = null var currentProblem: Problem? = null
var judgementTypes: Map<String, SDJudgementType>? = null
suspend fun getContests() { suspend fun getContests() {
val result = client.get("${SDCredentials.url}contests") val result = client.get("${SDCredentials.url}contests")
@ -143,8 +149,63 @@ object SDom {
currentProblem = null currentProblem = null
} }
} }
judgementTypes = client.get("${SDCredentials.url}contests/${currentContest!!.id}/judgement-types")
.body<List<SDJudgementType>>()
.associateBy { it.id }
} }
suspend fun submitSolution(
contest: Contest, problem: Problem, solution: String, fileName: String
): SharedFlow<Result<SDJudgement>> = coroutineScope {
val resultFlow: MutableSharedFlow<Result<SDJudgement>> = MutableSharedFlow()
launch {
try {
val response: SDSubmission = client.post("${SDCredentials.url}contests/${contest.id}/submissions") {
contentType(ContentType.Application.Json)
setBody(
SDAddSubmission(
problem = problem.id,
problemId = problem.id,
language = "cpp",
languageId = "cpp",
entryPoint = null,
files = listOf(SDAddSubmissionFile(zip(fileName, solution)))
)
)
}.body()
println(response)
if (response.importError != null) {
resultFlow.emit(Result.failure(Exception(response.importError)))
return@launch
}
do {
val result: List<SDJudgement> = client.get("${SDCredentials.url}contests/${contest.id}/judgements?submission_id=${response.id}").body()
if (result.isNotEmpty()) {
result.forEach { resultFlow.emit(Result.success(it)) }
break
} else {
Thread.sleep(1000)
}
} while (true)
} catch (e: Exception) {
resultFlow.emit(Result.failure(e))
}
}
resultFlowListeners.forEach { it -> ApplicationManager.getApplication().invokeLater { it(resultFlow, contest, problem) } }
return@coroutineScope resultFlow
}
private fun zip(name: String, content: String): String =
String(Base64.getEncoder().encode(ByteArrayOutputStream().use { baos ->
ZipOutputStream(baos).use {
it.putNextEntry(ZipEntry(name))
it.write(content.toByteArray())
it.closeEntry()
}
baos.toByteArray()
}))
val loggedIn: Boolean val loggedIn: Boolean
get() = SDCredentials.credentials.first != null && SDCredentials.credentials.second != null get() = SDCredentials.credentials.first != null && SDCredentials.credentials.second != null

View File

@ -0,0 +1,83 @@
package io.gitlab.jfronny.sdom.actions
import com.intellij.icons.AllIcons
import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.diagnostic.LogLevel
import com.intellij.openapi.diagnostic.Logger
import com.intellij.testFramework.utils.editor.getVirtualFile
import io.gitlab.jfronny.sdom.SDom
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class SDSubmitAction(text: String) : NotificationAction(text) {
constructor() : this("")
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun actionPerformed(e: AnActionEvent, notification: Notification) = actionPerformed(e)
override fun actionPerformed(e: AnActionEvent) {
println("Submitting")
val editor = e.getData(CommonDataKeys.EDITOR)
e.project?.let { project ->
if (editor == null) {
NotificationGroupManager.getInstance()
.getNotificationGroup("sdom.notifications")
.createNotification("You have to select a file", NotificationType.ERROR)
.setTitle("No file selected")
.addAction(SDSubmitAction("Retry"))
.notify(e.project)
return
}
val currentFile = editor.document.text
val fileName = editor.document.getVirtualFile().name
val contest = SDom.currentContest
if (contest == null) {
NotificationGroupManager.getInstance()
.getNotificationGroup("sdom.notifications")
.createNotification("You have to select a contest", NotificationType.ERROR)
.setTitle("No contest selected")
.addAction(SDContestSelectionNotificationAction("Select Contest"))
.addAction(SDSubmitAction("Retry"))
.notify(e.project)
return
}
val problem = SDom.currentProblem
if (problem == null) {
NotificationGroupManager.getInstance()
.getNotificationGroup("sdom.notifications")
.createNotification("You have to select a problem", NotificationType.ERROR)
.setTitle("No problem selected")
.addAction(SDProblemSelectionNotificationAction("Select Problem"))
.addAction(SDSubmitAction("Retry"))
.notify(e.project)
return
}
CoroutineScope(Job() + Dispatchers.IO).launch {
val result = SDom.submitSolution(contest, problem, currentFile, fileName)
println(result.first())
}
}
}
private val icon = AllIcons.Actions.Upload
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.project != null
e.presentation.icon = icon
}
companion object {
private val LOG = Logger.getInstance(SDSubmitAction::class.java).apply {
setLevel(LogLevel.DEBUG)
}
}
}

View File

@ -0,0 +1,19 @@
package io.gitlab.jfronny.sdom.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SDAddSubmission(
val problem: String?,
@SerialName("problem_id") val problemId: String?,
val language: String?,
@SerialName("language_id") val languageId: String?,
//@SerialName("team_id") val teamId: String? = null,
//@SerialName("user_id") val userId: String? = null,
//val time: String? = null, // DateTime
@SerialName("entry_point") val entryPoint: String?,
//val id: String? = null,
val files: List<SDAddSubmissionFile>,
//val code: List<String>?, // binary
)

View File

@ -0,0 +1,9 @@
package io.gitlab.jfronny.sdom.model
import kotlinx.serialization.Serializable
@Serializable
data class SDAddSubmissionFile(
val data: String,
val mime: String? = "application/zip"
)

View File

@ -0,0 +1,18 @@
package io.gitlab.jfronny.sdom.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SDJudgement(
@SerialName("start_time") val startTime: String?,
@SerialName("start_contest_time") val startContestTime: String,
@SerialName("end_time") val endTime: String?,
@SerialName("end_contest_time") val endContestTime: String?,
@SerialName("submission_id") val submissionId: String,
val id: String,
val valid: Boolean,
@SerialName("judgement_type_id") val judgementTypeId: String?,
@SerialName("judgehost") val judgeHost: String?,
@SerialName("max_run_time") val maxRunTime: Float?,
)

View File

@ -0,0 +1,8 @@
package io.gitlab.jfronny.sdom.model
data class SDJudgementType(
val id: String,
val name: String,
val penalty: Boolean,
val solved: Boolean
)

View File

@ -9,7 +9,7 @@ data class SDLoginResult(
@SerialName("last_api_login_time") val lastApiLoginTime: String?, // DateTime @SerialName("last_api_login_time") val lastApiLoginTime: String?, // DateTime
@SerialName("first_login_time") val firstLoginTime: String?, // DateTime @SerialName("first_login_time") val firstLoginTime: String?, // DateTime
val team: String?, val team: String?,
@SerialName("team_id") val teamId: Int?, @SerialName("team_id") val teamId: String?,
val roles: List<String>, val roles: List<String>,
val type: String?, val type: String?,
val id: String, val id: String,

View File

@ -1,4 +0,0 @@
package io.gitlab.jfronny.sdom.model
class SDResult {
}

View File

@ -0,0 +1,16 @@
package io.gitlab.jfronny.sdom.model
import kotlinx.serialization.SerialName
data class SDSubmission(
@SerialName("language_id") val languageId: String,
val time: String,
@SerialName("contest_time") val contestTime: String,
@SerialName("team_id") val teamId: String,
@SerialName("problem_id") val problemId: String,
val files: List<FileWithName>?,
val id: String,
@SerialName("external_id") val externalId: String?,
@SerialName("entry_point") val entryPoint: String?,
@SerialName("import_error") val importError: String?,
)

View File

@ -5,8 +5,10 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.openapi.wm.ToolWindowFactory
import io.gitlab.jfronny.sdom.SDom import io.gitlab.jfronny.sdom.SDom
import io.gitlab.jfronny.sdom.model.Contest
import io.gitlab.jfronny.sdom.model.Problem import io.gitlab.jfronny.sdom.model.Problem
import io.gitlab.jfronny.sdom.model.SDResult import io.gitlab.jfronny.sdom.model.SDJudgement
import io.gitlab.jfronny.sdom.ui.SDResultPanel
import io.gitlab.jfronny.sdom.ui.SDSubmitPanel import io.gitlab.jfronny.sdom.ui.SDSubmitPanel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -30,8 +32,13 @@ class SDToolWindowFactory : ToolWindowFactory, DumbAware {
contentManager.addContent(loggedOutContent) contentManager.addContent(loggedOutContent)
} }
fun showResultContent(resultFlow: Flow<Result<SDResult>>, task: Problem) { fun showResultContent(resultFlow: Flow<Result<SDJudgement>>, contest: Contest, problem: Problem) {
val resultPanel = SDResultPanel(project, resultFlow, contest, problem)
val resultContent = contentManager.factory.createContent(resultPanel.component, "Result", false).apply {
preferredFocusableComponent = resultPanel.preferredFocusableComponent
}
contentManager.addContent(resultContent)
contentManager.setSelectedContent(resultContent)
} }
SDom.registerLoginListener(::showSubmitContent) SDom.registerLoginListener(::showSubmitContent)

View File

@ -2,23 +2,53 @@ package io.gitlab.jfronny.sdom.ui
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComponentContainer import com.intellij.openapi.ui.ComponentContainer
import io.gitlab.jfronny.sdom.model.SDResult import com.intellij.ui.JBColor
import io.gitlab.jfronny.sdom.SDom
import io.gitlab.jfronny.sdom.model.Contest
import io.gitlab.jfronny.sdom.model.Problem
import io.gitlab.jfronny.sdom.model.SDJudgement
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.awt.BorderLayout
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
class SDResultPanel( class SDResultPanel(
val project: Project, val project: Project,
resultFlow: Flow<Result<SDResult>> resultFlow: Flow<Result<SDJudgement>>,
contest: Contest,
problem: Problem,
) : ComponentContainer { ) : ComponentContainer {
override fun dispose() { private val panel = JPanel(BorderLayout())
TODO("Not yet implemented")
init {
CoroutineScope(Job() + Dispatchers.IO).launch {
val resultResult = resultFlow.first()
if (resultResult.isSuccess) {
val result = resultResult.getOrThrow()
if (!result.valid) {
panel.add(JLabel("Judgement failed"), BorderLayout.CENTER)
return@launch
}
val parsedResult =
result.judgementTypeId
?.let { SDom.judgementTypes?.get(it) }
?.let { it.name to it.solved }
?: ("Unknown" to false)
panel.add(JLabel(parsedResult.first).apply { foreground = if (parsedResult.second) JBColor.GREEN else JBColor.RED }, BorderLayout.CENTER)
} else {
panel.add(JLabel("Judgement failed"), BorderLayout.CENTER)
}
}
} }
override fun getComponent(): JComponent { override fun dispose() {}
TODO("Not yet implemented") override fun getComponent(): JComponent = panel
} override fun getPreferredFocusableComponent(): JComponent = panel // TODO focus on actual content once it's there
override fun getPreferredFocusableComponent(): JComponent {
TODO("Not yet implemented")
}
} }

View File

@ -56,7 +56,11 @@
text="Refresh Problems"> text="Refresh Problems">
<add-to-group group-id="io.gitlab.jfronny.sdom.actions.SDToolbarActions"/> <add-to-group group-id="io.gitlab.jfronny.sdom.actions.SDToolbarActions"/>
</action> </action>
<!-- TODO: add submission action --> <action id="io.gitlab.jfronny.sdom.actions.SDSubmitAction"
class="io.gitlab.jfronny.sdom.actions.SDSubmitAction"
text="Submit Solution">
<add-to-group group-id="io.gitlab.jfronny.sdom.actions.SDToolbarActions"/>
</action>
<action class="io.gitlab.jfronny.sdom.actions.SDLogoutAction" <action class="io.gitlab.jfronny.sdom.actions.SDLogoutAction"
id="io.gitlab.jfronny.sdom.actions.SDLogoutAction" id="io.gitlab.jfronny.sdom.actions.SDLogoutAction"
text="Log out of DOMjudge"/> text="Log out of DOMjudge"/>