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()