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.
## TODO
- Implement submissions
- Implement reading results
- Implement reading detailed results
- Implement reading problems (and testcases)
## References

View File

@ -7,19 +7,20 @@ import com.intellij.ide.passwordSafe.PasswordSafe
import com.intellij.openapi.application.ApplicationManager
object SDCredentials {
private fun createCredentialAttributes(): CredentialAttributes {
return CredentialAttributes(generateServiceName("s-dom", "httpAuth"))
private fun createCredentialAttributes(name: String): CredentialAttributes {
return CredentialAttributes(generateServiceName("s-dom", name))
}
var credentials: Pair<String?, String?>
get() = PasswordSafe.instance[createCredentialAttributes()]
get() = PasswordSafe.instance[createCredentialAttributes("httpAuth")]
.run { this?.userName to this?.getPasswordAsString() }
set(value) {
PasswordSafe.instance[createCredentialAttributes()] = Credentials(value.first, value.second)
PasswordSafe.instance[createCredentialAttributes("httpAuth")] = Credentials(value.first, value.second)
}
fun logOut() {
PasswordSafe.instance[createCredentialAttributes()] = null
PasswordSafe.instance[createCredentialAttributes("httpAuth")] = null
PasswordSafe.instance[createCredentialAttributes("teamId")] = null
}
var url: String
@ -27,4 +28,10 @@ object SDCredentials {
set(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 io.gitlab.jfronny.sdom.actions.SDGetContestsAction
import io.gitlab.jfronny.sdom.actions.SDGetProblemsAction
import io.gitlab.jfronny.sdom.model.Contest
import io.gitlab.jfronny.sdom.model.Problem
import io.gitlab.jfronny.sdom.model.SDLoginResult
import io.gitlab.jfronny.sdom.model.SDResult
import io.gitlab.jfronny.sdom.model.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.java.*
@ -21,8 +18,15 @@ import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
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 {
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 resultFlowListeners: MutableList<(SharedFlow<Result<SDResult>>, Problem) -> Unit> = mutableListOf()
private val resultFlowListeners: MutableList<(SharedFlow<Result<SDJudgement>>, Contest, Problem) -> Unit> = mutableListOf()
fun registerLoginListener(listener: () -> Unit) {
loginListeners.add(listener)
@ -71,7 +75,7 @@ object SDom {
logoutListeners.add(listener)
}
fun registerResultFlowListener(listener: (SharedFlow<Result<SDResult>>, Problem) -> Unit) {
fun registerResultFlowListener(listener: (SharedFlow<Result<SDJudgement>>, Contest, Problem) -> Unit) {
resultFlowListeners.add(listener)
}
@ -84,6 +88,7 @@ object SDom {
if (!result.enabled) throw Exception("User is not enabled")
SDCredentials.credentials = Pair(username, password)
SDCredentials.teamId = result.teamId
SDCredentials.url = fixedApi
loginListeners.forEach { ApplicationManager.getApplication().invokeLater(it) }
@ -103,6 +108,7 @@ object SDom {
}
var problems: List<Problem> = listOf()
var currentProblem: Problem? = null
var judgementTypes: Map<String, SDJudgementType>? = null
suspend fun getContests() {
val result = client.get("${SDCredentials.url}contests")
@ -143,8 +149,63 @@ object SDom {
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
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("first_login_time") val firstLoginTime: String?, // DateTime
val team: String?,
@SerialName("team_id") val teamId: Int?,
@SerialName("team_id") val teamId: String?,
val roles: List<String>,
val type: 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.ToolWindowFactory
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.SDResult
import io.gitlab.jfronny.sdom.model.SDJudgement
import io.gitlab.jfronny.sdom.ui.SDResultPanel
import io.gitlab.jfronny.sdom.ui.SDSubmitPanel
import kotlinx.coroutines.flow.Flow
@ -30,8 +32,13 @@ class SDToolWindowFactory : ToolWindowFactory, DumbAware {
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)

View File

@ -2,23 +2,53 @@ package io.gitlab.jfronny.sdom.ui
import com.intellij.openapi.project.Project
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.first
import kotlinx.coroutines.launch
import java.awt.BorderLayout
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
class SDResultPanel(
val project: Project,
resultFlow: Flow<Result<SDResult>>
resultFlow: Flow<Result<SDJudgement>>,
contest: Contest,
problem: Problem,
) : ComponentContainer {
override fun dispose() {
TODO("Not yet implemented")
private val panel = JPanel(BorderLayout())
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 {
TODO("Not yet implemented")
}
override fun getPreferredFocusableComponent(): JComponent {
TODO("Not yet implemented")
}
override fun dispose() {}
override fun getComponent(): JComponent = panel
override fun getPreferredFocusableComponent(): JComponent = panel // TODO focus on actual content once it's there
}

View File

@ -56,7 +56,11 @@
text="Refresh Problems">
<add-to-group group-id="io.gitlab.jfronny.sdom.actions.SDToolbarActions"/>
</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"
id="io.gitlab.jfronny.sdom.actions.SDLogoutAction"
text="Log out of DOMjudge"/>