diff --git a/build.gradle.kts b/build.gradle.kts
index 5931698..9aafe03 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -56,11 +56,11 @@ tasks {
version.set(properties("pluginVersion").get())
changeNotes.set(
"""
-
Removed Deprecation
+ Added Review Mode
Improvements
- - Removed all DSL1 UI-Elements
- - Upgraded to DSL2
+ - Added Review Mode for Students
+ - Refactored Assessment Editor a bit
"""
)
diff --git a/gradle.properties b/gradle.properties
index 63853e4..e12d3ca 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,7 +4,7 @@ pluginGroup=de.tum.www1.artemis.plugin.intellij
pluginName=orion
pluginRepositoryUrl=https://github.com/ls1intum/Orion
# SemVer format -> https://semver.org
-pluginVersion=1.2.6
+pluginVersion=1.2.7
# Last 2 digits of the year and the major version digit, 211-211.* equals (20)21.1.*
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild=232
diff --git a/src/main/java/de/tum/www1/orion/OrionStartupProjectRefreshActivity.kt b/src/main/java/de/tum/www1/orion/OrionStartupProjectRefreshActivity.kt
index 9eaf7c4..2669dfa 100644
--- a/src/main/java/de/tum/www1/orion/OrionStartupProjectRefreshActivity.kt
+++ b/src/main/java/de/tum/www1/orion/OrionStartupProjectRefreshActivity.kt
@@ -58,6 +58,7 @@ class OrionStartupProjectRefreshActivity : ProjectActivity, DumbAware {
return
}
when (exerciseInfo.currentView) {
+ ExerciseView.STUDENT -> Unit
ExerciseView.TUTOR -> OrionAssessmentUtils.configureEditorsForAssessment(project)
else -> Unit
}
diff --git a/src/main/java/de/tum/www1/orion/connector/ide/exercise/IOrionExerciseConnector.kt b/src/main/java/de/tum/www1/orion/connector/ide/exercise/IOrionExerciseConnector.kt
index d5e0c63..1d8de55 100644
--- a/src/main/java/de/tum/www1/orion/connector/ide/exercise/IOrionExerciseConnector.kt
+++ b/src/main/java/de/tum/www1/orion/connector/ide/exercise/IOrionExerciseConnector.kt
@@ -37,6 +37,13 @@ interface IOrionExerciseConnector {
*/
fun initializeAssessment(submissionId: Long, feedback: String)
+ /**
+ * Initializes the [OrionFeedbackService]
+ *
+ * @param feedback a serialized [Programming]
+ */
+ fun initializeFeedback(feedback: String)
+
/**
* Clones the exercise participation repository and saves it under the artemis home directory
*
diff --git a/src/main/java/de/tum/www1/orion/connector/ide/exercise/OrionExerciseConnector.kt b/src/main/java/de/tum/www1/orion/connector/ide/exercise/OrionExerciseConnector.kt
index 7c4f1fb..959d119 100644
--- a/src/main/java/de/tum/www1/orion/connector/ide/exercise/OrionExerciseConnector.kt
+++ b/src/main/java/de/tum/www1/orion/connector/ide/exercise/OrionExerciseConnector.kt
@@ -9,6 +9,7 @@ import de.tum.www1.orion.dto.Feedback
import de.tum.www1.orion.dto.ProgrammingExercise
import de.tum.www1.orion.exercise.OrionAssessmentService
import de.tum.www1.orion.exercise.OrionExerciseService
+import de.tum.www1.orion.exercise.OrionFeedbackService
import de.tum.www1.orion.messaging.OrionIntellijStateNotifier
import de.tum.www1.orion.ui.browser.IBrowser
import de.tum.www1.orion.ui.util.notify
@@ -20,7 +21,7 @@ import java.util.*
/**
* Java handler for when an exercise is first opened
*/
-@Service
+@Service(Service.Level.PROJECT)
class OrionExerciseConnector(val project: Project) : OrionConnector(), IOrionExerciseConnector {
override fun editExercise(exerciseJson: String) {
val exercise = gson().fromJson(exerciseJson, ProgrammingExercise::class.java)
@@ -50,6 +51,19 @@ class OrionExerciseConnector(val project: Project) : OrionConnector(), IOrionExe
project.service().initializeFeedback(submissionId, feedbackArray)
}
+ override fun initializeFeedback(feedback: String) {
+ val feedbackArray = gson().fromJson(feedback, Array::class.java)
+ initializeFeedbackForParticipations(feedbackArray)
+ }
+
+ /**
+ * initializes feedback object for a student. it takes the first rated participation
+ * @param feedback an array of [Feedback] provided by the artemis client.
+ */
+ private fun initializeFeedbackForParticipations(feedback: Array) {
+ project.service().initializeFeedback(0, feedback)
+ }
+
override fun initializeHandlers(browser: IBrowser, queryInjector: JBCefJSQuery) {
val reactions = mapOf("editExercise" to { scanner: Scanner -> editExercise(scanner.nextAll()) },
"importParticipation" to { scanner: Scanner -> importParticipation(scanner.nextLine(), scanner.nextAll()) },
@@ -71,7 +85,8 @@ class OrionExerciseConnector(val project: Project) : OrionConnector(), IOrionExe
},
"initializeAssessment" to { scanner ->
initializeAssessment(scanner.nextLine().toLong(), scanner.nextAll())
- })
+ },
+ "initializeFeedback" to { scanner -> initializeFeedback(scanner.nextAll()) })
addJavaHandler(browser, reactions)
val parameterNames = mapOf(
@@ -82,7 +97,8 @@ class OrionExerciseConnector(val project: Project) : OrionConnector(), IOrionExe
"submissionId", "correctionRound", //"testRun",
"downloadURL"
),
- "initializeAssessment" to listOf("submissionId", "feedback")
+ "initializeAssessment" to listOf("submissionId", "feedback"),
+ "initializeFeedback" to listOf("feedback")
)
addLoadHandler(browser, queryInjector, parameterNames)
}
diff --git a/src/main/java/de/tum/www1/orion/dto/Dto.kt b/src/main/java/de/tum/www1/orion/dto/Dto.kt
index 8f5116c..6632cfc 100644
--- a/src/main/java/de/tum/www1/orion/dto/Dto.kt
+++ b/src/main/java/de/tum/www1/orion/dto/Dto.kt
@@ -3,6 +3,8 @@ package de.tum.www1.orion.dto
import de.tum.www1.orion.enumeration.ProgrammingLanguage
import java.net.URL
+// All entities are defined like the entities in Artemis
+
/**
* Course with properties as defined in Artemis at entities/course.model.ts
*/
@@ -21,6 +23,7 @@ data class ProgrammingExercise(
val programmingLanguage: ProgrammingLanguage,
val auxiliaryRepositories: List?,
val exerciseGroup: ExerciseGroup?,
+ val studentParticipations: Array
) {
/**
* Returns the course of the exercise, either directly or, if it is not set, from the associated exam
@@ -34,14 +37,59 @@ data class ProgrammingExercise(
}
}
-data class ExerciseGroup(val id: Long, val exam: Exam)
+/**
+ * A group of Exercises
+ * @param id the unique id
+ * @param exam the [Exam] the exercise group belongs to
+ */
+data class ExerciseGroup(
+ val id: Long,
+ val exam: Exam
+)
-data class Exam(val id: Long, val title: String, val course: Course)
+/**
+ * Exam class providing some values of the exam
+ */
+data class Exam(
+ val id: Long,
+ val title: String,
+ val course: Course
+)
/**
* Programming exercise participation as defined in Artemis at entities/participation/programming-exercise-student-participation.model.ts
+ * @param id the unique id of a programming exercise
+ * @param repositoryUrl the URL of the repository
+ * @param buildPlanId the id of the buildplan
*/
-data class ProgrammingExerciseParticipation(val id: Long, val repositoryUrl: URL, val buildPlanId: String)
+data class ProgrammingExerciseParticipation(
+ val id: Long,
+ val repositoryUrl: URL,
+ val buildPlanId: String,
+ var locked: Boolean
+)
+
+/**
+ * The Result of s student submission
+ * @param id the unique id of the result
+ * @param rated boolean value indicating if the result has a rating
+ * @param feedbacks an array containing tutor-feedback of the type [Feedback]
+ */
+data class Result(
+ val id: Long,
+ val rated: Boolean,
+ val feedbacks: Array?
+)
+
+/**
+ * A Programming excercise participation
+ * @param id the unique id
+ * @param results an Array containing [Result]s
+ */
+data class ProgrammingExerciseStudentParticipation(
+ val id: Long,
+ val results: Array?
+)
/**
* Auxiliary repository as defined in Artemis at entities/programming-exercise-auxiliary-repository-model.ts
@@ -66,7 +114,7 @@ data class AuxiliaryRepository(
data class Feedback(
var credits: Double,
var detailText: String,
- val reference: String,
+ val reference: String?,
val text: String,
val type: String,
var gradingInstruction: GradingInstruction?,
diff --git a/src/main/java/de/tum/www1/orion/exercise/OrionAssessmentService.kt b/src/main/java/de/tum/www1/orion/exercise/OrionAssessmentService.kt
index 4d4f183..0ae984e 100644
--- a/src/main/java/de/tum/www1/orion/exercise/OrionAssessmentService.kt
+++ b/src/main/java/de/tum/www1/orion/exercise/OrionAssessmentService.kt
@@ -1,7 +1,6 @@
package de.tum.www1.orion.exercise
import com.intellij.collaboration.ui.codereview.diff.EditorComponentInlaysManager
-import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.Service
@@ -26,21 +25,8 @@ import de.tum.www1.orion.util.translate
* @property project the service belongs to
*/
@Service(Service.Level.PROJECT)
-class OrionAssessmentService(private val project: Project) {
- private var feedbackPerFile: MutableMap> = mutableMapOf()
- // set storing a pair of path and line for all new, unsaved feedback comments
- // required to ensure only one feedback can be created per line
- private val pendingFeedback: MutableSet> = mutableSetOf()
- private var isInitialized: Boolean = false
-
- /**
- * Initializes the map with feedback sent from Artemis and notifies all [OrionAssessmentEditor]s
- *
- * @param submissionId to check validity against
- * @param feedback to load
- */
- fun initializeFeedback(submissionId: Long, feedback: Array) {
- // validate submissionId, reject feedback for a different submission
+class OrionAssessmentService(private val project: Project) : OrionInlineCommentService(project = project) {
+ override fun initializeFeedback(submissionId: Long, feedback: Array) {
if (project.service().submissionId != submissionId) {
project.notify(translate("orion.warning.assessment.submissionId"))
return
@@ -57,11 +43,14 @@ class OrionAssessmentService(private val project: Project) {
// reference has the format "file:FILE_line:LINE"
feedback.forEach {
- val textParts = it.reference.split("_")
- if (textParts.size == 2 && textParts[0].startsWith("file:") && textParts[1].startsWith("line:")) {
- it.path = textParts[0].substring(5)
- it.line = textParts[1].substring(5).toInt()
+ if (it.reference != null) {
+ val textParts = it.reference.split("_")
+ if (textParts.size == 2 && textParts[0].startsWith("file:") && textParts[1].startsWith("line:")) {
+ it.path = textParts[0].substring(5)
+ it.line = textParts[1].substring(5).toInt()
+ }
}
+
}
// filter invalid entries, group by file
@@ -81,21 +70,33 @@ class OrionAssessmentService(private val project: Project) {
}
} else {
// if not first load, close and reopen all files opened by assessment editors to reload the feedback
- closeAssessmentEditors(true)
+ closeEditors(true)
}
}
}
+
/**
- * Retrieves the feedback list for the given file or null if no feedback has been loaded yet
+ * Adds a new feedback comment to the given file and line, if no feedback comment is present at that position yet
*
- * @param relativePath of the file to get the feedback for
- * @return feedback of the file
+ * @param path of the file to add a feedback to
+ * @param line line in that file to add feedback to
+ * @param inlaysManager passed through to the new comment
*/
- fun getFeedbackFor(relativePath: String): List? {
- if (!isInitialized) return null
+ fun addFeedbackCommentIfPossible(path: String, line: Int, inlaysManager: EditorComponentInlaysManager) {
+
+ val pair = Pair(path, line)
+ // if there is already a feedback comment in that file and line, abort
+ if (pendingFeedback.contains(pair) || getFeedbackFor(path)
+ ?.any { it.line == line } == true
+ ) {
+ return
+ }
+
+ // add feedback
+ InlineAssessmentComment(path, line, inlaysManager)
+ pendingFeedback.add(pair)
- return feedbackPerFile[relativePath] ?: emptyList()
}
/**
@@ -117,6 +118,16 @@ class OrionAssessmentService(private val project: Project) {
synchronizeWithArtemis()
}
+ /**
+ * Removes a pending feedback. This allows a different feedback to be added to the given file and line
+ *
+ * @param path
+ * @param line
+ */
+ fun deletePendingFeedback(path: String, line: Int) {
+ pendingFeedback.remove(Pair(path, line))
+ }
+
/**
* Adds the given feedback to the map and informs Artemis
*
@@ -130,68 +141,13 @@ class OrionAssessmentService(private val project: Project) {
}
/**
- * Close all open [OrionAssessmentEditor]s and reinitialize all variables.
- * Should be called upon downloading a new submission
- */
- fun reset() {
- closeAssessmentEditors(false)
- feedbackPerFile = mutableMapOf()
- isInitialized = false
- }
-
- /**
- * Adds a new feedback comment to the given file and line, if no feedback comment is present at that position yet
- *
- * @param path of the file to add a feedback to
- * @param line line in that file to add feedback to
- * @param inlaysManager passed through to the new comment
- */
- fun addFeedbackCommentIfPossible(path: String, line: Int, inlaysManager: EditorComponentInlaysManager) {
- val pair = Pair(path, line)
- // if there is already a feedback comment in that file and line, abort
- if (pendingFeedback.contains(pair) || getFeedbackFor(path)
- ?.any { it.line == line } == true) {
- return
- }
-
- // add feedback
- InlineAssessmentComment(path, line, inlaysManager)
- pendingFeedback.add(pair)
- }
-
- /**
- * Removes a pending feedback. This allows a different feedback to be added to the given file and line
- *
- * @param path
- * @param line
+ * Synchronizes tutorfeedback with Artemis
*/
- fun deletePendingFeedback(path: String, line: Int) {
- pendingFeedback.remove(Pair(path, line))
- }
+ fun synchronizeWithArtemis() {
- private fun synchronizeWithArtemis() {
val submissionId = project.service().submissionId ?: return
val feedbackAsJson = gson().toJson(feedbackPerFile.values.flatten())
project.messageBus.syncPublisher(OrionIntellijStateNotifier.INTELLIJ_STATE_TOPIC)
.updateAssessment(submissionId, feedbackAsJson)
}
-
- private fun closeAssessmentEditors(reopen: Boolean) {
- WriteAction.runAndWait {
- FileEditorManager.getInstance(project).let { manager ->
- val selectedFile = manager.selectedEditor?.file
- manager.allEditors.filterIsInstance().map { it.file }
- .forEach {
- manager.closeFile(it)
- if (reopen && it != selectedFile) {
- manager.openFile(it, false)
- }
- }
- // open selected file last to ensure focus; the focusEditor parameter does not seem to work
- if (reopen && selectedFile != null) {
- manager.openFile(selectedFile, true)
- }
- }
- }
- }
}
diff --git a/src/main/java/de/tum/www1/orion/exercise/OrionExerciseService.kt b/src/main/java/de/tum/www1/orion/exercise/OrionExerciseService.kt
index f319877..369ab48 100644
--- a/src/main/java/de/tum/www1/orion/exercise/OrionExerciseService.kt
+++ b/src/main/java/de/tum/www1/orion/exercise/OrionExerciseService.kt
@@ -31,7 +31,7 @@ import de.tum.www1.orion.util.runWithIndeterminateProgressModal
import de.tum.www1.orion.util.translate
import de.tum.www1.orion.vcs.OrionGitAdapter
import de.tum.www1.orion.vcs.OrionGitAdapter.clone
-import org.slf4j.LoggerFactory
+import com.intellij.openapi.diagnostic.Logger
import java.io.File
import java.io.IOException
import java.nio.file.Files
@@ -107,7 +107,7 @@ class OrionExerciseService(private val project: Project) {
ProjectUtil.openOrImport(projectPath, project, false)
}
} catch (e: IOException) {
- LoggerFactory.getLogger(OrionExerciseConnector::class.java).error(e.message, e)
+ Logger.getInstance(OrionExerciseConnector::class.java).error(e.message, e)
project.notify(e.toString())
}
}
diff --git a/src/main/java/de/tum/www1/orion/exercise/OrionFeedbackService.kt b/src/main/java/de/tum/www1/orion/exercise/OrionFeedbackService.kt
new file mode 100644
index 0000000..15a8744
--- /dev/null
+++ b/src/main/java/de/tum/www1/orion/exercise/OrionFeedbackService.kt
@@ -0,0 +1,59 @@
+package de.tum.www1.orion.exercise
+
+import com.intellij.openapi.application.invokeAndWaitIfNeeded
+import com.intellij.openapi.application.runInEdt
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.project.Project
+import de.tum.www1.orion.dto.Feedback
+import de.tum.www1.orion.ui.feedback.OrionFeedbackCommentEditor
+import de.tum.www1.orion.ui.util.YesNoChooser
+
+/**
+ * A Feedback service that provides a feedback from tutors for students
+ */
+@Service(Service.Level.PROJECT)
+class OrionFeedbackService(private val project: Project) : OrionInlineCommentService(project = project) {
+ override fun initializeFeedback(submissionId: Long, feedback: Array) {
+ runInEdt {
+ if (isInitialized) {
+ // if feedback already has been loaded, ask if it should be overridden
+ if (!invokeAndWaitIfNeeded { YesNoChooser(project, "feedbackOverwrite").showAndGet() }) {
+ // if no, do nothing
+ return@runInEdt
+ }
+ }
+
+ // reference has the format "file:FILE_line:LINE"
+ feedback.forEach {
+ if (it.reference != null) {
+ val textParts = it.reference.split("_")
+ if (textParts.size == 2 && textParts[0].startsWith("file:") && textParts[1].startsWith("line:")) {
+ it.path = textParts[0].substring(5)
+ it.line = textParts[1].substring(5).toInt()
+ }
+ }
+
+ }
+
+ // filter invalid entries, group by file
+ this.feedbackPerFile = feedback.filter {
+ it.path != null && it.line != null
+ }.groupByTo(mutableMapOf()) {
+ it.path!!
+ }
+ if (!isInitialized) {
+ isInitialized = true
+ // if first load, insert feedback into all currently open editors since their own initialization will have occurred before the feedback was loaded and therefore failed
+ FileEditorManager.getInstance(project).let {
+ it.allEditors.forEach { editor ->
+ (editor as? OrionFeedbackCommentEditor)?.initializeFeedback()
+ }
+ }
+ } else {
+ // if not first load, close and reopen all files opened by assessment editors to reload the feedback
+ closeEditors(true)
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/tum/www1/orion/exercise/OrionInlineCommentService.kt b/src/main/java/de/tum/www1/orion/exercise/OrionInlineCommentService.kt
new file mode 100644
index 0000000..b214b5e
--- /dev/null
+++ b/src/main/java/de/tum/www1/orion/exercise/OrionInlineCommentService.kt
@@ -0,0 +1,74 @@
+package de.tum.www1.orion.exercise
+
+import com.intellij.openapi.application.WriteAction
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.project.Project
+import de.tum.www1.orion.dto.Feedback
+import de.tum.www1.orion.ui.assessment.OrionAssessmentEditor
+
+/**
+ * Super class that provides shared functionality to create Editors including comments
+ */
+abstract class OrionInlineCommentService(private val project: Project) {
+ var feedbackPerFile: MutableMap> = mutableMapOf()
+
+ // set storing a pair of path and line for all new, unsaved feedback comments
+ // required to ensure only one feedback can be created per line
+ val pendingFeedback: MutableSet> = mutableSetOf()
+ var isInitialized: Boolean = false
+
+
+ /**
+ * Initializes the map with feedback sent from Artemis and notifies all [OrionAssessmentEditor]s
+ *
+ * @param submissionId to check validity against
+ * @param feedback to load
+ */
+ abstract fun initializeFeedback(submissionId: Long, feedback: Array)
+
+
+ /**
+ * Retrieves the feedback list for the given file or null if no feedback has been loaded yet
+ *
+ * @param relativePath of the file to get the feedback for
+ * @return feedback of the file
+ */
+ fun getFeedbackFor(relativePath: String): List? {
+ if (!isInitialized) return null
+
+ return feedbackPerFile[relativePath] ?: emptyList()
+ }
+
+ /**
+ * Close all open [OrionAssessmentEditor]s and reinitialize all variables.
+ * Should be called upon downloading a new submission
+ */
+ fun reset() {
+ closeEditors(false)
+ feedbackPerFile = mutableMapOf()
+ isInitialized = false
+ }
+
+ /**
+ * Closes the assementEditor
+ * @param reopen specifies if the editor should be opened again after closing
+ */
+ fun closeEditors(reopen: Boolean) {
+ WriteAction.runAndWait {
+ FileEditorManager.getInstance(project).let { manager ->
+ val selectedFile = manager.selectedEditor?.file
+ manager.allEditors.filterIsInstance().map { it.file }
+ .forEach {
+ manager.closeFile(it)
+ if (reopen && it != selectedFile) {
+ manager.openFile(it, false)
+ }
+ }
+ // open selected file last to ensure focus; the focusEditor parameter does not seem to work
+ if (reopen && selectedFile != null) {
+ manager.openFile(selectedFile, true)
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/tum/www1/orion/exercise/OrionJavaInstructorProjectCreator.kt b/src/main/java/de/tum/www1/orion/exercise/OrionJavaInstructorProjectCreator.kt
index 145174f..eceec83 100644
--- a/src/main/java/de/tum/www1/orion/exercise/OrionJavaInstructorProjectCreator.kt
+++ b/src/main/java/de/tum/www1/orion/exercise/OrionJavaInstructorProjectCreator.kt
@@ -13,7 +13,7 @@ object OrionJavaInstructorProjectCreator {
private const val BASE_TEMPLATE_PATH = "template/instructor_project"
/**
- * Prepares an exercise opened as instructor by editing the IntelliJ configuration to match the project setup
+ * Prepares an exercise opened as instructor by editing the IDE configuration to match the project setup
* Uses the templates from /resources/template
*
* @param baseDir location of the project
diff --git a/src/main/java/de/tum/www1/orion/ui/assessment/InlineAssessmentComment.kt b/src/main/java/de/tum/www1/orion/ui/assessment/InlineAssessmentComment.kt
index 56202cc..d8d0b7e 100644
--- a/src/main/java/de/tum/www1/orion/ui/assessment/InlineAssessmentComment.kt
+++ b/src/main/java/de/tum/www1/orion/ui/assessment/InlineAssessmentComment.kt
@@ -8,12 +8,11 @@ import com.intellij.openapi.fileTypes.FileTypes
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.EditorTextField
-import com.intellij.ui.JBColor
import de.tum.www1.orion.dto.Feedback
import de.tum.www1.orion.exercise.OrionAssessmentService
+import de.tum.www1.orion.ui.util.ColorUtils
import de.tum.www1.orion.util.translate
import java.awt.BorderLayout
-import java.awt.Color
import java.awt.Component
import javax.swing.*
import javax.swing.border.TitledBorder
@@ -99,13 +98,19 @@ class InlineAssessmentComment(
// create a border of the background color, so we don't have to set the color manually
val textPanel = JPanel()
- textPanel.border = BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(4, 1, 4, 2), translate("orion.exercise.assessment.feedback"))
+ textPanel.border = BorderFactory.createTitledBorder(
+ BorderFactory.createEmptyBorder(25, 5, 5, 5),
+ translate("orion.exercise.assessment.feedback")
+ )
textPanel.layout = BorderLayout()
textPanel.add(gradingInstructionLabel, BorderLayout.NORTH)
textPanel.add(textField.component, BorderLayout.CENTER)
val spinnerPanel = JPanel()
- spinnerPanel.border = BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(4,1,4,2), translate("orion.exercise.assessment.score"))
+ spinnerPanel.border = BorderFactory.createTitledBorder(
+ BorderFactory.createEmptyBorder(25, 5, 5, 10),
+ translate("orion.exercise.assessment.score")
+ )
spinnerPanel.layout = BorderLayout()
spinnerPanel.add(spinner, BorderLayout.CENTER)
@@ -218,24 +223,12 @@ class InlineAssessmentComment(
private fun updateColor() {
val spinnerValue = spinner.value.toString().toDouble()
- // colors are the same as in Artemis
- val color = when {
- spinnerValue > 0 -> JBColor(0xd4edda, 0x00231a)
- spinnerValue < 0 -> JBColor(0xf8d7da, 0x370b07)
- else -> JBColor(0xfff3cd, 0x362203)
- }
- val textColor = when {
- spinnerValue > 0 -> JBColor(0x186429, 0x8cb294)
- spinnerValue < 0 -> JBColor(0x842029, 0xc29094)
- else -> JBColor(0x664d03, 0xb3a681)
- }
-
coloredBackgroundComponentList.forEach {
- it.background = color
+ it.background = ColorUtils.getFeedbackColor(spinnerValue)
}
coloredForegroundComponentList.forEach {
- (it as? TitledBorder)?.titleColor = textColor
- (it as? JComponent)?.foreground = textColor
+ (it as? TitledBorder)?.titleColor = ColorUtils.getFeedbackTextColor(spinnerValue)
+ (it as? JComponent)?.foreground = ColorUtils.getFeedbackTextColor(spinnerValue)
}
}
}
diff --git a/src/main/java/de/tum/www1/orion/ui/feedback/FeedbackCommentEditorProvider.kt b/src/main/java/de/tum/www1/orion/ui/feedback/FeedbackCommentEditorProvider.kt
new file mode 100644
index 0000000..4464963
--- /dev/null
+++ b/src/main/java/de/tum/www1/orion/ui/feedback/FeedbackCommentEditorProvider.kt
@@ -0,0 +1,46 @@
+package de.tum.www1.orion.ui.feedback
+
+import com.intellij.openapi.components.service
+import com.intellij.openapi.editor.EditorFactory
+import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.intellij.openapi.fileEditor.FileEditor
+import com.intellij.openapi.fileEditor.FileEditorPolicy
+import com.intellij.openapi.fileEditor.FileEditorProvider
+import com.intellij.openapi.project.DumbAware
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.vfs.VirtualFile
+import de.tum.www1.orion.exercise.OrionFeedbackService
+import de.tum.www1.orion.util.translate
+
+
+/**
+ * Provides an Editor that shows Feedback in IntelliJ
+ */
+class FeedbackCommentEditorProvider : FileEditorProvider, DumbAware {
+ override fun accept(project: Project, file: VirtualFile): Boolean {
+ //Check if there is feedback available
+ val relativePath = file.path.removePrefix(project.basePath.toString()).removePrefix("/")
+ return project.service().getFeedbackFor(relativePath) != null
+ }
+
+ override fun createEditor(project: Project, file: VirtualFile): FileEditor {
+ val factory = EditorFactory.getInstance()
+ val viewer = file.let { it ->
+ FileDocumentManager.getInstance().getDocument(it)?.let {
+ factory.createEditor(it, project, file.fileType, true)
+ }
+ } ?: factory.createViewer(factory.createDocument(translate("orion.error.file.loaded")), project)
+ // remove base bath from the path
+ val relativePath = file.path.removePrefix(project.basePath.toString()).removePrefix("/")
+ val editor = OrionFeedbackCommentEditor(viewer, relativePath, file)
+ // dispose editor with the project
+ Disposer.register(project, editor)
+ return editor
+ }
+
+ override fun getEditorTypeId(): String = "OrionFeedbackEditor"
+
+ override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.HIDE_DEFAULT_EDITOR
+
+}
diff --git a/src/main/java/de/tum/www1/orion/ui/feedback/InlineFeedbackComment.kt b/src/main/java/de/tum/www1/orion/ui/feedback/InlineFeedbackComment.kt
new file mode 100644
index 0000000..fb7bd79
--- /dev/null
+++ b/src/main/java/de/tum/www1/orion/ui/feedback/InlineFeedbackComment.kt
@@ -0,0 +1,101 @@
+package de.tum.www1.orion.ui.feedback
+
+import com.intellij.collaboration.ui.codereview.diff.EditorComponentInlaysManager
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.fileTypes.FileTypes
+import com.intellij.openapi.project.Project
+import com.intellij.ui.EditorTextField
+import de.tum.www1.orion.dto.Feedback
+import de.tum.www1.orion.ui.util.ColorUtils
+import de.tum.www1.orion.util.translate
+import java.awt.BorderLayout
+import java.awt.Component
+import javax.swing.BorderFactory
+import javax.swing.BoxLayout
+import javax.swing.JComponent
+import javax.swing.JPanel
+import javax.swing.border.TitledBorder
+
+/**
+ * A ui class that holds a not editable feedback comment
+ */
+class InlineFeedbackComment(
+ private var feedback: Feedback,
+ inlaysManager: EditorComponentInlaysManager
+) {
+ private var disposer: Disposable?
+ private val project: Project
+ private val coloredBackgroundComponentList: List
+ private val coloredForegroundComponentList: List
+
+ val component: JComponent = JPanel()
+ private val textField: EditorTextField
+ private val pointsTextField: EditorTextField
+ private val buttonBar: JPanel = JPanel()
+
+ init {
+ project = inlaysManager.editor.project!!
+
+ // the text field must be an [EditorTextField], otherwise important keys like enter or delete will not get forwarded by IntelliJ
+ textField = EditorTextField(feedback.detailText, project, FileTypes.PLAIN_TEXT)
+ textField.isEnabled = false
+ textField.setOneLineMode(false)
+ textField.border = null
+
+ // enter points
+ pointsTextField = EditorTextField(" " + feedback.credits.toString() + " ", project, FileTypes.PLAIN_TEXT)
+ pointsTextField.isEnabled = false
+
+ // create a border of the background color, so we don't have to set the color manually
+ val textPanel = JPanel()
+ textPanel.border = BorderFactory.createTitledBorder(
+ BorderFactory.createEmptyBorder(25, 5, 5, 5),
+ translate("orion.exercise.assessment.feedback")
+ )
+ textPanel.layout = BorderLayout()
+ textPanel.add(textField.component, BorderLayout.CENTER)
+
+ val pointsPanel = JPanel()
+ pointsPanel.border = BorderFactory.createTitledBorder(
+ BorderFactory.createEmptyBorder(25, 5, 5, 10),
+ translate("orion.exercise.assessment.score")
+ )
+ pointsPanel.layout = BorderLayout()
+ pointsPanel.add(pointsTextField, BorderLayout.CENTER)
+
+ // create a for points
+ val rightBar = JPanel()
+ rightBar.isOpaque = false
+ rightBar.layout = BoxLayout(rightBar, BoxLayout.LINE_AXIS)
+ pointsPanel.alignmentX = Component.BOTTOM_ALIGNMENT
+ rightBar.add(pointsPanel)
+
+ buttonBar.isOpaque = false
+
+ component.layout = BorderLayout()
+ component.add(textPanel, BorderLayout.CENTER)
+ component.add(rightBar, BorderLayout.EAST)
+ component.add(buttonBar, BorderLayout.SOUTH)
+
+ coloredBackgroundComponentList =
+ listOf(
+ component,
+ textPanel,
+ pointsPanel,
+ )
+ coloredForegroundComponentList = listOf(textPanel.border, pointsPanel.border)
+
+ updateColor()
+ disposer = inlaysManager.insertAfter(feedback.line!!, component)
+ }
+
+ private fun updateColor() {
+ coloredBackgroundComponentList.forEach {
+ it.background = ColorUtils.getFeedbackColor(feedback.credits)
+ }
+ coloredForegroundComponentList.forEach {
+ (it as? TitledBorder)?.titleColor = ColorUtils.getFeedbackTextColor(feedback.credits)
+ (it as? JComponent)?.foreground = ColorUtils.getFeedbackTextColor(feedback.credits)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/tum/www1/orion/ui/feedback/OrionFeedbackCommentEditor.kt b/src/main/java/de/tum/www1/orion/ui/feedback/OrionFeedbackCommentEditor.kt
new file mode 100644
index 0000000..2ac1e66
--- /dev/null
+++ b/src/main/java/de/tum/www1/orion/ui/feedback/OrionFeedbackCommentEditor.kt
@@ -0,0 +1,65 @@
+package de.tum.www1.orion.ui.feedback
+
+import com.intellij.collaboration.ui.codereview.diff.EditorComponentInlaysManager
+import com.intellij.diff.util.FileEditorBase
+import com.intellij.openapi.components.service
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.EditorFactory
+import com.intellij.openapi.editor.impl.EditorImpl
+import com.intellij.openapi.vfs.VirtualFile
+import de.tum.www1.orion.exercise.OrionFeedbackService
+import de.tum.www1.orion.util.OrionAssessmentUtils
+import de.tum.www1.orion.util.translate
+import javax.swing.JComponent
+import javax.swing.JLabel
+
+/**
+ * A editor for Feedback comments providing a view for students to review feedback with maintaining the edit ability of their code.
+ */
+class OrionFeedbackCommentEditor(
+ private var myEditor: Editor,
+ private val relativePath: String,
+ private val file: VirtualFile
+) : FileEditorBase() {
+ private val headerLabel: JLabel =
+ OrionAssessmentUtils.createHeader(translate("orion.exercise.feedbackModeLoading").uppercase())
+
+ init {
+ myEditor.headerComponent = headerLabel
+
+ initializeFeedback()
+ }
+
+ override fun getComponent(): JComponent = myEditor.component
+
+ override fun getName(): String = translate("orion.exercise.feedback")
+
+ override fun getPreferredFocusedComponent(): JComponent? = null
+
+ // needed to avoid deprecation warning; Should return the file for which the provider was called, note however this editor is showing a different file
+ override fun getFile(): VirtualFile = file
+
+ /**
+ * Requests the [OrionFeedbackService] for feedback comments for the opened file.
+ * If successful, adds the returned feedback comments as well as the gutter icons to create new comments to the editor
+ * If not, does nothing. Relies on the [OrionFeedbackService] to be called again if feedback becomes available
+ */
+ fun initializeFeedback() {
+ // request feedback, if not yet initialized, abort
+ val feedback = myEditor.project?.service()?.getFeedbackFor(relativePath) ?: return
+ val editorImpl = myEditor as? EditorImpl ?: return
+ // inlays manager that manages the inline comments
+ val inlaysManager = EditorComponentInlaysManager(editorImpl)
+ // add feedback
+ feedback.forEach {
+ InlineFeedbackComment(it, inlaysManager)
+ }
+ // remove loading text
+ headerLabel.text = translate("orion.exercise.feedbackMode").uppercase()
+ }
+
+ override fun dispose() {
+ super.dispose()
+ EditorFactory.getInstance().releaseEditor(myEditor)
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/tum/www1/orion/ui/util/ColorUtils.kt b/src/main/java/de/tum/www1/orion/ui/util/ColorUtils.kt
new file mode 100644
index 0000000..98e7f1f
--- /dev/null
+++ b/src/main/java/de/tum/www1/orion/ui/util/ColorUtils.kt
@@ -0,0 +1,38 @@
+package de.tum.www1.orion.ui.util
+
+import com.intellij.ui.JBColor
+
+/**
+ * Provides color values for Dark and Light-mode in several places
+ */
+class ColorUtils {
+ companion object {
+
+ /**
+ * Returns the jetbrains color values for dark and light mode for a feedback box
+ * colors are the same as in Artemis
+ */
+ fun getFeedbackColor(value: Double): JBColor {
+
+ // colors are the same as in Artemis
+ return when {
+ // JB Color for Light and Dark themes
+ value > 0 -> JBColor(0xd4edda, 0x00231a)
+ value < 0 -> JBColor(0xf8d7da, 0x370b07)
+ else -> JBColor(0xfff3cd, 0x362203)
+ }
+ }
+
+ /**
+ * Returns the jetbrains color values for dark and light mode for a feedback text
+ * colors are the same as in Artemis
+ */
+ fun getFeedbackTextColor(value: Double): JBColor {
+ return when {
+ value > 0 -> JBColor(0x186429, 0x8cb294)
+ value < 0 -> JBColor(0x842029, 0xc29094)
+ else -> JBColor(0x664d03, 0xb3a681)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/tum/www1/orion/ui/util/CommitMessageChooser.kt b/src/main/java/de/tum/www1/orion/ui/util/CommitMessageChooser.kt
index 9a0862b..cc4def9 100644
--- a/src/main/java/de/tum/www1/orion/ui/util/CommitMessageChooser.kt
+++ b/src/main/java/de/tum/www1/orion/ui/util/CommitMessageChooser.kt
@@ -3,6 +3,7 @@ package de.tum.www1.orion.ui.util
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
+import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import de.tum.www1.orion.settings.OrionBundle
import de.tum.www1.orion.settings.OrionSettingsProvider
@@ -61,7 +62,9 @@ class CommitMessageChooser(val project: Project) :
label(translate("orion.dialog.commitmessagechooser.title"))
}
row {
- textField().component.text = settings.getSetting(OrionSettingsProvider.KEYS.COMMIT_MESSAGE)
+ commitMessageField =
+ textField().bindText({ settings.getSetting(OrionSettingsProvider.KEYS.COMMIT_MESSAGE) },
+ {}).component
}
}
diff --git a/src/main/java/de/tum/www1/orion/ui/util/ConfirmPasswordSaveDialog.kt b/src/main/java/de/tum/www1/orion/ui/util/ConfirmPasswordSaveDialog.kt
index 352e39c..f0f1877 100644
--- a/src/main/java/de/tum/www1/orion/ui/util/ConfirmPasswordSaveDialog.kt
+++ b/src/main/java/de/tum/www1/orion/ui/util/ConfirmPasswordSaveDialog.kt
@@ -10,7 +10,7 @@ class ConfirmPasswordSaveDialog(project: Project?) : DialogWrapper(project) {
private lateinit var confirmationPanel: JPanel
init {
- title = "Import Credentials Into IntelliJ"
+ title = "Import Credentials Into your IDE"
init()
isOKActionEnabled = true
}
@@ -19,7 +19,7 @@ class ConfirmPasswordSaveDialog(project: Project?) : DialogWrapper(project) {
confirmationPanel = panel {
row {
label(
- "Do you want to save your Artemis credentials in IntelliJ?\n" +
+ "Do you want to save your Artemis credentials in your IDE?\n" +
"This makes importing and submitting exercises a lot easier!"
)
}
@@ -27,5 +27,4 @@ class ConfirmPasswordSaveDialog(project: Project?) : DialogWrapper(project) {
return confirmationPanel
}
-
}
diff --git a/src/main/java/de/tum/www1/orion/ui/util/NotificationManager.kt b/src/main/java/de/tum/www1/orion/ui/util/NotificationManager.kt
index dc731b8..95150a6 100644
--- a/src/main/java/de/tum/www1/orion/ui/util/NotificationManager.kt
+++ b/src/main/java/de/tum/www1/orion/ui/util/NotificationManager.kt
@@ -11,7 +11,7 @@ import com.intellij.openapi.project.Project
* Inform the user about something using the notification balloon (bottom right corner). It is logged so the user can
* open it later
*/
-@Service
+@Service(Service.Level.PROJECT)
class NotificationManager(val project: Project) {
/**
* Send a notification of type Orion Errors
diff --git a/src/main/java/de/tum/www1/orion/util/OrionAssessmentUtils.kt b/src/main/java/de/tum/www1/orion/util/OrionAssessmentUtils.kt
index 87f997d..ab00371 100644
--- a/src/main/java/de/tum/www1/orion/util/OrionAssessmentUtils.kt
+++ b/src/main/java/de/tum/www1/orion/util/OrionAssessmentUtils.kt
@@ -109,27 +109,61 @@ object OrionAssessmentUtils {
file: VirtualFile
) {
val filePath = file.fileSystem.getNioPath(file) ?: return
- when {
- filePath.startsWith(getTemplateOf(project)) -> manager.getEditors(file).forEach {
- (it as? TextEditor)?.editor?.document?.setReadOnly(true)
- }
- filePath.startsWith(getStudentSubmissionOf(project)) -> {
- manager.closeFile(file)
- val relativePath =
- getRelativePathForStudentSubmission(project, filePath)
- val assignmentFile =
- VirtualFileManager.getInstance().refreshAndFindFileByNioPath(getAssignmentOf(project).resolve(relativePath)) ?: return Unit.also {
- project.notify(translate("orion.error.file.noAssignmentEquivalent"))
- }
- manager.openFile(assignmentFile, true)
- }
- filePath.startsWith(getAssignmentOf(project)) -> manager.getEditors(file).forEach {
- (it as? TextEditor)?.editor?.headerComponent =
- createHeader(translate("orion.exercise.editMode").uppercase())
+ if (manager.getEditors(file).size > 1) {
+ when {
+ filePath.startsWith(getTemplateOf(project)) -> manager.getEditors(file).forEach {
+ (it as? TextEditor)?.editor?.document?.setReadOnly(true)
+ }
+
+ filePath.startsWith(getStudentSubmissionOf(project)) -> {
+ manager.closeFile(file)
+ val relativePath =
+ getRelativePathForStudentSubmission(project, filePath)
+ val assignmentFile =
+ VirtualFileManager.getInstance()
+ .refreshAndFindFileByNioPath(getAssignmentOf(project).resolve(relativePath))
+ ?: return Unit.also {
+ project.notify(translate("orion.error.file.noAssignmentEquivalent"))
+ }
+ manager.openFile(assignmentFile, true)
+ }
+
+ filePath.startsWith(getAssignmentOf(project)) -> manager.getEditors(file).forEach {
+ (it as? TextEditor)?.editor?.headerComponent =
+ createHeader(translate("orion.exercise.editMode").uppercase())
+ }
+
}
}
}
+ /**
+ * Configures the review editor if the exercise is ready for review
+ * @param project to configure the editors for
+ */
+ fun configureEditorsForReview(project: Project) {
+ project.messageBus.connect()
+ .subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {
+ override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
+ configureEditorForReview(source, file)
+ }
+ })
+ runInEdt {
+ val manager = FileEditorManager.getInstance(project)
+ manager.openFiles.forEach { configureEditorForReview(manager, it) }
+ }
+ }
+
+ private fun configureEditorForReview(
+ manager: FileEditorManager,
+ file: VirtualFile
+ ) {
+ manager.getEditors(file).forEach {
+ (it as? TextEditor)?.editor?.headerComponent =
+ createHeader(translate("orion.exercise.editMode").uppercase())
+ }
+ }
+
/**
* Wraps the given String into a JLabel using a bold font with 1.5 times the default size
*/
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 0487af8..feecb4b 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -55,6 +55,7 @@
+
diff --git a/src/main/resources/i18n/OrionBundle.properties b/src/main/resources/i18n/OrionBundle.properties
index a82c154..07fc656 100644
--- a/src/main/resources/i18n/OrionBundle.properties
+++ b/src/main/resources/i18n/OrionBundle.properties
@@ -46,6 +46,11 @@ orion.exercise.assessment.sgi.delete=Do you want to remove the link to the asses
orion.exercise.assessment.sgi.tooltip=This feedback is associated with an assessment instruction. You can provide additional feedback for this submission element. The student will see the combined feedback during the review.
orion.exercise.assessment.feedback=Feedback
orion.exercise.assessment.score=Points
+# Feedback related
+orion.exercise.feedback=Feedback
+orion.exercise.noFeedback=No feedback available!
+orion.exercise.feedbackMode=Feedback Mode
+orion.exercise.feedbackModeLoading=Feedback Mode loading. Open Orion if this does not happen automatically.\
orion.warning.auxiliaryRepositories=Auxiliary Repositories detected. Orion did download the repositories, but configuration of them is not yet supported.
orion.warning.assessment.submissionId=Id of the opened submission and the loaded Artemis page do not match, loading of feedback aborted. Make sure to open the correct submission in Orion.
diff --git a/src/main/resources/i18n/OrionBundle_de.properties b/src/main/resources/i18n/OrionBundle_de.properties
index 57fe1e6..0bdf4d7 100644
--- a/src/main/resources/i18n/OrionBundle_de.properties
+++ b/src/main/resources/i18n/OrionBundle_de.properties
@@ -45,7 +45,13 @@ orion.exercise.assessment.sgi.link=Bewertungsanweisung: %s
orion.exercise.assessment.sgi.delete=Möchtest du den Link zur Bewertungsanweisung entfernen?
orion.exercise.assessment.sgi.tooltip=Dieses Feedback ist mit einer Bewertungsanweisung verbunden. Du kannst zusätzliches Feedback für dieses Abgabeelement geben. Der/Die Studierende sieht das kombinierte Feedback während der Überprüfung.
orion.exercise.assessment.feedback=Feedback
-orion.exercise.assessment.score=Punktzahl
+# short form Punkte because Punktzahl is too long
+orion.exercise.assessment.score=Punkte
+# Feedback related
+orion.exercise.feedback=Feedback
+orion.exercise.noFeedback=Kein Feedback vorhanden!
+orion.exercise.feedbackMode=Feedbackmodus
+orion.exercise.feedbackModeLoading=Feedbackmodus lädt. Öffne Orion wenn dies nicht automatisch passiert.
orion.warning.auxiliaryRepositories=Auxiliary Repositories erkannt. Orion hat die Repositories heruntergeladen, aber ihre Konfiguration wird noch nicht unterstützt.
orion.warning.assessment.submissionId=Id der geöffneten Abgabe stimmt nicht mit der in Artemis geladenen Seite überein. Laden des Feedbacks wurde abgebrochen. Stelle sicher, dass die korrekte Abgabe in Orion geladen ist.