From 2ba00e72014c88389a62aa5454cc40ee51496e8d Mon Sep 17 00:00:00 2001 From: Marcel Salvenmoser <18032233+malthee@users.noreply.github.com> Date: Mon, 20 Feb 2023 19:50:02 +0100 Subject: [PATCH 1/7] Remove SaveFeedbackCommand from MailCommand For multiple tutors its better to save the feedback selection right after its been chosen so its easier to synchronize. --- .../tutorbot/commands/MailCommand.kt | 7 ------- .../tutorbot/commands/MailCommandTest.kt | 19 +------------------ 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommand.kt index 5bf4cff..2877bfd 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommand.kt @@ -16,7 +16,6 @@ import javax.mail.AuthenticationFailedException ) class MailCommand @Inject constructor( private val mailClient: MailClient, - private val saveFeedbackCommand: SaveFeedbackCommand, private val credentialStore: CredentialStore, private val configHandler: ConfigHandler ) : BaseCommand() { @@ -72,12 +71,6 @@ class MailCommand @Inject constructor( } } } - - // Prompt for feedback saving also - if (promptBooleanInput("Do you also want to save the feedbacks count to CSV?")) - { - saveFeedbackCommand.execute() - } } private fun String.promptTemplateArguments(name: String): String { diff --git a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommandTest.kt b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommandTest.kt index 37d7d50..e63fde0 100644 --- a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommandTest.kt +++ b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommandTest.kt @@ -33,7 +33,7 @@ class MailCommandTest : CommandLineTest() { every { getReviewsSubDir() } returns "reviews" } - private val mailCommand = MailCommand(mailClient, saveFeedbackCommand, credentialStore, configHandler) + private val mailCommand = MailCommand(mailClient, credentialStore, configHandler) @get:Rule val fileSystem = FileSystemRule() @@ -120,23 +120,6 @@ class MailCommandTest : CommandLineTest() { assertThrows { mailCommand.execute() } } - @Test - fun `Save feedback is executed when confirmed`() { - systemIn.provideLines("Subject", "Body", "Y", "Y") - mailCommand.execute() - - verify { saveFeedbackCommand.execute() } - confirmVerified(saveFeedbackCommand) - } - - @Test - fun `Save feedback is not executed when not confirmed`() { - systemIn.provideLines("Subject", "Body", "Y", "N") - mailCommand.execute() - - confirmVerified(saveFeedbackCommand) - } - private fun verifyTestMail(){ verify { mailClient.sendMail(any()) } } From 1ef958a3f662b798135a25b47ab7a77f43cde69f Mon Sep 17 00:00:00 2001 From: Marcel Salvenmoser <18032233+malthee@users.noreply.github.com> Date: Mon, 20 Feb 2023 22:38:50 +0100 Subject: [PATCH 2/7] Refactored domain classes and logic out of ChooseFeedbackCommand --- .../commands/ChooseFeedbackCommand.kt | 125 ++++-------------- .../tutorbot/commands/ReviewsCommand.kt | 2 +- .../tutorbot/commands/SaveFeedbackCommand.kt | 14 +- .../components/FeedbackChooseLogic.kt | 107 +++++++++++++++ ...eedbackHelper.kt => FeedbackFileHelper.kt} | 19 +-- .../tutorbot/domain/FeedbackCount.kt | 10 ++ .../fhooe/hagenberg/tutorbot/domain/Review.kt | 10 ++ .../commands/ChooseFeedbackCommandTest.kt | 51 ++++--- .../commands/SaveFeedbackCommandTest.kt | 16 +-- ...elperTest.kt => FeedbackFileHelperTest.kt} | 36 ++--- 10 files changed, 221 insertions(+), 169 deletions(-) create mode 100644 src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackChooseLogic.kt rename src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/{FeedbackHelper.kt => FeedbackFileHelper.kt} (83%) create mode 100644 src/main/kotlin/at/fhooe/hagenberg/tutorbot/domain/FeedbackCount.kt create mode 100644 src/main/kotlin/at/fhooe/hagenberg/tutorbot/domain/Review.kt rename src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/{FeedbackHelperTest.kt => FeedbackFileHelperTest.kt} (74%) diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommand.kt index 6c9bfe7..6067bd6 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommand.kt @@ -1,19 +1,18 @@ package at.fhooe.hagenberg.tutorbot.commands import at.fhooe.hagenberg.tutorbot.components.ConfigHandler -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper.FeedbackCount -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper.Review +import at.fhooe.hagenberg.tutorbot.components.FeedbackChooseLogic +import at.fhooe.hagenberg.tutorbot.components.FeedbackFileHelper +import at.fhooe.hagenberg.tutorbot.domain.FeedbackCount +import at.fhooe.hagenberg.tutorbot.domain.Review import at.fhooe.hagenberg.tutorbot.util.exitWithError import at.fhooe.hagenberg.tutorbot.util.printlnCyan import at.fhooe.hagenberg.tutorbot.util.printlnGreen import at.fhooe.hagenberg.tutorbot.util.promptBooleanInput import picocli.CommandLine.Command -import java.io.File import java.io.FileNotFoundException import java.nio.file.Path import javax.inject.Inject -import kotlin.random.Random @Command( name = "choose-feedback", @@ -21,106 +20,16 @@ import kotlin.random.Random ) class ChooseFeedbackCommand @Inject constructor( private val configHandler: ConfigHandler, - private val feedbackHelper: FeedbackHelper, - private val random: Random + private val feedbackFileHelper: FeedbackFileHelper, + private val feedbackChooseLogic: FeedbackChooseLogic ) : BaseCommand() { - /** - * Adds the review to the chosenReviews if both participating students are not in the chosenReviews already. - * @return true if added - */ - private fun addIfAllowed(review: Review, chosenReviews: MutableCollection): Boolean = - if (chosenReviews.none { r -> - r.subStudentNr == review.revStudentNr || r.subStudentNr == review.subStudentNr - || r.revStudentNr == review.revStudentNr || r.revStudentNr == review.subStudentNr - }) chosenReviews.add(review) else false - - /** - * Tries to pop a Review from the reviews, which either has the student as submitter or reviewer. - * @return Review if present in reviews - */ - private fun tryPopReviewForStudent( - student: String, - reviews: MutableCollection, - asSubmitter: Boolean - ): Review? = - when (asSubmitter) { - true -> reviews.firstOrNull { r -> r.subStudentNr == student } - false -> reviews.firstOrNull { r -> r.revStudentNr == student } - }?.also { reviews.remove(it) } - - /** - * Chooses reviews to be selected for feedback. Students who have gotten the least feedbacks on submissions and reviews are preferred. - * Random reviews regardless of their received feedback amount may also be chosen if defined (useful to avoid predictability). - */ - private fun pickReviewsToFeedback( - reviews: MutableSet, - feedbackCsv: File, - feedbackCount: Int, - randomCount: Int - ): Set { - val feedbackCountMap = try { - feedbackHelper.readFeedbackCountFromCsv(feedbackCsv) - } catch (e: FileNotFoundException) { - mapOf() // If file does not exist, just return empty map - } catch (e: Exception) { - exitWithError(e.message ?: "Parsing feedback CSV failed.") - } - val chosenReviews = mutableSetOf() - val canStillPickReviews = { reviews.isNotEmpty() && chosenReviews.size < feedbackCount } - - // 1) Chose random reviews first - while (canStillPickReviews() && chosenReviews.size < randomCount) { - val review = reviews.random(random).also { reviews.remove(it) } - addIfAllowed(review, chosenReviews) - } - - if (!canStillPickReviews()) return chosenReviews - - // 2) Choose students who have not gotten any feedback, does not matter if feedback on submission or review - val studentsWithNoFeedback = reviews - .flatMap { r -> listOf(r.subStudentNr, r.revStudentNr) } - .distinct() - .filter { s -> s !in feedbackCountMap } - .shuffled(random) - .toMutableList() - while (canStillPickReviews() && studentsWithNoFeedback.isNotEmpty()) { - val student = studentsWithNoFeedback.first() - studentsWithNoFeedback.remove(student) - tryPopReviewForStudent(student, reviews, true)?.also { - addIfAllowed(it, chosenReviews) - } - } - - if (!canStillPickReviews()) return chosenReviews - - // 3) Choose students by ordering amount of feedback received - val feedbackOrdering = feedbackCountMap - .toList() - .filter { pair -> pair.first in reviews.flatMap { r -> listOf(r.subStudentNr, r.revStudentNr) } } - .shuffled(random) - .sortedBy { pair -> pair.second } - .toMutableList() - while (canStillPickReviews() && feedbackOrdering.isNotEmpty()) { - val picked = feedbackOrdering.first() - feedbackOrdering.remove(picked) - - // Try to keep amount of feedbacks on submissions and reviews the same - val shouldPickSubmission = picked.second.submission <= picked.second.review - tryPopReviewForStudent(picked.first, reviews, shouldPickSubmission)?.also { - addIfAllowed(it, chosenReviews) - } - } - - return chosenReviews - } - override fun execute() { // Current ex. reviews path val baseDir = configHandler.getBaseDir() val exerciseSubDir = configHandler.getExerciseSubDir() val reviewsDir = configHandler.getReviewsSubDir() val sourceDirectory = Path.of(baseDir, exerciseSubDir, reviewsDir) - val reviews = feedbackHelper.readAllReviewsFromDir(sourceDirectory.toFile()) + val reviews = feedbackFileHelper.readAllReviewsFromDir(sourceDirectory.toFile()) if (reviews.isEmpty()) exitWithError("Reviews folder does not contain any valid files!") // Get CSV with count of previous feedbacks @@ -134,8 +43,15 @@ class ChooseFeedbackCommand @Inject constructor( if (randomCount < 0 || randomCount > feedbackCount) exitWithError("Random feedback count must be >= 0 and <= feedback count.") // Pick reviews + val feedbackCountMap = try { + feedbackFileHelper.readFeedbackCountFromCsv(feedbackCsv) + } catch (e: FileNotFoundException) { + mapOf() // If file does not exist, just return empty map + } catch (e: Exception) { + exitWithError(e.message ?: "Parsing feedback CSV failed.") + } val reviewsToFeedback = - pickReviewsToFeedback(reviews.toMutableSet(), feedbackCsv, feedbackCount, randomCount) + feedbackChooseLogic.pickReviewsToFeedback(feedbackCountMap, reviews, feedbackCount, randomCount) val reviewsToMove = reviews - reviewsToFeedback if (reviewsToFeedback.size != feedbackCount) @@ -143,6 +59,15 @@ class ChooseFeedbackCommand @Inject constructor( else printlnGreen("Picked $feedbackCount reviews.") + moveReviews(sourceDirectory, reviewsToMove) + + printlnGreen("Finished selecting reviews to feedback.") + } + + private fun moveReviews( + sourceDirectory: Path, + reviewsToMove: Set + ) { val targetDirectory = sourceDirectory.resolve(NOT_SELECTED_DIR) if (!targetDirectory.toFile().mkdir()) { if (!promptBooleanInput("Target location $targetDirectory already exists, should its contents be overwritten?")) { @@ -161,8 +86,6 @@ class ChooseFeedbackCommand @Inject constructor( exitWithError(ex.message ?: "Moving ${rev.fileName} failed.") } } - - printlnGreen("Finished selecting reviews to feedback.") } companion object { diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ReviewsCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ReviewsCommand.kt index 8335202..5edc24b 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ReviewsCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ReviewsCommand.kt @@ -14,7 +14,7 @@ import javax.inject.Inject @Command( name = "reviews", - description = ["Downloads all reviews for a certain exercise"] + description = ["Downloads all reviews for a certain exercise optionally also downloading the submissions."] ) class ReviewsCommand @Inject constructor( private val moodleClient: MoodleClient, diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommand.kt index a33d5a7..bc44730 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommand.kt @@ -1,8 +1,8 @@ package at.fhooe.hagenberg.tutorbot.commands import at.fhooe.hagenberg.tutorbot.components.ConfigHandler -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper.FeedbackCount +import at.fhooe.hagenberg.tutorbot.components.FeedbackFileHelper +import at.fhooe.hagenberg.tutorbot.domain.FeedbackCount import at.fhooe.hagenberg.tutorbot.util.exitWithError import at.fhooe.hagenberg.tutorbot.util.printlnGreen import picocli.CommandLine.Command @@ -12,11 +12,11 @@ import javax.inject.Inject @Command( name = "save-feedback", - description = ["Counts the feedbacks of an exercise and appends the data to the feedback CSV file"] + description = ["Should only be run if the feedback count was not saved when choosing the feedbacks. Counts the feedbacks in an exercise folder and updates the feedback CSV file."] ) class SaveFeedbackCommand @Inject constructor( private val configHandler: ConfigHandler, - private val feedbackHelper: FeedbackHelper + private val feedbackFileHelper: FeedbackFileHelper ) : BaseCommand() { override fun execute() { // Current ex. reviews path @@ -24,14 +24,14 @@ class SaveFeedbackCommand @Inject constructor( val exerciseSubDir = configHandler.getExerciseSubDir() val reviewsDir = configHandler.getReviewsSubDir() val sourceDirectory = Path.of(baseDir, exerciseSubDir, reviewsDir) - val feedbackCount = feedbackHelper.readFeedbackCountFromReviews(sourceDirectory.toFile()) + val feedbackCount = feedbackFileHelper.readFeedbackCountFromReviews(sourceDirectory.toFile()) if (feedbackCount.isEmpty()) exitWithError("Reviews folder does not contain any valid files!") // Get CSV with count of previous feedbacks val feedbackDirPath = configHandler.getFeedbackCsv() val feedbackCsv = Path.of(feedbackDirPath).toFile() val existingFeedbackCount = try { - feedbackHelper.readFeedbackCountFromCsv(feedbackCsv) + feedbackFileHelper.readFeedbackCountFromCsv(feedbackCsv) } catch (e: FileNotFoundException) { mapOf() // If file does not exist, just return empty map } catch (e: Exception) { @@ -50,7 +50,7 @@ class SaveFeedbackCommand @Inject constructor( } try { - feedbackHelper.writeFeedbackCountToCsv(feedbackCsv, mergedCount) + feedbackFileHelper.writeFeedbackCountToCsv(feedbackCsv, mergedCount) printlnGreen("Successfully wrote new feedback count to ${feedbackCsv.absolutePath}") } catch (e: Exception){ exitWithError(e.message ?: "Could not write to ${feedbackCsv.absolutePath}") diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackChooseLogic.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackChooseLogic.kt new file mode 100644 index 0000000..7c49938 --- /dev/null +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackChooseLogic.kt @@ -0,0 +1,107 @@ +package at.fhooe.hagenberg.tutorbot.components + +import at.fhooe.hagenberg.tutorbot.domain.FeedbackCount +import at.fhooe.hagenberg.tutorbot.domain.Review +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.random.Random + +/** + * Logic behind choosing feedback from a list of reviews. + * Criteria: The students with the least amount of feedback should be chosen. + * But no student should be chosen twice for the same exercise (as reviewer and submitter). + */ +@Singleton +class FeedbackChooseLogic @Inject constructor(private val random: Random) { + /** + * Adds the review to the chosenReviews if both participating students are not in the chosenReviews already. + * @return true if added + */ + private fun addIfAllowed( + review: Review, + chosenReviews: MutableCollection + ): Boolean = + if (chosenReviews.none { r -> + r.subStudentNr == review.revStudentNr || r.subStudentNr == review.subStudentNr + || r.revStudentNr == review.revStudentNr || r.revStudentNr == review.subStudentNr + }) chosenReviews.add(review) else false + + /** + * Tries to pop a Review from the reviews, which either has the student as submitter or reviewer. + * @return Review if present in reviews + */ + private fun tryPopReviewForStudent( + student: String, + reviews: MutableCollection, + asSubmitter: Boolean + ): Review? = + when (asSubmitter) { + true -> reviews.firstOrNull { r -> r.subStudentNr == student } + false -> reviews.firstOrNull { r -> r.revStudentNr == student } + }?.also { reviews.remove(it) } + + /** + * Chooses reviews to be selected for feedback. Students who have gotten the least feedbacks on submissions and reviews are preferred. + * Random reviews regardless of their received feedback amount may also be chosen if defined (useful to avoid predictability). + * @param feedbackCountMap number of feedbacks a student has received on submissions and reviews + * @param reviewsLeft reviews to choose from + * @param feedbackCount amount of reviews to choose + * @param randomCount amount of random reviews to choose with no regard to feedback count + * @return List of chosen reviews + */ + fun pickReviewsToFeedback( + feedbackCountMap: Map, + reviews: Set, + feedbackCount: Int, + randomCount: Int + ): Set { + val chosenReviews = mutableSetOf() + val reviewsLeft = reviews.toMutableSet() + val canStillPickReviews = { reviewsLeft.isNotEmpty() && chosenReviews.size < feedbackCount } + + // 1) Chose random reviews first + while (canStillPickReviews() && chosenReviews.size < randomCount) { + val review = reviewsLeft.random(random).also { reviewsLeft.remove(it) } + addIfAllowed(review, chosenReviews) + } + + if (!canStillPickReviews()) return chosenReviews + + // 2) Choose students who have not gotten any feedback, does not matter if feedback on submission or review + val studentsWithNoFeedback = reviewsLeft + .flatMap { r -> listOf(r.subStudentNr, r.revStudentNr) } + .distinct() + .filter { s -> s !in feedbackCountMap } + .shuffled(random) + .toMutableList() + while (canStillPickReviews() && studentsWithNoFeedback.isNotEmpty()) { + val student = studentsWithNoFeedback.first() + studentsWithNoFeedback.remove(student) + tryPopReviewForStudent(student, reviewsLeft, true)?.also { + addIfAllowed(it, chosenReviews) + } + } + + if (!canStillPickReviews()) return chosenReviews + + // 3) Choose students by ordering amount of feedback received + val feedbackOrdering = feedbackCountMap + .toList() + .filter { pair -> pair.first in reviewsLeft.flatMap { r -> listOf(r.subStudentNr, r.revStudentNr) } } + .shuffled(random) + .sortedBy { pair -> pair.second } + .toMutableList() + while (canStillPickReviews() && feedbackOrdering.isNotEmpty()) { + val picked = feedbackOrdering.first() + feedbackOrdering.remove(picked) + + // Try to keep amount of feedbacks on submissions and reviews the same + val shouldPickSubmission = picked.second.submission <= picked.second.review + tryPopReviewForStudent(picked.first, reviewsLeft, shouldPickSubmission)?.also { + addIfAllowed(it, chosenReviews) + } + } + + return chosenReviews + } +} \ No newline at end of file diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackHelper.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackFileHelper.kt similarity index 83% rename from src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackHelper.kt rename to src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackFileHelper.kt index bf38b39..8e5e951 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackHelper.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackFileHelper.kt @@ -1,22 +1,13 @@ package at.fhooe.hagenberg.tutorbot.components +import at.fhooe.hagenberg.tutorbot.domain.FeedbackCount +import at.fhooe.hagenberg.tutorbot.domain.Review import java.io.File import javax.inject.Inject +import javax.inject.Singleton -class FeedbackHelper @Inject constructor() { - // Tracks amount of feedbacks a student has received on submissions and reviews. Ordered by submission first then review. - data class FeedbackCount(val submission: Int, val review: Int) : Comparable { - override fun compareTo(other: FeedbackCount): Int { - return compareValuesBy(this, other, { it.submission }, { it.review }) - } - } - - // Represents a Review, which has a student who submitted the code and one who reviewed it. - data class Review( - val fileName: String, - val subStudentNr: String, - val revStudentNr: String - ) +@Singleton +class FeedbackFileHelper @Inject constructor() { /** * Reads all review files from a directory matching with the STUDENT_NR_PATTERN ignoring other files. diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/domain/FeedbackCount.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/domain/FeedbackCount.kt new file mode 100644 index 0000000..06f9702 --- /dev/null +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/domain/FeedbackCount.kt @@ -0,0 +1,10 @@ +package at.fhooe.hagenberg.tutorbot.domain + +/** + * Tracks amount of feedbacks a student has received on submissions and reviews. Ordered by submission first then review. + */ +data class FeedbackCount(val submission: Int, val review: Int) : Comparable { + override fun compareTo(other: FeedbackCount): Int { + return compareValuesBy(this, other, { it.submission }, { it.review }) + } +} \ No newline at end of file diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/domain/Review.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/domain/Review.kt new file mode 100644 index 0000000..bbd694e --- /dev/null +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/domain/Review.kt @@ -0,0 +1,10 @@ +package at.fhooe.hagenberg.tutorbot.domain + +/** + * Represents a Review, which has a student who submitted the code and one who reviewed it. + */ +data class Review( + val fileName: String, + val subStudentNr: String, + val revStudentNr: String +) \ No newline at end of file diff --git a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommandTest.kt b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommandTest.kt index cf0bf56..2deeab3 100644 --- a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommandTest.kt +++ b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommandTest.kt @@ -1,9 +1,10 @@ package at.fhooe.hagenberg.tutorbot.commands import at.fhooe.hagenberg.tutorbot.components.ConfigHandler -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper.FeedbackCount -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper.Review +import at.fhooe.hagenberg.tutorbot.components.FeedbackChooseLogic +import at.fhooe.hagenberg.tutorbot.components.FeedbackFileHelper +import at.fhooe.hagenberg.tutorbot.domain.FeedbackCount +import at.fhooe.hagenberg.tutorbot.domain.Review import at.fhooe.hagenberg.tutorbot.testutil.CommandLineTest import at.fhooe.hagenberg.tutorbot.testutil.assertThrows import at.fhooe.hagenberg.tutorbot.testutil.getResource @@ -25,7 +26,7 @@ class ChooseFeedbackCommandTest : CommandLineTest() { private val reviewSubDir = "reviews" private val exerciseSubDir = "ue01" - private val feedbackHelper = mockk () + private val feedbackFileHelper = mockk() private val configHandler = mockk { every { getExerciseSubDir() } returns exerciseSubDir every { getReviewsSubDir() } returns reviewSubDir @@ -33,8 +34,9 @@ class ChooseFeedbackCommandTest : CommandLineTest() { private val random = mockk { every { nextInt(any()) } returns 0 } + private val feedbackChooseLogic = FeedbackChooseLogic(random) // The logic is being tested as part of the command - private val chooseFeedbackCmd = ChooseFeedbackCommand(configHandler, feedbackHelper, random) + private val chooseFeedbackCmd = ChooseFeedbackCommand(configHandler, feedbackFileHelper, feedbackChooseLogic) @get:Rule val fileSystem = FileSystemRule() @@ -59,7 +61,7 @@ class ChooseFeedbackCommandTest : CommandLineTest() { } private fun setupTestFiles() { - every { feedbackHelper.readAllReviewsFromDir(any()) } returns setOf( + every { feedbackFileHelper.readAllReviewsFromDir(any()) } returns setOf( Review("S3-S4_S3-S4.pdf", "s3", "s4"), Review("S4-S2210101010_S4_TestName.pdf", "s4", "s2210101010") ) @@ -72,7 +74,7 @@ class ChooseFeedbackCommandTest : CommandLineTest() { @Test fun `Exit when review dir is empty`() { - every { feedbackHelper.readAllReviewsFromDir(any()) } returns setOf() + every { feedbackFileHelper.readAllReviewsFromDir(any()) } returns setOf() assertThrows { chooseFeedbackCmd.execute() @@ -81,7 +83,7 @@ class ChooseFeedbackCommandTest : CommandLineTest() { @Test fun `Exit when feedbackCount is smaller than 1`() { - every { feedbackHelper.readAllReviewsFromDir(any()) } returns + every { feedbackFileHelper.readAllReviewsFromDir(any()) } returns setOf(Review("S1-S2_S1-S2.pdf", "s1", "s2")) every { configHandler.getFeedbackAmount() } returns 0 @@ -92,7 +94,7 @@ class ChooseFeedbackCommandTest : CommandLineTest() { @Test fun `Exit when feedbackRandomCount is greater than feedbackCount`() { - every { feedbackHelper.readAllReviewsFromDir(any()) } returns + every { feedbackFileHelper.readAllReviewsFromDir(any()) } returns setOf(Review("S1-S2_S1-S2.pdf", "s1", "s2")) every { configHandler.getFeedbackAmount() } returns 4 every { configHandler.getFeedbackRandomAmount() } returns 5 @@ -104,7 +106,7 @@ class ChooseFeedbackCommandTest : CommandLineTest() { @Test fun `Exit when feedbackRandomCount smaller than 0`() { - every { feedbackHelper.readAllReviewsFromDir(any()) } returns + every { feedbackFileHelper.readAllReviewsFromDir(any()) } returns setOf(Review("S1-S2_S1-S2.pdf", "s1", "s2")) every { configHandler.getFeedbackAmount() } returns 4 every { configHandler.getFeedbackRandomAmount() } returns -1 @@ -116,12 +118,12 @@ class ChooseFeedbackCommandTest : CommandLineTest() { @Test fun `With no feedback csv, the only review is chosen`() { - every { feedbackHelper.readAllReviewsFromDir(any()) } returns + every { feedbackFileHelper.readAllReviewsFromDir(any()) } returns setOf(Review("S1-S2_S1-S2.pdf", "s1", "s2")) getResource("pdfs/S1-S2_S1-S2.pdf").copyTo( File(reviewLocation, "S1-S2_S1-S2.pdf") ) - every { feedbackHelper.readFeedbackCountFromCsv(any()) } throws FileNotFoundException() + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } throws FileNotFoundException() every { configHandler.getFeedbackAmount() } returns 1 every { configHandler.getFeedbackRandomAmount() } returns 0 @@ -132,9 +134,9 @@ class ChooseFeedbackCommandTest : CommandLineTest() { @Test fun `Exits when read feedback count throws IOException`() { - every { feedbackHelper.readAllReviewsFromDir(any()) } returns + every { feedbackFileHelper.readAllReviewsFromDir(any()) } returns setOf(Review("S1-S2_S1-S2.pdf", "s1", "s2")) - every { feedbackHelper.readFeedbackCountFromCsv(any()) } throws IOException() + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } throws IOException() every { configHandler.getFeedbackAmount() } returns 1 every { configHandler.getFeedbackRandomAmount() } returns 0 @@ -145,10 +147,10 @@ class ChooseFeedbackCommandTest : CommandLineTest() { @Test fun `Same student cannot be selected for two reviews, prefer student with no feedbacks`() { - setupTestFiles() + setupTestFiles() // S4 has feedback, S3 does not, prefer S3 - every { feedbackHelper.readFeedbackCountFromCsv(any()) } returns mapOf("s4" to FeedbackHelper.FeedbackCount(1, 0)) + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } returns mapOf("s4" to FeedbackCount(1, 0)) every { configHandler.getFeedbackAmount() } returns 2 every { configHandler.getFeedbackRandomAmount() } returns 0 @@ -162,7 +164,10 @@ class ChooseFeedbackCommandTest : CommandLineTest() { fun `Same student cannot be selected for two reviews, prefer student with fewer feedbacks`() { // S4 has more reviews, so S3 is picked setupTestFiles() - every { feedbackHelper.readFeedbackCountFromCsv(any()) } returns mapOf("s3" to FeedbackHelper.FeedbackCount(1, 1), "s4" to FeedbackHelper.FeedbackCount(2, 1)) + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } returns mapOf( + "s3" to FeedbackCount(1, 1), + "s4" to FeedbackCount(2, 1) + ) every { configHandler.getFeedbackAmount() } returns 2 every { configHandler.getFeedbackRandomAmount() } returns 0 @@ -176,7 +181,10 @@ class ChooseFeedbackCommandTest : CommandLineTest() { fun `Random student is selected with same feedback count`() { // Both have same amount of reviews, S4 is picked randomly setupTestFiles() - every { feedbackHelper.readFeedbackCountFromCsv(any()) } returns mapOf("s3" to FeedbackHelper.FeedbackCount(1, 1), "s4" to FeedbackHelper.FeedbackCount(1, 1)) + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } returns mapOf( + "s3" to FeedbackCount(1, 1), + "s4" to FeedbackCount(1, 1) + ) every { random.nextInt(any()) } returns 1 // Take second item which is submission of s4 every { configHandler.getFeedbackAmount() } returns 1 every { configHandler.getFeedbackRandomAmount() } returns 0 @@ -193,7 +201,10 @@ class ChooseFeedbackCommandTest : CommandLineTest() { fun `Random student is selected regardless of feedback count`() { // S4 has more reviews, but is picked because of randomCount setupTestFiles() - every { feedbackHelper.readFeedbackCountFromCsv(any()) } returns mapOf("s3" to FeedbackHelper.FeedbackCount(1, 1), "s4" to FeedbackHelper.FeedbackCount(2, 1)) + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } returns mapOf( + "s3" to FeedbackCount(1, 1), + "s4" to FeedbackCount(2, 1) + ) every { random.nextInt(any()) } returns 1 // Take second item which is submission of s4 every { configHandler.getFeedbackAmount() } returns 1 every { configHandler.getFeedbackRandomAmount() } returns 1 @@ -210,7 +221,7 @@ class ChooseFeedbackCommandTest : CommandLineTest() { fun `Student with fewer reviews than submissions gets selected as reviewer`() { // S4 has not gotten any feedbacks on reviews, choose where S4 is reviewer setupTestFiles() - every { feedbackHelper.readFeedbackCountFromCsv(any()) } returns mapOf( + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } returns mapOf( "s3" to FeedbackCount(1, 1), "s4" to FeedbackCount(1, 0) ) diff --git a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommandTest.kt b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommandTest.kt index b0098be..f798658 100644 --- a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommandTest.kt +++ b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommandTest.kt @@ -1,8 +1,8 @@ package at.fhooe.hagenberg.tutorbot.commands import at.fhooe.hagenberg.tutorbot.components.ConfigHandler -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper.FeedbackCount +import at.fhooe.hagenberg.tutorbot.components.FeedbackFileHelper +import at.fhooe.hagenberg.tutorbot.domain.FeedbackCount import at.fhooe.hagenberg.tutorbot.testutil.assertThrows import at.fhooe.hagenberg.tutorbot.testutil.rules.FileSystemRule import at.fhooe.hagenberg.tutorbot.util.ProgramExitError @@ -29,12 +29,12 @@ class SaveFeedbackCommandTest { every { getExerciseSubDir() } returns exerciseSubDir every { getReviewsSubDir() } returns reviewSubDir } - private val feedbackHelper = mockk { + private val feedbackFileHelper = mockk { every { readFeedbackCountFromReviews(any()) } returns reviewsInFolder every { writeFeedbackCountToCsv(any(), capture(feedbackCountWriteCapture)) } returns Unit } - private val saveFeedbackCommand = SaveFeedbackCommand(configHandler, feedbackHelper) + private val saveFeedbackCommand = SaveFeedbackCommand(configHandler, feedbackFileHelper) @get:Rule val fileSystem = FileSystemRule() @@ -46,7 +46,7 @@ class SaveFeedbackCommandTest { @Test fun `When feedback dir empty, exits`() { - every { feedbackHelper.readFeedbackCountFromReviews(any()) } returns mapOf() + every { feedbackFileHelper.readFeedbackCountFromReviews(any()) } returns mapOf() assertThrows { saveFeedbackCommand.execute() @@ -55,7 +55,7 @@ class SaveFeedbackCommandTest { @Test fun `When read csv throws exception, exits`() { - every { feedbackHelper.readFeedbackCountFromCsv(any()) } throws IOException() + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } throws IOException() assertThrows { saveFeedbackCommand.execute() @@ -71,7 +71,7 @@ class SaveFeedbackCommandTest { @Test fun `When write throws exception, exits`() { - every { feedbackHelper.writeFeedbackCountToCsv(any(), capture(feedbackCountWriteCapture)) } throws IOException() + every { feedbackFileHelper.writeFeedbackCountToCsv(any(), capture(feedbackCountWriteCapture)) } throws IOException() assertThrows { saveFeedbackCommand.execute() @@ -84,7 +84,7 @@ class SaveFeedbackCommandTest { "S1" to FeedbackCount(0, 1), "S2" to FeedbackCount(0, 1) ) - every { feedbackHelper.readFeedbackCountFromCsv(any()) } returns reviewsInCsv + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } returns reviewsInCsv val expectedFeedbackCount = mapOf( "S1" to FeedbackCount(1, 1), "S2" to FeedbackCount(0, 2) diff --git a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackHelperTest.kt b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackFileHelperTest.kt similarity index 74% rename from src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackHelperTest.kt rename to src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackFileHelperTest.kt index 9fa126b..82c50cf 100644 --- a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackHelperTest.kt +++ b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/FeedbackFileHelperTest.kt @@ -1,6 +1,6 @@ package at.fhooe.hagenberg.tutorbot.components -import at.fhooe.hagenberg.tutorbot.components.FeedbackHelper.FeedbackCount +import at.fhooe.hagenberg.tutorbot.domain.FeedbackCount import at.fhooe.hagenberg.tutorbot.testutil.CommandLineTest import at.fhooe.hagenberg.tutorbot.testutil.assertThrows import at.fhooe.hagenberg.tutorbot.testutil.getResource @@ -12,7 +12,7 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException -class FeedbackHelperTest : CommandLineTest() { +class FeedbackFileHelperTest : CommandLineTest() { private val lowerCaseStudentNumbers = listOf("s1", "s2", "s3", "s4", "s2210101010") private val studentFeedbackCount = listOf( "s1" to FeedbackCount(2, 0), @@ -30,14 +30,14 @@ class FeedbackHelperTest : CommandLineTest() { private val reviewDir = getResource("pdfs") - private val feedbackHelper = FeedbackHelper() + private val feedbackFileHelper = FeedbackFileHelper() @get:Rule val fileSystem = FileSystemRule() @Test fun `Read reviews ignores invalid files`() { - val res = feedbackHelper.readAllReviewsFromDir(reviewDir) + val res = feedbackFileHelper.readAllReviewsFromDir(reviewDir) val invalidFile1 = "review.pdf" val invalidFile2 = "s1-invalid-file.pdf" @@ -47,21 +47,21 @@ class FeedbackHelperTest : CommandLineTest() { @Test fun `Read reviews returns empty when dir empty`() { val emptyDir = File(fileSystem.directory.absolutePath) - val res = feedbackHelper.readAllReviewsFromDir(emptyDir) + val res = feedbackFileHelper.readAllReviewsFromDir(emptyDir) assert(res.isEmpty()) } @Test fun `Read reviews includes right amount of files`() { - val res = feedbackHelper.readAllReviewsFromDir(reviewDir) + val res = feedbackFileHelper.readAllReviewsFromDir(reviewDir) assert(res.size == 4) } @Test fun `Read reviews gets student numbers lower case`() { - val res = feedbackHelper.readAllReviewsFromDir(reviewDir) + val res = feedbackFileHelper.readAllReviewsFromDir(reviewDir) assert(res.all { r -> r.revStudentNr in lowerCaseStudentNumbers && r.subStudentNr in lowerCaseStudentNumbers }) } @@ -69,21 +69,21 @@ class FeedbackHelperTest : CommandLineTest() { @Test fun `Read feedback returns empty when dir empty`() { val emptyDir = File(fileSystem.directory.absolutePath) - val res = feedbackHelper.readFeedbackCountFromReviews(emptyDir) + val res = feedbackFileHelper.readFeedbackCountFromReviews(emptyDir) assert(res.isEmpty()) } @Test fun `Read feedback has key for every lower case student number`() { - val res = feedbackHelper.readFeedbackCountFromReviews(reviewDir) + val res = feedbackFileHelper.readFeedbackCountFromReviews(reviewDir) assert(res.all { f -> f.key in lowerCaseStudentNumbers }) } @Test fun `Read feedback FeedbackCount has correct amount of submissions and reviews for each student`() { - val res = feedbackHelper.readFeedbackCountFromReviews(reviewDir) + val res = feedbackFileHelper.readFeedbackCountFromReviews(reviewDir) assert(studentFeedbackCount.all { s -> res[s.first] == s.second }) } @@ -93,7 +93,7 @@ class FeedbackHelperTest : CommandLineTest() { val file = fileSystem.directory.resolve("invalid-empty.csv") assertThrows { - feedbackHelper.readFeedbackCountFromCsv(file) + feedbackFileHelper.readFeedbackCountFromCsv(file) } } @@ -102,7 +102,7 @@ class FeedbackHelperTest : CommandLineTest() { val file = fileSystem.directory.resolve("notexists.csv") assertThrows { - feedbackHelper.readFeedbackCountFromCsv(file) + feedbackFileHelper.readFeedbackCountFromCsv(file) } } @@ -111,7 +111,7 @@ class FeedbackHelperTest : CommandLineTest() { val file = getResource("csv/invalid-noheader.csv") assertThrows { - feedbackHelper.readFeedbackCountFromCsv(file) + feedbackFileHelper.readFeedbackCountFromCsv(file) } } @@ -120,14 +120,14 @@ class FeedbackHelperTest : CommandLineTest() { val file = getResource("csv/invalid-count.csv") assertThrows { - feedbackHelper.readFeedbackCountFromCsv(file) + feedbackFileHelper.readFeedbackCountFromCsv(file) } } @Test fun `Read feedback from valid csv returns same values`(){ val file = getResource("csv/valid.csv") - val res = feedbackHelper.readFeedbackCountFromCsv(file) + val res = feedbackFileHelper.readFeedbackCountFromCsv(file) assertEquals(validCsv, res) } @@ -137,9 +137,9 @@ class FeedbackHelperTest : CommandLineTest() { val file = getResource("csv/valid.csv") val newFile = fileSystem.directory.resolve("newfile.csv") - val expected = feedbackHelper.readFeedbackCountFromCsv(file) - feedbackHelper.writeFeedbackCountToCsv(newFile, expected) - val res = feedbackHelper.readFeedbackCountFromCsv(newFile) + val expected = feedbackFileHelper.readFeedbackCountFromCsv(file) + feedbackFileHelper.writeFeedbackCountToCsv(newFile, expected) + val res = feedbackFileHelper.readFeedbackCountFromCsv(newFile) assertEquals(expected, res) } From ce1ed122b6ae1a247811c1e4f5cadbb11ed943dd Mon Sep 17 00:00:00 2001 From: Marcel Salvenmoser <18032233+malthee@users.noreply.github.com> Date: Mon, 20 Feb 2023 23:18:07 +0100 Subject: [PATCH 3/7] Doc update --- README.md | 6 +++--- .../hagenberg/tutorbot/commands/InstructionsCommand.kt | 6 +++--- .../hagenberg/tutorbot/commands/SaveFeedbackCommand.kt | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index abc8bc5..d31bb24 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,11 @@ https://github.com/jheinzel/tutorbot/releases Tutorbot comes with a range of different features, it can support you by: +* downloading all reviews for a certain exercise * downloading (and extracting) all submissions for a certain exercise * checking submissions for plagiarism -* downloading all reviews for a certain exercise +* choosing reviews to give feedback on based on the amount of feedbacks a student has received * sending feedback emails to students -* choosing reviews to give feedback to -* saving amount of feedbacks each student has received ## Configuration @@ -137,6 +136,7 @@ under `/build/libs/tutorbot.jar`. Please note that a JDK with version 11 or high tool, this limitation comes from JPlag. ## Releasing a new version +First the version in the `build.gradle.kts` has to be updated. Then the following steps have to be performed: Before releasing, check if your changes are merged in the main branch. To release a new version, create and push a `tag` on the main branch with a name like `v*.*`. This can be done with the following commands: diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt index 864fb2e..eb26097 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt @@ -11,13 +11,13 @@ class InstructionsCommand @Inject constructor() : BaseCommand() { override fun execute() { println("To review a homework, the following steps need to be taken:") - println("- Download all the reviews (use the reviews command). May also download submissions in this step if needed.") + println("- Download all the reviews (use the reviews command). May also download submissions to skip the next step.") println("- Download all the submissions and check for plagiarism (use the download submissions command).") println("- Select reviews randomly by using the choose-feedback command or manually making sure everybody gets chosen fairly.") + println("- If you have not done it already in the choose-feedback command, save the feedback count using the save-feedback command. This data will be used for choosing reviews next time.") println("- Add your feedback to the selected reviews.") println("- Collect general feedback and common mistakes for this homework and write it in a markdown file.") - println("- Send emails to students with the reviewed PDFs (use the mail command). May also save the feedback counts in this step.") - println("- (Do this only if it was not already done in the mail command!) Save the feedback amount using the save-feedback command. This data will be used for choosing reviews next time.") + println("- Send emails to students with the reviewed PDFs (use the mail command).") println("- Upload all reviewed PDfs as well as the markdown file to the file share.") println("- Enter which students you reviewed in the excel sheet.") } diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommand.kt index bc44730..9305a70 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SaveFeedbackCommand.kt @@ -12,7 +12,7 @@ import javax.inject.Inject @Command( name = "save-feedback", - description = ["Should only be run if the feedback count was not saved when choosing the feedbacks. Counts the feedbacks in an exercise folder and updates the feedback CSV file."] + description = ["Should only be run if the feedback count was not saved with choose-feedback. Counts the feedbacks in an exercise folder and updates the feedback CSV file."] ) class SaveFeedbackCommand @Inject constructor( private val configHandler: ConfigHandler, From 1156a9e274ece12a4f02b85f36f570fab6b073b4 Mon Sep 17 00:00:00 2001 From: Marcel Salvenmoser <18032233+malthee@users.noreply.github.com> Date: Tue, 21 Feb 2023 00:06:57 +0100 Subject: [PATCH 4/7] ChooseFeedbackCommand now also allows saving Now the workflow is download -> choose-feedback (save) -> mail this allows for better synchronous work with multiple tutors --- .../commands/ChooseFeedbackCommand.kt | 10 ++++-- .../commands/ChooseFeedbackCommandTest.kt | 32 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommand.kt index 6067bd6..36e0ab3 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommand.kt @@ -21,7 +21,8 @@ import javax.inject.Inject class ChooseFeedbackCommand @Inject constructor( private val configHandler: ConfigHandler, private val feedbackFileHelper: FeedbackFileHelper, - private val feedbackChooseLogic: FeedbackChooseLogic + private val feedbackChooseLogic: FeedbackChooseLogic, + private val saveFeedbackCommand: SaveFeedbackCommand ) : BaseCommand() { override fun execute() { // Current ex. reviews path @@ -60,8 +61,13 @@ class ChooseFeedbackCommand @Inject constructor( printlnGreen("Picked $feedbackCount reviews.") moveReviews(sourceDirectory, reviewsToMove) - printlnGreen("Finished selecting reviews to feedback.") + + if (promptBooleanInput("Save current feedback selection?")) { + saveFeedbackCommand.execute() + } else { + printlnCyan("Feedback selection was not saved. Please save your selection with save-feedback when you are done.") + } } private fun moveReviews( diff --git a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommandTest.kt b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommandTest.kt index 2deeab3..b6d0d05 100644 --- a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommandTest.kt +++ b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/ChooseFeedbackCommandTest.kt @@ -27,6 +27,7 @@ class ChooseFeedbackCommandTest : CommandLineTest() { private val exerciseSubDir = "ue01" private val feedbackFileHelper = mockk() + private val saveFeedbackCommand = mockk() private val configHandler = mockk { every { getExerciseSubDir() } returns exerciseSubDir every { getReviewsSubDir() } returns reviewSubDir @@ -36,7 +37,8 @@ class ChooseFeedbackCommandTest : CommandLineTest() { } private val feedbackChooseLogic = FeedbackChooseLogic(random) // The logic is being tested as part of the command - private val chooseFeedbackCmd = ChooseFeedbackCommand(configHandler, feedbackFileHelper, feedbackChooseLogic) + private val chooseFeedbackCmd = + ChooseFeedbackCommand(configHandler, feedbackFileHelper, feedbackChooseLogic, saveFeedbackCommand) @get:Rule val fileSystem = FileSystemRule() @@ -233,4 +235,32 @@ class ChooseFeedbackCommandTest : CommandLineTest() { verifyExpectedFiles(listOf("S3-S4_S3-S4.pdf")) verifyMovedFiles(listOf("S4-S2210101010_S4_TestName.pdf")) } + + @Test + fun `When save feedback chosen, calls SaveFeedbackCommand`() { + // This configuration should lead to a successful execution of ChooseFeedbackCommand to a point where saving is possible + setupTestFiles() + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } returns mapOf("s4" to FeedbackCount(1, 0)) + every { configHandler.getFeedbackAmount() } returns 2 + every { configHandler.getFeedbackRandomAmount() } returns 0 + systemIn.provideLines("Y") + + chooseFeedbackCmd.execute() + + verify(exactly = 1) { saveFeedbackCommand.execute() } + } + + @Test + fun `When save feedback not chosen, does not call SaveFeedbackCommand`() { + // This configuration should lead to a successful execution of ChooseFeedbackCommand to a point where saving is possible + setupTestFiles() + every { feedbackFileHelper.readFeedbackCountFromCsv(any()) } returns mapOf("s4" to FeedbackCount(1, 0)) + every { configHandler.getFeedbackAmount() } returns 2 + every { configHandler.getFeedbackRandomAmount() } returns 0 + systemIn.provideLines("N") + + chooseFeedbackCmd.execute() + + confirmVerified(saveFeedbackCommand) + } } \ No newline at end of file From 18235e087f9639a1ecc18109ffd9cc20cc5b03fa Mon Sep 17 00:00:00 2001 From: Marcel Salvenmoser <18032233+malthee@users.noreply.github.com> Date: Tue, 21 Feb 2023 03:21:11 +0100 Subject: [PATCH 5/7] Moved plagiarism folder up to exercise folder, added log file --- .../tutorbot/commands/PlagiarismCommand.kt | 18 ++++++--- .../tutorbot/commands/SubmissionsCommand.kt | 21 +++++----- .../tutorbot/components/PlagiarismChecker.kt | 29 ++++++++++++-- .../commands/PlagiarismCommandTest.kt | 32 +++++++++++---- .../commands/SubmissionsCommandTest.kt | 5 ++- .../components/PlagiarismCheckerTest.kt | 40 ++++++++++++++----- 6 files changed, 105 insertions(+), 40 deletions(-) diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommand.kt index 3309e62..37526d1 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommand.kt @@ -1,10 +1,11 @@ package at.fhooe.hagenberg.tutorbot.commands +import at.fhooe.hagenberg.tutorbot.components.ConfigHandler import at.fhooe.hagenberg.tutorbot.components.PlagiarismChecker import at.fhooe.hagenberg.tutorbot.util.exitWithError -import at.fhooe.hagenberg.tutorbot.util.promptTextInput import picocli.CommandLine.Command import java.io.File +import java.nio.file.Path import javax.inject.Inject @Command( @@ -12,19 +13,24 @@ import javax.inject.Inject description = ["Checks downloaded submissions for plagiarism"] ) class PlagiarismCommand @Inject constructor( - private val plagiarismChecker: PlagiarismChecker + private val plagiarismChecker: PlagiarismChecker, + private val configHandler: ConfigHandler ) : BaseCommand() { override fun execute() { - val submissionsPath = promptTextInput("Enter submissions location (leave empty for current directory):") - val submissionsDirectory = File(submissionsPath) + val baseDir = configHandler.getBaseDir() + val exerciseSubDir = configHandler.getExerciseSubDir() + val submissionsDir = configHandler.getSubmissionsSubDir() + // Target is exercise dir not submissions dir so it's easier to find the folder + val targetDirectory = File(baseDir, exerciseSubDir) + val submissionsDirectory = Path.of(baseDir, exerciseSubDir, submissionsDir).toFile() // Make sure the submissions directory exists if (!submissionsDirectory.isDirectory) { - exitWithError("Submissions directory $submissionsPath does not point to a valid directory.") + exitWithError("Submissions directory '${submissionsDirectory.absolutePath}' does not point to a valid directory.") } // Check the results for plagiarism - plagiarismChecker.generatePlagiarismReport(submissionsDirectory) + plagiarismChecker.generatePlagiarismReport(submissionsDirectory, targetDirectory) } } \ No newline at end of file diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SubmissionsCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SubmissionsCommand.kt index 0251d48..104d4dc 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SubmissionsCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/SubmissionsCommand.kt @@ -33,25 +33,22 @@ class SubmissionsCommand @Inject constructor( execute(null, null) } - fun execute(assignmentUrl: String?, submissionOptions: Triple?){ + fun execute(assignmentUrl: String?, submissionOptions: Triple?) { authenticator.authenticate() - val targetDirectory = setupTargetDirectory() - + val submissionsDirectory = setupTargetDirectory() val url = assignmentUrl ?: promptTextInput("Enter assignment URL:") - val (deleteArchives, checkPlagiarism, deleteSubmissions) = submissionOptions ?: promptForSubmissionOptions() - var downloadLinks = getAllDownloadLinks(url) while (downloadLinks.isEmpty()) { printlnRed("Could not find any submissions to download") - if(!promptBooleanInput("Try again? (Y/N)")){ + if (!promptBooleanInput("Try again? (Y/N)")) { exitProcess(0) } downloadLinks = getAllDownloadLinks(promptTextInput("Enter assignment URL:")) } - val files = downloadLinks.map { link -> File(targetDirectory, getFileName(link)) } + val files = downloadLinks.map { link -> File(submissionsDirectory, getFileName(link)) } // Download and unzip all submitted solutions val submissions = downloadLinks.zip(files) @@ -67,7 +64,10 @@ class SubmissionsCommand @Inject constructor( // Check the results for plagiarism if wanted if (checkPlagiarism) { - plagiarismChecker.generatePlagiarismReport(targetDirectory) + val baseDir = configHandler.getBaseDir() + val exerciseSubDir = configHandler.getExerciseSubDir() + val targetDirectory = File(baseDir, exerciseSubDir) + plagiarismChecker.generatePlagiarismReport(submissionsDirectory, targetDirectory) } // Delete the downloaded submissions if wanted @@ -83,7 +83,8 @@ class SubmissionsCommand @Inject constructor( } private fun getAllDownloadLinks(assignmentUrl: String): List = try { - val assignmentPage = moodleClient.getHtmlDocument(assignmentUrl) // Query the links to all submission detail pages + val assignmentPage = + moodleClient.getHtmlDocument(assignmentUrl) // Query the links to all submission detail pages val detailUrls = assignmentPage.select(".submission a.title").map { element -> element.href() } // Follow the links to all detail pages and extract the real download URL @@ -102,7 +103,7 @@ class SubmissionsCommand @Inject constructor( } -fun promptForSubmissionOptions(): Triple{ +fun promptForSubmissionOptions(): Triple { val deleteArchives = promptBooleanInput("Do you want to delete the extracted archives?") val checkPlagiarism = promptBooleanInput("Do you want to check submissions for plagiarism?") val deleteSubmissions = promptBooleanInput("Do you want to delete the downloaded submissions again?") diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/PlagiarismChecker.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/PlagiarismChecker.kt index dca5618..615799d 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/PlagiarismChecker.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/components/PlagiarismChecker.kt @@ -12,16 +12,31 @@ class PlagiarismChecker @Inject constructor( private val configHandler: ConfigHandler ) { - fun generatePlagiarismReport(submissionDirectory: File) { + fun generatePlagiarismReport(submissionDirectory: File, outputDirectory: File) { val language = detectSubmissionLanguage(submissionDirectory) - val reportDirectory = File(submissionDirectory, "plagiarism-report") - val args = arrayOf("-l", language, "-r", reportDirectory.absolutePath, "-s", submissionDirectory.absolutePath) + val reportDirectory = File(outputDirectory, REPORT_FOLDER) + reportDirectory.mkdirs() + val logFile = File(reportDirectory, LOG_FILE) + + // With subdirs (-s) verbose parser logging (-vp) and output to log file (-o) + val args = arrayOf( + "-l", + language, + "-r", + reportDirectory.absolutePath, + "-vp", + "-o", + logFile.absolutePath, + "-s", + submissionDirectory.absolutePath + ) // Capture output while running JPlag runWithCapturedOutput { jplagWrapper.run(args) } - val reportFilePath = File(reportDirectory, "index.html").absolutePath + + val reportFilePath = File(reportDirectory, INDEX_FILE).absolutePath println("Plagiarism output generated, you can check it here: $reportFilePath") } @@ -39,4 +54,10 @@ class PlagiarismChecker @Inject constructor( Program(CommandLineOptions(args)).run() } } + + companion object { + const val REPORT_FOLDER = "plagiarism-report" + const val LOG_FILE = "parser.log" + const val INDEX_FILE = "index.html" + } } diff --git a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommandTest.kt b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommandTest.kt index 34a8575..4fcd808 100644 --- a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommandTest.kt +++ b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommandTest.kt @@ -1,38 +1,56 @@ package at.fhooe.hagenberg.tutorbot.commands +import at.fhooe.hagenberg.tutorbot.components.ConfigHandler import at.fhooe.hagenberg.tutorbot.components.PlagiarismChecker import at.fhooe.hagenberg.tutorbot.testutil.CommandLineTest import at.fhooe.hagenberg.tutorbot.testutil.assertThrows import at.fhooe.hagenberg.tutorbot.testutil.rules.FileSystemRule import at.fhooe.hagenberg.tutorbot.util.ProgramExitError +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import org.junit.Before import org.junit.Rule import org.junit.Test import java.io.File +import java.nio.file.Path class PlagiarismCommandTest : CommandLineTest() { + private val submissionsLoc = "submissions" + private val exerciseLoc = "ue01" + private val plagiarismChecker = mockk() - private val plagiarismCommand = PlagiarismCommand(plagiarismChecker) + private val configHandler = mockk() { + every { getExerciseSubDir() } returns exerciseLoc + every { getSubmissionsSubDir() } returns submissionsLoc + } + + private val plagiarismCommand = PlagiarismCommand(plagiarismChecker, configHandler) @get:Rule val fileSystem = FileSystemRule() + private lateinit var submissionsDirFile: File + private lateinit var outputDirFile: File + + @Before + fun setup() { + every { configHandler.getBaseDir() } returns fileSystem.directory.absolutePath.toString() + submissionsDirFile = Path.of(fileSystem.directory.absolutePath, exerciseLoc, submissionsLoc).toFile() + outputDirFile = Path.of(fileSystem.directory.absolutePath, exerciseLoc).toFile() + } + @Test fun `Plagiarism checker is invoked correctly`() { - systemIn.provideLines(fileSystem.directory.absolutePath) + submissionsDirFile.mkdirs() plagiarismCommand.execute() - verify { plagiarismChecker.generatePlagiarismReport(fileSystem.directory) } + verify { plagiarismChecker.generatePlagiarismReport(submissionsDirFile, outputDirFile) } } @Test fun `Program exits with error if the submissions directory is not valid`() { - systemIn.provideLines(fileSystem.file.absolutePath) - assertThrows { plagiarismCommand.execute() } - - systemIn.provideLines(File(fileSystem.file, "nonexistant").absolutePath) assertThrows { plagiarismCommand.execute() } } } diff --git a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/SubmissionsCommandTest.kt b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/SubmissionsCommandTest.kt index 1d32861..a890952 100644 --- a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/SubmissionsCommandTest.kt +++ b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/commands/SubmissionsCommandTest.kt @@ -91,8 +91,9 @@ class SubmissionsCommandTest : CommandLineTest() { } private fun verifySubmissions(archiveExists: Boolean = false, submissionExists: Boolean = false) { - val submissionsLoc = Path.of(fileSystem.directory.absolutePath, exerciseLoc, submissionsLoc).toString() - verify { plagiarismChecker.generatePlagiarismReport(File(submissionsLoc)) } + val submissionsLoc = Path.of(fileSystem.directory.absolutePath, exerciseLoc, submissionsLoc).toFile() + val outputLoc = Path.of(fileSystem.directory.absolutePath, exerciseLoc).toFile() + verify { plagiarismChecker.generatePlagiarismReport(submissionsLoc, outputLoc) } assertEquals(archiveExists, File(submissionsLoc, "submission.zip").exists()) assertEquals(submissionExists, File(submissionsLoc, "submission/submission.pdf").exists()) } diff --git a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/PlagiarismCheckerTest.kt b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/PlagiarismCheckerTest.kt index a73f33a..92a1f2d 100644 --- a/src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/PlagiarismCheckerTest.kt +++ b/src/test/kotlin/at/fhooe/hagenberg/tutorbot/components/PlagiarismCheckerTest.kt @@ -10,9 +10,13 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import java.io.File +import java.nio.file.Path import java.nio.file.Paths class PlagiarismCheckerTest : CommandLineTest() { + private val submissionsLoc = "submissions" + private val exerciseLoc = "ue01" + private val jplagWrapper = mockk() private val configHandler = mockk() private val args = slot>() @@ -22,36 +26,50 @@ class PlagiarismCheckerTest : CommandLineTest() { @get:Rule val fileSystem = FileSystemRule() + private lateinit var submissionsDirFile: File + private lateinit var outputDirFile: File + @Before fun setup() { every { jplagWrapper.run(capture(args)) } just Runs + submissionsDirFile = Path.of(fileSystem.directory.absolutePath, exerciseLoc, submissionsLoc).toFile() + submissionsDirFile.mkdirs() + outputDirFile = Path.of(fileSystem.directory.absolutePath, exerciseLoc).toFile() } @Test fun `JPlag is invoked correctly`() { - File(fileSystem.directory, "test.java").createNewFile() - plagiarismChecker.generatePlagiarismReport(fileSystem.directory) + File(submissionsDirFile, "test.java").createNewFile() + plagiarismChecker.generatePlagiarismReport(submissionsDirFile, outputDirFile) + + val resultsPath = Paths.get(outputDirFile.path, PlagiarismChecker.REPORT_FOLDER).toString() + val reportPath = Paths.get(resultsPath, PlagiarismChecker.INDEX_FILE).toString() + val logFilePath = Path.of(outputDirFile.path, PlagiarismChecker.REPORT_FOLDER, PlagiarismChecker.LOG_FILE) + .toString() - val submissionPath = fileSystem.directory.absolutePath - val resultsPath = Paths.get(submissionPath, "plagiarism-report").toString() - val reportPath = Paths.get(resultsPath, "index.html").toString() - assertTrue(args.captured.joinToString(separator = " ").contains("-r $resultsPath -s $submissionPath")) + val capturedArgs = args.captured.joinToString(separator = " ") + // All arguments present, this test is not that useful + assertTrue( + capturedArgs.contains("-o") && capturedArgs.contains("-s") && + capturedArgs.contains("-vp") && capturedArgs.contains("-r") && capturedArgs.contains("-l") && + capturedArgs.contains(logFilePath) && capturedArgs.contains(resultsPath) + ) assertTrue(systemOut.log.contains(reportPath)) } @Test fun `Java language version from config is used if available`() { every { configHandler.getJavaLanguageLevel() } returns "java11" - File(fileSystem.directory, "test.java").createNewFile() - plagiarismChecker.generatePlagiarismReport(fileSystem.directory) + File(submissionsDirFile, "test.java").createNewFile() + plagiarismChecker.generatePlagiarismReport(submissionsDirFile, outputDirFile) assertTrue(args.captured.joinToString(separator = " ").contains("-l java11")) } @Test fun `C++ is detected correctly`() { - File(fileSystem.directory, "test.cpp").createNewFile() - plagiarismChecker.generatePlagiarismReport(fileSystem.directory) + File(submissionsDirFile, "test.cpp").createNewFile() + plagiarismChecker.generatePlagiarismReport(submissionsDirFile, outputDirFile) assertTrue(args.captured.joinToString(separator = " ").contains("-l c/c++")) } @@ -59,7 +77,7 @@ class PlagiarismCheckerTest : CommandLineTest() { @Test fun `Program exits if language could not be detected`() { assertThrows { - plagiarismChecker.generatePlagiarismReport(fileSystem.directory) + plagiarismChecker.generatePlagiarismReport(submissionsDirFile, outputDirFile) } } } From 3742d7d09117d05642ae266276f7c349b08bf662 Mon Sep 17 00:00:00 2001 From: Marcel Salvenmoser <18032233+malthee@users.noreply.github.com> Date: Tue, 21 Feb 2023 03:39:30 +0100 Subject: [PATCH 6/7] Added plagiarism instruction --- .../at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt index eb26097..47f0fba 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt @@ -13,6 +13,7 @@ class InstructionsCommand @Inject constructor() : BaseCommand() { println("To review a homework, the following steps need to be taken:") println("- Download all the reviews (use the reviews command). May also download submissions to skip the next step.") println("- Download all the submissions and check for plagiarism (use the download submissions command).") + println("- Check the plagiarism report (index.html) and if there are errors parser.log will have more information.") println("- Select reviews randomly by using the choose-feedback command or manually making sure everybody gets chosen fairly.") println("- If you have not done it already in the choose-feedback command, save the feedback count using the save-feedback command. This data will be used for choosing reviews next time.") println("- Add your feedback to the selected reviews.") From 021bdd967d51e08b62b93e9b293cc80b6ce1bf5a Mon Sep 17 00:00:00 2001 From: Marcel Salvenmoser <18032233+malthee@users.noreply.github.com> Date: Tue, 21 Feb 2023 04:31:53 +0100 Subject: [PATCH 7/7] Doc and version upgrade --- build.gradle.kts | 2 +- .../hagenberg/tutorbot/commands/InstructionsCommand.kt | 6 +++--- .../at/fhooe/hagenberg/tutorbot/commands/MailCommand.kt | 2 +- .../fhooe/hagenberg/tutorbot/commands/PlagiarismCommand.kt | 2 +- .../at/fhooe/hagenberg/tutorbot/commands/ReviewsCommand.kt | 2 +- .../at/fhooe/hagenberg/tutorbot/commands/VersionCommand.kt | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b232a6b..ccab909 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "at.fhooe.hagenberg" -version = "1.4.0" +version = "1.4.1" repositories { mavenCentral() diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt index 47f0fba..4ed9189 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/InstructionsCommand.kt @@ -5,7 +5,7 @@ import javax.inject.Inject @Command( name = "instructions", - description = ["Prints out general instructions on which steps need to be done by the tutor"] + description = ["Prints out general instructions on which steps need to be done by the tutor."] ) class InstructionsCommand @Inject constructor() : BaseCommand() { @@ -16,10 +16,10 @@ class InstructionsCommand @Inject constructor() : BaseCommand() { println("- Check the plagiarism report (index.html) and if there are errors parser.log will have more information.") println("- Select reviews randomly by using the choose-feedback command or manually making sure everybody gets chosen fairly.") println("- If you have not done it already in the choose-feedback command, save the feedback count using the save-feedback command. This data will be used for choosing reviews next time.") + println("- Enter which students you are going to review in the excel sheet.") println("- Add your feedback to the selected reviews.") println("- Collect general feedback and common mistakes for this homework and write it in a markdown file.") println("- Send emails to students with the reviewed PDFs (use the mail command).") - println("- Upload all reviewed PDfs as well as the markdown file to the file share.") - println("- Enter which students you reviewed in the excel sheet.") + println("- Upload all reviewed PDFs as well as the markdown file to the file share.") } } diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommand.kt index 2877bfd..d94b2ad 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/MailCommand.kt @@ -12,7 +12,7 @@ import javax.mail.AuthenticationFailedException @Command( name = "mail", - description = ["Sends PDFs containing the feedback via email"] + description = ["Sends PDFs containing the feedback via email."] ) class MailCommand @Inject constructor( private val mailClient: MailClient, diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommand.kt index 37526d1..1a06f17 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/PlagiarismCommand.kt @@ -10,7 +10,7 @@ import javax.inject.Inject @Command( name = "plagiarism", - description = ["Checks downloaded submissions for plagiarism"] + description = ["Checks downloaded submissions for plagiarism."] ) class PlagiarismCommand @Inject constructor( private val plagiarismChecker: PlagiarismChecker, diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ReviewsCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ReviewsCommand.kt index 5edc24b..7d511b4 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ReviewsCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/ReviewsCommand.kt @@ -14,7 +14,7 @@ import javax.inject.Inject @Command( name = "reviews", - description = ["Downloads all reviews for a certain exercise optionally also downloading the submissions."] + description = ["Downloads all reviews for a certain exercise. Optionally also downloads the submissions and performs a plagiarism check."] ) class ReviewsCommand @Inject constructor( private val moodleClient: MoodleClient, diff --git a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/VersionCommand.kt b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/VersionCommand.kt index 0e2f5a9..5e81f06 100644 --- a/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/VersionCommand.kt +++ b/src/main/kotlin/at/fhooe/hagenberg/tutorbot/commands/VersionCommand.kt @@ -5,7 +5,7 @@ import javax.inject.Inject @Command( name = "version", - description = ["Shows tutorbot version information"] + description = ["Shows tutorbot version information."] ) class VersionCommand @Inject constructor() : BaseCommand() {