From 74bbc91ea443d938de37daead216d3fb58968a48 Mon Sep 17 00:00:00 2001 From: JFronny Date: Sun, 12 May 2024 16:24:48 +0200 Subject: [PATCH] feat: implement submission (still needs testing) --- README.md | 3 +- .../io/gitlab/jfronny/sdom/SDCredentials.kt | 17 ++-- .../kotlin/io/gitlab/jfronny/sdom/SDom.kt | 73 ++++++++++++++-- .../jfronny/sdom/actions/SDSubmitAction.kt | 83 +++++++++++++++++++ .../jfronny/sdom/model/SDAddSubmission.kt | 19 +++++ .../jfronny/sdom/model/SDAddSubmissionFile.kt | 9 ++ .../gitlab/jfronny/sdom/model/SDJudgement.kt | 18 ++++ .../jfronny/sdom/model/SDJudgementType.kt | 8 ++ .../jfronny/sdom/model/SDLoginResult.kt | 2 +- .../io/gitlab/jfronny/sdom/model/SDResult.kt | 4 - .../gitlab/jfronny/sdom/model/SDSubmission.kt | 16 ++++ .../sdom/toolwindow/SDToolWindowFactory.kt | 13 ++- .../gitlab/jfronny/sdom/ui/SDResultPanel.kt | 52 +++++++++--- src/main/resources/META-INF/plugin.xml | 6 +- 14 files changed, 290 insertions(+), 33 deletions(-) create mode 100644 src/main/kotlin/io/gitlab/jfronny/sdom/actions/SDSubmitAction.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/sdom/model/SDAddSubmission.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/sdom/model/SDAddSubmissionFile.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/sdom/model/SDJudgement.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/sdom/model/SDJudgementType.kt delete mode 100644 src/main/kotlin/io/gitlab/jfronny/sdom/model/SDResult.kt create mode 100644 src/main/kotlin/io/gitlab/jfronny/sdom/model/SDSubmission.kt diff --git a/README.md b/README.md index da82d86..dafafbf 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/SDCredentials.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/SDCredentials.kt index 21325e9..55c41a7 100644 --- a/src/main/kotlin/io/gitlab/jfronny/sdom/SDCredentials.kt +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/SDCredentials.kt @@ -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 - 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) + } } \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/SDom.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/SDom.kt index 8eee410..175200c 100644 --- a/src/main/kotlin/io/gitlab/jfronny/sdom/SDom.kt +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/SDom.kt @@ -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>, Problem) -> Unit> = mutableListOf() + private val resultFlowListeners: MutableList<(SharedFlow>, Contest, Problem) -> Unit> = mutableListOf() fun registerLoginListener(listener: () -> Unit) { loginListeners.add(listener) @@ -71,7 +75,7 @@ object SDom { logoutListeners.add(listener) } - fun registerResultFlowListener(listener: (SharedFlow>, Problem) -> Unit) { + fun registerResultFlowListener(listener: (SharedFlow>, 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 = listOf() var currentProblem: Problem? = null + var judgementTypes: Map? = 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>() + .associateBy { it.id } } + suspend fun submitSolution( + contest: Contest, problem: Problem, solution: String, fileName: String + ): SharedFlow> = coroutineScope { + val resultFlow: MutableSharedFlow> = 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 = 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 diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/actions/SDSubmitAction.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/actions/SDSubmitAction.kt new file mode 100644 index 0000000..4beae03 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/actions/SDSubmitAction.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDAddSubmission.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDAddSubmission.kt new file mode 100644 index 0000000..c0b5476 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDAddSubmission.kt @@ -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, + //val code: List?, // binary +) \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDAddSubmissionFile.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDAddSubmissionFile.kt new file mode 100644 index 0000000..81d4f96 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDAddSubmissionFile.kt @@ -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" +) diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDJudgement.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDJudgement.kt new file mode 100644 index 0000000..6a246af --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDJudgement.kt @@ -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?, +) \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDJudgementType.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDJudgementType.kt new file mode 100644 index 0000000..91403f2 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDJudgementType.kt @@ -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 +) diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDLoginResult.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDLoginResult.kt index 36d2bca..f02b5dd 100644 --- a/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDLoginResult.kt +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDLoginResult.kt @@ -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, val type: String?, val id: String, diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDResult.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDResult.kt deleted file mode 100644 index 9504ac8..0000000 --- a/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDResult.kt +++ /dev/null @@ -1,4 +0,0 @@ -package io.gitlab.jfronny.sdom.model - -class SDResult { -} \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDSubmission.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDSubmission.kt new file mode 100644 index 0000000..caa17a1 --- /dev/null +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/model/SDSubmission.kt @@ -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?, + val id: String, + @SerialName("external_id") val externalId: String?, + @SerialName("entry_point") val entryPoint: String?, + @SerialName("import_error") val importError: String?, +) \ No newline at end of file diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/toolwindow/SDToolWindowFactory.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/toolwindow/SDToolWindowFactory.kt index bcb63b9..8f16589 100644 --- a/src/main/kotlin/io/gitlab/jfronny/sdom/toolwindow/SDToolWindowFactory.kt +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/toolwindow/SDToolWindowFactory.kt @@ -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>, task: Problem) { - + fun showResultContent(resultFlow: Flow>, 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) diff --git a/src/main/kotlin/io/gitlab/jfronny/sdom/ui/SDResultPanel.kt b/src/main/kotlin/io/gitlab/jfronny/sdom/ui/SDResultPanel.kt index a2e3927..7572436 100644 --- a/src/main/kotlin/io/gitlab/jfronny/sdom/ui/SDResultPanel.kt +++ b/src/main/kotlin/io/gitlab/jfronny/sdom/ui/SDResultPanel.kt @@ -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> + resultFlow: Flow>, + 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 } \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e61f31b..0b48949 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -56,7 +56,11 @@ text="Refresh Problems"> - + + +