diff --git a/core/src/main/kotlin/org/sourcegrade/jagr/core/CommonModule.kt b/core/src/main/kotlin/org/sourcegrade/jagr/core/CommonModule.kt index cab9b24e..88a35ea5 100644 --- a/core/src/main/kotlin/org/sourcegrade/jagr/core/CommonModule.kt +++ b/core/src/main/kotlin/org/sourcegrade/jagr/core/CommonModule.kt @@ -37,7 +37,6 @@ import org.sourcegrade.jagr.core.export.rubric.GermanCSVExporter import org.sourcegrade.jagr.core.export.rubric.MoodleJSONExporter import org.sourcegrade.jagr.core.export.submission.EclipseSubmissionExporter import org.sourcegrade.jagr.core.export.submission.GradleSubmissionExporter -import org.sourcegrade.jagr.core.extra.ExtrasManagerImpl import org.sourcegrade.jagr.core.io.SerializationFactoryLocatorImpl import org.sourcegrade.jagr.core.rubric.CriterionFactoryImpl import org.sourcegrade.jagr.core.rubric.CriterionHolderPointCalculatorFactoryImpl @@ -56,7 +55,6 @@ import org.sourcegrade.jagr.launcher.env.ModuleFactory import org.sourcegrade.jagr.launcher.executor.GradingQueue import org.sourcegrade.jagr.launcher.executor.RuntimeGrader import org.sourcegrade.jagr.launcher.executor.RuntimeInvoker -import org.sourcegrade.jagr.launcher.io.ExtrasManager import org.sourcegrade.jagr.launcher.io.GradedRubricExporter import org.sourcegrade.jagr.launcher.io.SerializerFactory import org.sourcegrade.jagr.launcher.io.SubmissionExporter @@ -74,7 +72,6 @@ class CommonModule(private val configuration: LaunchConfiguration) : AbstractMod bind(ClassTransformer.Factory::class.java).to(ClassTransformerFactoryImpl::class.java) bind(Criterion.Factory::class.java).to(CriterionFactoryImpl::class.java) bind(CriterionHolderPointCalculator.Factory::class.java).to(CriterionHolderPointCalculatorFactoryImpl::class.java) - bind(ExtrasManager::class.java).to(ExtrasManagerImpl::class.java) bind(GradedRubricExporter.CSV::class.java).to(GermanCSVExporter::class.java) bind(GradedRubricExporter.HTML::class.java).to(BasicHTMLExporter::class.java) bind(GradedRubricExporter.Moodle::class.java).to(MoodleJSONExporter::class.java) diff --git a/core/src/main/kotlin/org/sourcegrade/jagr/core/extra/ExtrasManagerImpl.kt b/core/src/main/kotlin/org/sourcegrade/jagr/core/extra/ExtrasManagerImpl.kt deleted file mode 100644 index 484a4457..00000000 --- a/core/src/main/kotlin/org/sourcegrade/jagr/core/extra/ExtrasManagerImpl.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Jagr - SourceGrade.org - * Copyright (C) 2021-2022 Alexander Staeding - * Copyright (C) 2021-2022 Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.sourcegrade.jagr.core.extra - -import com.google.inject.Inject -import org.apache.logging.log4j.Logger -import org.sourcegrade.jagr.launcher.env.Config -import org.sourcegrade.jagr.launcher.io.ExtrasManager - -class ExtrasManagerImpl @Inject constructor( - private val config: Config, - private val logger: Logger, - private val moodleUnpack: MoodleUnpack, -) : ExtrasManager { - - private fun tryRunExtra(condition: Boolean, extra: Extra) { - if (condition) { - logger.info("Running extra ${extra.name}") - extra.run() - } - } - - override fun runExtras() { - tryRunExtra(config.extras.moodleUnpack.enabled, moodleUnpack) - } -} diff --git a/docs/usage/command-line/index.md b/docs/usage/command-line/index.md new file mode 100644 index 00000000..9624939b --- /dev/null +++ b/docs/usage/command-line/index.md @@ -0,0 +1,36 @@ +## Basic Command-Line Usage + +1. Create a [grader](architecture/grader) +2. Create a [submission](architecture/submission) +3. Download the [latest release](https://github.com/sourcegrade/jagr/releases) + + !!! tip + + The [jagr-bin](https://aur.archlinux.org/packages/jagr-bin) package is available on the AUR for Arch Linux users. + +4. Create an empty working directory and copy the Jagr jar into it +5. Run `java -jar Jagr-x.jar`, which should create the following folder structure: + + ```text + ./graders -- input folder for grader jars (tests and rubric providers) + ./libs -- for libraries that are required on each submission's classpath + ./logs -- saved log files + ./rubrics -- the output folder for graded rubrics + ./submissions -- input folder for submissions + ./submissions-export -- output folder for submissions + ``` + +6. Prepare the grader and submission for grading + 1. Prepare the grader jar by running the `graderBuildGrader` Gradle task in the grader project + 2. Prepare the submission jar by running the `mainBuildSubmission` Gradle task in the submission project + 3. Locate the respective jars in the `build/libs` folder of the grader and submission projects + +7. Copy the grader jar into the `graders` folder and the submission jar into the `submissions` folder. + If the grader requires any runtime dependencies (that are not already included in Jagr), copy them into the `libs` folder + + !!! tip + + The `graderBuildLibs` gradle task provided by the jagr-gradle plugin can be used to generate a fat jar containing all runtime dependencies. + This task automatically excludes dependencies already present in the Jagr runtime. + +8. Run `java -jar Jagr-x-x-x.jar` again to grade the submission diff --git a/docs/usage/command-line/moodle-unpack.md b/docs/usage/command-line/moodle-unpack.md new file mode 100644 index 00000000..cfe86b56 --- /dev/null +++ b/docs/usage/command-line/moodle-unpack.md @@ -0,0 +1 @@ +# Moodle Unpack diff --git a/docs/usage/command-line/options.md b/docs/usage/command-line/options.md index 105bbad7..ea128281 100644 --- a/docs/usage/command-line/options.md +++ b/docs/usage/command-line/options.md @@ -14,6 +14,14 @@ Progress bar style. Choices: "rainbow", "xmas" +### --create-moodle-unpack-config + +Creates default moodle unpack config at the requested path and exits. + +### --moodle-unpack, -m + +Runs a moodle unpack with the given configuration. + ### --child (internal) Waits to receive grading job details via IPC diff --git a/docs/usage/extras/moodle-unpack.md b/docs/usage/extras/moodle-unpack.md new file mode 100644 index 00000000..49e3f79d --- /dev/null +++ b/docs/usage/extras/moodle-unpack.md @@ -0,0 +1,39 @@ +# Moodle Unpack + + +## JSON Format + +| Name | JSON Key | Default Value | +|---------------------------------------------------------|---------------------------|-------------------------------------------------------------------------------------------------------| +| [Moodle Zip Regex](#moodle-zip-regex) | `moodleZipRegex` | `.*[.]zip` | +| [Assignment Id Regex](#assignment-id-regex) | `assignmentIdRegex` | `.*Abgabe[^0-9]*(?[0-9]{1,2}).*[.]zip` | +| [Assignment Id Transformer](#assignment-id-transformer) | `assignmentIdTransformer` | `h%id%` | +| [Student Id Regex](#student-id-regex) | `studentIdRegex` | .* - (?([a-z]{2}[0-9]{2}[a-z]{4})|([a-z]+_[a-z]+))/submissions/.*[.]jar` | + +### Moodle Zip Regex + +`moodleZipRegex` + +Matches "moodle zip" file names that should be unpacked using this config. + +### Assignment Id Regex + + +The "moodle zip" has a specific path format from which it is usually possible to extract an assignment id. +This is useful for bulk grading where individual submissions may not have correct information. +The format of this regex depends on the name of the submission module in moodle. + +### Assignment Id Transformer + + + +The assignment ids extracted from the "moodle zip" are numeric only. +Use this option to transform each numeric assignment id to match the intended full assignment id. +By default, this is "h%id" which prefixes the id with 'h'. + +### Student Id Regex + +`studentIdRegex` + +The "moodle zip" contains each submission at a path that includes the student id. +This regex parses and extracts the id. diff --git a/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/Config.kt b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/Config.kt index 9613cb11..d81764fe 100644 --- a/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/Config.kt +++ b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/Config.kt @@ -1,7 +1,7 @@ /* * Jagr - SourceGrade.org - * Copyright (C) 2021-2022 Alexander Staeding - * Copyright (C) 2021-2022 Contributors + * Copyright (C) 2021-2025 Alexander Städing + * Copyright (C) 2021-2025 Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -27,7 +27,6 @@ data class Config( @field:Comment("The locations of the following directories may be configured here") val dir: Dir = Dir(), val executor: Executor = Executor(), - val extras: Extras = Extras(), val transformers: Transformers = Transformers(), ) @@ -106,22 +105,6 @@ invocations of checkTimeout() will result in an AssertionFailedError val timeoutTotal: Long = 150_000L, ) -@ConfigSerializable -data class Extras( - val moodleUnpack: MoodleUnpack = MoodleUnpack(), -) { - @ConfigSerializable - data class MoodleUnpack( - override val enabled: Boolean = true, - val assignmentIdRegex: String = ".*Abgabe[^0-9]*(?[0-9]{1,2}).*[.]zip", - val studentRegex: String = ".* - (?([a-z]{2}[0-9]{2}[a-z]{4})|([a-z]+_[a-z]+))/submissions/.*[.]jar", - ) : Extra - - interface Extra { - val enabled: Boolean - } -} - @ConfigSerializable data class Transformers( val timeout: TimeoutTransformer = TimeoutTransformer(), diff --git a/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/Jagr.kt b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/Jagr.kt index b1fe3086..af77232a 100644 --- a/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/Jagr.kt +++ b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/Jagr.kt @@ -24,7 +24,7 @@ import org.apache.logging.log4j.Logger import org.sourcegrade.jagr.launcher.executor.GradingQueue import org.sourcegrade.jagr.launcher.executor.RuntimeGrader import org.sourcegrade.jagr.launcher.executor.RuntimeInvoker -import org.sourcegrade.jagr.launcher.io.ExtrasManager +import org.sourcegrade.jagr.launcher.extra.ExtrasManager import org.sourcegrade.jagr.launcher.io.SerializerFactory import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KClass diff --git a/core/src/main/kotlin/org/sourcegrade/jagr/core/extra/Extra.kt b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/MoodleUnpackConfig.kt similarity index 61% rename from core/src/main/kotlin/org/sourcegrade/jagr/core/extra/Extra.kt rename to launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/MoodleUnpackConfig.kt index 1826dc84..0acc6df8 100644 --- a/core/src/main/kotlin/org/sourcegrade/jagr/core/extra/Extra.kt +++ b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/env/MoodleUnpackConfig.kt @@ -1,7 +1,7 @@ /* * Jagr - SourceGrade.org - * Copyright (C) 2021-2022 Alexander Staeding - * Copyright (C) 2021-2022 Contributors + * Copyright (C) 2021-2024 Alexander Städing + * Copyright (C) 2021-2024 Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,9 +17,11 @@ * along with this program. If not, see . */ -package org.sourcegrade.jagr.core.extra +package org.sourcegrade.jagr.launcher.env -interface Extra { - val name: String - fun run() -} +data class MoodleUnpackConfig( + val moodleZipRegex: String = ".*[.]zip", + val assignmentIdRegex: String = ".*Abgabe[^0-9]*(?[0-9]{1,2}).*[.]zip", + val assignmentIdTransformer: String = "h%id", + val studentIdRegex: String = ".* - (?([a-z]{2}[0-9]{2}[a-z]{4})|([a-z]+_[a-z]+))/submissions/.*[.]jar", +) diff --git a/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/io/ExtrasManager.kt b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/extra/ExtrasManager.kt similarity index 74% rename from launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/io/ExtrasManager.kt rename to launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/extra/ExtrasManager.kt index a5edecd6..f5832d18 100644 --- a/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/io/ExtrasManager.kt +++ b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/extra/ExtrasManager.kt @@ -1,7 +1,7 @@ /* * Jagr - SourceGrade.org - * Copyright (C) 2021-2022 Alexander Staeding - * Copyright (C) 2021-2022 Contributors + * Copyright (C) 2021-2025 Alexander Städing + * Copyright (C) 2021-2025 Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,8 +17,10 @@ * along with this program. If not, see . */ -package org.sourcegrade.jagr.launcher.io +package org.sourcegrade.jagr.launcher.extra -interface ExtrasManager { - fun runExtras() -} +import com.google.inject.Inject + +class ExtrasManager @Inject constructor( + val moodleUnpack: MoodleUnpack.Factory +) diff --git a/core/src/main/kotlin/org/sourcegrade/jagr/core/extra/MoodleUnpack.kt b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/extra/MoodleUnpack.kt similarity index 68% rename from core/src/main/kotlin/org/sourcegrade/jagr/core/extra/MoodleUnpack.kt rename to launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/extra/MoodleUnpack.kt index 5dccc680..3e93ad80 100644 --- a/core/src/main/kotlin/org/sourcegrade/jagr/core/extra/MoodleUnpack.kt +++ b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/extra/MoodleUnpack.kt @@ -1,7 +1,7 @@ /* * Jagr - SourceGrade.org - * Copyright (C) 2021-2022 Alexander Staeding - * Copyright (C) 2021-2022 Contributors + * Copyright (C) 2021-2025 Alexander Städing + * Copyright (C) 2021-2025 Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,40 +17,43 @@ * along with this program. If not, see . */ -package org.sourcegrade.jagr.core.extra +package org.sourcegrade.jagr.launcher.extra import com.google.inject.Inject import org.apache.logging.log4j.Logger import org.sourcegrade.jagr.launcher.env.Config +import org.sourcegrade.jagr.launcher.env.MoodleUnpackConfig import java.io.File import java.util.zip.ZipEntry import java.util.zip.ZipFile -class MoodleUnpack @Inject constructor( +class MoodleUnpack private constructor( override val config: Config, override val logger: Logger, + private val moodleUnpackConfig: MoodleUnpackConfig, ) : Unpack() { - private val assignmentIdRegex = config.extras.moodleUnpack.assignmentIdRegex.toRegex() - override val name: String = "moodle-unpack" override fun run() { val submissions = File(config.dir.submissions) - val studentRegex = Regex(config.extras.moodleUnpack.studentRegex) + val moodleZipRegex = Regex(moodleUnpackConfig.moodleZipRegex) + val assignmentIdRegex = Regex(moodleUnpackConfig.assignmentIdRegex) + val studentRegex = Regex(moodleUnpackConfig.studentRegex) + val idRegex = Regex("%id%") + val assignmentIdTransformer = { id: String -> moodleUnpackConfig.assignmentIdTransformer.replace(idRegex, id) } val unpackedFiles: MutableList = mutableListOf() - for (candidate in submissions.listFiles { _, t -> t.endsWith(".zip") }!!) { - logger.info("extra($name) :: Discovered candidate zip $candidate") + for (candidate in submissions.listFiles { _, name -> name.matches(moodleZipRegex) }!!) { + logger.info("moodle-unpack :: Discovered candidate zip $candidate") val zipFile = ZipFile(candidate) - // TODO: Fix this hack val assignmentId = assignmentIdRegex.matchEntire(candidate.name) ?.run { groups["assignmentId"]?.value } ?.padStart(length = 2, padChar = '0') - ?.let { "h$it" } + ?.let(assignmentIdTransformer) ?: "none" for (entry in zipFile.entries()) { val matcher = studentRegex.matchEntire(entry.name) ?: continue try { unpackedFiles += zipFile.unpackEntry(entry, submissions, assignmentId, matcher) } catch (e: Throwable) { - logger.info("extra($name) :: Unable to unpack entry ${entry.name} in candidate $candidate", e) + logger.info("moodle-unpack :: Unable to unpack entry ${entry.name} in candidate $candidate", e) } } } @@ -78,4 +81,11 @@ class MoodleUnpack @Inject constructor( studentId = studentId, ) } + + class Factory @Inject constructor( + val config: Config, + val logger: Logger, + ) { + fun create(moodleUnpackConfig: MoodleUnpackConfig): MoodleUnpack = MoodleUnpack(config, logger, moodleUnpackConfig) + } } diff --git a/core/src/main/kotlin/org/sourcegrade/jagr/core/extra/Unpack.kt b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/extra/Unpack.kt similarity index 95% rename from core/src/main/kotlin/org/sourcegrade/jagr/core/extra/Unpack.kt rename to launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/extra/Unpack.kt index 7dfa7319..a2c45cf0 100644 --- a/core/src/main/kotlin/org/sourcegrade/jagr/core/extra/Unpack.kt +++ b/launcher/src/main/kotlin/org/sourcegrade/jagr/launcher/extra/Unpack.kt @@ -1,7 +1,7 @@ /* * Jagr - SourceGrade.org - * Copyright (C) 2021-2022 Alexander Staeding - * Copyright (C) 2021-2022 Contributors + * Copyright (C) 2021-2025 Alexander Städing + * Copyright (C) 2021-2025 Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,14 +17,12 @@ * along with this program. If not, see . */ -package org.sourcegrade.jagr.core.extra +package org.sourcegrade.jagr.launcher.extra import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import kotlinx.serialization.SerializationException -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.apache.logging.log4j.Logger import org.sourcegrade.jagr.launcher.env.Config @@ -35,7 +33,9 @@ import java.nio.file.FileSystems import kotlin.io.path.bufferedReader import kotlin.io.path.bufferedWriter -abstract class Unpack : Extra { +abstract class Unpack { + + abstract fun run() protected abstract val config: Config protected abstract val logger: Logger diff --git a/mkdocs.yml b/mkdocs.yml index a6173884..f7434219 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,6 +50,8 @@ nav: - Command Line: - Basics: usage/command-line/basics.md - Options: usage/command-line/options.md + - Extras: + - Moodle Unpack: usage/extras/moodle-unpack.md - Development: - Getting Started: - Gradle Setup: development/getting-started/gradle-setup.md diff --git a/src/main/kotlin/org/sourcegrade/jagr/Main.kt b/src/main/kotlin/org/sourcegrade/jagr/Main.kt index 9040f4a9..a06a9d92 100644 --- a/src/main/kotlin/org/sourcegrade/jagr/Main.kt +++ b/src/main/kotlin/org/sourcegrade/jagr/Main.kt @@ -24,7 +24,9 @@ import com.github.ajalt.clikt.core.main import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.prompt import com.github.ajalt.clikt.parameters.types.choice +import com.github.ajalt.clikt.parameters.types.path import org.sourcegrade.jagr.launcher.env.Environment import org.sourcegrade.jagr.launcher.env.Jagr import org.sourcegrade.jagr.launcher.env.logger @@ -44,17 +46,31 @@ class MainCommand : CliktCommand() { /** * Command line option to indicate that this process will listen to (via std in) to a grading request */ - private val child by option("--child", "-c").flag() + private val child by option("--child", "-c") + .flag() .help("Waits to receive grading job details via IPC") - private val noExport by option("--no-export", "-n").flag() + private val noExport by option("--no-export", "-n") + .flag() .help("Do not export submissions") - private val exportOnly by option("--export-only", "-e").flag() + private val exportOnly by option("--export-only", "-e") + .flag() .help("Do not grade, only export submissions") - private val progress by option("--progress").choice("rainbow", "xmas") + private val progress by option("--progress") + .choice("rainbow", "xmas") .help("Progress bar style") + private val createMoodleUnpackConfig: String? by option("--create-moodle-unpack-config") + .prompt( + text = "Configuration output path", + default = "moodle-unpack.conf", + ) + .help("Creates default moodle unpack config at the requested path and exits") + private val moodleUnpack by option("--moodle-unpack").path(mustExist = true, canBeFile = true, canBeDir = false) + .help("Runs a moodle unpack with the given configuration") override fun run() { - if (child) { + if (createMoodleUnpackConfig != null) { + + } else if (child) { println(ProcessWorker.MARK_CHILD_BOOT) Environment.initializeChildProcess() ChildProcGrading().grade()